Offline signing (#258)

* init

* add offline-sign-transfer

* test and write document
pull/262/head
LuttyYang 3 years ago committed by GitHub
parent ab1cb6df13
commit 091a3a1178
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      README.md
  2. 5
      cmd/subcommands/root.go
  3. 127
      cmd/subcommands/transfer.go
  4. 59
      pkg/transaction/controller.go
  5. 2
      pkg/transaction/ethcontroller.go

@ -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

@ -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 {

@ -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)
}

@ -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.

@ -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)

Loading…
Cancel
Save