From 5cd73318005e29014d7c75fd6f667e5503175b38 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Wed, 10 Jul 2024 13:31:42 -0500 Subject: [PATCH] Work on JSON --- pkg/types/encoding/eip712.go | 87 ++++++++++++++++++++++++++++------- protocol/signature_eip712.go | 77 ++++++++++++++++++++++--------- protocol/signature_test.go | 85 +++++++++++++++------------------- test/cmd/gen-testdata/main.go | 6 ++- 4 files changed, 165 insertions(+), 90 deletions(-) diff --git a/pkg/types/encoding/eip712.go b/pkg/types/encoding/eip712.go index 662b64a75..93fa88734 100644 --- a/pkg/types/encoding/eip712.go +++ b/pkg/types/encoding/eip712.go @@ -38,13 +38,20 @@ type EIP712Domain struct { ChainId *big.Int `json:"chainId,omitempty" form:"chainId" query:"chainId" validate:"required"` } -var Eip712Domain = EIP712Domain{Name: "Accumulate", Version: "1.0.0", ChainId: big.NewInt(281)} var EIP712DomainMap map[string]interface{} var EIP712DomainHash []byte +var Eip712Domain = EIP712Domain{ + Name: "Accumulate", + Version: "1.0.0", + + // Use a fake domain for the moment + ChainId: big.NewInt(1), + // ChainId: big.NewInt(281), +} type Eip712Encoder struct { hasher func(v interface{}) ([]byte, error) - types func(ret map[string]*TypeDefinition, v interface{}, fieldType string) error + types func(ret map[string][]*TypeField, v interface{}, fieldType string) error } var eip712EncoderMap map[string]Eip712Encoder @@ -141,7 +148,7 @@ func RegisterEnumeratedTypeInterface[T any, R any](op Func[T, R]) { tp, typesMap := mapEnumTypes(op) eip712EncoderMap[tp] = NewEncoder(func(v interface{}) ([]byte, error) { return FromTypedInterfaceToBytes(v, typesMap) - }, func(ret map[string]*TypeDefinition, v interface{}, typeField string) error { + }, func(ret map[string][]*TypeField, v interface{}, typeField string) error { return FromTypedInterfaceToTypes(ret, v, typesMap) }) } @@ -175,7 +182,7 @@ func FromTypedInterfaceToBytes(v interface{}, typesAliasMap map[string]string) ( return keccak256(b), nil } -func FromTypedInterfaceToTypes(ret map[string]*TypeDefinition, v interface{}, typesAliasMap map[string]string) error { +func FromTypedInterfaceToTypes(ret map[string][]*TypeField, v interface{}, typesAliasMap map[string]string) error { //this is a complex structure, so upcast it to an interface map vv, ok := v.(map[string]interface{}) if !ok { @@ -210,11 +217,11 @@ type TypedData struct { Types []TypeField } -func (td *TypeDefinition) types(ret map[string]*TypeDefinition, d interface{}, typeName string) error { +func (td *TypeDefinition) types(ret map[string][]*TypeField, d interface{}, typeName string) error { var err error //define the type structure if ret == nil { - ret = make(map[string]*TypeDefinition) + ret = make(map[string][]*TypeField) } data := d.(map[string]interface{}) @@ -223,8 +230,6 @@ func (td *TypeDefinition) types(ret map[string]*TypeDefinition, d interface{}, t //the stripping shouldn't be necessary, but do it as a precaution strippedType, _ := stripSlice(typeName) - tdr := &TypeDefinition{} - ret[strippedType] = tdr for i, field := range *td.Fields { value, ok := data[field.Name] @@ -233,7 +238,7 @@ func (td *TypeDefinition) types(ret map[string]*TypeDefinition, d interface{}, t } //append the fields - *tdr.Fields = append(*tdr.Fields, (*td.Fields)[i]) + ret[strippedType] = append(ret[strippedType], (*td.Fields)[i]) //breakdown field further if required err = field.types(ret, value, field.Type) @@ -288,7 +293,7 @@ func (td *TypeDefinition) hash(v interface{}, typeName string) ([]byte, error) { return keccak256(append(keccak256(header.Bytes()), body.Bytes()...)), nil } -func (t *TypeField) types(ret map[string]*TypeDefinition, v interface{}, fieldType string) error { +func (t *TypeField) types(ret map[string][]*TypeField, v interface{}, fieldType string) error { if t.encoder.types != nil { //process more complex type return t.encoder.types(ret, v, fieldType) @@ -296,7 +301,7 @@ func (t *TypeField) types(ret map[string]*TypeDefinition, v interface{}, fieldTy return nil } -func NewEncoder[T any](hasher func(T) ([]byte, error), types func(ret map[string]*TypeDefinition, v interface{}, typeField string) error) Eip712Encoder { +func NewEncoder[T any](hasher func(T) ([]byte, error), types func(ret map[string][]*TypeField, v interface{}, typeField string) error) Eip712Encoder { return Eip712Encoder{func(v interface{}) ([]byte, error) { // JSON always decodes numbers as floats if u, ok := v.(float64); ok { @@ -314,8 +319,7 @@ func NewEncoder[T any](hasher func(T) ([]byte, error), types func(ret map[string return nil, fmt.Errorf("eip712 value of type %T does not match type field", v) } return hasher(t) - }, types, - } + }, types} } func NewTypeField(n string, tp string) *TypeField { @@ -385,10 +389,49 @@ func NewTypeField(n string, tp string) *TypeField { return nil, err } return b, nil - }, func(ret map[string]*TypeDefinition, v interface{}, fieldType string) error { - return nil - }, - }, + }, func(ret map[string][]*TypeField, v interface{}, fieldType string) error { + strippedType, slices := stripSlice(tp) + encoder, ok := eip712EncoderMap[strippedType] + if ok { + if encoder.types == nil { + return nil + } + if slices > 0 { + vv, ok := v.([]interface{}) + if !ok { + return fmt.Errorf("eip712 field %s is not of an array of interfaces", n) + } + for _, vvv := range vv { + err := encoder.types(ret, vvv, fieldType) + if err != nil { + return err + } + } + return nil + } + return encoder.types(ret, v, fieldType) + } + + fields, ok := SchemaDictionary[strippedType] + if !ok { + return fmt.Errorf("eip712 field %s", tp) + } + if slices > 0 { + vv, ok := v.([]interface{}) + if !ok { + return fmt.Errorf("eip712 field %s is not of an array of interfaces", n) + } + for _, vvv := range vv { + err := fields.types(ret, vvv, fieldType) + if err != nil { + return err + } + } + return nil + } + + return fields.types(ret, v, fieldType) + }}, } } @@ -444,6 +487,16 @@ func Eip712Hash(v map[string]interface{}, typeName string, td *TypeDefinition) ( return keccak256(append(EIP712DomainHash, messageHash...)), nil } +func Eip712Types(v map[string]any, typeName string, td *TypeDefinition) (map[string][]*TypeField, error) { + ret := map[string][]*TypeField{} + err := td.types(ret, v, typeName) + return ret, err +} + +func Eip712DomainType() *TypeDefinition { + return SchemaDictionary["EIP712Domain"] +} + func FromstringToBytes(s string) ([]byte, error) { return keccak256([]byte(s)), nil } diff --git a/protocol/signature_eip712.go b/protocol/signature_eip712.go index c7f252b7f..eef09f775 100644 --- a/protocol/signature_eip712.go +++ b/protocol/signature_eip712.go @@ -4,6 +4,7 @@ import ( _ "embed" "encoding/json" "fmt" + "strings" "gitlab.com/accumulatenetwork/accumulate/pkg/types/encoding" ) @@ -33,43 +34,64 @@ func NewEip712TransactionDefinition(txn *Transaction) *encoding.TypeDefinition { encoding.NewTypeField("signature", "SignatureMetadata"), } - return &encoding.TypeDefinition{txnSchema} + return &encoding.TypeDefinition{Fields: txnSchema} } -// MarshalEip712 This will create an EIP712 json message needed to submit to a wallet -func MarshalEip712(transaction Transaction) (ret []byte, err error) { +// MarshalEip712 This will create an EIP-712 json message needed to submit to a +// wallet. +func MarshalEip712(txn *Transaction, sig Signature) (ret []byte, err error) { + // Convert the transaction and signature to an EIP-712 message + jtx, err := makeEIP712Message(txn, sig) + if err != nil { + return nil, err + } + + // Construct the wallet RPC call type eip712 struct { - PrimaryType string `json:"primary_type"` - Types []encoding.TypeDefinition - EIP712Domain encoding.EIP712Domain `json:"EIP712Domain"` - Message json.RawMessage `json:"message"` + Types map[string][]*encoding.TypeField `json:"types"` + PrimaryType string `json:"primaryType"` + Domain encoding.EIP712Domain `json:"domain"` + Message any `json:"message"` } e := eip712{} e.PrimaryType = "Transaction" - e.Message, err = transaction.MarshalJSON() - e.EIP712Domain = encoding.Eip712Domain - //go through transaction and build types list - txMap := map[string]interface{}{} - txj, err := transaction.MarshalJSON() - if err != nil { - return nil, err - } + e.Domain = encoding.Eip712Domain - err = json.Unmarshal(txj, &txMap) + // Reformat the message JSON to be compatible with Ethereum + td := NewEip712TransactionDefinition(txn) + formatEIP712Message(jtx, td) + e.Message = jtx + + e.Types, err = encoding.Eip712Types(jtx, "Transaction", td) if err != nil { return nil, err } + e.Types["EIP712Domain"] = *encoding.Eip712DomainType().Fields - //capture types from txMap + return json.Marshal(e) +} - j, err := json.Marshal(e) - if err != nil { - return nil, err +func formatEIP712Message(v map[string]any, td *encoding.TypeDefinition) { + for _, field := range *td.Fields { + fv, ok := v[field.Name] + if !ok { + continue + } + + switch field.Type { + case "bytes", "bytes32": + v[field.Name] = fmt.Sprintf("0x%v", fv) + continue + } + + sch, ok := encoding.SchemaDictionary[strings.TrimPrefix(field.Type, "[]")] + if ok { + formatEIP712Message(fv.(map[string]any), sch) + } } - return j, nil -} // +} -func Eip712Hasher(txn *Transaction, sig Signature) ([]byte, error) { +func makeEIP712Message(txn *Transaction, sig Signature) (map[string]any, error) { var delegators []any var inner *Eip712TypedDataSignature for inner == nil { @@ -109,6 +131,15 @@ func Eip712Hasher(txn *Transaction, sig Signature) ([]byte, error) { } jtx["signature"] = jsig + return jtx, nil +} + +func Eip712Hasher(txn *Transaction, sig Signature) ([]byte, error) { + jtx, err := makeEIP712Message(txn, sig) + if err != nil { + return nil, err + } + h, err := encoding.Eip712Hash(jtx, "Transaction", NewEip712TransactionDefinition(txn)) if err != nil { return nil, err diff --git a/protocol/signature_test.go b/protocol/signature_test.go index 32a3b0a93..7c8f15b0e 100644 --- a/protocol/signature_test.go +++ b/protocol/signature_test.go @@ -492,54 +492,6 @@ func TestTypesFromCerts(t *testing.T) { } func TestEip712TypedDataSignature(t *testing.T) { - tokenTransaction := []byte(`{ - "types": { - "EIP712Domain": [ - { "name": "name", "type": "string" }, - { "name": "version", "type": "string" }, - { "name": "chainId", "type": "uint256" }, - ], - "SendTokens": [ - { "name": "type", "type": "string" }, - { "name": "to", "type": "TokenRecipient[]" } - ], - "TokenRecipient": [ - { "name": "url", "type": "string" }, - { "name": "amount", "type": "uint256" }, - ], - "TransactionHeader": [ - { "name": "principal", "type": "string" }, - { "name": "initiator", "type": "bytes32" }, - ], - "Transaction": [ - { "name": "header", "type": "TransactionHeader" }, - { "name": "body", "type": "SendTokens" }, - ], - }, - "primaryType": "SendTokens", - "domain": { - "name": "Accumulate", - "version": "1.0.0", - "chainId": "281", - }, - "message": { - "header": { - "principal": "acc://adi.acme/ACME", - "initiator": "84e032fba8a5456f631c822a2b2466c18b3fa7804330ab87088ed6e30d690505" - }, - "body": { - "type": "sendTokens", - "to": [ - { - "url": "acc://other.acme/ACME", - "amount": "10000000000" - } - ] - } - } - }`) - _ = tokenTransaction - txn := &Transaction{} err := txn.UnmarshalJSON([]byte(`{ "header": { @@ -606,3 +558,40 @@ func TestEIP712DelegatedKeyPageUpdate(t *testing.T) { // Verify the signature require.True(t, outer.Verify(nil, txn)) } + +func TestEIP712MessageForWallet(t *testing.T) { + txn := &Transaction{} + err := txn.UnmarshalJSON([]byte(`{ + "header": { + "principal": "acc://adi.acme/ACME" + }, + "body": { + "type": "sendTokens", + "to": [{ + "url": "acc://other.acme/ACME", + "amount": "10000000000" + }] + } + }`)) + require.NoError(t, err) + + pub, err := hex.DecodeString("04c4755e0a7a0f7082749bf46cdae4fcddb784e11428446a01478d656f588f94c17d02f3312b43364a0c480d628483c4fb4e3e9f687ac064717d90fdc42cfb6e0e") + require.NoError(t, err) + sig := &Eip712TypedDataSignature{ + PublicKey: pub, + Signer: url.MustParse("acc://adi.acme/book/1"), + SignerVersion: 1, + Timestamp: 1720564975623, + Vote: VoteTypeAccept, + } + txn.Header.Initiator = [32]byte(sig.Metadata().Hash()) + + b, err := MarshalEip712(txn, sig) + require.NoError(t, err) + fmt.Printf("%s\n", b) + + // Result from metamask + sig.Signature, err = hex.DecodeString("d420cddc64babaa548a09a9b05ae4b5cab6ab78fcb715870bd3a794be84b608763f52b044f672e4e2152beb42dcea00b8b5e36a1eecf6aa26ae62436c6e6d70f1b") + require.NoError(t, err) + require.True(t, sig.Verify(nil, txn)) +} diff --git a/test/cmd/gen-testdata/main.go b/test/cmd/gen-testdata/main.go index 988e1181d..b47833b51 100644 --- a/test/cmd/gen-testdata/main.go +++ b/test/cmd/gen-testdata/main.go @@ -523,11 +523,13 @@ func printStructFields(typeName string, t reflect.Type, indent string) *TypedDat parts = strings.Split(fieldTypeName, ".") fieldTypeName = parts[len(parts)-1] - encoding.RegisterTypeDefinitionResolver("KeyPageOperation", func() { + encoding.RegisterTypeDefinitionResolver("KeyPageOperation", func() error { _ = typeFields(&typeEntries, structName, "KeyPageOperation", NewKeyPageOperation) + return nil }) - encoding.RegisterTypeDefinitionResolver("DataEntry", func() { + encoding.RegisterTypeDefinitionResolver("DataEntry", func() error { _ = typeFields(&typeEntries, structName, "DataEntry", NewDataEntry) + return nil }) _ = typeFields(&typeEntries, structName, "KeyPageOperation", NewKeyPageOperation)