From 5437b742d9f5a19ada75e2e5c6ece05061a062d1 Mon Sep 17 00:00:00 2001 From: Antony Denyer Date: Fri, 5 Jan 2024 10:30:19 +0000 Subject: [PATCH 1/2] Add input type to allow function selector decoding --- eth/block_from_raw_test.go | 2 +- eth/block_test.go | 4 +-- eth/data.go | 20 +++++++++++++ eth/input.go | 51 +++++++++++++++++++++++++++++++++ eth/input_test.go | 33 +++++++++++++++++++++ eth/transaction.go | 4 +-- eth/transaction_from_raw.go | 18 ++++++++---- eth/transaction_signing_test.go | 10 +++---- 8 files changed, 126 insertions(+), 16 deletions(-) create mode 100644 eth/input.go create mode 100644 eth/input_test.go diff --git a/eth/block_from_raw_test.go b/eth/block_from_raw_test.go index 7055a7d..736962e 100644 --- a/eth/block_from_raw_test.go +++ b/eth/block_from_raw_test.go @@ -100,7 +100,7 @@ func TestBlock_FromRaw_EIP2930(t *testing.T) { Gas: *eth.MustQuantity("0x7a120"), GasPrice: eth.MustQuantity("0x1"), Hash: *eth.MustHash("0x0503e1a4ead116b0d1c942c47d54d54d10ed5eaf3a57d2e974bc5cb2e8ee0c47"), - Input: *eth.MustData("0x1a8451e600000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000001000"), + Input: *eth.MustInput("0x1a8451e600000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000001000"), Nonce: *eth.MustQuantity("0xd"), To: eth.MustAddress("0xcccccccccccccccccccccccccccccccccccccccc"), Index: eth.MustQuantity("0xd"), diff --git a/eth/block_test.go b/eth/block_test.go index ad9b7f2..9160235 100644 --- a/eth/block_test.go +++ b/eth/block_test.go @@ -109,7 +109,7 @@ func TestMainnetGethBlocks(t *testing.T) { require.Equal(t, eth.Data("0xd783010302844765746887676f312e352e31856c696e7578"), block.ExtraData) require.Equal(t, true, block.Transactions[0].Populated) require.Equal(t, int64(0), block.Transactions[0].Index.Int64()) - require.Equal(t, eth.Data("0x"), block.Transactions[0].Input) + require.Equal(t, eth.Input("0x"), block.Transactions[0].Input) j, err := json.Marshal(&block) require.NoError(t, err) @@ -179,7 +179,7 @@ func TestMainnetParityBlocks(t *testing.T) { require.Equal(t, eth.Data("0xd783010302844765746887676f312e352e31856c696e7578"), block.ExtraData) require.Equal(t, true, block.Transactions[0].Populated) require.Equal(t, int64(0), block.Transactions[0].Index.Int64()) - require.Equal(t, eth.Data("0x"), block.Transactions[0].Input) + require.Equal(t, eth.Input("0x"), block.Transactions[0].Input) j, err := json.Marshal(&block) require.NoError(t, err) diff --git a/eth/data.go b/eth/data.go index 0601dfd..e54ce70 100644 --- a/eth/data.go +++ b/eth/data.go @@ -12,6 +12,7 @@ import ( ) type Data string +type Data4 Data type Data8 Data type Data20 Data type Data32 Data @@ -31,6 +32,16 @@ func NewData(value string) (*Data, error) { return &d, nil } +func NewData4(value string) (*Data4, error) { + parsed, err := validateHex(value, 4, "data") + if err != nil { + return nil, err + } + + d := Data4(parsed) + return &d, nil +} + func NewData8(value string) (*Data8, error) { parsed, err := validateHex(value, 8, "data") if err != nil { @@ -88,6 +99,15 @@ func MustData(value string) *Data { return d } +func MustData4(value string) *Data4 { + d, err := NewData4(value) + if err != nil { + panic(err) + } + + return d +} + func MustData8(value string) *Data8 { d, err := NewData8(value) if err != nil { diff --git a/eth/input.go b/eth/input.go new file mode 100644 index 0000000..5ea9d78 --- /dev/null +++ b/eth/input.go @@ -0,0 +1,51 @@ +package eth + +import ( + "github.com/INFURA/go-ethlibs/rlp" + "github.com/pkg/errors" + "strings" +) + +type Input Data + +func NewInput(value string) (*Input, error) { + if !strings.HasPrefix(value, "0x") { + return nil, errors.Errorf("invalid input: %s", value) + } + + a := Input(value) + return &a, nil +} + +func MustInput(value string) *Input { + a, err := NewInput(value) + if err != nil { + panic(err) + } + + return a +} + +func (i Input) String() string { + return string(i) +} + +func (i Input) Bytes() []byte { + return Data20(i).Bytes() +} + +// RLP returns the Input as an RLP-encoded string, note Input can never be null +func (i Input) RLP() rlp.Value { + return rlp.Value{ + String: strings.ToLower(i.String()), + } +} + +func (i Input) FunctionSelector() *Data4 { + if len(i) >= 10 { + b := Data4(i[:10]) + return &b + } + + return nil +} diff --git a/eth/input_test.go b/eth/input_test.go new file mode 100644 index 0000000..cd16398 --- /dev/null +++ b/eth/input_test.go @@ -0,0 +1,33 @@ +package eth + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestInput_FunctionSelector(t *testing.T) { + t.Run("empty input is nil", func(t *testing.T) { + input, err := NewInput("0x") + require.NoError(t, err) + require.Nil(t, input.FunctionSelector()) + }) + + t.Run("short input is nil", func(t *testing.T) { + input, err := NewInput("0x1234") + require.NoError(t, err) + require.Nil(t, input.FunctionSelector()) + }) + + t.Run("exact input returns selector", func(t *testing.T) { + input, err := NewInput("0x2fbbe334") + require.NoError(t, err) + require.Equal(t, input.FunctionSelector(), MustData4("0x2fbbe334")) + }) + + t.Run("input with arg returns selector", func(t *testing.T) { + input, err := NewInput("0xa41368620000000000000000000000000000000000000000000000000000000000000020") + require.NoError(t, err) + require.Equal(t, input.FunctionSelector(), MustData4("0xa4136862")) + }) + +} diff --git a/eth/transaction.go b/eth/transaction.go index c5f82e7..11eb0f5 100644 --- a/eth/transaction.go +++ b/eth/transaction.go @@ -25,7 +25,7 @@ type Transaction struct { From Address `json:"from"` Gas Quantity `json:"gas"` Hash Hash `json:"hash"` - Input Data `json:"input"` + Input Input `json:"input"` Nonce Quantity `json:"nonce"` To *Address `json:"to"` Index *Quantity `json:"transactionIndex"` @@ -366,7 +366,7 @@ func (t Transaction) MarshalJSON() ([]byte, error) { Gas Quantity `json:"gas"` GasPrice *Quantity `json:"gasPrice"` Hash Hash `json:"hash"` - Input Data `json:"input"` + Input Input `json:"input"` Nonce Quantity `json:"nonce"` To *Address `json:"to"` Index *Quantity `json:"transactionIndex"` diff --git a/eth/transaction_from_raw.go b/eth/transaction_from_raw.go index 1e4d65e..835d3d5 100644 --- a/eth/transaction_from_raw.go +++ b/eth/transaction_from_raw.go @@ -29,7 +29,7 @@ func (t *Transaction) FromRaw(input string) error { maxFeePerGas Quantity to *Address value Quantity - data Data + data Input v Quantity r Quantity s Quantity @@ -332,11 +332,11 @@ func (t *Transaction) FromRaw(input string) error { // Note that when calling this function, the receivers MUST be pointers never values, and for "optional" receivers // such as Address a pointer to a pointer must be passed. For example: // -// var ( -// addr *eth.Address -// nonce eth.Quantity -// ) -// err := rlpDecodeList(payload, &addr, &nonce) +// var ( +// addr *eth.Address +// nonce eth.Quantity +// ) +// err := rlpDecodeList(payload, &addr, &nonce) // // TODO: Consider making this function public once all receiver types in the eth package are supported. func rlpDecodeList(input interface{}, receivers ...interface{}) error { @@ -375,6 +375,12 @@ func rlpDecodeList(input interface{}, receivers ...interface{}) error { } *receiver = a } + case *Input: + d, err := NewInput(value.String) + if err != nil { + return errors.Wrapf(err, "could not decode list item %d to Input", i) + } + *receiver = *d case *Data: d, err := NewData(value.String) if err != nil { diff --git a/eth/transaction_signing_test.go b/eth/transaction_signing_test.go index be2572a..ac99dbb 100644 --- a/eth/transaction_signing_test.go +++ b/eth/transaction_signing_test.go @@ -18,7 +18,7 @@ func TestTransaction_Sign(t *testing.T) { Gas: eth.QuantityFromUInt64(90000), To: eth.MustAddress("0xc149Be1bcDFa69a94384b46A1F91350E5f81c1AB"), Value: eth.QuantityFromUInt64(950000000000000000), - Input: *eth.MustData("0x"), + Input: *eth.MustInput("0x"), } // This purposefully uses the already highly compromised keypair from the go-ethereum book: @@ -65,7 +65,7 @@ func TestTransaction_Sign_2(t *testing.T) { Gas: eth.QuantityFromUInt64(22000), To: eth.MustAddress("0x43700db832E9Ac990D36d6279A846608643c904E"), Value: eth.QuantityFromUInt64(1000000000), - Input: *eth.MustData("0x"), + Input: *eth.MustInput("0x"), } signed, err := tx.Sign("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19", chainId) @@ -113,7 +113,7 @@ func TestTransaction_Sign_3(t *testing.T) { Gas: eth.QuantityFromUInt64(22000), To: eth.MustAddress("0x43700db832E9Ac990D36d6279A846608643c904E"), Value: eth.QuantityFromUInt64(1000000000), - Input: *eth.MustData("0x"), + Input: *eth.MustInput("0x"), } signed, err := tx.Sign("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19", chainId) @@ -201,7 +201,7 @@ func TestTransaction_Sign_EIP2930(t *testing.T) { ChainId: &chainId, Gas: eth.QuantityFromInt64(0x62d4), GasPrice: eth.OptionalQuantityFromInt(0x3b9aca00), - Input: eth.Data("0x"), + Input: eth.Input("0x"), Nonce: eth.QuantityFromInt64(0), To: eth.MustAddress("0xdf0a88b2b68c673713a8ec826003676f272e3573"), Value: eth.QuantityFromInt64(0x1), @@ -276,7 +276,7 @@ func TestTransaction_Sign_EIP1559(t *testing.T) { ChainId: &chainId, MaxFeePerGas: eth.OptionalQuantityFromInt(15488430592 * 2), MaxPriorityFeePerGas: eth.OptionalQuantityFromInt(15488430592), - Input: eth.Data("0x"), + Input: eth.Input("0x"), Nonce: eth.QuantityFromInt64(0), To: eth.MustAddress("0xdf0a88b2b68c673713a8ec826003676f272e3573"), Value: eth.QuantityFromInt64(0x1), From 348b328c3255d58398772bff1ea9a7602c5277fa Mon Sep 17 00:00:00 2001 From: Antony Denyer Date: Wed, 10 Jan 2024 10:47:28 +0000 Subject: [PATCH 2/2] Update eth/input.go Could be any size Co-authored-by: Nicolas Hewitt <88170854+nthpool@users.noreply.github.com> --- eth/input.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eth/input.go b/eth/input.go index 5ea9d78..4a922cf 100644 --- a/eth/input.go +++ b/eth/input.go @@ -31,7 +31,7 @@ func (i Input) String() string { } func (i Input) Bytes() []byte { - return Data20(i).Bytes() + return Data(i).Bytes() } // RLP returns the Input as an RLP-encoded string, note Input can never be null