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  }