From 345a094ef4588128f4dad3254ba096a5eca59490 Mon Sep 17 00:00:00 2001 From: evgeniy-scherbina Date: Thu, 7 Mar 2024 14:08:32 -0500 Subject: [PATCH] Initial implementation of IBC Precompile --- accounts/abi/abi.go | 59 +++++++++++- accounts/abi/abi_extra_test.go | 159 +++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 accounts/abi/abi_extra_test.go diff --git a/accounts/abi/abi.go b/accounts/abi/abi.go index 6e1075c715fd..90bbec18bdb7 100644 --- a/accounts/abi/abi.go +++ b/accounts/abi/abi.go @@ -1,3 +1,8 @@ +// Copyright 2024 Kava Labs, Inc. +// Copyright 2024 Ava Labs, Inc. +// +// Derived from https://github.com/ava-labs/subnet-evm@49b0e31 +// // Copyright 2015 The go-ethereum Authors // This file is part of the go-ethereum library. // @@ -82,6 +87,32 @@ func (abi ABI) Pack(name string, args ...interface{}) ([]byte, error) { return append(method.ID, arguments...), nil } +// Upstream implementation provides API for `pack inputs` and `unpack outputs`. +// We extend API with `unpack inputs` functionality by adding getInputs and UnpackInput methods. +// Perhaps in the future we extend API with `pack outputs` functionality. + +// getInputs gets method's inputs metadata by method name according to the ABI specification. +// It can be used to decode encoded method's inputs. +// getInputs and getArguments have similar implementations +func (abi ABI) getInputs(name string) (Arguments, error) { + // since there can't be naming collisions with contracts and events, + // we need to decide whether we're calling a method or an event + var args Arguments + if method, ok := abi.Methods[name]; ok { + args = method.Inputs + } + if event, ok := abi.Events[name]; ok { + args = event.Inputs + } + if args == nil { + return nil, fmt.Errorf("abi: could not locate named method or event: %s", name) + } + return args, nil +} + +// getArguments gets method's outputs metadata by method name according to the ABI specification. +// It can be used to decode encoded method's outputs. +// getInputs and getArguments have similar implementations. func (abi ABI) getArguments(name string, data []byte) (Arguments, error) { // since there can't be naming collisions with contracts and events, // we need to decide whether we're calling a method or an event @@ -101,7 +132,18 @@ func (abi ABI) getArguments(name string, data []byte) (Arguments, error) { return args, nil } -// Unpack unpacks the output according to the abi specification. +// UnpackInput unpacks the input according to the ABI specification. +// UnpackInput and Unpack have similar implementations. +func (abi ABI) UnpackInput(name string, data []byte) ([]interface{}, error) { + args, err := abi.getInputs(name) + if err != nil { + return nil, err + } + return args.Unpack(data) +} + +// Unpack unpacks the output according to the ABI specification. +// UnpackInput and Unpack have similar implementations. func (abi ABI) Unpack(name string, data []byte) ([]interface{}, error) { args, err := abi.getArguments(name, data) if err != nil { @@ -110,6 +152,21 @@ func (abi ABI) Unpack(name string, data []byte) ([]interface{}, error) { return args.Unpack(data) } +// UnpackInputIntoInterface unpacks the input in v according to the ABI specification. +// It performs an additional copy. Please only use, if you want to unpack into a +// structure that does not strictly conform to the ABI structure (e.g. has additional arguments) +func (abi ABI) UnpackInputIntoInterface(v interface{}, name string, data []byte) error { + args, err := abi.getInputs(name) + if err != nil { + return err + } + unpacked, err := args.Unpack(data) + if err != nil { + return err + } + return args.Copy(v, unpacked) +} + // UnpackIntoInterface unpacks the output in v according to the abi specification. // It performs an additional copy. Please only use, if you want to unpack into a // structure that does not strictly conform to the abi structure (e.g. has additional arguments) diff --git a/accounts/abi/abi_extra_test.go b/accounts/abi/abi_extra_test.go new file mode 100644 index 000000000000..82d36be3197c --- /dev/null +++ b/accounts/abi/abi_extra_test.go @@ -0,0 +1,159 @@ +// Copyright 2024 Kava Labs, Inc. +// Copyright 2024 Ava Labs, Inc. +// +// Derived from https://github.com/ava-labs/subnet-evm@49b0e31 + +package abi + +import ( + "bytes" + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// Note: This file contains tests in addition to those found in go-ethereum. + +const TEST_ABI = `[{"type":"function","name":"receive","inputs":[{"name":"sender","type":"address"},{"name":"amount","type":"uint256"},{"name":"memo","type":"bytes"}],"outputs":[{"internalType":"bool","name":"isAllowed","type":"bool"}]}]` + +func TestUnpackInput(t *testing.T) { + abi, err := JSON(strings.NewReader(TEST_ABI)) + require.NoError(t, err) + + type inputType struct { + Sender common.Address + Amount *big.Int + Memo []byte + } + input := inputType{ + Sender: common.HexToAddress("0x02"), + Amount: big.NewInt(100), + Memo: []byte("hello"), + } + + rawData, err := abi.Pack("receive", input.Sender, input.Amount, input.Memo) + require.NoError(t, err) + + abi, err = JSON(strings.NewReader(TEST_ABI)) + require.NoError(t, err) + + for _, test := range []struct { + name string + extraPaddingBytes int + expectedErrorSubstring string + }{ + { + name: "No extra padding to input data", + }, + { + name: "Valid input data with 32 extra padding(%32) ", + extraPaddingBytes: 32, + }, + { + name: "Valid input data with 64 extra padding(%32)", + extraPaddingBytes: 64, + }, + { + name: "Valid input data with extra padding indivisible by 32", + extraPaddingBytes: 33, + }, + } { + { + t.Run(test.name, func(t *testing.T) { + // skip 4 byte selector + data := rawData[4:] + // Add extra padding to data + data = append(data, make([]byte, test.extraPaddingBytes)...) + + args, err := abi.UnpackInput("receive", data) // skips 4 byte selector + v := inputType{ + Sender: *ConvertType(args[0], new(common.Address)).(*common.Address), + Amount: ConvertType(args[1], new(big.Int)).(*big.Int), + Memo: *ConvertType(args[2], new([]byte)).(*[]byte), + } + + if test.expectedErrorSubstring != "" { + require.Error(t, err) + require.ErrorContains(t, err, test.expectedErrorSubstring) + } else { + require.NoError(t, err) + // Verify unpacked values match input + require.Equal(t, v.Amount, input.Amount) + require.EqualValues(t, v.Amount, input.Amount) + require.True(t, bytes.Equal(v.Memo, input.Memo)) + } + }) + } + } +} + +func TestUnpackInputIntoInterface(t *testing.T) { + abi, err := JSON(strings.NewReader(TEST_ABI)) + require.NoError(t, err) + + type inputType struct { + Sender common.Address + Amount *big.Int + Memo []byte + } + input := inputType{ + Sender: common.HexToAddress("0x02"), + Amount: big.NewInt(100), + Memo: []byte("hello"), + } + + rawData, err := abi.Pack("receive", input.Sender, input.Amount, input.Memo) + require.NoError(t, err) + + abi, err = JSON(strings.NewReader(TEST_ABI)) + require.NoError(t, err) + + for _, test := range []struct { + name string + extraPaddingBytes int + expectedErrorSubstring string + }{ + { + name: "No extra padding to input data", + }, + { + name: "Valid input data with 32 extra padding(%32) ", + extraPaddingBytes: 32, + }, + { + name: "Valid input data with 64 extra padding(%32)", + extraPaddingBytes: 64, + }, + { + name: "Valid input data with extra padding indivisible by 32", + extraPaddingBytes: 33, + }, + } { + { + t.Run(test.name, func(t *testing.T) { + // skip 4 byte selector + data := rawData[4:] + // Add extra padding to data + data = append(data, make([]byte, test.extraPaddingBytes)...) + + // Unpack into interface + var v inputType + err = abi.UnpackInputIntoInterface(&v, "receive", data) // skips 4 byte selector + + if test.expectedErrorSubstring != "" { + require.Error(t, err) + require.ErrorContains(t, err, test.expectedErrorSubstring) + } else { + require.NoError(t, err) + // Verify unpacked values match input + require.Equal(t, v.Amount, input.Amount) + require.EqualValues(t, v.Amount, input.Amount) + require.True(t, bytes.Equal(v.Memo, input.Memo)) + } + }) + } + } +}