diff --git a/op-chain-ops/script/precompile.go b/op-chain-ops/script/precompile.go new file mode 100644 index 0000000000000..57976dd45c004 --- /dev/null +++ b/op-chain-ops/script/precompile.go @@ -0,0 +1,441 @@ +package script + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math/big" + "reflect" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" +) + +// precompileFunc is a prepared function to perform a method call / field read with ABI decoding/encoding. +type precompileFunc struct { + goName string + abiSignature string + fn func(input []byte) ([]byte, error) +} + +// bytes4 computes a 4-byte method-selector ID of a solidity method signature +func bytes4(sig string) [4]byte { + return [4]byte(crypto.Keccak256([]byte(sig))[:4]) +} + +// big-endian uint64 to bytes32 +func b32(v uint64) []byte { + out := make([]byte, 32) + binary.BigEndian.PutUint64(out[24:], v) + return out +} + +// pad to multiple of 32 bytes +func pad32(data []byte) []byte { + out := bytes.Clone(data) + if len(out)%32 == 0 { + return out + } + return append(out, make([]byte, 32-(len(out)%32))...) +} + +// Precompile is a wrapper around a Go object, making it a precompile. +type Precompile[E any] struct { + Precompile E + + // abiMethods is effectively the jump-table for 4-byte ABI calls to the precompile. + abiMethods map[[4]byte]*precompileFunc +} + +var _ vm.PrecompiledContract = (*Precompile[struct{}])(nil) + +// NewPrecompile wraps a Go object into a Precompile. +// All exported fields and methods will have a corresponding ABI interface. +// Fields with a tag `evm:"-"` will be ignored. +// Field names and method names are adjusted to start with a lowercase character in the ABI signature. +// Method names may end with a `_X` where X must be the 4byte selector (this is sanity-checked), +// to support multiple variants of the same method with different ABI input parameters. +// Methods may return an error, which will result in a revert, rather than become an ABI encoded arg, if not nil. +// All precompile methods have 0 gas cost. +func NewPrecompile[E any](e E) (*Precompile[E], error) { + out := &Precompile[E]{Precompile: e, abiMethods: make(map[[4]byte]*precompileFunc)} + elemVal := reflect.ValueOf(e) + // setup methods (and if pointer, the indirect methods also) + if err := out.setupMethods(&elemVal); err != nil { + return nil, fmt.Errorf("failed to setup methods of precompile: %w", err) + } + // setup fields and embedded types (if a struct) + if err := out.setupFields(&elemVal); err != nil { + return nil, fmt.Errorf("failed to setup fields of precompile: %w", err) + } + return out, nil +} + +func (p *Precompile[E]) setupMethods(val *reflect.Value) error { + typ := val.Type() + methodCount := val.NumMethod() + for i := 0; i < methodCount; i++ { + methodDef := typ.Method(i) + if !methodDef.IsExported() { + continue + } + if err := p.setupMethod(val, &methodDef); err != nil { + return fmt.Errorf("failed to set up call-handler for method %d (%s): %w", i, methodDef.Name, err) + } + } + return nil +} + +func (p *Precompile[E]) setupMethod(selfVal *reflect.Value, methodDef *reflect.Method) error { + methodName := methodDef.Name + + abiFunctionName := methodName + // Solidity allows multiple functions with the same name, but different input params. + // So cut off the suffix after the last "_", to allow the different variants to be defined in Go. + variantSuffixIndex := strings.LastIndexByte(methodName, '_') + variantSuffix := "" + if variantSuffixIndex >= 0 { + abiFunctionName = methodName[:variantSuffixIndex] + variantSuffix = methodName[variantSuffixIndex+1:] // strip out the underscore + } + if len(abiFunctionName) == 0 { + return fmt.Errorf("ABI method name of %s must not be empty", methodDef.Name) + } + if lo := strings.ToLower(abiFunctionName[:1]); lo != abiFunctionName[:1] { + abiFunctionName = lo + abiFunctionName[1:] + } + // Prepare ABI definitions of call parameters. + inArgCount := methodDef.Type.NumIn() - 1 + if inArgCount < 0 { + return errors.New("expected method with receiver as first argument") + } + inArgs := make(abi.Arguments, inArgCount) + inArgTypes := make([]string, inArgCount) + inArgAllocators := make([]func() interface{}, inArgCount) + for i := 0; i < inArgCount; i++ { + argType := methodDef.Type.In(i + 1) // account for receiver + abiTyp, err := goTypeToABIType(argType) + if err != nil { + return fmt.Errorf("failed to determine ABI type of input arg %d: %w", i, err) + } + inArgs[i] = abi.Argument{ + Name: fmt.Sprintf("in_%d", i), + Type: abiTyp, + } + inArgAllocators[i] = func() interface{} { + return reflect.New(argType).Elem().Interface() + } + inArgTypes[i] = abiTyp.String() + } + methodSig := fmt.Sprintf("%v(%v)", abiFunctionName, strings.Join(inArgTypes, ",")) + byte4Sig := bytes4(methodSig) + if variantSuffix != "" { + if expected := fmt.Sprintf("%x", byte4Sig); expected != variantSuffix { + return fmt.Errorf("expected variant suffix %s for ABI method %s (Go: %s), but got %s", + expected, methodSig, methodDef.Name, variantSuffix) + } + } + if m, ok := p.abiMethods[byte4Sig]; ok { + return fmt.Errorf("method %s conflicts with existing ABI method %s (Go: %s), signature: %x", + methodDef.Name, m.abiSignature, m.goName, byte4Sig) + } + + outArgCount := methodDef.Type.NumOut() + // A Go method may return an error, which we do not ABI-encode, but rather forward as revert. + errReturn := false + if outArgCount > 0 { + errIndex := outArgCount - 1 + lastTyp := methodDef.Type.Out(errIndex) + if lastTyp.Kind() == reflect.Interface && lastTyp.Implements(reflect.TypeFor[error]()) { + outArgCount -= 1 + errReturn = true + } + } + // Prepare ABI definitions of return parameters. + outArgs := make(abi.Arguments, outArgCount) + for i := 0; i < outArgCount; i++ { + argType := methodDef.Type.Out(i) + abiTyp, err := goTypeToABIType(argType) + if err != nil { + return fmt.Errorf("failed to determine ABI type of output arg %d: %w", i, err) + } + outArgs[i] = abi.Argument{ + Name: fmt.Sprintf("out_%d", i), + Type: abiTyp, + } + } + + fn := makeFn(selfVal, &methodDef.Func, errReturn, inArgs, outArgs, inArgAllocators) + + p.abiMethods[byte4Sig] = &precompileFunc{ + goName: methodName, + abiSignature: methodSig, + fn: fn, + } + return nil +} + +// makeFn is a helper function to perform a method call: +// - ABI decoding of input +// - type conversion of inputs +// - actual function Go call +// - handling of error return value +// - and ABI encoding of outputs +func makeFn(selfVal, methodVal *reflect.Value, errReturn bool, inArgs, outArgs abi.Arguments, inArgAllocators []func() any) func(input []byte) ([]byte, error) { + return func(input []byte) ([]byte, error) { + // Unpack the ABI data into default Go types + inVals, err := inArgs.UnpackValues(input) + if err != nil { + return nil, fmt.Errorf("failed to decode input: %x\nerr: %w", input, err) + } + // Sanity check that the ABI util returned the expected number of inputs + if len(inVals) != len(inArgAllocators) { + return nil, fmt.Errorf("expected %d args, got %d", len(inArgAllocators), len(inVals)) + } + // Convert each default Go type into the expected opinionated Go type + callArgs := make([]reflect.Value, 0, 1+len(inArgAllocators)) + callArgs = append(callArgs, *selfVal) + for i, inAlloc := range inArgAllocators { + argSrc := inVals[i] + argDest := inAlloc() + argDest, err = convertType(argSrc, argDest) + if err != nil { + return nil, fmt.Errorf("failed to convert arg %d from Go type %T to %T: %w", i, argSrc, argDest, err) + } + callArgs = append(callArgs, reflect.ValueOf(argDest)) + } + // Call the precompile Go function + returnReflectVals := methodVal.Call(callArgs) + // Collect the return values + returnVals := make([]interface{}, len(returnReflectVals)) + for i := range returnReflectVals { + returnVals[i] = returnReflectVals[i].Interface() + } + if errReturn { + errIndex := len(returnVals) - 1 + if errV := returnVals[errIndex]; errV != nil { + if err, ok := errV.(error); ok { + return nil, err + } + } + returnVals = returnVals[:errIndex] + } + // Encode the return values + out, err := outArgs.PackValues(returnVals) + if err != nil { + return nil, fmt.Errorf("failed to encode return data: %w", err) + } + return out, nil + } +} + +// convertType is a helper to run the Geth type conversion util, +// forcing one Go type into another approximately equivalent Go type +// (handling pointers and underlying equivalent types). +func convertType(src, dest any) (out any, err error) { + defer func() { + if rErr := recover(); rErr != nil { + err = fmt.Errorf("ConvertType fail: %v", rErr) + } + }() + out = abi.ConvertType(src, dest) // no error return, just panics if invalid. + return +} + +// goTypeToABIType infers the geth ABI type definition from a Go reflect type definition. +func goTypeToABIType(typ reflect.Type) (abi.Type, error) { + solType, internalType, err := goTypeToSolidityType(typ) + if err != nil { + return abi.Type{}, err + } + return abi.NewType(solType, internalType, nil) +} + +// ABIInt256 is an alias for big.Int that is represented as int256 in ABI method signature, +// since big.Int interpretation defaults to uint256. +type ABIInt256 big.Int + +var abiInt256Type = reflect.TypeFor[ABIInt256]() + +func goTypeToSolidityType(typ reflect.Type) (typeDef, internalType string, err error) { + switch typ.Kind() { + case reflect.Int, reflect.Uint: + return "", "", fmt.Errorf("ints must have explicit size, type not valid: %s", typ) + case reflect.Bool, reflect.String, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strings.ToLower(typ.Kind().String()), "", nil + case reflect.Array: + if typ.Elem().Kind() == reflect.Uint8 { + if typ.Len() == 20 && typ.Name() == "Address" { + return "address", "", nil + } + if typ.Len() > 32 { + return "", "", fmt.Errorf("byte array too large: %d", typ.Len()) + } + return fmt.Sprintf("bytes%d", typ.Len()), "", nil + } + elemTyp, internalTyp, err := goTypeToSolidityType(typ.Elem()) + if err != nil { + return "", "", fmt.Errorf("unrecognized slice-elem type: %w", err) + } + if internalTyp != "" { + return "", "", fmt.Errorf("nested internal types not supported: %w", err) + } + return fmt.Sprintf("%s[%d]", elemTyp, typ.Len()), "", nil + case reflect.Slice: + if typ.Elem().Kind() == reflect.Uint8 { + return "bytes", "", nil + } + elemABITyp, internalTyp, err := goTypeToSolidityType(typ.Elem()) + if err != nil { + return "", "", fmt.Errorf("unrecognized slice-elem type: %w", err) + } + if internalTyp != "" { + return "", "", fmt.Errorf("nested internal types not supported: %w", err) + } + return elemABITyp + "[]", "", nil + case reflect.Struct: + if typ.AssignableTo(abiInt256Type) { + return "int256", "", nil + } + if typ.ConvertibleTo(reflect.TypeFor[big.Int]()) { + return "uint256", "", nil + } + // We can parse into abi.TupleTy in the future, if necessary + return "", "", fmt.Errorf("structs are not supported, cannot handle type %s", typ) + case reflect.Pointer: + elemABITyp, internalTyp, err := goTypeToSolidityType(typ.Elem()) + if err != nil { + return "", "", fmt.Errorf("unrecognized pointer-elem type: %w", err) + } + return elemABITyp, internalTyp, nil + default: + return "", "", fmt.Errorf("unrecognized typ: %s", typ) + } +} + +// setupFields registers all exported non-ignored fields as public ABI getters. +// Fields and methods of embedded structs are registered along the way. +func (p *Precompile[E]) setupFields(val *reflect.Value) error { + if val.Kind() == reflect.Pointer { + if val.IsNil() { + return fmt.Errorf("cannot setupFields of nil value (type: %s)", val.Type()) + } + inner := val.Elem() + if err := p.setupFields(&inner); err != nil { + return fmt.Errorf("failed to setupFields of inner pointer type: %w", err) + } + return nil + } + if val.Kind() != reflect.Struct { + return nil // ignore non-struct types + } + typ := val.Type() + fieldCount := val.NumField() + for i := 0; i < fieldCount; i++ { + fieldTyp := typ.Field(i) + if !fieldTyp.IsExported() { + continue + } + if tag, ok := fieldTyp.Tag.Lookup("evm"); ok && tag == "-" { + continue + } + fieldVal := val.Field(i) + if fieldTyp.Anonymous { + // process methods and inner fields of embedded fields + if err := p.setupMethods(&fieldVal); err != nil { + return fmt.Errorf("failed to setup methods of embedded field %s (type: %s): %w", + fieldTyp.Name, fieldTyp.Type, err) + } + if err := p.setupFields(&fieldVal); err != nil { + return fmt.Errorf("failed to setup fields of embedded field %s (type %s): %w", + fieldTyp.Name, fieldTyp.Type, err) + } + continue + } + if err := p.setupStructField(&fieldTyp, &fieldVal); err != nil { + return fmt.Errorf("failed to setup struct field %s (type %s): %w", fieldTyp.Name, fieldTyp.Type, err) + } + } + return nil +} + +// setupStructField registers a struct field as a public-getter ABI method. +func (p *Precompile[E]) setupStructField(fieldDef *reflect.StructField, fieldVal *reflect.Value) error { + abiFunctionName := fieldDef.Name + if len(abiFunctionName) == 0 { + return fmt.Errorf("ABI name of %s must not be empty", fieldDef.Name) + } + if lo := strings.ToLower(abiFunctionName[:1]); lo != abiFunctionName[:1] { + abiFunctionName = lo + abiFunctionName[1:] + } + + methodSig := abiFunctionName + "()" + byte4Sig := bytes4(methodSig) + if m, ok := p.abiMethods[byte4Sig]; ok { + return fmt.Errorf("struct field %s conflicts with existing ABI method %s (Go: %s), signature: %x", + fieldDef.Name, m.abiSignature, m.goName, byte4Sig) + } + abiTyp, err := goTypeToABIType(fieldDef.Type) + if err != nil { + return fmt.Errorf("failed to determine ABI type of struct field of type %s: %w", fieldDef.Type, err) + } + outArgs := abi.Arguments{ + { + Name: abiFunctionName, + Type: abiTyp, + }, + } + fn := func(input []byte) ([]byte, error) { + if len(input) != 0 { // 4 byte selector is already trimmed + return nil, fmt.Errorf("unexpected input: %x", input) + } + outData, err := outArgs.PackValues([]any{fieldVal.Interface()}) + if err != nil { + return nil, fmt.Errorf("method %s failed to pack return data: %w", methodSig, err) + } + return outData, nil + } + p.abiMethods[byte4Sig] = &precompileFunc{ + goName: fieldDef.Name, + abiSignature: methodSig, + fn: fn, + } + return nil +} + +func (p *Precompile[E]) RequiredGas(input []byte) uint64 { + return 0 +} + +func (p *Precompile[E]) Run(input []byte) ([]byte, error) { + if len(input) < 4 { + return encodeRevert(fmt.Errorf("expected at least 4 bytes, but got '%x'", input)) + } + sig := [4]byte(input[:4]) + params := input[4:] + fn, ok := p.abiMethods[sig] + if !ok { + return encodeRevert(fmt.Errorf("unrecognized 4 byte signature: %x", sig)) + } + out, err := fn.fn(params) + if err != nil { + return encodeRevert(fmt.Errorf("failed to run %s, ABI: %q, err: %w", fn.goName, fn.abiSignature, err)) + } + return out, nil +} + +var revertSelector = crypto.Keccak256([]byte("Error(string)"))[:4] + +func encodeRevert(outErr error) ([]byte, error) { + outErrStr := []byte(outErr.Error()) + out := make([]byte, 0, 4+32*2+len(outErrStr)+32) + out = append(out, revertSelector...) + out = append(out, b32(0x20)...) + out = append(out, b32(uint64(len(outErrStr)))...) + out = append(out, pad32(outErrStr)...) + return out, vm.ErrExecutionReverted // Geth EVM will pick this up as a revert with return-data +} diff --git a/op-chain-ops/script/precompile_test.go b/op-chain-ops/script/precompile_test.go new file mode 100644 index 0000000000000..fd5b0100a3b3e --- /dev/null +++ b/op-chain-ops/script/precompile_test.go @@ -0,0 +1,121 @@ +package script + +import ( + "errors" + "math/big" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" +) + +type EmbeddedExample struct { + Foo uint64 +} + +func (e *EmbeddedExample) TwoFoo() uint64 { + e.Foo *= 2 + return e.Foo +} + +type ExamplePrecompile struct { + EmbeddedExample + + Bar *big.Int + hello string + helloFrom string +} + +var testErr = errors.New("test err") + +func (e *ExamplePrecompile) Greet(name string) (string, error) { + if name == "mallory" { + return "", testErr + } + e.helloFrom = name + return e.hello + " " + name + "!", nil +} + +func (e *ExamplePrecompile) Things() (bar *big.Int, hello string, seen string) { + return e.Bar, e.hello, e.helloFrom +} + +func (e *ExamplePrecompile) AddAndMul(a, b, c uint64, x uint8) uint64 { + return (a + b + c) * uint64(x) +} + +func TestPrecompile(t *testing.T) { + e := &ExamplePrecompile{hello: "Hola", EmbeddedExample: EmbeddedExample{Foo: 42}, Bar: big.NewInt(123)} + p, err := NewPrecompile[*ExamplePrecompile](e) + require.NoError(t, err) + + for k, v := range p.abiMethods { + t.Logf("4byte: %x ABI: %s Go: %s", k, v.abiSignature, v.goName) + } + + // input/output + input := crypto.Keccak256([]byte("greet(string)"))[:4] + input = append(input, b32(0x20)...) // offset + input = append(input, b32(uint64(len("alice")))...) // length + input = append(input, "alice"...) + out, err := p.Run(input) + require.NoError(t, err) + require.Equal(t, e.helloFrom, "alice") + require.Equal(t, out[:32], b32(0x20)) + require.Equal(t, out[32:32*2], b32(uint64(len("Hola alice!")))) + require.Equal(t, out[32*2:32*3], pad32([]byte("Hola alice!"))) + + // error handling + input = crypto.Keccak256([]byte("greet(string)"))[:4] + input = append(input, b32(0x20)...) // offset + input = append(input, b32(uint64(len("mallory")))...) // length + input = append(input, "mallory"...) + out, err = p.Run(input) + require.Equal(t, err, vm.ErrExecutionReverted) + msg, err := abi.UnpackRevert(out) + require.NoError(t, err, "must unpack revert data") + require.True(t, strings.HasSuffix(msg, testErr.Error()), "revert data must end with the inner error") + + // field reads + input = crypto.Keccak256([]byte("foo()"))[:4] + out, err = p.Run(input) + require.NoError(t, err) + require.Equal(t, out, b32(42)) + + input = crypto.Keccak256([]byte("twoFoo()"))[:4] + out, err = p.Run(input) + require.NoError(t, err) + require.Equal(t, out, b32(42*2)) + + // persistent state changes + input = crypto.Keccak256([]byte("twoFoo()"))[:4] + out, err = p.Run(input) + require.NoError(t, err) + require.Equal(t, out, b32(42*2*2)) + + // multi-output + input = crypto.Keccak256([]byte("things()"))[:4] + out, err = p.Run(input) + require.NoError(t, err) + require.Equal(t, b32(123), out[:32]) + require.Equal(t, b32(32*3), out[32*1:32*2]) // offset of hello + require.Equal(t, b32(32*5), out[32*2:32*3]) // offset of seen + require.Equal(t, b32(uint64(len("Hola"))), out[32*3:32*4]) // length of hello + require.Equal(t, pad32([]byte("Hola")), out[32*4:32*5]) // hello content + require.Equal(t, b32(uint64(len("alice"))), out[32*5:32*6]) // length of seen + require.Equal(t, pad32([]byte("alice")), out[32*6:32*7]) // seen content + + // multi-input + input = crypto.Keccak256([]byte("addAndMul(uint64,uint64,uint64,uint8)"))[:4] + input = append(input, b32(42)...) + input = append(input, b32(100)...) + input = append(input, b32(7)...) + input = append(input, b32(3)...) + out, err = p.Run(input) + require.NoError(t, err) + require.Equal(t, b32((42+100+7)*3), out) +}