diff --git a/.gitignore b/.gitignore index 7c8a97285..4d3b2e281 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ tests/spec-tests/ contracts/abi contracts/bin + +precompile/bindings +precompile/out diff --git a/core/vm/evm.go b/core/vm/evm.go index c18353a97..ac510f5fb 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + pconfig "github.com/ethereum/go-ethereum/precompile/config" "github.com/holiman/uint256" ) @@ -125,6 +126,8 @@ type EVM struct { // available gas is calculated in gasCall* according to the 63/64 rule and later // applied in opCall*. callGasTemp uint64 + // stateful precompiles + precompileManager *precompileManager } // NewEVM returns a new EVM. The returned EVM is not thread safe and should @@ -150,6 +153,11 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time), } evm.interpreter = NewEVMInterpreter(evm) + + // Set up precompiles + evm.precompileManager = NewPrecompileManager(evm) + evm.precompileManager.RegisterMap(pconfig.PrecompileConfig(chainConfig, blockCtx.BlockNumber.Uint64(), blockCtx.Time)) + return evm } @@ -197,7 +205,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas return nil, gas, ErrInsufficientBalance } snapshot := evm.StateDB.Snapshot() - p, isPrecompile := evm.precompile(addr) + isPrecompile := evm.precompileManager.IsPrecompile(addr) if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() { @@ -209,7 +217,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas evm.Context.Transfer(evm.StateDB, caller.Address(), addr, value) if isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), value, gas, false) } else { // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. @@ -274,8 +282,8 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, var snapshot = evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall - if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + if isPrecompile := evm.precompileManager.IsPrecompile(addr); isPrecompile { + ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), value, gas, false) } else { addrCopy := addr // Initialise a new contract and set the code that is to be used by the EVM. @@ -322,8 +330,8 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by var snapshot = evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall - if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + if isPrecompile := evm.precompileManager.IsPrecompile(addr); isPrecompile { + ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), nil, gas, false) } else { addrCopy := addr // Initialise a new contract and make initialise the delegate values @@ -373,8 +381,8 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte // future scenarios evm.StateDB.AddBalance(addr, new(uint256.Int), tracing.BalanceChangeTouchAccount) - if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + if isPrecompile := evm.precompileManager.IsPrecompile(addr); isPrecompile { + ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), new(uint256.Int), gas, true) } else { // At this point, we use a copy of address. If we don't, the go compiler will // leak the 'contract' to the outer scope, and make allocation for 'contract' diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go index 02fc94840..cd5dd1ec7 100644 --- a/core/vm/gas_table_test.go +++ b/core/vm/gas_table_test.go @@ -95,6 +95,7 @@ func TestEIP2200(t *testing.T) { vmctx := BlockContext{ CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true }, Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, + BlockNumber: big.NewInt(0), } vmenv := NewEVM(vmctx, TxContext{}, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}}) diff --git a/core/vm/instructions_test.go b/core/vm/instructions_test.go index 8653864d1..4b494e329 100644 --- a/core/vm/instructions_test.go +++ b/core/vm/instructions_test.go @@ -105,7 +105,7 @@ func init() { func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFunc, name string) { var ( - env = NewEVM(BlockContext{}, TxContext{}, nil, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, TxContext{}, nil, params.TestChainConfig, Config{}) stack = newstack() pc = uint64(0) evmInterpreter = env.interpreter @@ -204,7 +204,7 @@ func TestSAR(t *testing.T) { func TestAddMod(t *testing.T) { var ( - env = NewEVM(BlockContext{}, TxContext{}, nil, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, TxContext{}, nil, params.TestChainConfig, Config{}) stack = newstack() evmInterpreter = NewEVMInterpreter(env) pc = uint64(0) @@ -248,7 +248,7 @@ func TestWriteExpectedValues(t *testing.T) { // getResult is a convenience function to generate the expected values getResult := func(args []*twoOperandParams, opFn executionFunc) []TwoOperandTestcase { var ( - env = NewEVM(BlockContext{}, TxContext{}, nil, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, TxContext{}, nil, params.TestChainConfig, Config{}) stack = newstack() pc = uint64(0) interpreter = env.interpreter @@ -293,7 +293,7 @@ func TestJsonTestcases(t *testing.T) { func opBenchmark(bench *testing.B, op executionFunc, args ...string) { var ( - env = NewEVM(BlockContext{}, TxContext{}, nil, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, TxContext{}, nil, params.TestChainConfig, Config{}) stack = newstack() scope = &ScopeContext{nil, stack, nil} evmInterpreter = NewEVMInterpreter(env) @@ -534,7 +534,7 @@ func BenchmarkOpIsZero(b *testing.B) { func TestOpMstore(t *testing.T) { var ( - env = NewEVM(BlockContext{}, TxContext{}, nil, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, TxContext{}, nil, params.TestChainConfig, Config{}) stack = newstack() mem = NewMemory() evmInterpreter = NewEVMInterpreter(env) @@ -560,7 +560,7 @@ func TestOpMstore(t *testing.T) { func BenchmarkOpMstore(bench *testing.B) { var ( - env = NewEVM(BlockContext{}, TxContext{}, nil, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, TxContext{}, nil, params.TestChainConfig, Config{}) stack = newstack() mem = NewMemory() evmInterpreter = NewEVMInterpreter(env) @@ -583,7 +583,7 @@ func BenchmarkOpMstore(bench *testing.B) { func TestOpTstore(t *testing.T) { var ( statedb, _ = state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) - env = NewEVM(BlockContext{}, TxContext{}, statedb, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, TxContext{}, statedb, params.TestChainConfig, Config{}) stack = newstack() mem = NewMemory() evmInterpreter = NewEVMInterpreter(env) @@ -625,7 +625,7 @@ func TestOpTstore(t *testing.T) { func BenchmarkOpKeccak256(bench *testing.B) { var ( - env = NewEVM(BlockContext{}, TxContext{}, nil, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, TxContext{}, nil, params.TestChainConfig, Config{}) stack = newstack() mem = NewMemory() evmInterpreter = NewEVMInterpreter(env) @@ -729,7 +729,7 @@ func TestRandom(t *testing.T) { {name: "hash(0x010203)", random: crypto.Keccak256Hash([]byte{0x01, 0x02, 0x03})}, } { var ( - env = NewEVM(BlockContext{Random: &tt.random}, TxContext{}, nil, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{Random: &tt.random, BlockNumber: big.NewInt(0)}, TxContext{}, nil, params.TestChainConfig, Config{}) stack = newstack() pc = uint64(0) evmInterpreter = env.interpreter @@ -770,7 +770,7 @@ func TestBlobHash(t *testing.T) { {name: "out-of-bounds (nil)", idx: 25, expect: zero, hashes: nil}, } { var ( - env = NewEVM(BlockContext{}, TxContext{BlobHashes: tt.hashes}, nil, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, TxContext{BlobHashes: tt.hashes}, nil, params.TestChainConfig, Config{}) stack = newstack() pc = uint64(0) evmInterpreter = env.interpreter @@ -873,7 +873,7 @@ func TestOpMCopy(t *testing.T) { }, } { var ( - env = NewEVM(BlockContext{}, TxContext{}, nil, params.TestChainConfig, Config{}) + env = NewEVM(BlockContext{BlockNumber: big.NewInt(0)}, TxContext{}, nil, params.TestChainConfig, Config{}) stack = newstack() pc = uint64(0) evmInterpreter = env.interpreter diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go index ff4977d72..e15e8b94a 100644 --- a/core/vm/interpreter_test.go +++ b/core/vm/interpreter_test.go @@ -17,6 +17,7 @@ package vm import ( + "math/big" "testing" "time" @@ -39,7 +40,8 @@ var loopInterruptTests = []string{ func TestLoopInterrupt(t *testing.T) { address := common.BytesToAddress([]byte("contract")) vmctx := BlockContext{ - Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, + Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {}, + BlockNumber: big.NewInt(0), } for i, tt := range loopInterruptTests { diff --git a/core/vm/precompile_manager.go b/core/vm/precompile_manager.go new file mode 100644 index 000000000..2a8a94a0d --- /dev/null +++ b/core/vm/precompile_manager.go @@ -0,0 +1,238 @@ +package vm + +import ( + "fmt" + "reflect" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/precompile" + "github.com/holiman/uint256" +) + +type methodID [4]byte + +type statefulMethod struct { + abiMethod abi.Method + reflectMethod reflect.Method +} + +type precompileMethods map[methodID]*statefulMethod +type gasMethods map[methodID]reflect.Method + +type precompileManager struct { + evm *EVM + precompiles map[common.Address]precompile.StatefulPrecompiledContract + pMethods map[common.Address]precompileMethods + gMethods map[common.Address]gasMethods +} + +func NewPrecompileManager(evm *EVM) *precompileManager { + precompiles := make(map[common.Address]precompile.StatefulPrecompiledContract) + pMethods := make(map[common.Address]precompileMethods) + gMethods := make(map[common.Address]gasMethods) + return &precompileManager{ + evm: evm, + precompiles: precompiles, + pMethods: pMethods, + gMethods: gMethods, + } +} + +func (pm *precompileManager) IsPrecompile(addr common.Address) bool { + _, isEvmPrecompile := pm.evm.precompile(addr) + if isEvmPrecompile { + return true + } + + _, isStatefulPrecompile := pm.precompiles[addr] + return isStatefulPrecompile +} + +func (pm *precompileManager) Run( + addr common.Address, + input []byte, + caller common.Address, + value *uint256.Int, + suppliedGas uint64, + readOnly bool, +) (ret []byte, remainingGas uint64, err error) { + + // run core evm precompile + p, isEvmPrecompile := pm.evm.precompile(addr) + if isEvmPrecompile { + return RunPrecompiledContract(p, input, suppliedGas, pm.evm.Config.Tracer) + } + + contract, ok := pm.precompiles[addr] + if !ok { + return nil, 0, fmt.Errorf("no precompiled contract at address %v", addr.Hex()) + } + + // Extract the method ID from the input + methodId := methodID(input) + // Try to get the method from the precompiled contracts using the method ID + method, exists := pm.pMethods[addr][methodId] + if !exists { + return nil, 0, fmt.Errorf("no method with id %x in precompiled contract at address %v", methodId, addr.Hex()) + } + + // refund gas for act of calling custom precompile + if suppliedGas > 0 { + if pm.evm.chainRules.IsEIP150 && suppliedGas > params.CallGasEIP150 { + suppliedGas += params.CallGasEIP150 + } else if suppliedGas > params.CallGasFrontier { + suppliedGas += params.CallGasFrontier + } + } + + // Unpack the input arguments using the ABI method's inputs + unpackedArgs, err := method.abiMethod.Inputs.Unpack(input[4:]) + if err != nil { + return nil, 0, err + } + + // Convert the unpacked args to reflect values. + reflectedUnpackedArgs := make([]reflect.Value, 0, len(unpackedArgs)) + for _, unpacked := range unpackedArgs { + reflectedUnpackedArgs = append(reflectedUnpackedArgs, reflect.ValueOf(unpacked)) + } + + // set precompile nonce to 1 to avoid state deletion for being considered an empty account + // this conforms precompile contracts to EIP-161 + if !readOnly && pm.evm.StateDB.GetNonce(addr) == 0 { + pm.evm.StateDB.SetNonce(addr, 1) + } + + ctx := precompile.NewStatefulContext(pm.evm.StateDB, addr, caller, value) + + // Make sure the readOnly is only set if we aren't in readOnly yet. + // This also makes sure that the readOnly flag isn't removed for child calls. + if readOnly && !ctx.IsReadOnly() { + ctx.SetReadOnly(true) + defer func() { ctx.SetReadOnly(false) }() + } + + // check if enough gas is supplied + var gasCost uint64 = contract.DefaultGas(input) + gasMethod, exists := pm.gMethods[addr][methodId] + if exists { + gasResult := gasMethod.Func.Call(append( + []reflect.Value{ + reflect.ValueOf(contract), + reflect.ValueOf(ctx), + }, + reflectedUnpackedArgs..., + )) + if len(gasResult) > 0 { + gasCost, ok = gasResult[0].Interface().(uint64) + if !ok { + gasCost = contract.DefaultGas(input) + } + } + } + + if gasCost > suppliedGas { + return nil, 0, ErrOutOfGas + } + + // call the precompile method + results := method.reflectMethod.Func.Call(append( + []reflect.Value{ + reflect.ValueOf(contract), + reflect.ValueOf(ctx), + }, + reflectedUnpackedArgs..., + )) + + // check if precompile returned an error + if len(results) > 0 { + if err, ok := results[len(results)-1].Interface().(error); ok && err != nil { + return nil, 0, err + } + } + + // Pack the result + var output []byte + if len(results) > 1 { + interfaceArgs := make([]interface{}, len(results)-1) + for i, v := range results[:len(results)-1] { + interfaceArgs[i] = v.Interface() + } + output, err = method.abiMethod.Outputs.Pack(interfaceArgs...) + if err != nil { + return nil, 0, err + } + } + + suppliedGas -= gasCost + return output, suppliedGas, nil +} + +func (pm *precompileManager) RegisterMap(m precompile.PrecompileMap) error { + for addr, p := range m { + err := pm.Register(addr, p) + if err != nil { + return err + } + } + return nil +} + +func (pm *precompileManager) Register(addr common.Address, p precompile.StatefulPrecompiledContract) error { + if _, isEvmPrecompile := pm.evm.precompile(addr); isEvmPrecompile { + return fmt.Errorf("precompiled contract already exists at address %v", addr.Hex()) + } + + if _, exists := pm.precompiles[addr]; exists { + return fmt.Errorf("precompiled contract already exists at address %v", addr.Hex()) + } + + // niaeve implementation; parsed abi method names must match precompile method names 1:1 + // + // Note on method naming: + // Method name is the abi method name used for internal representation. It's derived from + // the abi raw name and a suffix will be added in the case of a function overload. + // + // e.g. + // These are two functions that have the same name: + // * foo(int,int) + // * foo(uint,uint) + // The method name of the first one will be resolved as Foo while the second one + // will be resolved as Foo0. + // + // Alternatively could require each precompile to define the func mapping instead of doing this magic + abiMethods := p.GetABI().Methods + contractType := reflect.ValueOf(p).Type() + precompileMethods := make(precompileMethods) + gasMethods := make(gasMethods) + for _, abiMethod := range abiMethods { + mName := strings.ToUpper(string(abiMethod.Name[0])) + abiMethod.Name[1:] + reflectMethod, exists := contractType.MethodByName(mName) + if !exists { + return fmt.Errorf("precompiled contract does not implement abi method %s with signature %s", abiMethod.Name, abiMethod.RawName) + } + mID := methodID(abiMethod.ID) + precompileMethods[mID] = &statefulMethod{ + abiMethod: abiMethod, + reflectMethod: reflectMethod, + } + + // precompile method has custom gas calc + gName := mName + "RequiredGas" + gasMethod, exists := contractType.MethodByName(gName) + if exists { + if gasMethod.Type.NumOut() != 1 || gasMethod.Type.Out(0).Kind() != reflect.Uint64 { + return fmt.Errorf("gas method %s does not return uint64", gName) + } + gasMethods[mID] = gasMethod + } + } + + pm.precompiles[addr] = p + pm.pMethods[addr] = precompileMethods + pm.gMethods[addr] = gasMethods + return nil +} diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go index 137608f88..82d232d99 100644 --- a/eth/tracers/logger/logger_test.go +++ b/eth/tracers/logger/logger_test.go @@ -52,11 +52,23 @@ type dummyStatedb struct { func (*dummyStatedb) GetRefund() uint64 { return 1337 } func (*dummyStatedb) GetState(_ common.Address, _ common.Hash) common.Hash { return common.Hash{} } func (*dummyStatedb) SetState(_ common.Address, _ common.Hash, _ common.Hash) {} +func (*dummyStatedb) GetCommittedState(_ common.Address, _ common.Hash) common.Hash { + return common.Hash{} +} +func (*dummyStatedb) SlotInAccessList(_ common.Address, _ common.Hash) (bool, bool) { + return true, true +} + +func newDummyBlockContext() vm.BlockContext { + return vm.BlockContext{ + BlockNumber: big.NewInt(0), + } +} func TestStoreCapture(t *testing.T) { var ( logger = NewStructLogger(nil) - env = vm.NewEVM(vm.BlockContext{}, vm.TxContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()}) + env = vm.NewEVM(newDummyBlockContext(), vm.TxContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()}) contract = vm.NewContract(&dummyContractRef{}, &dummyContractRef{}, new(uint256.Int), 100000) ) contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)} diff --git a/precompile/README.md b/precompile/README.md new file mode 100644 index 000000000..8ec4dd2de --- /dev/null +++ b/precompile/README.md @@ -0,0 +1,40 @@ +# Writing a Precompile Contract + +1. Create a Solidity interface in `contracts/interfaces`, e.g, IBase64.sol + +2. Generate bindings with `./gen.sh` + +3. Copy generate `ABI` definition from `./bindings/i_.abigen.go` to `./abi/abi.go`, assigning it to a unique const name, i.e, `Base64ABI`. + +4. Implement the precompile in Go at `./contracts//`. + - The struct should implement the `StatefulPrecompiledContract` interface + - You must implement methods defined in the Solidity interface + - Implement custom gas handlers as needed + - You can use the `StatefulContext` to access and modify the evm state db + +5. Enable the precompile by returning it from `PrecompileConfig()` in `./config/config.go`. Existing chains should only enable new precompiles at hard forks. + + For example, to enable the example base64 precompile at genesis, the config could look like this: + + ```go + package config + + import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/precompile" + + pcbase64 "github.com/ethereum/go-ethereum/precompile/contracts/base64" + ) + + var NullPrecompiles = precompile.PrecompileMap{} + + var MyRollupGenesisPrecompiles = precompile.PrecompileMap{ + common.HexToAddress("0x01000"): pcbase64.NewBase64(), + } + + // return precompiles that are enabled at height + func PrecompileConfig(chainConfig *params.ChainConfig, height uint64, timestamp uint64) precompile.PrecompileMap { + return MyRollupGenesisPrecompiles + } + ``` diff --git a/precompile/abi/abi.go b/precompile/abi/abi.go new file mode 100644 index 000000000..7619e544e --- /dev/null +++ b/precompile/abi/abi.go @@ -0,0 +1,5 @@ +package abi + +const ( + Base64ABI = "[{\"type\":\"function\",\"name\":\"decode\",\"inputs\":[{\"name\":\"_data\",\"type\":\"string\",\"internalType\":\"string\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"stateMutability\":\"pure\"},{\"type\":\"function\",\"name\":\"decodeURL\",\"inputs\":[{\"name\":\"_data\",\"type\":\"string\",\"internalType\":\"string\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"stateMutability\":\"pure\"},{\"type\":\"function\",\"name\":\"encode\",\"inputs\":[{\"name\":\"_data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"pure\"},{\"type\":\"function\",\"name\":\"encodeURL\",\"inputs\":[{\"name\":\"_data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"pure\"}]" +) diff --git a/precompile/config/config.go b/precompile/config/config.go new file mode 100644 index 000000000..9a60604f7 --- /dev/null +++ b/precompile/config/config.go @@ -0,0 +1,19 @@ +package config + +import ( + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/precompile" +) + +var NullPrecompiles = precompile.PrecompileMap{} + +// return precompiles that are enabled at height +func PrecompileConfig(chainConfig *params.ChainConfig, height uint64, timestamp uint64) precompile.PrecompileMap { + // Example, enable the base64 precompile at address 0x0000000000000000000000000000000000001000: + // (add `import pcbase64 "github.com/ethereum/go-ethereum/precompile/contracts/base64"`) + // return precompile.PrecompileMap{ + // common.HexToAddress("0x01000"): pcbase64.NewBase64(), + // } + + return NullPrecompiles +} diff --git a/precompile/contracts/base64/base64.go b/precompile/contracts/base64/base64.go new file mode 100644 index 000000000..7620d817d --- /dev/null +++ b/precompile/contracts/base64/base64.go @@ -0,0 +1,61 @@ +package base64 + +import ( + b64 "encoding/base64" + + "github.com/ethereum/go-ethereum/precompile" + "github.com/ethereum/go-ethereum/precompile/abi" +) + +type Base64 struct { + precompile.StatefulPrecompiledContract +} + +func NewBase64() *Base64 { + return &Base64{ + StatefulPrecompiledContract: precompile.NewStatefulPrecompiledContract( + abi.Base64ABI, + ), + } +} + +func (c *Base64) Encode(ctx precompile.StatefulContext, data []byte) (string, error) { + return b64.StdEncoding.EncodeToString(data), nil +} + +func (c *Base64) EncodeURL(ctx precompile.StatefulContext, data []byte) (string, error) { + return b64.URLEncoding.EncodeToString(data), nil +} + +func (c *Base64) Decode(ctx precompile.StatefulContext, data string) ([]byte, error) { + decoded, err := b64.StdEncoding.DecodeString(data) + if err != nil { + return nil, err + } + return decoded, nil +} + +func (c *Base64) DecodeURL(ctx precompile.StatefulContext, data string) ([]byte, error) { + decoded, err := b64.URLEncoding.DecodeString(data) + if err != nil { + return nil, err + } + return decoded, nil +} + +// gas calcs +func (c *Base64) EncodeRequiredGas(ctx precompile.StatefulContext, data []byte) uint64 { + return precompile.WordLength(data, 256) * precompile.GasQuickStep +} + +func (c *Base64) EncodeURLRequiredGas(ctx precompile.StatefulContext, data []byte) uint64 { + return precompile.WordLength(data, 256) * precompile.GasQuickStep +} + +func (c *Base64) DecodeRequiredGas(ctx precompile.StatefulContext, data string) uint64 { + return precompile.WordLength([]byte(data), 256) * precompile.GasQuickStep +} + +func (c *Base64) DecodeURLRequiredGas(ctx precompile.StatefulContext, data string) uint64 { + return precompile.WordLength([]byte(data), 256) * precompile.GasQuickStep +} diff --git a/precompile/contracts/base64/base64_test.go b/precompile/contracts/base64/base64_test.go new file mode 100644 index 000000000..26b1ff0c3 --- /dev/null +++ b/precompile/contracts/base64/base64_test.go @@ -0,0 +1,104 @@ +package base64 + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/precompile" + "github.com/ethereum/go-ethereum/precompile/mocks" + "github.com/holiman/uint256" +) + +func NewMockStatefulContext() precompile.StatefulContext { + return precompile.NewStatefulContext( + mocks.NewMockStateDB(), + common.BytesToAddress([]byte("0xSelf")), + common.BytesToAddress([]byte("0xMsgSender")), + uint256.NewInt(0), + ) +} +func TestEncode(t *testing.T) { + c := NewBase64() + ctx := NewMockStatefulContext() + + input := []byte("Hello, Astria!") + + // Test for Non-Error + output, err := c.Encode(ctx, input) + if err != nil { + t.Fatalf("Encode failed: %v", err) + } + + // Test for Correct Encoding + expectedOutput := "SGVsbG8sIEFzdHJpYSE=" + if output != expectedOutput { + t.Errorf("Encode output mismatch. Got %v, expected %v", output, expectedOutput) + } +} + +func TestDecode(t *testing.T) { + c := NewBase64() + ctx := NewMockStatefulContext() + + input := "SGVsbG8sIFJvbGx1cCE=" + expectedOutput := []byte("Hello, Rollup!") + + output, err := c.Decode(ctx, input) + if err != nil { + t.Fatalf("Decode failed: %v", err) + } + + if !bytes.Equal(output, expectedOutput) { + t.Errorf("Decode output mismatch. Got %v, expected %v", output, expectedOutput) + } +} + +func TestEncodeURL(t *testing.T) { + c := NewBase64() + ctx := NewMockStatefulContext() + + input := []byte{255, 0, 23, 40, 33, 32, 1, 56, 89, 23, 156, 21} + + // Test for Non-Error + output, err := c.EncodeURL(ctx, input) + if err != nil { + t.Fatalf("EncodeURL failed: %v", err) + } + + // Test for Correct Encoding + expectedOutput := "_wAXKCEgAThZF5wV" + if output != expectedOutput { + t.Errorf("EncodeURL output mismatch. Got %v, expected %v", output, expectedOutput) + } +} + +func TestDecodeURL(t *testing.T) { + c := NewBase64() + ctx := NewMockStatefulContext() + + input := "SGVsbG8sIEFzdHJpYSE=" + expectedOutput := []byte("Hello, Astria!") + + output, err := c.DecodeURL(ctx, input) + if err != nil { + t.Fatalf("DecodeURL failed: %v", err) + } + + if !bytes.Equal(output, expectedOutput) { + t.Errorf("DecodeURL output mismatch. Got %v, expected %v", output, expectedOutput) + } +} + +func TestEncodeRequiredGas(t *testing.T) { + c := NewBase64() + ctx := NewMockStatefulContext() + + input := []byte("Hello, Astria!") + expectedGas := uint64(2) + + gas := c.EncodeRequiredGas(ctx, input) + if gas != expectedGas { + t.Errorf("EncodeRequiredGas mismatch. Got %v, expected %v", gas, expectedGas) + } +} diff --git a/precompile/contracts/interfaces/IBase64.sol b/precompile/contracts/interfaces/IBase64.sol new file mode 100644 index 000000000..ca197f522 --- /dev/null +++ b/precompile/contracts/interfaces/IBase64.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IBase64 { + + /// @dev Encodes the input data into a base64 string + function encode(bytes memory _data) external pure returns (string memory); + + /// @dev Encodes the input data into a URL-safe base64 string + function encodeURL(bytes memory _data) external pure returns (string memory); + + /// @dev Decodes the input base64 string into bytes + function decode(string memory _data) external pure returns (bytes memory); + + /// @dev Decodes the input URL-safe base64 string into bytes + function decodeURL(string memory _data) external pure returns (bytes memory); + +} diff --git a/precompile/errors.go b/precompile/errors.go new file mode 100644 index 000000000..4858fdfe0 --- /dev/null +++ b/precompile/errors.go @@ -0,0 +1,7 @@ +package precompile + +import "errors" + +var ( + ErrWriteProtection = errors.New("write protection") +) diff --git a/precompile/foundry.toml b/precompile/foundry.toml new file mode 100644 index 000000000..d2b280d46 --- /dev/null +++ b/precompile/foundry.toml @@ -0,0 +1,10 @@ +[profile.default] +fuzz_runs = 1024 +evm_version = 'shanghai' +solc_version = '0.8.24' +cache = false +force = false +optimizer = false + +[profile.ci] +fuzz_runs = 8192 diff --git a/precompile/gen.sh b/precompile/gen.sh new file mode 100755 index 000000000..fda5d8956 --- /dev/null +++ b/precompile/gen.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +rm -fr ./bindings +mkdir ./bindings + +rm -fr ./out +mkdir ./out + +forge build --extra-output-files bin --extra-output-files abi --root . + +for dir in ./out/*/ +do + NAME=$(basename $dir) + NAME=${NAME%.sol} + NAME_LOWER=$(echo "${NAME:1}" | tr '[:upper:]' '[:lower:]') + abigen --pkg bindings \ + --abi ./out/$NAME.sol/$NAME.abi.json \ + --bin ./out/$NAME.sol/$NAME.bin \ + --out ./bindings/i_${NAME_LOWER}.abigen.go \ + --type ${NAME:1} +done diff --git a/precompile/interface.go b/precompile/interface.go new file mode 100644 index 000000000..91ca61f89 --- /dev/null +++ b/precompile/interface.go @@ -0,0 +1,40 @@ +package precompile + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/holiman/uint256" +) + +type BalanceChangeReason byte + +type StateDB interface { + SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) + AddBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) + GetBalance(common.Address) *uint256.Int + GetState(common.Address, common.Hash) common.Hash + SetState(common.Address, common.Hash, common.Hash) + GetCommittedState(common.Address, common.Hash) common.Hash +} + +type StatefulContext interface { + SetState(common.Hash, common.Hash) error + GetState(common.Hash) common.Hash + GetCommittedState(common.Hash) common.Hash + SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) error + AddBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) error + GetBalance(common.Address) *uint256.Int + Address() common.Address + MsgSender() common.Address + MsgValue() *uint256.Int + IsReadOnly() bool + SetReadOnly(bool) +} + +type StatefulPrecompiledContract interface { + GetABI() abi.ABI + DefaultGas(input []byte) uint64 +} + +type PrecompileMap map[common.Address]StatefulPrecompiledContract diff --git a/precompile/mocks/state_db.go b/precompile/mocks/state_db.go new file mode 100644 index 000000000..3ac0d0562 --- /dev/null +++ b/precompile/mocks/state_db.go @@ -0,0 +1,62 @@ +package mocks + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/precompile" + "github.com/holiman/uint256" +) + +type mockStateDB struct { + balances map[common.Address]*uint256.Int + states map[common.Address]map[common.Hash]common.Hash +} + +func NewMockStateDB() precompile.StateDB { + return &mockStateDB{ + balances: make(map[common.Address]*uint256.Int), + states: make(map[common.Address]map[common.Hash]common.Hash), + } +} + +func (m *mockStateDB) SubBalance(address common.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) { + if _, ok := m.balances[address]; !ok { + m.balances[address] = uint256.NewInt(0) + } + m.balances[address].Sub(m.balances[address], amount) +} + +func (m *mockStateDB) AddBalance(address common.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) { + if _, ok := m.balances[address]; !ok { + m.balances[address] = uint256.NewInt(0) + } + m.balances[address].Add(m.balances[address], amount) +} + +func (m *mockStateDB) GetBalance(address common.Address) *uint256.Int { + if _, ok := m.balances[address]; !ok { + m.balances[address] = uint256.NewInt(0) + } + return new(uint256.Int).Set(m.balances[address]) +} + +func (m *mockStateDB) GetState(address common.Address, hash common.Hash) common.Hash { + if _, ok := m.states[address]; !ok { + m.states[address] = make(map[common.Hash]common.Hash) + } + return m.states[address][hash] +} + +func (m *mockStateDB) SetState(address common.Address, hash common.Hash, value common.Hash) { + if _, ok := m.states[address]; !ok { + m.states[address] = make(map[common.Hash]common.Hash) + } + m.states[address][hash] = value +} + +func (m *mockStateDB) GetCommittedState(address common.Address, hash common.Hash) common.Hash { + if _, ok := m.states[address]; !ok { + m.states[address] = make(map[common.Hash]common.Hash) + } + return m.states[address][hash] +} diff --git a/precompile/stateful_context.go b/precompile/stateful_context.go new file mode 100644 index 000000000..7160d475b --- /dev/null +++ b/precompile/stateful_context.go @@ -0,0 +1,86 @@ +package precompile + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/holiman/uint256" +) + +type statefulContext struct { + state StateDB + address common.Address + msgSender common.Address + msgValue *uint256.Int + readOnly bool +} + +func NewStatefulContext( + state StateDB, + address common.Address, + msgSender common.Address, + msgValue *uint256.Int, +) StatefulContext { + return &statefulContext{ + state: state, + address: address, + msgSender: msgSender, + msgValue: msgValue, + readOnly: false, + } +} + +func (sc *statefulContext) SetState(key common.Hash, value common.Hash) error { + if sc.readOnly { + return ErrWriteProtection + } + sc.state.SetState(sc.address, key, value) + return nil +} + +func (sc *statefulContext) GetState(key common.Hash) common.Hash { + return sc.state.GetState(sc.address, key) +} + +func (sc *statefulContext) GetCommittedState(key common.Hash) common.Hash { + return sc.state.GetCommittedState(sc.address, key) +} + +func (sc *statefulContext) SubBalance(address common.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) error { + if sc.readOnly { + return ErrWriteProtection + } + sc.state.SubBalance(address, amount, reason) + return nil +} + +func (sc *statefulContext) AddBalance(address common.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) error { + if sc.readOnly { + return ErrWriteProtection + } + sc.state.AddBalance(address, amount, reason) + return nil +} + +func (sc *statefulContext) GetBalance(address common.Address) *uint256.Int { + return sc.state.GetBalance(address) +} + +func (sc *statefulContext) Address() common.Address { + return sc.address +} + +func (sc *statefulContext) MsgSender() common.Address { + return sc.msgSender +} + +func (sc *statefulContext) MsgValue() *uint256.Int { + return sc.msgValue +} + +func (sc *statefulContext) IsReadOnly() bool { + return sc.readOnly +} + +func (sc *statefulContext) SetReadOnly(readOnly bool) { + sc.readOnly = readOnly +} diff --git a/precompile/stateful_context_test.go b/precompile/stateful_context_test.go new file mode 100644 index 000000000..21d3c1afd --- /dev/null +++ b/precompile/stateful_context_test.go @@ -0,0 +1,93 @@ +package precompile_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/precompile" + "github.com/ethereum/go-ethereum/precompile/mocks" + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" +) + +func setupStatefulContext() precompile.StatefulContext { + state := mocks.NewMockStateDB() + address := common.HexToAddress("0x123") + msgSender := common.HexToAddress("0x456") + msgValue := uint256.NewInt(1000) + + return precompile.NewStatefulContext(state, address, msgSender, msgValue) +} + +func TestAddress(t *testing.T) { + ctx := setupStatefulContext() + assert.Equal(t, common.HexToAddress("0x123"), ctx.Address()) +} + +func TestMsgSender(t *testing.T) { + ctx := setupStatefulContext() + assert.Equal(t, common.HexToAddress("0x456"), ctx.MsgSender()) +} + +func TestMsgValue(t *testing.T) { + ctx := setupStatefulContext() + assert.Equal(t, uint256.NewInt(1000), ctx.MsgValue()) +} + +func TestIsReadOnly(t *testing.T) { + ctx := setupStatefulContext() + + assert.False(t, ctx.IsReadOnly()) + + ctx.SetReadOnly(true) + assert.True(t, ctx.IsReadOnly()) +} + +func TestSetState(t *testing.T) { + ctx := setupStatefulContext() + + key := common.HexToHash("0x789") + value := common.HexToHash("0xabc") + + assert.Equal(t, common.HexToHash("0x00"), ctx.GetState(key)) + + ctx.SetReadOnly(true) + err := ctx.SetState(key, value) + assert.Error(t, err) + + ctx.SetReadOnly(false) + err = ctx.SetState(key, value) + assert.NoError(t, err) + + assert.Equal(t, value, ctx.GetState(key)) +} + +func TestBalance(t *testing.T) { + ctx := setupStatefulContext() + + initialBalance := ctx.GetBalance(common.HexToAddress("0x123")) + assert.Equal(t, uint256.NewInt(0), initialBalance) + + amount := uint256.NewInt(500) + + err := ctx.AddBalance(common.HexToAddress("0x123"), amount, tracing.BalanceChangeUnspecified) + assert.NoError(t, err) + assert.Equal(t, uint256.NewInt(500), ctx.GetBalance(common.HexToAddress("0x123"))) + + err = ctx.AddBalance(common.HexToAddress("0x123"), amount, tracing.BalanceChangeUnspecified) + assert.NoError(t, err) + assert.Equal(t, uint256.NewInt(1000), ctx.GetBalance(common.HexToAddress("0x123"))) + + err = ctx.SubBalance(common.HexToAddress("0x123"), amount, tracing.BalanceChangeUnspecified) + assert.NoError(t, err) + assert.Equal(t, uint256.NewInt(500), ctx.GetBalance(common.HexToAddress("0x123"))) + + ctx.SetReadOnly(true) + + err = ctx.AddBalance(common.HexToAddress("0x123"), amount, tracing.BalanceChangeUnspecified) + assert.Error(t, err) + + err = ctx.SubBalance(common.HexToAddress("0x123"), amount, tracing.BalanceChangeUnspecified) + assert.Error(t, err) +} diff --git a/precompile/stateful_contract.go b/precompile/stateful_contract.go new file mode 100644 index 000000000..83fa32202 --- /dev/null +++ b/precompile/stateful_contract.go @@ -0,0 +1,46 @@ +package precompile + +import ( + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" +) + +// Gas costs +const ( + GasFree uint64 = 0 + GasQuickStep uint64 = 2 + GasFastestStep uint64 = 3 + GasFastStep uint64 = 5 + GasMidStep uint64 = 8 + GasSlowStep uint64 = 10 + GasExtStep uint64 = 20 +) + +func WordLength(input []byte, wordSize uint64) uint64 { + return (uint64(len(input)) + wordSize - 1) / wordSize +} + +type statefulPrecompiledContract struct { + abi abi.ABI +} + +func NewStatefulPrecompiledContract(abiStr string) StatefulPrecompiledContract { + abi, err := abi.JSON(strings.NewReader(abiStr)) + if err != nil { + panic(err) + } + return &statefulPrecompiledContract{ + abi: abi, + } +} + +func (spc *statefulPrecompiledContract) GetABI() abi.ABI { + return spc.abi +} + +// This is a placeholder implementation. The actual gas required would depend on the specific contract. +// You should replace this with the actual implementation. +func (spc *statefulPrecompiledContract) DefaultGas(input []byte) uint64 { + return GasFree +}