accounts, signer: implement gnosis safe support (#21593)
* accounts, signer: implement gnosis safe support * common/math: add type for marshalling big to dec * accounts, signer: properly sign gnosis requests * signer, clef: implement account_signGnosisTx * signer: fix auditlog print, change rpc-name (signGnosisTx to signGnosisSafeTx) * signer: pass validation-messages/warnings to the UI for gnonsis-safe txs * signer/core: minor change to validationmessages of typed data
This commit is contained in:
		
							parent
							
								
									6c8310ebb4
								
							
						
					
					
						commit
						dad26582b6
					
				| @ -10,6 +10,64 @@ TL;DR: Given a version number MAJOR.MINOR.PATCH, increment the: | |||||||
| 
 | 
 | ||||||
| Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. | Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. | ||||||
| 
 | 
 | ||||||
|  | ### 6.1.0 | ||||||
|  | 
 | ||||||
|  | The API-method `account_signGnosisSafeTx` was added. This method takes two parameters,  | ||||||
|  | `[address, safeTx]`. The latter, `safeTx`, can be copy-pasted from the gnosis relay. For example:  | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | { | ||||||
|  |   "jsonrpc": "2.0", | ||||||
|  |   "method": "account_signGnosisSafeTx", | ||||||
|  |   "params": ["0xfd1c4226bfD1c436672092F4eCbfC270145b7256", | ||||||
|  |     { | ||||||
|  |       "safe": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3", | ||||||
|  |       "to": "0xB372a646f7F05Cc1785018dBDA7EBc734a2A20E2", | ||||||
|  |       "value": "20000000000000000", | ||||||
|  |       "data": null, | ||||||
|  |       "operation": 0, | ||||||
|  |       "gasToken": "0x0000000000000000000000000000000000000000", | ||||||
|  |       "safeTxGas": 27845, | ||||||
|  |       "baseGas": 0, | ||||||
|  |       "gasPrice": "0", | ||||||
|  |       "refundReceiver": "0x0000000000000000000000000000000000000000", | ||||||
|  |       "nonce": 2, | ||||||
|  |       "executionDate": null, | ||||||
|  |       "submissionDate": "2020-09-15T21:54:49.617634Z", | ||||||
|  |       "modified": "2020-09-15T21:54:49.617634Z", | ||||||
|  |       "blockNumber": null, | ||||||
|  |       "transactionHash": null, | ||||||
|  |       "safeTxHash": "0x2edfbd5bc113ff18c0631595db32eb17182872d88d9bf8ee4d8c2dd5db6d95e2", | ||||||
|  |       "executor": null, | ||||||
|  |       "isExecuted": false, | ||||||
|  |       "isSuccessful": null, | ||||||
|  |       "ethGasPrice": null, | ||||||
|  |       "gasUsed": null, | ||||||
|  |       "fee": null, | ||||||
|  |       "origin": null, | ||||||
|  |       "dataDecoded": null, | ||||||
|  |       "confirmationsRequired": null, | ||||||
|  |       "confirmations": [ | ||||||
|  |         { | ||||||
|  |           "owner": "0xAd2e180019FCa9e55CADe76E4487F126Fd08DA34", | ||||||
|  |           "submissionDate": "2020-09-15T21:54:49.663299Z", | ||||||
|  |           "transactionHash": null, | ||||||
|  |           "confirmationType": "CONFIRMATION", | ||||||
|  |           "signature": "0x95a7250bb645f831c86defc847350e7faff815b2fb586282568e96cc859e39315876db20a2eed5f7a0412906ec5ab57652a6f645ad4833f345bda059b9da2b821c", | ||||||
|  |           "signatureType": "EOA" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "signatures": null | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "id": 67 | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Not all fields are required, though. This method is really just a UX helper, which massages the  | ||||||
|  | input to conform to the `EIP-712` [specification](https://docs.gnosis.io/safe/docs/contracts_tx_execution/#transaction-hash)  | ||||||
|  | for the Gnosis Safe, and making the output be directly importable to by a relay service.  | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| ### 6.0.0 | ### 6.0.0 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -67,6 +67,40 @@ func (i *HexOrDecimal256) MarshalText() ([]byte, error) { | |||||||
| 	return []byte(fmt.Sprintf("%#x", (*big.Int)(i))), nil | 	return []byte(fmt.Sprintf("%#x", (*big.Int)(i))), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Decimal256 unmarshals big.Int as a decimal string. When unmarshalling,
 | ||||||
|  | // it however accepts either "0x"-prefixed (hex encoded) or non-prefixed (decimal)
 | ||||||
|  | type Decimal256 big.Int | ||||||
|  | 
 | ||||||
|  | // NewHexOrDecimal256 creates a new Decimal256
 | ||||||
|  | func NewDecimal256(x int64) *Decimal256 { | ||||||
|  | 	b := big.NewInt(x) | ||||||
|  | 	d := Decimal256(*b) | ||||||
|  | 	return &d | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UnmarshalText implements encoding.TextUnmarshaler.
 | ||||||
|  | func (i *Decimal256) UnmarshalText(input []byte) error { | ||||||
|  | 	bigint, ok := ParseBig256(string(input)) | ||||||
|  | 	if !ok { | ||||||
|  | 		return fmt.Errorf("invalid hex or decimal integer %q", input) | ||||||
|  | 	} | ||||||
|  | 	*i = Decimal256(*bigint) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarshalText implements encoding.TextMarshaler.
 | ||||||
|  | func (i *Decimal256) MarshalText() ([]byte, error) { | ||||||
|  | 	return []byte(i.String()), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // String implements Stringer.
 | ||||||
|  | func (i *Decimal256) String() string { | ||||||
|  | 	if i == nil { | ||||||
|  | 		return "0" | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprintf("%#d", (*big.Int)(i)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // ParseBig256 parses s as a 256 bit integer in decimal or hexadecimal syntax.
 | // ParseBig256 parses s as a 256 bit integer in decimal or hexadecimal syntax.
 | ||||||
| // Leading zeros are accepted. The empty string parses as zero.
 | // Leading zeros are accepted. The empty string parses as zero.
 | ||||||
| func ParseBig256(s string) (*big.Int, bool) { | func ParseBig256(s string) (*big.Int, bool) { | ||||||
|  | |||||||
| @ -41,7 +41,7 @@ const ( | |||||||
| 	// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
 | 	// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
 | ||||||
| 	numberOfAccountsToDerive = 10 | 	numberOfAccountsToDerive = 10 | ||||||
| 	// ExternalAPIVersion -- see extapi_changelog.md
 | 	// ExternalAPIVersion -- see extapi_changelog.md
 | ||||||
| 	ExternalAPIVersion = "6.0.0" | 	ExternalAPIVersion = "6.1.0" | ||||||
| 	// InternalAPIVersion -- see intapi_changelog.md
 | 	// InternalAPIVersion -- see intapi_changelog.md
 | ||||||
| 	InternalAPIVersion = "7.0.1" | 	InternalAPIVersion = "7.0.1" | ||||||
| ) | ) | ||||||
| @ -62,6 +62,8 @@ type ExternalAPI interface { | |||||||
| 	EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) | 	EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) | ||||||
| 	// Version info about the APIs
 | 	// Version info about the APIs
 | ||||||
| 	Version(ctx context.Context) (string, error) | 	Version(ctx context.Context) (string, error) | ||||||
|  | 	// SignGnosisSafeTransaction signs/confirms a gnosis-safe multisig transaction
 | ||||||
|  | 	SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // UIClientAPI specifies what method a UI needs to implement to be able to be used as a
 | // UIClientAPI specifies what method a UI needs to implement to be able to be used as a
 | ||||||
| @ -234,6 +236,7 @@ type ( | |||||||
| 		Address     common.MixedcaseAddress `json:"address"` | 		Address     common.MixedcaseAddress `json:"address"` | ||||||
| 		Rawdata     []byte                  `json:"raw_data"` | 		Rawdata     []byte                  `json:"raw_data"` | ||||||
| 		Messages    []*NameValueType        `json:"messages"` | 		Messages    []*NameValueType        `json:"messages"` | ||||||
|  | 		Callinfo    []ValidationInfo        `json:"call_info"` | ||||||
| 		Hash        hexutil.Bytes           `json:"hash"` | 		Hash        hexutil.Bytes           `json:"hash"` | ||||||
| 		Meta        Metadata                `json:"meta"` | 		Meta        Metadata                `json:"meta"` | ||||||
| 	} | 	} | ||||||
| @ -581,6 +584,33 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, meth | |||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (api *SignerAPI) SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) { | ||||||
|  | 	// Do the usual validations, but on the last-stage transaction
 | ||||||
|  | 	args := gnosisTx.ArgsForValidation() | ||||||
|  | 	msgs, err := api.validator.ValidateTransaction(methodSelector, args) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	// If we are in 'rejectMode', then reject rather than show the user warnings
 | ||||||
|  | 	if api.rejectMode { | ||||||
|  | 		if err := msgs.getWarnings(); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	typedData := gnosisTx.ToTypedData() | ||||||
|  | 	signature, preimage, err := api.signTypedData(ctx, signerAddress, typedData, msgs) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	checkSummedSender, _ := common.NewMixedcaseAddressFromString(signerAddress.Address().Hex()) | ||||||
|  | 
 | ||||||
|  | 	gnosisTx.Signature = signature | ||||||
|  | 	gnosisTx.SafeTxHash = common.BytesToHash(preimage) | ||||||
|  | 	gnosisTx.Sender = *checkSummedSender // Must be checksumed to be accepted by relay
 | ||||||
|  | 
 | ||||||
|  | 	return &gnosisTx, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Returns the external api version. This method does not require user acceptance. Available methods are
 | // Returns the external api version. This method does not require user acceptance. Available methods are
 | ||||||
| // available via enumeration anyway, and this info does not contain user-specific data
 | // available via enumeration anyway, and this info does not contain user-specific data
 | ||||||
| func (api *SignerAPI) Version(ctx context.Context) (string, error) { | func (api *SignerAPI) Version(ctx context.Context) (string, error) { | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ package core | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
| 
 | 
 | ||||||
| 	"github.com/ethereum/go-ethereum/common" | 	"github.com/ethereum/go-ethereum/common" | ||||||
| 	"github.com/ethereum/go-ethereum/common/hexutil" | 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||||
| @ -61,13 +62,32 @@ func (l *AuditLogger) SignTransaction(ctx context.Context, args SendTxArgs, meth | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (l *AuditLogger) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) { | func (l *AuditLogger) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) { | ||||||
|  | 	marshalledData, _ := json.Marshal(data) // can ignore error, marshalling what we just unmarshalled
 | ||||||
| 	l.log.Info("SignData", "type", "request", "metadata", MetadataFromContext(ctx).String(), | 	l.log.Info("SignData", "type", "request", "metadata", MetadataFromContext(ctx).String(), | ||||||
| 		"addr", addr.String(), "data", data, "content-type", contentType) | 		"addr", addr.String(), "data", marshalledData, "content-type", contentType) | ||||||
| 	b, e := l.api.SignData(ctx, contentType, addr, data) | 	b, e := l.api.SignData(ctx, contentType, addr, data) | ||||||
| 	l.log.Info("SignData", "type", "response", "data", common.Bytes2Hex(b), "error", e) | 	l.log.Info("SignData", "type", "response", "data", common.Bytes2Hex(b), "error", e) | ||||||
| 	return b, e | 	return b, e | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (l *AuditLogger) SignGnosisSafeTx(ctx context.Context, addr common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) { | ||||||
|  | 	sel := "<nil>" | ||||||
|  | 	if methodSelector != nil { | ||||||
|  | 		sel = *methodSelector | ||||||
|  | 	} | ||||||
|  | 	data, _ := json.Marshal(gnosisTx) // can ignore error, marshalling what we just unmarshalled
 | ||||||
|  | 	l.log.Info("SignGnosisSafeTx", "type", "request", "metadata", MetadataFromContext(ctx).String(), | ||||||
|  | 		"addr", addr.String(), "data", string(data), "selector", sel) | ||||||
|  | 	res, e := l.api.SignGnosisSafeTx(ctx, addr, gnosisTx, methodSelector) | ||||||
|  | 	if res != nil { | ||||||
|  | 		data, _ := json.Marshal(res) // can ignore error, marshalling what we just unmarshalled
 | ||||||
|  | 		l.log.Info("SignGnosisSafeTx", "type", "response", "data", string(data), "error", e) | ||||||
|  | 	} else { | ||||||
|  | 		l.log.Info("SignGnosisSafeTx", "type", "response", "data", res, "error", e) | ||||||
|  | 	} | ||||||
|  | 	return res, e | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) { | func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) { | ||||||
| 	l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(), | 	l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(), | ||||||
| 		"addr", addr.String(), "data", data) | 		"addr", addr.String(), "data", data) | ||||||
|  | |||||||
| @ -148,6 +148,13 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp | |||||||
| 
 | 
 | ||||||
| 	fmt.Printf("-------- Sign data request--------------\n") | 	fmt.Printf("-------- Sign data request--------------\n") | ||||||
| 	fmt.Printf("Account:  %s\n", request.Address.String()) | 	fmt.Printf("Account:  %s\n", request.Address.String()) | ||||||
|  | 	if len(request.Callinfo) != 0 { | ||||||
|  | 		fmt.Printf("\nValidation messages:\n") | ||||||
|  | 		for _, m := range request.Callinfo { | ||||||
|  | 			fmt.Printf("  * %s : %s\n", m.Typ, m.Message) | ||||||
|  | 		} | ||||||
|  | 		fmt.Println() | ||||||
|  | 	} | ||||||
| 	fmt.Printf("messages:\n") | 	fmt.Printf("messages:\n") | ||||||
| 	for _, nvt := range request.Messages { | 	for _, nvt := range request.Messages { | ||||||
| 		fmt.Printf("\u00a0\u00a0%v\n", strings.TrimSpace(nvt.Pprint(1))) | 		fmt.Printf("\u00a0\u00a0%v\n", strings.TrimSpace(nvt.Pprint(1))) | ||||||
|  | |||||||
							
								
								
									
										91
									
								
								signer/core/gnosis_safe.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								signer/core/gnosis_safe.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | |||||||
|  | package core | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"math/big" | ||||||
|  | 
 | ||||||
|  | 	"github.com/ethereum/go-ethereum/common" | ||||||
|  | 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||||
|  | 	"github.com/ethereum/go-ethereum/common/math" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // GnosisSafeTx is a type to parse the safe-tx returned by the relayer,
 | ||||||
|  | // it also conforms to the API required by the Gnosis Safe tx relay service.
 | ||||||
|  | // See 'SafeMultisigTransaction' on https://safe-transaction.mainnet.gnosis.io/
 | ||||||
|  | type GnosisSafeTx struct { | ||||||
|  | 	// These fields are only used on output
 | ||||||
|  | 	Signature  hexutil.Bytes           `json:"signature"` | ||||||
|  | 	SafeTxHash common.Hash             `json:"contractTransactionHash"` | ||||||
|  | 	Sender     common.MixedcaseAddress `json:"sender"` | ||||||
|  | 	// These fields are used both on input and output
 | ||||||
|  | 	Safe           common.MixedcaseAddress `json:"safe"` | ||||||
|  | 	To             common.MixedcaseAddress `json:"to"` | ||||||
|  | 	Value          math.Decimal256         `json:"value"` | ||||||
|  | 	GasPrice       math.Decimal256         `json:"gasPrice"` | ||||||
|  | 	Data           *hexutil.Bytes          `json:"data"` | ||||||
|  | 	Operation      uint8                   `json:"operation"` | ||||||
|  | 	GasToken       common.Address          `json:"gasToken"` | ||||||
|  | 	RefundReceiver common.Address          `json:"refundReceiver"` | ||||||
|  | 	BaseGas        big.Int                 `json:"baseGas"` | ||||||
|  | 	SafeTxGas      big.Int                 `json:"safeTxGas"` | ||||||
|  | 	Nonce          big.Int                 `json:"nonce"` | ||||||
|  | 	InputExpHash   common.Hash             `json:"safeTxHash"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ToTypedData converts the tx to a EIP-712 Typed Data structure for signing
 | ||||||
|  | func (tx *GnosisSafeTx) ToTypedData() TypedData { | ||||||
|  | 	var data hexutil.Bytes | ||||||
|  | 	if tx.Data != nil { | ||||||
|  | 		data = *tx.Data | ||||||
|  | 	} | ||||||
|  | 	gnosisTypedData := TypedData{ | ||||||
|  | 		Types: Types{ | ||||||
|  | 			"EIP712Domain": []Type{{Name: "verifyingContract", Type: "address"}}, | ||||||
|  | 			"SafeTx": []Type{ | ||||||
|  | 				{Name: "to", Type: "address"}, | ||||||
|  | 				{Name: "value", Type: "uint256"}, | ||||||
|  | 				{Name: "data", Type: "bytes"}, | ||||||
|  | 				{Name: "operation", Type: "uint8"}, | ||||||
|  | 				{Name: "safeTxGas", Type: "uint256"}, | ||||||
|  | 				{Name: "baseGas", Type: "uint256"}, | ||||||
|  | 				{Name: "gasPrice", Type: "uint256"}, | ||||||
|  | 				{Name: "gasToken", Type: "address"}, | ||||||
|  | 				{Name: "refundReceiver", Type: "address"}, | ||||||
|  | 				{Name: "nonce", Type: "uint256"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		Domain: TypedDataDomain{ | ||||||
|  | 			VerifyingContract: tx.Safe.Address().Hex(), | ||||||
|  | 		}, | ||||||
|  | 		PrimaryType: "SafeTx", | ||||||
|  | 		Message: TypedDataMessage{ | ||||||
|  | 			"to":             tx.To.Address().Hex(), | ||||||
|  | 			"value":          tx.Value.String(), | ||||||
|  | 			"data":           data, | ||||||
|  | 			"operation":      fmt.Sprintf("%d", tx.Operation), | ||||||
|  | 			"safeTxGas":      fmt.Sprintf("%#d", &tx.SafeTxGas), | ||||||
|  | 			"baseGas":        fmt.Sprintf("%#d", &tx.BaseGas), | ||||||
|  | 			"gasPrice":       tx.GasPrice.String(), | ||||||
|  | 			"gasToken":       tx.GasToken.Hex(), | ||||||
|  | 			"refundReceiver": tx.RefundReceiver.Hex(), | ||||||
|  | 			"nonce":          fmt.Sprintf("%d", tx.Nonce.Uint64()), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	return gnosisTypedData | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ArgsForValidation returns a SendTxArgs struct, which can be used for the
 | ||||||
|  | // common validations, e.g. look up 4byte destinations
 | ||||||
|  | func (tx *GnosisSafeTx) ArgsForValidation() *SendTxArgs { | ||||||
|  | 	args := &SendTxArgs{ | ||||||
|  | 		From:     tx.Safe, | ||||||
|  | 		To:       &tx.To, | ||||||
|  | 		Gas:      hexutil.Uint64(tx.SafeTxGas.Uint64()), | ||||||
|  | 		GasPrice: hexutil.Big(tx.GasPrice), | ||||||
|  | 		Value:    hexutil.Big(tx.Value), | ||||||
|  | 		Nonce:    hexutil.Uint64(tx.Nonce.Uint64()), | ||||||
|  | 		Data:     tx.Data, | ||||||
|  | 		Input:    nil, | ||||||
|  | 	} | ||||||
|  | 	return args | ||||||
|  | } | ||||||
| @ -125,7 +125,7 @@ var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`) | |||||||
| //
 | //
 | ||||||
| // Note, the produced signature conforms to the secp256k1 curve R, S and V values,
 | // Note, the produced signature conforms to the secp256k1 curve R, S and V values,
 | ||||||
| // where the V value will be 27 or 28 for legacy reasons, if legacyV==true.
 | // where the V value will be 27 or 28 for legacy reasons, if legacyV==true.
 | ||||||
| func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) { | func (api *SignerAPI) sign(req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) { | ||||||
| 	// We make the request prior to looking up if we actually have the account, to prevent
 | 	// We make the request prior to looking up if we actually have the account, to prevent
 | ||||||
| 	// account-enumeration via the API
 | 	// account-enumeration via the API
 | ||||||
| 	res, err := api.UI.ApproveSignData(req) | 	res, err := api.UI.ApproveSignData(req) | ||||||
| @ -136,7 +136,7 @@ func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, l | |||||||
| 		return nil, ErrRequestDenied | 		return nil, ErrRequestDenied | ||||||
| 	} | 	} | ||||||
| 	// Look up the wallet containing the requested signer
 | 	// Look up the wallet containing the requested signer
 | ||||||
| 	account := accounts.Account{Address: addr.Address()} | 	account := accounts.Account{Address: req.Address.Address()} | ||||||
| 	wallet, err := api.am.Find(account) | 	wallet, err := api.am.Find(account) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @ -167,7 +167,7 @@ func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr com | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	signature, err := api.sign(addr, req, transformV) | 	signature, err := api.sign(req, transformV) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		api.UI.ShowError(err.Error()) | 		api.UI.ShowError(err.Error()) | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @ -312,28 +312,47 @@ func cliqueHeaderHashAndRlp(header *types.Header) (hash, rlp []byte, err error) | |||||||
| 
 | 
 | ||||||
| // SignTypedData signs EIP-712 conformant typed data
 | // SignTypedData signs EIP-712 conformant typed data
 | ||||||
| // hash = keccak256("\x19${byteVersion}${domainSeparator}${hashStruct(message)}")
 | // hash = keccak256("\x19${byteVersion}${domainSeparator}${hashStruct(message)}")
 | ||||||
|  | // It returns
 | ||||||
|  | // - the signature,
 | ||||||
|  | // - and/or any error
 | ||||||
| func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData TypedData) (hexutil.Bytes, error) { | func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData TypedData) (hexutil.Bytes, error) { | ||||||
|  | 	signature, _, err := api.signTypedData(ctx, addr, typedData, nil) | ||||||
|  | 	return signature, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // signTypedData is identical to the capitalized version, except that it also returns the hash (preimage)
 | ||||||
|  | // - the signature preimage (hash)
 | ||||||
|  | func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAddress, | ||||||
|  | 	typedData TypedData, validationMessages *ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) { | ||||||
| 	domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) | 	domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, nil, err | ||||||
| 	} | 	} | ||||||
| 	typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) | 	typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, nil, err | ||||||
| 	} | 	} | ||||||
| 	rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) | 	rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) | ||||||
| 	sighash := crypto.Keccak256(rawData) | 	sighash := crypto.Keccak256(rawData) | ||||||
| 	messages, err := typedData.Format() | 	messages, err := typedData.Format() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, nil, err | ||||||
| 	} | 	} | ||||||
| 	req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Messages: messages, Hash: sighash} | 	req := &SignDataRequest{ | ||||||
| 	signature, err := api.sign(addr, req, true) | 		ContentType: DataTyped.Mime, | ||||||
|  | 		Rawdata:     rawData, | ||||||
|  | 		Messages:    messages, | ||||||
|  | 		Hash:        sighash, | ||||||
|  | 		Address:     addr} | ||||||
|  | 	if validationMessages != nil { | ||||||
|  | 		req.Callinfo = validationMessages.Messages | ||||||
|  | 	} | ||||||
|  | 	signature, err := api.sign(req, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		api.UI.ShowError(err.Error()) | 		api.UI.ShowError(err.Error()) | ||||||
| 		return nil, err | 		return nil, nil, err | ||||||
| 	} | 	} | ||||||
| 	return signature, nil | 	return signature, sighash, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // HashStruct generates a keccak256 hash of the encoding of the provided data
 | // HashStruct generates a keccak256 hash of the encoding of the provided data
 | ||||||
| @ -420,8 +439,8 @@ func (typedData *TypedData) EncodeData(primaryType string, data map[string]inter | |||||||
| 	buffer := bytes.Buffer{} | 	buffer := bytes.Buffer{} | ||||||
| 
 | 
 | ||||||
| 	// Verify extra data
 | 	// Verify extra data
 | ||||||
| 	if len(typedData.Types[primaryType]) < len(data) { | 	if exp, got := len(typedData.Types[primaryType]), len(data); exp < got { | ||||||
| 		return nil, errors.New("there is extra data provided in the message") | 		return nil, fmt.Errorf("there is extra data provided in the message (%d < %d)", exp, got) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Add typehash
 | 	// Add typehash
 | ||||||
| @ -834,7 +853,11 @@ func (nvt *NameValueType) Pprint(depth int) string { | |||||||
| 			output.WriteString(sublevel) | 			output.WriteString(sublevel) | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
|  | 		if nvt.Value != nil { | ||||||
| 			output.WriteString(fmt.Sprintf("%q\n", nvt.Value)) | 			output.WriteString(fmt.Sprintf("%q\n", nvt.Value)) | ||||||
|  | 		} else { | ||||||
|  | 			output.WriteString("\n") | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	return output.String() | 	return output.String() | ||||||
| } | } | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ | |||||||
| package core_test | package core_test | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| @ -414,3 +415,119 @@ func TestFuzzerFiles(t *testing.T) { | |||||||
| 		typedData.Format() | 		typedData.Format() | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | var gnosisTypedData = ` | ||||||
|  | { | ||||||
|  | 	"types": { | ||||||
|  | 		"EIP712Domain": [ | ||||||
|  | 			{ "type": "address", "name": "verifyingContract" } | ||||||
|  | 		], | ||||||
|  | 		"SafeTx": [ | ||||||
|  | 			{ "type": "address", "name": "to" }, | ||||||
|  | 			{ "type": "uint256", "name": "value" }, | ||||||
|  | 			{ "type": "bytes", "name": "data" }, | ||||||
|  | 			{ "type": "uint8", "name": "operation" }, | ||||||
|  | 			{ "type": "uint256", "name": "safeTxGas" }, | ||||||
|  | 			{ "type": "uint256", "name": "baseGas" }, | ||||||
|  | 			{ "type": "uint256", "name": "gasPrice" }, | ||||||
|  | 			{ "type": "address", "name": "gasToken" }, | ||||||
|  | 			{ "type": "address", "name": "refundReceiver" }, | ||||||
|  | 			{ "type": "uint256", "name": "nonce" } | ||||||
|  | 		] | ||||||
|  | 	}, | ||||||
|  | 	"domain": { | ||||||
|  | 		"verifyingContract": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3" | ||||||
|  | 	}, | ||||||
|  | 	"primaryType": "SafeTx", | ||||||
|  | 	"message": { | ||||||
|  | 		"to": "0x9eE457023bB3De16D51A003a247BaEaD7fce313D", | ||||||
|  | 		"value": "20000000000000000", | ||||||
|  | 		"data": "0x", | ||||||
|  | 		"operation": 0, | ||||||
|  | 		"safeTxGas": 27845, | ||||||
|  | 		"baseGas": 0, | ||||||
|  | 		"gasPrice": "0", | ||||||
|  | 		"gasToken": "0x0000000000000000000000000000000000000000", | ||||||
|  | 		"refundReceiver": "0x0000000000000000000000000000000000000000", | ||||||
|  | 		"nonce": 3 | ||||||
|  | 	} | ||||||
|  | }` | ||||||
|  | 
 | ||||||
|  | var gnosisTx = ` | ||||||
|  | { | ||||||
|  |       "safe": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3", | ||||||
|  |       "to": "0x9eE457023bB3De16D51A003a247BaEaD7fce313D", | ||||||
|  |       "value": "20000000000000000", | ||||||
|  |       "data": null, | ||||||
|  |       "operation": 0, | ||||||
|  |       "gasToken": "0x0000000000000000000000000000000000000000", | ||||||
|  |       "safeTxGas": 27845, | ||||||
|  |       "baseGas": 0, | ||||||
|  |       "gasPrice": "0", | ||||||
|  |       "refundReceiver": "0x0000000000000000000000000000000000000000", | ||||||
|  |       "nonce": 3, | ||||||
|  |       "executionDate": null, | ||||||
|  |       "submissionDate": "2020-09-15T21:59:23.815748Z", | ||||||
|  |       "modified": "2020-09-15T21:59:23.815748Z", | ||||||
|  |       "blockNumber": null, | ||||||
|  |       "transactionHash": null, | ||||||
|  |       "safeTxHash": "0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f", | ||||||
|  |       "executor": null, | ||||||
|  |       "isExecuted": false, | ||||||
|  |       "isSuccessful": null, | ||||||
|  |       "ethGasPrice": null, | ||||||
|  |       "gasUsed": null, | ||||||
|  |       "fee": null, | ||||||
|  |       "origin": null, | ||||||
|  |       "dataDecoded": null, | ||||||
|  |       "confirmationsRequired": null, | ||||||
|  |       "confirmations": [ | ||||||
|  |         { | ||||||
|  |           "owner": "0xAd2e180019FCa9e55CADe76E4487F126Fd08DA34", | ||||||
|  |           "submissionDate": "2020-09-15T21:59:28.281243Z", | ||||||
|  |           "transactionHash": null, | ||||||
|  |           "confirmationType": "CONFIRMATION", | ||||||
|  |           "signature": "0x5e562065a0cb15d766dac0cd49eb6d196a41183af302c4ecad45f1a81958d7797753f04424a9b0aa1cb0448e4ec8e189540fbcdda7530ef9b9d95dfc2d36cb521b", | ||||||
|  |           "signatureType": "EOA" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "signatures": null | ||||||
|  |     } | ||||||
|  | ` | ||||||
|  | 
 | ||||||
|  | // TestGnosisTypedData tests the scenario where a user submits a full EIP-712
 | ||||||
|  | // struct without using the gnosis-specific endpoint
 | ||||||
|  | func TestGnosisTypedData(t *testing.T) { | ||||||
|  | 	var td core.TypedData | ||||||
|  | 	err := json.Unmarshal([]byte(gnosisTypedData), &td) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("unmarshalling failed '%v'", err) | ||||||
|  | 	} | ||||||
|  | 	_, sighash, err := sign(td) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	expSigHash := common.FromHex("0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f") | ||||||
|  | 	if !bytes.Equal(expSigHash, sighash) { | ||||||
|  | 		t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TestGnosisCustomData tests the scenario where a user submits only the gnosis-safe
 | ||||||
|  | // specific data, and we fill the TypedData struct on our side
 | ||||||
|  | func TestGnosisCustomData(t *testing.T) { | ||||||
|  | 	var tx core.GnosisSafeTx | ||||||
|  | 	err := json.Unmarshal([]byte(gnosisTx), &tx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	var td = tx.ToTypedData() | ||||||
|  | 	_, sighash, err := sign(td) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	expSigHash := common.FromHex("0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f") | ||||||
|  | 	if !bytes.Equal(expSigHash, sighash) { | ||||||
|  | 		t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user