From b9c0b482470f48e69c2f87fdc54e7009fcd4e043 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Fri, 12 Jul 2024 14:08:52 -0500 Subject: [PATCH] Add chain ID --- internal/core/execute/v2/block/sig_user.go | 17 +++ pkg/build/adi_account.go | 43 ++++++ pkg/types/address/from.go | 10 ++ pkg/types/encoding/eip712.go | 80 ++++++----- pkg/types/encoding/eip712_test.go | 1 + pkg/types/network/validators.go | 5 + protocol/signature.go | 13 +- protocol/signature_eip712.go | 24 +++- protocol/signature_test.go | 12 +- protocol/signatures.yml | 3 + protocol/types_gen.go | 90 ++++++++---- test/e2e/sig_eip712_test.go | 154 +++++++++++++++++++++ 12 files changed, 382 insertions(+), 70 deletions(-) create mode 100644 test/e2e/sig_eip712_test.go diff --git a/internal/core/execute/v2/block/sig_user.go b/internal/core/execute/v2/block/sig_user.go index 9fd3e4d6d..84055fd27 100644 --- a/internal/core/execute/v2/block/sig_user.go +++ b/internal/core/execute/v2/block/sig_user.go @@ -116,6 +116,12 @@ func (x UserSignature) check(batch *database.Batch, ctx *userSigContext) error { return errors.Unauthenticated.WithFormat("invalid signature") } + // Check the chain ID, if this is an EIP-712 signature + err = x.checkChainID(ctx) + if err != nil { + return errors.UnknownError.Wrap(err) + } + // Check if the signature initiates the transaction err = x.checkInit(ctx) if err != nil { @@ -177,6 +183,17 @@ func (UserSignature) unwrapDelegated(ctx *userSigContext) error { return nil } +func (UserSignature) checkChainID(ctx *userSigContext) error { + sig, ok := ctx.keySig.(*protocol.Eip712TypedDataSignature) + if !ok { + return nil + } + if ctx.GetActiveGlobals().ChainID().Cmp(sig.ChainID) != 0 { + return errors.BadRequest.WithFormat("invalid chain ID: want %d, got %d", ctx.GetActiveGlobals().ChainID(), sig.ChainID) + } + return nil +} + func (UserSignature) checkInit(ctx *userSigContext) error { var initMerkle bool ctx.isInitiator, initMerkle = protocol.SignatureDidInitiate(ctx.signature, ctx.transaction.Header.Initiator[:], nil) diff --git a/pkg/build/adi_account.go b/pkg/build/adi_account.go index 1e4719406..f4c9522e4 100644 --- a/pkg/build/adi_account.go +++ b/pkg/build/adi_account.go @@ -7,9 +7,16 @@ package build import ( + "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" + "encoding/binary" "fmt" + "io" + "math/big" + badrand "math/rand" + btc "github.com/btcsuite/btcd/btcec" "gitlab.com/accumulatenetwork/accumulate/internal/database" "gitlab.com/accumulatenetwork/accumulate/internal/database/record" "gitlab.com/accumulatenetwork/accumulate/pkg/errors" @@ -134,12 +141,48 @@ func (b *KeyPageBuilder) GenerateKey(typ protocol.SignatureType, seedParts ...an addr.Type = typ b.AddKey(addr) return addr + + case protocol.SignatureTypeETH, + protocol.SignatureTypeBTC, + protocol.SignatureTypeBTCLegacy: + sk := ecdsaFromSeed(btc.S256(), seed) + addr := address.FromETHPrivateKey(sk) + addr.Type = typ + b.AddKey(addr) + return addr + default: b.errorf(errors.BadRequest, "generating %v keys is not supported", typ) return nil } } +func ecdsaFromSeed(c elliptic.Curve, seed [32]byte) *ecdsa.PrivateKey { + rand := badrand.New(badrand.NewSource(int64(binary.BigEndian.Uint64(seed[:])))) + + var k *big.Int + for { + N := c.Params().N + b := make([]byte, (N.BitLen()+7)/8) + if _, err := io.ReadFull(rand, b); err != nil { + panic(err) + } + if excess := len(b)*8 - N.BitLen(); excess > 0 { + b[0] >>= excess + } + k = new(big.Int).SetBytes(b) + if k.Sign() != 0 && k.Cmp(N) < 0 { + break + } + } + + priv := new(ecdsa.PrivateKey) + priv.PublicKey.Curve = c + priv.D = k + priv.PublicKey.X, priv.PublicKey.Y = c.ScalarBaseMult(k.Bytes()) + return priv +} + func (b *KeyPageBuilder) SetAcceptThreshold(v uint64) *KeyPageBuilder { return b.Update(func(page *protocol.KeyPage) { page.AcceptThreshold = v diff --git a/pkg/types/address/from.go b/pkg/types/address/from.go index e1106f28a..dde78611d 100644 --- a/pkg/types/address/from.go +++ b/pkg/types/address/from.go @@ -14,6 +14,7 @@ import ( "fmt" btc "github.com/btcsuite/btcd/btcec" + eth "github.com/ethereum/go-ethereum/crypto" "gitlab.com/accumulatenetwork/accumulate/protocol" ) @@ -92,6 +93,15 @@ func FromEcdsaPrivateKey(key *ecdsa.PrivateKey) *PrivateKey { return priv } +func FromETHPrivateKey(key *ecdsa.PrivateKey) *PrivateKey { + priv := new(PrivateKey) + priv.Type = protocol.SignatureTypeEcdsaSha256 + priv.Key = eth.FromECDSA(key) + priv.PublicKey.Type = protocol.SignatureTypeEcdsaSha256 + priv.PublicKey.Key = eth.FromECDSAPub(&key.PublicKey) + return priv +} + func FromPrivateKeyBytes(priv []byte, typ protocol.SignatureType) (*PrivateKey, error) { var pub []byte switch typ { diff --git a/pkg/types/encoding/eip712.go b/pkg/types/encoding/eip712.go index 16f62bb88..350780272 100644 --- a/pkg/types/encoding/eip712.go +++ b/pkg/types/encoding/eip712.go @@ -11,6 +11,7 @@ import ( "encoding/binary" "encoding/hex" "encoding/json" + "errors" "fmt" "math/big" "os" @@ -59,14 +60,6 @@ type EIP712Domain struct { ChainId *big.Int `json:"chainId,omitempty" form:"chainId" query:"chainId" validate:"required"` } -var EIP712DomainValue eipResolvedValue -var EIP712DomainHash []byte -var Eip712Domain = EIP712Domain{ - Name: "Accumulate", - Version: "1.0.0", - ChainId: big.NewInt(281), -} - type EIP712Resolver interface { Resolve(any) (eipResolvedValue, error) } @@ -80,6 +73,15 @@ type eipResolvedValue interface { var eip712EncoderMap = map[string]EIP712Resolver{} var schemaDictionary = map[string]*TypeDefinition{} +var eip712DomainTypeDef = &TypeDefinition{ + Name: "EIP712Domain", + Fields: &[]*TypeField{ + NewTypeField("name", "string"), + NewTypeField("version", "string"), + NewTypeField("chainId", "uint256"), + }, +} + func init() { eip712EncoderMap["bool"] = newAtomicEncoder("bool", FromboolToBytes) eip712EncoderMap["bytes"] = newAtomicEncoder("bytes", FrombytesToBytes) @@ -91,24 +93,6 @@ func init() { eip712EncoderMap["uint256"] = newAtomicEncoder("uint256", Fromuint256ToBytes) eip712EncoderMap["float64"] = newAtomicEncoder("float64", FromfloatToBytes) eip712EncoderMap["float"] = newAtomicEncoder("float", FromfloatToBytes) //Note = Float is not a valid type in EIP-712, so it is converted to a string - - // Handle EIP712 domain initialization - var jdomain map[string]interface{} - j := must2(Eip712Domain.MarshalJSON()) - must(json.Unmarshal(j, &jdomain)) - - const eipDomainKey = "EIP712Domain" - RegisterTypeDefinition(&[]*TypeField{ - NewTypeField("name", "string"), - NewTypeField("version", "string"), - NewTypeField("chainId", "uint256"), - }, eipDomainKey) - - td := schemaDictionary[eipDomainKey] - EIP712DomainValue = must2(td.Resolve(jdomain)) - EIP712DomainHash = must2(EIP712DomainValue.Hash(map[string][]*TypeField{ - eipDomainKey: *td.Fields, - })) } func must(err error) { @@ -582,34 +566,62 @@ type EIP712Call struct { Message eipResolvedValue `json:"message"` } -func NewEIP712Call(value any, typ EIP712Resolver) (*EIP712Call, error) { +func NewEIP712Call(value any, chainID *big.Int, typ EIP712Resolver) (*EIP712Call, error) { r, err := typ.Resolve(value) if err != nil { return nil, err } - e := EIP712Call{} - e.PrimaryType = "Transaction" - e.Domain = Eip712Domain - e.Message = r + e := EIP712Call{ + PrimaryType: "Transaction", + Message: r, + Domain: EIP712Domain{ + Name: "Accumulate", + Version: "1.0.0", + ChainId: chainID, + }, + Types: map[string][]*TypeField{ + eip712DomainTypeDef.Name: *eip712DomainTypeDef.Fields, + }, + } - e.Types = map[string][]*TypeField{} r.Types(e.Types) - EIP712DomainValue.Types(e.Types) return &e, nil } func (c *EIP712Call) Hash() ([]byte, error) { + if c.Domain.ChainId == nil { + return nil, errors.New("missing chain ID") + } + messageHash, err := c.Message.Hash(c.Types) if err != nil { return nil, err } + var jdomain map[string]any + b, err := c.Domain.MarshalJSON() + if err != nil { + return nil, err + } + err = json.Unmarshal(b, &jdomain) + if err != nil { + return nil, err + } + domain, err := eip712DomainTypeDef.Resolve(jdomain) + if err != nil { + return nil, err + } + domainHash, err := domain.Hash(c.Types) + if err != nil { + return nil, err + } + var buf bytes.Buffer buf.WriteByte(0x19) buf.WriteByte(0x01) - buf.Write(EIP712DomainHash) + buf.Write(domainHash) buf.Write(messageHash) return keccak256(buf.Bytes()), nil } diff --git a/pkg/types/encoding/eip712_test.go b/pkg/types/encoding/eip712_test.go index e4ae9c67d..7984e1636 100644 --- a/pkg/types/encoding/eip712_test.go +++ b/pkg/types/encoding/eip712_test.go @@ -47,6 +47,7 @@ func TestEIP712Arrays(t *testing.T) { t.Run(hex.EncodeToString(txn.GetHash()), func(t *testing.T) { priv := acctesting.NewSECP256K1(t.Name()) sig := &protocol.Eip712TypedDataSignature{ + ChainID: protocol.EthChainID("Kermit"), PublicKey: eth.FromECDSAPub(&priv.PublicKey), Signer: url.MustParse("acc://adi.acme/book/1"), SignerVersion: 1, diff --git a/pkg/types/network/validators.go b/pkg/types/network/validators.go index 479b6084f..8fdc0c689 100644 --- a/pkg/types/network/validators.go +++ b/pkg/types/network/validators.go @@ -9,6 +9,7 @@ package network import ( "fmt" "math" + "math/big" "strings" "github.com/robfig/cron/v3" @@ -17,6 +18,10 @@ import ( var CronFormat = cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) +func (g *GlobalValues) ChainID() *big.Int { + return protocol.EthChainID(g.Network.NetworkName) +} + type globalValueMemos struct { bvns []string threshold map[string]uint64 diff --git a/protocol/signature.go b/protocol/signature.go index acc9c22bc..adfad5a6f 100644 --- a/protocol/signature.go +++ b/protocol/signature.go @@ -1194,12 +1194,6 @@ func (e *EcdsaSha256Signature) Verify(sig Signature, msg Signable) bool { * privateKey must be ecdsa */ func SignEip712TypedData(sig *Eip712TypedDataSignature, privateKey []byte, outer Signature, txn *Transaction) error { - priv, err := eth.ToECDSA(privateKey) - if err != nil { - return err - } - sig.PublicKey = eth.FromECDSAPub(&priv.PublicKey) - if outer == nil { outer = sig } @@ -1208,6 +1202,11 @@ func SignEip712TypedData(sig *Eip712TypedDataSignature, privateKey []byte, outer return err } + priv, err := eth.ToECDSA(privateKey) + if err != nil { + return err + } + sig.TransactionHash = txn.Hash() sig.Signature, err = eth.Sign(hash, priv) return err @@ -1226,7 +1225,7 @@ func (s *Eip712TypedDataSignature) GetSignerVersion() uint64 { return s.SignerVe func (s *Eip712TypedDataSignature) GetTimestamp() uint64 { return s.Timestamp } // GetPublicKeyHash returns the hash of PublicKey. -func (s *Eip712TypedDataSignature) GetPublicKeyHash() []byte { return doSha256(s.PublicKey) } +func (s *Eip712TypedDataSignature) GetPublicKeyHash() []byte { return ETHhash(s.PublicKey) } // GetPublicKey returns PublicKey. func (s *Eip712TypedDataSignature) GetPublicKey() []byte { return s.PublicKey } diff --git a/protocol/signature_eip712.go b/protocol/signature_eip712.go index c8a323cf0..d7a5e226a 100644 --- a/protocol/signature_eip712.go +++ b/protocol/signature_eip712.go @@ -7,16 +7,38 @@ package protocol import ( + "crypto/sha256" _ "embed" + "encoding/binary" "encoding/json" "fmt" + "math/big" "reflect" + "strings" "gitlab.com/accumulatenetwork/accumulate/pkg/types/encoding" ) var eip712Transaction *encoding.TypeDefinition +// EthChainID returns the Ethereum chain ID for an Accumulate network name. +func EthChainID(name string) *big.Int { + // This exists purely for documentation purposes + const MetaMask_MaxSafeChainID = 0xFFFFFFFFFFFEC + + if strings.EqualFold(name, "mainnet") { + return big.NewInt(281) // 0x119 + } + + // Use the last 2 bytes of the hash + name = strings.ToLower(name) + hash := sha256.Sum256([]byte(name)) + id := binary.BigEndian.Uint16(hash[len(hash)-2:]) + + // This will generate something like 0xBEEF0119 + return big.NewInt(281 | int64(id)<<16) +} + func init() { //need to handle the edge cases for Key page operations and data entries encoding.RegisterUnion(NewKeyPageOperation) @@ -118,5 +140,5 @@ func newEIP712Call(txn *Transaction, sig Signature) (*encoding.EIP712Call, error delete(body.(map[string]any), "type") jtx[txn.Body.Type().String()] = body - return encoding.NewEIP712Call(jtx, eip712Transaction) + return encoding.NewEIP712Call(jtx, inner.ChainID, eip712Transaction) } diff --git a/protocol/signature_test.go b/protocol/signature_test.go index a9a0ed7e9..909e132b6 100644 --- a/protocol/signature_test.go +++ b/protocol/signature_test.go @@ -32,6 +32,7 @@ import ( "gitlab.com/accumulatenetwork/accumulate/pkg/build" "gitlab.com/accumulatenetwork/accumulate/pkg/types/address" "gitlab.com/accumulatenetwork/accumulate/pkg/url" + "gitlab.com/accumulatenetwork/accumulate/protocol" . "gitlab.com/accumulatenetwork/accumulate/protocol" . "gitlab.com/accumulatenetwork/accumulate/test/harness" . "gitlab.com/accumulatenetwork/accumulate/test/helpers" @@ -501,7 +502,10 @@ func TestEip712TypedDataSignature(t *testing.T) { Done() require.NoError(t, err) + priv := acctesting.NewSECP256K1(t.Name()) eip712sig := &Eip712TypedDataSignature{ + ChainID: protocol.EthChainID("MainNet"), + PublicKey: eth.FromECDSAPub(&priv.PublicKey), Signer: url.MustParse("acc://adi.acme/book/1"), SignerVersion: 1, Timestamp: 1720564975623, @@ -510,7 +514,7 @@ func TestEip712TypedDataSignature(t *testing.T) { txn.Header.Initiator = [32]byte(eip712sig.Metadata().Hash()) // Sign the transaction - priv := acctesting.NewSECP256K1(t.Name()) + require.NoError(t, SignEip712TypedData(eip712sig, priv.Serialize(), nil, txn)) // Verify the signature @@ -525,7 +529,10 @@ func TestEIP712DelegatedKeyPageUpdate(t *testing.T) { Done() require.NoError(t, err) + priv := acctesting.NewSECP256K1(t.Name()) inner := &Eip712TypedDataSignature{ + ChainID: protocol.EthChainID("MainNet"), + PublicKey: eth.FromECDSAPub(&priv.PublicKey), Signer: url.MustParse("acc://adi.acme/book/1"), SignerVersion: 1, Timestamp: 1720564975623, @@ -538,7 +545,6 @@ func TestEIP712DelegatedKeyPageUpdate(t *testing.T) { txn.Header.Initiator = [32]byte(outer.Metadata().Hash()) // Sign the transaction - priv := acctesting.NewSECP256K1(t.Name()) require.NoError(t, SignEip712TypedData(inner, priv.Serialize(), outer, txn)) // Verify the signature @@ -565,6 +571,7 @@ func TestEIP712MessageForWallet(t *testing.T) { priv := acctesting.NewSECP256K1(t.Name()) sig := &Eip712TypedDataSignature{ + ChainID: protocol.EthChainID("MainNet"), PublicKey: eth.FromECDSAPub(&priv.PublicKey), Signer: url.MustParse("acc://adi.acme/book/1"), SignerVersion: 1, @@ -575,6 +582,7 @@ func TestEIP712MessageForWallet(t *testing.T) { b, err := MarshalEip712(txn, sig) require.NoError(t, err) + fmt.Printf("%s\n", b) cmd := exec.Command("../test/cmd/eth_signTypedData/execute.sh", hex.EncodeToString(priv.Serialize()), string(b)) cmd.Stderr = os.Stderr diff --git a/protocol/signatures.yml b/protocol/signatures.yml index 9b10b9a7b..9b0da9793 100644 --- a/protocol/signatures.yml +++ b/protocol/signatures.yml @@ -231,6 +231,9 @@ Eip712TypedDataSignature: fields: - name: PublicKey type: bytes + - name: ChainID + type: bigint + pointer: true - name: Signature type: bytes - name: Signer diff --git a/protocol/types_gen.go b/protocol/types_gen.go index 76c64d1fc..0c72c46e8 100644 --- a/protocol/types_gen.go +++ b/protocol/types_gen.go @@ -389,6 +389,7 @@ type EcdsaSha256Signature struct { type Eip712TypedDataSignature struct { fieldsSet []bool PublicKey []byte `json:"publicKey,omitempty" form:"publicKey" query:"publicKey" validate:"required"` + ChainID *big.Int `json:"chainID,omitempty" form:"chainID" query:"chainID" validate:"required"` Signature []byte `json:"signature,omitempty" form:"signature" query:"signature" validate:"required"` Signer *url.URL `json:"signer,omitempty" form:"signer" query:"signer" validate:"required"` SignerVersion uint64 `json:"signerVersion,omitempty" form:"signerVersion" query:"signerVersion" validate:"required"` @@ -2130,6 +2131,9 @@ func (v *Eip712TypedDataSignature) Copy() *Eip712TypedDataSignature { u := new(Eip712TypedDataSignature) u.PublicKey = encoding.BytesCopy(v.PublicKey) + if v.ChainID != nil { + u.ChainID = encoding.BigintCopy(v.ChainID) + } u.Signature = encoding.BytesCopy(v.Signature) if v.Signer != nil { u.Signer = v.Signer @@ -4602,6 +4606,14 @@ func (v *Eip712TypedDataSignature) Equal(u *Eip712TypedDataSignature) bool { if !(bytes.Equal(v.PublicKey, u.PublicKey)) { return false } + switch { + case v.ChainID == u.ChainID: + // equal + case v.ChainID == nil || u.ChainID == nil: + return false + case !((v.ChainID).Cmp(u.ChainID) == 0): + return false + } if !(bytes.Equal(v.Signature, u.Signature)) { return false } @@ -8750,14 +8762,15 @@ func (v *EcdsaSha256Signature) IsValid() error { var fieldNames_Eip712TypedDataSignature = []string{ 1: "Type", 2: "PublicKey", - 3: "Signature", - 4: "Signer", - 5: "SignerVersion", - 6: "Timestamp", - 7: "Vote", - 8: "TransactionHash", - 9: "Memo", - 10: "Data", + 3: "ChainID", + 4: "Signature", + 5: "Signer", + 6: "SignerVersion", + 7: "Timestamp", + 8: "Vote", + 9: "TransactionHash", + 10: "Memo", + 11: "Data", } func (v *Eip712TypedDataSignature) MarshalBinary() ([]byte, error) { @@ -8772,29 +8785,32 @@ func (v *Eip712TypedDataSignature) MarshalBinary() ([]byte, error) { if !(len(v.PublicKey) == 0) { writer.WriteBytes(2, v.PublicKey) } + if !(v.ChainID == nil) { + writer.WriteBigInt(3, v.ChainID) + } if !(len(v.Signature) == 0) { - writer.WriteBytes(3, v.Signature) + writer.WriteBytes(4, v.Signature) } if !(v.Signer == nil) { - writer.WriteUrl(4, v.Signer) + writer.WriteUrl(5, v.Signer) } if !(v.SignerVersion == 0) { - writer.WriteUint(5, v.SignerVersion) + writer.WriteUint(6, v.SignerVersion) } if !(v.Timestamp == 0) { - writer.WriteUint(6, v.Timestamp) + writer.WriteUint(7, v.Timestamp) } if !(v.Vote == 0) { - writer.WriteEnum(7, v.Vote) + writer.WriteEnum(8, v.Vote) } if !(v.TransactionHash == ([32]byte{})) { - writer.WriteHash(8, &v.TransactionHash) + writer.WriteHash(9, &v.TransactionHash) } if !(len(v.Memo) == 0) { - writer.WriteString(9, v.Memo) + writer.WriteString(10, v.Memo) } if !(len(v.Data) == 0) { - writer.WriteBytes(10, v.Data) + writer.WriteBytes(11, v.Data) } _, _, err := writer.Reset(fieldNames_Eip712TypedDataSignature) @@ -8817,16 +8833,21 @@ func (v *Eip712TypedDataSignature) IsValid() error { errs = append(errs, "field PublicKey is not set") } if len(v.fieldsSet) > 2 && !v.fieldsSet[2] { + errs = append(errs, "field ChainID is missing") + } else if v.ChainID == nil { + errs = append(errs, "field ChainID is not set") + } + if len(v.fieldsSet) > 3 && !v.fieldsSet[3] { errs = append(errs, "field Signature is missing") } else if len(v.Signature) == 0 { errs = append(errs, "field Signature is not set") } - if len(v.fieldsSet) > 3 && !v.fieldsSet[3] { + if len(v.fieldsSet) > 4 && !v.fieldsSet[4] { errs = append(errs, "field Signer is missing") } else if v.Signer == nil { errs = append(errs, "field Signer is not set") } - if len(v.fieldsSet) > 4 && !v.fieldsSet[4] { + if len(v.fieldsSet) > 5 && !v.fieldsSet[5] { errs = append(errs, "field SignerVersion is missing") } else if v.SignerVersion == 0 { errs = append(errs, "field SignerVersion is not set") @@ -15517,28 +15538,31 @@ func (v *Eip712TypedDataSignature) UnmarshalFieldsFrom(reader *encoding.Reader) if x, ok := reader.ReadBytes(2); ok { v.PublicKey = x } - if x, ok := reader.ReadBytes(3); ok { + if x, ok := reader.ReadBigInt(3); ok { + v.ChainID = x + } + if x, ok := reader.ReadBytes(4); ok { v.Signature = x } - if x, ok := reader.ReadUrl(4); ok { + if x, ok := reader.ReadUrl(5); ok { v.Signer = x } - if x, ok := reader.ReadUint(5); ok { + if x, ok := reader.ReadUint(6); ok { v.SignerVersion = x } - if x, ok := reader.ReadUint(6); ok { + if x, ok := reader.ReadUint(7); ok { v.Timestamp = x } - if x := new(VoteType); reader.ReadEnum(7, x) { + if x := new(VoteType); reader.ReadEnum(8, x) { v.Vote = *x } - if x, ok := reader.ReadHash(8); ok { + if x, ok := reader.ReadHash(9); ok { v.TransactionHash = *x } - if x, ok := reader.ReadString(9); ok { + if x, ok := reader.ReadString(10); ok { v.Memo = x } - if x, ok := reader.ReadBytes(10); ok { + if x, ok := reader.ReadBytes(11); ok { v.Data = x } @@ -18966,6 +18990,7 @@ func init() { encoding.RegisterTypeDefinition(&[]*encoding.TypeField{ encoding.NewTypeField("type", "string"), encoding.NewTypeField("publicKey", "bytes"), + encoding.NewTypeField("chainID", "uint256"), encoding.NewTypeField("signature", "bytes"), encoding.NewTypeField("signer", "string"), encoding.NewTypeField("signerVersion", "uint64"), @@ -20424,6 +20449,7 @@ func (v *Eip712TypedDataSignature) MarshalJSON() ([]byte, error) { u := struct { Type SignatureType `json:"type"` PublicKey *string `json:"publicKey,omitempty"` + ChainID *string `json:"chainID,omitempty"` Signature *string `json:"signature,omitempty"` Signer *url.URL `json:"signer,omitempty"` SignerVersion uint64 `json:"signerVersion,omitempty"` @@ -20438,6 +20464,9 @@ func (v *Eip712TypedDataSignature) MarshalJSON() ([]byte, error) { if !(len(v.PublicKey) == 0) { u.PublicKey = encoding.BytesToJSON(v.PublicKey) } + if !(v.ChainID == nil) { + u.ChainID = encoding.BigintToJSON(v.ChainID) + } if !(len(v.Signature) == 0) { u.Signature = encoding.BytesToJSON(v.Signature) } @@ -23288,6 +23317,7 @@ func (v *Eip712TypedDataSignature) UnmarshalJSON(data []byte) error { u := struct { Type SignatureType `json:"type"` PublicKey *string `json:"publicKey,omitempty"` + ChainID *string `json:"chainID,omitempty"` Signature *string `json:"signature,omitempty"` Signer *url.URL `json:"signer,omitempty"` SignerVersion uint64 `json:"signerVersion,omitempty"` @@ -23300,6 +23330,7 @@ func (v *Eip712TypedDataSignature) UnmarshalJSON(data []byte) error { }{} u.Type = v.Type() u.PublicKey = encoding.BytesToJSON(v.PublicKey) + u.ChainID = encoding.BigintToJSON(v.ChainID) u.Signature = encoding.BytesToJSON(v.Signature) u.Signer = v.Signer u.SignerVersion = v.SignerVersion @@ -23320,6 +23351,13 @@ func (v *Eip712TypedDataSignature) UnmarshalJSON(data []byte) error { } else { v.PublicKey = x } + if u.ChainID != nil { + if x, err := encoding.BigintFromJSON(u.ChainID); err != nil { + return fmt.Errorf("error decoding ChainID: %w", err) + } else { + v.ChainID = x + } + } if x, err := encoding.BytesFromJSON(u.Signature); err != nil { return fmt.Errorf("error decoding Signature: %w", err) } else { diff --git a/test/e2e/sig_eip712_test.go b/test/e2e/sig_eip712_test.go new file mode 100644 index 000000000..0c0c06555 --- /dev/null +++ b/test/e2e/sig_eip712_test.go @@ -0,0 +1,154 @@ +package e2e + +import ( + "bytes" + "encoding/hex" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/accumulatenetwork/accumulate/pkg/build" + "gitlab.com/accumulatenetwork/accumulate/pkg/types/messaging" + . "gitlab.com/accumulatenetwork/accumulate/protocol" + . "gitlab.com/accumulatenetwork/accumulate/test/harness" + "gitlab.com/accumulatenetwork/accumulate/test/simulator" + acctesting "gitlab.com/accumulatenetwork/accumulate/test/testing" +) + +func TestEIP712Signature(t *testing.T) { + alice := build. + Identity("alice").Create("book"). + Tokens("tokens").Create("ACME").Add(1e9).Identity(). + Book("book").Page(1).Create().AddCredits(1e9).Book().Identity() + aliceKey := alice.Book("book").Page(1). + GenerateKey(SignatureTypeETH) + + t.Run("Correct chain", func(t *testing.T) { + // Initialize + sim := NewSim(t, + simulator.SimpleNetwork("DevNet", 3, 1), + simulator.Genesis(GenesisTime).With(alice), + ) + + // Build a transaction + txn, err := build.Transaction().For(alice, "book", "1"). + BurnCredits(1). + Done() + require.NoError(t, err) + + // Sign it + pk, _ := aliceKey.GetPublicKey() + sig := &Eip712TypedDataSignature{ + ChainID: EthChainID("MainNet"), + Signer: alice.Url().JoinPath("book", "1"), + PublicKey: pk, + SignerVersion: 1, + Timestamp: 1, + } + txn.Header.Initiator = [32]byte(sig.Metadata().Hash()) + sk, _ := aliceKey.GetPrivateKey() + require.NoError(t, SignEip712TypedData(sig, sk, nil, txn)) + + // Submit + st := sim.SubmitSuccessfully(&messaging.Envelope{ + Signatures: []Signature{sig}, + Transaction: []*Transaction{txn}, + }) + + sim.StepUntil( + Txn(st[0].TxID).Completes()) + }) + + t.Run("Incorrect chain", func(t *testing.T) { + // Initialize + sim := NewSim(t, + simulator.SimpleNetwork("DevNet", 3, 1), + simulator.Genesis(GenesisTime).With(alice), + ) + + // Build a transaction + txn, err := build.Transaction().For(alice, "book", "1"). + BurnCredits(1). + Done() + require.NoError(t, err) + + // Sign it + pk, _ := aliceKey.GetPublicKey() + sig := &Eip712TypedDataSignature{ + ChainID: EthChainID("MainNet"), + Signer: alice.Url().JoinPath("book", "1"), + PublicKey: pk, + SignerVersion: 1, + Timestamp: 1, + } + txn.Header.Initiator = [32]byte(sig.Metadata().Hash()) + sk, _ := aliceKey.GetPrivateKey() + require.NoError(t, SignEip712TypedData(sig, sk, nil, txn)) + + // Submit + st := sim.Submit(&messaging.Envelope{ + Signatures: []Signature{sig}, + Transaction: []*Transaction{txn}, + }) + require.EqualError(t, st[1].AsError(), "invalid chain ID: want 317849881, got 281") + }) +} + +func TestEIP712ExternalWallet(t *testing.T) { + acctesting.SkipWithoutTool(t, "node") + + alice := build. + Identity("alice").Create("book"). + Tokens("tokens").Create("ACME").Add(1e9).Identity(). + Book("book").Page(1).Create().AddCredits(1e9).Book().Identity() + aliceKey := alice.Book("book").Page(1). + GenerateKey(SignatureTypeETH) + + // Initialize + sim := NewSim(t, + simulator.SimpleNetwork("DevNet", 3, 1), + simulator.Genesis(GenesisTime).With(alice), + ) + + // Build a transaction + txn, err := build.Transaction().For(alice, "book", "1"). + BurnCredits(1). + Done() + require.NoError(t, err) + + // Sign it + pk, _ := aliceKey.GetPublicKey() + sig := &Eip712TypedDataSignature{ + ChainID: EthChainID("DevNet"), + Signer: alice.Url().JoinPath("book", "1"), + PublicKey: pk, + SignerVersion: 1, + Timestamp: 1, + } + txn.Header.Initiator = [32]byte(sig.Metadata().Hash()) + + b, err := MarshalEip712(txn, sig) + require.NoError(t, err) + sk, _ := aliceKey.GetPrivateKey() + + cmd := exec.Command("../cmd/eth_signTypedData/execute.sh", hex.EncodeToString(sk), string(b)) + cmd.Stderr = os.Stderr + out, err := cmd.Output() + require.NoError(t, err) + + out = bytes.TrimSpace(out) + out = bytes.TrimPrefix(out, []byte("0x")) + sig.Signature = make([]byte, len(out)/2) + _, err = hex.Decode(sig.Signature, out) + require.NoError(t, err) + + // Submit + st := sim.SubmitSuccessfully(&messaging.Envelope{ + Signatures: []Signature{sig}, + Transaction: []*Transaction{txn}, + }) + + sim.StepUntil( + Txn(st[0].TxID).Completes()) +}