mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
An import accepts what it inserts
admin_importChain and --import-chain-data validate blocks with InsertChain and then move the accepted tip with SetLastAcceptedBlockDirect. Neither step does what the acceptor does for a block consensus accepts: index its transactions and hand its trie to the state manager. An archive node therefore committed only the roots the import force-committed at round ends and indexed only each round's last block. After a restart every other height's state was a missing trie node, eth_getTransactionByHash found one transaction per round, and with commit-interval 1 the startup head repair could not re-execute the tip from the height before it, so the C-Chain did not start. AcceptImported does the acceptor's durable work for each inserted block, in order, through acceptState, which the acceptor now calls too. TestImportArchive_HistorySurvivesRestart imports in rounds shorter than the chain, reopens it on the same database, and checks every height's state and every transaction's lookup. On the previous code it fails at height 1, and with commit-interval 1 the chain does not reopen. A database an earlier release imported keeps the gaps; only a fresh import under this one fills them. Fixes #152 Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -608,6 +608,12 @@ func importBlocksFromFile(chain *core.BlockChain, file string, batch int, afterC
|
||||
lastInsertedBlock := blocks[n-1]
|
||||
currentHeight := lastInsertedBlock.NumberU64()
|
||||
|
||||
// Accept each inserted block's indices and state as consensus Accept would:
|
||||
// in archive mode this is what puts every height's state on disk.
|
||||
if err := chain.AcceptImported(blocks[:n]); err != nil {
|
||||
return totalImported, common.Hash{}, 0, fmt.Errorf("batch %d: accept failed: %w", batch, err)
|
||||
}
|
||||
|
||||
// Update last accepted so RPC can query imported blocks
|
||||
if err := chain.SetLastAcceptedBlockDirect(lastInsertedBlock); err != nil {
|
||||
return totalImported, common.Hash{}, 0, fmt.Errorf("batch %d: failed to set last accepted: %w", batch, err)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package evm
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
"github.com/luxfi/evm/consensus/dummy"
|
||||
"github.com/luxfi/evm/core"
|
||||
"github.com/luxfi/evm/params"
|
||||
gethcommon "github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/core/rawdb"
|
||||
gethtypes "github.com/luxfi/geth/core/types"
|
||||
gethvm "github.com/luxfi/geth/core/vm"
|
||||
"github.com/luxfi/geth/rlp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestImportArchive_HistorySurvivesRestart imports an RLP into an archive chain
|
||||
// (pruning disabled), stops it, reopens a chain on the same database, and requires
|
||||
// what an archive node exists to serve: the state at every imported height and a
|
||||
// lookup for every imported transaction. The import runs in rounds smaller than
|
||||
// the chain, so heights inside a round are checked and not only a round's last.
|
||||
//
|
||||
// Commit interval 1 is the shape that left an imported testnet unable to start:
|
||||
// the startup head repair re-executes the tip from the height before it, whose
|
||||
// state the import had never committed.
|
||||
func TestImportArchive_HistorySurvivesRestart(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
commitInterval uint64
|
||||
}{
|
||||
{"default commit interval", core.DefaultCacheConfig.CommitInterval},
|
||||
{"commit interval 1", 1},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
const n, round = 24, 5
|
||||
key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
senderAddr := crypto.PubkeyToAddress(key.PublicKey)
|
||||
sender := gethcommon.BytesToAddress(senderAddr[:])
|
||||
recipient := gethcommon.HexToAddress("0x000000000000000000000000000000000000aaaa")
|
||||
bal, _ := new(big.Int).SetString("100000000000000000000000", 10)
|
||||
gspec := &core.Genesis{Config: params.TestChainConfig, Alloc: gethtypes.GenesisAlloc{sender: {Balance: bal}}}
|
||||
engine := dummy.NewETHFaker()
|
||||
signer := gethtypes.LatestSigner(params.TestChainConfig)
|
||||
|
||||
// Block i pays the recipient i+1 wei: its balance at height h is h(h+1)/2,
|
||||
// so every height's state is distinct.
|
||||
coinbase := gethcommon.HexToAddress("0x0100000000000000000000000000000000000000")
|
||||
_, blocks, _, err := core.GenerateChainWithGenesis(gspec, engine, n, 10, func(i int, b *core.BlockGen) {
|
||||
b.SetCoinbase(coinbase)
|
||||
tx, err := gethtypes.SignTx(gethtypes.NewTransaction(uint64(i), recipient, big.NewInt(int64(i+1)), 21000, b.BaseFee(), nil), signer, key)
|
||||
require.NoError(t, err)
|
||||
b.AddTx(tx)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
file := filepath.Join(t.TempDir(), "chain.rlp")
|
||||
f, err := os.Create(file)
|
||||
require.NoError(t, err)
|
||||
for _, b := range blocks {
|
||||
require.NoError(t, rlp.Encode(f, b))
|
||||
}
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
archive := *core.DefaultCacheConfig
|
||||
archive.Pruning = false
|
||||
archive.CommitInterval = tc.commitInterval
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
|
||||
chain, err := core.NewBlockChain(db, &archive, gspec, engine, gethvm.Config{}, gethcommon.Hash{}, false, nil)
|
||||
require.NoError(t, err)
|
||||
imported, tipHash, tip, err := importBlocksFromFile(chain, file, round, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, imported)
|
||||
require.EqualValues(t, n, tip)
|
||||
chain.Stop()
|
||||
|
||||
reopened, err := core.NewBlockChain(db, &archive, gspec, engine, gethvm.Config{}, tipHash, false, nil)
|
||||
require.NoError(t, err, "an imported archive must start again")
|
||||
t.Cleanup(reopened.Stop)
|
||||
require.Equal(t, tipHash, reopened.LastAcceptedBlock().Hash())
|
||||
|
||||
for h := uint64(1); h <= n; h++ {
|
||||
b := reopened.GetBlockByNumber(h)
|
||||
require.NotNilf(t, b, "block %d", h)
|
||||
require.Truef(t, reopened.HasState(b.Root()), "state at height %d missing after restart", h)
|
||||
st, err := reopened.StateAt(b.Root())
|
||||
require.NoErrorf(t, err, "state at height %d", h)
|
||||
require.EqualValuesf(t, h*(h+1)/2, st.GetBalance(recipient).Uint64(), "recipient balance at height %d", h)
|
||||
for _, tx := range b.Transactions() {
|
||||
require.NotNilf(t, rawdb.ReadTxLookupEntry(db, tx.Hash()), "tx %s at height %d not indexed", tx.Hash(), h)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user