Sign typed data implementation (#1250)

* Implement EIP 712

* Cover majority of cases with tests

* Add solidity and signer tests and cover integer edges case

* Add thin api to sign type data using selected status account

* All integers are extended to int256 and marshalled into big.Int

* Document how deps works

* Fix linter

* Fix errors test

* Add validation tests

* Unmarshal every atomic type in separate functions
This commit is contained in:
Dmitry Shulyak 2018-11-06 07:26:12 +01:00 committed by GitHub
parent ee3c05c79b
commit db786ef1d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1209 additions and 0 deletions

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"math/big"
"sync"
gethcommon "github.com/ethereum/go-ethereum/common"
@ -22,6 +23,7 @@ import (
"github.com/status-im/status-go/services/rpcfilters"
"github.com/status-im/status-go/services/shhext/chat"
"github.com/status-im/status-go/services/shhext/chat/crypto"
"github.com/status-im/status-go/services/typeddata"
"github.com/status-im/status-go/signal"
"github.com/status-im/status-go/transactions"
)
@ -258,6 +260,20 @@ func (b *StatusBackend) Recover(rpcParams personal.RecoverParams) (gethcommon.Ad
return b.personalAPI.Recover(rpcParams)
}
// SignTypedData accepts data and password. Gets verified account and signs typed data.
func (b *StatusBackend) SignTypedData(typed typeddata.TypedData, password string) (hexutil.Bytes, error) {
account, err := b.getVerifiedAccount(password)
if err != nil {
return hexutil.Bytes{}, err
}
chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID)
sig, err := typeddata.Sign(typed, account.AccountKey.PrivateKey, chain)
if err != nil {
return hexutil.Bytes{}, err
}
return hexutil.Bytes(sig), err
}
func (b *StatusBackend) getVerifiedAccount(password string) (*account.SelectedExtKey, error) {
selectedAccount, err := b.accountManager.SelectedAccount()
if err != nil {

View File

@ -15,6 +15,7 @@ import (
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/profiling"
"github.com/status-im/status-go/services/personal"
"github.com/status-im/status-go/services/typeddata"
"github.com/status-im/status-go/signal"
"github.com/status-im/status-go/transactions"
"gopkg.in/go-playground/validator.v9"
@ -349,6 +350,22 @@ func SendTransaction(txArgsJSON, password *C.char) *C.char {
return C.CString(prepareJSONResponseWithCode(hash.String(), err, code))
}
// SignTypedData unmarshall data into TypedData, validate it and signs with selected account,
// if password matches selected account.
//export SignTypedData
func SignTypedData(data, password *C.char) *C.char {
var typed typeddata.TypedData
err := json.Unmarshal([]byte(C.GoString(data)), &typed)
if err != nil {
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
}
if err := typed.Validate(); err != nil {
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
}
result, err := statusBackend.SignTypedData(typed, C.GoString(password))
return C.CString(prepareJSONResponse(result.String(), err))
}
//StartCPUProfile runs pprof for cpu
//export StartCPUProfile
func StartCPUProfile(dataDir *C.char) *C.char {

View File

@ -0,0 +1,5 @@
EIP712 example smart contract
=============================
example.sol is taken from https://github.com/ethereum/EIPs/blob/master/assets/eip-712/Example.sol
and slightly modified in order to read results from golang bindings.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,98 @@
pragma solidity ^0.4.24;
contract Example {
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}
struct Person {
string name;
address wallet;
}
struct Mail {
Person from;
Person to;
string contents;
}
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 constant PERSON_TYPEHASH = keccak256(
"Person(string name,address wallet)"
);
bytes32 constant MAIL_TYPEHASH = keccak256(
"Mail(Person from,Person to,string contents)Person(string name,address wallet)"
);
Mail internal mail = Mail({
from: Person({
name: "Cow",
wallet: 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826
}),
to: Person({
name: "Bob",
wallet: 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB
}),
contents: "Hello, Bob!"
});
bytes32 public DOMAIN_SEPARATOR;
bytes32 public MAIL;
constructor () public {
DOMAIN_SEPARATOR = hash(EIP712Domain({
name: "Ether Mail",
version: '1',
chainId: 1,
// verifyingContract: this
verifyingContract: 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC
}));
MAIL = hash(mail);
}
function hash(EIP712Domain eip712Domain) internal pure returns (bytes32) {
return keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes(eip712Domain.name)),
keccak256(bytes(eip712Domain.version)),
eip712Domain.chainId,
eip712Domain.verifyingContract
));
}
function hash(Person person) internal pure returns (bytes32) {
return keccak256(abi.encode(
PERSON_TYPEHASH,
keccak256(bytes(person.name)),
person.wallet
));
}
function hash(Mail mail) internal pure returns (bytes32) {
return keccak256(abi.encode(
MAIL_TYPEHASH,
hash(mail.from),
hash(mail.to),
keccak256(bytes(mail.contents))
));
}
function verify(uint8 v, bytes32 r, bytes32 s) public returns (bool) {
// Note: we need to use `encodePacked` here instead of `encode`.
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(mail)
));
require(ecrecover(digest, v, r, s) == msg.sender);
return true;
}
}

180
services/typeddata/hash.go Normal file
View File

@ -0,0 +1,180 @@
package typeddata
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math/big"
"sort"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
var (
bytes32Type, _ = abi.NewType("bytes32")
int256Type, _ = abi.NewType("int256")
)
// deps runs breadth-first traversal starting from target and collects all
// found composite dependencies types into result slice. target always will be first
// in the result array. all other dependencies are sorted alphabetically.
// for example: Z{c C, a A} A{c C} and the target is Z.
// result would be Z, A, B, C
func deps(target string, types Types) []string {
unique := map[string]struct{}{}
unique[target] = struct{}{}
visited := []string{target}
deps := []string{}
for len(visited) > 0 {
current := visited[0]
fields := types[current]
for i := range fields {
f := fields[i]
if _, defined := types[f.Type]; defined {
if _, exist := unique[f.Type]; !exist {
visited = append(visited, f.Type)
unique[f.Type] = struct{}{}
}
}
}
visited = visited[1:]
deps = append(deps, current)
}
sort.Slice(deps[1:], func(i, j int) bool {
return deps[1:][i] < deps[1:][j]
})
return deps
}
func typeString(target string, types Types) string {
b := new(bytes.Buffer)
for _, dep := range deps(target, types) {
b.WriteString(dep)
b.WriteString("(")
fields := types[dep]
first := true
for i := range fields {
if !first {
b.WriteString(",")
} else {
first = false
}
f := fields[i]
b.WriteString(f.Type)
b.WriteString(" ")
b.WriteString(f.Name)
}
b.WriteString(")")
}
return b.String()
}
func typeHash(target string, types Types) (rst common.Hash) {
return crypto.Keccak256Hash([]byte(typeString(target, types)))
}
func hashStruct(target string, data map[string]json.RawMessage, types Types) (rst common.Hash, err error) {
fields := types[target]
typeh := typeHash(target, types)
args := abi.Arguments{{Type: bytes32Type}}
vals := []interface{}{typeh}
for i := range fields {
f := fields[i]
val, typ, err := toABITypeAndValue(f, data, types)
if err != nil {
return rst, err
}
vals = append(vals, val)
args = append(args, abi.Argument{Name: f.Name, Type: typ})
}
packed, err := args.Pack(vals...)
if err != nil {
return rst, err
}
return crypto.Keccak256Hash(packed), nil
}
func toABITypeAndValue(f Field, data map[string]json.RawMessage, types Types) (val interface{}, typ abi.Type, err error) {
if f.Type == "string" {
var str string
if err = json.Unmarshal(data[f.Name], &str); err != nil {
return
}
return crypto.Keccak256Hash([]byte(str)), bytes32Type, nil
} else if f.Type == "bytes" {
var bytes hexutil.Bytes
if err = json.Unmarshal(data[f.Name], &bytes); err != nil {
return
}
return crypto.Keccak256Hash(bytes), bytes32Type, nil
} else if _, exist := types[f.Type]; exist {
var obj map[string]json.RawMessage
if err = json.Unmarshal(data[f.Name], &obj); err != nil {
return
}
val, err = hashStruct(f.Type, obj, types)
if err != nil {
return
}
return val, bytes32Type, nil
}
return atomicType(f, data)
}
func atomicType(f Field, data map[string]json.RawMessage) (val interface{}, typ abi.Type, err error) {
typ, err = abi.NewType(f.Type)
if err != nil {
return
}
if typ.T == abi.SliceTy || typ.T == abi.ArrayTy || typ.T == abi.FunctionTy {
return val, typ, errors.New("arrays, slices and functions are not supported")
} else if typ.T == abi.FixedBytesTy {
return toFixedBytes(f, data[f.Name])
} else if typ.T == abi.AddressTy {
val, err = toAddress(f, data[f.Name])
} else if typ.T == abi.IntTy || typ.T == abi.UintTy {
return toInt(f, data[f.Name])
} else if typ.T == abi.BoolTy {
val, err = toBool(f, data[f.Name])
} else {
err = fmt.Errorf("type %s is not supported", f.Type)
}
return
}
func toFixedBytes(f Field, data json.RawMessage) (rst [32]byte, typ abi.Type, err error) {
var bytes hexutil.Bytes
if err = json.Unmarshal(data, &bytes); err != nil {
return
}
typ = bytes32Type
rst = [32]byte{}
// reduce the length to the advertised size
if len(bytes) > typ.Size {
bytes = bytes[:typ.Size]
}
copy(rst[:], bytes)
return rst, typ, nil
}
func toInt(f Field, data json.RawMessage) (val *big.Int, typ abi.Type, err error) {
var rst big.Int
if err = json.Unmarshal(data, &rst); err != nil {
return
}
return &rst, int256Type, nil
}
func toAddress(f Field, data json.RawMessage) (rst common.Address, err error) {
err = json.Unmarshal(data, &rst)
return
}
func toBool(f Field, data json.RawMessage) (rst bool, err error) {
err = json.Unmarshal(data, &rst)
return
}

View File

@ -0,0 +1,273 @@
package typeddata
import (
"encoding/json"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTypeString(t *testing.T) {
type testCase struct {
description string
typeString string
types Types
target string
}
for _, tc := range []testCase{
{
"WithoutDeps",
"Person(string name,address wallet)",
Types{"Person": []Field{{Name: "name", Type: "string"}, {Name: "wallet", Type: "address"}}},
"Person",
},
{
"SingleDep",
"Mail(Person from,Person to)Person(string name,address wallet)",
Types{
"Person": []Field{{Name: "name", Type: "string"}, {Name: "wallet", Type: "address"}},
"Mail": []Field{{Name: "from", Type: "Person"}, {Name: "to", Type: "Person"}},
},
"Mail",
},
{
"DepsOrdered",
"Z(A a,B b)A(string name)B(string name)",
Types{
"A": []Field{{Name: "name", Type: "string"}},
"B": []Field{{Name: "name", Type: "string"}},
"Z": []Field{{Name: "a", Type: "A"}, {Name: "b", Type: "B"}},
},
"Z",
},
{
"RecursiveDepsIgnored",
"Z(A a)A(Z z)",
Types{
"A": []Field{{Name: "z", Type: "Z"}},
"Z": []Field{{Name: "a", Type: "A"}},
},
"Z",
},
} {
tc := tc
t.Run(tc.description, func(t *testing.T) {
require.Equal(t, tc.typeString, typeString(tc.target, tc.types))
})
}
}
func TestEncodeData(t *testing.T) {
type testCase struct {
description string
message map[string]json.RawMessage
types Types
target string
result func(testCase) common.Hash
}
bytes32, _ := abi.NewType("bytes32")
addr, _ := abi.NewType("address")
boolT, _ := abi.NewType("bool")
for _, tc := range []testCase{
{
"HexAddressConvertedToBytes",
map[string]json.RawMessage{"wallet": json.RawMessage(`"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"`)},
Types{"A": []Field{{Name: "wallet", Type: "address"}}},
"A",
func(tc testCase) common.Hash {
args := abi.Arguments{{Type: bytes32}, {Type: addr}}
typehash := typeHash(tc.target, tc.types)
var data common.Address
assert.NoError(t, json.Unmarshal(tc.message["wallet"], &data))
packed, _ := args.Pack(typehash, data)
return crypto.Keccak256Hash(packed)
},
},
{
"StringHashed",
map[string]json.RawMessage{"name": json.RawMessage(`"AAA"`)},
Types{"A": []Field{{Name: "name", Type: "string"}}},
"A",
func(tc testCase) common.Hash {
args := abi.Arguments{{Type: bytes32}, {Type: bytes32}}
typehash := typeHash(tc.target, tc.types)
var data string
assert.NoError(t, json.Unmarshal(tc.message["name"], &data))
packed, _ := args.Pack(typehash, crypto.Keccak256Hash([]byte(data)))
return crypto.Keccak256Hash(packed)
},
},
{
"BytesHashed",
map[string]json.RawMessage{"name": json.RawMessage(`"0x010203"`)}, // []byte{1,2,3}
Types{"A": []Field{{Name: "name", Type: "bytes"}}},
"A",
func(tc testCase) common.Hash {
args := abi.Arguments{{Type: bytes32}, {Type: bytes32}}
typehash := typeHash(tc.target, tc.types)
var data hexutil.Bytes
assert.NoError(t, json.Unmarshal(tc.message["name"], &data))
packed, _ := args.Pack(typehash, crypto.Keccak256Hash(data))
return crypto.Keccak256Hash(packed)
},
},
{
"FixedBytesAsIs",
map[string]json.RawMessage{"name": json.RawMessage(`"0x010203"`)}, // []byte{1,2,3}
Types{"A": []Field{{Name: "name", Type: "bytes32"}}},
"A",
func(tc testCase) common.Hash {
args := abi.Arguments{{Type: bytes32}, {Type: bytes32}}
typehash := typeHash(tc.target, tc.types)
var data hexutil.Bytes
assert.NoError(t, json.Unmarshal(tc.message["name"], &data))
rst := [32]byte{}
copy(rst[:], data)
packed, _ := args.Pack(typehash, rst)
return crypto.Keccak256Hash(packed)
},
},
{
"BoolAsIs",
map[string]json.RawMessage{"flag": json.RawMessage("true")},
Types{"A": []Field{{Name: "flag", Type: "bool"}}},
"A",
func(tc testCase) common.Hash {
args := abi.Arguments{{Type: bytes32}, {Type: boolT}}
typehash := typeHash(tc.target, tc.types)
var data bool
assert.NoError(t, json.Unmarshal(tc.message["flag"], &data))
packed, _ := args.Pack(typehash, data)
return crypto.Keccak256Hash(packed)
},
},
{
"Int32Uint32AsIs",
map[string]json.RawMessage{"I": json.RawMessage("-10"), "UI": json.RawMessage("10")},
Types{"A": []Field{{Name: "I", Type: "int32"}, {Name: "UI", Type: "uint32"}}},
"A",
func(tc testCase) common.Hash {
args := abi.Arguments{{Type: bytes32}, {Type: int256Type}, {Type: int256Type}}
typehash := typeHash(tc.target, tc.types)
packed, _ := args.Pack(typehash, big.NewInt(-10), big.NewInt(10))
return crypto.Keccak256Hash(packed)
},
},
{
"SignedUnsignedIntegersBiggerThen64",
map[string]json.RawMessage{
"i128": json.RawMessage("1"),
"i256": json.RawMessage("1"),
"ui128": json.RawMessage("1"),
"ui256": json.RawMessage("1"),
},
Types{"A": []Field{
{Name: "i128", Type: "int128"}, {Name: "i256", Type: "int256"},
{Name: "ui128", Type: "uint128"}, {Name: "ui256", Type: "uint256"},
}},
"A",
func(tc testCase) common.Hash {
intBig, _ := abi.NewType("int128")
uintBig, _ := abi.NewType("uint128")
args := abi.Arguments{{Type: bytes32},
{Type: intBig}, {Type: intBig}, {Type: uintBig}, {Type: uintBig}}
typehash := typeHash(tc.target, tc.types)
val := big.NewInt(1)
packed, _ := args.Pack(typehash, val, val, val, val)
return crypto.Keccak256Hash(packed)
},
},
{
"CompositeTypesAreRecursivelyEncoded",
map[string]json.RawMessage{"a": json.RawMessage(`{"name":"AAA"}`)},
Types{"A": []Field{{Name: "name", Type: "string"}}, "Z": []Field{{Name: "a", Type: "A"}}},
"Z",
func(tc testCase) common.Hash {
args := abi.Arguments{{Type: bytes32}, {Type: bytes32}}
zhash := typeHash(tc.target, tc.types)
ahash := typeHash("A", tc.types)
var A map[string]string
assert.NoError(t, json.Unmarshal(tc.message["a"], &A))
apacked, _ := args.Pack(ahash, crypto.Keccak256Hash([]byte(A["name"])))
packed, _ := args.Pack(zhash, crypto.Keccak256Hash(apacked))
return crypto.Keccak256Hash(packed)
},
},
} {
tc := tc
t.Run(tc.description, func(t *testing.T) {
encoded, err := hashStruct(tc.target, tc.message, tc.types)
require.NoError(t, err)
require.Equal(t, tc.result(tc), encoded)
})
}
}
func TestEncodeDataErrors(t *testing.T) {
type testCase struct {
description string
message map[string]json.RawMessage
types Types
target string
}
for _, tc := range []testCase{
{
"FailedUnmxarshalAsAString",
map[string]json.RawMessage{"a": json.RawMessage("1")},
Types{"A": []Field{{Name: "name", Type: "string"}}},
"A",
},
{
"FailedUnmarshalToHexBytesToABytes",
map[string]json.RawMessage{"a": {1, 2, 3}},
Types{"A": []Field{{Name: "name", Type: "bytes"}}},
"A",
},
{
"CompositeTypeIsNotAnObject",
map[string]json.RawMessage{"a": json.RawMessage(`"AAA"`)},
Types{"A": []Field{{Name: "name", Type: "string"}}, "Z": []Field{{Name: "a", Type: "A"}}},
"Z",
},
{
"CompositeTypesFailed",
map[string]json.RawMessage{"a": json.RawMessage(`{"name":10}`)},
Types{"A": []Field{{Name: "name", Type: "string"}}, "Z": []Field{{Name: "a", Type: "A"}}},
"Z",
},
{
"ArraysNotSupported",
map[string]json.RawMessage{"a": json.RawMessage("[1,2]")},
Types{"A": []Field{{Name: "name", Type: "int8[2]"}}},
"A",
},
{
"SlicesNotSupported",
map[string]json.RawMessage{"a": json.RawMessage("[1,2]")},
Types{"A": []Field{{Name: "name", Type: "int[]"}}},
"A",
},
{
"FailedToUnmarshalInteger",
map[string]json.RawMessage{"a": json.RawMessage("x00x")},
Types{"A": []Field{{Name: "name", Type: "uint256"}}},
"A",
},
} {
tc := tc
t.Run(tc.description, func(t *testing.T) {
encoded, err := hashStruct(tc.target, tc.message, tc.types)
require.Error(t, err)
require.Equal(t, common.Hash{}, encoded)
})
}
}

View File

@ -0,0 +1,43 @@
package typeddata
import (
"crypto/ecdsa"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
var (
// x19 to avoid collision with rlp encode. x01 version byte defined in EIP-191
messagePadding = []byte{0x19, 0x01}
)
func encodeData(typed TypedData) (rst common.Hash, err error) {
domainSeparator, err := hashStruct(eip712Domain, typed.Domain, typed.Types)
if err != nil {
return rst, err
}
primary, err := hashStruct(typed.PrimaryType, typed.Message, typed.Types)
if err != nil {
return rst, err
}
return crypto.Keccak256Hash(messagePadding, domainSeparator[:], primary[:]), nil
}
// Sign TypedData with a given private key. Verify that chainId in the typed data matches currently selected chain.
func Sign(typed TypedData, prv *ecdsa.PrivateKey, chain *big.Int) ([]byte, error) {
if err := typed.ValidateChainID(chain); err != nil {
return nil, err
}
hash, err := encodeData(typed)
if err != nil {
return nil, err
}
sig, err := crypto.Sign(hash[:], prv)
if err != nil {
return nil, err
}
sig[64] += 27
return sig, nil
}

View File

@ -0,0 +1,137 @@
package typeddata
import (
"context"
"encoding/json"
"math"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/services/typeddata/eip712example"
"github.com/stretchr/testify/require"
)
var (
fromWallet = `
{
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
}
`
toWallet = `
{
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
}
`
)
func TestChainIDValidation(t *testing.T) {
chain := big.NewInt(10)
type testCase struct {
description string
domain map[string]json.RawMessage
}
for _, tc := range []testCase{
{
"ChainIDMismatch",
map[string]json.RawMessage{chainIDKey: json.RawMessage("1")},
},
{
"ChainIDNotAnInt",
map[string]json.RawMessage{chainIDKey: json.RawMessage(`"aa"`)},
},
{
"NoChainIDKey",
nil,
},
} {
t.Run(tc.description, func(t *testing.T) {
typed := TypedData{Domain: tc.domain}
_, err := Sign(typed, nil, chain)
require.Error(t, err)
})
}
}
func TestInteroparableWithSolidity(t *testing.T) {
key, _ := crypto.GenerateKey()
testaddr := crypto.PubkeyToAddress(key.PublicKey)
genesis := core.GenesisAlloc{
testaddr: {Balance: new(big.Int).SetInt64(math.MaxInt64)},
}
backend := backends.NewSimulatedBackend(genesis, math.MaxInt64)
opts := bind.NewKeyedTransactor(key)
_, _, example, err := eip712example.DeployExample(opts, backend)
require.NoError(t, err)
backend.Commit()
domainSol, err := example.DOMAINSEPARATOR(nil)
require.NoError(t, err)
mailSol, err := example.MAIL(nil)
require.NoError(t, err)
mytypes := Types{
eip712Domain: []Field{
{Name: "name", Type: "string"},
{Name: "version", Type: "string"},
{Name: "chainId", Type: "uint256"},
{Name: "verifyingContract", Type: "address"},
},
"Person": []Field{
{Name: "name", Type: "string"},
{Name: "wallet", Type: "address"},
},
"Mail": []Field{
{Name: "from", Type: "Person"},
{Name: "to", Type: "Person"},
{Name: "contents", Type: "string"},
},
}
domain := map[string]json.RawMessage{
"name": json.RawMessage(`"Ether Mail"`),
"version": json.RawMessage(`"1"`),
"chainId": json.RawMessage("1"),
"verifyingContract": json.RawMessage(`"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"`),
}
msg := map[string]json.RawMessage{
"from": json.RawMessage(fromWallet),
"to": json.RawMessage(toWallet),
"contents": json.RawMessage(`"Hello, Bob!"`),
}
typed := TypedData{
Types: mytypes,
PrimaryType: "Mail",
Domain: domain,
Message: msg,
}
domainHash, err := hashStruct(eip712Domain, typed.Domain, typed.Types)
require.NoError(t, err)
require.Equal(t, domainSol[:], domainHash[:])
mailHash, err := hashStruct(typed.PrimaryType, typed.Message, typed.Types)
require.NoError(t, err)
require.Equal(t, mailSol[:], mailHash[:])
signature, err := Sign(typed, key, big.NewInt(1))
require.NoError(t, err)
require.Len(t, signature, 65)
r := [32]byte{}
copy(r[:], signature[:32])
s := [32]byte{}
copy(s[:], signature[32:64])
v := signature[64]
tx, err := example.Verify(opts, v, r, s)
require.NoError(t, err)
backend.Commit()
receipt, err := bind.WaitMined(context.TODO(), backend, tx)
require.NoError(t, err)
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status)
}

View File

@ -0,0 +1,86 @@
package typeddata
import (
"encoding/json"
"errors"
"fmt"
"math/big"
)
const (
eip712Domain = "EIP712Domain"
chainIDKey = "chainId"
)
// Types define fields for each composite type.
type Types map[string][]Field
// Field stores name and solidity type of the field.
type Field struct {
Name string `json:"name"`
Type string `json:"type"`
}
// Validate checks that both name and type are not empty.
func (f Field) Validate() error {
if len(f.Name) == 0 {
return errors.New("`name` is required")
}
if len(f.Type) == 0 {
return errors.New("`type` is required")
}
return nil
}
// TypedData defines typed data according to eip-712.
type TypedData struct {
Types Types `json:"types"`
PrimaryType string `json:"primaryType"`
Domain map[string]json.RawMessage `json:"domain"`
Message map[string]json.RawMessage `json:"message"`
}
// Validate that required fields are defined.
// This method doesn't check if dependencies of the main type are defined, it will be validated
// when type string is computed.
func (t TypedData) Validate() error {
if _, exist := t.Types[eip712Domain]; !exist {
return fmt.Errorf("`%s` must be in `types`", eip712Domain)
}
if t.PrimaryType == "" {
return errors.New("`primaryType` is required")
}
if _, exist := t.Types[t.PrimaryType]; !exist {
return fmt.Errorf("primary type `%s` not defined in types", t.PrimaryType)
}
if t.Domain == nil {
return errors.New("`domain` is required")
}
if t.Message == nil {
return errors.New("`message` is required")
}
for typ := range t.Types {
fields := t.Types[typ]
for i := range fields {
if err := fields[i].Validate(); err != nil {
return fmt.Errorf("field %d from type `%s` is invalid: %v", i, typ, err)
}
}
}
return nil
}
// ValidateChainID accept chain as big integer and verifies if typed data belongs to the same chain.
func (t TypedData) ValidateChainID(chain *big.Int) error {
if _, exist := t.Domain[chainIDKey]; !exist {
return fmt.Errorf("domain misses chain key %s", chainIDKey)
}
var chainID int64
if err := json.Unmarshal(t.Domain[chainIDKey], &chainID); err != nil {
return err
}
if chainID != chain.Int64() {
return fmt.Errorf("chainId %d doesn't match selected chain %s", chainID, chain)
}
return nil
}

View File

@ -0,0 +1,106 @@
package typeddata
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestUnmarshalFull(t *testing.T) {
data := `
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person"
},
{
"name": "contents",
"type": "string"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": 1,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}
`
var typed TypedData
require.NoError(t, json.Unmarshal([]byte(data), &typed))
}
func TestValidateField(t *testing.T) {
f := Field{}
require.EqualError(t, f.Validate(), "`name` is required")
f.Name = "name"
require.EqualError(t, f.Validate(), "`type` is required")
f.Type = "type"
require.NoError(t, f.Validate())
}
func TestValidateTypedData(t *testing.T) {
d := TypedData{Types: Types{}}
require.EqualError(t, d.Validate(), "`EIP712Domain` must be in `types`")
d.Types[eip712Domain] = []Field{}
require.EqualError(t, d.Validate(), "`primaryType` is required")
d.PrimaryType = "primary"
d.Types[d.PrimaryType] = []Field{}
require.EqualError(t, d.Validate(), "`domain` is required")
d.Domain = map[string]json.RawMessage{}
require.EqualError(t, d.Validate(), "`message` is required")
d.Message = map[string]json.RawMessage{}
require.NoError(t, d.Validate())
d.Types[d.PrimaryType] = append(d.Types[d.PrimaryType], Field{Name: "name"})
require.EqualError(t, d.Validate(), "field 0 from type `primary` is invalid: `type` is required")
d.Types[d.PrimaryType][0].Type = "tttt"
require.NoError(t, d.Validate())
}