diff --git a/accounts/abi/bind/bind.go b/accounts/abi/bind/bind.go index f40dcbea04..c45ad34f42 100644 --- a/accounts/abi/bind/bind.go +++ b/accounts/abi/bind/bind.go @@ -53,7 +53,7 @@ const ( LangGo Lang = iota ) -func isKeyWord(arg string) bool { +func IsKeyWord(arg string) bool { switch arg { case "break": case "case": @@ -165,7 +165,7 @@ func BindHelper(types []string, abis []string, bytecodes []string, fsigs []map[s normalized.Inputs = make([]abi.Argument, len(original.Inputs)) copy(normalized.Inputs, original.Inputs) for j, input := range normalized.Inputs { - if input.Name == "" || isKeyWord(input.Name) { + if input.Name == "" || IsKeyWord(input.Name) { normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j) } if hasStruct(input.Type) { @@ -209,7 +209,7 @@ func BindHelper(types []string, abis []string, bytecodes []string, fsigs []map[s normalized.Inputs = make([]abi.Argument, len(original.Inputs)) copy(normalized.Inputs, original.Inputs) for j, input := range normalized.Inputs { - if input.Name == "" || isKeyWord(input.Name) { + if input.Name == "" || IsKeyWord(input.Name) { normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j) } // Event is a bit special, we need to define event struct in binding, @@ -298,6 +298,7 @@ func BindHelper(types []string, abis []string, bytecodes []string, fsigs []map[s funcs := map[string]interface{}{ "bindtype": bindType[lang], + "bindtypenew": bindTypeNew[lang], "bindtopictype": bindTopicType[lang], "namedtype": namedType[lang], "capitalise": capitalise, @@ -328,6 +329,10 @@ var bindType = map[Lang]func(kind abi.Type, structs map[string]*TmplStruct) stri LangGo: bindTypeGo, } +var bindTypeNew = map[Lang]func(kind abi.Type, structs map[string]*TmplStruct) string{ + LangGo: bindTypeNewGo, +} + // bindBasicTypeGo converts basic solidity types(except array, slice and tuple) to Go ones. func bindBasicTypeGo(kind abi.Type) string { switch kind.T { @@ -352,6 +357,39 @@ func bindBasicTypeGo(kind abi.Type) string { } } +// bindNewTypeNewGo converts new types to Go ones. +func bindTypeNewGo(kind abi.Type, structs map[string]*TmplStruct) string { + switch kind.T { + case abi.TupleTy: + return structs[kind.TupleRawName+kind.String()].Name + "{}" + case abi.ArrayTy: + return fmt.Sprintf("[%d]", kind.Size) + bindTypeGo(*kind.Elem, structs) + "{}" + case abi.SliceTy: + return "nil" + case abi.AddressTy: + return "common.Address{}" + case abi.IntTy, abi.UintTy: + parts := regexp.MustCompile(`(u)?int([0-9]*)`).FindStringSubmatch(kind.String()) + switch parts[2] { + case "8", "16", "32", "64": + return "0" + } + return "new(big.Int)" + case abi.FixedBytesTy: + return fmt.Sprintf("[%d]byte", kind.Size) + "{}" + case abi.BytesTy: + return "[]byte{}" + case abi.FunctionTy: + return "[24]byte{}" + case abi.BoolTy: + return "false" + case abi.StringTy: + return `""` + default: + return "nil" + } +} + // bindTypeGo converts solidity types to Go ones. Since there is no clear mapping // from all Solidity types to Go ones (e.g. uint17), those that cannot be exactly // mapped will use an upscaled type (e.g. BigDecimal). diff --git a/accounts/abi/bind/precompilebind/precompile_bind.go b/accounts/abi/bind/precompilebind/precompile_bind.go index 483470a295..199e6cf36d 100644 --- a/accounts/abi/bind/precompilebind/precompile_bind.go +++ b/accounts/abi/bind/precompilebind/precompile_bind.go @@ -33,7 +33,9 @@ package precompilebind import ( "errors" "fmt" + "strings" + "github.com/ava-labs/subnet-evm/accounts/abi" "github.com/ava-labs/subnet-evm/accounts/abi/bind" ) @@ -55,7 +57,7 @@ type BindedFiles struct { } // PrecompileBind generates a Go binding for a precompiled contract. It returns config binding and contract binding. -func PrecompileBind(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, lang bind.Lang, libs map[string]string, aliases map[string]string, abifilename string, generateTests bool) (BindedFiles, error) { +func PrecompileBind(types []string, abiData string, bytecodes []string, fsigs []map[string]string, pkg string, lang bind.Lang, libs map[string]string, aliases map[string]string, abifilename string, generateTests bool) (BindedFiles, error) { // create hooks configHook := createPrecompileHook(abifilename, tmplSourcePrecompileConfigGo) contractHook := createPrecompileHook(abifilename, tmplSourcePrecompileContractGo) @@ -63,6 +65,12 @@ func PrecompileBind(types []string, abis []string, bytecodes []string, fsigs []m configTestHook := createPrecompileHook(abifilename, tmplSourcePrecompileConfigTestGo) contractTestHook := createPrecompileHook(abifilename, tmplSourcePrecompileContractTestGo) + if err := verifyABI(abiData); err != nil { + return BindedFiles{}, err + } + + abis := []string{abiData} + configBind, err := bind.BindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, configHook) if err != nil { return BindedFiles{}, fmt.Errorf("failed to generate config binding: %w", err) @@ -114,16 +122,10 @@ func createPrecompileHook(abifilename string, template string) bind.BindHook { contract := contracts[types[0]] for k, v := range contract.Transacts { - if err := checkOutputName(*v); err != nil { - return nil, "", err - } funcs[k] = v } for k, v := range contract.Calls { - if err := checkOutputName(*v); err != nil { - return nil, "", err - } funcs[k] = v } isAllowList := allowListEnabled(funcs) @@ -163,11 +165,42 @@ func allowListEnabled(funcs map[string]*bind.TmplMethod) bool { return true } -func checkOutputName(method bind.TmplMethod) error { - for _, output := range method.Original.Outputs { - if output.Name == "" { - return fmt.Errorf("ABI outputs for %s require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", method.Original.Name) +func verifyABI(abiData string) error { + // check abi first + evmABI, err := abi.JSON(strings.NewReader(abiData)) + if err != nil { + return err + } + if len(evmABI.Methods) == 0 { + return errors.New("no ABI methods found") + } + for _, method := range evmABI.Methods { + names := make(map[string]bool) + for _, input := range method.Inputs { + if bind.IsKeyWord(input.Name) { + return fmt.Errorf("input name %s is a keyword", input.Name) + } + name := abi.ToCamelCase(input.Name) + if names[name] { + return fmt.Errorf("normalized input name is duplicated: %s", name) + } + names[name] = true + } + names = make(map[string]bool) + for _, output := range method.Outputs { + if output.Name == "" { + return fmt.Errorf("ABI outputs for %s require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", method.Name) + } + if bind.IsKeyWord(output.Name) { + return fmt.Errorf("output name %s is a keyword", output.Name) + } + name := abi.ToCamelCase(output.Name) + if names[name] { + return fmt.Errorf("normalized output name is duplicated: %s", name) + } + names[name] = true } } + return nil } diff --git a/accounts/abi/bind/precompilebind/precompile_bind_test.go b/accounts/abi/bind/precompilebind/precompile_bind_test.go index 4c98a14e62..ee4e56763d 100644 --- a/accounts/abi/bind/precompilebind/precompile_bind_test.go +++ b/accounts/abi/bind/precompilebind/precompile_bind_test.go @@ -27,83 +27,544 @@ package precompilebind import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" "testing" "github.com/ava-labs/subnet-evm/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) -var bindFailedTests = []struct { +var bindTests = []struct { name string contract string - bytecode []string - abi []string - errorMsg string - fsigs []map[string]string - libs map[string]string - aliases map[string]string - types []string + abi string + imports string + tester string + errMsg string }{ { - `AnonOutputChecker`, ``, - []string{``}, - []string{` + "AnonOutputChecker", + "", + ` [ {"type":"function","name":"anonOutput","constant":true,"inputs":[],"outputs":[{"name":"","type":"string"}]} ] - `}, + `, + "", + "", "ABI outputs for anonOutput require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", - nil, - nil, - nil, - nil, }, - { - `AnonOutputsChecker`, ``, - []string{``}, - []string{` + "AnonOutputsChecker", + "", + ` [ {"type":"function","name":"anonOutputs","constant":true,"inputs":[],"outputs":[{"name":"","type":"string"},{"name":"","type":"string"}]} ] - `}, + `, + "", + "", "ABI outputs for anonOutputs require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", - nil, - nil, - nil, - nil, }, - { - `MixedOutputsChecker`, ``, - []string{``}, - []string{` + "MixedOutputsChecker", + "", + ` [ {"type":"function","name":"mixedOutputs","constant":true,"inputs":[],"outputs":[{"name":"","type":"string"},{"name":"str","type":"string"}]} ] - `}, + `, + "", + "", "ABI outputs for mixedOutputs require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", - nil, - nil, - nil, - nil, }, -} + // Test that module is generated correctly + { + `EmptyContract`, + `contract EmptyContract {}`, + "[]", + "", + "", + "no ABI methods found", + }, + // Test that named and anonymous inputs are handled correctly + { + `InputChecker`, ``, + ` + [ + {"type":"function","name":"noInput","constant":true,"inputs":[],"outputs":[]}, + {"type":"function","name":"namedInput","constant":true,"inputs":[{"name":"str","type":"string"}],"outputs":[]}, + {"type":"function","name":"namedInputs","constant":true,"inputs":[{"name":"str1","type":"string"},{"name":"str2","type":"string"}],"outputs":[]} + ] + `, + ` + "github.com/stretchr/testify/require" + `, + ` + testInput := "test" + packedInput, err := PackNamedInput(testInput) + require.NoError(t, err) + // remove the first 4 bytes of the packed input + packedInput = packedInput[4:] + unpackedInput, err := UnpackNamedInputInput(packedInput) + require.NoError(t, err) + require.Equal(t, testInput, unpackedInput) + + testInputStruct := NamedInputsInput{ + Str1: "test1", + Str2: "test2", + } + packedInputStruct, err := PackNamedInputs(testInputStruct) + require.NoError(t, err) + // remove the first 4 bytes of the packed input + packedInputStruct = packedInputStruct[4:] + unpackedInputStruct, err := UnpackNamedInputsInput(packedInputStruct) + require.NoError(t, err) + require.Equal(t, unpackedInputStruct, testInputStruct) + `, + "", + }, + // Test that named and anonymous outputs are handled correctly + { + `OutputChecker`, ``, + ` + [ + {"type":"function","name":"noOutput","constant":true,"inputs":[],"outputs":[]}, + {"type":"function","name":"namedOutput","constant":true,"inputs":[],"outputs":[{"name":"str","type":"string"}]}, + {"type":"function","name":"namedOutputs","constant":true,"inputs":[],"outputs":[{"name":"str1","type":"string"},{"name":"str2","type":"string"}]} + ] + `, + ` + "github.com/stretchr/testify/require" + `, + ` + testOutput := "test" + packedOutput, err := PackNamedOutputOutput(testOutput) + require.NoError(t, err) + unpackedOutput, err := UnpackNamedOutputOutput(packedOutput) + require.NoError(t, err) + require.Equal(t, testOutput, unpackedOutput) + + testNamedOutputs := NamedOutputsOutput{ + Str1: "test1", + Str2: "test2", + } + packedNamedOutputs, err := PackNamedOutputsOutput(testNamedOutputs) + require.NoError(t, err) + unpackedNamedOutputs, err := UnpackNamedOutputsOutput(packedNamedOutputs) + require.NoError(t, err) + require.Equal(t, testNamedOutputs, unpackedNamedOutputs) + `, + "", + }, + { + `Tupler`, + ` + interface Tupler { + function tuple() constant returns (string a, int b, bytes32 c); + } + `, + `[{"constant":true,"inputs":[],"name":"tuple","outputs":[{"name":"a","type":"string"},{"name":"b","type":"int256"},{"name":"c","type":"bytes32"}],"type":"function"}]`, + ` + "math/big" + "github.com/stretchr/testify/require" + `, + ` + testOutput := TupleOutput{"Hi", big.NewInt(123), [32]byte{1, 2, 3}} + packedOutput, err := PackTupleOutput(testOutput) + require.NoError(t, err) + unpackedOutput, err := UnpackTupleOutput(packedOutput) + require.NoError(t, err) + require.Equal(t, testOutput, unpackedOutput) + `, + "", + }, + { + `Slicer`, + ` + interface Slicer { + function echoAddresses(address[] input) constant returns (address[] output); + function echoInts(int[] input) constant returns (int[] output); + function echoFancyInts(uint8[23] input) constant returns (uint8[23] output); + function echoBools(bool[] input) constant returns (bool[] output); + } + `, + `[{"constant":true,"inputs":[{"name":"input","type":"address[]"}],"name":"echoAddresses","outputs":[{"name":"output","type":"address[]"}],"type":"function"},{"constant":true,"inputs":[{"name":"input","type":"uint8[23]"}],"name":"echoFancyInts","outputs":[{"name":"output","type":"uint8[23]"}],"type":"function"},{"constant":true,"inputs":[{"name":"input","type":"int256[]"}],"name":"echoInts","outputs":[{"name":"output","type":"int256[]"}],"type":"function"},{"constant":true,"inputs":[{"name":"input","type":"bool[]"}],"name":"echoBools","outputs":[{"name":"output","type":"bool[]"}],"type":"function"}]`, + ` + "math/big" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/common" + `, + ` + testArgs := []common.Address{common.HexToAddress("1"), common.HexToAddress("2"), common.HexToAddress("3")} + packedOutput, err := PackEchoAddressesOutput(testArgs) + require.NoError(t, err) + unpackedOutput, err := UnpackEchoAddressesOutput(packedOutput) + require.NoError(t, err) + require.Equal(t, testArgs, unpackedOutput) + packedInput, err := PackEchoAddresses(testArgs) + // remove the first 4 bytes of the packed input + packedInput = packedInput[4:] + require.NoError(t, err) + unpackedInput, err := UnpackEchoAddressesInput(packedInput) + require.NoError(t, err) + require.Equal(t, testArgs, unpackedInput) + + testArgs2 := []*big.Int{common.Big1, common.Big2, common.Big3} + packedOutput2, err := PackEchoIntsOutput(testArgs2) + require.NoError(t, err) + unpackedOutput2, err := UnpackEchoIntsOutput(packedOutput2) + require.NoError(t, err) + require.Equal(t, testArgs2, unpackedOutput2) + packedInput2, err := PackEchoInts(testArgs2) + // remove the first 4 bytes of the packed input + packedInput2 = packedInput2[4:] + require.NoError(t, err) + unpackedInput2, err := UnpackEchoIntsInput(packedInput2) + require.NoError(t, err) + require.Equal(t, testArgs2, unpackedInput2) + + testArgs3 := [23]uint8{1, 2, 3} + packedOutput3, err := PackEchoFancyIntsOutput(testArgs3) + require.NoError(t, err) + unpackedOutput3, err := UnpackEchoFancyIntsOutput(packedOutput3) + require.NoError(t, err) + require.Equal(t, testArgs3, unpackedOutput3) + packedInput3, err := PackEchoFancyInts(testArgs3) + // remove the first 4 bytes of the packed input + packedInput3 = packedInput3[4:] + require.NoError(t, err) + unpackedInput3, err := UnpackEchoFancyIntsInput(packedInput3) + require.NoError(t, err) + require.Equal(t, testArgs3, unpackedInput3) + + testArgs4 := []bool{true, false, true} + packedOutput4, err := PackEchoBoolsOutput(testArgs4) + require.NoError(t, err) + unpackedOutput4, err := UnpackEchoBoolsOutput(packedOutput4) + require.NoError(t, err) + require.Equal(t, testArgs4, unpackedOutput4) + packedInput4, err := PackEchoBools(testArgs4) + // remove the first 4 bytes of the packed input + packedInput4 = packedInput4[4:] + require.NoError(t, err) + unpackedInput4, err := UnpackEchoBoolsInput(packedInput4) + require.NoError(t, err) + require.Equal(t, testArgs4, unpackedInput4) + `, + "", + }, + { + `Fallback`, + ` + interface Fallback { + fallback() external payable; + + receive() external payable; + function testFunction(uint t) external; + } + `, + `[{"stateMutability":"payable","type":"fallback"},{"inputs":[{"internalType":"uint256","name":"t","type":"uint256"}],"name":"testFunction","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]`, + ` + "github.com/stretchr/testify/require" + "math/big" + `, + ` + packedInput, err := PackTestFunction(big.NewInt(5)) + require.NoError(t, err) + // remove the first 4 bytes of the packed input + packedInput = packedInput[4:] + unpackedInput, err := UnpackTestFunctionInput(packedInput) + require.NoError(t, err) + require.Equal(t, big.NewInt(5), unpackedInput) + `, + "", + }, + { + `Structs`, + ` + interface Struct { + struct A { + bytes32 B; + } + function F() external view returns (A[] memory a, uint256[] memory c, bool[] memory d); + function G() external view returns (A[] memory a); + } + `, + `[{"inputs":[],"name":"F","outputs":[{"components":[{"internalType":"bytes32","name":"B","type":"bytes32"}],"internalType":"struct Structs.A[]","name":"a","type":"tuple[]"},{"internalType":"uint256[]","name":"c","type":"uint256[]"},{"internalType":"bool[]","name":"d","type":"bool[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"G","outputs":[{"components":[{"internalType":"bytes32","name":"B","type":"bytes32"}],"internalType":"struct Structs.A[]","name":"a","type":"tuple[]"}],"stateMutability":"view","type":"function"}]`, + ` + "github.com/stretchr/testify/require" + "math/big" + `, + ` + testOutput := FOutput{ + A: []StructsA{ + { + B: [32]byte{1}, + }, + }, + C: []*big.Int{big.NewInt(2)}, + D: []bool{true,false}, + } + packedOutput, err := PackFOutput(testOutput) + require.NoError(t, err) + unpackedInput, err := UnpackFOutput(packedOutput) + require.NoError(t, err) + require.Equal(t, testOutput, unpackedInput) + `, + "", + }, + { + `Underscorer`, + ` + interface Underscorer { + function UnderscoredOutput() external returns (int _int, string _string); + } + `, + `[{"inputs":[],"name":"UnderscoredOutput","outputs":[{"internalType":"int256","name":"_int","type":"int256"},{"internalType":"string","name":"_string","type":"string"}],"stateMutability":"nonpayable","type":"function"}]`, + ` + "github.com/stretchr/testify/require" + "math/big" + `, + ` + testOutput := UnderscoredOutputOutput{ + Int: big.NewInt(5), + String: "hello", + } + packedOutput, err := PackUnderscoredOutputOutput(testOutput) + require.NoError(t, err) + unpackedInput, err := UnpackUnderscoredOutputOutput(packedOutput) + require.NoError(t, err) + require.Equal(t, testOutput, unpackedInput) + `, + "", + }, + { + `OutputCollision`, + ` + interface Collision { + function LowerLowerCollision() external returns (int _res, int res, int res_); + `, + `[{"inputs":[],"name":"LowerLowerCollision","outputs":[{"internalType":"int256","name":"_res","type":"int256"},{"internalType":"int256","name":"res","type":"int256"},{"internalType":"int256","name":"res_","type":"int256"}],"stateMutability":"nonpayable","type":"function"}]`, + "", + "", + "normalized output name is duplicated", + }, + + { + `InputCollision`, + ` + interface Collision { + function LowerUpperCollision(int _res, int Res) external; + } + `, + `[{"inputs":[{"internalType":"int256","name":"_res","type":"int256"},{"internalType":"int256","name":"Res","type":"int256"}],"name":"LowerUpperCollision","outputs":[],"stateMutability":"nonpayable","type":"function"}]`, "", + "", + "normalized input name is duplicated", + }, + { + `DeeplyNestedArray`, + ` + interface DeeplyNestedArray { + function storeDeepUintArray(uint64[3][4][5] arr) external public; + function retrieveDeepArray() public external view returns (uint64[3][4][5] arr); + } + `, + `[{"inputs":[],"name":"retrieveDeepArray","outputs":[{"internalType":"uint64[3][4][5]","name":"arr","type":"uint64[3][4][5]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint64[3][4][5]","name":"arr","type":"uint64[3][4][5]"}],"name":"storeDeepUintArray","outputs":[],"stateMutability":"nonpayable","type":"function"}]`, ` + "github.com/stretchr/testify/require" + `, + ` + testArr := [5][4][3]uint64{ + { + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9}, + {10, 11, 12}, + }, + { + {13, 14, 15}, + {16, 17, 18}, + {19, 20, 21}, + {22, 23, 24}, + }, + } + packedInput, err := PackStoreDeepUintArray(testArr) + require.NoError(t, err) + // remove the first 4 bytes of the packed input + packedInput = packedInput[4:] + unpackedInput, err := UnpackStoreDeepUintArrayInput(packedInput) + require.NoError(t, err) + require.Equal(t, testArr, unpackedInput) + + packedOutput, err := PackRetrieveDeepArrayOutput(testArr) + require.NoError(t, err) + unpackedOutput, err := UnpackRetrieveDeepArrayOutput(packedOutput) + require.NoError(t, err) + require.Equal(t, testArr, unpackedOutput) + `, + "", + }, + { + "RangeKeyword", + ` + interface keywordcontract { + function functionWithKeywordParameter(uint8 func, uint8 range) external pure; + } + `, + `[{"inputs":[{"internalType":"uint8","name":"func","type":"uint8"},{"internalType":"uint8","name":"range","type":"uint8"}],"name":"functionWithKeywordParameter","outputs":[],"stateMutability":"pure","type":"function"}]`, + "", + "", + "input name func is a keyword", + }, + { + `HelloWorld`, + `interface IHelloWorld is IAllowList { + // sayHello returns the stored greeting string + function sayHello() external view returns (string calldata result); -func TestPrecompileBindings(t *testing.T) { - golangBindingsFailure(t) + // setGreeting stores the greeting string + function setGreeting(string calldata response) external; + } + `, + `[{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"readAllowList","outputs":[{"internalType":"uint256","name":"role","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"sayHello","outputs":[{"internalType":"string","name":"result","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setEnabled","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"response","type":"string"}],"name":"setGreeting","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setNone","outputs":[],"stateMutability":"nonpayable","type":"function"}]`, + `"github.com/stretchr/testify/require"`, + ` + testGreeting := "test" + packedGreeting, err := PackSetGreeting(testGreeting) + require.NoError(t, err) + // remove the first 4 bytes of the packed greeting + packedGreeting = packedGreeting[4:] + unpackedGreeting, err := UnpackSetGreetingInput(packedGreeting) + require.NoError(t, err) + require.Equal(t, testGreeting, unpackedGreeting) + `, + "", + }, + { + `HelloWorldNoAL`, + `interface IHelloWorld{ + // sayHello returns the stored greeting string + function sayHello() external view returns (string calldata result); + + // setGreeting stores the greeting string + function setGreeting(string calldata response) external; + } + `, + `[{"inputs":[],"name":"sayHello","outputs":[{"internalType":"string","name":"result","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"response","type":"string"}],"name":"setGreeting","outputs":[],"stateMutability":"nonpayable","type":"function"}]`, + `"github.com/stretchr/testify/require"`, + ` + testGreeting := "test" + packedGreeting, err := PackSetGreeting(testGreeting) + require.NoError(t, err) + // remove the first 4 bytes of the packed greeting + packedGreeting = packedGreeting[4:] + unpackedGreeting, err := UnpackSetGreetingInput(packedGreeting) + require.NoError(t, err) + require.Equal(t, testGreeting, unpackedGreeting) + `, + "", + }, } -func golangBindingsFailure(t *testing.T) { +// Tests that packages generated by the binder can be successfully compiled and +// the requested tester run against it. +func TestPrecompileBind(t *testing.T) { + // Skip the test if no Go command can be found + gocmd := runtime.GOROOT() + "/bin/go" + if !common.FileExist(gocmd) { + t.Skip("go sdk not found for testing") + } + // Create a temporary workspace for the test suite + ws := t.TempDir() + + pkg := filepath.Join(ws, "bindtest") + if err := os.MkdirAll(pkg, 0o700); err != nil { + t.Fatalf("failed to create package: %v", err) + } // Generate the test suite for all the contracts - for i, tt := range bindFailedTests { + for i, tt := range bindTests { t.Run(tt.name, func(t *testing.T) { - // Generate the binding - _, err := PrecompileBind([]string{tt.name}, tt.abi, tt.bytecode, tt.fsigs, "bindtest", bind.LangGo, tt.libs, tt.aliases, "", true) - if err == nil { - t.Fatalf("test %d: no error occurred but was expected", i) + types := []string{tt.name} + + // Generate the binding and create a Go source file in the workspace + bindedFiles, err := PrecompileBind(types, tt.abi, []string{""}, nil, tt.name, bind.LangGo, nil, nil, "contract.abi", true) + if tt.errMsg != "" { + require.ErrorContains(t, err, tt.errMsg) + return + } + if err != nil { + t.Fatalf("test %d: failed to generate binding: %v", i, err) + } + precompilePath := filepath.Join(pkg, tt.name) + if err := os.MkdirAll(precompilePath, 0o700); err != nil { + t.Fatalf("failed to create package: %v", err) + } + // change address to a suitable one for testing + bindedFiles.Module = strings.Replace(bindedFiles.Module, `common.HexToAddress("{ASUITABLEHEXADDRESS}")`, `common.HexToAddress("0x03000000000000000000000000000000000000ff")`, 1) + if err = os.WriteFile(filepath.Join(precompilePath, "module.go"), []byte(bindedFiles.Module), 0o600); err != nil { + t.Fatalf("test %d: failed to write binding: %v", i, err) + } + if err = os.WriteFile(filepath.Join(precompilePath, "config.go"), []byte(bindedFiles.Config), 0o600); err != nil { + t.Fatalf("test %d: failed to write binding: %v", i, err) + } + if err = os.WriteFile(filepath.Join(precompilePath, "contract.go"), []byte(bindedFiles.Contract), 0o600); err != nil { + t.Fatalf("test %d: failed to write binding: %v", i, err) + } + if err = os.WriteFile(filepath.Join(precompilePath, "contract_test.go"), []byte(bindedFiles.ConfigTest), 0o600); err != nil { + t.Fatalf("test %d: failed to write binding: %v", i, err) + } + if err = os.WriteFile(filepath.Join(precompilePath, "contract_test.go"), []byte(bindedFiles.ContractTest), 0o600); err != nil { + t.Fatalf("test %d: failed to write binding: %v", i, err) + } + if err = os.WriteFile(filepath.Join(precompilePath, "contract.abi"), []byte(tt.abi), 0o600); err != nil { + t.Fatalf("test %d: failed to write binding: %v", i, err) + } + + // Generate the test file with the injected test code + code := fmt.Sprintf(` + package %s + + import ( + "testing" + %s + ) + + func Test%s(t *testing.T) { + %s + } + `, tt.name, tt.imports, tt.name, tt.tester) + if err := os.WriteFile(filepath.Join(precompilePath, strings.ToLower(tt.name)+"_test.go"), []byte(code), 0o600); err != nil { + t.Fatalf("test %d: failed to write tests: %v", i, err) } - require.ErrorContains(t, err, tt.errorMsg) }) } + + moder := exec.Command(gocmd, "mod", "init", "bindtest") + moder.Dir = pkg + if out, err := moder.CombinedOutput(); err != nil { + t.Fatalf("failed to convert binding test to modules: %v\n%s", err, out) + } + pwd, _ := os.Getwd() + replacer := exec.Command(gocmd, "mod", "edit", "-x", "-require", "github.com/ava-labs/subnet-evm@v0.0.0", "-replace", "github.com/ava-labs/subnet-evm="+filepath.Join(pwd, "..", "..", "..", "..")) // Repo root + replacer.Dir = pkg + if out, err := replacer.CombinedOutput(); err != nil { + t.Fatalf("failed to replace binding test dependency to current source tree: %v\n%s", err, out) + } + tidier := exec.Command(gocmd, "mod", "tidy", "-compat=1.19") + tidier.Dir = pkg + if out, err := tidier.CombinedOutput(); err != nil { + t.Fatalf("failed to tidy Go module file: %v\n%s", err, out) + } + // Test the entire package and report any failures + cmd := exec.Command(gocmd, "test", "./...", "-v", "-count", "1") + cmd.Dir = pkg + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to run binding test: %v\n%s", err, out) + } } diff --git a/accounts/abi/bind/precompilebind/precompile_contract_template.go b/accounts/abi/bind/precompilebind/precompile_contract_template.go index d4e5695a61..cdb2da1871 100644 --- a/accounts/abi/bind/precompilebind/precompile_contract_template.go +++ b/accounts/abi/bind/precompilebind/precompile_contract_template.go @@ -67,6 +67,8 @@ var ( _ = abi.JSON _ = errors.New _ = big.NewInt + _ = vmerrs.ErrOutOfGas + _ = common.Big0 ) // Singleton StatefulPrecompiledContract and signatures. @@ -159,7 +161,7 @@ func Pack{{.Normalized.Name}}(inputStruct {{capitalise .Normalized.Name}}Input) func Unpack{{capitalise .Normalized.Name}}Input(input []byte)({{$bindedType}}, error) { res, err := {{$contract.Type}}ABI.UnpackInput("{{$method.Original.Name}}", input) if err != nil { - return *new({{$bindedType}}), err + return {{bindtypenew $input.Type $structs}}, err } unpacked := *abi.ConvertType(res[0], new({{$bindedType}})).(*{{$bindedType}}) return unpacked, nil @@ -190,6 +192,15 @@ func Pack{{capitalise .Normalized.Name}}Output (outputStruct {{capitalise .Norma ) } +// Unpack{{capitalise .Normalized.Name}}Output attempts to unpack [output] as {{capitalise .Normalized.Name}}Output +// assumes that [output] does not include selector (omits first 4 func signature bytes) +func Unpack{{capitalise .Normalized.Name}}Output(output []byte) ({{capitalise .Normalized.Name}}Output, error) { + outputStruct := {{capitalise .Normalized.Name}}Output{} + err := {{$contract.Type}}ABI.UnpackIntoInterface(&outputStruct, "{{.Original.Name}}", output) + + return outputStruct, err +} + {{else if len .Normalized.Outputs | eq 1 }} {{$method := .}} {{$output := index $method.Normalized.Outputs 0}} @@ -199,6 +210,17 @@ func Pack{{capitalise .Normalized.Name}}Output (outputStruct {{capitalise .Norma func Pack{{$method.Normalized.Name}}Output ({{decapitalise $output.Name}} {{$bindedType}}) ([]byte, error) { return {{$contract.Type}}ABI.PackOutput("{{$method.Original.Name}}", {{decapitalise $output.Name}}) } + +// Unpack{{capitalise .Normalized.Name}}Output attempts to unpack given [output] into the {{$bindedType}} type output +// assumes that [output] does not include selector (omits first 4 func signature bytes) +func Unpack{{capitalise .Normalized.Name}}Output(output []byte)({{$bindedType}}, error) { + res, err := {{$contract.Type}}ABI.Unpack("{{$method.Original.Name}}", output) + if err != nil { + return {{bindtypenew $output.Type $structs}}, err + } + unpacked := *abi.ConvertType(res[0], new({{$bindedType}})).(*{{$bindedType}}) + return unpacked, nil +} {{end}} func {{decapitalise .Normalized.Name}}(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { diff --git a/accounts/abi/bind/precompilebind/precompile_contract_test_template.go b/accounts/abi/bind/precompilebind/precompile_contract_test_template.go index eb4c2f3f56..4592080544 100644 --- a/accounts/abi/bind/precompilebind/precompile_contract_test_template.go +++ b/accounts/abi/bind/precompilebind/precompile_contract_test_template.go @@ -12,6 +12,7 @@ package {{.Package}} import ( "testing" + "math/big" "github.com/ava-labs/subnet-evm/core/state" {{- if .Contract.AllowList}} @@ -23,106 +24,160 @@ import ( "github.com/stretchr/testify/require" ) - // These tests are run against the precompile contract directly with - // the given input and expected output. They're just a guide to - // help you write your own tests. These tests are for general cases like - // allowlist, readOnly behaviour, and gas cost. You should write your own - // tests for specific cases. - var( - tests = map[string]testutils.PrecompileTest{ - {{- $contract := .Contract}} - {{- $structs := .Structs}} - {{- range .Contract.Funcs}} - {{- $func := .}} - {{- if $contract.AllowList}} - {{- $roles := mkList "NoRole" "Enabled" "Admin"}} - {{- range $role := $roles}} - {{- $fail := and (not $func.Original.IsConstant) (eq $role "NoRole")}} - "calling {{decapitalise $func.Normalized.Name}} from {{$role}} should {{- if $fail}} fail {{- else}} succeed{{- end}}": { - Caller: allowlist.Test{{$role}}Addr, - BeforeHook: allowlist.SetDefaultRoles(Module.Address), - InputFn: func(t testing.TB) []byte { - {{- if len $func.Normalized.Inputs | lt 1}} - // CUSTOM CODE STARTS HERE - // populate test input here - testInput := {{capitalise $func.Normalized.Name}}Input{} - input, err := Pack{{$func.Normalized.Name}}(testInput) - {{- else if len $func.Normalized.Inputs | eq 1 }} - {{- $input := index $func.Normalized.Inputs 0}} - // CUSTOM CODE STARTS HERE - // set test input to a value here - var testInput {{bindtype $input.Type $structs}} - input, err := Pack{{$func.Normalized.Name}}(testInput) - {{- else}} - input, err := Pack{{$func.Normalized.Name}}() - {{- end}} - require.NoError(t, err) - return input - }, - {{- if not $fail}} - // This test is for a successful call. You can set the expected output here. +var ( + _ = vmerrs.ErrOutOfGas + _ = big.NewInt + _ = common.Big0 + _ = require.New +) + +// These tests are run against the precompile contract directly with +// the given input and expected output. They're just a guide to +// help you write your own tests. These tests are for general cases like +// allowlist, readOnly behaviour, and gas cost. You should write your own +// tests for specific cases. +var( + tests = map[string]testutils.PrecompileTest{ + {{- $contract := .Contract}} + {{- $structs := .Structs}} + {{- range .Contract.Funcs}} + {{- $func := .}} + {{- if $contract.AllowList}} + {{- $roles := mkList "NoRole" "Enabled" "Admin"}} + {{- range $role := $roles}} + {{- $fail := and (not $func.Original.IsConstant) (eq $role "NoRole")}} + "calling {{decapitalise $func.Normalized.Name}} from {{$role}} should {{- if $fail}} fail {{- else}} succeed{{- end}}": { + Caller: allowlist.Test{{$role}}Addr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + {{- if len $func.Normalized.Inputs | lt 1}} // CUSTOM CODE STARTS HERE - ExpectedRes: []byte{}, + // populate test input here + testInput := {{capitalise $func.Normalized.Name}}Input{} + input, err := Pack{{$func.Normalized.Name}}(testInput) + {{- else if len $func.Normalized.Inputs | eq 1 }} + {{- $input := index $func.Normalized.Inputs 0}} + // CUSTOM CODE STARTS HERE + // set test input to a value here + var testInput {{bindtype $input.Type $structs}} + testInput = {{bindtypenew $input.Type $structs}} + input, err := Pack{{$func.Normalized.Name}}(testInput) + {{- else}} + input, err := Pack{{$func.Normalized.Name}}() {{- end}} - SuppliedGas: {{$func.Normalized.Name}}GasCost, - ReadOnly: false, - ExpectedErr: {{if $fail}} ErrCannot{{$func.Normalized.Name}}.Error() {{- else}} "" {{- end}}, + require.NoError(t, err) + return input }, + {{- if not $fail}} + // This test is for a successful call. You can set the expected output here. + // CUSTOM CODE STARTS HERE + ExpectedRes: func() []byte{ + {{- if len $func.Normalized.Outputs | eq 0}} + // this function does not return an output, leave this one as is + packedOutput := []byte{} + {{- else}} + {{- if len $func.Normalized.Outputs | lt 1}} + var output {{capitalise $func.Normalized.Name}}Output // CUSTOM CODE FOR AN OUTPUT + {{- else }} + {{$output := index $func.Normalized.Outputs 0}} + var output {{bindtype $output.Type $structs}} // CUSTOM CODE FOR AN OUTPUT + output = {{bindtypenew $output.Type $structs}} // CUSTOM CODE FOR AN OUTPUT + {{- end}} + packedOutput, err := Pack{{$func.Normalized.Name}}Output(output) + if err != nil { + panic(err) + } + {{- end}} + return packedOutput + }(), {{- end}} - {{- end}} - {{- if not $func.Original.IsConstant}} - "readOnly {{decapitalise $func.Normalized.Name}} should fail": { - Caller: common.Address{1}, - InputFn: func(t testing.TB) []byte { - {{- if len $func.Normalized.Inputs | lt 1}} - // CUSTOM CODE STARTS HERE - // populate test input here - testInput := {{capitalise $func.Normalized.Name}}Input{} - input, err := Pack{{$func.Normalized.Name}}(testInput) - {{- else if len $func.Normalized.Inputs | eq 1 }} - {{- $input := index $func.Normalized.Inputs 0}} - // CUSTOM CODE STARTS HERE - // set test input to a value here - var testInput {{bindtype $input.Type $structs}} - input, err := Pack{{$func.Normalized.Name}}(testInput) - {{- else}} - input, err := Pack{{$func.Normalized.Name}}() - {{- end}} - require.NoError(t, err) - return input - }, - SuppliedGas: {{$func.Normalized.Name}}GasCost, - ReadOnly: true, - ExpectedErr: vmerrs.ErrWriteProtection.Error(), + SuppliedGas: {{$func.Normalized.Name}}GasCost, + ReadOnly: false, + ExpectedErr: {{if $fail}} ErrCannot{{$func.Normalized.Name}}.Error() {{- else}} "" {{- end}}, + }, + {{- end}} + {{- end}} + {{- if not $func.Original.IsConstant}} + "readOnly {{decapitalise $func.Normalized.Name}} should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + {{- if len $func.Normalized.Inputs | lt 1}} + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := {{capitalise $func.Normalized.Name}}Input{} + input, err := Pack{{$func.Normalized.Name}}(testInput) + {{- else if len $func.Normalized.Inputs | eq 1 }} + {{- $input := index $func.Normalized.Inputs 0}} + // CUSTOM CODE STARTS HERE + // set test input to a value here + var testInput {{bindtype $input.Type $structs}} + testInput = {{bindtypenew $input.Type $structs}} + input, err := Pack{{$func.Normalized.Name}}(testInput) + {{- else}} + input, err := Pack{{$func.Normalized.Name}}() + {{- end}} + require.NoError(t, err) + return input }, - {{- end}} - "insufficient gas for {{decapitalise $func.Normalized.Name}} should fail": { - Caller: common.Address{1}, - InputFn: func(t testing.TB) []byte { - {{- if len $func.Normalized.Inputs | lt 1}} - // CUSTOM CODE STARTS HERE - // populate test input here - testInput := {{capitalise $func.Normalized.Name}}Input{} - input, err := Pack{{$func.Normalized.Name}}(testInput) - {{- else if len $func.Normalized.Inputs | eq 1 }} - {{- $input := index $func.Normalized.Inputs 0}} - // CUSTOM CODE STARTS HERE - // set test input to a value here - var testInput {{bindtype $input.Type $structs}} - input, err := Pack{{$func.Normalized.Name}}(testInput) - {{- else}} - input, err := Pack{{$func.Normalized.Name}}() - {{- end}} - require.NoError(t, err) - return input - }, - SuppliedGas: {{$func.Normalized.Name}}GasCost - 1, - ReadOnly: false, - ExpectedErr: vmerrs.ErrOutOfGas.Error(), + SuppliedGas: {{$func.Normalized.Name}}GasCost, + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), + }, + {{- end}} + "insufficient gas for {{decapitalise $func.Normalized.Name}} should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + {{- if len $func.Normalized.Inputs | lt 1}} + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := {{capitalise $func.Normalized.Name}}Input{} + input, err := Pack{{$func.Normalized.Name}}(testInput) + {{- else if len $func.Normalized.Inputs | eq 1 }} + {{- $input := index $func.Normalized.Inputs 0}} + // CUSTOM CODE STARTS HERE + // set test input to a value here + var testInput {{bindtype $input.Type $structs}} + testInput = {{bindtypenew $input.Type $structs}} + input, err := Pack{{$func.Normalized.Name}}(testInput) + {{- else}} + input, err := Pack{{$func.Normalized.Name}}() + {{- end}} + require.NoError(t, err) + return input }, - {{- end}} - } - ) + SuppliedGas: {{$func.Normalized.Name}}GasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + {{- end}} + {{- if .Contract.Fallback}} + "insufficient gas for fallback should fail": { + Caller: common.Address{1}, + Input: []byte{}, + SuppliedGas: {{.Contract.Type}}FallbackGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "readOnly fallback should fail": { + Caller: common.Address{1}, + Input: []byte{}, + SuppliedGas: {{.Contract.Type}}FallbackGasCost, + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "fallback should succeed": { + Caller: common.Address{1}, + Input: []byte{}, + SuppliedGas: {{.Contract.Type}}FallbackGasCost, + ReadOnly: false, + ExpectedErr: "", + // CUSTOM CODE STARTS HERE + // set expected output here + ExpectedRes: []byte{}, + }, + {{- end}} + } +) // Test{{.Contract.Type}}Run tests the Run function of the precompile contract. func Test{{.Contract.Type}}Run(t *testing.T) { diff --git a/cmd/precompilegen/main.go b/cmd/precompilegen/main.go index 29e5bf8135..e4273a1e45 100644 --- a/cmd/precompilegen/main.go +++ b/cmd/precompilegen/main.go @@ -43,10 +43,8 @@ import ( "github.com/urfave/cli/v2" ) -var ( - //go:embed template-readme.md - readme string -) +//go:embed template-readme.md +var readme string var ( // Flags needed by abigen @@ -91,7 +89,6 @@ func precompilegen(c *cli.Context) error { lang := bind.LangGo // If the entire solidity code was specified, build and bind based on that var ( - abis []string bins []string types []string sigs []map[string]string @@ -116,7 +113,6 @@ func precompilegen(c *cli.Context) error { if err != nil { utils.Fatalf("Failed to read input ABI: %v", err) } - abis = append(abis, string(abi)) bins = append(bins, "") @@ -149,7 +145,7 @@ func precompilegen(c *cli.Context) error { generateTests := !isOutStdout // Generate the contract precompile - bindedFiles, err := precompilebind.PrecompileBind(types, abis, bins, sigs, pkg, lang, libs, aliases, abifilename, generateTests) + bindedFiles, err := precompilebind.PrecompileBind(types, string(abi), bins, sigs, pkg, lang, libs, aliases, abifilename, generateTests) if err != nil { utils.Fatalf("Failed to generate precompile: %v", err) } @@ -193,7 +189,7 @@ func precompilegen(c *cli.Context) error { } // Write the ABI to the output folder - if err := os.WriteFile(abipath, []byte(abis[0]), 0o600); err != nil { + if err := os.WriteFile(abipath, abi, 0o600); err != nil { utils.Fatalf("Failed to write ABI: %v", err) } @@ -204,18 +200,16 @@ func precompilegen(c *cli.Context) error { } // Write the test code to the output folder - if generateTests { - configTestCode := bindedFiles.ConfigTest - configTestCodeOut := filepath.Join(outFlagStr, "config_test.go") - if err := os.WriteFile(configTestCodeOut, []byte(configTestCode), 0o600); err != nil { - utils.Fatalf("Failed to write generated test code: %v", err) - } - - contractTestCode := bindedFiles.ContractTest - contractTestCodeOut := filepath.Join(outFlagStr, "contract_test.go") - if err := os.WriteFile(contractTestCodeOut, []byte(contractTestCode), 0o600); err != nil { - utils.Fatalf("Failed to write generated test code: %v", err) - } + configTestCode := bindedFiles.ConfigTest + configTestCodeOut := filepath.Join(outFlagStr, "config_test.go") + if err := os.WriteFile(configTestCodeOut, []byte(configTestCode), 0o600); err != nil { + utils.Fatalf("Failed to write generated test code: %v", err) + } + + contractTestCode := bindedFiles.ContractTest + contractTestCodeOut := filepath.Join(outFlagStr, "contract_test.go") + if err := os.WriteFile(contractTestCodeOut, []byte(contractTestCode), 0o600); err != nil { + utils.Fatalf("Failed to write generated test code: %v", err) } fmt.Println("Precompile files generated successfully at: ", outFlagStr)