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:
parent
ee3c05c79b
commit
db786ef1d2
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
Loading…
Reference in New Issue