From 091a3a1178f38ab0cc7a17452a67255cbae314d9 Mon Sep 17 00:00:00 2001 From: LuttyYang Date: Fri, 9 Jul 2021 16:12:58 +0800 Subject: [PATCH] Offline signing (#258) * init * add offline-sign-transfer * test and write document --- README.md | 16 ++++ cmd/subcommands/root.go | 5 +- cmd/subcommands/transfer.go | 127 +++++++++++++++++++++++++++---- pkg/transaction/controller.go | 59 ++++++++------ pkg/transaction/ethcontroller.go | 2 +- 5 files changed, 170 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 7dc027f..ad48714 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,22 @@ Example of returned JSON Array: ] ``` +## Offline sign transfer +1. Get Nonce From a Account. (Need to be online, but no passphrase required) +```bash +./hmy get-nonce --node=https://api.s0.t.hmny.io --from=[ONE address] +``` + +2. Sign transfer and write to file. (Passphrase required, But no need to be online) +```bash +./hmy transfer --offline-sign --nonce=[nonce value from previous] --from=[ONE address] --to=[ONE address] --amount=1000 --from-shard=0 --to-shard=0 > signed.json +``` + +3. send `signed.json` to Harmony blockchain! (Need to be online, but no passphrase required) +```bash +./hmy offline-sign-transfer --node=https://api.s0.b.hmny.io --file ./signed.json +``` + # Debugging The go-sdk code respects `HMY_RPC_DEBUG HMY_TX_DEBUG` as debugging diff --git a/cmd/subcommands/root.go b/cmd/subcommands/root.go index 738b223..635cfcf 100644 --- a/cmd/subcommands/root.go +++ b/cmd/subcommands/root.go @@ -181,7 +181,10 @@ var ( func Execute() { RootCmd.SilenceErrors = true if err := RootCmd.Execute(); err != nil { - resp, _ := http.Get(versionLink) + resp, httpErr := http.Get(versionLink) + if httpErr != nil { + return + } defer resp.Body.Close() // If error, no op if resp != nil && resp.StatusCode == 200 { diff --git a/cmd/subcommands/transfer.go b/cmd/subcommands/transfer.go index 5f052c2..e5e611c 100644 --- a/cmd/subcommands/transfer.go +++ b/cmd/subcommands/transfer.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io/ioutil" + "os" "strconv" "strings" "time" @@ -33,6 +34,7 @@ var ( targetChain string chainName chainIDWrapper dryRun bool + offlineSign bool trueNonce bool inputNonce string gasPrice string @@ -99,17 +101,22 @@ func handlerForError(txLog *transactionLog, err error) error { // Note that the vars need to be set before calling this handler. func handlerForTransaction(txLog *transactionLog) error { from := fromAddress.String() - s, err := sharding.Structure(node) - if handlerForError(txLog, err) != nil { - return err - } - err = validation.ValidShardIDs(fromShardID, toShardID, uint32(len(s))) - if handlerForError(txLog, err) != nil { - return err - } - networkHandler, err := handlerForShard(fromShardID, node) - if handlerForError(txLog, err) != nil { - return err + + var networkHandler *rpc.HTTPMessenger + if !offlineSign { + s, err := sharding.Structure(node) + if handlerForError(txLog, err) != nil { + return err + } + err = validation.ValidShardIDs(fromShardID, toShardID, uint32(len(s))) + if handlerForError(txLog, err) != nil { + return err + } + + networkHandler, err = handlerForShard(fromShardID, node) + if handlerForError(txLog, err) != nil { + return err + } } var ctrlr *transaction.Controller @@ -125,7 +132,7 @@ func handlerForTransaction(txLog *transactionLog) error { } nonce, err := getNonce(fromAddress.String(), networkHandler) - if err != nil { + if handlerForError(txLog, err) != nil { return err } @@ -264,6 +271,9 @@ func opts(ctlr *transaction.Controller) { if dryRun { ctlr.Behavior.DryRun = true } + if offlineSign { + ctlr.Behavior.OfflineSign = true + } if useLedgerWallet { ctlr.Behavior.SigningImpl = transaction.Ledger } @@ -291,6 +301,8 @@ func getNonceFromInput(addr, inputNonce string, messenger rpc.T) (uint64, error) } else { return nonce, nil } + } else if offlineSign { + return 0, errors.New("nonce value must be specified when offline sign") } else { return transaction.GetNextPendingNonce(addr, messenger), nil } @@ -327,6 +339,10 @@ func init() { Create a transaction, sign it, and send off to the Harmony blockchain `, PreRunE: func(cmd *cobra.Command, args []string) error { + if offlineSign { + dryRun = true + } + if givenFilePath == "" { for _, flagName := range [...]string{"from", "to", "amount", "from-shard", "to-shard"} { _ = cmd.MarkFlagRequired(flagName) @@ -360,7 +376,7 @@ Create a transaction, sign it, and send off to the Harmony blockchain passphrase = pp // needed for passphrase assignment used in handler txLog := transactionLog{} err = handlerForTransaction(&txLog) - fmt.Println(common.ToJSONUnsafe(txLog, !noPrettyOutput)) + fmt.Println(common.ToJSONUnsafe([]transactionLog{txLog}, !noPrettyOutput)) return err } else { hasError := false @@ -376,7 +392,7 @@ Create a transaction, sign it, and send off to the Harmony blockchain } } } - fmt.Println(common.ToJSONUnsafe(txLogs, true)) + fmt.Println(common.ToJSONUnsafe(txLogs, !noPrettyOutput)) if hasError { return fmt.Errorf("one or more of your transactions returned an error " + "-- check the log for more information") @@ -390,6 +406,7 @@ Create a transaction, sign it, and send off to the Harmony blockchain cmdTransfer.Flags().Var(&fromAddress, "from", "sender's one address, keystore must exist locally") cmdTransfer.Flags().Var(&toAddress, "to", "the destination one address") cmdTransfer.Flags().BoolVar(&dryRun, "dry-run", false, "do not send signed transaction") + cmdTransfer.Flags().BoolVar(&offlineSign, "offline-sign", false, "output offline signing") cmdTransfer.Flags().BoolVar(&trueNonce, "true-nonce", false, "send transaction with on-chain nonce") cmdTransfer.Flags().StringVar(&amount, "amount", "0", "amount to send (ONE)") cmdTransfer.Flags().StringVar(&gasPrice, "gas-price", "1", "gas price to pay (NANO)") @@ -403,4 +420,86 @@ Create a transaction, sign it, and send off to the Harmony blockchain cmdTransfer.Flags().StringVar(&passphraseFilePath, "passphrase-file", "", "path to a file containing the passphrase") RootCmd.AddCommand(cmdTransfer) + + cmdGetNonce := &cobra.Command{ + Use: "get-nonce", + Short: "Get Nonce From a Account", + Args: cobra.ExactArgs(0), + Long: ` +Get Nonce From a Account +`, + RunE: func(cmd *cobra.Command, args []string) error { + networkHandler, err := handlerForShard(fromShardID, node) + if err != nil { + return err + } + + nonce := transaction.GetNextPendingNonce(fromAddress.address, networkHandler) + fmt.Printf("nonce is \"%d\"", nonce) + return err + }, + } + + cmdGetNonce.Flags().Var(&fromAddress, "from", "sender's one address, keystore must exist locally") + cmdGetNonce.Flags().Uint32Var(&fromShardID, "from-shard", 0, "source shard id") + RootCmd.AddCommand(cmdGetNonce) + + cmdOfflineSignTransfer := &cobra.Command{ + Use: "offline-sign-transfer", + Short: "Send a Offline Signed transaction", + Args: cobra.ExactArgs(0), + Long: ` +Send a offline signed to the Harmony blockchain +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if givenFilePath == "" { + return fmt.Errorf("must give a offline-signed file") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + var txLogs []*transactionLog + + networkHandler, err := handlerForShard(fromShardID, node) + if err != nil { + return err + } + + openFile, err := os.Open(givenFilePath) + if err != nil { + return err + } + defer openFile.Close() + + err = json.NewDecoder(openFile).Decode(&txLogs) + if err != nil { + return err + } + + for _, txLog := range txLogs { + if len(txLog.Errors) > 0 { + continue + } + + ctrlr := transaction.NewController(networkHandler, nil, nil, *chainName.chainID, opts) + err := ctrlr.ExecuteRawTransaction(txLog.RawTxn) + if handlerForError(txLog, err) != nil { + txLog.Errors = append(txLog.Errors, err.Error()) + continue + } + + if txHash := ctrlr.TransactionHash(); txHash != nil { + txLog.TxHash = *txHash + } + + txLog.Receipt = ctrlr.Receipt()["result"] + } + + fmt.Println(common.ToJSONUnsafe(txLogs, true)) + return nil + }, + } + + cmdOfflineSignTransfer.Flags().Uint32Var(&fromShardID, "from-shard", 0, "source shard id") + RootCmd.AddCommand(cmdOfflineSignTransfer) } diff --git a/pkg/transaction/controller.go b/pkg/transaction/controller.go index 1cdc203..21c75b5 100644 --- a/pkg/transaction/controller.go +++ b/pkg/transaction/controller.go @@ -58,6 +58,7 @@ type Controller struct { type behavior struct { DryRun bool + OfflineSign bool SigningImpl SignerImpl ConfirmationWaitTime uint32 } @@ -83,7 +84,7 @@ func NewController( receipt: nil, }, chain: chain, - Behavior: behavior{false, Software, 0}, + Behavior: behavior{false, false, Software, 0}, } for _, option := range options { option(ctrlr) @@ -172,35 +173,39 @@ func (C *Controller) setAmount(amount numeric.Dec) { }) return } - balanceRPCReply, err := C.messenger.SendRPC( - rpc.Method.GetBalance, - p{address.ToBech32(C.sender.account.Address), "latest"}, - ) - if err != nil { - C.executionError = err - return - } - currentBalance, _ := balanceRPCReply["result"].(string) - bal, _ := new(big.Int).SetString(currentBalance[2:], 16) - balance := numeric.NewDecFromBigInt(bal) + gasAsDec := C.transactionForRPC.params["gas-price"].(numeric.Dec) gasAsDec = gasAsDec.Mul(numeric.NewDec(int64(C.transactionForRPC.params["gas-limit"].(uint64)))) amountInAtto := amount.Mul(oneAsDec) total := amountInAtto.Add(gasAsDec) - if total.GT(balance) { - balanceInOne := balance.Quo(oneAsDec) - C.executionError = ErrBadTransactionParam - errorMsg := fmt.Sprintf( - "insufficient balance of %s in shard %d for the requested transfer of %s", - balanceInOne.String(), C.transactionForRPC.params["from-shard"].(uint32), amount.String(), + if !C.Behavior.OfflineSign { + balanceRPCReply, err := C.messenger.SendRPC( + rpc.Method.GetBalance, + p{address.ToBech32(C.sender.account.Address), "latest"}, ) - C.transactionErrors = append(C.transactionErrors, &Error{ - ErrMessage: &errorMsg, - TimestampOfRejection: time.Now().Unix(), - }) - return + if err != nil { + C.executionError = err + return + } + currentBalance, _ := balanceRPCReply["result"].(string) + bal, _ := new(big.Int).SetString(currentBalance[2:], 16) + balance := numeric.NewDecFromBigInt(bal) + if total.GT(balance) { + balanceInOne := balance.Quo(oneAsDec) + C.executionError = ErrBadTransactionParam + errorMsg := fmt.Sprintf( + "insufficient balance of %s in shard %d for the requested transfer of %s", + balanceInOne.String(), C.transactionForRPC.params["from-shard"].(uint32), amount.String(), + ) + C.transactionErrors = append(C.transactionErrors, &Error{ + ErrMessage: &errorMsg, + TimestampOfRejection: time.Now().Unix(), + }) + return + } } + C.transactionForRPC.params["transfer-amount"] = amountInAtto } @@ -379,4 +384,12 @@ func (C *Controller) SignTransaction( return C.executionError } +func (C *Controller) ExecuteRawTransaction(txn string) error { + C.transactionForRPC.signature = &txn + + C.sendSignedTx() + C.txConfirmation() + return C.executionError +} + // TODO: add logic to create staking transactions in the SDK. diff --git a/pkg/transaction/ethcontroller.go b/pkg/transaction/ethcontroller.go index e797493..9df42aa 100644 --- a/pkg/transaction/ethcontroller.go +++ b/pkg/transaction/ethcontroller.go @@ -57,7 +57,7 @@ func NewEthController( receipt: nil, }, chain: chain, - Behavior: behavior{false, Software, 0}, + Behavior: behavior{false, false, Software, 0}, } for _, option := range options { option(ctrlr)