Merge tag 'v1.10.9' into develop
Notes: the AppendAncient plugin hook is broken by this commit. This adds CaptureEnter() and CaptureExit() as no-ops for interface compliance, but these capabilities should be added for plugin tracers soon.
This commit is contained in:
+84
-119
@@ -207,8 +207,7 @@ type BlockChain struct {
|
||||
processor Processor // Block transaction processor interface
|
||||
vmConfig vm.Config
|
||||
|
||||
shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block.
|
||||
terminateInsert func(common.Hash, uint64) bool // Testing hook used to terminate ancient receipt chain insertion.
|
||||
shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block.
|
||||
}
|
||||
|
||||
// NewBlockChain returns a fully initialised block chain using information
|
||||
@@ -1085,38 +1084,6 @@ const (
|
||||
SideStatTy
|
||||
)
|
||||
|
||||
// truncateAncient rewinds the blockchain to the specified header and deletes all
|
||||
// data in the ancient store that exceeds the specified header.
|
||||
func (bc *BlockChain) truncateAncient(head uint64) error {
|
||||
frozen, err := bc.db.Ancients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Short circuit if there is no data to truncate in ancient store.
|
||||
if frozen <= head+1 {
|
||||
return nil
|
||||
}
|
||||
// Truncate all the data in the freezer beyond the specified head
|
||||
if err := bc.db.TruncateAncients(head + 1); err != nil {
|
||||
return err
|
||||
}
|
||||
// Clear out any stale content from the caches
|
||||
bc.hc.headerCache.Purge()
|
||||
bc.hc.tdCache.Purge()
|
||||
bc.hc.numberCache.Purge()
|
||||
|
||||
// Clear out any stale content from the caches
|
||||
bc.bodyCache.Purge()
|
||||
bc.bodyRLPCache.Purge()
|
||||
bc.receiptsCache.Purge()
|
||||
bc.blockCache.Purge()
|
||||
bc.txLookupCache.Purge()
|
||||
bc.futureBlocks.Purge()
|
||||
|
||||
log.Info("Rewind ancient data", "number", head)
|
||||
return nil
|
||||
}
|
||||
|
||||
// numberHash is just a container for a number and a hash, to represent a block
|
||||
type numberHash struct {
|
||||
number uint64
|
||||
@@ -1155,12 +1122,14 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
|
||||
var (
|
||||
stats = struct{ processed, ignored int32 }{}
|
||||
start = time.Now()
|
||||
size = 0
|
||||
size = int64(0)
|
||||
)
|
||||
|
||||
// updateHead updates the head fast sync block if the inserted blocks are better
|
||||
// and returns an indicator whether the inserted blocks are canonical.
|
||||
updateHead := func(head *types.Block) bool {
|
||||
bc.chainmu.Lock()
|
||||
defer bc.chainmu.Unlock()
|
||||
|
||||
// Rewind may have occurred, skip in that case.
|
||||
if bc.CurrentHeader().Number.Cmp(head.Number()) >= 0 {
|
||||
@@ -1169,68 +1138,63 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
|
||||
rawdb.WriteHeadFastBlockHash(bc.db, head.Hash())
|
||||
bc.currentFastBlock.Store(head)
|
||||
headFastBlockGauge.Update(int64(head.NumberU64()))
|
||||
bc.chainmu.Unlock()
|
||||
return true
|
||||
}
|
||||
}
|
||||
bc.chainmu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
// writeAncient writes blockchain and corresponding receipt chain into ancient store.
|
||||
//
|
||||
// this function only accepts canonical chain data. All side chain will be reverted
|
||||
// eventually.
|
||||
writeAncient := func(blockChain types.Blocks, receiptChain []types.Receipts) (int, error) {
|
||||
var (
|
||||
previous = bc.CurrentFastBlock()
|
||||
batch = bc.db.NewBatch()
|
||||
)
|
||||
// If any error occurs before updating the head or we are inserting a side chain,
|
||||
// all the data written this time wll be rolled back.
|
||||
defer func() {
|
||||
if previous != nil {
|
||||
if err := bc.truncateAncient(previous.NumberU64()); err != nil {
|
||||
log.Crit("Truncate ancient store failed", "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
var deleted []*numberHash
|
||||
for i, block := range blockChain {
|
||||
// Short circuit insertion if shutting down or processing failed
|
||||
if bc.insertStopped() {
|
||||
return 0, errInsertionInterrupted
|
||||
}
|
||||
// Short circuit insertion if it is required(used in testing only)
|
||||
if bc.terminateInsert != nil && bc.terminateInsert(block.Hash(), block.NumberU64()) {
|
||||
return i, errors.New("insertion is terminated for testing purpose")
|
||||
}
|
||||
// Short circuit if the owner header is unknown
|
||||
if !bc.HasHeader(block.Hash(), block.NumberU64()) {
|
||||
return i, fmt.Errorf("containing header #%d [%x..] unknown", block.Number(), block.Hash().Bytes()[:4])
|
||||
}
|
||||
if block.NumberU64() == 1 {
|
||||
// Make sure to write the genesis into the freezer
|
||||
if frozen, _ := bc.db.Ancients(); frozen == 0 {
|
||||
h := rawdb.ReadCanonicalHash(bc.db, 0)
|
||||
b := rawdb.ReadBlock(bc.db, h, 0)
|
||||
size += rawdb.WriteAncientBlock(bc.db, b, rawdb.ReadReceipts(bc.db, h, 0, bc.chainConfig), rawdb.ReadTd(bc.db, h, 0))
|
||||
log.Info("Wrote genesis to ancients")
|
||||
}
|
||||
}
|
||||
// Flush data into ancient database.
|
||||
size += rawdb.WriteAncientBlock(bc.db, block, receiptChain[i], bc.GetTd(block.Hash(), block.NumberU64()))
|
||||
first := blockChain[0]
|
||||
last := blockChain[len(blockChain)-1]
|
||||
|
||||
// Write tx indices if any condition is satisfied:
|
||||
// * If user requires to reserve all tx indices(txlookuplimit=0)
|
||||
// * If all ancient tx indices are required to be reserved(txlookuplimit is even higher than ancientlimit)
|
||||
// * If block number is large enough to be regarded as a recent block
|
||||
// It means blocks below the ancientLimit-txlookupLimit won't be indexed.
|
||||
//
|
||||
// But if the `TxIndexTail` is not nil, e.g. Geth is initialized with
|
||||
// an external ancient database, during the setup, blockchain will start
|
||||
// a background routine to re-indexed all indices in [ancients - txlookupLimit, ancients)
|
||||
// range. In this case, all tx indices of newly imported blocks should be
|
||||
// generated.
|
||||
// Ensure genesis is in ancients.
|
||||
if first.NumberU64() == 1 {
|
||||
if frozen, _ := bc.db.Ancients(); frozen == 0 {
|
||||
b := bc.genesisBlock
|
||||
td := bc.genesisBlock.Difficulty()
|
||||
writeSize, err := rawdb.WriteAncientBlocks(bc.db, []*types.Block{b}, []types.Receipts{nil}, td)
|
||||
size += writeSize
|
||||
if err != nil {
|
||||
log.Error("Error writing genesis to ancients", "err", err)
|
||||
return 0, err
|
||||
}
|
||||
log.Info("Wrote genesis to ancients")
|
||||
}
|
||||
}
|
||||
// Before writing the blocks to the ancients, we need to ensure that
|
||||
// they correspond to the what the headerchain 'expects'.
|
||||
// We only check the last block/header, since it's a contiguous chain.
|
||||
if !bc.HasHeader(last.Hash(), last.NumberU64()) {
|
||||
return 0, fmt.Errorf("containing header #%d [%x..] unknown", last.Number(), last.Hash().Bytes()[:4])
|
||||
}
|
||||
|
||||
// Write all chain data to ancients.
|
||||
td := bc.GetTd(first.Hash(), first.NumberU64())
|
||||
writeSize, err := rawdb.WriteAncientBlocks(bc.db, blockChain, receiptChain, td)
|
||||
size += writeSize
|
||||
if err != nil {
|
||||
log.Error("Error importing chain data to ancients", "err", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Write tx indices if any condition is satisfied:
|
||||
// * If user requires to reserve all tx indices(txlookuplimit=0)
|
||||
// * If all ancient tx indices are required to be reserved(txlookuplimit is even higher than ancientlimit)
|
||||
// * If block number is large enough to be regarded as a recent block
|
||||
// It means blocks below the ancientLimit-txlookupLimit won't be indexed.
|
||||
//
|
||||
// But if the `TxIndexTail` is not nil, e.g. Geth is initialized with
|
||||
// an external ancient database, during the setup, blockchain will start
|
||||
// a background routine to re-indexed all indices in [ancients - txlookupLimit, ancients)
|
||||
// range. In this case, all tx indices of newly imported blocks should be
|
||||
// generated.
|
||||
var batch = bc.db.NewBatch()
|
||||
for _, block := range blockChain {
|
||||
if bc.txLookupLimit == 0 || ancientLimit <= bc.txLookupLimit || block.NumberU64() >= ancientLimit-bc.txLookupLimit {
|
||||
rawdb.WriteTxLookupEntriesByBlock(batch, block)
|
||||
} else if rawdb.ReadTxIndexTail(bc.db) != nil {
|
||||
@@ -1238,51 +1202,50 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
|
||||
}
|
||||
stats.processed++
|
||||
}
|
||||
|
||||
// Flush all tx-lookup index data.
|
||||
size += batch.ValueSize()
|
||||
size += int64(batch.ValueSize())
|
||||
if err := batch.Write(); err != nil {
|
||||
// The tx index data could not be written.
|
||||
// Roll back the ancient store update.
|
||||
fastBlock := bc.CurrentFastBlock().NumberU64()
|
||||
if err := bc.db.TruncateAncients(fastBlock + 1); err != nil {
|
||||
log.Error("Can't truncate ancient store after failed insert", "err", err)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
batch.Reset()
|
||||
|
||||
// Sync the ancient store explicitly to ensure all data has been flushed to disk.
|
||||
if err := bc.db.Sync(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Update the current fast block because all block data is now present in DB.
|
||||
previousFastBlock := bc.CurrentFastBlock().NumberU64()
|
||||
if !updateHead(blockChain[len(blockChain)-1]) {
|
||||
return 0, errors.New("side blocks can't be accepted as the ancient chain data")
|
||||
}
|
||||
previous = nil // disable rollback explicitly
|
||||
|
||||
// Wipe out canonical block data.
|
||||
for _, nh := range deleted {
|
||||
rawdb.DeleteBlockWithoutNumber(batch, nh.hash, nh.number)
|
||||
rawdb.DeleteCanonicalHash(batch, nh.number)
|
||||
}
|
||||
for _, block := range blockChain {
|
||||
// Always keep genesis block in active database.
|
||||
if block.NumberU64() != 0 {
|
||||
rawdb.DeleteBlockWithoutNumber(batch, block.Hash(), block.NumberU64())
|
||||
rawdb.DeleteCanonicalHash(batch, block.NumberU64())
|
||||
// We end up here if the header chain has reorg'ed, and the blocks/receipts
|
||||
// don't match the canonical chain.
|
||||
if err := bc.db.TruncateAncients(previousFastBlock + 1); err != nil {
|
||||
log.Error("Can't truncate ancient store after failed insert", "err", err)
|
||||
}
|
||||
return 0, errSideChainReceipts
|
||||
}
|
||||
if err := batch.Write(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Delete block data from the main database.
|
||||
batch.Reset()
|
||||
|
||||
// Wipe out side chain too.
|
||||
for _, nh := range deleted {
|
||||
for _, hash := range rawdb.ReadAllHashes(bc.db, nh.number) {
|
||||
rawdb.DeleteBlock(batch, hash, nh.number)
|
||||
}
|
||||
}
|
||||
canonHashes := make(map[common.Hash]struct{})
|
||||
for _, block := range blockChain {
|
||||
// Always keep genesis block in active database.
|
||||
if block.NumberU64() != 0 {
|
||||
for _, hash := range rawdb.ReadAllHashes(bc.db, block.NumberU64()) {
|
||||
rawdb.DeleteBlock(batch, hash, block.NumberU64())
|
||||
}
|
||||
canonHashes[block.Hash()] = struct{}{}
|
||||
if block.NumberU64() == 0 {
|
||||
continue
|
||||
}
|
||||
rawdb.DeleteCanonicalHash(batch, block.NumberU64())
|
||||
rawdb.DeleteBlockWithoutNumber(batch, block.Hash(), block.NumberU64())
|
||||
}
|
||||
// Delete side chain hash-to-number mappings.
|
||||
for _, nh := range rawdb.ReadAllHashesInRange(bc.db, first.NumberU64(), last.NumberU64()) {
|
||||
if _, canon := canonHashes[nh.Hash]; !canon {
|
||||
rawdb.DeleteHeader(batch, nh.Hash, nh.Number)
|
||||
}
|
||||
}
|
||||
if err := batch.Write(); err != nil {
|
||||
@@ -1290,6 +1253,7 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// writeLive writes blockchain and corresponding receipt chain into active store.
|
||||
writeLive := func(blockChain types.Blocks, receiptChain []types.Receipts) (int, error) {
|
||||
skipPresenceCheck := false
|
||||
@@ -1327,7 +1291,7 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
|
||||
if err := batch.Write(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
size += batch.ValueSize()
|
||||
size += int64(batch.ValueSize())
|
||||
batch.Reset()
|
||||
}
|
||||
stats.processed++
|
||||
@@ -1336,7 +1300,7 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
|
||||
// we can ensure all components of body is completed(body, receipts,
|
||||
// tx indexes)
|
||||
if batch.ValueSize() > 0 {
|
||||
size += batch.ValueSize()
|
||||
size += int64(batch.ValueSize())
|
||||
if err := batch.Write(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -1344,6 +1308,7 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
|
||||
updateHead(blockChain[len(blockChain)-1])
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Write downloaded chain data and corresponding receipt chain data
|
||||
if len(ancientBlocks) > 0 {
|
||||
if n, err := writeAncient(ancientBlocks, ancientReceipts); err != nil {
|
||||
|
||||
+80
-41
@@ -670,6 +670,7 @@ func TestFastVsFullChains(t *testing.T) {
|
||||
if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(len(blocks)/2)); err != nil {
|
||||
t.Fatalf("failed to insert receipt %d: %v", n, err)
|
||||
}
|
||||
|
||||
// Iterate over all chain data components, and cross reference
|
||||
for i := 0; i < len(blocks); i++ {
|
||||
num, hash := blocks[i].NumberU64(), blocks[i].Hash()
|
||||
@@ -693,10 +694,27 @@ func TestFastVsFullChains(t *testing.T) {
|
||||
} else if types.CalcUncleHash(fblock.Uncles()) != types.CalcUncleHash(arblock.Uncles()) || types.CalcUncleHash(anblock.Uncles()) != types.CalcUncleHash(arblock.Uncles()) {
|
||||
t.Errorf("block #%d [%x]: uncles mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, fblock.Uncles(), anblock, arblock.Uncles())
|
||||
}
|
||||
if freceipts, anreceipts, areceipts := rawdb.ReadReceipts(fastDb, hash, *rawdb.ReadHeaderNumber(fastDb, hash), fast.Config()), rawdb.ReadReceipts(ancientDb, hash, *rawdb.ReadHeaderNumber(ancientDb, hash), fast.Config()), rawdb.ReadReceipts(archiveDb, hash, *rawdb.ReadHeaderNumber(archiveDb, hash), fast.Config()); types.DeriveSha(freceipts, trie.NewStackTrie(nil)) != types.DeriveSha(areceipts, trie.NewStackTrie(nil)) {
|
||||
|
||||
// Check receipts.
|
||||
freceipts := rawdb.ReadReceipts(fastDb, hash, num, fast.Config())
|
||||
anreceipts := rawdb.ReadReceipts(ancientDb, hash, num, fast.Config())
|
||||
areceipts := rawdb.ReadReceipts(archiveDb, hash, num, fast.Config())
|
||||
if types.DeriveSha(freceipts, trie.NewStackTrie(nil)) != types.DeriveSha(areceipts, trie.NewStackTrie(nil)) {
|
||||
t.Errorf("block #%d [%x]: receipts mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, freceipts, anreceipts, areceipts)
|
||||
}
|
||||
|
||||
// Check that hash-to-number mappings are present in all databases.
|
||||
if m := rawdb.ReadHeaderNumber(fastDb, hash); m == nil || *m != num {
|
||||
t.Errorf("block #%d [%x]: wrong hash-to-number mapping in fastdb: %v", num, hash, m)
|
||||
}
|
||||
if m := rawdb.ReadHeaderNumber(ancientDb, hash); m == nil || *m != num {
|
||||
t.Errorf("block #%d [%x]: wrong hash-to-number mapping in ancientdb: %v", num, hash, m)
|
||||
}
|
||||
if m := rawdb.ReadHeaderNumber(archiveDb, hash); m == nil || *m != num {
|
||||
t.Errorf("block #%d [%x]: wrong hash-to-number mapping in archivedb: %v", num, hash, m)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the canonical chains are the same between the databases
|
||||
for i := 0; i < len(blocks)+1; i++ {
|
||||
if fhash, ahash := rawdb.ReadCanonicalHash(fastDb, uint64(i)), rawdb.ReadCanonicalHash(archiveDb, uint64(i)); fhash != ahash {
|
||||
@@ -1639,20 +1657,34 @@ func TestBlockchainRecovery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncompleteAncientReceiptChainInsertion(t *testing.T) {
|
||||
// Configure and generate a sample block chain
|
||||
var (
|
||||
gendb = rawdb.NewMemoryDatabase()
|
||||
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
address = crypto.PubkeyToAddress(key.PublicKey)
|
||||
funds = big.NewInt(1000000000)
|
||||
gspec = &Genesis{Config: params.TestChainConfig, Alloc: GenesisAlloc{address: {Balance: funds}}}
|
||||
genesis = gspec.MustCommit(gendb)
|
||||
)
|
||||
height := uint64(1024)
|
||||
blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), nil)
|
||||
// This test checks that InsertReceiptChain will roll back correctly when attempting to insert a side chain.
|
||||
func TestInsertReceiptChainRollback(t *testing.T) {
|
||||
// Generate forked chain. The returned BlockChain object is used to process the side chain blocks.
|
||||
tmpChain, sideblocks, canonblocks, err := getLongAndShortChains()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tmpChain.Stop()
|
||||
// Get the side chain receipts.
|
||||
if _, err := tmpChain.InsertChain(sideblocks); err != nil {
|
||||
t.Fatal("processing side chain failed:", err)
|
||||
}
|
||||
t.Log("sidechain head:", tmpChain.CurrentBlock().Number(), tmpChain.CurrentBlock().Hash())
|
||||
sidechainReceipts := make([]types.Receipts, len(sideblocks))
|
||||
for i, block := range sideblocks {
|
||||
sidechainReceipts[i] = tmpChain.GetReceiptsByHash(block.Hash())
|
||||
}
|
||||
// Get the canon chain receipts.
|
||||
if _, err := tmpChain.InsertChain(canonblocks); err != nil {
|
||||
t.Fatal("processing canon chain failed:", err)
|
||||
}
|
||||
t.Log("canon head:", tmpChain.CurrentBlock().Number(), tmpChain.CurrentBlock().Hash())
|
||||
canonReceipts := make([]types.Receipts, len(canonblocks))
|
||||
for i, block := range canonblocks {
|
||||
canonReceipts[i] = tmpChain.GetReceiptsByHash(block.Hash())
|
||||
}
|
||||
|
||||
// Import the chain as a ancient-first node and ensure all pointers are updated
|
||||
// Set up a BlockChain that uses the ancient store.
|
||||
frdir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp freezer dir: %v", err)
|
||||
@@ -1662,38 +1694,43 @@ func TestIncompleteAncientReceiptChainInsertion(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp freezer db: %v", err)
|
||||
}
|
||||
gspec := Genesis{Config: params.AllEthashProtocolChanges}
|
||||
gspec.MustCommit(ancientDb)
|
||||
ancient, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil)
|
||||
defer ancient.Stop()
|
||||
ancientChain, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil)
|
||||
defer ancientChain.Stop()
|
||||
|
||||
headers := make([]*types.Header, len(blocks))
|
||||
for i, block := range blocks {
|
||||
headers[i] = block.Header()
|
||||
// Import the canonical header chain.
|
||||
canonHeaders := make([]*types.Header, len(canonblocks))
|
||||
for i, block := range canonblocks {
|
||||
canonHeaders[i] = block.Header()
|
||||
}
|
||||
if n, err := ancient.InsertHeaderChain(headers, 1); err != nil {
|
||||
t.Fatalf("failed to insert header %d: %v", n, err)
|
||||
if _, err = ancientChain.InsertHeaderChain(canonHeaders, 1); err != nil {
|
||||
t.Fatal("can't import canon headers:", err)
|
||||
}
|
||||
// Abort ancient receipt chain insertion deliberately
|
||||
ancient.terminateInsert = func(hash common.Hash, number uint64) bool {
|
||||
return number == blocks[len(blocks)/2].NumberU64()
|
||||
|
||||
// Try to insert blocks/receipts of the side chain.
|
||||
_, err = ancientChain.InsertReceiptChain(sideblocks, sidechainReceipts, uint64(len(sideblocks)))
|
||||
if err == nil {
|
||||
t.Fatal("expected error from InsertReceiptChain.")
|
||||
}
|
||||
previousFastBlock := ancient.CurrentFastBlock()
|
||||
if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(3*len(blocks)/4)); err == nil {
|
||||
t.Fatalf("failed to insert receipt %d: %v", n, err)
|
||||
if ancientChain.CurrentFastBlock().NumberU64() != 0 {
|
||||
t.Fatalf("failed to rollback ancient data, want %d, have %d", 0, ancientChain.CurrentFastBlock().NumberU64())
|
||||
}
|
||||
if ancient.CurrentFastBlock().NumberU64() != previousFastBlock.NumberU64() {
|
||||
t.Fatalf("failed to rollback ancient data, want %d, have %d", previousFastBlock.NumberU64(), ancient.CurrentFastBlock().NumberU64())
|
||||
if frozen, err := ancientChain.db.Ancients(); err != nil || frozen != 1 {
|
||||
t.Fatalf("failed to truncate ancient data, frozen index is %d", frozen)
|
||||
}
|
||||
if frozen, err := ancient.db.Ancients(); err != nil || frozen != 1 {
|
||||
t.Fatalf("failed to truncate ancient data")
|
||||
|
||||
// Insert blocks/receipts of the canonical chain.
|
||||
_, err = ancientChain.InsertReceiptChain(canonblocks, canonReceipts, uint64(len(canonblocks)))
|
||||
if err != nil {
|
||||
t.Fatalf("can't import canon chain receipts: %v", err)
|
||||
}
|
||||
ancient.terminateInsert = nil
|
||||
if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(3*len(blocks)/4)); err != nil {
|
||||
t.Fatalf("failed to insert receipt %d: %v", n, err)
|
||||
}
|
||||
if ancient.CurrentFastBlock().NumberU64() != blocks[len(blocks)-1].NumberU64() {
|
||||
if ancientChain.CurrentFastBlock().NumberU64() != canonblocks[len(canonblocks)-1].NumberU64() {
|
||||
t.Fatalf("failed to insert ancient recept chain after rollback")
|
||||
}
|
||||
if frozen, _ := ancientChain.db.Ancients(); frozen != uint64(len(canonblocks))+1 {
|
||||
t.Fatalf("wrong ancients count %d", frozen)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that importing a very large side fork, which is larger than the canon chain,
|
||||
@@ -1958,9 +1995,8 @@ func testInsertKnownChainData(t *testing.T, typ string) {
|
||||
asserter(t, blocks2[len(blocks2)-1])
|
||||
}
|
||||
|
||||
// getLongAndShortChains returns two chains,
|
||||
// A is longer, B is heavier
|
||||
func getLongAndShortChains() (*BlockChain, []*types.Block, []*types.Block, error) {
|
||||
// getLongAndShortChains returns two chains: A is longer, B is heavier.
|
||||
func getLongAndShortChains() (bc *BlockChain, longChain []*types.Block, heavyChain []*types.Block, err error) {
|
||||
// Generate a canonical chain to act as the main dataset
|
||||
engine := ethash.NewFaker()
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
@@ -1968,7 +2004,7 @@ func getLongAndShortChains() (*BlockChain, []*types.Block, []*types.Block, error
|
||||
|
||||
// Generate and import the canonical chain,
|
||||
// Offset the time, to keep the difficulty low
|
||||
longChain, _ := GenerateChain(params.TestChainConfig, genesis, engine, db, 80, func(i int, b *BlockGen) {
|
||||
longChain, _ = GenerateChain(params.TestChainConfig, genesis, engine, db, 80, func(i int, b *BlockGen) {
|
||||
b.SetCoinbase(common.Address{1})
|
||||
})
|
||||
diskdb := rawdb.NewMemoryDatabase()
|
||||
@@ -1982,10 +2018,13 @@ func getLongAndShortChains() (*BlockChain, []*types.Block, []*types.Block, error
|
||||
// Generate fork chain, make it shorter than canon, with common ancestor pretty early
|
||||
parentIndex := 3
|
||||
parent := longChain[parentIndex]
|
||||
heavyChain, _ := GenerateChain(params.TestChainConfig, parent, engine, db, 75, func(i int, b *BlockGen) {
|
||||
heavyChainExt, _ := GenerateChain(params.TestChainConfig, parent, engine, db, 75, func(i int, b *BlockGen) {
|
||||
b.SetCoinbase(common.Address{2})
|
||||
b.OffsetTime(-9)
|
||||
})
|
||||
heavyChain = append(heavyChain, longChain[:parentIndex+1]...)
|
||||
heavyChain = append(heavyChain, heavyChainExt...)
|
||||
|
||||
// Verify that the test is sane
|
||||
var (
|
||||
longerTd = new(big.Int)
|
||||
|
||||
@@ -510,8 +510,9 @@ type MatcherSession struct {
|
||||
closer sync.Once // Sync object to ensure we only ever close once
|
||||
quit chan struct{} // Quit channel to request pipeline termination
|
||||
|
||||
ctx context.Context // Context used by the light client to abort filtering
|
||||
err atomic.Value // Global error to track retrieval failures deep in the chain
|
||||
ctx context.Context // Context used by the light client to abort filtering
|
||||
err error // Global error to track retrieval failures deep in the chain
|
||||
errLock sync.Mutex
|
||||
|
||||
pend sync.WaitGroup
|
||||
}
|
||||
@@ -529,10 +530,10 @@ func (s *MatcherSession) Close() {
|
||||
|
||||
// Error returns any failure encountered during the matching session.
|
||||
func (s *MatcherSession) Error() error {
|
||||
if err := s.err.Load(); err != nil {
|
||||
return err.(error)
|
||||
}
|
||||
return nil
|
||||
s.errLock.Lock()
|
||||
defer s.errLock.Unlock()
|
||||
|
||||
return s.err
|
||||
}
|
||||
|
||||
// allocateRetrieval assigns a bloom bit index to a client process that can either
|
||||
@@ -630,7 +631,9 @@ func (s *MatcherSession) Multiplex(batch int, wait time.Duration, mux chan chan
|
||||
|
||||
result := <-request
|
||||
if result.Error != nil {
|
||||
s.err.Store(result.Error)
|
||||
s.errLock.Lock()
|
||||
s.err = result.Error
|
||||
s.errLock.Unlock()
|
||||
s.Close()
|
||||
}
|
||||
s.deliverSections(result.Bit, result.Sections, result.Bitsets)
|
||||
|
||||
@@ -31,6 +31,8 @@ var (
|
||||
|
||||
// ErrNoGenesis is returned when there is no Genesis Block.
|
||||
ErrNoGenesis = errors.New("genesis not found in chain")
|
||||
|
||||
errSideChainReceipts = errors.New("side blocks can't be accepted as ancient chain data")
|
||||
)
|
||||
|
||||
// List of evm-call-message pre-checking errors. All state transition messages will
|
||||
|
||||
+4
-1
@@ -310,7 +310,7 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block {
|
||||
func (g *Genesis) Commit(db ethdb.Database) (*types.Block, error) {
|
||||
block := g.ToBlock(db)
|
||||
if block.Number().Sign() != 0 {
|
||||
return nil, fmt.Errorf("can't commit genesis block with number > 0")
|
||||
return nil, errors.New("can't commit genesis block with number > 0")
|
||||
}
|
||||
config := g.Config
|
||||
if config == nil {
|
||||
@@ -319,6 +319,9 @@ func (g *Genesis) Commit(db ethdb.Database) (*types.Block, error) {
|
||||
if err := config.CheckConfigForkOrder(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.Clique != nil && len(block.Extra()) == 0 {
|
||||
return nil, errors.New("can't start clique chain without signers")
|
||||
}
|
||||
rawdb.WriteTd(db, block.Hash(), block.NumberU64(), g.Difficulty)
|
||||
rawdb.WriteBlock(db, block)
|
||||
rawdb.WriteReceipts(db, block.Hash(), block.NumberU64(), nil)
|
||||
|
||||
@@ -39,6 +39,22 @@ func TestDefaultGenesisBlock(t *testing.T) {
|
||||
if block.Hash() != params.RopstenGenesisHash {
|
||||
t.Errorf("wrong ropsten genesis hash, got %v, want %v", block.Hash(), params.RopstenGenesisHash)
|
||||
}
|
||||
block = DefaultRinkebyGenesisBlock().ToBlock(nil)
|
||||
if block.Hash() != params.RinkebyGenesisHash {
|
||||
t.Errorf("wrong rinkeby genesis hash, got %v, want %v", block.Hash(), params.RinkebyGenesisHash)
|
||||
}
|
||||
block = DefaultGoerliGenesisBlock().ToBlock(nil)
|
||||
if block.Hash() != params.GoerliGenesisHash {
|
||||
t.Errorf("wrong goerli genesis hash, got %v, want %v", block.Hash(), params.GoerliGenesisHash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidCliqueConfig(t *testing.T) {
|
||||
block := DefaultGoerliGenesisBlock()
|
||||
block.ExtraData = []byte{}
|
||||
if _, err := block.Commit(nil); err == nil {
|
||||
t.Fatal("Expected error on invalid clique config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupGenesis(t *testing.T) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build none
|
||||
// +build none
|
||||
|
||||
/*
|
||||
|
||||
+150
-23
@@ -19,6 +19,8 @@ package rawdb
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
|
||||
@@ -81,6 +83,37 @@ func ReadAllHashes(db ethdb.Iteratee, number uint64) []common.Hash {
|
||||
return hashes
|
||||
}
|
||||
|
||||
type NumberHash struct {
|
||||
Number uint64
|
||||
Hash common.Hash
|
||||
}
|
||||
|
||||
// ReadAllHashes retrieves all the hashes assigned to blocks at a certain heights,
|
||||
// both canonical and reorged forks included.
|
||||
// This method considers both limits to be _inclusive_.
|
||||
func ReadAllHashesInRange(db ethdb.Iteratee, first, last uint64) []*NumberHash {
|
||||
var (
|
||||
start = encodeBlockNumber(first)
|
||||
keyLength = len(headerPrefix) + 8 + 32
|
||||
hashes = make([]*NumberHash, 0, 1+last-first)
|
||||
it = db.NewIterator(headerPrefix, start)
|
||||
)
|
||||
defer it.Release()
|
||||
for it.Next() {
|
||||
key := it.Key()
|
||||
if len(key) != keyLength {
|
||||
continue
|
||||
}
|
||||
num := binary.BigEndian.Uint64(key[len(headerPrefix) : len(headerPrefix)+8])
|
||||
if num > last {
|
||||
break
|
||||
}
|
||||
hash := common.BytesToHash(key[len(key)-32:])
|
||||
hashes = append(hashes, &NumberHash{num, hash})
|
||||
}
|
||||
return hashes
|
||||
}
|
||||
|
||||
// ReadAllCanonicalHashes retrieves all canonical number and hash mappings at the
|
||||
// certain chain range. If the accumulated entries reaches the given threshold,
|
||||
// abort the iteration and return the semi-finish result.
|
||||
@@ -631,6 +664,86 @@ func DeleteReceipts(db ethdb.KeyValueWriter, hash common.Hash, number uint64) {
|
||||
}
|
||||
}
|
||||
|
||||
// storedReceiptRLP is the storage encoding of a receipt.
|
||||
// Re-definition in core/types/receipt.go.
|
||||
type storedReceiptRLP struct {
|
||||
PostStateOrStatus []byte
|
||||
CumulativeGasUsed uint64
|
||||
Logs []*types.LogForStorage
|
||||
}
|
||||
|
||||
// ReceiptLogs is a barebone version of ReceiptForStorage which only keeps
|
||||
// the list of logs. When decoding a stored receipt into this object we
|
||||
// avoid creating the bloom filter.
|
||||
type receiptLogs struct {
|
||||
Logs []*types.Log
|
||||
}
|
||||
|
||||
// DecodeRLP implements rlp.Decoder.
|
||||
func (r *receiptLogs) DecodeRLP(s *rlp.Stream) error {
|
||||
var stored storedReceiptRLP
|
||||
if err := s.Decode(&stored); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Logs = make([]*types.Log, len(stored.Logs))
|
||||
for i, log := range stored.Logs {
|
||||
r.Logs[i] = (*types.Log)(log)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeriveLogFields fills the logs in receiptLogs with information such as block number, txhash, etc.
|
||||
func deriveLogFields(receipts []*receiptLogs, hash common.Hash, number uint64, txs types.Transactions) error {
|
||||
logIndex := uint(0)
|
||||
if len(txs) != len(receipts) {
|
||||
return errors.New("transaction and receipt count mismatch")
|
||||
}
|
||||
for i := 0; i < len(receipts); i++ {
|
||||
txHash := txs[i].Hash()
|
||||
// The derived log fields can simply be set from the block and transaction
|
||||
for j := 0; j < len(receipts[i].Logs); j++ {
|
||||
receipts[i].Logs[j].BlockNumber = number
|
||||
receipts[i].Logs[j].BlockHash = hash
|
||||
receipts[i].Logs[j].TxHash = txHash
|
||||
receipts[i].Logs[j].TxIndex = uint(i)
|
||||
receipts[i].Logs[j].Index = logIndex
|
||||
logIndex++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadLogs retrieves the logs for all transactions in a block. The log fields
|
||||
// are populated with metadata. In case the receipts or the block body
|
||||
// are not found, a nil is returned.
|
||||
func ReadLogs(db ethdb.Reader, hash common.Hash, number uint64) [][]*types.Log {
|
||||
// Retrieve the flattened receipt slice
|
||||
data := ReadReceiptsRLP(db, hash, number)
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
receipts := []*receiptLogs{}
|
||||
if err := rlp.DecodeBytes(data, &receipts); err != nil {
|
||||
log.Error("Invalid receipt array RLP", "hash", hash, "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
body := ReadBody(db, hash, number)
|
||||
if body == nil {
|
||||
log.Error("Missing body but have receipt", "hash", hash, "number", number)
|
||||
return nil
|
||||
}
|
||||
if err := deriveLogFields(receipts, hash, number, body.Transactions); err != nil {
|
||||
log.Error("Failed to derive block receipts fields", "hash", hash, "number", number, "err", err)
|
||||
return nil
|
||||
}
|
||||
logs := make([][]*types.Log, len(receipts))
|
||||
for i, receipt := range receipts {
|
||||
logs[i] = receipt.Logs
|
||||
}
|
||||
return logs
|
||||
}
|
||||
|
||||
// ReadBlock retrieves an entire block corresponding to the hash, assembling it
|
||||
// back from the stored header and body. If either the header or body could not
|
||||
// be retrieved nil is returned.
|
||||
@@ -656,34 +769,48 @@ func WriteBlock(db ethdb.KeyValueWriter, block *types.Block) {
|
||||
}
|
||||
|
||||
// WriteAncientBlock writes entire block data into ancient store and returns the total written size.
|
||||
func WriteAncientBlock(db ethdb.AncientWriter, block *types.Block, receipts types.Receipts, td *big.Int) int {
|
||||
// Encode all block components to RLP format.
|
||||
headerBlob, err := rlp.EncodeToBytes(block.Header())
|
||||
if err != nil {
|
||||
log.Crit("Failed to RLP encode block header", "err", err)
|
||||
func WriteAncientBlocks(db ethdb.AncientWriter, blocks []*types.Block, receipts []types.Receipts, td *big.Int) (int64, error) {
|
||||
var (
|
||||
tdSum = new(big.Int).Set(td)
|
||||
stReceipts []*types.ReceiptForStorage
|
||||
)
|
||||
return db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
||||
for i, block := range blocks {
|
||||
// Convert receipts to storage format and sum up total difficulty.
|
||||
stReceipts = stReceipts[:0]
|
||||
for _, receipt := range receipts[i] {
|
||||
stReceipts = append(stReceipts, (*types.ReceiptForStorage)(receipt))
|
||||
}
|
||||
header := block.Header()
|
||||
if i > 0 {
|
||||
tdSum.Add(tdSum, header.Difficulty)
|
||||
}
|
||||
if err := writeAncientBlock(op, block, header, stReceipts, tdSum); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func writeAncientBlock(op ethdb.AncientWriteOp, block *types.Block, header *types.Header, receipts []*types.ReceiptForStorage, td *big.Int) error {
|
||||
num := block.NumberU64()
|
||||
if err := op.AppendRaw(freezerHashTable, num, block.Hash().Bytes()); err != nil {
|
||||
return fmt.Errorf("can't add block %d hash: %v", num, err)
|
||||
}
|
||||
bodyBlob, err := rlp.EncodeToBytes(block.Body())
|
||||
if err != nil {
|
||||
log.Crit("Failed to RLP encode body", "err", err)
|
||||
if err := op.Append(freezerHeaderTable, num, header); err != nil {
|
||||
return fmt.Errorf("can't append block header %d: %v", num, err)
|
||||
}
|
||||
storageReceipts := make([]*types.ReceiptForStorage, len(receipts))
|
||||
for i, receipt := range receipts {
|
||||
storageReceipts[i] = (*types.ReceiptForStorage)(receipt)
|
||||
if err := op.Append(freezerBodiesTable, num, block.Body()); err != nil {
|
||||
return fmt.Errorf("can't append block body %d: %v", num, err)
|
||||
}
|
||||
receiptBlob, err := rlp.EncodeToBytes(storageReceipts)
|
||||
if err != nil {
|
||||
log.Crit("Failed to RLP encode block receipts", "err", err)
|
||||
if err := op.Append(freezerReceiptTable, num, receipts); err != nil {
|
||||
return fmt.Errorf("can't append block %d receipts: %v", num, err)
|
||||
}
|
||||
tdBlob, err := rlp.EncodeToBytes(td)
|
||||
if err != nil {
|
||||
log.Crit("Failed to RLP encode block total difficulty", "err", err)
|
||||
if err := op.Append(freezerDifficultyTable, num, td); err != nil {
|
||||
return fmt.Errorf("can't append block %d total difficulty: %v", num, err)
|
||||
}
|
||||
// Write all blob to flatten files.
|
||||
err = db.AppendAncient(block.NumberU64(), block.Hash().Bytes(), headerBlob, bodyBlob, receiptBlob, tdBlob)
|
||||
if err != nil {
|
||||
log.Crit("Failed to write block data to ancient store", "err", err)
|
||||
}
|
||||
return len(headerBlob) + len(bodyBlob) + len(receiptBlob) + len(tdBlob) + common.HashLength
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteBlock removes all block data associated with a hash.
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"golang.org/x/crypto/sha3"
|
||||
@@ -438,7 +439,7 @@ func TestAncientStorage(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp freezer dir: %v", err)
|
||||
}
|
||||
defer os.Remove(frdir)
|
||||
defer os.RemoveAll(frdir)
|
||||
|
||||
db, err := NewDatabaseWithFreezer(NewMemoryDatabase(), frdir, "", false)
|
||||
if err != nil {
|
||||
@@ -467,8 +468,10 @@ func TestAncientStorage(t *testing.T) {
|
||||
if blob := ReadTdRLP(db, hash, number); len(blob) > 0 {
|
||||
t.Fatalf("non existent td returned")
|
||||
}
|
||||
|
||||
// Write and verify the header in the database
|
||||
WriteAncientBlock(db, block, nil, big.NewInt(100))
|
||||
WriteAncientBlocks(db, []*types.Block{block}, []types.Receipts{nil}, big.NewInt(100))
|
||||
|
||||
if blob := ReadHeaderRLP(db, hash, number); len(blob) == 0 {
|
||||
t.Fatalf("no header returned")
|
||||
}
|
||||
@@ -481,6 +484,7 @@ func TestAncientStorage(t *testing.T) {
|
||||
if blob := ReadTdRLP(db, hash, number); len(blob) == 0 {
|
||||
t.Fatalf("no td returned")
|
||||
}
|
||||
|
||||
// Use a fake hash for data retrieval, nothing should be returned.
|
||||
fakeHash := common.BytesToHash([]byte{0x01, 0x02, 0x03})
|
||||
if blob := ReadHeaderRLP(db, fakeHash, number); len(blob) != 0 {
|
||||
@@ -528,3 +532,354 @@ func TestCanonicalHashIteration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashesInRange(t *testing.T) {
|
||||
mkHeader := func(number, seq int) *types.Header {
|
||||
h := types.Header{
|
||||
Difficulty: new(big.Int),
|
||||
Number: big.NewInt(int64(number)),
|
||||
GasLimit: uint64(seq),
|
||||
}
|
||||
return &h
|
||||
}
|
||||
db := NewMemoryDatabase()
|
||||
// For each number, write N versions of that particular number
|
||||
total := 0
|
||||
for i := 0; i < 15; i++ {
|
||||
for ii := 0; ii < i; ii++ {
|
||||
WriteHeader(db, mkHeader(i, ii))
|
||||
total++
|
||||
}
|
||||
}
|
||||
if have, want := len(ReadAllHashesInRange(db, 10, 10)), 10; have != want {
|
||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
||||
}
|
||||
if have, want := len(ReadAllHashesInRange(db, 10, 9)), 0; have != want {
|
||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
||||
}
|
||||
if have, want := len(ReadAllHashesInRange(db, 0, 100)), total; have != want {
|
||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
||||
}
|
||||
if have, want := len(ReadAllHashesInRange(db, 9, 10)), 9+10; have != want {
|
||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
||||
}
|
||||
if have, want := len(ReadAllHashes(db, 10)), 10; have != want {
|
||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
||||
}
|
||||
if have, want := len(ReadAllHashes(db, 16)), 0; have != want {
|
||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
||||
}
|
||||
if have, want := len(ReadAllHashes(db, 1)), 1; have != want {
|
||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
||||
}
|
||||
}
|
||||
|
||||
// This measures the write speed of the WriteAncientBlocks operation.
|
||||
func BenchmarkWriteAncientBlocks(b *testing.B) {
|
||||
// Open freezer database.
|
||||
frdir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp freezer dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(frdir)
|
||||
db, err := NewDatabaseWithFreezer(NewMemoryDatabase(), frdir, "", false)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create database with ancient backend")
|
||||
}
|
||||
|
||||
// Create the data to insert. The blocks must have consecutive numbers, so we create
|
||||
// all of them ahead of time. However, there is no need to create receipts
|
||||
// individually for each block, just make one batch here and reuse it for all writes.
|
||||
const batchSize = 128
|
||||
const blockTxs = 20
|
||||
allBlocks := makeTestBlocks(b.N, blockTxs)
|
||||
batchReceipts := makeTestReceipts(batchSize, blockTxs)
|
||||
b.ResetTimer()
|
||||
|
||||
// The benchmark loop writes batches of blocks, but note that the total block count is
|
||||
// b.N. This means the resulting ns/op measurement is the time it takes to write a
|
||||
// single block and its associated data.
|
||||
var td = big.NewInt(55)
|
||||
var totalSize int64
|
||||
for i := 0; i < b.N; i += batchSize {
|
||||
length := batchSize
|
||||
if i+batchSize > b.N {
|
||||
length = b.N - i
|
||||
}
|
||||
|
||||
blocks := allBlocks[i : i+length]
|
||||
receipts := batchReceipts[:length]
|
||||
writeSize, err := WriteAncientBlocks(db, blocks, receipts, td)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
totalSize += writeSize
|
||||
}
|
||||
|
||||
// Enable MB/s reporting.
|
||||
b.SetBytes(totalSize / int64(b.N))
|
||||
}
|
||||
|
||||
// makeTestBlocks creates fake blocks for the ancient write benchmark.
|
||||
func makeTestBlocks(nblock int, txsPerBlock int) []*types.Block {
|
||||
key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
signer := types.LatestSignerForChainID(big.NewInt(8))
|
||||
|
||||
// Create transactions.
|
||||
txs := make([]*types.Transaction, txsPerBlock)
|
||||
for i := 0; i < len(txs); i++ {
|
||||
var err error
|
||||
to := common.Address{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
|
||||
txs[i], err = types.SignNewTx(key, signer, &types.LegacyTx{
|
||||
Nonce: 2,
|
||||
GasPrice: big.NewInt(30000),
|
||||
Gas: 0x45454545,
|
||||
To: &to,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the blocks.
|
||||
blocks := make([]*types.Block, nblock)
|
||||
for i := 0; i < nblock; i++ {
|
||||
header := &types.Header{
|
||||
Number: big.NewInt(int64(i)),
|
||||
Extra: []byte("test block"),
|
||||
}
|
||||
blocks[i] = types.NewBlockWithHeader(header).WithBody(txs, nil)
|
||||
blocks[i].Hash() // pre-cache the block hash
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
// makeTestReceipts creates fake receipts for the ancient write benchmark.
|
||||
func makeTestReceipts(n int, nPerBlock int) []types.Receipts {
|
||||
receipts := make([]*types.Receipt, nPerBlock)
|
||||
for i := 0; i < len(receipts); i++ {
|
||||
receipts[i] = &types.Receipt{
|
||||
Status: types.ReceiptStatusSuccessful,
|
||||
CumulativeGasUsed: 0x888888888,
|
||||
Logs: make([]*types.Log, 5),
|
||||
}
|
||||
}
|
||||
allReceipts := make([]types.Receipts, n)
|
||||
for i := 0; i < n; i++ {
|
||||
allReceipts[i] = receipts
|
||||
}
|
||||
return allReceipts
|
||||
}
|
||||
|
||||
type fullLogRLP struct {
|
||||
Address common.Address
|
||||
Topics []common.Hash
|
||||
Data []byte
|
||||
BlockNumber uint64
|
||||
TxHash common.Hash
|
||||
TxIndex uint
|
||||
BlockHash common.Hash
|
||||
Index uint
|
||||
}
|
||||
|
||||
func newFullLogRLP(l *types.Log) *fullLogRLP {
|
||||
return &fullLogRLP{
|
||||
Address: l.Address,
|
||||
Topics: l.Topics,
|
||||
Data: l.Data,
|
||||
BlockNumber: l.BlockNumber,
|
||||
TxHash: l.TxHash,
|
||||
TxIndex: l.TxIndex,
|
||||
BlockHash: l.BlockHash,
|
||||
Index: l.Index,
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that logs associated with a single block can be retrieved.
|
||||
func TestReadLogs(t *testing.T) {
|
||||
db := NewMemoryDatabase()
|
||||
|
||||
// Create a live block since we need metadata to reconstruct the receipt
|
||||
tx1 := types.NewTransaction(1, common.HexToAddress("0x1"), big.NewInt(1), 1, big.NewInt(1), nil)
|
||||
tx2 := types.NewTransaction(2, common.HexToAddress("0x2"), big.NewInt(2), 2, big.NewInt(2), nil)
|
||||
|
||||
body := &types.Body{Transactions: types.Transactions{tx1, tx2}}
|
||||
|
||||
// Create the two receipts to manage afterwards
|
||||
receipt1 := &types.Receipt{
|
||||
Status: types.ReceiptStatusFailed,
|
||||
CumulativeGasUsed: 1,
|
||||
Logs: []*types.Log{
|
||||
{Address: common.BytesToAddress([]byte{0x11})},
|
||||
{Address: common.BytesToAddress([]byte{0x01, 0x11})},
|
||||
},
|
||||
TxHash: tx1.Hash(),
|
||||
ContractAddress: common.BytesToAddress([]byte{0x01, 0x11, 0x11}),
|
||||
GasUsed: 111111,
|
||||
}
|
||||
receipt1.Bloom = types.CreateBloom(types.Receipts{receipt1})
|
||||
|
||||
receipt2 := &types.Receipt{
|
||||
PostState: common.Hash{2}.Bytes(),
|
||||
CumulativeGasUsed: 2,
|
||||
Logs: []*types.Log{
|
||||
{Address: common.BytesToAddress([]byte{0x22})},
|
||||
{Address: common.BytesToAddress([]byte{0x02, 0x22})},
|
||||
},
|
||||
TxHash: tx2.Hash(),
|
||||
ContractAddress: common.BytesToAddress([]byte{0x02, 0x22, 0x22}),
|
||||
GasUsed: 222222,
|
||||
}
|
||||
receipt2.Bloom = types.CreateBloom(types.Receipts{receipt2})
|
||||
receipts := []*types.Receipt{receipt1, receipt2}
|
||||
|
||||
hash := common.BytesToHash([]byte{0x03, 0x14})
|
||||
// Check that no receipt entries are in a pristine database
|
||||
if rs := ReadReceipts(db, hash, 0, params.TestChainConfig); len(rs) != 0 {
|
||||
t.Fatalf("non existent receipts returned: %v", rs)
|
||||
}
|
||||
// Insert the body that corresponds to the receipts
|
||||
WriteBody(db, hash, 0, body)
|
||||
|
||||
// Insert the receipt slice into the database and check presence
|
||||
WriteReceipts(db, hash, 0, receipts)
|
||||
|
||||
logs := ReadLogs(db, hash, 0)
|
||||
if len(logs) == 0 {
|
||||
t.Fatalf("no logs returned")
|
||||
}
|
||||
if have, want := len(logs), 2; have != want {
|
||||
t.Fatalf("unexpected number of logs returned, have %d want %d", have, want)
|
||||
}
|
||||
if have, want := len(logs[0]), 2; have != want {
|
||||
t.Fatalf("unexpected number of logs[0] returned, have %d want %d", have, want)
|
||||
}
|
||||
if have, want := len(logs[1]), 2; have != want {
|
||||
t.Fatalf("unexpected number of logs[1] returned, have %d want %d", have, want)
|
||||
}
|
||||
|
||||
// Fill in log fields so we can compare their rlp encoding
|
||||
if err := types.Receipts(receipts).DeriveFields(params.TestChainConfig, hash, 0, body.Transactions); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, pr := range receipts {
|
||||
for j, pl := range pr.Logs {
|
||||
rlpHave, err := rlp.EncodeToBytes(newFullLogRLP(logs[i][j]))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rlpWant, err := rlp.EncodeToBytes(newFullLogRLP(pl))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(rlpHave, rlpWant) {
|
||||
t.Fatalf("receipt #%d: receipt mismatch: have %s, want %s", i, hex.EncodeToString(rlpHave), hex.EncodeToString(rlpWant))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveLogFields(t *testing.T) {
|
||||
// Create a few transactions to have receipts for
|
||||
to2 := common.HexToAddress("0x2")
|
||||
to3 := common.HexToAddress("0x3")
|
||||
txs := types.Transactions{
|
||||
types.NewTx(&types.LegacyTx{
|
||||
Nonce: 1,
|
||||
Value: big.NewInt(1),
|
||||
Gas: 1,
|
||||
GasPrice: big.NewInt(1),
|
||||
}),
|
||||
types.NewTx(&types.LegacyTx{
|
||||
To: &to2,
|
||||
Nonce: 2,
|
||||
Value: big.NewInt(2),
|
||||
Gas: 2,
|
||||
GasPrice: big.NewInt(2),
|
||||
}),
|
||||
types.NewTx(&types.AccessListTx{
|
||||
To: &to3,
|
||||
Nonce: 3,
|
||||
Value: big.NewInt(3),
|
||||
Gas: 3,
|
||||
GasPrice: big.NewInt(3),
|
||||
}),
|
||||
}
|
||||
// Create the corresponding receipts
|
||||
receipts := []*receiptLogs{
|
||||
{
|
||||
Logs: []*types.Log{
|
||||
{Address: common.BytesToAddress([]byte{0x11})},
|
||||
{Address: common.BytesToAddress([]byte{0x01, 0x11})},
|
||||
},
|
||||
},
|
||||
{
|
||||
Logs: []*types.Log{
|
||||
{Address: common.BytesToAddress([]byte{0x22})},
|
||||
{Address: common.BytesToAddress([]byte{0x02, 0x22})},
|
||||
},
|
||||
},
|
||||
{
|
||||
Logs: []*types.Log{
|
||||
{Address: common.BytesToAddress([]byte{0x33})},
|
||||
{Address: common.BytesToAddress([]byte{0x03, 0x33})},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Derive log metadata fields
|
||||
number := big.NewInt(1)
|
||||
hash := common.BytesToHash([]byte{0x03, 0x14})
|
||||
if err := deriveLogFields(receipts, hash, number.Uint64(), txs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Iterate over all the computed fields and check that they're correct
|
||||
logIndex := uint(0)
|
||||
for i := range receipts {
|
||||
for j := range receipts[i].Logs {
|
||||
if receipts[i].Logs[j].BlockNumber != number.Uint64() {
|
||||
t.Errorf("receipts[%d].Logs[%d].BlockNumber = %d, want %d", i, j, receipts[i].Logs[j].BlockNumber, number.Uint64())
|
||||
}
|
||||
if receipts[i].Logs[j].BlockHash != hash {
|
||||
t.Errorf("receipts[%d].Logs[%d].BlockHash = %s, want %s", i, j, receipts[i].Logs[j].BlockHash.String(), hash.String())
|
||||
}
|
||||
if receipts[i].Logs[j].TxHash != txs[i].Hash() {
|
||||
t.Errorf("receipts[%d].Logs[%d].TxHash = %s, want %s", i, j, receipts[i].Logs[j].TxHash.String(), txs[i].Hash().String())
|
||||
}
|
||||
if receipts[i].Logs[j].TxIndex != uint(i) {
|
||||
t.Errorf("receipts[%d].Logs[%d].TransactionIndex = %d, want %d", i, j, receipts[i].Logs[j].TxIndex, i)
|
||||
}
|
||||
if receipts[i].Logs[j].Index != logIndex {
|
||||
t.Errorf("receipts[%d].Logs[%d].Index = %d, want %d", i, j, receipts[i].Logs[j].Index, logIndex)
|
||||
}
|
||||
logIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecodeRLPLogs(b *testing.B) {
|
||||
// Encoded receipts from block 0x14ee094309fbe8f70b65f45ebcc08fb33f126942d97464aad5eb91cfd1e2d269
|
||||
buf, err := ioutil.ReadFile("testdata/stored_receipts.bin")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Run("ReceiptForStorage", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var r []*types.ReceiptForStorage
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := rlp.DecodeBytes(buf, &r); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
b.Run("rlpLogs", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var r []*receiptLogs
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := rlp.DecodeBytes(buf, &r); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,9 +104,9 @@ func (db *nofreezedb) AncientSize(kind string) (uint64, error) {
|
||||
return 0, errNotSupported
|
||||
}
|
||||
|
||||
// AppendAncient returns an error as we don't have a backing chain freezer.
|
||||
func (db *nofreezedb) AppendAncient(number uint64, hash, header, body, receipts, td []byte) error {
|
||||
return errNotSupported
|
||||
// ModifyAncients is not supported.
|
||||
func (db *nofreezedb) ModifyAncients(func(ethdb.AncientWriteOp) error) (int64, error) {
|
||||
return 0, errNotSupported
|
||||
}
|
||||
|
||||
// TruncateAncients returns an error as we don't have a backing chain freezer.
|
||||
@@ -122,9 +122,7 @@ func (db *nofreezedb) Sync() error {
|
||||
// NewDatabase creates a high level database on top of a given key-value data
|
||||
// store without a freezer moving immutable chain segments into cold storage.
|
||||
func NewDatabase(db ethdb.KeyValueStore) ethdb.Database {
|
||||
return &nofreezedb{
|
||||
KeyValueStore: db,
|
||||
}
|
||||
return &nofreezedb{KeyValueStore: db}
|
||||
}
|
||||
|
||||
// NewDatabaseWithFreezer creates a high level database on top of a given key-
|
||||
@@ -132,7 +130,7 @@ func NewDatabase(db ethdb.KeyValueStore) ethdb.Database {
|
||||
// storage.
|
||||
func NewDatabaseWithFreezer(db ethdb.KeyValueStore, freezer string, namespace string, readonly bool) (ethdb.Database, error) {
|
||||
// Create the idle freezer instance
|
||||
frdb, err := newFreezer(freezer, namespace, readonly)
|
||||
frdb, err := newFreezer(freezer, namespace, readonly, freezerTableSize, FreezerNoSnappy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+136
-97
@@ -61,6 +61,9 @@ const (
|
||||
// freezerBatchLimit is the maximum number of blocks to freeze in one batch
|
||||
// before doing an fsync and deleting it from the key-value store.
|
||||
freezerBatchLimit = 30000
|
||||
|
||||
// freezerTableSize defines the maximum size of freezer data files.
|
||||
freezerTableSize = 2 * 1000 * 1000 * 1000
|
||||
)
|
||||
|
||||
// freezer is an memory mapped append-only database to store immutable chain data
|
||||
@@ -77,6 +80,10 @@ type freezer struct {
|
||||
frozen uint64 // Number of blocks already frozen
|
||||
threshold uint64 // Number of recent blocks not to freeze (params.FullImmutabilityThreshold apart from tests)
|
||||
|
||||
// This lock synchronizes writers and the truncate operation.
|
||||
writeLock sync.Mutex
|
||||
writeBatch *freezerBatch
|
||||
|
||||
readonly bool
|
||||
tables map[string]*freezerTable // Data tables for storing everything
|
||||
instanceLock fileutil.Releaser // File-system lock to prevent double opens
|
||||
@@ -90,7 +97,10 @@ type freezer struct {
|
||||
|
||||
// newFreezer creates a chain freezer that moves ancient chain data into
|
||||
// append-only flat file containers.
|
||||
func newFreezer(datadir string, namespace string, readonly bool) (*freezer, error) {
|
||||
//
|
||||
// The 'tables' argument defines the data tables. If the value of a map
|
||||
// entry is true, snappy compression is disabled for the table.
|
||||
func newFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]bool) (*freezer, error) {
|
||||
// Create the initial freezer object
|
||||
var (
|
||||
readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil)
|
||||
@@ -119,8 +129,10 @@ func newFreezer(datadir string, namespace string, readonly bool) (*freezer, erro
|
||||
trigger: make(chan chan struct{}),
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
for name, disableSnappy := range FreezerNoSnappy {
|
||||
table, err := newTable(datadir, name, readMeter, writeMeter, sizeGauge, disableSnappy)
|
||||
|
||||
// Create the tables.
|
||||
for name, disableSnappy := range tables {
|
||||
table, err := newTable(datadir, name, readMeter, writeMeter, sizeGauge, maxTableSize, disableSnappy)
|
||||
if err != nil {
|
||||
for _, table := range freezer.tables {
|
||||
table.Close()
|
||||
@@ -130,6 +142,8 @@ func newFreezer(datadir string, namespace string, readonly bool) (*freezer, erro
|
||||
}
|
||||
freezer.tables[name] = table
|
||||
}
|
||||
|
||||
// Truncate all tables to common length.
|
||||
if err := freezer.repair(); err != nil {
|
||||
for _, table := range freezer.tables {
|
||||
table.Close()
|
||||
@@ -137,12 +151,19 @@ func newFreezer(datadir string, namespace string, readonly bool) (*freezer, erro
|
||||
lock.Release()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the write batch.
|
||||
freezer.writeBatch = newFreezerBatch(freezer)
|
||||
|
||||
log.Info("Opened ancient database", "database", datadir, "readonly", readonly)
|
||||
return freezer, nil
|
||||
}
|
||||
|
||||
// Close terminates the chain freezer, unmapping all the data files.
|
||||
func (f *freezer) Close() error {
|
||||
f.writeLock.Lock()
|
||||
defer f.writeLock.Unlock()
|
||||
|
||||
var errs []error
|
||||
f.closeOnce.Do(func() {
|
||||
close(f.quit)
|
||||
@@ -199,61 +220,49 @@ func (f *freezer) Ancients() (uint64, error) {
|
||||
|
||||
// AncientSize returns the ancient size of the specified category.
|
||||
func (f *freezer) AncientSize(kind string) (uint64, error) {
|
||||
// This needs the write lock to avoid data races on table fields.
|
||||
// Speed doesn't matter here, AncientSize is for debugging.
|
||||
f.writeLock.Lock()
|
||||
defer f.writeLock.Unlock()
|
||||
|
||||
if table := f.tables[kind]; table != nil {
|
||||
return table.size()
|
||||
}
|
||||
return 0, errUnknownTable
|
||||
}
|
||||
|
||||
// AppendAncient injects all binary blobs belong to block at the end of the
|
||||
// append-only immutable table files.
|
||||
//
|
||||
// Notably, this function is lock free but kind of thread-safe. All out-of-order
|
||||
// injection will be rejected. But if two injections with same number happen at
|
||||
// the same time, we can get into the trouble.
|
||||
func (f *freezer) AppendAncient(number uint64, hash, header, body, receipts, td []byte) (err error) {
|
||||
// ModifyAncients runs the given write operation.
|
||||
func (f *freezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) {
|
||||
if f.readonly {
|
||||
return errReadOnly
|
||||
return 0, errReadOnly
|
||||
}
|
||||
// Ensure the binary blobs we are appending is continuous with freezer.
|
||||
if atomic.LoadUint64(&f.frozen) != number {
|
||||
return errOutOrderInsertion
|
||||
}
|
||||
// Rollback all inserted data if any insertion below failed to ensure
|
||||
// the tables won't out of sync.
|
||||
f.writeLock.Lock()
|
||||
defer f.writeLock.Unlock()
|
||||
|
||||
// Roll back all tables to the starting position in case of error.
|
||||
prevItem := f.frozen
|
||||
defer func() {
|
||||
if err != nil {
|
||||
rerr := f.repair()
|
||||
if rerr != nil {
|
||||
log.Crit("Failed to repair freezer", "err", rerr)
|
||||
// The write operation has failed. Go back to the previous item position.
|
||||
for name, table := range f.tables {
|
||||
err := table.truncate(prevItem)
|
||||
if err != nil {
|
||||
log.Error("Freezer table roll-back failed", "table", name, "index", prevItem, "err", err)
|
||||
}
|
||||
}
|
||||
log.Info("Append ancient failed", "number", number, "err", err)
|
||||
}
|
||||
}()
|
||||
pluginAppendAncient(number, hash, header, body, receipts, td)
|
||||
// Inject all the components into the relevant data tables
|
||||
if err := f.tables[freezerHashTable].Append(f.frozen, hash[:]); err != nil {
|
||||
log.Error("Failed to append ancient hash", "number", f.frozen, "hash", hash, "err", err)
|
||||
return err
|
||||
|
||||
f.writeBatch.reset()
|
||||
if err := fn(f.writeBatch); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := f.tables[freezerHeaderTable].Append(f.frozen, header); err != nil {
|
||||
log.Error("Failed to append ancient header", "number", f.frozen, "hash", hash, "err", err)
|
||||
return err
|
||||
item, writeSize, err := f.writeBatch.commit()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := f.tables[freezerBodiesTable].Append(f.frozen, body); err != nil {
|
||||
log.Error("Failed to append ancient body", "number", f.frozen, "hash", hash, "err", err)
|
||||
return err
|
||||
}
|
||||
if err := f.tables[freezerReceiptTable].Append(f.frozen, receipts); err != nil {
|
||||
log.Error("Failed to append ancient receipts", "number", f.frozen, "hash", hash, "err", err)
|
||||
return err
|
||||
}
|
||||
if err := f.tables[freezerDifficultyTable].Append(f.frozen, td); err != nil {
|
||||
log.Error("Failed to append ancient difficulty", "number", f.frozen, "hash", hash, "err", err)
|
||||
return err
|
||||
}
|
||||
atomic.AddUint64(&f.frozen, 1) // Only modify atomically
|
||||
return nil
|
||||
atomic.StoreUint64(&f.frozen, item)
|
||||
return writeSize, nil
|
||||
}
|
||||
|
||||
// TruncateAncients discards any recent data above the provided threshold number.
|
||||
@@ -261,6 +270,9 @@ func (f *freezer) TruncateAncients(items uint64) error {
|
||||
if f.readonly {
|
||||
return errReadOnly
|
||||
}
|
||||
f.writeLock.Lock()
|
||||
defer f.writeLock.Unlock()
|
||||
|
||||
if atomic.LoadUint64(&f.frozen) <= items {
|
||||
return nil
|
||||
}
|
||||
@@ -287,6 +299,24 @@ func (f *freezer) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// repair truncates all data tables to the same length.
|
||||
func (f *freezer) repair() error {
|
||||
min := uint64(math.MaxUint64)
|
||||
for _, table := range f.tables {
|
||||
items := atomic.LoadUint64(&table.items)
|
||||
if min > items {
|
||||
min = items
|
||||
}
|
||||
}
|
||||
for _, table := range f.tables {
|
||||
if err := table.truncate(min); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
atomic.StoreUint64(&f.frozen, min)
|
||||
return nil
|
||||
}
|
||||
|
||||
// freeze is a background thread that periodically checks the blockchain for any
|
||||
// import progress and moves ancient data from the fast database into the freezer.
|
||||
//
|
||||
@@ -353,54 +383,28 @@ func (f *freezer) freeze(db ethdb.KeyValueStore) {
|
||||
backoff = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Seems we have data ready to be frozen, process in usable batches
|
||||
limit := *number - threshold
|
||||
if limit-f.frozen > freezerBatchLimit {
|
||||
limit = f.frozen + freezerBatchLimit
|
||||
}
|
||||
var (
|
||||
start = time.Now()
|
||||
first = f.frozen
|
||||
ancients = make([]common.Hash, 0, limit-f.frozen)
|
||||
first, _ = f.Ancients()
|
||||
limit = *number - threshold
|
||||
)
|
||||
for f.frozen <= limit {
|
||||
// Retrieves all the components of the canonical block
|
||||
hash := ReadCanonicalHash(nfdb, f.frozen)
|
||||
if hash == (common.Hash{}) {
|
||||
log.Error("Canonical hash missing, can't freeze", "number", f.frozen)
|
||||
break
|
||||
}
|
||||
header := ReadHeaderRLP(nfdb, hash, f.frozen)
|
||||
if len(header) == 0 {
|
||||
log.Error("Block header missing, can't freeze", "number", f.frozen, "hash", hash)
|
||||
break
|
||||
}
|
||||
body := ReadBodyRLP(nfdb, hash, f.frozen)
|
||||
if len(body) == 0 {
|
||||
log.Error("Block body missing, can't freeze", "number", f.frozen, "hash", hash)
|
||||
break
|
||||
}
|
||||
receipts := ReadReceiptsRLP(nfdb, hash, f.frozen)
|
||||
if len(receipts) == 0 {
|
||||
log.Error("Block receipts missing, can't freeze", "number", f.frozen, "hash", hash)
|
||||
break
|
||||
}
|
||||
td := ReadTdRLP(nfdb, hash, f.frozen)
|
||||
if len(td) == 0 {
|
||||
log.Error("Total difficulty missing, can't freeze", "number", f.frozen, "hash", hash)
|
||||
break
|
||||
}
|
||||
log.Trace("Deep froze ancient block", "number", f.frozen, "hash", hash)
|
||||
// Inject all the components into the relevant data tables
|
||||
if err := f.AppendAncient(f.frozen, hash[:], header, body, receipts, td); err != nil {
|
||||
break
|
||||
}
|
||||
ancients = append(ancients, hash)
|
||||
if limit-first > freezerBatchLimit {
|
||||
limit = first + freezerBatchLimit
|
||||
}
|
||||
ancients, err := f.freezeRange(nfdb, first, limit)
|
||||
if err != nil {
|
||||
log.Error("Error in block freeze operation", "err", err)
|
||||
backoff = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Batch of blocks have been frozen, flush them before wiping from leveldb
|
||||
if err := f.Sync(); err != nil {
|
||||
log.Crit("Failed to flush frozen tables", "err", err)
|
||||
}
|
||||
|
||||
// Wipe out all data from the active database
|
||||
batch := db.NewBatch()
|
||||
for i := 0; i < len(ancients); i++ {
|
||||
@@ -465,6 +469,7 @@ func (f *freezer) freeze(db ethdb.KeyValueStore) {
|
||||
log.Crit("Failed to delete dangling side blocks", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Log something friendly for the user
|
||||
context := []interface{}{
|
||||
"blocks", f.frozen - first, "elapsed", common.PrettyDuration(time.Since(start)), "number", f.frozen - 1,
|
||||
@@ -481,20 +486,54 @@ func (f *freezer) freeze(db ethdb.KeyValueStore) {
|
||||
}
|
||||
}
|
||||
|
||||
// repair truncates all data tables to the same length.
|
||||
func (f *freezer) repair() error {
|
||||
min := uint64(math.MaxUint64)
|
||||
for _, table := range f.tables {
|
||||
items := atomic.LoadUint64(&table.items)
|
||||
if min > items {
|
||||
min = items
|
||||
func (f *freezer) freezeRange(nfdb *nofreezedb, number, limit uint64) (hashes []common.Hash, err error) {
|
||||
hashes = make([]common.Hash, 0, limit-number)
|
||||
|
||||
_, err = f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
||||
for ; number <= limit; number++ {
|
||||
// Retrieve all the components of the canonical block.
|
||||
hash := ReadCanonicalHash(nfdb, number)
|
||||
if hash == (common.Hash{}) {
|
||||
return fmt.Errorf("canonical hash missing, can't freeze block %d", number)
|
||||
}
|
||||
header := ReadHeaderRLP(nfdb, hash, number)
|
||||
if len(header) == 0 {
|
||||
return fmt.Errorf("block header missing, can't freeze block %d", number)
|
||||
}
|
||||
body := ReadBodyRLP(nfdb, hash, number)
|
||||
if len(body) == 0 {
|
||||
return fmt.Errorf("block body missing, can't freeze block %d", number)
|
||||
}
|
||||
receipts := ReadReceiptsRLP(nfdb, hash, number)
|
||||
if len(receipts) == 0 {
|
||||
return fmt.Errorf("block receipts missing, can't freeze block %d", number)
|
||||
}
|
||||
td := ReadTdRLP(nfdb, hash, number)
|
||||
if len(td) == 0 {
|
||||
return fmt.Errorf("total difficulty missing, can't freeze block %d", number)
|
||||
}
|
||||
|
||||
// Write to the batch.
|
||||
if err := op.AppendRaw(freezerHashTable, number, hash[:]); err != nil {
|
||||
return fmt.Errorf("can't write hash to freezer: %v", err)
|
||||
}
|
||||
if err := op.AppendRaw(freezerHeaderTable, number, header); err != nil {
|
||||
return fmt.Errorf("can't write header to freezer: %v", err)
|
||||
}
|
||||
if err := op.AppendRaw(freezerBodiesTable, number, body); err != nil {
|
||||
return fmt.Errorf("can't write body to freezer: %v", err)
|
||||
}
|
||||
if err := op.AppendRaw(freezerReceiptTable, number, receipts); err != nil {
|
||||
return fmt.Errorf("can't write receipts to freezer: %v", err)
|
||||
}
|
||||
if err := op.AppendRaw(freezerDifficultyTable, number, td); err != nil {
|
||||
return fmt.Errorf("can't write td to freezer: %v", err)
|
||||
}
|
||||
|
||||
hashes = append(hashes, hash)
|
||||
}
|
||||
}
|
||||
for _, table := range f.tables {
|
||||
if err := table.truncate(min); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
atomic.StoreUint64(&f.frozen, min)
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
|
||||
return hashes, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
// Copyright 2021 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rawdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/math"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/golang/snappy"
|
||||
)
|
||||
|
||||
// This is the maximum amount of data that will be buffered in memory
|
||||
// for a single freezer table batch.
|
||||
const freezerBatchBufferLimit = 2 * 1024 * 1024
|
||||
|
||||
// freezerBatch is a write operation of multiple items on a freezer.
|
||||
type freezerBatch struct {
|
||||
tables map[string]*freezerTableBatch
|
||||
}
|
||||
|
||||
func newFreezerBatch(f *freezer) *freezerBatch {
|
||||
batch := &freezerBatch{tables: make(map[string]*freezerTableBatch, len(f.tables))}
|
||||
for kind, table := range f.tables {
|
||||
batch.tables[kind] = table.newBatch()
|
||||
}
|
||||
return batch
|
||||
}
|
||||
|
||||
// Append adds an RLP-encoded item of the given kind.
|
||||
func (batch *freezerBatch) Append(kind string, num uint64, item interface{}) error {
|
||||
return batch.tables[kind].Append(num, item)
|
||||
}
|
||||
|
||||
// AppendRaw adds an item of the given kind.
|
||||
func (batch *freezerBatch) AppendRaw(kind string, num uint64, item []byte) error {
|
||||
return batch.tables[kind].AppendRaw(num, item)
|
||||
}
|
||||
|
||||
// reset initializes the batch.
|
||||
func (batch *freezerBatch) reset() {
|
||||
for _, tb := range batch.tables {
|
||||
tb.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// commit is called at the end of a write operation and
|
||||
// writes all remaining data to tables.
|
||||
func (batch *freezerBatch) commit() (item uint64, writeSize int64, err error) {
|
||||
// Check that count agrees on all batches.
|
||||
item = uint64(math.MaxUint64)
|
||||
for name, tb := range batch.tables {
|
||||
if item < math.MaxUint64 && tb.curItem != item {
|
||||
return 0, 0, fmt.Errorf("table %s is at item %d, want %d", name, tb.curItem, item)
|
||||
}
|
||||
item = tb.curItem
|
||||
}
|
||||
|
||||
// Commit all table batches.
|
||||
for _, tb := range batch.tables {
|
||||
if err := tb.commit(); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
writeSize += tb.totalBytes
|
||||
}
|
||||
return item, writeSize, nil
|
||||
}
|
||||
|
||||
// freezerTableBatch is a batch for a freezer table.
|
||||
type freezerTableBatch struct {
|
||||
t *freezerTable
|
||||
|
||||
sb *snappyBuffer
|
||||
encBuffer writeBuffer
|
||||
dataBuffer []byte
|
||||
indexBuffer []byte
|
||||
curItem uint64 // expected index of next append
|
||||
totalBytes int64 // counts written bytes since reset
|
||||
}
|
||||
|
||||
// newBatch creates a new batch for the freezer table.
|
||||
func (t *freezerTable) newBatch() *freezerTableBatch {
|
||||
batch := &freezerTableBatch{t: t}
|
||||
if !t.noCompression {
|
||||
batch.sb = new(snappyBuffer)
|
||||
}
|
||||
batch.reset()
|
||||
return batch
|
||||
}
|
||||
|
||||
// reset clears the batch for reuse.
|
||||
func (batch *freezerTableBatch) reset() {
|
||||
batch.dataBuffer = batch.dataBuffer[:0]
|
||||
batch.indexBuffer = batch.indexBuffer[:0]
|
||||
batch.curItem = atomic.LoadUint64(&batch.t.items)
|
||||
batch.totalBytes = 0
|
||||
}
|
||||
|
||||
// Append rlp-encodes and adds data at the end of the freezer table. The item number is a
|
||||
// precautionary parameter to ensure data correctness, but the table will reject already
|
||||
// existing data.
|
||||
func (batch *freezerTableBatch) Append(item uint64, data interface{}) error {
|
||||
if item != batch.curItem {
|
||||
return errOutOrderInsertion
|
||||
}
|
||||
|
||||
// Encode the item.
|
||||
batch.encBuffer.Reset()
|
||||
if err := rlp.Encode(&batch.encBuffer, data); err != nil {
|
||||
return err
|
||||
}
|
||||
encItem := batch.encBuffer.data
|
||||
if batch.sb != nil {
|
||||
encItem = batch.sb.compress(encItem)
|
||||
}
|
||||
return batch.appendItem(encItem)
|
||||
}
|
||||
|
||||
// AppendRaw injects a binary blob at the end of the freezer table. The item number is a
|
||||
// precautionary parameter to ensure data correctness, but the table will reject already
|
||||
// existing data.
|
||||
func (batch *freezerTableBatch) AppendRaw(item uint64, blob []byte) error {
|
||||
if item != batch.curItem {
|
||||
return errOutOrderInsertion
|
||||
}
|
||||
|
||||
encItem := blob
|
||||
if batch.sb != nil {
|
||||
encItem = batch.sb.compress(blob)
|
||||
}
|
||||
return batch.appendItem(encItem)
|
||||
}
|
||||
|
||||
func (batch *freezerTableBatch) appendItem(data []byte) error {
|
||||
// Check if item fits into current data file.
|
||||
itemSize := int64(len(data))
|
||||
itemOffset := batch.t.headBytes + int64(len(batch.dataBuffer))
|
||||
if itemOffset+itemSize > int64(batch.t.maxFileSize) {
|
||||
// It doesn't fit, go to next file first.
|
||||
if err := batch.commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := batch.t.advanceHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
itemOffset = 0
|
||||
}
|
||||
|
||||
// Put data to buffer.
|
||||
batch.dataBuffer = append(batch.dataBuffer, data...)
|
||||
batch.totalBytes += itemSize
|
||||
|
||||
// Put index entry to buffer.
|
||||
entry := indexEntry{filenum: batch.t.headId, offset: uint32(itemOffset + itemSize)}
|
||||
batch.indexBuffer = entry.append(batch.indexBuffer)
|
||||
batch.curItem++
|
||||
|
||||
return batch.maybeCommit()
|
||||
}
|
||||
|
||||
// maybeCommit writes the buffered data if the buffer is full enough.
|
||||
func (batch *freezerTableBatch) maybeCommit() error {
|
||||
if len(batch.dataBuffer) > freezerBatchBufferLimit {
|
||||
return batch.commit()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// commit writes the batched items to the backing freezerTable.
|
||||
func (batch *freezerTableBatch) commit() error {
|
||||
// Write data.
|
||||
_, err := batch.t.head.Write(batch.dataBuffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dataSize := int64(len(batch.dataBuffer))
|
||||
batch.dataBuffer = batch.dataBuffer[:0]
|
||||
|
||||
// Write index.
|
||||
_, err = batch.t.index.Write(batch.indexBuffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
indexSize := int64(len(batch.indexBuffer))
|
||||
batch.indexBuffer = batch.indexBuffer[:0]
|
||||
|
||||
// Update headBytes of table.
|
||||
batch.t.headBytes += dataSize
|
||||
atomic.StoreUint64(&batch.t.items, batch.curItem)
|
||||
|
||||
// Update metrics.
|
||||
batch.t.sizeGauge.Inc(dataSize + indexSize)
|
||||
batch.t.writeMeter.Mark(dataSize + indexSize)
|
||||
return nil
|
||||
}
|
||||
|
||||
// snappyBuffer writes snappy in block format, and can be reused. It is
|
||||
// reset when WriteTo is called.
|
||||
type snappyBuffer struct {
|
||||
dst []byte
|
||||
}
|
||||
|
||||
// compress snappy-compresses the data.
|
||||
func (s *snappyBuffer) compress(data []byte) []byte {
|
||||
// The snappy library does not care what the capacity of the buffer is,
|
||||
// but only checks the length. If the length is too small, it will
|
||||
// allocate a brand new buffer.
|
||||
// To avoid that, we check the required size here, and grow the size of the
|
||||
// buffer to utilize the full capacity.
|
||||
if n := snappy.MaxEncodedLen(len(data)); len(s.dst) < n {
|
||||
if cap(s.dst) < n {
|
||||
s.dst = make([]byte, n)
|
||||
}
|
||||
s.dst = s.dst[:n]
|
||||
}
|
||||
|
||||
s.dst = snappy.Encode(s.dst, data)
|
||||
return s.dst
|
||||
}
|
||||
|
||||
// writeBuffer implements io.Writer for a byte slice.
|
||||
type writeBuffer struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (wb *writeBuffer) Write(data []byte) (int, error) {
|
||||
wb.data = append(wb.data, data...)
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (wb *writeBuffer) Reset() {
|
||||
wb.data = wb.data[:0]
|
||||
}
|
||||
+58
-111
@@ -17,6 +17,7 @@
|
||||
package rawdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -55,19 +56,20 @@ type indexEntry struct {
|
||||
|
||||
const indexEntrySize = 6
|
||||
|
||||
// unmarshallBinary deserializes binary b into the rawIndex entry.
|
||||
// unmarshalBinary deserializes binary b into the rawIndex entry.
|
||||
func (i *indexEntry) unmarshalBinary(b []byte) error {
|
||||
i.filenum = uint32(binary.BigEndian.Uint16(b[:2]))
|
||||
i.offset = binary.BigEndian.Uint32(b[2:6])
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshallBinary serializes the rawIndex entry into binary.
|
||||
func (i *indexEntry) marshallBinary() []byte {
|
||||
b := make([]byte, indexEntrySize)
|
||||
binary.BigEndian.PutUint16(b[:2], uint16(i.filenum))
|
||||
binary.BigEndian.PutUint32(b[2:6], i.offset)
|
||||
return b
|
||||
// append adds the encoded entry to the end of b.
|
||||
func (i *indexEntry) append(b []byte) []byte {
|
||||
offset := len(b)
|
||||
out := append(b, make([]byte, indexEntrySize)...)
|
||||
binary.BigEndian.PutUint16(out[offset:], uint16(i.filenum))
|
||||
binary.BigEndian.PutUint32(out[offset+2:], i.offset)
|
||||
return out
|
||||
}
|
||||
|
||||
// bounds returns the start- and end- offsets, and the file number of where to
|
||||
@@ -107,7 +109,7 @@ type freezerTable struct {
|
||||
// to count how many historic items have gone missing.
|
||||
itemOffset uint32 // Offset (number of discarded items)
|
||||
|
||||
headBytes uint32 // Number of bytes written to the head file
|
||||
headBytes int64 // Number of bytes written to the head file
|
||||
readMeter metrics.Meter // Meter for measuring the effective amount of data read
|
||||
writeMeter metrics.Meter // Meter for measuring the effective amount of data written
|
||||
sizeGauge metrics.Gauge // Gauge for tracking the combined size of all freezer tables
|
||||
@@ -118,12 +120,7 @@ type freezerTable struct {
|
||||
|
||||
// NewFreezerTable opens the given path as a freezer table.
|
||||
func NewFreezerTable(path, name string, disableSnappy bool) (*freezerTable, error) {
|
||||
return newTable(path, name, metrics.NilMeter{}, metrics.NilMeter{}, metrics.NilGauge{}, disableSnappy)
|
||||
}
|
||||
|
||||
// newTable opens a freezer table with default settings - 2G files
|
||||
func newTable(path string, name string, readMeter metrics.Meter, writeMeter metrics.Meter, sizeGauge metrics.Gauge, disableSnappy bool) (*freezerTable, error) {
|
||||
return newCustomTable(path, name, readMeter, writeMeter, sizeGauge, 2*1000*1000*1000, disableSnappy)
|
||||
return newTable(path, name, metrics.NilMeter{}, metrics.NilMeter{}, metrics.NilGauge{}, freezerTableSize, disableSnappy)
|
||||
}
|
||||
|
||||
// openFreezerFileForAppend opens a freezer table file and seeks to the end
|
||||
@@ -164,10 +161,10 @@ func truncateFreezerFile(file *os.File, size int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// newCustomTable opens a freezer table, creating the data and index files if they are
|
||||
// newTable opens a freezer table, creating the data and index files if they are
|
||||
// non existent. Both files are truncated to the shortest common length to ensure
|
||||
// they don't go out of sync.
|
||||
func newCustomTable(path string, name string, readMeter metrics.Meter, writeMeter metrics.Meter, sizeGauge metrics.Gauge, maxFilesize uint32, noCompression bool) (*freezerTable, error) {
|
||||
func newTable(path string, name string, readMeter metrics.Meter, writeMeter metrics.Meter, sizeGauge metrics.Gauge, maxFilesize uint32, noCompression bool) (*freezerTable, error) {
|
||||
// Ensure the containing directory exists and open the indexEntry file
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return nil, err
|
||||
@@ -313,7 +310,7 @@ func (t *freezerTable) repair() error {
|
||||
}
|
||||
// Update the item and byte counters and return
|
||||
t.items = uint64(t.itemOffset) + uint64(offsetsSize/indexEntrySize-1) // last indexEntry points to the end of the data file
|
||||
t.headBytes = uint32(contentSize)
|
||||
t.headBytes = contentSize
|
||||
t.headId = lastIndex.filenum
|
||||
|
||||
// Close opened files and preopen all files
|
||||
@@ -387,14 +384,14 @@ func (t *freezerTable) truncate(items uint64) error {
|
||||
t.releaseFilesAfter(expected.filenum, true)
|
||||
// Set back the historic head
|
||||
t.head = newHead
|
||||
atomic.StoreUint32(&t.headId, expected.filenum)
|
||||
t.headId = expected.filenum
|
||||
}
|
||||
if err := truncateFreezerFile(t.head, int64(expected.offset)); err != nil {
|
||||
return err
|
||||
}
|
||||
// All data files truncated, set internal counters and return
|
||||
t.headBytes = int64(expected.offset)
|
||||
atomic.StoreUint64(&t.items, items)
|
||||
atomic.StoreUint32(&t.headBytes, expected.offset)
|
||||
|
||||
// Retrieve the new size and update the total size counter
|
||||
newSize, err := t.sizeNolock()
|
||||
@@ -471,94 +468,6 @@ func (t *freezerTable) releaseFilesAfter(num uint32, remove bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// Append injects a binary blob at the end of the freezer table. The item number
|
||||
// is a precautionary parameter to ensure data correctness, but the table will
|
||||
// reject already existing data.
|
||||
//
|
||||
// Note, this method will *not* flush any data to disk so be sure to explicitly
|
||||
// fsync before irreversibly deleting data from the database.
|
||||
func (t *freezerTable) Append(item uint64, blob []byte) error {
|
||||
// Encode the blob before the lock portion
|
||||
if !t.noCompression {
|
||||
blob = snappy.Encode(nil, blob)
|
||||
}
|
||||
// Read lock prevents competition with truncate
|
||||
retry, err := t.append(item, blob, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if retry {
|
||||
// Read lock was insufficient, retry with a writelock
|
||||
_, err = t.append(item, blob, true)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// append injects a binary blob at the end of the freezer table.
|
||||
// Normally, inserts do not require holding the write-lock, so it should be invoked with 'wlock' set to
|
||||
// false.
|
||||
// However, if the data will grown the current file out of bounds, then this
|
||||
// method will return 'true, nil', indicating that the caller should retry, this time
|
||||
// with 'wlock' set to true.
|
||||
func (t *freezerTable) append(item uint64, encodedBlob []byte, wlock bool) (bool, error) {
|
||||
if wlock {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
} else {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
}
|
||||
// Ensure the table is still accessible
|
||||
if t.index == nil || t.head == nil {
|
||||
return false, errClosed
|
||||
}
|
||||
// Ensure only the next item can be written, nothing else
|
||||
if atomic.LoadUint64(&t.items) != item {
|
||||
return false, fmt.Errorf("appending unexpected item: want %d, have %d", t.items, item)
|
||||
}
|
||||
bLen := uint32(len(encodedBlob))
|
||||
if t.headBytes+bLen < bLen ||
|
||||
t.headBytes+bLen > t.maxFileSize {
|
||||
// Writing would overflow, so we need to open a new data file.
|
||||
// If we don't already hold the writelock, abort and let the caller
|
||||
// invoke this method a second time.
|
||||
if !wlock {
|
||||
return true, nil
|
||||
}
|
||||
nextID := atomic.LoadUint32(&t.headId) + 1
|
||||
// We open the next file in truncated mode -- if this file already
|
||||
// exists, we need to start over from scratch on it
|
||||
newHead, err := t.openFile(nextID, openFreezerFileTruncated)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Close old file, and reopen in RDONLY mode
|
||||
t.releaseFile(t.headId)
|
||||
t.openFile(t.headId, openFreezerFileForReadOnly)
|
||||
|
||||
// Swap out the current head
|
||||
t.head = newHead
|
||||
atomic.StoreUint32(&t.headBytes, 0)
|
||||
atomic.StoreUint32(&t.headId, nextID)
|
||||
}
|
||||
if _, err := t.head.Write(encodedBlob); err != nil {
|
||||
return false, err
|
||||
}
|
||||
newOffset := atomic.AddUint32(&t.headBytes, bLen)
|
||||
idx := indexEntry{
|
||||
filenum: atomic.LoadUint32(&t.headId),
|
||||
offset: newOffset,
|
||||
}
|
||||
// Write indexEntry
|
||||
t.index.Write(idx.marshallBinary())
|
||||
|
||||
t.writeMeter.Mark(int64(bLen + indexEntrySize))
|
||||
t.sizeGauge.Inc(int64(bLen + indexEntrySize))
|
||||
|
||||
atomic.AddUint64(&t.items, 1)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// getIndices returns the index entries for the given from-item, covering 'count' items.
|
||||
// N.B: The actual number of returned indices for N items will always be N+1 (unless an
|
||||
// error is returned).
|
||||
@@ -651,6 +560,7 @@ func (t *freezerTable) RetrieveItems(start, count, maxBytes uint64) ([][]byte, e
|
||||
func (t *freezerTable) retrieveItems(start, count, maxBytes uint64) ([]byte, []int, error) {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
// Ensure the table and the item is accessible
|
||||
if t.index == nil || t.head == nil {
|
||||
return nil, nil, errClosed
|
||||
@@ -763,6 +673,32 @@ func (t *freezerTable) sizeNolock() (uint64, error) {
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// advanceHead should be called when the current head file would outgrow the file limits,
|
||||
// and a new file must be opened. The caller of this method must hold the write-lock
|
||||
// before calling this method.
|
||||
func (t *freezerTable) advanceHead() error {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
// We open the next file in truncated mode -- if this file already
|
||||
// exists, we need to start over from scratch on it.
|
||||
nextID := t.headId + 1
|
||||
newHead, err := t.openFile(nextID, openFreezerFileTruncated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Close old file, and reopen in RDONLY mode.
|
||||
t.releaseFile(t.headId)
|
||||
t.openFile(t.headId, openFreezerFileForReadOnly)
|
||||
|
||||
// Swap out the current head.
|
||||
t.head = newHead
|
||||
t.headBytes = 0
|
||||
t.headId = nextID
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sync pushes any pending data from memory out to disk. This is an expensive
|
||||
// operation, so use it with care.
|
||||
func (t *freezerTable) Sync() error {
|
||||
@@ -775,10 +711,21 @@ func (t *freezerTable) Sync() error {
|
||||
// DumpIndex is a debug print utility function, mainly for testing. It can also
|
||||
// be used to analyse a live freezer table index.
|
||||
func (t *freezerTable) DumpIndex(start, stop int64) {
|
||||
t.dumpIndex(os.Stdout, start, stop)
|
||||
}
|
||||
|
||||
func (t *freezerTable) dumpIndexString(start, stop int64) string {
|
||||
var out bytes.Buffer
|
||||
out.WriteString("\n")
|
||||
t.dumpIndex(&out, start, stop)
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func (t *freezerTable) dumpIndex(w io.Writer, start, stop int64) {
|
||||
buf := make([]byte, indexEntrySize)
|
||||
|
||||
fmt.Printf("| number | fileno | offset |\n")
|
||||
fmt.Printf("|--------|--------|--------|\n")
|
||||
fmt.Fprintf(w, "| number | fileno | offset |\n")
|
||||
fmt.Fprintf(w, "|--------|--------|--------|\n")
|
||||
|
||||
for i := uint64(start); ; i++ {
|
||||
if _, err := t.index.ReadAt(buf, int64(i*indexEntrySize)); err != nil {
|
||||
@@ -786,10 +733,10 @@ func (t *freezerTable) DumpIndex(start, stop int64) {
|
||||
}
|
||||
var entry indexEntry
|
||||
entry.unmarshalBinary(buf)
|
||||
fmt.Printf("| %03d | %03d | %03d | \n", i, entry.filenum, entry.offset)
|
||||
fmt.Fprintf(w, "| %03d | %03d | %03d | \n", i, entry.filenum, entry.offset)
|
||||
if stop > 0 && i >= uint64(stop) {
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Printf("|--------------------------|\n")
|
||||
fmt.Fprintf(w, "|--------------------------|\n")
|
||||
}
|
||||
|
||||
+206
-184
@@ -18,49 +18,36 @@ package rawdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/metrics"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().Unix())
|
||||
}
|
||||
|
||||
// Gets a chunk of data, filled with 'b'
|
||||
func getChunk(size int, b int) []byte {
|
||||
data := make([]byte, size)
|
||||
for i := range data {
|
||||
data[i] = byte(b)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// TestFreezerBasics test initializing a freezertable from scratch, writing to the table,
|
||||
// and reading it back.
|
||||
func TestFreezerBasics(t *testing.T) {
|
||||
t.Parallel()
|
||||
// set cutoff at 50 bytes
|
||||
f, err := newCustomTable(os.TempDir(),
|
||||
f, err := newTable(os.TempDir(),
|
||||
fmt.Sprintf("unittest-%d", rand.Uint64()),
|
||||
metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write 15 bytes 255 times, results in 85 files
|
||||
for x := 0; x < 255; x++ {
|
||||
data := getChunk(15, x)
|
||||
f.Append(uint64(x), data)
|
||||
}
|
||||
writeChunks(t, f, 255, 15)
|
||||
|
||||
//print(t, f, 0)
|
||||
//print(t, f, 1)
|
||||
@@ -98,16 +85,21 @@ func TestFreezerBasicsClosing(t *testing.T) {
|
||||
f *freezerTable
|
||||
err error
|
||||
)
|
||||
f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err = newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write 15 bytes 255 times, results in 85 files
|
||||
|
||||
// Write 15 bytes 255 times, results in 85 files.
|
||||
// In-between writes, the table is closed and re-opened.
|
||||
for x := 0; x < 255; x++ {
|
||||
data := getChunk(15, x)
|
||||
f.Append(uint64(x), data)
|
||||
batch := f.newBatch()
|
||||
require.NoError(t, batch.AppendRaw(uint64(x), data))
|
||||
require.NoError(t, batch.commit())
|
||||
f.Close()
|
||||
f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
|
||||
f, err = newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -124,7 +116,7 @@ func TestFreezerBasicsClosing(t *testing.T) {
|
||||
t.Fatalf("test %d, got \n%x != \n%x", y, got, exp)
|
||||
}
|
||||
f.Close()
|
||||
f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err = newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -137,22 +129,22 @@ func TestFreezerRepairDanglingHead(t *testing.T) {
|
||||
rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
|
||||
fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64())
|
||||
|
||||
{ // Fill table
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
// Fill table
|
||||
{
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write 15 bytes 255 times
|
||||
for x := 0; x < 255; x++ {
|
||||
data := getChunk(15, x)
|
||||
f.Append(uint64(x), data)
|
||||
}
|
||||
writeChunks(t, f, 255, 15)
|
||||
|
||||
// The last item should be there
|
||||
if _, err = f.Retrieve(0xfe); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// open the index
|
||||
idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
@@ -165,9 +157,10 @@ func TestFreezerRepairDanglingHead(t *testing.T) {
|
||||
}
|
||||
idxFile.Truncate(stat.Size() - 4)
|
||||
idxFile.Close()
|
||||
|
||||
// Now open it again
|
||||
{
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -188,22 +181,22 @@ func TestFreezerRepairDanglingHeadLarge(t *testing.T) {
|
||||
rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
|
||||
fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64())
|
||||
|
||||
{ // Fill a table and close it
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
// Fill a table and close it
|
||||
{
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write 15 bytes 255 times
|
||||
for x := 0; x < 0xff; x++ {
|
||||
data := getChunk(15, x)
|
||||
f.Append(uint64(x), data)
|
||||
}
|
||||
writeChunks(t, f, 255, 15)
|
||||
|
||||
// The last item should be there
|
||||
if _, err = f.Retrieve(f.items - 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// open the index
|
||||
idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
@@ -213,9 +206,10 @@ func TestFreezerRepairDanglingHeadLarge(t *testing.T) {
|
||||
// 0-indexEntry, 1-indexEntry, corrupt-indexEntry
|
||||
idxFile.Truncate(indexEntrySize + indexEntrySize + indexEntrySize/2)
|
||||
idxFile.Close()
|
||||
|
||||
// Now open it again
|
||||
{
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -228,15 +222,17 @@ func TestFreezerRepairDanglingHeadLarge(t *testing.T) {
|
||||
t.Errorf("Expected error for missing index entry")
|
||||
}
|
||||
// We should now be able to store items again, from item = 1
|
||||
batch := f.newBatch()
|
||||
for x := 1; x < 0xff; x++ {
|
||||
data := getChunk(15, ^x)
|
||||
f.Append(uint64(x), data)
|
||||
require.NoError(t, batch.AppendRaw(uint64(x), getChunk(15, ^x)))
|
||||
}
|
||||
require.NoError(t, batch.commit())
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// And if we open it, we should now be able to read all of them (new values)
|
||||
{
|
||||
f, _ := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, _ := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
for y := 1; y < 255; y++ {
|
||||
exp := getChunk(15, ^y)
|
||||
got, err := f.Retrieve(uint64(y))
|
||||
@@ -255,22 +251,21 @@ func TestSnappyDetection(t *testing.T) {
|
||||
t.Parallel()
|
||||
rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
|
||||
fname := fmt.Sprintf("snappytest-%d", rand.Uint64())
|
||||
|
||||
// Open with snappy
|
||||
{
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write 15 bytes 255 times
|
||||
for x := 0; x < 0xff; x++ {
|
||||
data := getChunk(15, x)
|
||||
f.Append(uint64(x), data)
|
||||
}
|
||||
writeChunks(t, f, 255, 15)
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// Open without snappy
|
||||
{
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, false)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -282,7 +277,7 @@ func TestSnappyDetection(t *testing.T) {
|
||||
|
||||
// Open with snappy
|
||||
{
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -292,8 +287,8 @@ func TestSnappyDetection(t *testing.T) {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func assertFileSize(f string, size int64) error {
|
||||
stat, err := os.Stat(f)
|
||||
if err != nil {
|
||||
@@ -303,7 +298,6 @@ func assertFileSize(f string, size int64) error {
|
||||
return fmt.Errorf("error, expected size %d, got %d", size, stat.Size())
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// TestFreezerRepairDanglingIndex checks that if the index has more entries than there are data,
|
||||
@@ -313,16 +307,15 @@ func TestFreezerRepairDanglingIndex(t *testing.T) {
|
||||
rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
|
||||
fname := fmt.Sprintf("dangling_indextest-%d", rand.Uint64())
|
||||
|
||||
{ // Fill a table and close it
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
// Fill a table and close it
|
||||
{
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write 15 bytes 9 times : 150 bytes
|
||||
for x := 0; x < 9; x++ {
|
||||
data := getChunk(15, x)
|
||||
f.Append(uint64(x), data)
|
||||
}
|
||||
writeChunks(t, f, 9, 15)
|
||||
|
||||
// The last item should be there
|
||||
if _, err = f.Retrieve(f.items - 1); err != nil {
|
||||
f.Close()
|
||||
@@ -331,6 +324,7 @@ func TestFreezerRepairDanglingIndex(t *testing.T) {
|
||||
f.Close()
|
||||
// File sizes should be 45, 45, 45 : items[3, 3, 3)
|
||||
}
|
||||
|
||||
// Crop third file
|
||||
fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0002.rdat", fname))
|
||||
// Truncate third file: 45 ,45, 20
|
||||
@@ -345,17 +339,18 @@ func TestFreezerRepairDanglingIndex(t *testing.T) {
|
||||
file.Truncate(20)
|
||||
file.Close()
|
||||
}
|
||||
|
||||
// Open db it again
|
||||
// It should restore the file(s) to
|
||||
// 45, 45, 15
|
||||
// with 3+3+1 items
|
||||
{
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
if f.items != 7 {
|
||||
f.Close()
|
||||
t.Fatalf("expected %d items, got %d", 7, f.items)
|
||||
}
|
||||
if err := assertFileSize(fileToCrop, 15); err != nil {
|
||||
@@ -365,30 +360,29 @@ func TestFreezerRepairDanglingIndex(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFreezerTruncate(t *testing.T) {
|
||||
|
||||
t.Parallel()
|
||||
rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
|
||||
fname := fmt.Sprintf("truncation-%d", rand.Uint64())
|
||||
|
||||
{ // Fill table
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
// Fill table
|
||||
{
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write 15 bytes 30 times
|
||||
for x := 0; x < 30; x++ {
|
||||
data := getChunk(15, x)
|
||||
f.Append(uint64(x), data)
|
||||
}
|
||||
writeChunks(t, f, 30, 15)
|
||||
|
||||
// The last item should be there
|
||||
if _, err = f.Retrieve(f.items - 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// Reopen, truncate
|
||||
{
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -401,9 +395,7 @@ func TestFreezerTruncate(t *testing.T) {
|
||||
if f.headBytes != 15 {
|
||||
t.Fatalf("expected %d bytes, got %d", 15, f.headBytes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestFreezerRepairFirstFile tests a head file with the very first item only half-written.
|
||||
@@ -412,20 +404,26 @@ func TestFreezerRepairFirstFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
|
||||
fname := fmt.Sprintf("truncationfirst-%d", rand.Uint64())
|
||||
{ // Fill table
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
|
||||
// Fill table
|
||||
{
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write 80 bytes, splitting out into two files
|
||||
f.Append(0, getChunk(40, 0xFF))
|
||||
f.Append(1, getChunk(40, 0xEE))
|
||||
batch := f.newBatch()
|
||||
require.NoError(t, batch.AppendRaw(0, getChunk(40, 0xFF)))
|
||||
require.NoError(t, batch.AppendRaw(1, getChunk(40, 0xEE)))
|
||||
require.NoError(t, batch.commit())
|
||||
|
||||
// The last item should be there
|
||||
if _, err = f.Retrieve(f.items - 1); err != nil {
|
||||
if _, err = f.Retrieve(1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// Truncate the file in half
|
||||
fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0001.rdat", fname))
|
||||
{
|
||||
@@ -439,9 +437,10 @@ func TestFreezerRepairFirstFile(t *testing.T) {
|
||||
file.Truncate(20)
|
||||
file.Close()
|
||||
}
|
||||
|
||||
// Reopen
|
||||
{
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -449,9 +448,14 @@ func TestFreezerRepairFirstFile(t *testing.T) {
|
||||
f.Close()
|
||||
t.Fatalf("expected %d items, got %d", 0, f.items)
|
||||
}
|
||||
|
||||
// Write 40 bytes
|
||||
f.Append(1, getChunk(40, 0xDD))
|
||||
batch := f.newBatch()
|
||||
require.NoError(t, batch.AppendRaw(1, getChunk(40, 0xDD)))
|
||||
require.NoError(t, batch.commit())
|
||||
|
||||
f.Close()
|
||||
|
||||
// Should have been truncated down to zero and then 40 written
|
||||
if err := assertFileSize(fileToCrop, 40); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -468,25 +472,26 @@ func TestFreezerReadAndTruncate(t *testing.T) {
|
||||
t.Parallel()
|
||||
rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
|
||||
fname := fmt.Sprintf("read_truncate-%d", rand.Uint64())
|
||||
{ // Fill table
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
|
||||
// Fill table
|
||||
{
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write 15 bytes 30 times
|
||||
for x := 0; x < 30; x++ {
|
||||
data := getChunk(15, x)
|
||||
f.Append(uint64(x), data)
|
||||
}
|
||||
writeChunks(t, f, 30, 15)
|
||||
|
||||
// The last item should be there
|
||||
if _, err = f.Retrieve(f.items - 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// Reopen and read all files
|
||||
{
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -497,40 +502,48 @@ func TestFreezerReadAndTruncate(t *testing.T) {
|
||||
for y := byte(0); y < 30; y++ {
|
||||
f.Retrieve(uint64(y))
|
||||
}
|
||||
|
||||
// Now, truncate back to zero
|
||||
f.truncate(0)
|
||||
|
||||
// Write the data again
|
||||
batch := f.newBatch()
|
||||
for x := 0; x < 30; x++ {
|
||||
data := getChunk(15, ^x)
|
||||
if err := f.Append(uint64(x), data); err != nil {
|
||||
t.Fatalf("error %v", err)
|
||||
}
|
||||
require.NoError(t, batch.AppendRaw(uint64(x), getChunk(15, ^x)))
|
||||
}
|
||||
require.NoError(t, batch.commit())
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestOffset(t *testing.T) {
|
||||
func TestFreezerOffset(t *testing.T) {
|
||||
t.Parallel()
|
||||
rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
|
||||
fname := fmt.Sprintf("offset-%d", rand.Uint64())
|
||||
{ // Fill table
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 40, true)
|
||||
|
||||
// Fill table
|
||||
{
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 40, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write 6 x 20 bytes, splitting out into three files
|
||||
f.Append(0, getChunk(20, 0xFF))
|
||||
f.Append(1, getChunk(20, 0xEE))
|
||||
batch := f.newBatch()
|
||||
require.NoError(t, batch.AppendRaw(0, getChunk(20, 0xFF)))
|
||||
require.NoError(t, batch.AppendRaw(1, getChunk(20, 0xEE)))
|
||||
|
||||
f.Append(2, getChunk(20, 0xdd))
|
||||
f.Append(3, getChunk(20, 0xcc))
|
||||
require.NoError(t, batch.AppendRaw(2, getChunk(20, 0xdd)))
|
||||
require.NoError(t, batch.AppendRaw(3, getChunk(20, 0xcc)))
|
||||
|
||||
f.Append(4, getChunk(20, 0xbb))
|
||||
f.Append(5, getChunk(20, 0xaa))
|
||||
f.DumpIndex(0, 100)
|
||||
require.NoError(t, batch.AppendRaw(4, getChunk(20, 0xbb)))
|
||||
require.NoError(t, batch.AppendRaw(5, getChunk(20, 0xaa)))
|
||||
require.NoError(t, batch.commit())
|
||||
|
||||
t.Log(f.dumpIndexString(0, 100))
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// Now crop it.
|
||||
{
|
||||
// delete files 0 and 1
|
||||
@@ -558,7 +571,7 @@ func TestOffset(t *testing.T) {
|
||||
filenum: tailId,
|
||||
offset: itemOffset,
|
||||
}
|
||||
buf := zeroIndex.marshallBinary()
|
||||
buf := zeroIndex.append(nil)
|
||||
// Overwrite index zero
|
||||
copy(indexBuf, buf)
|
||||
// Remove the four next indices by overwriting
|
||||
@@ -567,44 +580,36 @@ func TestOffset(t *testing.T) {
|
||||
// Need to truncate the moved index items
|
||||
indexFile.Truncate(indexEntrySize * (1 + 2))
|
||||
indexFile.Close()
|
||||
|
||||
}
|
||||
|
||||
// Now open again
|
||||
checkPresent := func(numDeleted uint64) {
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 40, true)
|
||||
{
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 40, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.DumpIndex(0, 100)
|
||||
// It should allow writing item 6
|
||||
f.Append(numDeleted+2, getChunk(20, 0x99))
|
||||
defer f.Close()
|
||||
t.Log(f.dumpIndexString(0, 100))
|
||||
|
||||
// It should be fine to fetch 4,5,6
|
||||
if got, err := f.Retrieve(numDeleted); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exp := getChunk(20, 0xbb); !bytes.Equal(got, exp) {
|
||||
t.Fatalf("expected %x got %x", exp, got)
|
||||
}
|
||||
if got, err := f.Retrieve(numDeleted + 1); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exp := getChunk(20, 0xaa); !bytes.Equal(got, exp) {
|
||||
t.Fatalf("expected %x got %x", exp, got)
|
||||
}
|
||||
if got, err := f.Retrieve(numDeleted + 2); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if exp := getChunk(20, 0x99); !bytes.Equal(got, exp) {
|
||||
t.Fatalf("expected %x got %x", exp, got)
|
||||
}
|
||||
// It should allow writing item 6.
|
||||
batch := f.newBatch()
|
||||
require.NoError(t, batch.AppendRaw(6, getChunk(20, 0x99)))
|
||||
require.NoError(t, batch.commit())
|
||||
|
||||
// It should error at 0, 1,2,3
|
||||
for i := numDeleted - 1; i > numDeleted-10; i-- {
|
||||
if _, err := f.Retrieve(i); err == nil {
|
||||
t.Fatal("expected err")
|
||||
}
|
||||
}
|
||||
checkRetrieveError(t, f, map[uint64]error{
|
||||
0: errOutOfBounds,
|
||||
1: errOutOfBounds,
|
||||
2: errOutOfBounds,
|
||||
3: errOutOfBounds,
|
||||
})
|
||||
checkRetrieve(t, f, map[uint64][]byte{
|
||||
4: getChunk(20, 0xbb),
|
||||
5: getChunk(20, 0xaa),
|
||||
6: getChunk(20, 0x99),
|
||||
})
|
||||
}
|
||||
checkPresent(4)
|
||||
// Now, let's pretend we have deleted 1M items
|
||||
|
||||
// Edit the index again, with a much larger initial offset of 1M.
|
||||
{
|
||||
// Read the index file
|
||||
p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.ridx", fname))
|
||||
@@ -624,13 +629,71 @@ func TestOffset(t *testing.T) {
|
||||
offset: itemOffset,
|
||||
filenum: tailId,
|
||||
}
|
||||
buf := zeroIndex.marshallBinary()
|
||||
buf := zeroIndex.append(nil)
|
||||
// Overwrite index zero
|
||||
copy(indexBuf, buf)
|
||||
indexFile.WriteAt(indexBuf, 0)
|
||||
indexFile.Close()
|
||||
}
|
||||
checkPresent(1000000)
|
||||
|
||||
// Check that existing items have been moved to index 1M.
|
||||
{
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 40, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
t.Log(f.dumpIndexString(0, 100))
|
||||
|
||||
checkRetrieveError(t, f, map[uint64]error{
|
||||
0: errOutOfBounds,
|
||||
1: errOutOfBounds,
|
||||
2: errOutOfBounds,
|
||||
3: errOutOfBounds,
|
||||
999999: errOutOfBounds,
|
||||
})
|
||||
checkRetrieve(t, f, map[uint64][]byte{
|
||||
1000000: getChunk(20, 0xbb),
|
||||
1000001: getChunk(20, 0xaa),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkRetrieve(t *testing.T, f *freezerTable, items map[uint64][]byte) {
|
||||
t.Helper()
|
||||
|
||||
for item, wantBytes := range items {
|
||||
value, err := f.Retrieve(item)
|
||||
if err != nil {
|
||||
t.Fatalf("can't get expected item %d: %v", item, err)
|
||||
}
|
||||
if !bytes.Equal(value, wantBytes) {
|
||||
t.Fatalf("item %d has wrong value %x (want %x)", item, value, wantBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkRetrieveError(t *testing.T, f *freezerTable, items map[uint64]error) {
|
||||
t.Helper()
|
||||
|
||||
for item, wantError := range items {
|
||||
value, err := f.Retrieve(item)
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected value %x for item %d, want error %v", item, value, wantError)
|
||||
}
|
||||
if err != wantError {
|
||||
t.Fatalf("wrong error for item %d: %v", item, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gets a chunk of data, filled with 'b'
|
||||
func getChunk(size int, b int) []byte {
|
||||
data := make([]byte, size)
|
||||
for i := range data {
|
||||
data[i] = byte(b)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// TODO (?)
|
||||
@@ -644,53 +707,18 @@ func TestOffset(t *testing.T) {
|
||||
// should be handled already, and the case described above can only (?) happen if an
|
||||
// external process/user deletes files from the filesystem.
|
||||
|
||||
// TestAppendTruncateParallel is a test to check if the Append/truncate operations are
|
||||
// racy.
|
||||
//
|
||||
// The reason why it's not a regular fuzzer, within tests/fuzzers, is that it is dependent
|
||||
// on timing rather than 'clever' input -- there's no determinism.
|
||||
func TestAppendTruncateParallel(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "freezer")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
func writeChunks(t *testing.T, ft *freezerTable, n int, length int) {
|
||||
t.Helper()
|
||||
|
||||
f, err := newCustomTable(dir, "tmp", metrics.NilMeter{}, metrics.NilMeter{}, metrics.NilGauge{}, 8, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fill := func(mark uint64) []byte {
|
||||
data := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(data, mark)
|
||||
return data
|
||||
}
|
||||
|
||||
for i := 0; i < 5000; i++ {
|
||||
f.truncate(0)
|
||||
data0 := fill(0)
|
||||
f.Append(0, data0)
|
||||
data1 := fill(1)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
f.truncate(0)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
f.Append(1, data1)
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
if have, err := f.Retrieve(0); err == nil {
|
||||
if !bytes.Equal(have, data0) {
|
||||
t.Fatalf("have %x want %x", have, data0)
|
||||
}
|
||||
batch := ft.newBatch()
|
||||
for i := 0; i < n; i++ {
|
||||
if err := batch.AppendRaw(uint64(i), getChunk(length, i)); err != nil {
|
||||
t.Fatalf("AppendRaw(%d, ...) returned error: %v", i, err)
|
||||
}
|
||||
}
|
||||
if err := batch.commit(); err != nil {
|
||||
t.Fatalf("Commit returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequentialRead does some basic tests on the RetrieveItems.
|
||||
@@ -698,20 +726,17 @@ func TestSequentialRead(t *testing.T) {
|
||||
rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
|
||||
fname := fmt.Sprintf("batchread-%d", rand.Uint64())
|
||||
{ // Fill table
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write 15 bytes 30 times
|
||||
for x := 0; x < 30; x++ {
|
||||
data := getChunk(15, x)
|
||||
f.Append(uint64(x), data)
|
||||
}
|
||||
writeChunks(t, f, 30, 15)
|
||||
f.DumpIndex(0, 30)
|
||||
f.Close()
|
||||
}
|
||||
{ // Open it, iterate, verify iteration
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -732,7 +757,7 @@ func TestSequentialRead(t *testing.T) {
|
||||
}
|
||||
{ // Open it, iterate, verify byte limit. The byte limit is less than item
|
||||
// size, so each lookup should only return one item
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 40, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 40, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -761,16 +786,13 @@ func TestSequentialReadByteLimit(t *testing.T) {
|
||||
rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
|
||||
fname := fmt.Sprintf("batchread-2-%d", rand.Uint64())
|
||||
{ // Fill table
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 100, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 100, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Write 10 bytes 30 times,
|
||||
// Splitting it at every 100 bytes (10 items)
|
||||
for x := 0; x < 30; x++ {
|
||||
data := getChunk(10, x)
|
||||
f.Append(uint64(x), data)
|
||||
}
|
||||
writeChunks(t, f, 30, 10)
|
||||
f.Close()
|
||||
}
|
||||
for i, tc := range []struct {
|
||||
@@ -786,7 +808,7 @@ func TestSequentialReadByteLimit(t *testing.T) {
|
||||
{100, 109, 10},
|
||||
} {
|
||||
{
|
||||
f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 100, true)
|
||||
f, err := newTable(os.TempDir(), fname, rm, wm, sg, 100, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
// Copyright 2021 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rawdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var freezerTestTableDef = map[string]bool{"test": true}
|
||||
|
||||
func TestFreezerModify(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create test data.
|
||||
var valuesRaw [][]byte
|
||||
var valuesRLP []*big.Int
|
||||
for x := 0; x < 100; x++ {
|
||||
v := getChunk(256, x)
|
||||
valuesRaw = append(valuesRaw, v)
|
||||
iv := big.NewInt(int64(x))
|
||||
iv = iv.Exp(iv, iv, nil)
|
||||
valuesRLP = append(valuesRLP, iv)
|
||||
}
|
||||
|
||||
tables := map[string]bool{"raw": true, "rlp": false}
|
||||
f, dir := newFreezerForTesting(t, tables)
|
||||
defer os.RemoveAll(dir)
|
||||
defer f.Close()
|
||||
|
||||
// Commit test data.
|
||||
_, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
||||
for i := range valuesRaw {
|
||||
if err := op.AppendRaw("raw", uint64(i), valuesRaw[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := op.Append("rlp", uint64(i), valuesRLP[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("ModifyAncients failed:", err)
|
||||
}
|
||||
|
||||
// Dump indexes.
|
||||
for _, table := range f.tables {
|
||||
t.Log(table.name, "index:", table.dumpIndexString(0, int64(len(valuesRaw))))
|
||||
}
|
||||
|
||||
// Read back test data.
|
||||
checkAncientCount(t, f, "raw", uint64(len(valuesRaw)))
|
||||
checkAncientCount(t, f, "rlp", uint64(len(valuesRLP)))
|
||||
for i := range valuesRaw {
|
||||
v, _ := f.Ancient("raw", uint64(i))
|
||||
if !bytes.Equal(v, valuesRaw[i]) {
|
||||
t.Fatalf("wrong raw value at %d: %x", i, v)
|
||||
}
|
||||
ivEnc, _ := f.Ancient("rlp", uint64(i))
|
||||
want, _ := rlp.EncodeToBytes(valuesRLP[i])
|
||||
if !bytes.Equal(ivEnc, want) {
|
||||
t.Fatalf("wrong RLP value at %d: %x", i, ivEnc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This checks that ModifyAncients rolls back freezer updates
|
||||
// when the function passed to it returns an error.
|
||||
func TestFreezerModifyRollback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, dir := newFreezerForTesting(t, freezerTestTableDef)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
theError := errors.New("oops")
|
||||
_, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
||||
// Append three items. This creates two files immediately,
|
||||
// because the table size limit of the test freezer is 2048.
|
||||
require.NoError(t, op.AppendRaw("test", 0, make([]byte, 2048)))
|
||||
require.NoError(t, op.AppendRaw("test", 1, make([]byte, 2048)))
|
||||
require.NoError(t, op.AppendRaw("test", 2, make([]byte, 2048)))
|
||||
return theError
|
||||
})
|
||||
if err != theError {
|
||||
t.Errorf("ModifyAncients returned wrong error %q", err)
|
||||
}
|
||||
checkAncientCount(t, f, "test", 0)
|
||||
f.Close()
|
||||
|
||||
// Reopen and check that the rolled-back data doesn't reappear.
|
||||
tables := map[string]bool{"test": true}
|
||||
f2, err := newFreezer(dir, "", false, 2049, tables)
|
||||
if err != nil {
|
||||
t.Fatalf("can't reopen freezer after failed ModifyAncients: %v", err)
|
||||
}
|
||||
defer f2.Close()
|
||||
checkAncientCount(t, f2, "test", 0)
|
||||
}
|
||||
|
||||
// This test runs ModifyAncients and Ancient concurrently with each other.
|
||||
func TestFreezerConcurrentModifyRetrieve(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, dir := newFreezerForTesting(t, freezerTestTableDef)
|
||||
defer os.RemoveAll(dir)
|
||||
defer f.Close()
|
||||
|
||||
var (
|
||||
numReaders = 5
|
||||
writeBatchSize = uint64(50)
|
||||
written = make(chan uint64, numReaders*6)
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
wg.Add(numReaders + 1)
|
||||
|
||||
// Launch the writer. It appends 10000 items in batches.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer close(written)
|
||||
for item := uint64(0); item < 10000; item += writeBatchSize {
|
||||
_, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
||||
for i := uint64(0); i < writeBatchSize; i++ {
|
||||
item := item + i
|
||||
value := getChunk(32, int(item))
|
||||
if err := op.AppendRaw("test", item, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for i := 0; i < numReaders; i++ {
|
||||
written <- item + writeBatchSize
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Launch the readers. They read random items from the freezer up to the
|
||||
// current frozen item count.
|
||||
for i := 0; i < numReaders; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for frozen := range written {
|
||||
for rc := 0; rc < 80; rc++ {
|
||||
num := uint64(rand.Intn(int(frozen)))
|
||||
value, err := f.Ancient("test", num)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("error reading %d (frozen %d): %v", num, frozen, err))
|
||||
}
|
||||
if !bytes.Equal(value, getChunk(32, int(num))) {
|
||||
panic(fmt.Errorf("wrong value at %d", num))
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// This test runs ModifyAncients and TruncateAncients concurrently with each other.
|
||||
func TestFreezerConcurrentModifyTruncate(t *testing.T) {
|
||||
f, dir := newFreezerForTesting(t, freezerTestTableDef)
|
||||
defer os.RemoveAll(dir)
|
||||
defer f.Close()
|
||||
|
||||
var item = make([]byte, 256)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
// First reset and write 100 items.
|
||||
if err := f.TruncateAncients(0); err != nil {
|
||||
t.Fatal("truncate failed:", err)
|
||||
}
|
||||
_, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
||||
for i := uint64(0); i < 100; i++ {
|
||||
if err := op.AppendRaw("test", i, item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("modify failed:", err)
|
||||
}
|
||||
checkAncientCount(t, f, "test", 100)
|
||||
|
||||
// Now append 100 more items and truncate concurrently.
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
truncateErr error
|
||||
modifyErr error
|
||||
)
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
_, modifyErr = f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
||||
for i := uint64(100); i < 200; i++ {
|
||||
if err := op.AppendRaw("test", i, item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
truncateErr = f.TruncateAncients(10)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
f.AncientSize("test")
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
// Now check the outcome. If the truncate operation went through first, the append
|
||||
// fails, otherwise it succeeds. In either case, the freezer should be positioned
|
||||
// at 10 after both operations are done.
|
||||
if truncateErr != nil {
|
||||
t.Fatal("concurrent truncate failed:", err)
|
||||
}
|
||||
if !(modifyErr == nil || modifyErr == errOutOrderInsertion) {
|
||||
t.Fatal("wrong error from concurrent modify:", modifyErr)
|
||||
}
|
||||
checkAncientCount(t, f, "test", 10)
|
||||
}
|
||||
}
|
||||
|
||||
func newFreezerForTesting(t *testing.T, tables map[string]bool) (*freezer, string) {
|
||||
t.Helper()
|
||||
|
||||
dir, err := ioutil.TempDir("", "freezer")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// note: using low max table size here to ensure the tests actually
|
||||
// switch between multiple files.
|
||||
f, err := newFreezer(dir, "", false, 2049, tables)
|
||||
if err != nil {
|
||||
t.Fatal("can't open freezer", err)
|
||||
}
|
||||
return f, dir
|
||||
}
|
||||
|
||||
// checkAncientCount verifies that the freezer contains n items.
|
||||
func checkAncientCount(t *testing.T, f *freezer, kind string, n uint64) {
|
||||
t.Helper()
|
||||
|
||||
if frozen, _ := f.Ancients(); frozen != n {
|
||||
t.Fatalf("Ancients() returned %d, want %d", frozen, n)
|
||||
}
|
||||
|
||||
// Check at index n-1.
|
||||
if n > 0 {
|
||||
index := n - 1
|
||||
if ok, _ := f.HasAncient(kind, index); !ok {
|
||||
t.Errorf("HasAncient(%q, %d) returned false unexpectedly", kind, index)
|
||||
}
|
||||
if _, err := f.Ancient(kind, index); err != nil {
|
||||
t.Errorf("Ancient(%q, %d) returned unexpected error %q", kind, index, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check at index n.
|
||||
index := n
|
||||
if ok, _ := f.HasAncient(kind, index); ok {
|
||||
t.Errorf("HasAncient(%q, %d) returned true unexpectedly", kind, index)
|
||||
}
|
||||
if _, err := f.Ancient(kind, index); err == nil {
|
||||
t.Errorf("Ancient(%q, %d) didn't return expected error", kind, index)
|
||||
} else if err != errOutOfBounds {
|
||||
t.Errorf("Ancient(%q, %d) returned unexpected error %q", kind, index, err)
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -80,10 +80,9 @@ func (t *table) AncientSize(kind string) (uint64, error) {
|
||||
return t.db.AncientSize(kind)
|
||||
}
|
||||
|
||||
// AppendAncient is a noop passthrough that just forwards the request to the underlying
|
||||
// database.
|
||||
func (t *table) AppendAncient(number uint64, hash, header, body, receipts, td []byte) error {
|
||||
return t.db.AppendAncient(number, hash, header, body, receipts, td)
|
||||
// ModifyAncients runs an ancient write operation on the underlying database.
|
||||
func (t *table) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (int64, error) {
|
||||
return t.db.ModifyAncients(fn)
|
||||
}
|
||||
|
||||
// TruncateAncients is a noop passthrough that just forwards the request to the underlying
|
||||
|
||||
BIN
Binary file not shown.
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
@@ -70,6 +71,9 @@ type Trie interface {
|
||||
// trie.MissingNodeError is returned.
|
||||
TryGet(key []byte) ([]byte, error)
|
||||
|
||||
// TryUpdateAccount abstract an account write in the trie.
|
||||
TryUpdateAccount(key []byte, account *types.StateAccount) error
|
||||
|
||||
// TryUpdate associates key with value in the trie. If value has length zero, any
|
||||
// existing value is deleted from the trie. The value bytes must not be modified
|
||||
// by the caller while they are stored in the trie. If a node was not found in the
|
||||
@@ -86,7 +90,7 @@ type Trie interface {
|
||||
|
||||
// Commit writes all nodes to the trie's memory database, tracking the internal
|
||||
// and external (for account tries) references.
|
||||
Commit(onleaf trie.LeafCallback) (common.Hash, error)
|
||||
Commit(onleaf trie.LeafCallback) (common.Hash, int, error)
|
||||
|
||||
// NodeIterator returns an iterator that returns nodes of the trie. Iteration
|
||||
// starts at the key after the given start key.
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
@@ -140,7 +141,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
|
||||
|
||||
it := trie.NewIterator(s.trie.NodeIterator(conf.Start))
|
||||
for it.Next() {
|
||||
var data Account
|
||||
var data types.StateAccount
|
||||
if err := rlp.DecodeBytes(it.Value, &data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
@@ -104,7 +105,7 @@ func (it *NodeIterator) step() error {
|
||||
return nil
|
||||
}
|
||||
// Otherwise we've reached an account node, initiate data iteration
|
||||
var account Account
|
||||
var account types.StateAccount
|
||||
if err := rlp.Decode(bytes.NewReader(it.stateIt.LeafBlob()), &account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2021 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package state
|
||||
|
||||
import "github.com/ethereum/go-ethereum/metrics"
|
||||
|
||||
var (
|
||||
accountUpdatedMeter = metrics.NewRegisteredMeter("state/update/account", nil)
|
||||
storageUpdatedMeter = metrics.NewRegisteredMeter("state/update/storage", nil)
|
||||
accountDeletedMeter = metrics.NewRegisteredMeter("state/delete/account", nil)
|
||||
storageDeletedMeter = metrics.NewRegisteredMeter("state/delete/storage", nil)
|
||||
accountCommittedMeter = metrics.NewRegisteredMeter("state/commit/account", nil)
|
||||
storageCommittedMeter = metrics.NewRegisteredMeter("state/commit/storage", nil)
|
||||
)
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/state/snapshot"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
@@ -426,7 +425,7 @@ func extractGenesis(db ethdb.Database, stateBloom *stateBloom) error {
|
||||
// If it's a leaf node, yes we are touching an account,
|
||||
// dig into the storage trie further.
|
||||
if accIter.Leaf() {
|
||||
var acc state.Account
|
||||
var acc types.StateAccount
|
||||
if err := rlp.DecodeBytes(accIter.LeafBlob(), &acc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -436,7 +436,7 @@ func (dl *diskLayer) generateRange(root common.Hash, prefix []byte, kind string,
|
||||
for i, key := range result.keys {
|
||||
snapTrie.Update(key, result.vals[i])
|
||||
}
|
||||
root, _ := snapTrie.Commit(nil)
|
||||
root, _, _ := snapTrie.Commit(nil)
|
||||
snapTrieDb.Commit(root, false, nil)
|
||||
}
|
||||
tr := result.tr
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestGeneration(t *testing.T) {
|
||||
acc = &Account{Balance: big.NewInt(3), Root: stTrie.Hash().Bytes(), CodeHash: emptyCode.Bytes()}
|
||||
val, _ = rlp.EncodeToBytes(acc)
|
||||
accTrie.Update([]byte("acc-3"), val) // 0x50815097425d000edfc8b3a4a13e175fc2bdcfee8bdfbf2d1ff61041d3c235b2
|
||||
root, _ := accTrie.Commit(nil) // Root: 0xe3712f1a226f3782caca78ca770ccc19ee000552813a9f59d479f8611db9b1fd
|
||||
root, _, _ := accTrie.Commit(nil) // Root: 0xe3712f1a226f3782caca78ca770ccc19ee000552813a9f59d479f8611db9b1fd
|
||||
triedb.Commit(root, false, nil)
|
||||
|
||||
if have, want := root, common.HexToHash("0xe3712f1a226f3782caca78ca770ccc19ee000552813a9f59d479f8611db9b1fd"); have != want {
|
||||
@@ -128,7 +128,7 @@ func TestGenerateExistentState(t *testing.T) {
|
||||
rawdb.WriteStorageSnapshot(diskdb, hashData([]byte("acc-3")), hashData([]byte("key-2")), []byte("val-2"))
|
||||
rawdb.WriteStorageSnapshot(diskdb, hashData([]byte("acc-3")), hashData([]byte("key-3")), []byte("val-3"))
|
||||
|
||||
root, _ := accTrie.Commit(nil) // Root: 0xe3712f1a226f3782caca78ca770ccc19ee000552813a9f59d479f8611db9b1fd
|
||||
root, _, _ := accTrie.Commit(nil) // Root: 0xe3712f1a226f3782caca78ca770ccc19ee000552813a9f59d479f8611db9b1fd
|
||||
triedb.Commit(root, false, nil)
|
||||
|
||||
snap := generateSnapshot(diskdb, triedb, 16, root)
|
||||
@@ -215,12 +215,12 @@ func (t *testHelper) makeStorageTrie(keys []string, vals []string) []byte {
|
||||
for i, k := range keys {
|
||||
stTrie.Update([]byte(k), []byte(vals[i]))
|
||||
}
|
||||
root, _ := stTrie.Commit(nil)
|
||||
root, _, _ := stTrie.Commit(nil)
|
||||
return root.Bytes()
|
||||
}
|
||||
|
||||
func (t *testHelper) Generate() (common.Hash, *diskLayer) {
|
||||
root, _ := t.accTrie.Commit(nil)
|
||||
root, _, _ := t.accTrie.Commit(nil)
|
||||
t.triedb.Commit(root, false, nil)
|
||||
snap := generateSnapshot(t.diskdb, t.triedb, 16, root)
|
||||
return root, snap
|
||||
@@ -575,7 +575,7 @@ func TestGenerateWithExtraAccounts(t *testing.T) {
|
||||
rawdb.WriteStorageSnapshot(diskdb, key, hashData([]byte("b-key-2")), []byte("b-val-2"))
|
||||
rawdb.WriteStorageSnapshot(diskdb, key, hashData([]byte("b-key-3")), []byte("b-val-3"))
|
||||
}
|
||||
root, _ := accTrie.Commit(nil)
|
||||
root, _, _ := accTrie.Commit(nil)
|
||||
t.Logf("root: %x", root)
|
||||
triedb.Commit(root, false, nil)
|
||||
// To verify the test: If we now inspect the snap db, there should exist extraneous storage items
|
||||
@@ -637,7 +637,7 @@ func TestGenerateWithManyExtraAccounts(t *testing.T) {
|
||||
rawdb.WriteAccountSnapshot(diskdb, key, val)
|
||||
}
|
||||
}
|
||||
root, _ := accTrie.Commit(nil)
|
||||
root, _, _ := accTrie.Commit(nil)
|
||||
t.Logf("root: %x", root)
|
||||
triedb.Commit(root, false, nil)
|
||||
|
||||
@@ -690,7 +690,7 @@ func TestGenerateWithExtraBeforeAndAfter(t *testing.T) {
|
||||
rawdb.WriteAccountSnapshot(diskdb, common.HexToHash("0x07"), val)
|
||||
}
|
||||
|
||||
root, _ := accTrie.Commit(nil)
|
||||
root, _, _ := accTrie.Commit(nil)
|
||||
t.Logf("root: %x", root)
|
||||
triedb.Commit(root, false, nil)
|
||||
|
||||
@@ -734,7 +734,7 @@ func TestGenerateWithMalformedSnapdata(t *testing.T) {
|
||||
rawdb.WriteAccountSnapshot(diskdb, common.HexToHash("0x05"), junk)
|
||||
}
|
||||
|
||||
root, _ := accTrie.Commit(nil)
|
||||
root, _, _ := accTrie.Commit(nil)
|
||||
t.Logf("root: %x", root)
|
||||
triedb.Commit(root, false, nil)
|
||||
|
||||
|
||||
@@ -385,7 +385,7 @@ func (it *diskStorageIterator) Hash() common.Hash {
|
||||
return common.BytesToHash(it.it.Key()) // The prefix will be truncated
|
||||
}
|
||||
|
||||
// Slot returns the raw strorage slot content the iterator is currently at.
|
||||
// Slot returns the raw storage slot content the iterator is currently at.
|
||||
func (it *diskStorageIterator) Slot() []byte {
|
||||
return it.it.Value()
|
||||
}
|
||||
|
||||
+13
-19
@@ -24,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/metrics"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
@@ -65,7 +66,7 @@ func (s Storage) Copy() Storage {
|
||||
type stateObject struct {
|
||||
address common.Address
|
||||
addrHash common.Hash // hash of ethereum address of the account
|
||||
data Account
|
||||
data types.StateAccount
|
||||
db *StateDB
|
||||
|
||||
// DB error.
|
||||
@@ -97,17 +98,8 @@ func (s *stateObject) empty() bool {
|
||||
return s.data.Nonce == 0 && s.data.Balance.Sign() == 0 && bytes.Equal(s.data.CodeHash, emptyCodeHash)
|
||||
}
|
||||
|
||||
// Account is the Ethereum consensus representation of accounts.
|
||||
// These objects are stored in the main account trie.
|
||||
type Account struct {
|
||||
Nonce uint64
|
||||
Balance *big.Int
|
||||
Root common.Hash // merkle root of the storage trie
|
||||
CodeHash []byte
|
||||
}
|
||||
|
||||
// newObject creates a state object.
|
||||
func newObject(db *StateDB, address common.Address, data Account) *stateObject {
|
||||
func newObject(db *StateDB, address common.Address, data types.StateAccount) *stateObject {
|
||||
if data.Balance == nil {
|
||||
data.Balance = new(big.Int)
|
||||
}
|
||||
@@ -130,7 +122,7 @@ func newObject(db *StateDB, address common.Address, data Account) *stateObject {
|
||||
|
||||
// EncodeRLP implements rlp.Encoder.
|
||||
func (s *stateObject) EncodeRLP(w io.Writer) error {
|
||||
return rlp.Encode(w, s.data)
|
||||
return rlp.Encode(w, &s.data)
|
||||
}
|
||||
|
||||
// setError remembers the first non-nil error it is called with.
|
||||
@@ -329,7 +321,7 @@ func (s *stateObject) finalise(prefetch bool) {
|
||||
// It will return nil if the trie has not been loaded and no changes have been made
|
||||
func (s *stateObject) updateTrie(db Database) Trie {
|
||||
// Make sure all dirty slots are finalized into the pending storage area
|
||||
s.finalise(false) // Don't prefetch any more, pull directly if need be
|
||||
s.finalise(false) // Don't prefetch anymore, pull directly if need be
|
||||
if len(s.pendingStorage) == 0 {
|
||||
return s.trie
|
||||
}
|
||||
@@ -354,10 +346,12 @@ func (s *stateObject) updateTrie(db Database) Trie {
|
||||
var v []byte
|
||||
if (value == common.Hash{}) {
|
||||
s.setError(tr.TryDelete(key[:]))
|
||||
s.db.StorageDeleted += 1
|
||||
} else {
|
||||
// Encoding []byte cannot fail, ok to ignore the error.
|
||||
v, _ = rlp.EncodeToBytes(common.TrimLeftZeroes(value[:]))
|
||||
s.setError(tr.TryUpdate(key[:], v))
|
||||
s.db.StorageUpdated += 1
|
||||
}
|
||||
// If state snapshotting is active, cache the data til commit
|
||||
if s.db.snap != nil {
|
||||
@@ -368,7 +362,7 @@ func (s *stateObject) updateTrie(db Database) Trie {
|
||||
s.db.snapStorage[s.addrHash] = storage
|
||||
}
|
||||
}
|
||||
storage[crypto.HashData(hasher, key[:])] = v // v will be nil if value is 0x00
|
||||
storage[crypto.HashData(hasher, key[:])] = v // v will be nil if it's deleted
|
||||
}
|
||||
usedStorage = append(usedStorage, common.CopyBytes(key[:])) // Copy needed for closure
|
||||
}
|
||||
@@ -396,23 +390,23 @@ func (s *stateObject) updateRoot(db Database) {
|
||||
|
||||
// CommitTrie the storage trie of the object to db.
|
||||
// This updates the trie root.
|
||||
func (s *stateObject) CommitTrie(db Database) error {
|
||||
func (s *stateObject) CommitTrie(db Database) (int, error) {
|
||||
// If nothing changed, don't bother with hashing anything
|
||||
if s.updateTrie(db) == nil {
|
||||
return nil
|
||||
return 0, nil
|
||||
}
|
||||
if s.dbErr != nil {
|
||||
return s.dbErr
|
||||
return 0, s.dbErr
|
||||
}
|
||||
// Track the amount of time wasted on committing the storage trie
|
||||
if metrics.EnabledExpensive {
|
||||
defer func(start time.Time) { s.db.StorageCommits += time.Since(start) }(time.Now())
|
||||
}
|
||||
root, err := s.trie.Commit(nil)
|
||||
root, committed, err := s.trie.Commit(nil)
|
||||
if err == nil {
|
||||
s.data.Root = root
|
||||
}
|
||||
return err
|
||||
return committed, err
|
||||
}
|
||||
|
||||
// AddBalance adds amount to s's balance.
|
||||
|
||||
+30
-13
@@ -117,6 +117,11 @@ type StateDB struct {
|
||||
SnapshotAccountReads time.Duration
|
||||
SnapshotStorageReads time.Duration
|
||||
SnapshotCommits time.Duration
|
||||
|
||||
AccountUpdated int
|
||||
StorageUpdated int
|
||||
AccountDeleted int
|
||||
StorageDeleted int
|
||||
}
|
||||
|
||||
// New creates a new state from a given trie.
|
||||
@@ -455,12 +460,7 @@ func (s *StateDB) updateStateObject(obj *stateObject) {
|
||||
}
|
||||
// Encode the account and update the account trie
|
||||
addr := obj.Address()
|
||||
|
||||
data, err := rlp.EncodeToBytes(obj)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't encode object at %x: %v", addr[:], err))
|
||||
}
|
||||
if err = s.trie.TryUpdate(addr[:], data); err != nil {
|
||||
if err := s.trie.TryUpdateAccount(addr[:], &obj.data); err != nil {
|
||||
s.setError(fmt.Errorf("updateStateObject (%x) error: %v", addr[:], err))
|
||||
}
|
||||
|
||||
@@ -507,7 +507,7 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject {
|
||||
}
|
||||
// If no live objects are available, attempt to use snapshots
|
||||
var (
|
||||
data *Account
|
||||
data *types.StateAccount
|
||||
err error
|
||||
)
|
||||
if s.snap != nil {
|
||||
@@ -519,7 +519,7 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject {
|
||||
if acc == nil {
|
||||
return nil
|
||||
}
|
||||
data = &Account{
|
||||
data = &types.StateAccount{
|
||||
Nonce: acc.Nonce,
|
||||
Balance: acc.Balance,
|
||||
CodeHash: acc.CodeHash,
|
||||
@@ -546,7 +546,7 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject {
|
||||
if len(enc) == 0 {
|
||||
return nil
|
||||
}
|
||||
data = new(Account)
|
||||
data = new(types.StateAccount)
|
||||
if err := rlp.DecodeBytes(enc, data); err != nil {
|
||||
log.Error("Failed to decode state object", "addr", addr, "err", err)
|
||||
return nil
|
||||
@@ -583,7 +583,7 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject)
|
||||
s.snapDestructs[prev.addrHash] = struct{}{}
|
||||
}
|
||||
}
|
||||
newobj = newObject(s, addr, Account{})
|
||||
newobj = newObject(s, addr, types.StateAccount{})
|
||||
if prev == nil {
|
||||
s.journal.append(createObjectChange{account: &addr})
|
||||
} else {
|
||||
@@ -860,8 +860,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
|
||||
for addr := range s.stateObjectsPending {
|
||||
if obj := s.stateObjects[addr]; obj.deleted {
|
||||
s.deleteStateObject(obj)
|
||||
s.AccountDeleted += 1
|
||||
} else {
|
||||
s.updateStateObject(obj)
|
||||
s.AccountUpdated += 1
|
||||
}
|
||||
usedAddrs = append(usedAddrs, common.CopyBytes(addr[:])) // Copy needed for closure
|
||||
}
|
||||
@@ -903,6 +905,7 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
|
||||
s.IntermediateRoot(deleteEmptyObjects)
|
||||
|
||||
// Commit objects to the trie, measuring the elapsed time
|
||||
var storageCommitted int
|
||||
codeUpdates := make(map[common.Hash][]byte)
|
||||
codeWriter := s.db.TrieDB().DiskDB().NewBatch()
|
||||
for addr := range s.stateObjectsDirty {
|
||||
@@ -914,9 +917,11 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
|
||||
obj.dirtyCode = false
|
||||
}
|
||||
// Write any storage changes in the state object to its storage trie
|
||||
if err := obj.CommitTrie(s.db); err != nil {
|
||||
committed, err := obj.CommitTrie(s.db)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
storageCommitted += committed
|
||||
}
|
||||
}
|
||||
if len(s.stateObjectsDirty) > 0 {
|
||||
@@ -934,8 +939,8 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
|
||||
}
|
||||
// The onleaf func is called _serially_, so we can reuse the same account
|
||||
// for unmarshalling every time.
|
||||
var account Account
|
||||
root, err := s.trie.Commit(func(_ [][]byte, _ []byte, leaf []byte, parent common.Hash) error {
|
||||
var account types.StateAccount
|
||||
root, accountCommitted, err := s.trie.Commit(func(_ [][]byte, _ []byte, leaf []byte, parent common.Hash) error {
|
||||
if err := rlp.DecodeBytes(leaf, &account); err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -944,8 +949,20 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
if metrics.EnabledExpensive {
|
||||
s.AccountCommits += time.Since(start)
|
||||
|
||||
accountUpdatedMeter.Mark(int64(s.AccountUpdated))
|
||||
storageUpdatedMeter.Mark(int64(s.StorageUpdated))
|
||||
accountDeletedMeter.Mark(int64(s.AccountDeleted))
|
||||
storageDeletedMeter.Mark(int64(s.StorageDeleted))
|
||||
accountCommittedMeter.Mark(int64(accountCommitted))
|
||||
storageCommittedMeter.Mark(int64(storageCommitted))
|
||||
s.AccountUpdated, s.AccountDeleted = 0, 0
|
||||
s.StorageUpdated, s.StorageDeleted = 0, 0
|
||||
}
|
||||
// If snapshotting is enabled, update the snapshot tree with this new version
|
||||
if s.snap != nil {
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ import (
|
||||
"bytes"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
@@ -43,7 +44,7 @@ func NewStateSync(root common.Hash, database ethdb.KeyValueReader, bloom *trie.S
|
||||
return err
|
||||
}
|
||||
}
|
||||
var obj Account
|
||||
var obj types.StateAccount
|
||||
if err := rlp.Decode(bytes.NewReader(leaf), &obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
||||
@@ -203,7 +204,7 @@ func testIterativeStateSync(t *testing.T, count int, commit bool, bypath bool) {
|
||||
}
|
||||
results[len(hashQueue)+i] = trie.SyncResult{Hash: crypto.Keccak256Hash(data), Data: data}
|
||||
} else {
|
||||
var acc Account
|
||||
var acc types.StateAccount
|
||||
if err := rlp.DecodeBytes(srcTrie.Get(path[0]), &acc); err != nil {
|
||||
t.Fatalf("failed to decode account on path %x: %v", path, err)
|
||||
}
|
||||
|
||||
+19
-9
@@ -21,6 +21,8 @@ import (
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
@@ -478,9 +480,15 @@ func (h *priceHeap) Pop() interface{} {
|
||||
// better candidates for inclusion while in other cases (at the top of the baseFee peak)
|
||||
// the floating heap is better. When baseFee is decreasing they behave similarly.
|
||||
type txPricedList struct {
|
||||
all *txLookup // Pointer to the map of all transactions
|
||||
urgent, floating priceHeap // Heaps of prices of all the stored **remote** transactions
|
||||
stales int // Number of stale price points to (re-heap trigger)
|
||||
// Number of stale price points to (re-heap trigger).
|
||||
// This field is accessed atomically, and must be the first field
|
||||
// to ensure it has correct alignment for atomic.AddInt64.
|
||||
// See https://golang.org/pkg/sync/atomic/#pkg-note-BUG.
|
||||
stales int64
|
||||
|
||||
all *txLookup // Pointer to the map of all transactions
|
||||
urgent, floating priceHeap // Heaps of prices of all the stored **remote** transactions
|
||||
reheapMu sync.Mutex // Mutex asserts that only one routine is reheaping the list
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -510,8 +518,8 @@ func (l *txPricedList) Put(tx *types.Transaction, local bool) {
|
||||
// the heap if a large enough ratio of transactions go stale.
|
||||
func (l *txPricedList) Removed(count int) {
|
||||
// Bump the stale counter, but exit if still too low (< 25%)
|
||||
l.stales += count
|
||||
if l.stales <= (len(l.urgent.list)+len(l.floating.list))/4 {
|
||||
stales := atomic.AddInt64(&l.stales, int64(count))
|
||||
if int(stales) <= (len(l.urgent.list)+len(l.floating.list))/4 {
|
||||
return
|
||||
}
|
||||
// Seems we've reached a critical number of stale transactions, reheap
|
||||
@@ -535,7 +543,7 @@ func (l *txPricedList) underpricedFor(h *priceHeap, tx *types.Transaction) bool
|
||||
for len(h.list) > 0 {
|
||||
head := h.list[0]
|
||||
if l.all.GetRemote(head.Hash()) == nil { // Removed or migrated
|
||||
l.stales--
|
||||
atomic.AddInt64(&l.stales, -1)
|
||||
heap.Pop(h)
|
||||
continue
|
||||
}
|
||||
@@ -561,7 +569,7 @@ func (l *txPricedList) Discard(slots int, force bool) (types.Transactions, bool)
|
||||
// Discard stale transactions if found during cleanup
|
||||
tx := heap.Pop(&l.urgent).(*types.Transaction)
|
||||
if l.all.GetRemote(tx.Hash()) == nil { // Removed or migrated
|
||||
l.stales--
|
||||
atomic.AddInt64(&l.stales, -1)
|
||||
continue
|
||||
}
|
||||
// Non stale transaction found, move to floating heap
|
||||
@@ -574,7 +582,7 @@ func (l *txPricedList) Discard(slots int, force bool) (types.Transactions, bool)
|
||||
// Discard stale transactions if found during cleanup
|
||||
tx := heap.Pop(&l.floating).(*types.Transaction)
|
||||
if l.all.GetRemote(tx.Hash()) == nil { // Removed or migrated
|
||||
l.stales--
|
||||
atomic.AddInt64(&l.stales, -1)
|
||||
continue
|
||||
}
|
||||
// Non stale transaction found, discard it
|
||||
@@ -594,8 +602,10 @@ func (l *txPricedList) Discard(slots int, force bool) (types.Transactions, bool)
|
||||
|
||||
// Reheap forcibly rebuilds the heap based on the current remote transaction set.
|
||||
func (l *txPricedList) Reheap() {
|
||||
l.reheapMu.Lock()
|
||||
defer l.reheapMu.Unlock()
|
||||
start := time.Now()
|
||||
l.stales = 0
|
||||
atomic.StoreInt64(&l.stales, 0)
|
||||
l.urgent.list = make([]*types.Transaction, 0, l.all.RemoteCount())
|
||||
l.all.Range(func(hash common.Hash, tx *types.Transaction, local bool) bool {
|
||||
l.urgent.list = append(l.urgent.list, tx)
|
||||
|
||||
+32
-1
@@ -22,6 +22,7 @@ import (
|
||||
"math/big"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
@@ -111,6 +112,14 @@ var (
|
||||
invalidTxMeter = metrics.NewRegisteredMeter("txpool/invalid", nil)
|
||||
underpricedTxMeter = metrics.NewRegisteredMeter("txpool/underpriced", nil)
|
||||
overflowedTxMeter = metrics.NewRegisteredMeter("txpool/overflowed", nil)
|
||||
// throttleTxMeter counts how many transactions are rejected due to too-many-changes between
|
||||
// txpool reorgs.
|
||||
throttleTxMeter = metrics.NewRegisteredMeter("txpool/throttle", nil)
|
||||
// reorgDurationTimer measures how long time a txpool reorg takes.
|
||||
reorgDurationTimer = metrics.NewRegisteredTimer("txpool/reorgtime", nil)
|
||||
// dropBetweenReorgHistogram counts how many drops we experience between two reorg runs. It is expected
|
||||
// that this number is pretty low, since txpool reorgs happen very frequently.
|
||||
dropBetweenReorgHistogram = metrics.NewRegisteredHistogram("txpool/dropbetweenreorg", nil, metrics.NewExpDecaySample(1028, 0.015))
|
||||
|
||||
pendingGauge = metrics.NewRegisteredGauge("txpool/pending", nil)
|
||||
queuedGauge = metrics.NewRegisteredGauge("txpool/queued", nil)
|
||||
@@ -256,6 +265,9 @@ type TxPool struct {
|
||||
reorgDoneCh chan chan struct{}
|
||||
reorgShutdownCh chan struct{} // requests shutdown of scheduleReorgLoop
|
||||
wg sync.WaitGroup // tracks loop, scheduleReorgLoop
|
||||
initDoneCh chan struct{} // is closed once the pool is initialized (for tests)
|
||||
|
||||
changesSinceReorg int // A counter for how many drops we've performed in-between reorg.
|
||||
}
|
||||
|
||||
type txpoolResetRequest struct {
|
||||
@@ -284,6 +296,7 @@ func NewTxPool(config TxPoolConfig, chainconfig *params.ChainConfig, chain block
|
||||
queueTxEventCh: make(chan *types.Transaction),
|
||||
reorgDoneCh: make(chan chan struct{}),
|
||||
reorgShutdownCh: make(chan struct{}),
|
||||
initDoneCh: make(chan struct{}),
|
||||
gasPrice: new(big.Int).SetUint64(config.PriceLimit),
|
||||
}
|
||||
pool.locals = newAccountSet(pool.signer)
|
||||
@@ -337,6 +350,8 @@ func (pool *TxPool) loop() {
|
||||
defer evict.Stop()
|
||||
defer journal.Stop()
|
||||
|
||||
// Notify tests that the init phase is done
|
||||
close(pool.initDoneCh)
|
||||
for {
|
||||
select {
|
||||
// Handle ChainHeadEvent
|
||||
@@ -355,8 +370,8 @@ func (pool *TxPool) loop() {
|
||||
case <-report.C:
|
||||
pool.mu.RLock()
|
||||
pending, queued := pool.stats()
|
||||
stales := pool.priced.stales
|
||||
pool.mu.RUnlock()
|
||||
stales := int(atomic.LoadInt64(&pool.priced.stales))
|
||||
|
||||
if pending != prevPending || queued != prevQueued || stales != prevStales {
|
||||
log.Debug("Transaction pool status report", "executable", pending, "queued", queued, "stales", stales)
|
||||
@@ -663,6 +678,15 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
|
||||
underpricedTxMeter.Mark(1)
|
||||
return false, ErrUnderpriced
|
||||
}
|
||||
// We're about to replace a transaction. The reorg does a more thorough
|
||||
// analysis of what to remove and how, but it runs async. We don't want to
|
||||
// do too many replacements between reorg-runs, so we cap the number of
|
||||
// replacements to 25% of the slots
|
||||
if pool.changesSinceReorg > int(pool.config.GlobalSlots/4) {
|
||||
throttleTxMeter.Mark(1)
|
||||
return false, ErrTxPoolOverflow
|
||||
}
|
||||
|
||||
// New transaction is better than our worse ones, make room for it.
|
||||
// If it's a local transaction, forcibly discard all available transactions.
|
||||
// Otherwise if we can't make enough room for new one, abort the operation.
|
||||
@@ -674,6 +698,8 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
|
||||
overflowedTxMeter.Mark(1)
|
||||
return false, ErrTxPoolOverflow
|
||||
}
|
||||
// Bump the counter of rejections-since-reorg
|
||||
pool.changesSinceReorg += len(drop)
|
||||
// Kick out the underpriced remote transactions.
|
||||
for _, tx := range drop {
|
||||
log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap())
|
||||
@@ -1114,6 +1140,9 @@ func (pool *TxPool) scheduleReorgLoop() {
|
||||
|
||||
// runReorg runs reset and promoteExecutables on behalf of scheduleReorgLoop.
|
||||
func (pool *TxPool) runReorg(done chan struct{}, reset *txpoolResetRequest, dirtyAccounts *accountSet, events map[common.Address]*txSortedMap) {
|
||||
defer func(t0 time.Time) {
|
||||
reorgDurationTimer.Update(time.Since(t0))
|
||||
}(time.Now())
|
||||
defer close(done)
|
||||
|
||||
var promoteAddrs []common.Address
|
||||
@@ -1163,6 +1192,8 @@ func (pool *TxPool) runReorg(done chan struct{}, reset *txpoolResetRequest, dirt
|
||||
highestPending := list.LastElement()
|
||||
pool.pendingNonces.set(addr, highestPending.Nonce()+1)
|
||||
}
|
||||
dropBetweenReorgHistogram.Update(int64(pool.changesSinceReorg))
|
||||
pool.changesSinceReorg = 0 // Reset change counter
|
||||
pool.mu.Unlock()
|
||||
|
||||
// Notify subsystems for newly added transactions
|
||||
|
||||
+33
-30
@@ -24,6 +24,7 @@ import (
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -57,14 +58,14 @@ func init() {
|
||||
}
|
||||
|
||||
type testBlockChain struct {
|
||||
gasLimit uint64 // must be first field for 64 bit alignment (atomic access)
|
||||
statedb *state.StateDB
|
||||
gasLimit uint64
|
||||
chainHeadFeed *event.Feed
|
||||
}
|
||||
|
||||
func (bc *testBlockChain) CurrentBlock() *types.Block {
|
||||
return types.NewBlock(&types.Header{
|
||||
GasLimit: bc.gasLimit,
|
||||
GasLimit: atomic.LoadUint64(&bc.gasLimit),
|
||||
}, nil, nil, nil, trie.NewStackTrie(nil))
|
||||
}
|
||||
|
||||
@@ -118,11 +119,13 @@ func setupTxPool() (*TxPool, *ecdsa.PrivateKey) {
|
||||
|
||||
func setupTxPoolWithConfig(config *params.ChainConfig) (*TxPool, *ecdsa.PrivateKey) {
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 10000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{10000000, statedb, new(event.Feed)}
|
||||
|
||||
key, _ := crypto.GenerateKey()
|
||||
pool := NewTxPool(testTxPoolConfig, config, blockchain)
|
||||
|
||||
// wait for the pool to initialize
|
||||
<-pool.initDoneCh
|
||||
return pool, key
|
||||
}
|
||||
|
||||
@@ -228,7 +231,7 @@ func TestStateChangeDuringTransactionPoolReset(t *testing.T) {
|
||||
|
||||
// setup pool with 2 transaction in it
|
||||
statedb.SetBalance(address, new(big.Int).SetUint64(params.Ether))
|
||||
blockchain := &testChain{&testBlockChain{statedb, 1000000000, new(event.Feed)}, address, &trigger}
|
||||
blockchain := &testChain{&testBlockChain{1000000000, statedb, new(event.Feed)}, address, &trigger}
|
||||
|
||||
tx0 := transaction(0, 100000, key)
|
||||
tx1 := transaction(1, 100000, key)
|
||||
@@ -426,7 +429,7 @@ func TestTransactionChainFork(t *testing.T) {
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
statedb.AddBalance(addr, big.NewInt(100000000000000))
|
||||
|
||||
pool.chain = &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
pool.chain = &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
<-pool.requestReset(nil, nil)
|
||||
}
|
||||
resetState()
|
||||
@@ -455,7 +458,7 @@ func TestTransactionDoubleNonce(t *testing.T) {
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
statedb.AddBalance(addr, big.NewInt(100000000000000))
|
||||
|
||||
pool.chain = &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
pool.chain = &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
<-pool.requestReset(nil, nil)
|
||||
}
|
||||
resetState()
|
||||
@@ -625,7 +628,7 @@ func TestTransactionDropping(t *testing.T) {
|
||||
t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 4)
|
||||
}
|
||||
// Reduce the block gas limit, check that invalidated transactions are dropped
|
||||
pool.chain.(*testBlockChain).gasLimit = 100
|
||||
atomic.StoreUint64(&pool.chain.(*testBlockChain).gasLimit, 100)
|
||||
<-pool.requestReset(nil, nil)
|
||||
|
||||
if _, ok := pool.pending[account].txs.items[tx0.Nonce()]; !ok {
|
||||
@@ -653,7 +656,7 @@ func TestTransactionPostponing(t *testing.T) {
|
||||
|
||||
// Create the pool to test the postponing with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
pool := NewTxPool(testTxPoolConfig, params.TestChainConfig, blockchain)
|
||||
defer pool.Stop()
|
||||
@@ -866,7 +869,7 @@ func testTransactionQueueGlobalLimiting(t *testing.T, nolocals bool) {
|
||||
|
||||
// Create the pool to test the limit enforcement with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
config := testTxPoolConfig
|
||||
config.NoLocals = nolocals
|
||||
@@ -958,7 +961,7 @@ func testTransactionQueueTimeLimiting(t *testing.T, nolocals bool) {
|
||||
|
||||
// Create the pool to test the non-expiration enforcement
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
config := testTxPoolConfig
|
||||
config.Lifetime = time.Second
|
||||
@@ -1143,7 +1146,7 @@ func TestTransactionPendingGlobalLimiting(t *testing.T) {
|
||||
|
||||
// Create the pool to test the limit enforcement with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
config := testTxPoolConfig
|
||||
config.GlobalSlots = config.AccountSlots * 10
|
||||
@@ -1245,7 +1248,7 @@ func TestTransactionCapClearsFromAll(t *testing.T) {
|
||||
|
||||
// Create the pool to test the limit enforcement with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
config := testTxPoolConfig
|
||||
config.AccountSlots = 2
|
||||
@@ -1279,7 +1282,7 @@ func TestTransactionPendingMinimumAllowance(t *testing.T) {
|
||||
|
||||
// Create the pool to test the limit enforcement with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
config := testTxPoolConfig
|
||||
config.GlobalSlots = 1
|
||||
@@ -1327,7 +1330,7 @@ func TestTransactionPoolRepricing(t *testing.T) {
|
||||
|
||||
// Create the pool to test the pricing enforcement with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
pool := NewTxPool(testTxPoolConfig, params.TestChainConfig, blockchain)
|
||||
defer pool.Stop()
|
||||
@@ -1575,7 +1578,7 @@ func TestTransactionPoolRepricingKeepsLocals(t *testing.T) {
|
||||
|
||||
// Create the pool to test the pricing enforcement with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
pool := NewTxPool(testTxPoolConfig, eip1559Config, blockchain)
|
||||
defer pool.Stop()
|
||||
@@ -1648,7 +1651,7 @@ func TestTransactionPoolUnderpricing(t *testing.T) {
|
||||
|
||||
// Create the pool to test the pricing enforcement with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
config := testTxPoolConfig
|
||||
config.GlobalSlots = 2
|
||||
@@ -1754,7 +1757,7 @@ func TestTransactionPoolStableUnderpricing(t *testing.T) {
|
||||
|
||||
// Create the pool to test the pricing enforcement with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
config := testTxPoolConfig
|
||||
config.GlobalSlots = 128
|
||||
@@ -1946,20 +1949,20 @@ func TestDualHeapEviction(t *testing.T) {
|
||||
}
|
||||
|
||||
add := func(urgent bool) {
|
||||
txs := make([]*types.Transaction, 20)
|
||||
for i := range txs {
|
||||
for i := 0; i < 20; i++ {
|
||||
var tx *types.Transaction
|
||||
// Create a test accounts and fund it
|
||||
key, _ := crypto.GenerateKey()
|
||||
testAddBalance(pool, crypto.PubkeyToAddress(key.PublicKey), big.NewInt(1000000000000))
|
||||
if urgent {
|
||||
txs[i] = dynamicFeeTx(0, 100000, big.NewInt(int64(baseFee+1+i)), big.NewInt(int64(1+i)), key)
|
||||
highTip = txs[i]
|
||||
tx = dynamicFeeTx(0, 100000, big.NewInt(int64(baseFee+1+i)), big.NewInt(int64(1+i)), key)
|
||||
highTip = tx
|
||||
} else {
|
||||
txs[i] = dynamicFeeTx(0, 100000, big.NewInt(int64(baseFee+200+i)), big.NewInt(1), key)
|
||||
highCap = txs[i]
|
||||
tx = dynamicFeeTx(0, 100000, big.NewInt(int64(baseFee+200+i)), big.NewInt(1), key)
|
||||
highCap = tx
|
||||
}
|
||||
pool.AddRemotesSync([]*types.Transaction{tx})
|
||||
}
|
||||
pool.AddRemotes(txs)
|
||||
pending, queued := pool.Stats()
|
||||
if pending+queued != 20 {
|
||||
t.Fatalf("transaction count mismatch: have %d, want %d", pending+queued, 10)
|
||||
@@ -1986,7 +1989,7 @@ func TestTransactionDeduplication(t *testing.T) {
|
||||
|
||||
// Create the pool to test the pricing enforcement with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
pool := NewTxPool(testTxPoolConfig, params.TestChainConfig, blockchain)
|
||||
defer pool.Stop()
|
||||
@@ -2052,7 +2055,7 @@ func TestTransactionReplacement(t *testing.T) {
|
||||
|
||||
// Create the pool to test the pricing enforcement with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
pool := NewTxPool(testTxPoolConfig, params.TestChainConfig, blockchain)
|
||||
defer pool.Stop()
|
||||
@@ -2257,7 +2260,7 @@ func testTransactionJournaling(t *testing.T, nolocals bool) {
|
||||
|
||||
// Create the original pool to inject transaction into the journal
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
config := testTxPoolConfig
|
||||
config.NoLocals = nolocals
|
||||
@@ -2299,7 +2302,7 @@ func testTransactionJournaling(t *testing.T, nolocals bool) {
|
||||
// Terminate the old pool, bump the local nonce, create a new pool and ensure relevant transaction survive
|
||||
pool.Stop()
|
||||
statedb.SetNonce(crypto.PubkeyToAddress(local.PublicKey), 1)
|
||||
blockchain = &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain = &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
pool = NewTxPool(config, params.TestChainConfig, blockchain)
|
||||
|
||||
@@ -2326,7 +2329,7 @@ func testTransactionJournaling(t *testing.T, nolocals bool) {
|
||||
pool.Stop()
|
||||
|
||||
statedb.SetNonce(crypto.PubkeyToAddress(local.PublicKey), 1)
|
||||
blockchain = &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain = &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
pool = NewTxPool(config, params.TestChainConfig, blockchain)
|
||||
|
||||
pending, queued = pool.Stats()
|
||||
@@ -2355,7 +2358,7 @@ func TestTransactionStatusCheck(t *testing.T) {
|
||||
|
||||
// Create the pool to test the status retrievals with
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
blockchain := &testBlockChain{statedb, 1000000, new(event.Feed)}
|
||||
blockchain := &testBlockChain{1000000, statedb, new(event.Feed)}
|
||||
|
||||
pool := NewTxPool(testTxPoolConfig, params.TestChainConfig, blockchain)
|
||||
defer pool.Stop()
|
||||
|
||||
@@ -273,9 +273,6 @@ func TestDeriveFields(t *testing.T) {
|
||||
if receipts[i].Logs[j].TxHash != txs[i].Hash() {
|
||||
t.Errorf("receipts[%d].Logs[%d].TxHash = %s, want %s", i, j, receipts[i].Logs[j].TxHash.String(), txs[i].Hash().String())
|
||||
}
|
||||
if receipts[i].Logs[j].TxHash != txs[i].Hash() {
|
||||
t.Errorf("receipts[%d].Logs[%d].TxHash = %s, want %s", i, j, receipts[i].Logs[j].TxHash.String(), txs[i].Hash().String())
|
||||
}
|
||||
if receipts[i].Logs[j].TxIndex != uint(i) {
|
||||
t.Errorf("receipts[%d].Logs[%d].TransactionIndex = %d, want %d", i, j, receipts[i].Logs[j].TxIndex, i)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2021 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
// StateAccount is the Ethereum consensus representation of accounts.
|
||||
// These objects are stored in the main account trie.
|
||||
type StateAccount struct {
|
||||
Nonce uint64
|
||||
Balance *big.Int
|
||||
Root common.Hash // merkle root of the storage trie
|
||||
CodeHash []byte
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2021 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
type devnull struct{ len int }
|
||||
|
||||
func (d *devnull) Write(p []byte) (int, error) {
|
||||
d.len += len(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func BenchmarkEncodeRLP(b *testing.B) {
|
||||
benchRLP(b, true)
|
||||
}
|
||||
|
||||
func BenchmarkDecodeRLP(b *testing.B) {
|
||||
benchRLP(b, false)
|
||||
}
|
||||
|
||||
func benchRLP(b *testing.B, encode bool) {
|
||||
key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
to := common.HexToAddress("0x00000000000000000000000000000000deadbeef")
|
||||
signer := NewLondonSigner(big.NewInt(1337))
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
obj interface{}
|
||||
}{
|
||||
{
|
||||
"legacy-header",
|
||||
&Header{
|
||||
Difficulty: big.NewInt(10000000000),
|
||||
Number: big.NewInt(1000),
|
||||
GasLimit: 8_000_000,
|
||||
GasUsed: 8_000_000,
|
||||
Time: 555,
|
||||
Extra: make([]byte, 32),
|
||||
},
|
||||
},
|
||||
{
|
||||
"london-header",
|
||||
&Header{
|
||||
Difficulty: big.NewInt(10000000000),
|
||||
Number: big.NewInt(1000),
|
||||
GasLimit: 8_000_000,
|
||||
GasUsed: 8_000_000,
|
||||
Time: 555,
|
||||
Extra: make([]byte, 32),
|
||||
BaseFee: big.NewInt(10000000000),
|
||||
},
|
||||
},
|
||||
{
|
||||
"receipt-for-storage",
|
||||
&ReceiptForStorage{
|
||||
Status: ReceiptStatusSuccessful,
|
||||
CumulativeGasUsed: 0x888888888,
|
||||
Logs: make([]*Log, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
"receipt-full",
|
||||
&Receipt{
|
||||
Status: ReceiptStatusSuccessful,
|
||||
CumulativeGasUsed: 0x888888888,
|
||||
Logs: make([]*Log, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
"legacy-transaction",
|
||||
MustSignNewTx(key, signer,
|
||||
&LegacyTx{
|
||||
Nonce: 1,
|
||||
GasPrice: big.NewInt(500),
|
||||
Gas: 1000000,
|
||||
To: &to,
|
||||
Value: big.NewInt(1),
|
||||
}),
|
||||
},
|
||||
{
|
||||
"access-transaction",
|
||||
MustSignNewTx(key, signer,
|
||||
&AccessListTx{
|
||||
Nonce: 1,
|
||||
GasPrice: big.NewInt(500),
|
||||
Gas: 1000000,
|
||||
To: &to,
|
||||
Value: big.NewInt(1),
|
||||
}),
|
||||
},
|
||||
{
|
||||
"1559-transaction",
|
||||
MustSignNewTx(key, signer,
|
||||
&DynamicFeeTx{
|
||||
Nonce: 1,
|
||||
Gas: 1000000,
|
||||
To: &to,
|
||||
Value: big.NewInt(1),
|
||||
GasTipCap: big.NewInt(500),
|
||||
GasFeeCap: big.NewInt(500),
|
||||
}),
|
||||
},
|
||||
} {
|
||||
if encode {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var null = &devnull{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
rlp.Encode(null, tc.obj)
|
||||
}
|
||||
b.SetBytes(int64(null.len / b.N))
|
||||
})
|
||||
} else {
|
||||
data, _ := rlp.EncodeToBytes(tc.obj)
|
||||
// Test decoding
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := rlp.DecodeBytes(data, tc.obj); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.SetBytes(int64(len(data)))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,6 +166,11 @@ func (*AccessListTracer) CaptureFault(env *EVM, pc uint64, op OpCode, gas, cost
|
||||
|
||||
func (*AccessListTracer) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) {}
|
||||
|
||||
func (*AccessListTracer) CaptureEnter(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||
}
|
||||
|
||||
func (*AccessListTracer) CaptureExit(output []byte, gasUsed uint64, err error) {}
|
||||
|
||||
// AccessList returns the current accesslist maintained by the tracer.
|
||||
func (a *AccessListTracer) AccessList() types.AccessList {
|
||||
return a.list.accessList()
|
||||
|
||||
@@ -55,9 +55,12 @@ func TestJumpDestAnalysis(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
const analysisCodeSize = 1200 * 1024
|
||||
|
||||
func BenchmarkJumpdestAnalysis_1200k(bench *testing.B) {
|
||||
// 1.4 ms
|
||||
code := make([]byte, 1200000)
|
||||
code := make([]byte, analysisCodeSize)
|
||||
bench.SetBytes(analysisCodeSize)
|
||||
bench.ResetTimer()
|
||||
for i := 0; i < bench.N; i++ {
|
||||
codeBitmap(code)
|
||||
@@ -66,7 +69,8 @@ func BenchmarkJumpdestAnalysis_1200k(bench *testing.B) {
|
||||
}
|
||||
func BenchmarkJumpdestHashing_1200k(bench *testing.B) {
|
||||
// 4 ms
|
||||
code := make([]byte, 1200000)
|
||||
code := make([]byte, analysisCodeSize)
|
||||
bench.SetBytes(analysisCodeSize)
|
||||
bench.ResetTimer()
|
||||
for i := 0; i < bench.N; i++ {
|
||||
crypto.Keccak256Hash(code)
|
||||
@@ -77,13 +81,19 @@ func BenchmarkJumpdestHashing_1200k(bench *testing.B) {
|
||||
func BenchmarkJumpdestOpAnalysis(bench *testing.B) {
|
||||
var op OpCode
|
||||
bencher := func(b *testing.B) {
|
||||
code := make([]byte, 32*b.N)
|
||||
code := make([]byte, analysisCodeSize)
|
||||
b.SetBytes(analysisCodeSize)
|
||||
for i := range code {
|
||||
code[i] = byte(op)
|
||||
}
|
||||
bits := make(bitvec, len(code)/8+1+4)
|
||||
b.ResetTimer()
|
||||
codeBitmapInternal(code, bits)
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := range bits {
|
||||
bits[j] = 0
|
||||
}
|
||||
codeBitmapInternal(code, bits)
|
||||
}
|
||||
}
|
||||
for op = PUSH1; op <= PUSH32; op++ {
|
||||
bench.Run(op.String(), bencher)
|
||||
|
||||
+53
-12
@@ -193,11 +193,19 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
|
||||
evm.Context.Transfer(evm.StateDB, caller.Address(), addr, value)
|
||||
|
||||
// Capture the tracer start/end events in debug mode
|
||||
if evm.Config.Debug && evm.depth == 0 {
|
||||
evm.Config.Tracer.CaptureStart(evm, caller.Address(), addr, false, input, gas, value)
|
||||
defer func(startGas uint64, startTime time.Time) { // Lazy evaluation of the parameters
|
||||
evm.Config.Tracer.CaptureEnd(ret, startGas-gas, time.Since(startTime), err)
|
||||
}(gas, time.Now())
|
||||
if evm.Config.Debug {
|
||||
if evm.depth == 0 {
|
||||
evm.Config.Tracer.CaptureStart(evm, caller.Address(), addr, false, input, gas, value)
|
||||
defer func(startGas uint64, startTime time.Time) { // Lazy evaluation of the parameters
|
||||
evm.Config.Tracer.CaptureEnd(ret, startGas-gas, time.Since(startTime), err)
|
||||
}(gas, time.Now())
|
||||
} else {
|
||||
// Handle tracer events for entering and exiting a call frame
|
||||
evm.Config.Tracer.CaptureEnter(CALL, caller.Address(), addr, input, gas, value)
|
||||
defer func(startGas uint64) {
|
||||
evm.Config.Tracer.CaptureExit(ret, startGas-gas, err)
|
||||
}(gas)
|
||||
}
|
||||
}
|
||||
|
||||
if isPrecompile {
|
||||
@@ -257,6 +265,14 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
|
||||
}
|
||||
var snapshot = evm.StateDB.Snapshot()
|
||||
|
||||
// Invoke tracer hooks that signal entering/exiting a call frame
|
||||
if evm.Config.Debug {
|
||||
evm.Config.Tracer.CaptureEnter(CALLCODE, caller.Address(), addr, input, gas, value)
|
||||
defer func(startGas uint64) {
|
||||
evm.Config.Tracer.CaptureExit(ret, startGas-gas, err)
|
||||
}(gas)
|
||||
}
|
||||
|
||||
// It is allowed to call precompiles, even via delegatecall
|
||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
||||
ret, gas, err = RunPrecompiledContract(p, input, gas)
|
||||
@@ -293,6 +309,14 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
|
||||
}
|
||||
var snapshot = evm.StateDB.Snapshot()
|
||||
|
||||
// Invoke tracer hooks that signal entering/exiting a call frame
|
||||
if evm.Config.Debug {
|
||||
evm.Config.Tracer.CaptureEnter(DELEGATECALL, caller.Address(), addr, input, gas, nil)
|
||||
defer func(startGas uint64) {
|
||||
evm.Config.Tracer.CaptureExit(ret, startGas-gas, err)
|
||||
}(gas)
|
||||
}
|
||||
|
||||
// It is allowed to call precompiles, even via delegatecall
|
||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
||||
ret, gas, err = RunPrecompiledContract(p, input, gas)
|
||||
@@ -338,6 +362,14 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
|
||||
// future scenarios
|
||||
evm.StateDB.AddBalance(addr, big0)
|
||||
|
||||
// Invoke tracer hooks that signal entering/exiting a call frame
|
||||
if evm.Config.Debug {
|
||||
evm.Config.Tracer.CaptureEnter(STATICCALL, caller.Address(), addr, input, gas, nil)
|
||||
defer func(startGas uint64) {
|
||||
evm.Config.Tracer.CaptureExit(ret, startGas-gas, err)
|
||||
}(gas)
|
||||
}
|
||||
|
||||
if p, isPrecompile := evm.precompile(addr); isPrecompile {
|
||||
ret, gas, err = RunPrecompiledContract(p, input, gas)
|
||||
} else {
|
||||
@@ -377,7 +409,7 @@ func (c *codeAndHash) Hash() common.Hash {
|
||||
}
|
||||
|
||||
// create creates a new contract using code as deployment code.
|
||||
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address) ([]byte, common.Address, uint64, error) {
|
||||
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) {
|
||||
// Depth check execution. Fail if we're trying to execute above the
|
||||
// limit.
|
||||
if evm.depth > int(params.CallCreateDepth) {
|
||||
@@ -415,9 +447,14 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
|
||||
return nil, address, gas, nil
|
||||
}
|
||||
|
||||
if evm.Config.Debug && evm.depth == 0 {
|
||||
evm.Config.Tracer.CaptureStart(evm, caller.Address(), address, true, codeAndHash.code, gas, value)
|
||||
if evm.Config.Debug {
|
||||
if evm.depth == 0 {
|
||||
evm.Config.Tracer.CaptureStart(evm, caller.Address(), address, true, codeAndHash.code, gas, value)
|
||||
} else {
|
||||
evm.Config.Tracer.CaptureEnter(typ, caller.Address(), address, codeAndHash.code, gas, value)
|
||||
}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
ret, err := evm.interpreter.Run(contract, nil, false)
|
||||
@@ -455,8 +492,12 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
|
||||
}
|
||||
}
|
||||
|
||||
if evm.Config.Debug && evm.depth == 0 {
|
||||
evm.Config.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
|
||||
if evm.Config.Debug {
|
||||
if evm.depth == 0 {
|
||||
evm.Config.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
|
||||
} else {
|
||||
evm.Config.Tracer.CaptureExit(ret, gas-contract.Gas, err)
|
||||
}
|
||||
}
|
||||
return ret, address, contract.Gas, err
|
||||
}
|
||||
@@ -464,7 +505,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
|
||||
// Create creates a new contract using code as deployment code.
|
||||
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
|
||||
contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address()))
|
||||
return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr)
|
||||
return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE)
|
||||
}
|
||||
|
||||
// Create2 creates a new contract using code as deployment code.
|
||||
@@ -474,7 +515,7 @@ func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.I
|
||||
func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment *big.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
|
||||
codeAndHash := &codeAndHash{code: code}
|
||||
contractAddr = crypto.CreateAddress2(caller.Address(), salt.Bytes32(), codeAndHash.Hash().Bytes())
|
||||
return evm.create(caller, codeAndHash, gas, endowment, contractAddr)
|
||||
return evm.create(caller, codeAndHash, gas, endowment, contractAddr, CREATE2)
|
||||
}
|
||||
|
||||
// ChainConfig returns the environment's chain configuration
|
||||
|
||||
@@ -60,7 +60,7 @@ func memoryGasCost(mem *Memory, newMemSize uint64) (uint64, error) {
|
||||
// as argument:
|
||||
// CALLDATACOPY (stack position 2)
|
||||
// CODECOPY (stack position 2)
|
||||
// EXTCODECOPY (stack poition 3)
|
||||
// EXTCODECOPY (stack position 3)
|
||||
// RETURNDATACOPY (stack position 2)
|
||||
func memoryCopierGas(stackpos int) gasFunc {
|
||||
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
|
||||
|
||||
@@ -791,6 +791,10 @@ func opSuicide(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]
|
||||
balance := interpreter.evm.StateDB.GetBalance(scope.Contract.Address())
|
||||
interpreter.evm.StateDB.AddBalance(beneficiary.Bytes20(), balance)
|
||||
interpreter.evm.StateDB.Suicide(scope.Contract.Address())
|
||||
if interpreter.cfg.Debug {
|
||||
interpreter.cfg.Tracer.CaptureEnter(SELFDESTRUCT, scope.Contract.Address(), beneficiary.Bytes20(), []byte{}, 0, balance)
|
||||
interpreter.cfg.Tracer.CaptureExit([]byte{}, 0, nil)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
+20
-8
@@ -46,12 +46,12 @@ func (s Storage) Copy() Storage {
|
||||
|
||||
// LogConfig are the configuration options for structured logger the EVM
|
||||
type LogConfig struct {
|
||||
DisableMemory bool // disable memory capture
|
||||
DisableStack bool // disable stack capture
|
||||
DisableStorage bool // disable storage capture
|
||||
DisableReturnData bool // disable return data capture
|
||||
Debug bool // print output during capture end
|
||||
Limit int // maximum length of output, but zero means unlimited
|
||||
EnableMemory bool // enable memory capture
|
||||
DisableStack bool // disable stack capture
|
||||
DisableStorage bool // disable storage capture
|
||||
EnableReturnData bool // enable return data capture
|
||||
Debug bool // print output during capture end
|
||||
Limit int // maximum length of output, but zero means unlimited
|
||||
// Chain overrides, can be used to execute a trace using future fork rules
|
||||
Overrides *params.ChainConfig `json:"overrides,omitempty"`
|
||||
}
|
||||
@@ -106,6 +106,8 @@ func (s *StructLog) ErrorString() string {
|
||||
type Tracer interface {
|
||||
CaptureStart(env *EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int)
|
||||
CaptureState(env *EVM, pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, rData []byte, depth int, err error)
|
||||
CaptureEnter(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int)
|
||||
CaptureExit(output []byte, gasUsed uint64, err error)
|
||||
CaptureFault(env *EVM, pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, depth int, err error)
|
||||
CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error)
|
||||
}
|
||||
@@ -160,7 +162,7 @@ func (l *StructLogger) CaptureState(env *EVM, pc uint64, op OpCode, gas, cost ui
|
||||
}
|
||||
// Copy a snapshot of the current memory state to a new buffer
|
||||
var mem []byte
|
||||
if !l.cfg.DisableMemory {
|
||||
if l.cfg.EnableMemory {
|
||||
mem = make([]byte, len(memory.Data()))
|
||||
copy(mem, memory.Data())
|
||||
}
|
||||
@@ -199,7 +201,7 @@ func (l *StructLogger) CaptureState(env *EVM, pc uint64, op OpCode, gas, cost ui
|
||||
}
|
||||
}
|
||||
var rdata []byte
|
||||
if !l.cfg.DisableReturnData {
|
||||
if l.cfg.EnableReturnData {
|
||||
rdata = make([]byte, len(rData))
|
||||
copy(rdata, rData)
|
||||
}
|
||||
@@ -225,6 +227,11 @@ func (l *StructLogger) CaptureEnd(output []byte, gasUsed uint64, t time.Duration
|
||||
}
|
||||
}
|
||||
|
||||
func (l *StructLogger) CaptureEnter(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||
}
|
||||
|
||||
func (l *StructLogger) CaptureExit(output []byte, gasUsed uint64, err error) {}
|
||||
|
||||
// StructLogs returns the captured log entries.
|
||||
func (l *StructLogger) StructLogs() []StructLog { return l.logs }
|
||||
|
||||
@@ -342,3 +349,8 @@ func (t *mdLogger) CaptureEnd(output []byte, gasUsed uint64, tm time.Duration, e
|
||||
fmt.Fprintf(t.out, "\nOutput: `0x%x`\nConsumed gas: `%d`\nError: `%v`\n",
|
||||
output, gasUsed, err)
|
||||
}
|
||||
|
||||
func (t *mdLogger) CaptureEnter(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||
}
|
||||
|
||||
func (t *mdLogger) CaptureExit(output []byte, gasUsed uint64, err error) {}
|
||||
|
||||
@@ -61,13 +61,13 @@ func (l *JSONLogger) CaptureState(env *EVM, pc uint64, op OpCode, gas, cost uint
|
||||
RefundCounter: env.StateDB.GetRefund(),
|
||||
Err: err,
|
||||
}
|
||||
if !l.cfg.DisableMemory {
|
||||
if l.cfg.EnableMemory {
|
||||
log.Memory = memory.Data()
|
||||
}
|
||||
if !l.cfg.DisableStack {
|
||||
log.Stack = stack.data
|
||||
}
|
||||
if !l.cfg.DisableReturnData {
|
||||
if l.cfg.EnableReturnData {
|
||||
log.ReturnData = rData
|
||||
}
|
||||
l.encoder.Encode(log)
|
||||
@@ -87,3 +87,8 @@ func (l *JSONLogger) CaptureEnd(output []byte, gasUsed uint64, t time.Duration,
|
||||
}
|
||||
l.encoder.Encode(endLog{common.Bytes2Hex(output), math.HexOrDecimal64(gasUsed), t, errMsg})
|
||||
}
|
||||
|
||||
func (l *JSONLogger) CaptureEnter(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||
}
|
||||
|
||||
func (l *JSONLogger) CaptureExit(output []byte, gasUsed uint64, err error) {}
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/eth/tracers"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
@@ -342,11 +343,21 @@ func (s *stepCounter) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, co
|
||||
|
||||
// benchmarkNonModifyingCode benchmarks code, but if the code modifies the
|
||||
// state, this should not be used, since it does not reset the state between runs.
|
||||
func benchmarkNonModifyingCode(gas uint64, code []byte, name string, b *testing.B) {
|
||||
func benchmarkNonModifyingCode(gas uint64, code []byte, name string, tracerCode string, b *testing.B) {
|
||||
cfg := new(Config)
|
||||
setDefaults(cfg)
|
||||
cfg.State, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
cfg.GasLimit = gas
|
||||
if len(tracerCode) > 0 {
|
||||
tracer, err := tracers.New(tracerCode, new(tracers.Context))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
cfg.EVMConfig = vm.Config{
|
||||
Debug: true,
|
||||
Tracer: tracer,
|
||||
}
|
||||
}
|
||||
var (
|
||||
destination = common.BytesToAddress([]byte("contract"))
|
||||
vmenv = NewEnv(cfg)
|
||||
@@ -486,12 +497,12 @@ func BenchmarkSimpleLoop(b *testing.B) {
|
||||
// Tracer: tracer,
|
||||
// }})
|
||||
// 100M gas
|
||||
benchmarkNonModifyingCode(100000000, staticCallIdentity, "staticcall-identity-100M", b)
|
||||
benchmarkNonModifyingCode(100000000, callIdentity, "call-identity-100M", b)
|
||||
benchmarkNonModifyingCode(100000000, loopingCode, "loop-100M", b)
|
||||
benchmarkNonModifyingCode(100000000, callInexistant, "call-nonexist-100M", b)
|
||||
benchmarkNonModifyingCode(100000000, callEOA, "call-EOA-100M", b)
|
||||
benchmarkNonModifyingCode(100000000, calllRevertingContractWithInput, "call-reverting-100M", b)
|
||||
benchmarkNonModifyingCode(100000000, staticCallIdentity, "staticcall-identity-100M", "", b)
|
||||
benchmarkNonModifyingCode(100000000, callIdentity, "call-identity-100M", "", b)
|
||||
benchmarkNonModifyingCode(100000000, loopingCode, "loop-100M", "", b)
|
||||
benchmarkNonModifyingCode(100000000, callInexistant, "call-nonexist-100M", "", b)
|
||||
benchmarkNonModifyingCode(100000000, callEOA, "call-EOA-100M", "", b)
|
||||
benchmarkNonModifyingCode(100000000, calllRevertingContractWithInput, "call-reverting-100M", "", b)
|
||||
|
||||
//benchmarkNonModifyingCode(10000000, staticCallIdentity, "staticcall-identity-10M", b)
|
||||
//benchmarkNonModifyingCode(10000000, loopingCode, "loop-10M", b)
|
||||
@@ -688,3 +699,241 @@ func TestColdAccountAccessCost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeJSTracer(t *testing.T) {
|
||||
jsTracers := []string{
|
||||
`{enters: 0, exits: 0, enterGas: 0, gasUsed: 0, steps:0,
|
||||
step: function() { this.steps++},
|
||||
fault: function() {},
|
||||
result: function() {
|
||||
return [this.enters, this.exits,this.enterGas,this.gasUsed, this.steps].join(",")
|
||||
},
|
||||
enter: function(frame) {
|
||||
this.enters++;
|
||||
this.enterGas = frame.getGas();
|
||||
},
|
||||
exit: function(res) {
|
||||
this.exits++;
|
||||
this.gasUsed = res.getGasUsed();
|
||||
}}`,
|
||||
`{enters: 0, exits: 0, enterGas: 0, gasUsed: 0, steps:0,
|
||||
fault: function() {},
|
||||
result: function() {
|
||||
return [this.enters, this.exits,this.enterGas,this.gasUsed, this.steps].join(",")
|
||||
},
|
||||
enter: function(frame) {
|
||||
this.enters++;
|
||||
this.enterGas = frame.getGas();
|
||||
},
|
||||
exit: function(res) {
|
||||
this.exits++;
|
||||
this.gasUsed = res.getGasUsed();
|
||||
}}`}
|
||||
tests := []struct {
|
||||
code []byte
|
||||
// One result per tracer
|
||||
results []string
|
||||
}{
|
||||
{
|
||||
// CREATE
|
||||
code: []byte{
|
||||
// Store initcode in memory at 0x00 (5 bytes left-padded to 32 bytes)
|
||||
byte(vm.PUSH5),
|
||||
// Init code: PUSH1 0, PUSH1 0, RETURN (3 steps)
|
||||
byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN),
|
||||
byte(vm.PUSH1), 0,
|
||||
byte(vm.MSTORE),
|
||||
// length, offset, value
|
||||
byte(vm.PUSH1), 5, byte(vm.PUSH1), 27, byte(vm.PUSH1), 0,
|
||||
byte(vm.CREATE),
|
||||
byte(vm.POP),
|
||||
},
|
||||
results: []string{`"1,1,4294935775,6,12"`, `"1,1,4294935775,6,0"`},
|
||||
},
|
||||
{
|
||||
// CREATE2
|
||||
code: []byte{
|
||||
// Store initcode in memory at 0x00 (5 bytes left-padded to 32 bytes)
|
||||
byte(vm.PUSH5),
|
||||
// Init code: PUSH1 0, PUSH1 0, RETURN (3 steps)
|
||||
byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN),
|
||||
byte(vm.PUSH1), 0,
|
||||
byte(vm.MSTORE),
|
||||
// salt, length, offset, value
|
||||
byte(vm.PUSH1), 1, byte(vm.PUSH1), 5, byte(vm.PUSH1), 27, byte(vm.PUSH1), 0,
|
||||
byte(vm.CREATE2),
|
||||
byte(vm.POP),
|
||||
},
|
||||
results: []string{`"1,1,4294935766,6,13"`, `"1,1,4294935766,6,0"`},
|
||||
},
|
||||
{
|
||||
// CALL
|
||||
code: []byte{
|
||||
// outsize, outoffset, insize, inoffset
|
||||
byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0,
|
||||
byte(vm.PUSH1), 0, // value
|
||||
byte(vm.PUSH1), 0xbb, //address
|
||||
byte(vm.GAS), // gas
|
||||
byte(vm.CALL),
|
||||
byte(vm.POP),
|
||||
},
|
||||
results: []string{`"1,1,4294964716,6,13"`, `"1,1,4294964716,6,0"`},
|
||||
},
|
||||
{
|
||||
// CALLCODE
|
||||
code: []byte{
|
||||
// outsize, outoffset, insize, inoffset
|
||||
byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0,
|
||||
byte(vm.PUSH1), 0, // value
|
||||
byte(vm.PUSH1), 0xcc, //address
|
||||
byte(vm.GAS), // gas
|
||||
byte(vm.CALLCODE),
|
||||
byte(vm.POP),
|
||||
},
|
||||
results: []string{`"1,1,4294964716,6,13"`, `"1,1,4294964716,6,0"`},
|
||||
},
|
||||
{
|
||||
// STATICCALL
|
||||
code: []byte{
|
||||
// outsize, outoffset, insize, inoffset
|
||||
byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0,
|
||||
byte(vm.PUSH1), 0xdd, //address
|
||||
byte(vm.GAS), // gas
|
||||
byte(vm.STATICCALL),
|
||||
byte(vm.POP),
|
||||
},
|
||||
results: []string{`"1,1,4294964719,6,12"`, `"1,1,4294964719,6,0"`},
|
||||
},
|
||||
{
|
||||
// DELEGATECALL
|
||||
code: []byte{
|
||||
// outsize, outoffset, insize, inoffset
|
||||
byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0,
|
||||
byte(vm.PUSH1), 0xee, //address
|
||||
byte(vm.GAS), // gas
|
||||
byte(vm.DELEGATECALL),
|
||||
byte(vm.POP),
|
||||
},
|
||||
results: []string{`"1,1,4294964719,6,12"`, `"1,1,4294964719,6,0"`},
|
||||
},
|
||||
{
|
||||
// CALL self-destructing contract
|
||||
code: []byte{
|
||||
// outsize, outoffset, insize, inoffset
|
||||
byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.PUSH1), 0,
|
||||
byte(vm.PUSH1), 0, // value
|
||||
byte(vm.PUSH1), 0xff, //address
|
||||
byte(vm.GAS), // gas
|
||||
byte(vm.CALL),
|
||||
byte(vm.POP),
|
||||
},
|
||||
results: []string{`"2,2,0,5003,12"`, `"2,2,0,5003,0"`},
|
||||
},
|
||||
}
|
||||
calleeCode := []byte{
|
||||
byte(vm.PUSH1), 0,
|
||||
byte(vm.PUSH1), 0,
|
||||
byte(vm.RETURN),
|
||||
}
|
||||
depressedCode := []byte{
|
||||
byte(vm.PUSH1), 0xaa,
|
||||
byte(vm.SELFDESTRUCT),
|
||||
}
|
||||
main := common.HexToAddress("0xaa")
|
||||
for i, jsTracer := range jsTracers {
|
||||
for j, tc := range tests {
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
statedb.SetCode(main, tc.code)
|
||||
statedb.SetCode(common.HexToAddress("0xbb"), calleeCode)
|
||||
statedb.SetCode(common.HexToAddress("0xcc"), calleeCode)
|
||||
statedb.SetCode(common.HexToAddress("0xdd"), calleeCode)
|
||||
statedb.SetCode(common.HexToAddress("0xee"), calleeCode)
|
||||
statedb.SetCode(common.HexToAddress("0xff"), depressedCode)
|
||||
|
||||
tracer, err := tracers.New(jsTracer, new(tracers.Context))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, err = Call(main, nil, &Config{
|
||||
State: statedb,
|
||||
EVMConfig: vm.Config{
|
||||
Debug: true,
|
||||
Tracer: tracer,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal("didn't expect error", err)
|
||||
}
|
||||
res, err := tracer.GetResult()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if have, want := string(res), tc.results[i]; have != want {
|
||||
t.Errorf("wrong result for tracer %d testcase %d, have \n%v\nwant\n%v\n", i, j, have, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSTracerCreateTx(t *testing.T) {
|
||||
jsTracer := `
|
||||
{enters: 0, exits: 0,
|
||||
step: function() {},
|
||||
fault: function() {},
|
||||
result: function() { return [this.enters, this.exits].join(",") },
|
||||
enter: function(frame) { this.enters++ },
|
||||
exit: function(res) { this.exits++ }}`
|
||||
code := []byte{byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN)}
|
||||
|
||||
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
tracer, err := tracers.New(jsTracer, new(tracers.Context))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, _, err = Create(code, &Config{
|
||||
State: statedb,
|
||||
EVMConfig: vm.Config{
|
||||
Debug: true,
|
||||
Tracer: tracer,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res, err := tracer.GetResult()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if have, want := string(res), `"0,0"`; have != want {
|
||||
t.Errorf("wrong result for tracer, have \n%v\nwant\n%v\n", have, want)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTracerStepVsCallFrame(b *testing.B) {
|
||||
// Simply pushes and pops some values in a loop
|
||||
code := []byte{
|
||||
byte(vm.JUMPDEST),
|
||||
byte(vm.PUSH1), 0,
|
||||
byte(vm.PUSH1), 0,
|
||||
byte(vm.POP),
|
||||
byte(vm.POP),
|
||||
byte(vm.PUSH1), 0, // jumpdestination
|
||||
byte(vm.JUMP),
|
||||
}
|
||||
|
||||
stepTracer := `
|
||||
{
|
||||
step: function() {},
|
||||
fault: function() {},
|
||||
result: function() {},
|
||||
}`
|
||||
callFrameTracer := `
|
||||
{
|
||||
enter: function() {},
|
||||
exit: function() {},
|
||||
fault: function() {},
|
||||
result: function() {},
|
||||
}`
|
||||
|
||||
benchmarkNonModifyingCode(10000000, code, "tracer-step-10M", stepTracer, b)
|
||||
benchmarkNonModifyingCode(10000000, code, "tracer-call-frame-10M", callFrameTracer, b)
|
||||
}
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ func (st *Stack) Print() {
|
||||
fmt.Println("### stack ###")
|
||||
if len(st.data) > 0 {
|
||||
for i, val := range st.data {
|
||||
fmt.Printf("%-3d %v\n", i, val)
|
||||
fmt.Printf("%-3d %s\n", i, val.String())
|
||||
}
|
||||
} else {
|
||||
fmt.Println("-- empty --")
|
||||
|
||||
Reference in New Issue
Block a user