github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/crypto/keys/client/sign_test.go (about) 1 package client 2 3 import ( 4 "context" 5 "flag" 6 "fmt" 7 "os" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/gnolang/gno/tm2/pkg/amino" 13 "github.com/gnolang/gno/tm2/pkg/commands" 14 "github.com/gnolang/gno/tm2/pkg/crypto/bip39" 15 "github.com/gnolang/gno/tm2/pkg/crypto/keys" 16 "github.com/gnolang/gno/tm2/pkg/crypto/keys/keyerror" 17 "github.com/gnolang/gno/tm2/pkg/sdk/bank" 18 "github.com/gnolang/gno/tm2/pkg/std" 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 ) 22 23 // generateTestMnemonic generates a random mnemonic 24 func generateTestMnemonic(t *testing.T) string { 25 t.Helper() 26 27 entropy, entropyErr := bip39.NewEntropy(256) 28 require.NoError(t, entropyErr) 29 30 mnemonic, mnemonicErr := bip39.NewMnemonic(entropy) 31 require.NoError(t, mnemonicErr) 32 33 return mnemonic 34 } 35 36 func TestSign_SignTx(t *testing.T) { 37 t.Parallel() 38 39 t.Run("no key provided", func(t *testing.T) { 40 t.Parallel() 41 42 var ( 43 kbHome = t.TempDir() 44 baseOptions = BaseOptions{ 45 InsecurePasswordStdin: true, 46 Home: kbHome, 47 } 48 ) 49 50 ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) 51 defer cancelFn() 52 53 // Create the command 54 cmd := NewRootCmdWithBaseConfig(commands.NewTestIO(), baseOptions) 55 56 args := []string{ 57 "sign", 58 "--insecure-password-stdin", 59 "--home", 60 kbHome, 61 } 62 63 assert.ErrorIs(t, cmd.ParseAndRun(ctx, args), flag.ErrHelp) 64 }) 65 66 t.Run("non-existing key", func(t *testing.T) { 67 t.Parallel() 68 69 var ( 70 kbHome = t.TempDir() 71 baseOptions = BaseOptions{ 72 InsecurePasswordStdin: true, 73 Home: kbHome, 74 } 75 ) 76 77 ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) 78 defer cancelFn() 79 80 // Create the command 81 cmd := NewRootCmdWithBaseConfig(commands.NewTestIO(), baseOptions) 82 83 args := []string{ 84 "sign", 85 "--insecure-password-stdin", 86 "--home", 87 kbHome, 88 "TotallyExistingKey", 89 } 90 91 assert.True(t, keyerror.IsErrKeyNotFound(cmd.ParseAndRun(ctx, args))) 92 }) 93 94 t.Run("non-existing tx file", func(t *testing.T) { 95 t.Parallel() 96 97 var ( 98 kbHome = t.TempDir() 99 baseOptions = BaseOptions{ 100 InsecurePasswordStdin: true, 101 Home: kbHome, 102 } 103 104 mnemonic = generateTestMnemonic(t) 105 keyName = "generated-key" 106 encryptPassword = "encrypt" 107 ) 108 109 // Generate a key in the keybase 110 kb, err := keys.NewKeyBaseFromDir(kbHome) 111 require.NoError(t, err) 112 113 _, err = kb.CreateAccount(keyName, mnemonic, "", encryptPassword, 0, 0) 114 require.NoError(t, err) 115 116 ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) 117 defer cancelFn() 118 119 // Create the command 120 cmd := NewRootCmdWithBaseConfig(commands.NewTestIO(), baseOptions) 121 122 args := []string{ 123 "sign", 124 "--insecure-password-stdin", 125 "--home", 126 kbHome, 127 "--tx-path", 128 "./TotallyExistingTxFile.json", 129 keyName, 130 } 131 132 assert.ErrorContains(t, cmd.ParseAndRun(ctx, args), "unable to read transaction file") 133 }) 134 135 t.Run("empty tx file", func(t *testing.T) { 136 t.Parallel() 137 138 var ( 139 kbHome = t.TempDir() 140 baseOptions = BaseOptions{ 141 InsecurePasswordStdin: true, 142 Home: kbHome, 143 } 144 145 mnemonic = generateTestMnemonic(t) 146 keyName = "generated-key" 147 encryptPassword = "encrypt" 148 ) 149 150 // Generate a key in the keybase 151 kb, err := keys.NewKeyBaseFromDir(kbHome) 152 require.NoError(t, err) 153 154 _, err = kb.CreateAccount(keyName, mnemonic, "", encryptPassword, 0, 0) 155 require.NoError(t, err) 156 157 // Create an empty tx file 158 txFile, err := os.CreateTemp("", "") 159 require.NoError(t, err) 160 161 ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) 162 defer cancelFn() 163 164 // Create the command 165 cmd := NewRootCmdWithBaseConfig(commands.NewTestIO(), baseOptions) 166 167 args := []string{ 168 "sign", 169 "--insecure-password-stdin", 170 "--home", 171 kbHome, 172 "--tx-path", 173 txFile.Name(), 174 keyName, 175 } 176 177 assert.ErrorIs(t, cmd.ParseAndRun(ctx, args), errInvalidTxFile) 178 }) 179 180 t.Run("corrupted tx amino JSON", func(t *testing.T) { 181 t.Parallel() 182 183 var ( 184 kbHome = t.TempDir() 185 baseOptions = BaseOptions{ 186 InsecurePasswordStdin: true, 187 Home: kbHome, 188 } 189 190 mnemonic = generateTestMnemonic(t) 191 keyName = "generated-key" 192 encryptPassword = "encrypt" 193 ) 194 195 // Generate a key in the keybase 196 kb, err := keys.NewKeyBaseFromDir(kbHome) 197 require.NoError(t, err) 198 199 _, err = kb.CreateAccount(keyName, mnemonic, "", encryptPassword, 0, 0) 200 require.NoError(t, err) 201 202 // Create an empty tx file 203 txFile, err := os.CreateTemp("", "") 204 require.NoError(t, err) 205 206 // Write invalid JSON 207 _, err = txFile.WriteString("{this is absolutely valid JSON]") 208 require.NoError(t, err) 209 210 ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) 211 defer cancelFn() 212 213 // Create the command 214 cmd := NewRootCmdWithBaseConfig(commands.NewTestIO(), baseOptions) 215 216 args := []string{ 217 "sign", 218 "--insecure-password-stdin", 219 "--home", 220 kbHome, 221 "--tx-path", 222 txFile.Name(), 223 keyName, 224 } 225 226 assert.ErrorContains( 227 t, 228 cmd.ParseAndRun(ctx, args), 229 "unable to unmarshal transaction", 230 ) 231 }) 232 233 t.Run("invalid tx params", func(t *testing.T) { 234 t.Parallel() 235 236 var ( 237 kbHome = t.TempDir() 238 baseOptions = BaseOptions{ 239 InsecurePasswordStdin: true, 240 Home: kbHome, 241 Quiet: true, 242 } 243 244 mnemonic = generateTestMnemonic(t) 245 keyName = "generated-key" 246 encryptPassword = "encrypt" 247 248 tx = std.Tx{ 249 Fee: std.Fee{ 250 GasFee: std.Coin{ // invalid gas fee 251 Amount: 0, 252 Denom: "ugnot", 253 }, 254 }, 255 } 256 ) 257 258 // Generate a key in the keybase 259 kb, err := keys.NewKeyBaseFromDir(kbHome) 260 require.NoError(t, err) 261 262 _, err = kb.CreateAccount(keyName, mnemonic, "", encryptPassword, 0, 0) 263 require.NoError(t, err) 264 265 // Create an empty tx file 266 txFile, err := os.CreateTemp("", "") 267 require.NoError(t, err) 268 269 // Marshal the tx and write it to the file 270 encodedTx, err := amino.MarshalJSON(tx) 271 require.NoError(t, err) 272 273 _, err = txFile.Write(encodedTx) 274 require.NoError(t, err) 275 276 ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) 277 defer cancelFn() 278 279 // Create the command IO 280 io := commands.NewTestIO() 281 io.SetIn( 282 strings.NewReader( 283 fmt.Sprintf( 284 "%s\n%s\n", 285 encryptPassword, 286 encryptPassword, 287 ), 288 ), 289 ) 290 291 // Create the command 292 cmd := NewRootCmdWithBaseConfig(io, baseOptions) 293 294 args := []string{ 295 "sign", 296 "--insecure-password-stdin", 297 "--home", 298 kbHome, 299 "--tx-path", 300 txFile.Name(), 301 keyName, 302 } 303 304 assert.ErrorContains( 305 t, 306 cmd.ParseAndRun(ctx, args), 307 "unable to validate transaction", 308 ) 309 }) 310 311 t.Run("empty signature list", func(t *testing.T) { 312 t.Parallel() 313 314 var ( 315 kbHome = t.TempDir() 316 baseOptions = BaseOptions{ 317 InsecurePasswordStdin: true, 318 Home: kbHome, 319 Quiet: true, 320 } 321 322 mnemonic = generateTestMnemonic(t) 323 keyName = "generated-key" 324 encryptPassword = "encrypt" 325 326 tx = std.Tx{ 327 Fee: std.Fee{ 328 GasWanted: 10, 329 GasFee: std.Coin{ 330 Amount: 10, 331 Denom: "ugnot", 332 }, 333 }, 334 Signatures: nil, // no signatures 335 } 336 ) 337 338 // Generate a key in the keybase 339 kb, err := keys.NewKeyBaseFromDir(kbHome) 340 require.NoError(t, err) 341 342 info, err := kb.CreateAccount(keyName, mnemonic, "", encryptPassword, 0, 0) 343 require.NoError(t, err) 344 345 // We need to prepare the message signer as well 346 // for validation to complete 347 tx.Msgs = []std.Msg{ 348 bank.MsgSend{ 349 FromAddress: info.GetAddress(), 350 }, 351 } 352 353 // Create an empty tx file 354 txFile, err := os.CreateTemp("", "") 355 require.NoError(t, err) 356 357 // Marshal the tx and write it to the file 358 encodedTx, err := amino.MarshalJSON(tx) 359 require.NoError(t, err) 360 361 _, err = txFile.Write(encodedTx) 362 require.NoError(t, err) 363 364 ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) 365 defer cancelFn() 366 367 // Create the command IO 368 io := commands.NewTestIO() 369 io.SetIn( 370 strings.NewReader( 371 fmt.Sprintf( 372 "%s\n%s\n", 373 encryptPassword, 374 encryptPassword, 375 ), 376 ), 377 ) 378 379 // Create the command 380 cmd := NewRootCmdWithBaseConfig(io, baseOptions) 381 382 args := []string{ 383 "sign", 384 "--insecure-password-stdin", 385 "--home", 386 kbHome, 387 "--tx-path", 388 txFile.Name(), 389 keyName, 390 } 391 392 // Run the command 393 require.NoError(t, cmd.ParseAndRun(ctx, args)) 394 395 // Make sure the tx file was updated with the signature 396 savedTxRaw, err := os.ReadFile(txFile.Name()) 397 require.NoError(t, err) 398 399 var savedTx std.Tx 400 require.NoError(t, amino.UnmarshalJSON(savedTxRaw, &savedTx)) 401 402 require.Len(t, savedTx.Signatures, 1) 403 assert.True(t, savedTx.Signatures[0].PubKey.Equals(info.GetPubKey())) 404 }) 405 406 t.Run("existing signature list", func(t *testing.T) { 407 t.Parallel() 408 409 var ( 410 kbHome = t.TempDir() 411 baseOptions = BaseOptions{ 412 InsecurePasswordStdin: true, 413 Home: kbHome, 414 Quiet: true, 415 } 416 417 mnemonic = generateTestMnemonic(t) 418 keyName = "generated-key" 419 encryptPassword = "encrypt" 420 421 anotherKey = "another-key" 422 423 tx = std.Tx{ 424 Fee: std.Fee{ 425 GasWanted: 10, 426 GasFee: std.Coin{ 427 Amount: 10, 428 Denom: "ugnot", 429 }, 430 }, 431 } 432 ) 433 434 // Generate a key in the keybase 435 kb, err := keys.NewKeyBaseFromDir(kbHome) 436 require.NoError(t, err) 437 438 // Create an initial account 439 info, err := kb.CreateAccount(keyName, mnemonic, "", encryptPassword, 0, 0) 440 require.NoError(t, err) 441 442 // Create a new account 443 anotherKeyInfo, err := kb.CreateAccount(anotherKey, mnemonic, "", encryptPassword, 0, 1) 444 require.NoError(t, err) 445 446 // Generate the signature 447 signBytes, err := tx.GetSignBytes("id", 1, 0) 448 require.NoError(t, err) 449 450 signature, pubKey, err := kb.Sign(anotherKey, encryptPassword, signBytes) 451 require.NoError(t, err) 452 453 tx.Signatures = []std.Signature{ 454 { 455 PubKey: pubKey, 456 Signature: signature, 457 }, 458 } 459 460 // We need to prepare the message signers as well 461 // for validation to complete 462 tx.Msgs = []std.Msg{ 463 bank.MsgSend{ 464 FromAddress: info.GetAddress(), 465 }, 466 bank.MsgSend{ 467 FromAddress: anotherKeyInfo.GetAddress(), 468 }, 469 } 470 471 // Create an empty tx file 472 txFile, err := os.CreateTemp("", "") 473 require.NoError(t, err) 474 475 // Marshal the tx and write it to the file 476 encodedTx, err := amino.MarshalJSON(tx) 477 require.NoError(t, err) 478 479 _, err = txFile.Write(encodedTx) 480 require.NoError(t, err) 481 482 ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) 483 defer cancelFn() 484 485 // Create the command IO 486 io := commands.NewTestIO() 487 io.SetIn( 488 strings.NewReader( 489 fmt.Sprintf( 490 "%s\n%s\n", 491 encryptPassword, 492 encryptPassword, 493 ), 494 ), 495 ) 496 497 // Create the command 498 cmd := NewRootCmdWithBaseConfig(io, baseOptions) 499 500 args := []string{ 501 "sign", 502 "--insecure-password-stdin", 503 "--home", 504 kbHome, 505 "--tx-path", 506 txFile.Name(), 507 keyName, 508 } 509 510 // Run the command 511 require.NoError(t, cmd.ParseAndRun(ctx, args)) 512 513 // Make sure the tx file was updated with the signature 514 savedTxRaw, err := os.ReadFile(txFile.Name()) 515 require.NoError(t, err) 516 517 var savedTx std.Tx 518 require.NoError(t, amino.UnmarshalJSON(savedTxRaw, &savedTx)) 519 520 require.Len(t, savedTx.Signatures, 2) 521 assert.True(t, savedTx.Signatures[0].PubKey.Equals(anotherKeyInfo.GetPubKey())) 522 assert.True(t, savedTx.Signatures[1].PubKey.Equals(info.GetPubKey())) 523 assert.NotEqual(t, savedTx.Signatures[0].Signature, savedTx.Signatures[1].Signature) 524 }) 525 526 t.Run("overwrite existing signature", func(t *testing.T) { 527 t.Parallel() 528 529 var ( 530 kbHome = t.TempDir() 531 baseOptions = BaseOptions{ 532 InsecurePasswordStdin: true, 533 Home: kbHome, 534 Quiet: true, 535 } 536 537 mnemonic = generateTestMnemonic(t) 538 keyName = "generated-key" 539 encryptPassword = "encrypt" 540 541 tx = std.Tx{ 542 Fee: std.Fee{ 543 GasWanted: 10, 544 GasFee: std.Coin{ 545 Amount: 10, 546 Denom: "ugnot", 547 }, 548 }, 549 } 550 ) 551 552 // Generate a key in the keybase 553 kb, err := keys.NewKeyBaseFromDir(kbHome) 554 require.NoError(t, err) 555 556 info, err := kb.CreateAccount(keyName, mnemonic, "", encryptPassword, 0, 0) 557 require.NoError(t, err) 558 559 // Generate the signature 560 signBytes, err := tx.GetSignBytes("id", 0, 0) 561 require.NoError(t, err) 562 563 signature, pubKey, err := kb.Sign(keyName, encryptPassword, signBytes) 564 require.NoError(t, err) 565 566 tx.Signatures = []std.Signature{ 567 { 568 PubKey: pubKey, 569 Signature: signature, 570 }, 571 } 572 573 // We need to prepare the message signer as well 574 // for validation to complete 575 tx.Msgs = []std.Msg{ 576 bank.MsgSend{ 577 FromAddress: info.GetAddress(), 578 }, 579 } 580 581 // Create an empty tx file 582 txFile, err := os.CreateTemp("", "") 583 require.NoError(t, err) 584 585 // Marshal the tx and write it to the file 586 encodedTx, err := amino.MarshalJSON(tx) 587 require.NoError(t, err) 588 589 _, err = txFile.Write(encodedTx) 590 require.NoError(t, err) 591 592 ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) 593 defer cancelFn() 594 595 // Create the command IO 596 io := commands.NewTestIO() 597 io.SetIn( 598 strings.NewReader( 599 fmt.Sprintf( 600 "%s\n%s\n", 601 encryptPassword, 602 encryptPassword, 603 ), 604 ), 605 ) 606 607 // Create the command 608 cmd := NewRootCmdWithBaseConfig(io, baseOptions) 609 610 args := []string{ 611 "sign", 612 "--insecure-password-stdin", 613 "--home", 614 kbHome, 615 "--tx-path", 616 txFile.Name(), 617 keyName, 618 } 619 620 // Run the command 621 require.NoError(t, cmd.ParseAndRun(ctx, args)) 622 623 // Make sure the tx file was updated with the signature 624 savedTxRaw, err := os.ReadFile(txFile.Name()) 625 require.NoError(t, err) 626 627 var savedTx std.Tx 628 require.NoError(t, amino.UnmarshalJSON(savedTxRaw, &savedTx)) 629 630 require.Len(t, savedTx.Signatures, 1) 631 assert.True(t, savedTx.Signatures[0].PubKey.Equals(info.GetPubKey())) 632 }) 633 }