forked from cerc-io/plugeth
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 {
|
||||||
output.WriteString(fmt.Sprintf("%q\n", nvt.Value))
|
if nvt.Value != nil {
|
||||||
|
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