Skip to content

Commit

Permalink
Merge pull request #4 from NethermindEth/core/rip/7728-precompile-impl
Browse files Browse the repository at this point in the history
[P2] Implements RIP-7728
  • Loading branch information
mralj authored Oct 7, 2024
2 parents 204ef24 + 56c1f67 commit 5f039c5
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 25 deletions.
62 changes: 56 additions & 6 deletions core/vm/contracts_rollup.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
package vm

import (
"context"
"errors"
"math/big"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)

Expand Down Expand Up @@ -41,31 +44,78 @@ func (evm *EVM) activateRollupPrecompiledContracts() {
})
}

//INPUT SPECS:
//Byte range Name Description
//------------------------------------------------------------
//[0: 19] (20 bytes) address The contract address
//[20: 51] (32 bytes) key1 The storage key
//... ... ...
//[k*32-12: k*32+19] (32 bytes) key_k The storage key

type L1SLoad struct {
L1RpcClient L1RpcClient
GetLatestL1BlockNumber func() *big.Int
}

func (c *L1SLoad) RequiredGas(input []byte) uint64 { return 0 }
func (c *L1SLoad) RequiredGas(input []byte) uint64 {
storageSlotsToLoad := len(input[common.AddressLength-1:]) / common.HashLength
storageSlotsToLoad = min(storageSlotsToLoad, params.L1SLoadMaxNumStorageSlots)

return params.L1SLoadBaseGas + uint64(storageSlotsToLoad)*params.L1SLoadPerLoadGas
}

func (c *L1SLoad) Run(input []byte) ([]byte, error) {
if !c.isL1SLoadActive() {
return nil, errors.New("L1SLoad precompile not active")
log.Error("L1SLOAD called, but not activated", "client", c.L1RpcClient, "and latest block number function", c.GetLatestL1BlockNumber)
return nil, errors.New("L1SLOAD precompile not active")
}

if len(input) < common.AddressLength+common.HashLength {
return nil, errors.New("L1SLOAD input too short")
}

countOfStorageKeysToRead := (len(input) - common.AddressLength) / common.HashLength
thereIsAtLeast1StorageKeyToRead := countOfStorageKeysToRead > 0
allStorageKeysAreExactly32Bytes := countOfStorageKeysToRead*common.HashLength == len(input)-common.AddressLength

if inputIsInvalid := !(thereIsAtLeast1StorageKeyToRead && allStorageKeysAreExactly32Bytes); inputIsInvalid {
return nil, errors.New("L1SLOAD input is malformed")
}

contractAddress := common.BytesToAddress(input[:common.AddressLength])
input = input[common.AddressLength-1:]
contractStorageKeys := make([]common.Hash, countOfStorageKeysToRead)
for k := 0; k < countOfStorageKeysToRead; k++ {
contractStorageKeys[k] = common.BytesToHash(input[k*common.HashLength : (k+1)*common.HashLength])
}

var ctx context.Context
if params.L1SLoadRPCTimeoutInSec > 0 {
c, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(params.L1SLoadRPCTimeoutInSec))
ctx = c
defer cancel()
} else {
ctx = context.Background()
}

res, err := c.L1RpcClient.StoragesAt(ctx, contractAddress, contractStorageKeys, c.GetLatestL1BlockNumber())
if err != nil {
return nil, err
}

return nil, nil
return res, nil
}

func (c *L1SLoad) isL1SLoadActive() bool {
return c.GetLatestL1BlockNumber != nil && c.L1RpcClient != nil
}

func (pc *PrecompiledContracts) activateL1SLoad(l1RpcClient L1RpcClient, getLatestL1BlockNumber func() *big.Int) {
rulesSayContractShouldBeActive := (*pc)[rollupL1SloadAddress] != nil
func (pc PrecompiledContracts) activateL1SLoad(l1RpcClient L1RpcClient, getLatestL1BlockNumber func() *big.Int) {
rulesSayContractShouldBeActive := pc[rollupL1SloadAddress] != nil
paramsNotNil := l1RpcClient != nil && getLatestL1BlockNumber != nil

if shouldActivateL1SLoad := rulesSayContractShouldBeActive && paramsNotNil; shouldActivateL1SLoad {
(*pc)[rollupL1SloadAddress] = &L1SLoad{
pc[rollupL1SloadAddress] = &L1SLoad{
L1RpcClient: l1RpcClient,
GetLatestL1BlockNumber: getLatestL1BlockNumber,
}
Expand Down
17 changes: 1 addition & 16 deletions core/vm/contracts_rollup_overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@

package vm

import (
"math/big"
)
import "math/big"

type RollupPrecompiledContractsOverrides struct {
l1SLoadGetLatestL1Block func() *big.Int
Expand All @@ -25,16 +23,3 @@ func getLatestL1BlockNumber(evm *EVM) func() *big.Int {
return evm.Context.BlockNumber
}
}

// [OVERRIDE] getLatestL1BlockNumber
// Each rollup should override this function so that it returns
// correct latest L1 block number
//
// EXAMPLE 2
// func getLatestL1BlockNumber(evm *EVM) func() *big.Int {
// return func() *big.Int {
// addressOfL1BlockContract := common.Address{}
// slotInContractRepresentingL1BlockNumber := common.Hash{}
// return evm.StateDB.GetState(addressOfL1BlockContract, slotInContractRepresentingL1BlockNumber).Big()
// }
// }
32 changes: 32 additions & 0 deletions core/vm/contracts_rollup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package vm

import (
"context"
"math/big"
"testing"

"github.com/ethereum/go-ethereum/common"
)

type MockL1RPCClient struct{}

func (m MockL1RPCClient) StoragesAt(ctx context.Context, account common.Address, keys []common.Hash, blockNumber *big.Int) ([]byte, error) {
// testcase is in format "abab", this makes output lenght 2 bytes
const mockedRespValueSize = 2
mockResp := make([]byte, mockedRespValueSize*len(keys))
for i := range keys {
copy(mockResp[mockedRespValueSize*i:], common.Hex2Bytes("abab"))
}

return mockResp, nil
}

func TestPrecompiledL1SLOAD(t *testing.T) {
mockL1RPCClient := MockL1RPCClient{}

allPrecompiles[rollupL1SloadAddress] = &L1SLoad{}
allPrecompiles.activateL1SLoad(mockL1RPCClient, func() *big.Int { return big1 })

testJson("l1sload", rollupL1SloadAddress.Hex(), t)
testJsonFail("l1sload", rollupL1SloadAddress.Hex(), t)
}
4 changes: 2 additions & 2 deletions core/vm/contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type precompiledFailureTest struct {

// allPrecompiles does not map to the actual set of precompiles, as it also contains
// repriced versions of precompiles at certain slots
var allPrecompiles = map[common.Address]PrecompiledContract{
var allPrecompiles = PrecompiledContracts{
common.BytesToAddress([]byte{1}): &ecrecover{},
common.BytesToAddress([]byte{2}): &sha256hash{},
common.BytesToAddress([]byte{3}): &ripemd160hash{},
Expand Down Expand Up @@ -181,7 +181,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) {
// Keep it as uint64, multiply 100 to get two digit float later
mgasps := (100 * 1000 * gasUsed) / elapsed
bench.ReportMetric(float64(mgasps)/100, "mgas/s")
//Check if it is correct
// Check if it is correct
if err != nil {
bench.Error(err)
return
Expand Down
2 changes: 1 addition & 1 deletion core/vm/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type Config struct {

// [rollup-geth]
type L1RpcClient interface {
StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error)
StoragesAt(ctx context.Context, account common.Address, keys []common.Hash, blockNumber *big.Int) ([]byte, error)
}

// ScopeContext contains the things that are per-call, such as stack and memory,
Expand Down
24 changes: 24 additions & 0 deletions core/vm/testdata/precompiles/fail-l1sload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[
{
"Name": "L1SLOAD FAIL: input contains only address",
"Input": "a83114A443dA1CecEFC50368531cACE9F37fCCcb",
"ExpectedError": "L1SLOAD input too short",
"Gas": 4000,
"NoBenchmark": true
},
{
"Name": "L1SLOAD FAIL: input key not 32 bytes",
"Input": "a83114A443dA1CecEFC50368531cACE9F37fCCcb112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7122",
"ExpectedError": "L1SLOAD input is malformed",
"Gas": 4000,
"NoBenchmark": true
},
{
"Name": "L1SLOAD FAIL: input too long",
"Input": "C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22d2c7bb6fc06067df8b0223aec460d1ebb51febb9012bc2554141a4dca08e8640112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7a112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7a112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7a112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7aa83114A443dA1CecEFC50368531cACE9F37fCCcb112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7122",
"ExpectedError": "L1SLOAD input is malformed",
"Gas": 4000,
"NoBenchmark": true
}

]
24 changes: 24 additions & 0 deletions core/vm/testdata/precompiles/l1sload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[
{
"Name": "L1SLOAD: 1 key",
"Input": "C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22d2c7bb6fc06067df8b0223aec460d1ebb51febb9012bc2554141a4dca08e864",
"Expected": "abab",
"Gas": 4000,
"NoBenchmark": true
},
{
"Name": "L1SLOAD: 2 keys",
"Input": "C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22d2c7bb6fc06067df8b0223aec460d1ebb51febb9012bc2554141a4dca08e8640112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7a",
"Expected": "abababab",
"Gas": 6000,
"NoBenchmark": true
},

{
"Name": "L1SLOAD: 5 keys",
"Input": "C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22d2c7bb6fc06067df8b0223aec460d1ebb51febb9012bc2554141a4dca08e8640112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7a112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7a112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7a112d016b65e9c617ad9ab60604f772a3620177bada4cdc773d9b6a982d3c2a7a",
"Expected": "abababababababababab",
"Gas": 12000,
"NoBenchmark": true
}
]
7 changes: 7 additions & 0 deletions eth/tracers/api_rollup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package tracers

import "github.com/ethereum/go-ethereum/core/vm"

func (b *testBackend) GetL1RpcClient() vm.L1RpcClient {
return nil
}
38 changes: 38 additions & 0 deletions ethclient/ethclient_rollup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ethclient

import (
"context"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
)

// StoragesAt returns the values of keys in the contract storage of the given account.
// The block number can be nil, in which case the value is taken from the latest known block.
func (ec *Client) StoragesAt(ctx context.Context, account common.Address, keys []common.Hash, blockNumber *big.Int) ([]byte, error) {
results := make([]hexutil.Bytes, len(keys))
reqs := make([]rpc.BatchElem, len(keys))

for i := range reqs {
reqs[i] = rpc.BatchElem{
Method: "eth_getStorageAt",
Args: []interface{}{account, keys[i], toBlockNumArg(blockNumber)},
Result: &results[i],
}
}
if err := ec.c.BatchCallContext(ctx, reqs); err != nil {
return nil, err
}

output := make([]byte, common.HashLength*len(keys))
for i := range reqs {
if reqs[i].Error != nil {
return nil, reqs[i].Error
}
copy(output[i*common.HashLength:], results[i])
}

return output, nil
}
7 changes: 7 additions & 0 deletions internal/ethapi/api_rollup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ethapi

import "github.com/ethereum/go-ethereum/core/vm"

func (b *testBackend) GetL1RpcClient() vm.L1RpcClient {
return nil
}
7 changes: 7 additions & 0 deletions internal/ethapi/transaction_args_rollup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ethapi

import "github.com/ethereum/go-ethereum/core/vm"

func (b *backendMock) GetL1RpcClient() vm.L1RpcClient {
return nil
}
8 changes: 8 additions & 0 deletions params/protocol_params_rollup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package params

const (
L1SLoadBaseGas uint64 = 2000 // Base price for L1Sload
L1SLoadPerLoadGas uint64 = 2000 // Per-load price for loading one storage slot
L1SLoadMaxNumStorageSlots = 5 // Max number of storage slots requested in L1Sload precompile
L1SLoadRPCTimeoutInSec = 3
)

0 comments on commit 5f039c5

Please sign in to comment.