diff --git a/eth/authorizationlist.go b/eth/authorizationlist.go new file mode 100644 index 0000000..1e14bfe --- /dev/null +++ b/eth/authorizationlist.go @@ -0,0 +1,86 @@ +package eth + +import ( + "github.com/INFURA/go-ethlibs/rlp" + "github.com/pkg/errors" +) + +type AuthorizationList []SetCodeAuthorization + +type SetCodeAuthorization struct { + ChainID *Quantity `json:"chainId"` + Address Address `json:"address"` + Nonce Quantity `json:"nonce"` + V Quantity `json:"yParity"` + R Quantity `json:"r"` + S Quantity `json:"s"` +} + +func (a *AuthorizationList) RLP() rlp.Value { + if a == nil { + return rlp.Value{} + } + + al := *a + val := rlp.Value{List: make([]rlp.Value, len(al))} + for i := range al { + val.List[i] = rlp.Value{List: []rlp.Value{ + al[i].ChainID.RLP(), + al[i].Address.RLP(), + al[i].Nonce.RLP(), + al[i].V.RLP(), + al[i].R.RLP(), + al[i].S.RLP(), + }} + } + return val +} + +// NewAuthorizationListFromRLP decodes an RLP list into an AuthorizationList, or returns an error. +// The RLP format of AuthorizationLists is defined in EIP-7702, each entry is a list of a chain ID, an address, +// and a list of authorization parameters. +func NewAuthorizationListFromRLP(v rlp.Value) (AuthorizationList, error) { + al := make(AuthorizationList, len(v.List)) + + for i := range v.List { + authorization := v.List[i].List + if len(authorization) < 5 { + return nil, errors.New("invalid authorization") + } + + chainID, err := NewQuantityFromRLP(authorization[0]) + if err != nil { + return nil, errors.Wrapf(err, "invalid authorization %d chain ID", i) + } + address, err := NewAddress(authorization[1].String) + if err != nil { + return nil, errors.Wrapf(err, "invalid authorization %d address", i) + } + nonce, err := NewQuantityFromRLP(authorization[2]) + if err != nil { + return nil, errors.Wrapf(err, "invalid authorization %d nonce", i) + } + v, err := NewQuantityFromRLP(authorization[3]) + if err != nil { + return nil, errors.Wrapf(err, "invalid authorization %d V", i) + } + r, err := NewQuantityFromRLP(authorization[4]) + if err != nil { + return nil, errors.Wrapf(err, "invalid authorization %d R", i) + } + s, err := NewQuantityFromRLP(authorization[5]) + if err != nil { + return nil, errors.Wrapf(err, "invalid authorization %d S", i) + } + + al[i] = SetCodeAuthorization{ + ChainID: chainID, + Address: *address, + Nonce: *nonce, + V: *v, + R: *r, + S: *s, + } + } + return al, nil +} diff --git a/eth/authorizationlist_test.go b/eth/authorizationlist_test.go new file mode 100644 index 0000000..90af235 --- /dev/null +++ b/eth/authorizationlist_test.go @@ -0,0 +1,54 @@ +package eth_test + +import ( + "testing" + + "github.com/INFURA/go-ethlibs/eth" + "github.com/stretchr/testify/require" +) + +func TestAuthorizationList_FromRLP(t *testing.T) { + src := eth.AuthorizationList{ + eth.SetCodeAuthorization{ + ChainID: eth.MustQuantity("0x1"), + Address: *eth.MustAddress("0x000000000000000000000000000000000000aaaa"), + Nonce: eth.QuantityFromInt64(0x1), + V: eth.QuantityFromInt64(0x1), + R: *eth.MustQuantity("0xf7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721"), + S: *eth.MustQuantity("0x6cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987"), + }, + eth.SetCodeAuthorization{ + ChainID: eth.MustQuantity("0x0"), + Address: *eth.MustAddress("0x000000000000000000000000000000000000bbbb"), + Nonce: eth.QuantityFromInt64(0x0), + V: eth.QuantityFromInt64(0x1), + R: *eth.MustQuantity("0x5011890f198f0356a887b0779bde5afa1ed04e6acb1e3f37f8f18c7b6f521b98"), + S: *eth.MustQuantity("0x56c3fa3456b103f3ef4a0acb4b647b9cab9ec4bc68fbcdf1e10b49fb2bcbcf61"), + }, + } + + // encode + asRLP := src.RLP() + encoded, err := asRLP.Encode() + require.NoError(t, err) + expected := "0xf8b8f85a0194000000000000000000000000000000000000aaaa0101a0f7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721a06cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987f85a8094000000000000000000000000000000000000bbbb8001a05011890f198f0356a887b0779bde5afa1ed04e6acb1e3f37f8f18c7b6f521b98a056c3fa3456b103f3ef4a0acb4b647b9cab9ec4bc68fbcdf1e10b49fb2bcbcf61" + require.Equal(t, expected, encoded, "wrong encoding") + + // get back the original list + authorizationList, err := eth.NewAuthorizationListFromRLP(asRLP) + require.NoError(t, err) + require.Equal(t, len(src), len(authorizationList), "authorization lists have different lengths") + + for i := range src { + srcAuth := src[i] + auth := authorizationList[i] + + require.Equal(t, srcAuth.ChainID, auth.ChainID, "authorization lists not equal") + require.Equal(t, srcAuth.Address, auth.Address, "authorization address not equal") + require.Equal(t, srcAuth.Nonce.String(), auth.Nonce.String(), "authorization nonce not equal") + require.Equal(t, srcAuth.V.String(), auth.V.String(), "authorization V not equal") + require.Equal(t, srcAuth.R.String(), auth.R.String(), "authorization R not equal") + require.Equal(t, srcAuth.S.String(), auth.S.String(), "authorization S not equal") + } + +} diff --git a/eth/transaction.go b/eth/transaction.go index 11eb0f5..3ba2b04 100644 --- a/eth/transaction.go +++ b/eth/transaction.go @@ -16,6 +16,7 @@ var ( TransactionTypeAccessList = int64(0x1) // TransactionTypeAccessList refers to EIP-2930 transactions. TransactionTypeDynamicFee = int64(0x2) // TransactionTypeDynamicFee refers to EIP-1559 transactions. TransactionTypeBlob = int64(0x3) // TransactionTypeBlob refers to EIP-4844 "blob" transactions. + TransactionTypeSetCode = int64(0x4) // TransactionTypeSetCode refers to EIP-7702 transactions. ) type Transaction struct { @@ -65,6 +66,9 @@ type Transaction struct { // raw transaction in "Network Representation" and the fields must be accessed directly. BlobBundle *BlobsBundleV1 `json:"-"` + // EIP-7702 + AuthorizationList *AuthorizationList `json:"authorizationList,omitempty"` + // Keep the source so we can recreate its expected representation source string } @@ -154,6 +158,27 @@ func (t *Transaction) RequiredFields() error { // Contract creation not supported in blob txs fields = append(fields, "to") } + case TransactionTypeSetCode: + // From EIP-7702: + // The fields chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, + // destination, value, data, and access_list of the outer transaction follow the same + // semantics as EIP-4844. Note, this means a null destination is not valid. + if t.ChainId == nil { + fields = append(fields, "chainId") + } + if t.MaxFeePerGas == nil { + fields = append(fields, "maxFeePerGas") + } + if t.MaxPriorityFeePerGas == nil { + fields = append(fields, "maxPriorityFeePerGas") + } + // Contract creation is not supported for setcode transactions + if t.To == nil { + fields = append(fields, "to") + } + if t.AuthorizationList == nil { + fields = append(fields, "authorizationList") + } default: return errors.New("unsupported transaction type") } @@ -279,6 +304,34 @@ func (t *Transaction) RawRepresentation() (*Data, error) { } else { return NewData(typePrefix + encodedPayload[2:]) } + case TransactionTypeSetCode: + // EIP-7702 + // rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, value, data, access_list, authorization_list, signature_y_parity, signature_r, signature_s]) + // authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...] + typePrefix, err := t.Type.RLP().Encode() + if err != nil { + return nil, err + } + payload := rlp.Value{List: []rlp.Value{ + t.ChainId.RLP(), + t.Nonce.RLP(), + t.MaxPriorityFeePerGas.RLP(), + t.MaxFeePerGas.RLP(), + t.Gas.RLP(), + t.To.RLP(), + t.Value.RLP(), + {String: t.Input.String()}, + t.AccessList.RLP(), + t.AuthorizationList.RLP(), + t.YParity.RLP(), + t.R.RLP(), + t.S.RLP(), + }} + if encodedPayload, err := payload.Encode(); err != nil { + return nil, err + } else { + return NewData(typePrefix + encodedPayload[2:]) + } default: return nil, errors.New("unsupported transaction type") } @@ -292,7 +345,7 @@ func (t *Transaction) NetworkRepresentation() (*Data, error) { } switch t.TransactionType() { - case TransactionTypeLegacy, TransactionTypeAccessList, TransactionTypeDynamicFee: + case TransactionTypeLegacy, TransactionTypeAccessList, TransactionTypeDynamicFee, TransactionTypeSetCode: // For most transaction types, the "Raw" and "Network" representations are the same return t.RawRepresentation() case TransactionTypeBlob: diff --git a/eth/transaction_from_raw.go b/eth/transaction_from_raw.go index 835d3d5..6cbef6c 100644 --- a/eth/transaction_from_raw.go +++ b/eth/transaction_from_raw.go @@ -34,6 +34,7 @@ func (t *Transaction) FromRaw(input string) error { r Quantity s Quantity accessList AccessList + authorizationList AuthorizationList maxFeePerBlobGas Quantity blobVersionedHashes []Hash ) @@ -271,7 +272,55 @@ func (t *Transaction) FromRaw(input string) error { t.Hash = raw.Hash() t.From = *sender return nil + case firstByte == byte(TransactionTypeSetCode): + // EIP-7702 transaction + // 0x04 || rlp([chainID, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, authorization_list, y_parity, r, s]) + payload := "0x" + input[4:] + decodedErr := rlpDecodeList(payload, &chainId, &nonce, &maxPriorityFeePerGas, &maxFeePerGas, &gasLimit, &to, &value, &data, &accessList, &authorizationList, &v, &r, &s) + if decodedErr != nil { + return errors.Wrap(decodedErr, "could not decode RLP components") + } + + // fill in the fields + t.Type = OptionalQuantityFromInt(int(firstByte)) + t.Nonce = nonce + t.MaxPriorityFeePerGas = &maxPriorityFeePerGas + t.MaxFeePerGas = &maxFeePerGas + t.Gas = gasLimit + t.To = to + t.Value = value + t.Input = data + t.AccessList = &accessList + t.AuthorizationList = &authorizationList + t.V = v + t.YParity = &v + t.R = r + t.S = s + t.ChainId = &chainId + + signingHash, signErr := t.SigningHash(chainId) + if signErr != nil { + return signErr + } + + signature, sigErr := NewEIP2718Signature(chainId, r, s, v) + if sigErr != nil { + return sigErr + } + + sender, recoverErr := signature.Recover(signingHash) + if recoverErr != nil { + return recoverErr + } + raw, rawErr := t.RawRepresentation() + if rawErr != nil { + return rawErr + } + + t.Hash = raw.Hash() + t.From = *sender + return nil case firstByte > 0x7f: // In EIP-2718 types larger than 0x7f are reserved since they potentially conflict with legacy RLP encoded // transactions. As such we can attempt to decode any such transactions as legacy format and attempt to @@ -413,6 +462,12 @@ func rlpDecodeList(input interface{}, receivers ...interface{}) error { return errors.Wrapf(err, "could not decode list item %d to AccessList", i) } *receiver = accessList + case *AuthorizationList: + authorizationList, err := NewAuthorizationListFromRLP(value) + if err != nil { + return errors.Wrapf(err, "could not decode list item %d to AuthorizationList", i) + } + *receiver = authorizationList default: return errors.Errorf("unsupported decode receiver %s", reflect.TypeOf(receiver).String()) } diff --git a/eth/transaction_from_raw_test.go b/eth/transaction_from_raw_test.go index 56401df..f44f0e5 100644 --- a/eth/transaction_from_raw_test.go +++ b/eth/transaction_from_raw_test.go @@ -191,6 +191,31 @@ func TestTransaction_FromRawEIP4844(t *testing.T) { require.Equal(t, "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", tx.BlobBundle.Proofs[0].String()) } +func TestTransaction_FromRawEIP7702(t *testing.T) { + // Transaction: {"type":"0x4","chainId":"0x1","nonce":"0x0","to":"0x71562b71999873db5b286df957af199ec94617f7","gas":"0x7a120","gasPrice":null,"maxPriorityFeePerGas":"0x2","maxFeePerGas":"0x12a05f200","value":"0x0","input":"0x","accessList":[],"authorizationList":[{"chainId":"0x1","address":"0x000000000000000000000000000000000000aaaa","nonce":"0x1","yParity":"0x1","r":"0xf7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721","s":"0x6cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987"},{"chainId":"0x0","address":"0x000000000000000000000000000000000000bbbb","nonce":"0x0","yParity":"0x1","r":"0x5011890f198f0356a887b0779bde5afa1ed04e6acb1e3f37f8f18c7b6f521b98","s":"0x56c3fa3456b103f3ef4a0acb4b647b9cab9ec4bc68fbcdf1e10b49fb2bcbcf61"}],"v":"0x1","r":"0x6d5ddb9420ce5d9ff7d1bc6fcf1098cc648a68207489c0fcfee54dc61352353a","s":"0x4ba532c2cfc4a1163e7ffd3d92dd5815b37ddcfa3293e1c4ee86f9604b6b59a2","yParity":"0x1","hash":"0x6a75a08b5be2bbdf655c788ede009fbc84caf63f15c060619b376554fac8d6cd"} + hash := `0x6a75a08b5be2bbdf655c788ede009fbc84caf63f15c060619b376554fac8d6cd` + raw := `0x04f9012201800285012a05f2008307a1209471562b71999873db5b286df957af199ec94617f78080c0f8b8f85a0194000000000000000000000000000000000000aaaa0101a0f7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721a06cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987f85a8094000000000000000000000000000000000000bbbb8001a05011890f198f0356a887b0779bde5afa1ed04e6acb1e3f37f8f18c7b6f521b98a056c3fa3456b103f3ef4a0acb4b647b9cab9ec4bc68fbcdf1e10b49fb2bcbcf6101a06d5ddb9420ce5d9ff7d1bc6fcf1098cc648a68207489c0fcfee54dc61352353aa04ba532c2cfc4a1163e7ffd3d92dd5815b37ddcfa3293e1c4ee86f9604b6b59a2` + + tx := eth.Transaction{} + err := tx.FromRaw(raw) + require.NoError(t, err) + require.Equal(t, hash, tx.Hash.String()) + require.Equal(t, "0x4", tx.Type.String(), "expected set code transaction type") + + require.NotNil(t, tx.AuthorizationList) + authList := *tx.AuthorizationList + require.NotEmpty(t, authList) + require.Equal(t, 2, len(authList)) + + firstAuth := authList[0] + // {"chainId":"0x1","address":"0x000000000000000000000000000000000000aaaa","nonce":"0x1","yParity":"0x1","r":"0xf7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721","s":"0x6cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987"} + require.Equal(t, "0x1", firstAuth.ChainID.String()) + require.Equal(t, "0x000000000000000000000000000000000000aaaa", firstAuth.Address.String()) + require.Equal(t, "0x1", firstAuth.V.String()) + require.Equal(t, "0xf7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721", firstAuth.R.String()) + require.Equal(t, "0x6cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987", firstAuth.S.String()) +} + func TestTransaction_FromRaw_BesuBlobs(t *testing.T) { // The tests in this function are derived from: // https://github.com/hyperledger/besu/blob/main/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/BlobTransactionEncodingTest.java#L42 diff --git a/eth/transaction_signing.go b/eth/transaction_signing.go index f72a3e7..9d6710e 100644 --- a/eth/transaction_signing.go +++ b/eth/transaction_signing.go @@ -42,8 +42,8 @@ func (t *Transaction) Sign(privateKey string, chainId Quantity) (*Data, error) { switch t.TransactionType() { case TransactionTypeLegacy: t.R, t.S, t.V = signature.EIP155Values() - case TransactionTypeAccessList, TransactionTypeDynamicFee, TransactionTypeBlob: - // set chainId and RSV to EIP2718 values + case TransactionTypeAccessList, TransactionTypeDynamicFee, TransactionTypeBlob, TransactionTypeSetCode: + // set chainID and RSV to EIP2718 values t.ChainId = &chainId t.R, t.S, t.V = signature.EIP2718Values() default: @@ -184,6 +184,28 @@ func (t *Transaction) SigningPreimage(chainId Quantity) (*Data, error) { } // And return it with the 0x03 prefix return NewData("0x03" + encoded[2:]) + case TransactionTypeSetCode: + // The signature values y_parity, r, and s are calculated by constructing a secp256k1 signature over the following digest: + // keccak256(0x04 || rlp([chainID, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, authorization_list, y_parity, r, s])) + payload := rlp.Value{List: []rlp.Value{ + chainId.RLP(), + t.Nonce.RLP(), + t.MaxPriorityFeePerGas.RLP(), + t.MaxFeePerGas.RLP(), + t.Gas.RLP(), + t.To.RLP(), + t.Value.RLP(), + {String: t.Input.String()}, + t.AccessList.RLP(), + t.AuthorizationList.RLP(), + }} + // encode the list as RLP + encoded, err := payload.Encode() + if err != nil { + return nil, err + } + // And return it with the 0x04 prefix + return NewData("0x04" + encoded[2:]) default: return nil, errors.New("unsupported transaction type") } @@ -216,7 +238,7 @@ func (t *Transaction) Signature() (*Signature, error) { return nil, errors.New("chainId is required") } return NewEIP2718Signature(*t.ChainId, t.R, t.S, t.V) - case TransactionTypeDynamicFee, TransactionTypeBlob: + case TransactionTypeDynamicFee, TransactionTypeBlob, TransactionTypeSetCode: return NewEIP2718Signature(*t.ChainId, t.R, t.S, t.V) default: return nil, errors.New("unsupported transaction type") diff --git a/eth/transaction_signing_test.go b/eth/transaction_signing_test.go index ac99dbb..6d4023c 100644 --- a/eth/transaction_signing_test.go +++ b/eth/transaction_signing_test.go @@ -339,6 +339,129 @@ func TestTransaction_Sign_EIP1559(t *testing.T) { } +func TestTransaction_Sign_EIP7702(t *testing.T) { + /* + unsigned transaction + { + "type":"0x4", + "chainId":"0x1", + "nonce":"0x0", + "to":"0x71562b71999873db5b286df957af199ec94617f7", + "gas":"0x7a120", + "maxPriorityFeePerGas":"0x2", + "maxFeePerGas":"0x12a05f200", + "value":"0x0", + "input":"0x", + "accessList":[], + "authorizationList":[ + {"chainId":"0x1","address":"0x000000000000000000000000000000000000aaaa","nonce":"0x1","yParity":"0x1","r":"0xf7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721","s":"0x6cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987"}, + {"chainId":"0x0","address":"0x000000000000000000000000000000000000bbbb","nonce":"0x0","yParity":"0x1","r":"0x5011890f198f0356a887b0779bde5afa1ed04e6acb1e3f37f8f18c7b6f521b98","s":"0x56c3fa3456b103f3ef4a0acb4b647b9cab9ec4bc68fbcdf1e10b49fb2bcbcf61"} + ], + "v":"0x0", + "r":"0x0", + "s":"0x0", + "yParity":"0x0", + "hash":"0x18e9c60fcdf98300ddf743ccf3015822b05eb8c42154dac82c7e1e065af16e45" + } + */ + // signed transaction generated with go-ethereum + // {"type":"0x4","chainId":"0x1","nonce":"0x0","to":"0x71562b71999873db5b286df957af199ec94617f7","gas":"0x7a120","gasPrice":null,"maxPriorityFeePerGas":"0x2","maxFeePerGas":"0x12a05f200","value":"0x0","input":"0x","accessList":[],"authorizationList":[{"chainId":"0x1","address":"0x000000000000000000000000000000000000aaaa","nonce":"0x1","yParity":"0x1","r":"0xf7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721","s":"0x6cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987"},{"chainId":"0x0","address":"0x000000000000000000000000000000000000bbbb","nonce":"0x0","yParity":"0x1","r":"0x5011890f198f0356a887b0779bde5afa1ed04e6acb1e3f37f8f18c7b6f521b98","s":"0x56c3fa3456b103f3ef4a0acb4b647b9cab9ec4bc68fbcdf1e10b49fb2bcbcf61"}],"v":"0x0","r":"0xe4ad40ffd468299b18a775ab3b743687a47087569a2d798b51ed02ae0920703b","s":"0x6b55c02a6000a3a344ba290c185321057a7994cd2537deeb3c254091a680d248","yParity":"0x0","hash":"0xb81ac1a9321c1a57605c9beef606967b8866eb53fb63650678b8ba586e5c1ad9"} + + chainId := eth.QuantityFromInt64(0x01) + tx := eth.Transaction{ + Type: eth.MustQuantity("0x4"), + ChainId: &chainId, + MaxFeePerGas: eth.MustQuantity("0x12a05f200"), + MaxPriorityFeePerGas: eth.MustQuantity("0x2"), + Input: eth.Input("0x"), + From: *eth.MustAddress("0x96216849c49358b10257cb55b28ea603c874b05e"), + Nonce: eth.QuantityFromInt64(0), + Gas: eth.QuantityFromInt64(0x7a120), + To: eth.MustAddress("0x71562b71999873db5b286df957af199ec94617f7"), + Value: eth.QuantityFromInt64(0x0), + AccessList: ð.AccessList{}, + AuthorizationList: ð.AuthorizationList{ + eth.SetCodeAuthorization{ + ChainID: eth.MustQuantity("0x1"), + Address: *eth.MustAddress("0x000000000000000000000000000000000000aaaa"), + Nonce: eth.QuantityFromInt64(0x1), + V: eth.QuantityFromInt64(0x1), + R: *eth.MustQuantity("0xf7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721"), + S: *eth.MustQuantity("0x6cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987"), + }, + eth.SetCodeAuthorization{ + ChainID: eth.MustQuantity("0x0"), + Address: *eth.MustAddress("0x000000000000000000000000000000000000bbbb"), + Nonce: eth.QuantityFromInt64(0x0), + V: eth.QuantityFromInt64(0x1), + R: *eth.MustQuantity("0x5011890f198f0356a887b0779bde5afa1ed04e6acb1e3f37f8f18c7b6f521b98"), + S: *eth.MustQuantity("0x56c3fa3456b103f3ef4a0acb4b647b9cab9ec4bc68fbcdf1e10b49fb2bcbcf61"), + }, + }, + YParity: eth.MustQuantity("0x0"), + V: eth.QuantityFromInt64(0x0), + R: eth.QuantityFromInt64(0), + S: eth.QuantityFromInt64(0), + } + + rlpData, err := rlp.Value{List: []rlp.Value{ + tx.ChainId.RLP(), + tx.Nonce.RLP(), + tx.MaxPriorityFeePerGas.RLP(), + tx.MaxFeePerGas.RLP(), + tx.Gas.RLP(), + tx.To.RLP(), + tx.Value.RLP(), + tx.Input.RLP(), + tx.AccessList.RLP(), + tx.AuthorizationList.RLP(), + }}.Encode() + require.NoError(t, err) + + // make sure raw tx is what we expect it to be + expectedUnsigned := "0x04f8e201800285012a05f2008307a1209471562b71999873db5b286df957af199ec94617f78080c0f8b8f85a0194000000000000000000000000000000000000aaaa0101a0f7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721a06cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987f85a8094000000000000000000000000000000000000bbbb8001a05011890f198f0356a887b0779bde5afa1ed04e6acb1e3f37f8f18c7b6f521b98a056c3fa3456b103f3ef4a0acb4b647b9cab9ec4bc68fbcdf1e10b49fb2bcbcf61808080" + unsigned, err := tx.RawRepresentation() + require.NoError(t, err) + require.Equal(t, expectedUnsigned, unsigned.String(), "unsigned tx mismatch") + + // which should match exactly what SigningPreimage returns + expectedPreimage := "0x04" + rlpData[2:] + preimage, err := tx.SigningPreimage(chainId) + require.NoError(t, err) + require.Equal(t, expectedPreimage, preimage.String(), "preimage mismatch") + + // So now we can sign the transaction with the same key used to sign + signed, err := tx.Sign("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19", chainId) + require.NoError(t, err) + + expectedHash := "0xb81ac1a9321c1a57605c9beef606967b8866eb53fb63650678b8ba586e5c1ad9" + require.Equal(t, expectedHash, signed.Hash().String(), "hash mismatch") + + // And get back the exact same signed transaction + expectedSigned := `0x04f9012201800285012a05f2008307a1209471562b71999873db5b286df957af199ec94617f78080c0f8b8f85a0194000000000000000000000000000000000000aaaa0101a0f7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721a06cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987f85a8094000000000000000000000000000000000000bbbb8001a05011890f198f0356a887b0779bde5afa1ed04e6acb1e3f37f8f18c7b6f521b98a056c3fa3456b103f3ef4a0acb4b647b9cab9ec4bc68fbcdf1e10b49fb2bcbcf6180a0e4ad40ffd468299b18a775ab3b743687a47087569a2d798b51ed02ae0920703ba06b55c02a6000a3a344ba290c185321057a7994cd2537deeb3c254091a680d248` + require.Equal(t, expectedSigned, signed.String(), "signed tx mismatch") + + // Double check signature is still valid + tx2 := eth.Transaction{} + err = tx2.FromRaw(signed.String()) + require.NoError(t, err) + + // And verify that .From, .Hash, .R, .S., .V, and .YParity are all set and match the original transaction + require.Equal(t, *eth.MustAddress("0x96216849c49358b10257cb55b28ea603c874b05e"), tx2.From) + require.Equal(t, "0xb81ac1a9321c1a57605c9beef606967b8866eb53fb63650678b8ba586e5c1ad9", tx2.Hash.String(), "hash mismatch") + require.Equal(t, "0xe4ad40ffd468299b18a775ab3b743687a47087569a2d798b51ed02ae0920703b", tx2.R.String(), "r mismatch") + require.Equal(t, "0x6b55c02a6000a3a344ba290c185321057a7994cd2537deeb3c254091a680d248", tx2.S.String(), "s mismatch") + require.Equal(t, "0x0", tx2.V.String(), "v mismatch") + require.Equal(t, "0x0", tx2.YParity.String(), "yParity mismatch") + + // Verify that the recovered address matches the original address + signingHash, err := tx2.SigningHash(chainId) + require.NoError(t, err) + recoveredAddress, err := eth.ECRecover(signingHash, &tx2.R, &tx2.S, &tx2.V) + require.NoError(t, err) + require.Equal(t, *eth.MustAddress("0x96216849c49358b10257cb55b28ea603c874b05e"), *recoveredAddress) +} + func TestTransaction_Sign_InvalidTxType(t *testing.T) { tx := eth.Transaction{ Type: eth.MustQuantity("0x7f"), diff --git a/eth/transaction_test.go b/eth/transaction_test.go index d2e1ab6..4fd8c00 100644 --- a/eth/transaction_test.go +++ b/eth/transaction_test.go @@ -291,6 +291,74 @@ func TestTransactionTypeBlob(t *testing.T) { require.JSONEq(t, payload, string(b)) } +func TestTransactionTypeSetCode(t *testing.T) { + payload := `{ + "blockHash": "0xfc2715ff196e23ae613ed6f837abd9035329a720a1f4e8dce3b0694c867ba052", + "blockNumber": "0x2a1cb", + "from": "0xad01b55d7c3448b8899862eb335fbb17075d8de2", + "gas": "0x5208", + "gasPrice": "0x1d1a94a201c", + "maxFeePerGas": "0x1d1a94a201c", + "maxPriorityFeePerGas": "0x1d1a94a201c", + "maxFeePerBlobGas": "0x3e8", + "hash": "0x5ceec39b631763ae0b45a8fb55c373f38b8fab308336ca1dc90ecd2b3cf06d00", + "input": "0x", + "nonce": "0x1b483", + "to": "0x000000000000000000000000000000000000f1c1", + "transactionIndex": "0x0", + "value": "0x0", + "type": "0x4", + "accessList": [], + "chainId": "0x1a1f0ff42", + "authorizationList": [ + { + "chainId": "0x1", + "address": "0xad01b55d7c3448b8899862eb335fbb17075d8de2", + "nonce": "0x1b", + "yParity": "0x0", + "r": "0x343c6239323a81ef61293cb4a4d37b6df47fbf68114adb5dd41581151a077da1", + "s": "0x48c21f6872feaf181d37cc4f9bbb356d3f10b352ceb38d1c3b190d749f95a11b" + }, + { + "chainId": "0x1", + "address": "0x000000000000000000000000000000000000aaaa", + "nonce": "0x1", + "yParity": "0x1", + "r": "0xf7e3e597fc097e71ed6c26b14b25e5395bc8510d58b9136af439e12715f2d721", + "s": "0x6cf7c3d7939bfdb784373effc0ebb0bd7549691a513f395e3cdabf8602724987" + } + ], + "v": "0x0", + "r": "0x343c6239323a81ef61293cb4a4d37b6df47fbf68114adb5dd41581151a077da1", + "s": "0x48c21f6872feaf181d37cc4f9bbb356d3f10b352ceb38d1c3b190d749f95a11b", + "yParity": "0x0" + }` + + tx := eth.Transaction{} + err := json.Unmarshal([]byte(payload), &tx) + require.NoError(t, err) + require.NotNil(t, tx.Type) + require.Equal(t, eth.TransactionTypeSetCode, tx.Type.Int64()) + require.Equal(t, eth.TransactionTypeSetCode, tx.TransactionType()) + require.NotNil(t, tx.YParity) + require.Equal(t, tx.V, *tx.YParity) + + b, err := json.Marshal(&tx) + require.NoError(t, err) + require.JSONEq(t, payload, string(b)) + + nr, err := tx.NetworkRepresentation() + require.NoError(t, err, "failed to get network representation") + require.NotNil(t, nr, "network representation is nil") + + err = tx.RequiredFields() + require.NoError(t, err, "failed to check required fields") + + rr, err := tx.RawRepresentation() + require.NoError(t, err, "failed to get raw representation") + require.NotNil(t, rr, "raw representation is nil") +} + func TestNewPendingTxNotificationParams(t *testing.T) { {