Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rebuilding of wasmstore as a part of node initialization #2314

Merged
merged 17 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions arbos/programs/programs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/log"
Expand Down Expand Up @@ -247,6 +248,10 @@ func (p Programs) CallProgram(

func getWasm(statedb vm.StateDB, program common.Address) ([]byte, error) {
prefixedWasm := statedb.GetCode(program)
return getWasmFromContractCode(prefixedWasm)
}

func getWasmFromContractCode(prefixedWasm []byte) ([]byte, error) {
if prefixedWasm == nil {
return nil, ProgramNotWasmError()
}
Expand Down Expand Up @@ -283,6 +288,70 @@ func (p Programs) getProgram(codeHash common.Hash, time uint64) (Program, error)
return program, err
}

// SaveActiveProgramToWasmStore is used to save active stylus programs to wasm store during rebuilding
func (p Programs) SaveActiveProgramToWasmStore(statedb *state.StateDB, codeHash common.Hash, code []byte, time uint64, debugMode bool, rebuildingStartBlockTime uint64) error {
ganeshvanahalli marked this conversation as resolved.
Show resolved Hide resolved
params, err := p.Params()
if err != nil {
return err
}

program, err := p.getActiveProgram(codeHash, time, params)
ganeshvanahalli marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
// The program is not active so return early
log.Info("program is not active, getActiveProgram returned error, hence do not include in rebuilding", "err", err)
return nil
}

// It might happen that node crashed some time after rebuilding commenced and before it completed, hence when rebuilding
// resumes after node is restarted the latest diskdb derived from statedb might now have codehashes that were activated
// during the last rebuilding session. In such cases we don't need to fetch moduleshashes but instead return early
// since they would already be added to the wasm store
currentHoursSince := hoursSinceArbitrum(rebuildingStartBlockTime)
if currentHoursSince < program.activatedAt {
return nil
}

moduleHash, err := p.moduleHashes.Get(codeHash)
if err != nil {
return err
}

// If already in wasm store then return early
localAsm, err := statedb.TryGetActivatedAsm(moduleHash)
if err == nil && len(localAsm) > 0 {
return nil
}

wasm, err := getWasmFromContractCode(code)
if err != nil {
log.Error("Failed to reactivate program while rebuilding wasm store: getWasmFromContractCode", "expected moduleHash", moduleHash, "err", err)
return fmt.Errorf("failed to reactivate program while rebuilding wasm store: %w", err)
}

unlimitedGas := uint64(0xffffffffffff)
// We know program is activated, so it must be in correct version and not use too much memory
// Empty program address is supplied because we dont have access to this during rebuilding of wasm store
info, asm, module, err := activateProgramInternal(statedb, common.Address{}, codeHash, wasm, params.PageLimit, program.version, debugMode, &unlimitedGas)
if err != nil {
log.Error("failed to reactivate program while rebuilding wasm store", "expected moduleHash", moduleHash, "err", err)
return fmt.Errorf("failed to reactivate program while rebuilding wasm store: %w", err)
}

if info.moduleHash != moduleHash {
log.Error("failed to reactivate program while rebuilding wasm store", "expected moduleHash", moduleHash, "got", info.moduleHash)
return fmt.Errorf("failed to reactivate program while rebuilding wasm store, expected ModuleHash: %v", moduleHash)
}

batch := statedb.Database().WasmStore().NewBatch()
rawdb.WriteActivation(batch, moduleHash, asm, module)
if err := batch.Write(); err != nil {
log.Error("failed writing re-activation to state while rebuilding wasm store", "err", err)
return err
}

return nil
}

// Gets a program entry. Errors if not active.
func (p Programs) getActiveProgram(codeHash common.Hash, time uint64, params *StylusParams) (Program, error) {
program, err := p.getProgram(codeHash, time)
Expand Down
3 changes: 3 additions & 0 deletions arbos/programs/wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ func cacheProgram(db vm.StateDB, module common.Hash, program Program, params *St
}
func evictProgram(db vm.StateDB, module common.Hash, version uint16, debug bool, mode core.MessageRunMode, forever bool) {
}
func activateProgramInternal(db vm.StateDB, program common.Address, codehash common.Hash, wasm []byte, page_limit uint16, version uint16, debug bool, gasLeft *uint64) (*activationInfo, []byte, []byte, error) {
return nil, nil, nil, nil
}

//go:wasmimport programs new_program
func newProgram(
Expand Down
36 changes: 35 additions & 1 deletion cmd/nitro/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,34 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo
return chainDb, l2BlockChain, fmt.Errorf("failed to recreate missing states: %w", err)
}
}

latestBlock := l2BlockChain.CurrentBlock()
if latestBlock.Number.Uint64() <= chainConfig.ArbitrumChainParams.GenesisBlockNum {
ganeshvanahalli marked this conversation as resolved.
Show resolved Hide resolved
// If there is only genesis block or no blocks in the blockchain, set Rebuilding of wasm store to Done
log.Info("setting rebuilding of wasm store to done")
if err = gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingPositionKey, gethexec.RebuildingDone); err != nil {
ganeshvanahalli marked this conversation as resolved.
Show resolved Hide resolved
return nil, nil, fmt.Errorf("unable to set rebuilding status of wasm store to done: %w", err)
}
} else {
position, err := gethexec.ReadFromKeyValueStore[common.Hash](wasmDb, gethexec.RebuildingPositionKey)
if err != nil {
log.Info("unable to get codehash position in rebuilding of wasm store, its possible it isnt initialized yet, so initializing it and starting rebuilding", "err", err)
if err := gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingPositionKey, common.Hash{}); err != nil {
return nil, nil, fmt.Errorf("unable to initialize codehash position in rebuilding of wasm store to beginning: %w", err)
}
}
if position != gethexec.RebuildingDone {
startBlockHash, err := gethexec.ReadFromKeyValueStore[common.Hash](wasmDb, gethexec.RebuildingStartBlockHashKey)
if err != nil {
log.Info("unable to get start block hash in rebuilding of wasm store, its possible it isnt initialized yet, so initializing it to latest block hash", "err", err)
if err := gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingStartBlockHashKey, latestBlock.Hash()); err != nil {
return nil, nil, fmt.Errorf("unable to initialize start block hash in rebuilding of wasm store to latest block hash: %w", err)
}
startBlockHash = latestBlock.Hash()
}
log.Info("starting or continuing rebuilding of wasm store", "codeHash", position, "startBlockHash", startBlockHash)
gethexec.RebuildWasmStore(ctx, wasmDb, l2BlockChain, position, startBlockHash)
}
}
return chainDb, l2BlockChain, nil
}
readOnlyDb.Close()
Expand Down Expand Up @@ -245,6 +272,13 @@ func openInitializeChainDb(ctx context.Context, stack *node.Node, config *NodeCo
}
chainDb := rawdb.WrapDatabaseWithWasm(chainData, wasmDb, 1)

// Rebuilding wasm store is not required when just starting out
err = gethexec.WriteToKeyValueStore(wasmDb, gethexec.RebuildingPositionKey, gethexec.RebuildingDone)
log.Info("setting codehash position in rebuilding of wasm store to done")
if err != nil {
return nil, nil, fmt.Errorf("unable to set codehash position in rebuilding of wasm store to done: %w", err)
}

if config.Init.ImportFile != "" {
initDataReader, err = statetransfer.NewJsonInitDataReader(config.Init.ImportFile)
if err != nil {
Expand Down
115 changes: 115 additions & 0 deletions execution/gethexec/wasmstorerebuilder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2021-2024, Offchain Labs, Inc.
// For license information, see https://github.com/nitro/blob/master/LICENSE

package gethexec

import (
"bytes"
"context"
"fmt"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/offchainlabs/nitro/arbos/arbosState"
)

var RebuildingPositionKey []byte = []byte("_rebuildingPosition") // contains the codehash upto which rebuilding of wasm store was last completed. Initialized to common.Hash{} at the start
var RebuildingStartBlockHashKey []byte = []byte("_rebuildingStartBlockHash") // contains the block hash of starting block when rebuilding of wasm store first began
var RebuildingDone common.Hash = common.BytesToHash([]byte("_done")) // indicates that the rebuilding is done, if RebuildingPositionKey holds this value it implies rebuilding was completed

func ReadFromKeyValueStore[T any](store ethdb.KeyValueStore, key []byte) (T, error) {
var empty T
posBytes, err := store.Get(key)
if err != nil {
return empty, err
}
var val T
err = rlp.DecodeBytes(posBytes, &val)
if err != nil {
return empty, fmt.Errorf("error decoding value stored for key in the KeyValueStore: %w", err)
}
return val, nil
}

func WriteToKeyValueStore[T any](store ethdb.KeyValueStore, key []byte, val T) error {
valBytes, err := rlp.EncodeToBytes(val)
if err != nil {
return err
}
err = store.Put(key, valBytes)
if err != nil {
return err
}
return nil
}

// RebuildWasmStore function runs a loop looking at every codehash in diskDb, checking if its an activated stylus contract and
// saving it to wasm store if it doesnt already exists. When errored it logs them and silently returns
//
// It stores the status of rebuilding to wasm store by updating the codehash (of the latest sucessfully checked contract) in
// RebuildingPositionKey every 50 checks.
ganeshvanahalli marked this conversation as resolved.
Show resolved Hide resolved
//
// It also stores a special value that is only set once when rebuilding commenced in RebuildingStartBlockHashKey as the block
// time of the latest block when rebuilding was first called, this is used to avoid recomputing of assembly and module of
// contracts that were created after rebuilding commenced since they would anyway already be added during sync.
func RebuildWasmStore(ctx context.Context, wasmStore ethdb.KeyValueStore, l2Blockchain *core.BlockChain, position, rebuildingStartBlockHash common.Hash) {
ganeshvanahalli marked this conversation as resolved.
Show resolved Hide resolved
var err error
var stateDb *state.StateDB
latestHeader := l2Blockchain.CurrentBlock()
// Attempt to get state at the start block when rebuilding commenced, if not available (in case of non-archival nodes) use latest state
rebuildingStartHeader := l2Blockchain.GetHeaderByHash(rebuildingStartBlockHash)
stateDb, err = l2Blockchain.StateAt(rebuildingStartHeader.Root)
ganeshvanahalli marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
log.Info("error getting state at start block of rebuilding wasm store, attempting rebuilding with latest state", "err", err)
stateDb, err = l2Blockchain.StateAt(latestHeader.Root)
if err != nil {
log.Error("error getting state at latest block, aborting rebuilding", "err", err)
return
}
}
diskDb := stateDb.Database().DiskDB()
arbState, err := arbosState.OpenSystemArbosState(stateDb, nil, true)
if err != nil {
log.Error("error getting arbos state, aborting rebuilding", "err", err)
return
}
programs := arbState.Programs()
iter := diskDb.NewIterator(rawdb.CodePrefix, position[:])
for count := 1; iter.Next(); count++ {
codeHashBytes := bytes.TrimPrefix(iter.Key(), rawdb.CodePrefix)
codeHash := common.BytesToHash(codeHashBytes)
code := iter.Value()
if state.IsStylusProgram(code) {
if err := programs.SaveActiveProgramToWasmStore(stateDb, codeHash, code, latestHeader.Time, l2Blockchain.Config().DebugMode(), rebuildingStartHeader.Time); err != nil {
log.Error("error while rebuilding of wasm store, aborting rebuilding", "err", err)
return
}
}
// After every fifty codeHash checks, update the rebuilding position
// This also notifies user that we are working on rebuilding
if count%50 == 0 || ctx.Err() != nil {
ganeshvanahalli marked this conversation as resolved.
Show resolved Hide resolved
log.Info("Storing rebuilding status to disk", "codeHash", codeHash)
if err := WriteToKeyValueStore(wasmStore, RebuildingPositionKey, codeHash); err != nil {
log.Error("error updating codehash position in rebuilding of wasm store", "err", err)
return
}
// If outer context is cancelled we should terminate rebuilding
// We attempted to write the latest checked codeHash to wasm store
if ctx.Err() != nil {
return
}
}
}
iter.Release()
ganeshvanahalli marked this conversation as resolved.
Show resolved Hide resolved
// Set rebuilding position to done indicating completion
if err := WriteToKeyValueStore(wasmStore, RebuildingPositionKey, RebuildingDone); err != nil {
log.Error("error updating codehash position in rebuilding of wasm store to done", "err", err)
return
}
log.Info("Rebuilding of wasm store was successful")
}
Loading
Loading