Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions eth/authorizationlist.go
Original file line number Diff line number Diff line change
@@ -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
}
54 changes: 54 additions & 0 deletions eth/authorizationlist_test.go
Original file line number Diff line number Diff line change
@@ -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")
}

}
55 changes: 54 additions & 1 deletion eth/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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")
}
Expand All @@ -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:
Expand Down
55 changes: 55 additions & 0 deletions eth/transaction_from_raw.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func (t *Transaction) FromRaw(input string) error {
r Quantity
s Quantity
accessList AccessList
authorizationList AuthorizationList
maxFeePerBlobGas Quantity
blobVersionedHashes []Hash
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
Expand Down
25 changes: 25 additions & 0 deletions eth/transaction_from_raw_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 25 additions & 3 deletions eth/transaction_signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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")
Expand Down
Loading