From a8e68b9cc3d374f6dc8bd68a3208dfd1439968ae Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Thu, 15 Feb 2018 00:33:16 +0100 Subject: [PATCH] signer: implement OnApprovedTx, change signing response (API BREAKAGE) --- cmd/signer/api.go | 150 +++++++++++++++++++++------------ cmd/signer/api_test.go | 64 +++++++------- cmd/signer/auditlog.go | 17 ++-- cmd/signer/cliui.go | 19 +++-- cmd/signer/main.go | 2 +- cmd/signer/rules/rules.go | 19 ++++- cmd/signer/rules/rules_test.go | 94 +++++++++++++++------ cmd/signer/stdioui.go | 13 ++- cmd/signer/types.go | 33 +++++++- 9 files changed, 280 insertions(+), 131 deletions(-) diff --git a/cmd/signer/api.go b/cmd/signer/api.go index d36efe9b91..03a182d849 100644 --- a/cmd/signer/api.go +++ b/cmd/signer/api.go @@ -33,8 +33,8 @@ import ( "github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" ) @@ -46,7 +46,7 @@ type ExternalAPI interface { // New request to create a new account New(ctx context.Context) (accounts.Account, error) // SignTransaction request to sign the specified transaction - SignTransaction(ctx context.Context, from common.MixedcaseAddress, args TransactionArg, methodSelector *string) (hexutil.Bytes, error) + SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) // Sign - request to sign the given data (plus prefix) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) // EcRecover - request to perform ecrecover @@ -77,6 +77,9 @@ type SignerUI interface { ShowError(message string) // ShowInfo displays info message to user ShowInfo(message string) + // OnApprovedTx notifies the UI about a transaction having been successfully signed. + // This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient. + OnApprovedTx(tx ethapi.SignTransactionResult) } // SignerAPI defines the actual implementation of ExternalAPI @@ -123,18 +126,16 @@ func (m Metadata) String() string { type ( // SignTxRequest contains info about a Transaction to sign SignTxRequest struct { - Transaction TransactionArg `json:"transaction"` - From common.MixedcaseAddress `json:"from"` - Callinfo string `json:"call_info"` - Meta Metadata `json:"meta"` + Transaction SendTxArgs `json:"transaction"` + Callinfo string `json:"call_info"` + Meta Metadata `json:"meta"` } // SignTxResponse result from SignTxRequest SignTxResponse struct { //The UI may make changes to the TX - Transaction TransactionArg `json:"transaction"` - From common.MixedcaseAddress `json:"from"` - Approved bool `json:"approved"` - Password string `json:"password"` + Transaction SendTxArgs `json:"transaction"` + Approved bool `json:"approved"` + Password string `json:"password"` } // ExportRequest info about query to export accounts ExportRequest struct { @@ -271,23 +272,17 @@ func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) { return be[0].(*keystore.KeyStore).NewAccount(resp.Password) } -func toTransaction(args *TransactionArg) *types.Transaction { - if args.To == nil { - return types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data) - } else { - return types.NewTransaction(uint64(*args.Nonce), args.To.Address(), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data) - } -} - // logDiff logs the difference between the incoming (original) transaction and the one returned from the signer. // it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow // UI-modifications to requests func logDiff(original *SignTxRequest, new *SignTxResponse) bool { modified := false - if f0, f1 := original.From, new.From; f0 != f1 { - modified = true + + if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) { log.Info("Sender-account changed by UI", "was", f0, "is", f1) + modified = true } + if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) { log.Info("Recipient-account changed by UI", "was", t0, "is", t1) modified = true @@ -310,9 +305,19 @@ func logDiff(original *SignTxRequest, new *SignTxResponse) bool { log.Info("Value changed by UI", "was", v0, "is", v1) } } - if d0, d1 := original.Transaction.Data, new.Transaction.Data; !bytes.Equal(d0, d1) { - modified = true - log.Info("Data changed by UI", "was", common.ToHex(d0), "is", common.ToHex(d1)) + if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 { + d0s := "" + d1s := "" + if d0 != nil { + d0s = common.ToHex(*d0) + } + if d1 != nil { + d1s = common.ToHex(*d1) + } + if d1s != d0s { + modified = true + log.Info("Data changed by UI", "was", d0s, "is", d1s) + } } if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 { @@ -324,41 +329,68 @@ func logDiff(original *SignTxRequest, new *SignTxResponse) bool { return modified } -// SignTransaction signs the given Transaction and returns it in an RLP encoded form -// that can be posted to `eth_sendRawTransaction`. -func (api *SignerAPI) SignTransaction(ctx context.Context, from common.MixedcaseAddress, args TransactionArg, methodSelector *string) (hexutil.Bytes, error) { +// determineCallInfo turns ABI-data + methodselector (if given) into a string suitable +// to present to the user. +func (api *SignerAPI) determineCallInfo(data []byte, methodSelector *string) string { + + if len(data) < 4 { + return "" + } var ( - err error - result SignTxResponse + selector string + err error ) - req := SignTxRequest{Transaction: args, From: from, Meta: MetadataFromContext(ctx)} - data := args.Data - if len(data) > 3 { - // Try to make sense of the data - var selector string - if methodSelector == nil { - selector, err = api.abidb.LookupMethodSelector(data[:4]) - if err != nil { - req.Callinfo = errorWrapper{"Warning! Could not locate ABI", err}.String() - } - } else { - selector = *methodSelector + // Try to make sense of the data + if methodSelector == nil { + selector, err = api.abidb.LookupMethodSelector(data[:4]) + if err != nil { + return errorWrapper{"Warning! Could not locate ABI", err}.String() } - if selector != "" { - abidata, err := MethodSelectorToAbi(selector) + } else { + selector = *methodSelector + } + if selector != "" { + abiData, err := MethodSelectorToAbi(selector) + if err != nil { + return errorWrapper{"Warning! Could not validate ABI-data against calldata", err}.String() + } else { + var info *decodedCallData + info, err = parseCallData(data, string(abiData)) if err != nil { - req.Callinfo = errorWrapper{"Warning! Could not validate ABI-data against calldata", err}.String() + return errorWrapper{"Warning! Could not validate ABI-data against calldata", err}.String() } else { - var info *decodedCallData - info, err = parseCallData(data, string(abidata)) - if err != nil { - req.Callinfo = errorWrapper{"Warning! Could not validate ABI-data against calldata", err}.String() - } else { - req.Callinfo = info.String() - } + return info.String() } } } + return "" +} + +// SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form +func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) { + var ( + err error + result SignTxResponse + data []byte + ) + // Prevent accidental erroneous usage of both 'input' and 'data' + if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) { + return nil, errors.New(`Ambiguous request: moth "data" and "input" are set and are not identical`) + } + + if args.Data != nil { + data = *args.Data + } else if args.Input != nil { + data = *args.Input + *args.Data = data + *args.Input = nil + } + req := SignTxRequest{ + Transaction: args, + Meta: MetadataFromContext(ctx), + Callinfo:api.determineCallInfo(data, methodSelector), + } + // Process approval result, err = api.ui.ApproveTx(&req) if err != nil { return nil, err @@ -368,25 +400,33 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Mixedcase } // Log changes made by the UI to the signing-request logDiff(&req, &result) - var ( acc accounts.Account wallet accounts.Wallet ) - acc = accounts.Account{Address: result.From.Address()} + acc = accounts.Account{Address: result.Transaction.From.Address()} wallet, err = api.am.Find(acc) if err != nil { return nil, err } - var tx = toTransaction(&result.Transaction) + // Convert fields into a real transaction + var unsignedTx = result.Transaction.toTransaction() // The one to sign is the one that was returned from the UI - signedTx, err := wallet.SignTxWithPassphrase(acc, result.Password, tx, api.chainID) + signedTx, err := wallet.SignTxWithPassphrase(acc, result.Password, unsignedTx, api.chainID) if err != nil { api.ui.ShowError(err.Error()) return nil, err } - return rlp.EncodeToBytes(signedTx) + + rlpdata, err := rlp.EncodeToBytes(signedTx) + response := ethapi.SignTransactionResult{rlpdata, signedTx} + + // Finally, send the signed tx to the UI + api.ui.OnApprovedTx(response) + // ...and to the external caller + return &response, nil + } // Sign calculates an Ethereum ECDSA signature for: diff --git a/cmd/signer/api_test.go b/cmd/signer/api_test.go index dbf0d6f7da..38f2c56aaf 100644 --- a/cmd/signer/api_test.go +++ b/cmd/signer/api_test.go @@ -17,6 +17,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/internal/ethapi" ) //Used for testing @@ -24,18 +25,22 @@ type HeadlessUI struct { controller chan string } +func (ui *HeadlessUI) OnApprovedTx(tx ethapi.SignTransactionResult) { + fmt.Printf("OnApproved called") +} + func (ui *HeadlessUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) { switch <-ui.controller { case "Y": - return SignTxResponse{request.Transaction, request.From, true, <-ui.controller}, nil + return SignTxResponse{request.Transaction, true, <-ui.controller}, nil case "M": //Modify old := (*big.Int)(request.Transaction.Value) newVal := big.NewInt(0).Add(old, big.NewInt(1)) request.Transaction.Value = (*hexutil.Big)(newVal) - return SignTxResponse{request.Transaction, request.From, true, <-ui.controller}, nil + return SignTxResponse{request.Transaction, true, <-ui.controller}, nil default: - return SignTxResponse{request.Transaction, request.From, false, ""}, nil + return SignTxResponse{request.Transaction, false, ""}, nil } } func (ui *HeadlessUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) { @@ -231,19 +236,21 @@ func TestSignData(t *testing.T) { t.Errorf("Expected 65 byte signature (got %d bytes)", len(h)) } } -func mkTestTx() TransactionArg { +func mkTestTx(from common.MixedcaseAddress) SendTxArgs { to := common.NewMixedcaseAddress(common.HexToAddress("0x1337")) gas := (*hexutil.Big)(big.NewInt(21000)) gasPrice := (*hexutil.Big)(big.NewInt(2000000000)) value := (*hexutil.Big)(big.NewInt(1e18)) nonce := (hexutil.Uint64)(0) - tx := TransactionArg{ - &to, - gas, - gasPrice, - value, - common.Hex2Bytes("01020304050607080a"), - &nonce} + data := hexutil.Bytes(common.Hex2Bytes("01020304050607080a")) + tx := SendTxArgs{ + From:from, + To: &to, + Gas: gas, + GasPrice: gasPrice, + Value: value, + Data: &data, + Nonce: &nonce} return tx } @@ -251,8 +258,7 @@ func TestSignTx(t *testing.T) { var ( list Accounts - h []byte - h2 []byte + res, res2 *ethapi.SignTransactionResult err error ) @@ -266,22 +272,22 @@ func TestSignTx(t *testing.T) { a := common.NewMixedcaseAddress(list[0].Address) methodSig := "test(uint)" - tx := mkTestTx() + tx := mkTestTx(a) control <- "Y" control <- "wrongpassword" - h, err = api.SignTransaction(context.Background(), a, tx, &methodSig) - if h != nil { - t.Errorf("Expected nil-data, got %h", h) + res, err = api.SignTransaction(context.Background(), tx, &methodSig) + if res != nil { + t.Errorf("Expected nil-response, got %v", res) } if err != keystore.ErrDecrypt { t.Errorf("Expected ErrLocked! %v", err) } control <- "No way" - h, err = api.SignTransaction(context.Background(), a, tx, &methodSig) - if h != nil { - t.Errorf("Expected nil-data, got %h", h) + res, err = api.SignTransaction(context.Background(), tx, &methodSig) + if res != nil { + t.Errorf("Expected nil-response, got %v", res) } if err != ErrRequestDenied { t.Errorf("Expected ErrRequestDenied! %v", err) @@ -289,13 +295,13 @@ func TestSignTx(t *testing.T) { control <- "Y" control <- "apassword" - h, err = api.SignTransaction(context.Background(), a, tx, &methodSig) + res, err = api.SignTransaction(context.Background(), tx, &methodSig) if err != nil { t.Fatal(err) } parsedTx := &types.Transaction{} - rlp.Decode(bytes.NewReader(h), parsedTx) + rlp.Decode(bytes.NewReader(res.Raw), parsedTx) //The tx should NOT be modified by the UI if parsedTx.Value().Cmp(tx.Value.ToInt()) != 0 { t.Errorf("Expected value to be unchanged, expected %v got %v", tx.Value, parsedTx.Value()) @@ -303,11 +309,11 @@ func TestSignTx(t *testing.T) { control <- "Y" control <- "apassword" - h2, err = api.SignTransaction(context.Background(), a, tx, &methodSig) + res2, err = api.SignTransaction(context.Background(), tx, &methodSig) if err != nil { t.Fatal(err) } - if !bytes.Equal(h, h2) { + if !bytes.Equal(res.Raw, res2.Raw) { t.Error("Expected tx to be unmodified by UI") } @@ -315,19 +321,19 @@ func TestSignTx(t *testing.T) { control <- "M" control <- "apassword" - h2, err = api.SignTransaction(context.Background(), a, tx, &methodSig) + res2, err = api.SignTransaction(context.Background(), tx, &methodSig) if err != nil { t.Fatal(err) } parsedTx2 := &types.Transaction{} - rlp.Decode(bytes.NewReader(h), parsedTx2) - //The tx should NOT be modified by the UI + rlp.Decode(bytes.NewReader(res.Raw), parsedTx2) + //The tx should be modified by the UI if parsedTx2.Value().Cmp(tx.Value.ToInt()) != 0 { - t.Errorf("Expected value to be changed, got %v", parsedTx.Value()) + t.Errorf("Expected value to be unchanged, got %v", parsedTx.Value()) } - if bytes.Equal(h, h2) { + if bytes.Equal(res.Raw, res2.Raw) { t.Error("Expected tx to be modified by UI") } diff --git a/cmd/signer/auditlog.go b/cmd/signer/auditlog.go index d47f27b71b..007faf45ef 100644 --- a/cmd/signer/auditlog.go +++ b/cmd/signer/auditlog.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/internal/ethapi" ) type AuditLogger struct { @@ -41,15 +42,17 @@ func (l *AuditLogger) New(ctx context.Context) (accounts.Account, error) { return l.api.New(ctx) } -func (l *AuditLogger) SignTransaction(ctx context.Context, from common.MixedcaseAddress, args TransactionArg, methodSelector *string) (hexutil.Bytes, error) { +func (l *AuditLogger) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) { l.log.Info("SignTransaction", "type", "request", "metadata", MetadataFromContext(ctx).String(), - "from", from.String(), "tx", args.String(), + "tx", args.String(), "methodSelector", methodSelector) - b, e := l.api.SignTransaction(ctx, from, args, methodSelector) - - l.log.Info("SignTransaction", "type", "response", "data", common.Bytes2Hex(b), "error", e) - - return b, e + res, e := l.api.SignTransaction(ctx, args, methodSelector) + if res != nil{ + l.log.Info("SignTransaction", "type", "response", "data", common.Bytes2Hex(res.Raw), "error", e) + }else{ + l.log.Info("SignTransaction", "type", "response", "data", res, "error", e) + } + return res, e } func (l *AuditLogger) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) { diff --git a/cmd/signer/cliui.go b/cmd/signer/cliui.go index b74e0f702a..7222744f8f 100644 --- a/cmd/signer/cliui.go +++ b/cmd/signer/cliui.go @@ -24,6 +24,7 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/log" "golang.org/x/crypto/ssh/terminal" ) @@ -108,10 +109,13 @@ func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, erro } else { fmt.Printf("to: \n") } - fmt.Printf("from: %v\n", request.From.String()) + fmt.Printf("from: %v\n", request.Transaction.From.String()) fmt.Printf("value: %v wei\n", weival) - if len(request.Transaction.Data) > 0 { - fmt.Printf("data: %v\n", common.Bytes2Hex(request.Transaction.Data)) + if request.Transaction.Data != nil{ + d := *request.Transaction.Data + if len(d) > 0 { + fmt.Printf("data: %v\n", common.Bytes2Hex(d)) + } } if request.Callinfo != "" { fmt.Printf("\nNote: This Transaction contains data. Review abi-decoding info below:") @@ -122,9 +126,9 @@ func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, erro showMetadata(request.Meta) fmt.Printf("-------------------------------------------\n") if !ui.confirm() { - return SignTxResponse{request.Transaction, request.From, false, ""}, nil + return SignTxResponse{request.Transaction, false, ""}, nil } - return SignTxResponse{request.Transaction, request.From, true, ui.readPassword()}, nil + return SignTxResponse{request.Transaction, true, ui.readPassword()}, nil } // ApproveSignData prompt the user for confirmation to request to sign data @@ -222,6 +226,9 @@ func (ui *CommandlineUI) ShowError(message string) { // ShowInfo displays info message to user func (ui *CommandlineUI) ShowInfo(message string) { - fmt.Printf("Info: %v\n", message) } + +func (ui *CommandlineUI) OnApprovedTx(tx ethapi.SignTransactionResult) { + fmt.Printf("Transaction signed: %v", tx.Tx.String()) +} diff --git a/cmd/signer/main.go b/cmd/signer/main.go index 897bd38191..398d1e2197 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -188,7 +188,7 @@ func testExternalUI(api *SignerAPI) { } var err error - _, err = api.SignTransaction(ctx, common.MixedcaseAddress{}, TransactionArg{}, nil) + _, err = api.SignTransaction(ctx, SendTxArgs{From:common.MixedcaseAddress{}}, nil) checkErr("SignTransaction", err) _, err = api.Sign(ctx, common.MixedcaseAddress{}, common.Hex2Bytes("01020304")) checkErr("Sign", err) diff --git a/cmd/signer/rules/rules.go b/cmd/signer/rules/rules.go index b658d08f35..65cf467f60 100644 --- a/cmd/signer/rules/rules.go +++ b/cmd/signer/rules/rules.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/cmd/signer" "github.com/ethereum/go-ethereum/cmd/signer/rules/deps" "github.com/ethereum/go-ethereum/cmd/signer/storage" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/log" "github.com/robertkrimen/otto" "os" @@ -84,7 +85,7 @@ func (r *rulesetUi) checkApproval(jsfunc string, jsarg []byte, err error) error if err != nil { return err } - v, err := r.vm.Call("ApproveTx", nil, string(jsarg)) + v, err := r.vm.Call(jsfunc, nil, string(jsarg)) if err != nil { log.Info("error occurred during execution", "error", err) @@ -106,7 +107,7 @@ func (r *rulesetUi) checkApproval(jsfunc string, jsarg []byte, err error) error func (r *rulesetUi) ApproveTx(request *signer.SignTxRequest) (signer.SignTxResponse, error) { jsonreq, err := json.Marshal(request) if err = r.checkApproval("ApproveTx", jsonreq, err); err == nil { - return signer.SignTxResponse{Transaction: request.Transaction, From: request.From, Approved: true, Password: ""}, nil + return signer.SignTxResponse{Transaction: request.Transaction, Approved: true, Password: ""}, nil } return signer.SignTxResponse{Approved: false}, err } @@ -156,3 +157,17 @@ func (r *rulesetUi) ShowInfo(message string) { log.Info(message) r.next.ShowInfo(message) } +func (r *rulesetUi) OnApprovedTx(tx ethapi.SignTransactionResult) { + + jsonTx, err := json.Marshal(tx) + if err != nil { + log.Warn("failed marshalling transaction", "tx", tx) + return + } + _, err = r.vm.Call("OnApprovedTx", nil, string(jsonTx)) + if err != nil { + fmt.Printf("Error in onapprove %v", err) + log.Warn("error occurred during execution", "error", err) + } + +} diff --git a/cmd/signer/rules/rules_test.go b/cmd/signer/rules/rules_test.go index 7f2c79706d..c283d6d5d0 100644 --- a/cmd/signer/rules/rules_test.go +++ b/cmd/signer/rules/rules_test.go @@ -6,6 +6,8 @@ import ( "github.com/ethereum/go-ethereum/cmd/signer" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/internal/ethapi" "math/big" "testing" ) @@ -75,7 +77,7 @@ func TestListRequest(t *testing.T) { accs[i] = acc } - js := `function ApproveListing(accounts, meta){ return "Approve" }` + js := `function ApproveListing(){ return "Approve" }` r, err := initRuleEngine(js) if err != nil { @@ -99,12 +101,12 @@ func TestSignTxRequest(t *testing.T) { function ApproveTx(jsonstr){ console.log(jsonstr) r = JSON.parse(jsonstr) - console.log("from", r.from) + console.log("transaction.from", r.transaction.from); console.log("transaction.to", r.transaction.to); console.log("transaction.value", r.transaction.value); console.log("transaction.nonce", r.transaction.nonce); - if(r.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"} - if(r.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"} + if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"} + if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"} }` r, err := initRuleEngine(js) @@ -125,10 +127,11 @@ func TestSignTxRequest(t *testing.T) { } fmt.Printf("to %v", to.Address().String()) resp, err := r.ApproveTx(&signer.SignTxRequest{ - Transaction: signer.TransactionArg{To: to}, - From: *from, - Callinfo: "", - Meta: signer.Metadata{"remoteip", "localip", "inproc"}, + Transaction: signer.SendTxArgs{ + From: *from, + To: to}, + Callinfo: "", + Meta: signer.Metadata{"remoteip", "localip", "inproc"}, }) if err != nil { t.Errorf("Unexpected error %v", err) @@ -243,46 +246,79 @@ const ExampleTxWindow = ` sum = new BigNumber(0) sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum); - console.log("Sum so far", sum); - console.log("Requested", value.toNumber()); + console.log("ApproveTx > Sum so far", sum); + console.log("ApproveTx > Requested", value.toNumber()); + // Would we exceed weekly limit ? - if (sum.plus(value).lt(limit)){ - // Add this to the storage - newtxs.push({tstamp: new Date().getTime(), value: value}); - storage.Put("txs", JSON.stringify(newtxs)); - return true; - } - return false; + return sum.plus(value).lt(limit) } function ApproveTx(jsonstr){ - r = JSON.parse(jsonstr); - console.log("Requested value ", r.transaction.value) - + var r = JSON.parse(jsonstr) if (isLimitOk(r.transaction)){ return "Approve" } return "Nope" } + /** + * OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter + * 'response_str' contains the return value that will be sent to the external caller. + * The return value from this method is ignore - the reason for having this callback is to allow the + * ruleset to keep track of approved transactions. + * + * When implementing rate-limited rules, this callback should be used. + * If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user + * then accepts the transaction, this method will be called. + * + * TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx. + */ + function OnApprovedTx(response_str){ + console.log("OnApprovedTx > called with data\n\t "+response_str) + var resp = JSON.parse(response_str) + var value = big(resp.tx.value) + var txs = [] + // Load stored transactions + var stored = storage.Get('txs'); + if(stored != ""){ + txs = JSON.parse(stored) + } + // Add this to the storage + txs.push({tstamp: new Date().getTime(), value: value}); + storage.Put("txs", JSON.stringify(txs)); + } + ` func dummyTx(value *hexutil.Big) *signer.SignTxRequest { to, _ := mixAddr("000000000000000000000000000000000000dead") from, _ := mixAddr("000000000000000000000000000000000000dead") + n := hexutil.Uint64(3) + gas := hexutil.Big(*big.NewInt(21000)) + gasPrice := hexutil.Big(*big.NewInt(2000000)) return &signer.SignTxRequest{ - Transaction: signer.TransactionArg{ - To: to, - Value: value, + Transaction: signer.SendTxArgs{ + From: *from, + To: to, + Value: value, + Nonce: &n, + GasPrice: &gas, + Gas: &gasPrice, }, - From: *from, Callinfo: "Warning, all your base are bellong to us", Meta: signer.Metadata{"remoteip", "localip", "inproc"}, } } +func dummySigned(value *big.Int) *types.Transaction { + to := common.HexToAddress("000000000000000000000000000000000000dead") + gas := big.NewInt(21000) + gasPrice := big.NewInt(2000000) + data := make([]byte, 0) + return types.NewTransaction(3, to, value, gas, gasPrice, data) +} func TestLimitWindow(t *testing.T) { r, err := initRuleEngine(ExampleTxWindow) @@ -299,13 +335,21 @@ func TestLimitWindow(t *testing.T) { h := hexutil.Big(*v) // The first three should succeed for i := 0; i < 3; i++ { - resp, err := r.ApproveTx(dummyTx(&h)) + unsigned := dummyTx(&h) + resp, err := r.ApproveTx(unsigned) if err != nil { t.Errorf("Unexpected error %v", err) } if !resp.Approved { t.Errorf("Expected check to resolve to 'Approve'") } + // Create a dummy signed transaction + + response := ethapi.SignTransactionResult{ + Tx: dummySigned(v), + Raw: common.Hex2Bytes("deadbeef"), + } + r.OnApprovedTx(response) } // Fourth should fail resp, err := r.ApproveTx(dummyTx(&h)) diff --git a/cmd/signer/stdioui.go b/cmd/signer/stdioui.go index 5476c7b8f7..9bc4ca60b2 100644 --- a/cmd/signer/stdioui.go +++ b/cmd/signer/stdioui.go @@ -18,13 +18,12 @@ package signer import ( - "github.com/ethereum/go-ethereum/log" - + "context" "sync" + "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" - - "context" ) type StdIOUI struct { @@ -114,3 +113,9 @@ func (ui *StdIOUI) ShowInfo(message string) { log.Info("Error calling 'ShowInfo'", "exc", err.Error(), "msg", message) } } +func (ui *StdIOUI) OnApprovedTx(tx ethapi.SignTransactionResult) { + err := ui.dispatch("OnApprovedTx", tx, nil) + if err != nil { + log.Info("Error calling 'OnApprovedTx'", "exc", err.Error(), "tx", tx) + } +} diff --git a/cmd/signer/types.go b/cmd/signer/types.go index 825638ef66..b91af6625e 100644 --- a/cmd/signer/types.go +++ b/cmd/signer/types.go @@ -23,6 +23,8 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "math/big" + "github.com/ethereum/go-ethereum/core/types" ) type Accounts []Account @@ -48,7 +50,7 @@ func (a Account) String() string { } return err.Error() } - +/* // TransactionArg represents a Transaction for the signer. type TransactionArg struct { To *common.MixedcaseAddress `json:"to"` @@ -58,11 +60,38 @@ type TransactionArg struct { Data hexutil.Bytes `json:"data"` Nonce *hexutil.Uint64 `json:"nonce"` } +*/ -func (t TransactionArg) String() string { +// SendTxArgs represents the arguments to submit a transaction +type SendTxArgs struct { + From common.MixedcaseAddress `json:"from"` + To *common.MixedcaseAddress `json:"to"` + Gas *hexutil.Big `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Nonce *hexutil.Uint64 `json:"nonce"` + // We accept "data" and "input" for backwards-compatibility reasons. + Data *hexutil.Bytes `json:"data"` + Input *hexutil.Bytes `json:"input"` +} + +func (t SendTxArgs) String() string { s, err := json.Marshal(t) if err == nil { return string(s) } return err.Error() } + +func (args *SendTxArgs) toTransaction() *types.Transaction { + var input []byte + if args.Data != nil { + input = *args.Data + } else if args.Input != nil { + input = *args.Input + } + if args.To == nil { + return types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), input) + } + return types.NewTransaction(uint64(*args.Nonce), (*args.To).Address(), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), input) +}