github.com/Finschia/finschia-sdk@v0.49.1/x/auth/client/cli/tx_multisign.go (about)

     1  package cli
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  
     8  	"github.com/spf13/cobra"
     9  	"github.com/spf13/viper"
    10  
    11  	"github.com/Finschia/finschia-sdk/client"
    12  	"github.com/Finschia/finschia-sdk/client/flags"
    13  	"github.com/Finschia/finschia-sdk/client/tx"
    14  	"github.com/Finschia/finschia-sdk/crypto/keyring"
    15  	kmultisig "github.com/Finschia/finschia-sdk/crypto/keys/multisig"
    16  	"github.com/Finschia/finschia-sdk/crypto/types/multisig"
    17  	sdk "github.com/Finschia/finschia-sdk/types"
    18  	"github.com/Finschia/finschia-sdk/types/errors"
    19  	signingtypes "github.com/Finschia/finschia-sdk/types/tx/signing"
    20  	"github.com/Finschia/finschia-sdk/version"
    21  	authclient "github.com/Finschia/finschia-sdk/x/auth/client"
    22  	"github.com/Finschia/finschia-sdk/x/auth/legacy/legacytx"
    23  	"github.com/Finschia/finschia-sdk/x/auth/signing"
    24  )
    25  
    26  // BroadcastReq defines a tx broadcasting request.
    27  type BroadcastReq struct {
    28  	Tx   legacytx.StdTx `json:"tx" yaml:"tx"`
    29  	Mode string         `json:"mode" yaml:"mode"`
    30  }
    31  
    32  // GetSignCommand returns the sign command
    33  func GetMultiSignCommand() *cobra.Command {
    34  	cmd := &cobra.Command{
    35  		Use:   "multisign [file] [name] [[signature]...]",
    36  		Short: "Generate multisig signatures for transactions generated offline",
    37  		Long: strings.TrimSpace(
    38  			fmt.Sprintf(`Sign transactions created with the --generate-only flag that require multisig signatures.
    39  
    40  Read one or more signatures from one or more [signature] file, generate a multisig signature compliant to the
    41  multisig key [name], and attach the key name to the transaction read from [file].
    42  
    43  Example:
    44  $ %s tx multisign transaction.json k1k2k3 k1sig.json k2sig.json k3sig.json
    45  
    46  If --signature-only flag is on, output a JSON representation
    47  of only the generated signature.
    48  
    49  If the --offline flag is on, the client will not reach out to an external node.
    50  Account number or sequence number lookups are not performed so you must
    51  set these parameters manually.
    52  
    53  The current multisig implementation defaults to amino-json sign mode.
    54  The SIGN_MODE_DIRECT sign mode is not supported.'
    55  `,
    56  				version.AppName,
    57  			),
    58  		),
    59  		RunE: makeMultiSignCmd(),
    60  		Args: cobra.MinimumNArgs(3),
    61  	}
    62  
    63  	cmd.Flags().Bool(flagSigOnly, false, "Print only the generated signature, then exit")
    64  	cmd.Flags().String(flags.FlagOutputDocument, "", "The document is written to the given file instead of STDOUT")
    65  	cmd.Flags().Bool(flagAmino, false, "Generate Amino-encoded JSON suitable for submitting to the txs REST endpoint")
    66  	flags.AddTxFlagsToCmd(cmd)
    67  	cmd.Flags().String(flags.FlagChainID, "", "network chain ID")
    68  
    69  	return cmd
    70  }
    71  
    72  func makeMultiSignCmd() func(cmd *cobra.Command, args []string) (err error) {
    73  	return func(cmd *cobra.Command, args []string) (err error) {
    74  		clientCtx, err := client.GetClientTxContext(cmd)
    75  		if err != nil {
    76  			return err
    77  		}
    78  		parsedTx, err := authclient.ReadTxFromFile(clientCtx, args[0])
    79  		if err != nil {
    80  			return err
    81  		}
    82  
    83  		txFactory := tx.NewFactoryCLI(clientCtx, cmd.Flags())
    84  		if txFactory.SignMode() == signingtypes.SignMode_SIGN_MODE_UNSPECIFIED {
    85  			txFactory = txFactory.WithSignMode(signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON)
    86  		}
    87  
    88  		txCfg := clientCtx.TxConfig
    89  		txBuilder, err := txCfg.WrapTxBuilder(parsedTx)
    90  		if err != nil {
    91  			return err
    92  		}
    93  
    94  		multisigInfo, err := getMultisigInfo(clientCtx, args[1])
    95  		if err != nil {
    96  			return err
    97  		}
    98  
    99  		multisigPub := multisigInfo.GetPubKey().(*kmultisig.LegacyAminoPubKey)
   100  		multisigSig := multisig.NewMultisig(len(multisigPub.PubKeys))
   101  		if !clientCtx.Offline {
   102  			accnum, seq, err := clientCtx.AccountRetriever.GetAccountNumberSequence(clientCtx, multisigInfo.GetAddress())
   103  			if err != nil {
   104  				return err
   105  			}
   106  
   107  			txFactory = txFactory.WithAccountNumber(accnum).WithSequence(seq)
   108  		}
   109  
   110  		// read each signature and add it to the multisig if valid
   111  		for i := 2; i < len(args); i++ {
   112  			sigs, err := unmarshalSignatureJSON(clientCtx, args[i])
   113  			if err != nil {
   114  				return err
   115  			}
   116  
   117  			if txFactory.ChainID() == "" {
   118  				return fmt.Errorf("set the chain id with either the --chain-id flag or config file")
   119  			}
   120  
   121  			signingData := signing.SignerData{
   122  				ChainID:       txFactory.ChainID(),
   123  				AccountNumber: txFactory.AccountNumber(),
   124  				Sequence:      txFactory.Sequence(),
   125  			}
   126  
   127  			for _, sig := range sigs {
   128  				err = signing.VerifySignature(sig.PubKey, signingData, sig.Data, txCfg.SignModeHandler(), txBuilder.GetTx())
   129  				if err != nil {
   130  					addr, _ := sdk.AccAddressFromHex(sig.PubKey.Address().String())
   131  					return fmt.Errorf("couldn't verify signature for address %s", addr)
   132  				}
   133  
   134  				if err := multisig.AddSignatureV2(multisigSig, sig, multisigPub.GetPubKeys()); err != nil {
   135  					return err
   136  				}
   137  			}
   138  		}
   139  
   140  		sigV2 := signingtypes.SignatureV2{
   141  			PubKey:   multisigPub,
   142  			Data:     multisigSig,
   143  			Sequence: txFactory.Sequence(),
   144  		}
   145  
   146  		err = txBuilder.SetSignatures(sigV2)
   147  		if err != nil {
   148  			return err
   149  		}
   150  
   151  		sigOnly, _ := cmd.Flags().GetBool(flagSigOnly)
   152  
   153  		aminoJSON, _ := cmd.Flags().GetBool(flagAmino)
   154  
   155  		var json []byte
   156  
   157  		if aminoJSON {
   158  			stdTx, err := tx.ConvertTxToStdTx(clientCtx.LegacyAmino, txBuilder.GetTx())
   159  			if err != nil {
   160  				return err
   161  			}
   162  
   163  			req := BroadcastReq{
   164  				Tx:   stdTx,
   165  				Mode: "block|sync|async",
   166  			}
   167  
   168  			json, _ = clientCtx.LegacyAmino.MarshalJSON(req)
   169  
   170  		} else {
   171  			json, err = marshalSignatureJSON(txCfg, txBuilder, sigOnly)
   172  			if err != nil {
   173  				return err
   174  			}
   175  		}
   176  
   177  		outputDoc, _ := cmd.Flags().GetString(flags.FlagOutputDocument)
   178  		if outputDoc == "" {
   179  			cmd.Printf("%s\n", json)
   180  			return err
   181  		}
   182  
   183  		fp, err := os.OpenFile(outputDoc, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
   184  		if err != nil {
   185  			return err
   186  		}
   187  
   188  		defer func() {
   189  			err2 := fp.Close()
   190  			if err == nil {
   191  				err = err2
   192  			}
   193  		}()
   194  
   195  		err = clientCtx.PrintBytes(json)
   196  
   197  		return err
   198  	}
   199  }
   200  
   201  func GetMultiSignBatchCmd() *cobra.Command {
   202  	cmd := &cobra.Command{
   203  		Use:   "multisign-batch [file] [name] [[signature-file]...]",
   204  		Short: "Assemble multisig transactions in batch from batch signatures",
   205  		Long: strings.TrimSpace(
   206  			fmt.Sprintf(`Assemble a batch of multisig transactions generated by batch sign command.
   207  
   208  Read one or more signatures from one or more [signature] file, generate a multisig signature compliant to the
   209  multisig key [name], and attach the key name to the transaction read from [file].
   210  
   211  Example:
   212  $ %s tx multisign-batch transactions.json multisigk1k2k3 k1sigs.json k2sigs.json k3sig.json
   213  
   214  The current multisig implementation defaults to amino-json sign mode.
   215  The SIGN_MODE_DIRECT sign mode is not supported.'
   216  `, version.AppName,
   217  			),
   218  		),
   219  		PreRun: preSignCmd,
   220  		RunE:   makeBatchMultisignCmd(),
   221  		Args:   cobra.MinimumNArgs(3),
   222  	}
   223  
   224  	cmd.Flags().Bool(flagNoAutoIncrement, false, "disable sequence auto increment")
   225  	cmd.Flags().String(
   226  		flagMultisig, "",
   227  		"Address of the multisig account that the transaction signs on behalf of",
   228  	)
   229  	cmd.Flags().String(flags.FlagOutputDocument, "", "The document is written to the given file instead of STDOUT")
   230  	flags.AddTxFlagsToCmd(cmd)
   231  
   232  	return cmd
   233  }
   234  
   235  func makeBatchMultisignCmd() func(cmd *cobra.Command, args []string) error {
   236  	return func(cmd *cobra.Command, args []string) (err error) {
   237  		var clientCtx client.Context
   238  
   239  		clientCtx, err = client.GetClientTxContext(cmd)
   240  		if err != nil {
   241  			return err
   242  		}
   243  
   244  		txCfg := clientCtx.TxConfig
   245  		txFactory := tx.NewFactoryCLI(clientCtx, cmd.Flags())
   246  		if txFactory.SignMode() == signingtypes.SignMode_SIGN_MODE_UNSPECIFIED {
   247  			txFactory = txFactory.WithSignMode(signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON)
   248  		}
   249  
   250  		infile := os.Stdin
   251  		if args[0] != "-" {
   252  			infile, err = os.Open(args[0])
   253  			defer func() {
   254  				err2 := infile.Close()
   255  				if err == nil {
   256  					err = err2
   257  				}
   258  			}()
   259  
   260  			if err != nil {
   261  				return fmt.Errorf("couldn't open %s: %w", args[0], err)
   262  			}
   263  		}
   264  		scanner := authclient.NewBatchScanner(txCfg, infile)
   265  
   266  		multisigInfo, err := getMultisigInfo(clientCtx, args[1])
   267  		if err != nil {
   268  			return err
   269  		}
   270  
   271  		var signatureBatch [][]signingtypes.SignatureV2
   272  		for i := 2; i < len(args); i++ {
   273  			sigs, err := readSignaturesFromFile(clientCtx, args[i])
   274  			if err != nil {
   275  				return err
   276  			}
   277  
   278  			signatureBatch = append(signatureBatch, sigs)
   279  		}
   280  
   281  		if !clientCtx.Offline {
   282  			accnum, seq, err := clientCtx.AccountRetriever.GetAccountNumberSequence(clientCtx, multisigInfo.GetAddress())
   283  			if err != nil {
   284  				return err
   285  			}
   286  
   287  			txFactory = txFactory.WithAccountNumber(accnum).WithSequence(seq)
   288  		}
   289  
   290  		// prepare output document
   291  		closeFunc, err := setOutputFile(cmd)
   292  		if err != nil {
   293  			return err
   294  		}
   295  
   296  		defer closeFunc()
   297  		clientCtx.WithOutput(cmd.OutOrStdout())
   298  
   299  		for i := 0; scanner.Scan(); i++ {
   300  			txBldr, err := txCfg.WrapTxBuilder(scanner.Tx())
   301  			if err != nil {
   302  				return err
   303  			}
   304  
   305  			multisigPub := multisigInfo.GetPubKey().(*kmultisig.LegacyAminoPubKey)
   306  			multisigSig := multisig.NewMultisig(len(multisigPub.PubKeys))
   307  			signingData := signing.SignerData{
   308  				ChainID:       txFactory.ChainID(),
   309  				AccountNumber: txFactory.AccountNumber(),
   310  				Sequence:      txFactory.Sequence(),
   311  			}
   312  
   313  			for _, sig := range signatureBatch {
   314  				err = signing.VerifySignature(sig[i].PubKey, signingData, sig[i].Data, txCfg.SignModeHandler(), txBldr.GetTx())
   315  				if err != nil {
   316  					return fmt.Errorf("couldn't verify signature: %w %v", err, sig)
   317  				}
   318  
   319  				if err := multisig.AddSignatureV2(multisigSig, sig[i], multisigPub.GetPubKeys()); err != nil {
   320  					return err
   321  				}
   322  			}
   323  
   324  			sigV2 := signingtypes.SignatureV2{
   325  				PubKey:   multisigPub,
   326  				Data:     multisigSig,
   327  				Sequence: txFactory.Sequence(),
   328  			}
   329  
   330  			err = txBldr.SetSignatures(sigV2)
   331  			if err != nil {
   332  				return err
   333  			}
   334  
   335  			sigOnly, _ := cmd.Flags().GetBool(flagSigOnly)
   336  			aminoJSON, _ := cmd.Flags().GetBool(flagAmino)
   337  
   338  			var json []byte
   339  
   340  			if aminoJSON {
   341  				stdTx, err := tx.ConvertTxToStdTx(clientCtx.LegacyAmino, txBldr.GetTx())
   342  				if err != nil {
   343  					return err
   344  				}
   345  
   346  				req := BroadcastReq{
   347  					Tx:   stdTx,
   348  					Mode: "block|sync|async",
   349  				}
   350  
   351  				json, _ = clientCtx.LegacyAmino.MarshalJSON(req)
   352  
   353  			} else {
   354  				json, err = marshalSignatureJSON(txCfg, txBldr, sigOnly)
   355  				if err != nil {
   356  					return err
   357  				}
   358  			}
   359  
   360  			err = clientCtx.PrintString(fmt.Sprintf("%s\n", json))
   361  			if err != nil {
   362  				return err
   363  			}
   364  
   365  			if viper.GetBool(flagNoAutoIncrement) {
   366  				continue
   367  			}
   368  			sequence := txFactory.Sequence() + 1
   369  			txFactory = txFactory.WithSequence(sequence)
   370  		}
   371  
   372  		return scanner.UnmarshalErr()
   373  	}
   374  }
   375  
   376  func unmarshalSignatureJSON(clientCtx client.Context, filename string) (sigs []signingtypes.SignatureV2, err error) {
   377  	var bytes []byte
   378  	if bytes, err = os.ReadFile(filename); err != nil {
   379  		return
   380  	}
   381  	return clientCtx.TxConfig.UnmarshalSignatureJSON(bytes)
   382  }
   383  
   384  func readSignaturesFromFile(ctx client.Context, filename string) (sigs []signingtypes.SignatureV2, err error) {
   385  	bz, err := os.ReadFile(filename)
   386  	if err != nil {
   387  		return nil, err
   388  	}
   389  
   390  	newString := strings.TrimSuffix(string(bz), "\n")
   391  	lines := strings.Split(newString, "\n")
   392  
   393  	for _, bz := range lines {
   394  		sig, err := ctx.TxConfig.UnmarshalSignatureJSON([]byte(bz))
   395  		if err != nil {
   396  			return nil, err
   397  		}
   398  
   399  		sigs = append(sigs, sig...)
   400  	}
   401  	return sigs, nil
   402  }
   403  
   404  func getMultisigInfo(clientCtx client.Context, name string) (keyring.Info, error) {
   405  	kb := clientCtx.Keyring
   406  	multisigInfo, err := kb.Key(name)
   407  	if err != nil {
   408  		return nil, errors.Wrap(err, "error getting keybase multisig account")
   409  	}
   410  	if multisigInfo.GetType() != keyring.TypeMulti {
   411  		return nil, fmt.Errorf("%q must be of type %s: %s", name, keyring.TypeMulti, multisigInfo.GetType())
   412  	}
   413  
   414  	return multisigInfo, nil
   415  }
   416  
   417  func getMultisigRecord(clientCtx client.Context, name string) (keyring.Info, error) {
   418  	kb := clientCtx.Keyring
   419  	multisigRecord, err := kb.Key(name)
   420  	if err != nil {
   421  		return nil, errors.Wrap(err, "error getting keybase multisig account")
   422  	}
   423  
   424  	return multisigRecord, nil
   425  }