mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-17 18:30:45 +00:00
feat(abi): pack event and output + unpack input (#249)
## Why this should be merged Allows ICM code to use the `abi` package from `libevm` instead of `subnet-evm`, as part of a broader transition. ## How this works Port and refactor of the following `abi.ABI` methods (with differences described) from `subnet-evm`: 1. `PackEvent()` returns topics and packed data for events. Unlike `subnet-evm`, the returned `[]common.Hash` topic slice MAY be nil instead of the non-nil but empty equivalent. This is in keeping with the upstream `abi.MakeTopics()`. 2. `PackOutput()` returns packed output for methods. 3. `UnpackInputIntoInterface()` unpacks method inputs or event arguments into an arbitrary interface. Unlike the `subnet-evm` implementation, it doesn't perform any checks on the length of the input buffer as these are an Avalanche-specific feature that can be performed by the consumer. ## How this was tested Unit tests from `subnet-evm`, refactored for clarity.
This commit is contained in:
parent
29166f0fc8
commit
865e03ca6a
2 changed files with 410 additions and 0 deletions
116
accounts/abi/abi.libevm.go
Normal file
116
accounts/abi/abi.libevm.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
// Copyright 2025 the libevm authors.
|
||||||
|
//
|
||||||
|
// The libevm additions to go-ethereum are free software: you can redistribute
|
||||||
|
// them and/or modify them under the terms of the GNU Lesser General Public License
|
||||||
|
// as published by the Free Software Foundation, either version 3 of the License,
|
||||||
|
// or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// The libevm additions are distributed in the hope that they will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
|
||||||
|
// General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
|
// along with the go-ethereum library. If not, see
|
||||||
|
// <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package abi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ava-labs/libevm/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PackEvent packs the given `args` to conform with the ABI for the specified
|
||||||
|
// event. Arguments MUST match the order specified in the event ABI. Indexed
|
||||||
|
// arguments are returned as topics (the slice of which MAY be nil),as described
|
||||||
|
// in the [Solidity docs], while the rest are packed into `data`.
|
||||||
|
//
|
||||||
|
// Struct, slice, and array arguments are not supported except for `[]byte`.
|
||||||
|
//
|
||||||
|
// [Solidity docs]:
|
||||||
|
// https://docs.soliditylang.org/en/latest/abi-spec.html#encoding-of-indexed-event-parameters
|
||||||
|
func (abi ABI) PackEvent(name string, args ...any) (topics []common.Hash, data []byte, _ error) {
|
||||||
|
event, ok := abi.Events[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("event %q not found", name)
|
||||||
|
}
|
||||||
|
if got, want := len(args), len(event.Inputs); got != want {
|
||||||
|
return nil, nil, fmt.Errorf("event %q received %d inputs; expecting %d", name, got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
indexed []any
|
||||||
|
packed []any
|
||||||
|
packArgs Arguments
|
||||||
|
)
|
||||||
|
for i, arg := range args {
|
||||||
|
if inp := event.Inputs[i]; inp.Indexed {
|
||||||
|
indexed = append(indexed, arg)
|
||||||
|
} else {
|
||||||
|
packed = append(packed, arg)
|
||||||
|
packArgs = append(packArgs, inp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topics, err := makeTopics1D(indexed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if !event.Anonymous {
|
||||||
|
topics = append([]common.Hash{event.ID}, topics...)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = packArgs.Pack(packed...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return topics, data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTopics1D(a []any) ([]common.Hash, error) {
|
||||||
|
t, err := MakeTopics(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return t[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackOutput packs the given `args` to conform with the ABI for the specified
|
||||||
|
// method's output.
|
||||||
|
func (abi ABI) PackOutput(method string, args ...any) ([]byte, error) {
|
||||||
|
m, ok := abi.Methods[method]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("method %q not found", method)
|
||||||
|
}
|
||||||
|
return m.Outputs.Pack(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnpackInputIntoInterface is equivalent to [ABI.UnpackIntoInterface], with all
|
||||||
|
// the same caveats, except that it treats `data` as:
|
||||||
|
//
|
||||||
|
// 1. Input when handling a method; or
|
||||||
|
// 2. Unindexed data when handling an event.
|
||||||
|
func (abi ABI) UnpackInputIntoInterface(v any, methodOrEventName string, data []byte) error {
|
||||||
|
in, err := abi.methodOrEventInputs(methodOrEventName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
unpacked, err := in.Unpack(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return in.Copy(v, unpacked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (abi ABI) methodOrEventInputs(name string) (Arguments, error) {
|
||||||
|
if m, ok := abi.Methods[name]; ok {
|
||||||
|
return m.Inputs, nil
|
||||||
|
}
|
||||||
|
if ev, ok := abi.Events[name]; ok {
|
||||||
|
return ev.Inputs, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no method nor event %q", name)
|
||||||
|
}
|
||||||
294
accounts/abi/abi.libevm_test.go
Normal file
294
accounts/abi/abi.libevm_test.go
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
// Copyright 2025 the libevm authors.
|
||||||
|
//
|
||||||
|
// The libevm additions to go-ethereum are free software: you can redistribute
|
||||||
|
// them and/or modify them under the terms of the GNU Lesser General Public License
|
||||||
|
// as published by the Free Software Foundation, either version 3 of the License,
|
||||||
|
// or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// The libevm additions are distributed in the hope that they will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
|
||||||
|
// General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
|
// along with the go-ethereum library. If not, see
|
||||||
|
// <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package abi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ava-labs/libevm/common"
|
||||||
|
"github.com/ava-labs/libevm/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEventPackingRoundTrip(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
abiJSON string
|
||||||
|
eventName string
|
||||||
|
args []any
|
||||||
|
wantTopics []common.Hash
|
||||||
|
wantData []byte
|
||||||
|
wantUnpacked any // MUST be a pointer
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "received",
|
||||||
|
abiJSON: `[{
|
||||||
|
"type": "event",
|
||||||
|
"name": "received",
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{"indexed": false, "name": "sender", "type": "address"},
|
||||||
|
{"indexed": false, "name": "amount", "type": "uint256"},
|
||||||
|
{"indexed": false, "name": "memo", "type": "bytes"}
|
||||||
|
]
|
||||||
|
}]`,
|
||||||
|
eventName: "received",
|
||||||
|
args: []any{
|
||||||
|
common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"),
|
||||||
|
big.NewInt(1),
|
||||||
|
[]byte{0x88},
|
||||||
|
},
|
||||||
|
wantTopics: []common.Hash{
|
||||||
|
crypto.Keccak256Hash([]byte("received(address,uint256,bytes)")),
|
||||||
|
},
|
||||||
|
wantData: common.Hex2Bytes("000000000000000000000000376c47978271565f56deb45495afa69e59c16ab20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000018800000000000000000000000000000000000000000000000000000000000000"),
|
||||||
|
wantUnpacked: &struct {
|
||||||
|
Sender common.Address
|
||||||
|
Amount *big.Int
|
||||||
|
Memo []byte
|
||||||
|
}{
|
||||||
|
common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"),
|
||||||
|
big.NewInt(1),
|
||||||
|
[]byte{0x88},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "anonymous",
|
||||||
|
abiJSON: `[{
|
||||||
|
"type": "event",
|
||||||
|
"name": "received",
|
||||||
|
"anonymous": true,
|
||||||
|
"inputs": [
|
||||||
|
{"indexed": false, "name": "sender", "type": "address"},
|
||||||
|
{"indexed": false, "name": "amount", "type": "uint256"},
|
||||||
|
{"indexed": false, "name": "memo", "type": "bytes"}
|
||||||
|
]
|
||||||
|
}]`,
|
||||||
|
eventName: "received",
|
||||||
|
args: []any{
|
||||||
|
common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"),
|
||||||
|
big.NewInt(1),
|
||||||
|
[]byte{0x88},
|
||||||
|
},
|
||||||
|
wantTopics: nil,
|
||||||
|
wantData: common.Hex2Bytes("000000000000000000000000376c47978271565f56deb45495afa69e59c16ab20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000018800000000000000000000000000000000000000000000000000000000000000"),
|
||||||
|
wantUnpacked: &struct {
|
||||||
|
Sender common.Address
|
||||||
|
Amount *big.Int
|
||||||
|
Memo []byte
|
||||||
|
}{
|
||||||
|
common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"),
|
||||||
|
big.NewInt(1),
|
||||||
|
[]byte{0x88},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Transfer",
|
||||||
|
abiJSON: `[{
|
||||||
|
"type": "event",
|
||||||
|
"name": "Transfer",
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{"indexed": true, "name": "from", "type": "address"},
|
||||||
|
{"indexed": true, "name": "to", "type": "address"},
|
||||||
|
{"indexed": false, "name": "value", "type": "uint256"}
|
||||||
|
]
|
||||||
|
}]`,
|
||||||
|
eventName: "Transfer",
|
||||||
|
args: []any{
|
||||||
|
common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"),
|
||||||
|
common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"),
|
||||||
|
big.NewInt(100),
|
||||||
|
},
|
||||||
|
wantTopics: []common.Hash{
|
||||||
|
crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")),
|
||||||
|
common.HexToHash("0x0000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fc"),
|
||||||
|
common.HexToHash("0x000000000000000000000000376c47978271565f56deb45495afa69e59c16ab2"),
|
||||||
|
},
|
||||||
|
wantData: common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000064"),
|
||||||
|
wantUnpacked: &struct {
|
||||||
|
Value *big.Int
|
||||||
|
}{
|
||||||
|
big.NewInt(100),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
abi, err := JSON(strings.NewReader(test.abiJSON))
|
||||||
|
require.NoErrorf(t, err, "JSON(%s)", test.abiJSON)
|
||||||
|
|
||||||
|
t.Run("pack", func(t *testing.T) {
|
||||||
|
topics, data, err := abi.PackEvent(test.eventName, test.args...)
|
||||||
|
require.NoErrorf(t, err, "%T.PackEvent(%q, %v...)", abi, test.eventName, test.args)
|
||||||
|
|
||||||
|
assert.Equal(t, test.wantTopics, topics, "topics")
|
||||||
|
assert.Equal(t, test.wantData, data, "data")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unpack", func(t *testing.T) {
|
||||||
|
typ := reflect.TypeOf(test.wantUnpacked)
|
||||||
|
require.Equal(t, reflect.Pointer, typ.Kind(), "unpacking type MUST be a pointer")
|
||||||
|
|
||||||
|
got := reflect.New(typ.Elem()).Interface()
|
||||||
|
require.NoError(t, abi.UnpackInputIntoInterface(got, test.eventName, test.wantData))
|
||||||
|
|
||||||
|
if diff := cmp.Diff(test.wantUnpacked, got, compareBigInts()); diff != "" {
|
||||||
|
t.Errorf("%T.UnpackInputIntoInterface(%T) diff (-want +got):\n%s", abi, got, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// receiveFuncInput matches the input signature of the "receive" method defined
|
||||||
|
// by [receiveFuncABI].
|
||||||
|
type receiveFuncInput struct {
|
||||||
|
Sender common.Address
|
||||||
|
Amount *big.Int
|
||||||
|
Memo []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var receiveFuncABI ABI
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
receiveFuncABI, err = JSON(strings.NewReader(`
|
||||||
|
[{
|
||||||
|
"type": "function",
|
||||||
|
"name": "receive",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "sender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"type": "uint256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memo",
|
||||||
|
"type": "bytes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "isAllowed",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "randomNumber",
|
||||||
|
"type": "uint64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
`))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnpackInputIntoInterface(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
extraPaddingBytes int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No extra padding to input data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid input data with 32 extra bytes",
|
||||||
|
extraPaddingBytes: 32,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid input data with 64 extra bytes",
|
||||||
|
extraPaddingBytes: 64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid input data with 33 extra bytes",
|
||||||
|
extraPaddingBytes: 33,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
abi := receiveFuncABI
|
||||||
|
const method = "receive"
|
||||||
|
|
||||||
|
input := receiveFuncInput{
|
||||||
|
Sender: common.Address{2},
|
||||||
|
Amount: big.NewInt(100),
|
||||||
|
Memo: []byte("hello"),
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []any{input.Sender, input.Amount, input.Memo}
|
||||||
|
packed, err := abi.Pack(method, args...)
|
||||||
|
require.NoErrorf(t, err, "%T.Pack(%q, %v...)", abi, method, args)
|
||||||
|
|
||||||
|
// skip 4 byte selector
|
||||||
|
data := append(packed[4:], make([]byte, test.extraPaddingBytes)...)
|
||||||
|
|
||||||
|
var got receiveFuncInput
|
||||||
|
require.NoErrorf(t, abi.UnpackInputIntoInterface(&got, method, data), "%T.UnpackInputIntoInterface()", abi)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(input, got, compareBigInts()); diff != "" {
|
||||||
|
t.Errorf("%T.Pack() -> %T.UnpackInputIntoInterface(%T, ...) round-trip diff (-want +got):\n%s", abi, abi, got, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackOutput(t *testing.T) {
|
||||||
|
abi := receiveFuncABI
|
||||||
|
const (
|
||||||
|
method = "receive"
|
||||||
|
boolReturn = true
|
||||||
|
uint64Return = uint64(42)
|
||||||
|
)
|
||||||
|
want := []any{boolReturn, uint64Return}
|
||||||
|
|
||||||
|
packed, err := abi.PackOutput(method, boolReturn, uint64Return)
|
||||||
|
require.NoErrorf(t, err, "%T.PackOutput(%q, %v, %v)", abi, method, boolReturn, uint64Return)
|
||||||
|
|
||||||
|
m := abi.Methods["receive"]
|
||||||
|
got, err := m.Outputs.Unpack(packed)
|
||||||
|
require.NoErrorf(t, err, "%T.Outputs.Unpack(%T.PackOutput())", m, abi)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("%T.PackOutput() -> %T.Outputs.Unpack() round-trip diff (-want +got):\n%s", abi, m, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareBigInts() cmp.Option {
|
||||||
|
return cmp.Comparer(func(a, b *big.Int) bool {
|
||||||
|
switch aN, bN := a == nil, b == nil; {
|
||||||
|
case aN != bN:
|
||||||
|
return false
|
||||||
|
case aN && bN:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return a.Cmp(b) == 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue