From 5c2875bc1b2e9828da8d5442353bd9c0939bf1a3 Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Wed, 28 Aug 2024 18:01:36 -0500 Subject: [PATCH 1/6] Trimming: Remove UTXOs at depth to keep set below maximum size --- consensus/blake3pow/consensus.go | 133 +++++++++++++++++++++++++++++-- consensus/consensus.go | 2 +- consensus/progpow/consensus.go | 18 +++-- core/chain_indexer.go | 92 ++++++++++++++++++++- core/headerchain.go | 5 ++ core/rawdb/accessors_chain.go | 105 +++++++++++++++++++++++- core/rawdb/schema.go | 32 +++++++- core/state_processor.go | 88 ++++++++++---------- params/protocol_params.go | 1 + 9 files changed, 416 insertions(+), 60 deletions(-) diff --git a/consensus/blake3pow/consensus.go b/consensus/blake3pow/consensus.go index 3b974e4bfe..479509e715 100644 --- a/consensus/blake3pow/consensus.go +++ b/consensus/blake3pow/consensus.go @@ -16,10 +16,12 @@ import ( "github.com/dominant-strategies/go-quai/core/state" "github.com/dominant-strategies/go-quai/core/types" "github.com/dominant-strategies/go-quai/core/vm" + "github.com/dominant-strategies/go-quai/ethdb" "github.com/dominant-strategies/go-quai/log" "github.com/dominant-strategies/go-quai/multiset" "github.com/dominant-strategies/go-quai/params" "github.com/dominant-strategies/go-quai/trie" + "google.golang.org/protobuf/proto" "modernc.org/mathutil" ) @@ -603,16 +605,17 @@ func (blake3pow *Blake3pow) Prepare(chain consensus.ChainHeaderReader, header *t // Finalize implements consensus.Engine, accumulating the block and uncle rewards, // setting the final state on the header -func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, error) { +func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) { nodeLocation := blake3pow.config.NodeLocation nodeCtx := blake3pow.config.NodeLocation.Context() var multiSet *multiset.MultiSet - if nodeCtx == common.ZONE_CTX && chain.IsGenesisHash(header.ParentHash(nodeCtx)) { + var utxoSetSize uint64 + if chain.IsGenesisHash(header.ParentHash(nodeCtx)) { multiSet = multiset.New() // Create the lockup contract account lockupContract, err := vm.LockupContractAddresses[[2]byte{nodeLocation[0], nodeLocation[1]}].InternalAndQuaiAddress() if err != nil { - return nil, err + return nil, 0, err } state.CreateAccount(lockupContract) state.SetNonce(lockupContract, 1) // so it's not considered "empty" @@ -645,11 +648,49 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, header * chain.WriteAddressOutpoints(addressOutpointMap) blake3pow.logger.Info("Indexed genesis utxos") } - } else { + } else if nodeCtx == common.ZONE_CTX { multiSet = rawdb.ReadMultiSet(chain.Database(), header.ParentHash(nodeCtx)) + utxoSetSize = rawdb.ReadUTXOSetSize(chain.Database(), header.ParentHash(nodeCtx)) } if multiSet == nil { - return nil, fmt.Errorf("Multiset is nil for block %s", header.ParentHash(nodeCtx).String()) + return nil, 0, fmt.Errorf("Multiset is nil for block %s", header.ParentHash(nodeCtx).String()) + } + + utxoSetSize += uint64(len(utxosCreate)) + utxoSetSize -= uint64(len(utxosDelete)) + if utxoSetSize > params.MaxUTXOSetSize { + trimmedUtxos := make([]*types.SpentUtxoEntry, 0) + blake3pow.logger.WithFields(log.Fields{ + "utxoSetSize": utxoSetSize, + "maxUTXOSetSize": params.MaxUTXOSetSize, + }).Info("UTXO set size is greater than the maximum allowed UTXO set size. Begin trimming process.") + lastTrimmedBlockHeight := rawdb.ReadLastTrimmedBlock(chain.Database(), header.ParentHash(nodeCtx)) + if lastTrimmedBlockHeight == 0 { + lastTrimmedBlockHeight++ + } + for utxoSetSize > params.MaxUTXOSetSize { + prevUtxoSetSize := utxoSetSize + + nextBlockToTrim := rawdb.ReadCanonicalHash(chain.Database(), lastTrimmedBlockHeight+1) + + TrimBlock(chain, batch, lastTrimmedBlockHeight+1, nextBlockToTrim, &utxosDelete, &trimmedUtxos, &utxoSetSize, !setRoots, blake3pow.logger) // setRoots is false when we are processing the block + + lastTrimmedBlockHeight++ + if lastTrimmedBlockHeight == header.NumberU64(nodeCtx) { + blake3pow.logger.Fatal("Last trimmed block height is equal to the current block height. This is a bug.") + } + blake3pow.logger.WithFields(log.Fields{ + "prevUtxoSetSize": prevUtxoSetSize, + "utxosDeleted": prevUtxoSetSize - utxoSetSize, + "utxoSetSizeAfter": utxoSetSize, + "maxUTXOSetSize": params.MaxUTXOSetSize, + "lastTrimmedBlock": lastTrimmedBlockHeight, + }).Info("Trimmed block UTXOs") + } + if !setRoots { + rawdb.WriteLastTrimmedBlock(batch, header.Hash(), lastTrimmedBlockHeight) + rawdb.WriteTrimmedUTXOs(batch, header.Hash(), trimmedUtxos) + } } for _, hash := range utxosCreate { multiSet.Add(hash.Bytes()) @@ -657,14 +698,88 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, header * for _, hash := range utxosDelete { multiSet.Remove(hash.Bytes()) } - blake3pow.logger.Infof("Parent hash: %s, header hash: %s, muhash: %s, block height: %d, setroots: %t, UtxosCreated: %d, UtxosDeleted: %d", header.ParentHash(nodeCtx).String(), header.Hash().String(), multiSet.Hash().String(), header.NumberU64(nodeCtx), setRoots, len(utxosCreate), len(utxosDelete)) + blake3pow.logger.Infof("Parent hash: %s, header hash: %s, muhash: %s, block height: %d, setroots: %t, UtxosCreated: %d, UtxosDeleted: %d, UTXO Set Size: %d", header.ParentHash(nodeCtx).String(), header.Hash().String(), multiSet.Hash().String(), header.NumberU64(nodeCtx), setRoots, len(utxosCreate), len(utxosDelete), utxoSetSize) + + if utxoSetSize < uint64(len(utxosDelete)) { + return nil, 0, fmt.Errorf("UTXO set size is less than the number of utxos to delete. This is a bug. UTXO set size: %d, UTXOs to delete: %d", utxoSetSize, len(utxosDelete)) + } + if setRoots { header.Header().SetUTXORoot(multiSet.Hash()) header.Header().SetEVMRoot(state.IntermediateRoot(true)) header.Header().SetEtxSetRoot(state.ETXRoot()) header.Header().SetQuaiStateSize(state.GetQuaiTrieSize()) } - return multiSet, nil + return multiSet, utxoSetSize, nil +} + +type UtxoEntryWithIndex struct { + *types.UtxoEntry + Index uint16 + Key []byte +} + +func TrimBlock(chain consensus.ChainHeaderReader, batch ethdb.Batch, blockHeight uint64, blockHash common.Hash, utxosDelete *[]common.Hash, trimmedUtxos *[]*types.SpentUtxoEntry, utxoSetSize *uint64, deleteFromDb bool, logger *log.Logger) { + utxosCreated, _ := rawdb.ReadCreatedUTXOKeys(chain.Database(), blockHash) + if utxosCreated == nil { + // This is likely always going to be the case, as the prune depth will almost always be shorter than the trim depth + utxosCreated, _ = rawdb.ReadPrunedUTXOKeys(chain.Database(), blockHeight) + } + logger.Infof("UTXOs created in block %d: %d", blockHeight, len(utxosCreated)) + utxos := make(map[common.Hash][]*UtxoEntryWithIndex) + // Start by grabbing all the UTXOs created in the block (that are still in the UTXO set) + for _, key := range utxosCreated { + if len(key) == 0 { + continue + } + it := chain.Database().NewIterator(key, nil) + for it.Next() { + data := it.Value() + if len(data) == 0 { + continue + } + utxoProto := new(types.ProtoTxOut) + if err := proto.Unmarshal(data, utxoProto); err != nil { + logger.Errorf("Failed to unmarshal ProtoTxOut: %+v data: %+v key: %+v", err, data, key) + continue + } + + utxo := new(types.UtxoEntry) + if err := utxo.ProtoDecode(utxoProto); err != nil { + logger.WithFields(log.Fields{ + "key": key, + "data": data, + "err": err, + }).Error("Invalid utxo Proto") + continue + } + txHash, index, err := rawdb.ReverseUtxoKey(it.Key()) + if err != nil { + logger.WithField("err", err).Error("Failed to parse utxo key") + continue + } + utxos[txHash] = append(utxos[txHash], &UtxoEntryWithIndex{utxo, index, it.Key()}) + } + it.Release() + } + + // Next, check if they are eligible for deletion and delete them + for txHash, utxoEntries := range utxos { + blockNumberForTx := rawdb.ReadTxLookupEntry(chain.Database(), txHash) + if blockNumberForTx != nil && *blockNumberForTx != blockHeight { // collision, wrong tx + continue + } + for _, utxo := range utxoEntries { + if utxo.Denomination < types.MaxDenomination { + *utxosDelete = append(*utxosDelete, types.UTXOHash(txHash, utxo.Index, utxo.UtxoEntry)) + if deleteFromDb { + batch.Delete(utxo.Key) + *trimmedUtxos = append(*trimmedUtxos, &types.SpentUtxoEntry{OutPoint: types.OutPoint{txHash, utxo.Index}, UtxoEntry: utxo.UtxoEntry}) + } + *utxoSetSize-- + } + } + } } // FinalizeAndAssemble implements consensus.Engine, accumulating the block and @@ -673,7 +788,9 @@ func (blake3pow *Blake3pow) FinalizeAndAssemble(chain consensus.ChainHeaderReade nodeCtx := blake3pow.config.NodeLocation.Context() if nodeCtx == common.ZONE_CTX && chain.ProcessingState() { // Finalize block - blake3pow.Finalize(chain, header, state, true, utxosCreate, utxosDelete) + if _, _, err := blake3pow.Finalize(chain, nil, header, state, true, utxosCreate, utxosDelete); err != nil { + return nil, err + } } woBody, err := types.NewWorkObjectBody(header.Header(), txs, etxs, uncles, subManifest, receipts, trie.NewStackTrie(nil), nodeCtx) diff --git a/consensus/consensus.go b/consensus/consensus.go index 740f39d849..477d9eaa23 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -189,7 +189,7 @@ type Engine interface { // // Note: The block header and state database might be updated to reflect any // consensus rules that happen at finalization (e.g. block rewards). - Finalize(chain ChainHeaderReader, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, error) + Finalize(chain ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) // FinalizeAndAssemble runs any post-transaction state modifications (e.g. block // rewards) and assembles the final block. diff --git a/consensus/progpow/consensus.go b/consensus/progpow/consensus.go index 7310b8cf0c..fe307279c3 100644 --- a/consensus/progpow/consensus.go +++ b/consensus/progpow/consensus.go @@ -17,6 +17,7 @@ import ( "github.com/dominant-strategies/go-quai/core/state" "github.com/dominant-strategies/go-quai/core/types" "github.com/dominant-strategies/go-quai/core/vm" + "github.com/dominant-strategies/go-quai/ethdb" "github.com/dominant-strategies/go-quai/log" "github.com/dominant-strategies/go-quai/multiset" "github.com/dominant-strategies/go-quai/params" @@ -661,16 +662,17 @@ func (progpow *Progpow) Prepare(chain consensus.ChainHeaderReader, header *types // Finalize implements consensus.Engine, accumulating the block and uncle rewards, // setting the final state on the header -func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, error) { +func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) { nodeLocation := progpow.NodeLocation() nodeCtx := progpow.NodeLocation().Context() var multiSet *multiset.MultiSet + var utxoSetSize uint64 if nodeCtx == common.ZONE_CTX && chain.IsGenesisHash(header.ParentHash(nodeCtx)) { multiSet = multiset.New() // Create the lockup contract account lockupContract, err := vm.LockupContractAddresses[[2]byte{nodeLocation[0], nodeLocation[1]}].InternalAndQuaiAddress() if err != nil { - return nil, err + return nil, 0, err } state.CreateAccount(lockupContract) state.SetNonce(lockupContract, 1) // so it's not considered "empty" @@ -704,9 +706,10 @@ func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, header *type } } else { multiSet = rawdb.ReadMultiSet(chain.Database(), header.ParentHash(nodeCtx)) + utxoSetSize = rawdb.ReadUTXOSetSize(chain.Database(), header.ParentHash(nodeCtx)) } if multiSet == nil { - return nil, fmt.Errorf("Multiset is nil for block %s", header.ParentHash(nodeCtx).String()) + return nil, 0, fmt.Errorf("Multiset is nil for block %s", header.ParentHash(nodeCtx).String()) } for _, hash := range utxosCreate { multiSet.Add(hash.Bytes()) @@ -714,13 +717,18 @@ func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, header *type for _, hash := range utxosDelete { multiSet.Remove(hash.Bytes()) } + if utxoSetSize < uint64(len(utxosDelete)) { + return nil, 0, fmt.Errorf("UTXO set size is less than the number of utxos to delete. This is a bug. UTXO set size: %d, UTXOs to delete: %d", utxoSetSize, len(utxosDelete)) + } + utxoSetSize += uint64(len(utxosCreate)) + utxoSetSize -= uint64(len(utxosDelete)) if setRoots { header.Header().SetUTXORoot(multiSet.Hash()) header.Header().SetEVMRoot(state.IntermediateRoot(true)) header.Header().SetEtxSetRoot(state.ETXRoot()) header.Header().SetQuaiStateSize(state.GetQuaiTrieSize()) } - return multiSet, nil + return multiSet, utxoSetSize, nil } // FinalizeAndAssemble implements consensus.Engine, accumulating the block and @@ -729,7 +737,7 @@ func (progpow *Progpow) FinalizeAndAssemble(chain consensus.ChainHeaderReader, h nodeCtx := progpow.config.NodeLocation.Context() if nodeCtx == common.ZONE_CTX && chain.ProcessingState() { // Finalize block - if _, err := progpow.Finalize(chain, header, state, true, utxosCreate, utxosDelete); err != nil { + if _, _, err := progpow.Finalize(chain, nil, header, state, true, utxosCreate, utxosDelete); err != nil { return nil, err } } diff --git a/core/chain_indexer.go b/core/chain_indexer.go index 7078631edf..2fa96cd6f6 100644 --- a/core/chain_indexer.go +++ b/core/chain_indexer.go @@ -36,6 +36,7 @@ import ( "github.com/dominant-strategies/go-quai/event" "github.com/dominant-strategies/go-quai/log" "github.com/dominant-strategies/go-quai/params" + "google.golang.org/protobuf/proto" ) var PruneDepth = uint64(100000000) // Number of blocks behind in which we begin pruning old block data @@ -108,6 +109,7 @@ type ChainIndexer struct { lock sync.Mutex indexAddressUtxos bool + utxoKeyPrunerChan chan []*types.SpentUtxoEntry } // NewChainIndexer creates a new chain indexer to do background processing on @@ -125,6 +127,7 @@ func NewChainIndexer(chainDb ethdb.Database, indexDb ethdb.Database, backend Cha throttling: throttling, logger: logger, indexAddressUtxos: indexAddressUtxos, + utxoKeyPrunerChan: make(chan []*types.SpentUtxoEntry, 1000), } // Initialize database dependent fields and start the updater c.loadValidSections() @@ -144,6 +147,7 @@ func (c *ChainIndexer) Start(chain ChainIndexerChain, config params.ChainConfig) c.GetBloom = chain.GetBloom c.StateAt = chain.StateAt go c.eventLoop(chain.CurrentHeader(), events, sub, chain.NodeCtx(), config) + go c.UTXOKeyPruner() } // Close tears down all goroutines belonging to the indexer and returns any error @@ -376,8 +380,94 @@ func (c *ChainIndexer) PruneOldBlockData(blockHeight uint64) { rawdb.DeletePendingEtxsRollup(c.chainDb, blockHash) rawdb.DeleteManifest(c.chainDb, blockHash) rawdb.DeletePbCacheBody(c.chainDb, blockHash) - rawdb.DeleteSpentUTXOs(c.chainDb, blockHash) + createdUtxos, _ := rawdb.ReadCreatedUTXOKeys(c.chainDb, blockHash) + createdUtxosToKeep := make([][]byte, 0, len(createdUtxos)) + for _, key := range createdUtxos { + data, _ := c.chainDb.Get(key) + if len(data) == 0 { + // Don't keep it if it doesn't exist + continue + } + utxoProto := new(types.ProtoTxOut) + if err := proto.Unmarshal(data, utxoProto); err != nil { + // Don't keep it if it can't be unmarshaled + continue + } + + utxo := new(types.UtxoEntry) + if err := utxo.ProtoDecode(utxoProto); err != nil { + // Don't keep it if it can't be decoded into UtxoEntry + continue + } + // Reduce key size to 8 bytes + key = key[:8] + createdUtxosToKeep = append(createdUtxosToKeep, key) + } + rawdb.WritePrunedUTXOKeys(c.chainDb, blockHeight, createdUtxosToKeep) rawdb.DeleteCreatedUTXOKeys(c.chainDb, blockHash) + sutxos, err := rawdb.ReadSpentUTXOs(c.chainDb, blockHash) + if err != nil { + c.logger.Error("Failed to read spent utxos", "err", err) + } else { + select { + case c.utxoKeyPrunerChan <- sutxos: + default: + c.logger.Warn("utxoKeyPrunerChan is full, dropping spent utxos") + } + } + rawdb.DeleteSpentUTXOs(c.chainDb, blockHash) +} + +func (c *ChainIndexer) UTXOKeyPruner() { + for { + select { + case errc := <-c.quit: + // Chain indexer terminating, report no failure and abort + errc <- nil + return + case spentUtxos := <-c.utxoKeyPrunerChan: + for _, spentUtxo := range spentUtxos { + blockheight := rawdb.ReadTxLookupEntry(c.chainDb, spentUtxo.TxHash) + if blockheight == nil { + continue + } + utxoKeys, err := rawdb.ReadPrunedUTXOKeys(c.chainDb, *blockheight) + if err != nil || utxoKeys == nil { + c.logger.Errorf("Failed to read pruned utxo keys: height %d err %+v", *blockheight, err) + continue + } + key := rawdb.UtxoKey(spentUtxo.TxHash, spentUtxo.Index) + for i := 0; i < len(utxoKeys); i++ { + if compareMinLength(utxoKeys[i], key) { + // Remove the element by shifting the slice to the left + utxoKeys = append(utxoKeys[:i], utxoKeys[i+1:]...) + break + } else { + i++ // Only increment i if no element was removed + } + } + rawdb.WritePrunedUTXOKeys(c.chainDb, *blockheight, utxoKeys) + + } + } + } +} + +func compareMinLength(a, b []byte) bool { + minLen := len(a) + if len(b) < minLen { + minLen = len(b) + } + + // Compare the slices up to the length of the shorter slice + for i := 0; i < minLen; i++ { + if a[i] != b[i] { + return false + } + } + + // If the slices are identical up to the shorter length, return true + return true } // newHead notifies the indexer about new chain heads and/or reorgs. diff --git a/core/headerchain.go b/core/headerchain.go index 3668bd93e8..5c95ab8f02 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -493,6 +493,11 @@ func (hc *HeaderChain) SetCurrentHeader(head *types.WorkObject) error { if err != nil { return err } + trimmedUtxos, err := rawdb.ReadTrimmedUTXOs(hc.headerDb, prevHeader.Hash()) + if err != nil { + return err + } + sutxos = append(sutxos, trimmedUtxos...) for _, sutxo := range sutxos { rawdb.CreateUTXO(hc.headerDb, sutxo.TxHash, sutxo.Index, sutxo.UtxoEntry) } diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 1aa9862537..0010b6b13e 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -1325,7 +1325,7 @@ func DeleteSpentUTXOs(db ethdb.KeyValueWriter, blockHash common.Hash) { } func WriteCreatedUTXOKeys(db ethdb.KeyValueWriter, blockHash common.Hash, createdUTXOKeys [][]byte) error { - protoKeys := &types.ProtoKeys{} + protoKeys := &types.ProtoKeys{Keys: make([][]byte, 0, len(createdUTXOKeys))} protoKeys.Keys = append(protoKeys.Keys, createdUTXOKeys...) @@ -1354,3 +1354,106 @@ func DeleteCreatedUTXOKeys(db ethdb.KeyValueWriter, blockHash common.Hash) { db.Logger().WithField("err", err).Fatal("Failed to delete created utxo keys") } } + +func ReadUTXOSetSize(db ethdb.Reader, blockHash common.Hash) uint64 { + data, _ := db.Get(utxoSetSizeKey(blockHash)) + if len(data) == 0 { + return 0 + } + return binary.BigEndian.Uint64(data) +} + +func WriteUTXOSetSize(db ethdb.KeyValueWriter, blockHash common.Hash, size uint64) { + data := make([]byte, 8) + binary.BigEndian.PutUint64(data, size) + if err := db.Put(utxoSetSizeKey(blockHash), data); err != nil { + db.Logger().WithField("err", err).Fatal("Failed to store utxo set size") + } +} + +func DeleteUTXOSetSize(db ethdb.KeyValueWriter, blockHash common.Hash) { + if err := db.Delete(utxoSetSizeKey(blockHash)); err != nil { + db.Logger().WithField("err", err).Fatal("Failed to delete utxo set size") + } +} + +func ReadLastTrimmedBlock(db ethdb.Reader, blockHash common.Hash) uint64 { + data, _ := db.Get(lastTrimmedBlockKey(blockHash)) + if len(data) == 0 { + return 0 + } + return binary.BigEndian.Uint64(data) +} + +func WriteLastTrimmedBlock(db ethdb.KeyValueWriter, blockHash common.Hash, blockHeight uint64) { + data := make([]byte, 8) + binary.BigEndian.PutUint64(data, blockHeight) + if err := db.Put(lastTrimmedBlockKey(blockHash), data); err != nil { + db.Logger().WithField("err", err).Fatal("Failed to store last trimmed block") + } +} + +func WritePrunedUTXOKeys(db ethdb.KeyValueWriter, blockHeight uint64, keys [][]byte) error { + protoKeys := &types.ProtoKeys{Keys: make([][]byte, 0, len(keys))} + protoKeys.Keys = append(protoKeys.Keys, keys...) + + data, err := proto.Marshal(protoKeys) + if err != nil { + db.Logger().WithField("err", err).Fatal("Failed to rlp encode utxo") + } + return db.Put(prunedUTXOsKey(blockHeight), data) +} + +func ReadPrunedUTXOKeys(db ethdb.Reader, blockHeight uint64) ([][]byte, error) { + // Try to look up the data in leveldb. + data, _ := db.Get(prunedUTXOsKey(blockHeight)) + if len(data) == 0 { + return nil, nil + } + protoKeys := new(types.ProtoKeys) + if err := proto.Unmarshal(data, protoKeys); err != nil { + return nil, err + } + return protoKeys.Keys, nil +} + +func ReadTrimmedUTXOs(db ethdb.Reader, blockHash common.Hash) ([]*types.SpentUtxoEntry, error) { + // Try to look up the data in leveldb. + data, _ := db.Get(trimmedUTXOsKey(blockHash)) + if len(data) == 0 { + return nil, nil + } + + protoSpentUTXOs := new(types.ProtoSpentUTXOs) + if err := proto.Unmarshal(data, protoSpentUTXOs); err != nil { + return nil, err + } + + spentUTXOs := make([]*types.SpentUtxoEntry, 0, len(protoSpentUTXOs.Sutxos)) + for _, utxoProto := range protoSpentUTXOs.Sutxos { + utxo := new(types.SpentUtxoEntry) + if err := utxo.ProtoDecode(utxoProto); err != nil { + return nil, err + } + spentUTXOs = append(spentUTXOs, utxo) + } + return spentUTXOs, nil +} + +func WriteTrimmedUTXOs(db ethdb.KeyValueWriter, blockHash common.Hash, spentUTXOs []*types.SpentUtxoEntry) error { + protoSpentUTXOs := &types.ProtoSpentUTXOs{} + for _, utxo := range spentUTXOs { + utxoProto, err := utxo.ProtoEncode() + if err != nil { + return err + } + protoSpentUTXOs.Sutxos = append(protoSpentUTXOs.Sutxos, utxoProto) + } + data, err := proto.Marshal(protoSpentUTXOs) + if err != nil { + db.Logger().WithField("err", err).Fatal("Failed to rlp encode utxo") + } + + // And finally, store the data in the database under the appropriate key + return db.Put(trimmedUTXOsKey(blockHash), data) +} diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 518b0957fb..02edbc3ced 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -20,6 +20,7 @@ package rawdb import ( "bytes" "encoding/binary" + "fmt" "github.com/dominant-strategies/go-quai/common" ) @@ -65,6 +66,8 @@ var ( // genesisHashesKey tracks the list of genesis hashes genesisHashesKey = []byte("GenesisHashes") + lastTrimmedBlockPrefix = []byte("ltb") + // Data item prefixes (use single byte to avoid mixing data types, avoid `i`, used for indexes). headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header headerTDSuffix = []byte("t") // headerPrefix + num (uint64 big endian) + hash + headerTDSuffix -> td @@ -83,8 +86,10 @@ var ( multiSetPrefix = []byte("ms") // multiSetPrefix + hash -> multiset utxoPrefix = []byte("ut") // outpointPrefix + hash -> types.Outpoint spentUTXOsPrefix = []byte("sutxo") // spentUTXOsPrefix + hash -> []types.SpentTxOut + trimmedUTXOsPrefix = []byte("tutxo") // trimmedUTXOsPrefix + hash -> []types.SpentTxOut createdUTXOsPrefix = []byte("cutxo") // createdUTXOsPrefix + hash -> []common.Hash - blockBodyPrefix = []byte("b") // blockBodyPrefix + num (uint64 big endian) + hash -> block body + prunedUTXOKeysPrefix = []byte("putxo") // prunedUTXOKeysPrefix + num (uint64 big endian) -> hash + utxoSetSizePrefix = []byte("us") // utxoSetSizePrefix + hash -> uint64 blockReceiptsPrefix = []byte("r") // blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts pendingEtxsPrefix = []byte("pe") // pendingEtxsPrefix + hash -> PendingEtxs at block pendingEtxsRollupPrefix = []byte("pr") // pendingEtxsRollupPrefix + hash -> PendingEtxsRollup at block @@ -303,10 +308,23 @@ func UtxoKey(hash common.Hash, index uint16) []byte { return append(utxoPrefix, append(hash.Bytes(), indexBytes...)...) } +func ReverseUtxoKey(key []byte) (common.Hash, uint16, error) { + if len(key) != len(utxoPrefix)+common.HashLength+2 { + return common.Hash{}, 0, fmt.Errorf("invalid key length %d", len(key)) + } + hash := common.BytesToHash(key[len(utxoPrefix) : common.HashLength+len(utxoPrefix)]) + index := binary.BigEndian.Uint16(key[common.HashLength+len(utxoPrefix):]) + return hash, index, nil +} + func spentUTXOsKey(blockHash common.Hash) []byte { return append(spentUTXOsPrefix, blockHash[:]...) } +func trimmedUTXOsKey(blockHash common.Hash) []byte { + return append(trimmedUTXOsPrefix, blockHash[:]...) +} + func createdUTXOsKey(blockHash common.Hash) []byte { return append(createdUTXOsPrefix, blockHash[:]...) } @@ -314,3 +332,15 @@ func createdUTXOsKey(blockHash common.Hash) []byte { func multiSetKey(hash common.Hash) []byte { return append(multiSetPrefix, hash.Bytes()...) } + +func utxoSetSizeKey(hash common.Hash) []byte { + return append(utxoSetSizePrefix, hash.Bytes()...) +} + +func prunedUTXOsKey(blockHeight uint64) []byte { + return append(prunedUTXOKeysPrefix, encodeBlockNumber(blockHeight)...) +} + +func lastTrimmedBlockKey(hash common.Hash) []byte { + return append(lastTrimmedBlockPrefix, hash.Bytes()...) +} diff --git a/core/state_processor.go b/core/state_processor.go index 983ee44233..dc522441c1 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -219,7 +219,7 @@ type UtxosCreatedDeleted struct { // Process returns the receipts and logs accumulated during the process and // returns the amount of gas that was used in the process. If any of the // transactions failed to execute due to insufficient gas it will return an error. -func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (types.Receipts, []*types.Transaction, []*types.Log, *state.StateDB, uint64, uint64, *multiset.MultiSet, error) { +func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (types.Receipts, []*types.Transaction, []*types.Log, *state.StateDB, uint64, uint64, uint64, *multiset.MultiSet, error) { var ( receipts types.Receipts usedGas = new(uint64) @@ -235,7 +235,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty start := time.Now() parent := p.hc.GetBlock(block.ParentHash(nodeCtx), block.NumberU64(nodeCtx)-1) if parent == nil { - return types.Receipts{}, []*types.Transaction{}, []*types.Log{}, nil, 0, 0, nil, errors.New("parent block is nil for the block given to process") + return types.Receipts{}, []*types.Transaction{}, []*types.Log{}, nil, 0, 0, 0, nil, errors.New("parent block is nil for the block given to process") } time1 := common.PrettyDuration(time.Since(start)) @@ -250,14 +250,14 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty // Initialize a statedb statedb, err := state.New(parentEvmRoot, parentEtxSetRoot, parentQuaiStateSize, p.stateCache, p.etxCache, p.snaps, nodeLocation, p.logger) if err != nil { - return types.Receipts{}, []*types.Transaction{}, []*types.Log{}, nil, 0, 0, nil, err + return types.Receipts{}, []*types.Transaction{}, []*types.Log{}, nil, 0, 0, 0, nil, err } utxosCreatedDeleted := new(UtxosCreatedDeleted) // utxos created and deleted in this block // Apply the previous inbound ETXs to the ETX set state prevInboundEtxs := rawdb.ReadInboundEtxs(p.hc.bc.db, header.ParentHash(nodeCtx)) if len(prevInboundEtxs) > 0 { if err := statedb.PushETXs(prevInboundEtxs); err != nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("could not push prev inbound etxs: %w", err) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not push prev inbound etxs: %w", err) } } time2 := common.PrettyDuration(time.Since(start)) @@ -287,7 +287,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty blockContext, err := NewEVMBlockContext(header, parent, p.hc, nil) if err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, p.vmConfig) time3 := common.PrettyDuration(time.Since(start)) @@ -318,7 +318,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty primeTerminus := p.hc.GetHeaderByHash(header.PrimeTerminusHash()) if primeTerminus == nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("could not find prime terminus header %032x", header.PrimeTerminusHash()) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not find prime terminus header %032x", header.PrimeTerminusHash()) } // Set the min gas price to the lowest gas price in the transaction If that // value is not the basefee mentioned in the block, the block is invalid In @@ -336,7 +336,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } qiTxFee, fees, etxs, err, timing := ProcessQiTx(tx, p.hc, checkSig, firstQiTx, header, batch, p.hc.headerDb, gp, usedGas, p.hc.pool.signer, p.hc.NodeLocation(), *p.config.ChainID, &etxRLimit, &etxPLimit, utxosCreatedDeleted) if err != nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } firstQiTx = false startEtxAppend := time.Now() @@ -375,7 +375,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty msg, err := tx.AsMessageWithSender(types.MakeSigner(p.config, header.Number(nodeCtx)), header.BaseFee(), senders[tx.Hash()]) if err != nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } timeSignDelta := time.Since(startProcess) timeSign += timeSignDelta @@ -392,13 +392,13 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty // ETXs MUST be included in order, so popping the first from the queue must equal the first in the block etx, err := statedb.PopETX() if err != nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("could not pop etx from statedb: %w", err) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not pop etx from statedb: %w", err) } if etx == nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("etx %x is nil", tx.Hash()) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("etx %x is nil", tx.Hash()) } if etx.Hash() != tx.Hash() { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("invalid external transaction: etx %x is not in order or not found in unspent etx set", tx.Hash()) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("invalid external transaction: etx %x is not in order or not found in unspent etx set", tx.Hash()) } // check if the tx is a coinbase tx @@ -409,22 +409,22 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty // 4) etx emit threshold numbers if types.IsCoinBaseTx(tx) { if tx.To() == nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("coinbase tx %x has no recipient", tx.Hash()) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("coinbase tx %x has no recipient", tx.Hash()) } if len(tx.Data()) == 0 { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("coinbase tx %x has no lockup byte", tx.Hash()) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("coinbase tx %x has no lockup byte", tx.Hash()) } if _, err := tx.To().InternalAddress(); err != nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("coinbase tx %x has invalid recipient: %w", tx.Hash(), err) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("coinbase tx %x has invalid recipient: %w", tx.Hash(), err) } lockupByte := tx.Data()[0] if tx.To().IsInQiLedgerScope() { if int(lockupByte) > len(params.LockupByteToBlockDepth) { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("coinbase lockup byte %d is out of range", lockupByte) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("coinbase lockup byte %d is out of range", lockupByte) } lockup := new(big.Int).SetUint64(params.LockupByteToBlockDepth[lockupByte]) if lockup.Uint64() < params.ConversionLockPeriod { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.ConversionLockPeriod) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.ConversionLockPeriod) } value := params.CalculateCoinbaseValueWithLockup(tx.Value(), lockupByte) denominations := misc.FindMinDenominations(value) @@ -443,7 +443,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty utxo := types.NewUtxoEntry(types.NewTxOut(uint8(denomination), tx.To().Bytes(), lockup)) // the ETX hash is guaranteed to be unique if err := rawdb.CreateUTXO(batch, etx.Hash(), outputIndex, utxo); err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, types.UTXOHash(etx.Hash(), outputIndex, utxo)) utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKey(etx.Hash(), outputIndex)) @@ -465,7 +465,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } // subtract the minimum tx gas from the gas pool if err := gp.SubGas(params.TxGas); err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } *usedGas += params.TxGas totalEtxGas += params.TxGas @@ -478,7 +478,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty lock := new(big.Int).Add(header.Number(nodeCtx), new(big.Int).SetUint64(params.ConversionLockPeriod)) primeTerminus := p.hc.GetHeaderByHash(header.PrimeTerminusHash()) if primeTerminus == nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("could not find prime terminus header %032x", header.PrimeTerminusHash()) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not find prime terminus header %032x", header.PrimeTerminusHash()) } value := misc.QuaiToQi(primeTerminus.WorkObjectHeader(), etx.Value()) // convert Quai to Qi txGas := etx.Gas() @@ -487,7 +487,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } txGas -= params.TxGas if err := gp.SubGas(params.TxGas); err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } *usedGas += params.TxGas totalEtxGas += params.TxGas @@ -506,14 +506,14 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } txGas -= params.CallValueTransferGas if err := gp.SubGas(params.CallValueTransferGas); err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } *usedGas += params.CallValueTransferGas // In the future we may want to determine what a fair gas cost is totalEtxGas += params.CallValueTransferGas // In the future we may want to determine what a fair gas cost is utxo := types.NewUtxoEntry(types.NewTxOut(uint8(denomination), etx.To().Bytes(), lock)) // the ETX hash is guaranteed to be unique if err := rawdb.CreateUTXO(batch, etx.Hash(), outputIndex, utxo); err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, types.UTXOHash(etx.Hash(), outputIndex, utxo)) utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKey(etx.Hash(), outputIndex)) @@ -525,13 +525,13 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty utxo := types.NewUtxoEntry(types.NewTxOut(uint8(etx.Value().Uint64()), etx.To().Bytes(), big.NewInt(0))) // There are no more checks to be made as the ETX is worked so add it to the set if err := rawdb.CreateUTXO(batch, etx.OriginatingTxHash(), etx.ETXIndex(), utxo); err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, types.UTXOHash(etx.OriginatingTxHash(), etx.ETXIndex(), utxo)) utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKey(etx.OriginatingTxHash(), etx.ETXIndex())) // This Qi ETX should cost more gas if err := gp.SubGas(params.CallValueTransferGas); err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } *usedGas += params.CallValueTransferGas // In the future we may want to determine what a fair gas cost is totalEtxGas += params.CallValueTransferGas // In the future we may want to determine what a fair gas cost is @@ -551,7 +551,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty receipt, fees, err = applyTransaction(msg, parent, p.config, p.hc, gp, statedb, blockNumber, blockHash, etx, usedGas, usedState, vmenv, &etxRLimit, &etxPLimit, p.logger) statedb.SetBalance(common.ZeroInternal(nodeLocation), prevZeroBal) // Reset the balance to what it previously was. Residual balance will be lost if err != nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } addReceipt = true @@ -567,7 +567,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty fees := big.NewInt(0) receipt, fees, err = applyTransaction(msg, parent, p.config, p.hc, gp, statedb, blockNumber, blockHash, tx, usedGas, usedState, vmenv, &etxRLimit, &etxPLimit, p.logger) if err != nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } addReceipt = true timeTxDelta := time.Since(startTimeTx) @@ -586,7 +586,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } } else { - return nil, nil, nil, nil, 0, 0, nil, ErrTxTypeNotSupported + return nil, nil, nil, nil, 0, 0, 0, nil, ErrTxTypeNotSupported } for _, etx := range receipt.OutboundEtxs { if receipt.Status == types.ReceiptStatusSuccessful { @@ -601,29 +601,29 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } if nonEtxExists && block.BaseFee().Cmp(big.NewInt(0)) == 0 { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("block base fee is nil though non etx transactions exist") + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("block base fee is nil though non etx transactions exist") } if minGasPrice != nil && block.BaseFee().Cmp(minGasPrice) != 0 { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("invalid base fee used (remote: %d local: %d)", block.BaseFee(), minGasPrice) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("invalid base fee used (remote: %d local: %d)", block.BaseFee(), minGasPrice) } etxAvailable := false oldestIndex, err := statedb.GetOldestIndex() if err != nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("could not get oldest index: %w", err) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not get oldest index: %w", err) } // Check if there is at least one ETX in the set etx, err := statedb.ReadETX(oldestIndex) if err != nil { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("could not read etx: %w", err) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not read etx: %w", err) } if etx != nil { etxAvailable = true } if (etxAvailable && totalEtxGas < minimumEtxGas) || totalEtxGas > maximumEtxGas { p.logger.Errorf("prevInboundEtxs: %d, oldestIndex: %d, etxHash: %s", len(prevInboundEtxs), oldestIndex.Int64(), etx.Hash().Hex()) - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("total gas used by ETXs %d is not within the range %d to %d", totalEtxGas, minimumEtxGas, maximumEtxGas) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("total gas used by ETXs %d is not within the range %d to %d", totalEtxGas, minimumEtxGas, maximumEtxGas) } lockupContractAddress := vm.LockupContractAddresses[[2]byte{nodeLocation[0], nodeLocation[1]}] for coinbaseAddrWithLockup, value := range quaiCoinbaseEtxs { @@ -631,23 +631,23 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty lockupByte := coinbaseAddrWithLockup[20] lockup := new(big.Int).SetUint64(params.LockupByteToBlockDepth[lockupByte]) if lockup.Uint64() < params.ConversionLockPeriod { - return nil, nil, nil, nil, 0, 0, nil, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.ConversionLockPeriod) + return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.ConversionLockPeriod) } value := params.CalculateCoinbaseValueWithLockup(value, lockupByte) gasUsed, _, err := vm.AddNewLock(parent.QuaiStateSize(), statedb, addr, new(types.GasPool).AddGas(math.MaxUint64), lockup, value, lockupContractAddress) if err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } p.logger.Debugf("Creating Lockup for coinbase addr %s with value %d lock %d gasUsed %d\n", addr.String(), value.Uint64(), lockup.Int64(), gasUsed) } quaiCoinbase, err := block.QuaiCoinbase() if err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } qiCoinbase, err := block.QiCoinbase() if err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } primaryCoinbase := block.PrimaryCoinbase() @@ -688,9 +688,9 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty time4 := common.PrettyDuration(time.Since(start)) // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) - multiSet, err := p.engine.Finalize(p.hc, block, statedb, false, utxosCreatedDeleted.UtxosCreatedHashes, utxosCreatedDeleted.UtxosDeletedHashes) + multiSet, utxoSetSize, err := p.engine.Finalize(p.hc, batch, block, statedb, false, utxosCreatedDeleted.UtxosCreatedHashes, utxosCreatedDeleted.UtxosDeletedHashes) if err != nil { - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } time5 := common.PrettyDuration(time.Since(start)) @@ -731,12 +731,13 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty "numTxs": len(block.Transactions()), }).Info("Total Tx Processing Time") if err := rawdb.WriteSpentUTXOs(batch, blockHash, utxosCreatedDeleted.UtxosDeleted); err != nil { // Could do this in Apply instead - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } if err := rawdb.WriteCreatedUTXOKeys(batch, blockHash, utxosCreatedDeleted.UtxosCreatedKeys); err != nil { // Could do this in Apply instead - return nil, nil, nil, nil, 0, 0, nil, err + return nil, nil, nil, nil, 0, 0, 0, nil, err } - return receipts, emittedEtxs, allLogs, statedb, *usedGas, *usedState, multiSet, nil + utxosCreatedDeleted = nil // Release memory + return receipts, emittedEtxs, allLogs, statedb, *usedGas, *usedState, utxoSetSize, multiSet, nil } func applyTransaction(msg types.Message, parent *types.WorkObject, config *params.ChainConfig, bc ChainContext, gp *types.GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, usedState *uint64, evm *vm.EVM, etxRLimit, etxPLimit *int, logger *log.Logger) (*types.Receipt, *big.Int, error) { @@ -1307,7 +1308,7 @@ func (p *StateProcessor) Apply(batch ethdb.Batch, block *types.WorkObject) ([]*t time1 := common.PrettyDuration(time.Since(start)) time2 := common.PrettyDuration(time.Since(start)) // Process our block - receipts, etxs, logs, statedb, usedGas, usedState, multiSet, err := p.Process(block, batch) + receipts, etxs, logs, statedb, usedGas, usedState, utxoSetSize, multiSet, err := p.Process(block, batch) if err != nil { return nil, err } @@ -1363,6 +1364,7 @@ func (p *StateProcessor) Apply(batch ethdb.Batch, block *types.WorkObject) ([]*t "t8": time8, }).Info("times during state processor apply") rawdb.WriteMultiSet(batch, block.Hash(), multiSet) + rawdb.WriteUTXOSetSize(batch, block.Hash(), utxoSetSize) // Indicate that we have processed the state of the block rawdb.WriteProcessedState(batch, block.Hash()) return logs, nil @@ -1607,7 +1609,7 @@ func (p *StateProcessor) StateAtBlock(block *types.WorkObject, reexec uint64, ba if currentBlock == nil { return nil, errors.New("detached block found trying to regenerate state") } - _, _, _, _, _, _, _, err := p.Process(currentBlock, batch) + _, _, _, _, _, _, _, _, err := p.Process(currentBlock, batch) if err != nil { return nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(nodeCtx), err) } diff --git a/params/protocol_params.go b/params/protocol_params.go index 88bb068412..b98dfc2cae 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -153,6 +153,7 @@ const ( ConversionLockPeriod uint64 = 10 // The number of zone blocks that a conversion output is locked for MinQiConversionDenomination = 1 ConversionConfirmationContext = common.PRIME_CTX // A conversion requires a single coincident Dom confirmation + MaxUTXOSetSize = 1000000 // The maximum number of UTXOs that can be stored in the UTXO set ) var ( From b014764488cc153ed8ac7cdb4202444512c9ad79 Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Thu, 29 Aug 2024 15:23:08 -0500 Subject: [PATCH 2/6] Trim denominations based on trim depths and adjust trim depths based on UTXO set size Disable on-line utxo key pruning --- common/proto_common.pb.go | 2 +- consensus/blake3pow/consensus.go | 104 +++++++---- core/chain_indexer.go | 67 +------ core/headerchain.go | 10 +- core/rawdb/accessors_chain.go | 40 +++++ core/rawdb/db.pb.go | 2 +- core/rawdb/schema.go | 5 + core/state_processor.go | 1 - core/types/proto_block.pb.go | 186 ++++++++++++++------ core/types/proto_block.proto | 4 + core/types/utxo.go | 13 ++ p2p/node/peerManager/peerdb/peer_info.pb.go | 2 +- p2p/pb/quai_messages.pb.go | 2 +- 13 files changed, 268 insertions(+), 170 deletions(-) diff --git a/common/proto_common.pb.go b/common/proto_common.pb.go index eb90de7fbc..9ffbc5669f 100644 --- a/common/proto_common.pb.go +++ b/common/proto_common.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2 -// protoc v5.28.1 +// protoc v5.28.2 // source: common/proto_common.proto package common diff --git a/consensus/blake3pow/consensus.go b/consensus/blake3pow/consensus.go index 479509e715..5cf7e6266a 100644 --- a/consensus/blake3pow/consensus.go +++ b/consensus/blake3pow/consensus.go @@ -648,7 +648,7 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch et chain.WriteAddressOutpoints(addressOutpointMap) blake3pow.logger.Info("Indexed genesis utxos") } - } else if nodeCtx == common.ZONE_CTX { + } else { multiSet = rawdb.ReadMultiSet(chain.Database(), header.ParentHash(nodeCtx)) utxoSetSize = rawdb.ReadUTXOSetSize(chain.Database(), header.ParentHash(nodeCtx)) } @@ -658,47 +658,43 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch et utxoSetSize += uint64(len(utxosCreate)) utxoSetSize -= uint64(len(utxosDelete)) - if utxoSetSize > params.MaxUTXOSetSize { - trimmedUtxos := make([]*types.SpentUtxoEntry, 0) - blake3pow.logger.WithFields(log.Fields{ - "utxoSetSize": utxoSetSize, - "maxUTXOSetSize": params.MaxUTXOSetSize, - }).Info("UTXO set size is greater than the maximum allowed UTXO set size. Begin trimming process.") - lastTrimmedBlockHeight := rawdb.ReadLastTrimmedBlock(chain.Database(), header.ParentHash(nodeCtx)) - if lastTrimmedBlockHeight == 0 { - lastTrimmedBlockHeight++ - } - for utxoSetSize > params.MaxUTXOSetSize { - prevUtxoSetSize := utxoSetSize - - nextBlockToTrim := rawdb.ReadCanonicalHash(chain.Database(), lastTrimmedBlockHeight+1) - - TrimBlock(chain, batch, lastTrimmedBlockHeight+1, nextBlockToTrim, &utxosDelete, &trimmedUtxos, &utxoSetSize, !setRoots, blake3pow.logger) // setRoots is false when we are processing the block - - lastTrimmedBlockHeight++ - if lastTrimmedBlockHeight == header.NumberU64(nodeCtx) { - blake3pow.logger.Fatal("Last trimmed block height is equal to the current block height. This is a bug.") + trimDepths := types.TrimDepths + if utxoSetSize > params.MaxUTXOSetSize/2 { + var err error + trimDepths, err = rawdb.ReadTrimDepths(chain.Database(), header.ParentHash(nodeCtx)) + if err != nil || trimDepths == nil { + blake3pow.logger.Errorf("Failed to read trim depths for block %s: %+v", header.ParentHash(nodeCtx).String(), err) + trimDepths = make(map[uint8]uint64, len(types.TrimDepths)) + for denomination, depth := range types.TrimDepths { // copy the default trim depths + trimDepths[denomination] = depth } - blake3pow.logger.WithFields(log.Fields{ - "prevUtxoSetSize": prevUtxoSetSize, - "utxosDeleted": prevUtxoSetSize - utxoSetSize, - "utxoSetSizeAfter": utxoSetSize, - "maxUTXOSetSize": params.MaxUTXOSetSize, - "lastTrimmedBlock": lastTrimmedBlockHeight, - }).Info("Trimmed block UTXOs") + } + if UpdateTrimDepths(trimDepths, utxoSetSize) { + blake3pow.logger.Infof("Updated trim depths at height %d new depths: %+v", header.NumberU64(nodeCtx), trimDepths) } if !setRoots { - rawdb.WriteLastTrimmedBlock(batch, header.Hash(), lastTrimmedBlockHeight) - rawdb.WriteTrimmedUTXOs(batch, header.Hash(), trimmedUtxos) + rawdb.WriteTrimDepths(batch, header.Hash(), trimDepths) + } + } + start := time.Now() + trimmedUtxos := make([]*types.SpentUtxoEntry, 0) + for denomination, depth := range trimDepths { + if header.NumberU64(nodeCtx) > depth+1 { + nextBlockToTrim := rawdb.ReadCanonicalHash(chain.Database(), header.NumberU64(nodeCtx)-depth) + TrimBlock(chain, batch, true, denomination, header.NumberU64(nodeCtx)-depth, nextBlockToTrim, &utxosDelete, &trimmedUtxos, &utxoSetSize, !setRoots, blake3pow.logger) // setRoots is false when we are processing the block } } + blake3pow.logger.Infof("Trimmed %d UTXOs from db in %s", len(trimmedUtxos), common.PrettyDuration(time.Since(start))) + if !setRoots { + rawdb.WriteTrimmedUTXOs(batch, header.Hash(), trimmedUtxos) + } for _, hash := range utxosCreate { multiSet.Add(hash.Bytes()) } for _, hash := range utxosDelete { multiSet.Remove(hash.Bytes()) } - blake3pow.logger.Infof("Parent hash: %s, header hash: %s, muhash: %s, block height: %d, setroots: %t, UtxosCreated: %d, UtxosDeleted: %d, UTXO Set Size: %d", header.ParentHash(nodeCtx).String(), header.Hash().String(), multiSet.Hash().String(), header.NumberU64(nodeCtx), setRoots, len(utxosCreate), len(utxosDelete), utxoSetSize) + blake3pow.logger.Infof("Parent hash: %s, header hash: %s, muhash: %s, block height: %d, setroots: %t, UtxosCreated: %d, UtxosDeleted: %d, UTXOs Trimmed from DB: %d, UTXO Set Size: %d", header.ParentHash(nodeCtx).String(), header.Hash().String(), multiSet.Hash().String(), header.NumberU64(nodeCtx), setRoots, len(utxosCreate), len(utxosDelete), len(trimmedUtxos), utxoSetSize) if utxoSetSize < uint64(len(utxosDelete)) { return nil, 0, fmt.Errorf("UTXO set size is less than the number of utxos to delete. This is a bug. UTXO set size: %d, UTXOs to delete: %d", utxoSetSize, len(utxosDelete)) @@ -719,7 +715,7 @@ type UtxoEntryWithIndex struct { Key []byte } -func TrimBlock(chain consensus.ChainHeaderReader, batch ethdb.Batch, blockHeight uint64, blockHash common.Hash, utxosDelete *[]common.Hash, trimmedUtxos *[]*types.SpentUtxoEntry, utxoSetSize *uint64, deleteFromDb bool, logger *log.Logger) { +func TrimBlock(chain consensus.ChainHeaderReader, batch ethdb.Batch, checkDenomination bool, denomination uint8, blockHeight uint64, blockHash common.Hash, utxosDelete *[]common.Hash, trimmedUtxos *[]*types.SpentUtxoEntry, utxoSetSize *uint64, deleteFromDb bool, logger *log.Logger) { utxosCreated, _ := rawdb.ReadCreatedUTXOKeys(chain.Database(), blockHash) if utxosCreated == nil { // This is likely always going to be the case, as the prune depth will almost always be shorter than the trim depth @@ -753,6 +749,9 @@ func TrimBlock(chain consensus.ChainHeaderReader, batch ethdb.Batch, blockHeight }).Error("Invalid utxo Proto") continue } + if checkDenomination && utxo.Denomination != denomination { + continue + } txHash, index, err := rawdb.ReverseUtxoKey(it.Key()) if err != nil { logger.WithField("err", err).Error("Failed to parse utxo key") @@ -767,21 +766,48 @@ func TrimBlock(chain consensus.ChainHeaderReader, batch ethdb.Batch, blockHeight for txHash, utxoEntries := range utxos { blockNumberForTx := rawdb.ReadTxLookupEntry(chain.Database(), txHash) if blockNumberForTx != nil && *blockNumberForTx != blockHeight { // collision, wrong tx + logger.Infof("Collision: tx %s was created in block %d, but is in block %d", txHash.String(), *blockNumberForTx, blockHeight) continue } for _, utxo := range utxoEntries { - if utxo.Denomination < types.MaxDenomination { - *utxosDelete = append(*utxosDelete, types.UTXOHash(txHash, utxo.Index, utxo.UtxoEntry)) - if deleteFromDb { - batch.Delete(utxo.Key) - *trimmedUtxos = append(*trimmedUtxos, &types.SpentUtxoEntry{OutPoint: types.OutPoint{txHash, utxo.Index}, UtxoEntry: utxo.UtxoEntry}) - } - *utxoSetSize-- + *utxosDelete = append(*utxosDelete, types.UTXOHash(txHash, utxo.Index, utxo.UtxoEntry)) + if deleteFromDb { + batch.Delete(utxo.Key) + *trimmedUtxos = append(*trimmedUtxos, &types.SpentUtxoEntry{OutPoint: types.OutPoint{txHash, utxo.Index}, UtxoEntry: utxo.UtxoEntry}) } + *utxoSetSize-- } } } +func UpdateTrimDepths(trimDepths map[uint8]uint64, utxoSetSize uint64) bool { + switch { + case utxoSetSize > params.MaxUTXOSetSize/2 && trimDepths[255] == 0: // 50% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth - (depth / 10) // decrease lifespan of this denomination by 10% + } + trimDepths[255] = 1 // level 1 + case utxoSetSize > params.MaxUTXOSetSize-(params.MaxUTXOSetSize/4) && trimDepths[255] == 1: // 75% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth - (depth / 5) // decrease lifespan of this denomination by an additional 20% + } + trimDepths[255] = 2 // level 2 + case utxoSetSize > params.MaxUTXOSetSize-(params.MaxUTXOSetSize/10) && trimDepths[255] == 2: // 90% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth - (depth / 2) // decrease lifespan of this denomination by an additional 50% + } + trimDepths[255] = 3 // level 3 + case utxoSetSize > params.MaxUTXOSetSize && trimDepths[255] == 3: + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth - (depth / 2) // decrease lifespan of this denomination by an additional 50% + } + trimDepths[255] = 4 // level 4 + default: + return false + } + return true +} + // FinalizeAndAssemble implements consensus.Engine, accumulating the block and // uncle rewards, setting the final state and assembling the block. func (blake3pow *Blake3pow) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.WorkObject, state *state.StateDB, txs []*types.Transaction, uncles []*types.WorkObjectHeader, etxs []*types.Transaction, subManifest types.BlockManifest, receipts []*types.Receipt, utxosCreate, utxosDelete []common.Hash) (*types.WorkObject, error) { diff --git a/core/chain_indexer.go b/core/chain_indexer.go index 2fa96cd6f6..d3e46822a9 100644 --- a/core/chain_indexer.go +++ b/core/chain_indexer.go @@ -36,7 +36,6 @@ import ( "github.com/dominant-strategies/go-quai/event" "github.com/dominant-strategies/go-quai/log" "github.com/dominant-strategies/go-quai/params" - "google.golang.org/protobuf/proto" ) var PruneDepth = uint64(100000000) // Number of blocks behind in which we begin pruning old block data @@ -109,7 +108,6 @@ type ChainIndexer struct { lock sync.Mutex indexAddressUtxos bool - utxoKeyPrunerChan chan []*types.SpentUtxoEntry } // NewChainIndexer creates a new chain indexer to do background processing on @@ -127,7 +125,6 @@ func NewChainIndexer(chainDb ethdb.Database, indexDb ethdb.Database, backend Cha throttling: throttling, logger: logger, indexAddressUtxos: indexAddressUtxos, - utxoKeyPrunerChan: make(chan []*types.SpentUtxoEntry, 1000), } // Initialize database dependent fields and start the updater c.loadValidSections() @@ -147,7 +144,6 @@ func (c *ChainIndexer) Start(chain ChainIndexerChain, config params.ChainConfig) c.GetBloom = chain.GetBloom c.StateAt = chain.StateAt go c.eventLoop(chain.CurrentHeader(), events, sub, chain.NodeCtx(), config) - go c.UTXOKeyPruner() } // Close tears down all goroutines belonging to the indexer and returns any error @@ -383,74 +379,15 @@ func (c *ChainIndexer) PruneOldBlockData(blockHeight uint64) { createdUtxos, _ := rawdb.ReadCreatedUTXOKeys(c.chainDb, blockHash) createdUtxosToKeep := make([][]byte, 0, len(createdUtxos)) for _, key := range createdUtxos { - data, _ := c.chainDb.Get(key) - if len(data) == 0 { - // Don't keep it if it doesn't exist - continue - } - utxoProto := new(types.ProtoTxOut) - if err := proto.Unmarshal(data, utxoProto); err != nil { - // Don't keep it if it can't be unmarshaled - continue - } - - utxo := new(types.UtxoEntry) - if err := utxo.ProtoDecode(utxoProto); err != nil { - // Don't keep it if it can't be decoded into UtxoEntry - continue - } // Reduce key size to 8 bytes key = key[:8] createdUtxosToKeep = append(createdUtxosToKeep, key) } rawdb.WritePrunedUTXOKeys(c.chainDb, blockHeight, createdUtxosToKeep) rawdb.DeleteCreatedUTXOKeys(c.chainDb, blockHash) - sutxos, err := rawdb.ReadSpentUTXOs(c.chainDb, blockHash) - if err != nil { - c.logger.Error("Failed to read spent utxos", "err", err) - } else { - select { - case c.utxoKeyPrunerChan <- sutxos: - default: - c.logger.Warn("utxoKeyPrunerChan is full, dropping spent utxos") - } - } rawdb.DeleteSpentUTXOs(c.chainDb, blockHash) -} - -func (c *ChainIndexer) UTXOKeyPruner() { - for { - select { - case errc := <-c.quit: - // Chain indexer terminating, report no failure and abort - errc <- nil - return - case spentUtxos := <-c.utxoKeyPrunerChan: - for _, spentUtxo := range spentUtxos { - blockheight := rawdb.ReadTxLookupEntry(c.chainDb, spentUtxo.TxHash) - if blockheight == nil { - continue - } - utxoKeys, err := rawdb.ReadPrunedUTXOKeys(c.chainDb, *blockheight) - if err != nil || utxoKeys == nil { - c.logger.Errorf("Failed to read pruned utxo keys: height %d err %+v", *blockheight, err) - continue - } - key := rawdb.UtxoKey(spentUtxo.TxHash, spentUtxo.Index) - for i := 0; i < len(utxoKeys); i++ { - if compareMinLength(utxoKeys[i], key) { - // Remove the element by shifting the slice to the left - utxoKeys = append(utxoKeys[:i], utxoKeys[i+1:]...) - break - } else { - i++ // Only increment i if no element was removed - } - } - rawdb.WritePrunedUTXOKeys(c.chainDb, *blockheight, utxoKeys) - - } - } - } + rawdb.DeleteTrimmedUTXOs(c.chainDb, blockHash) + rawdb.DeleteTrimDepths(c.chainDb, blockHash) } func compareMinLength(a, b []byte) bool { diff --git a/core/headerchain.go b/core/headerchain.go index 5c95ab8f02..cc6f8957db 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -444,10 +444,12 @@ func (hc *HeaderChain) SetCurrentHeader(head *types.WorkObject) error { // If head is the normal extension of canonical head, we can return by just wiring the canonical hash. if prevHeader.Hash() == head.ParentHash(hc.NodeCtx()) { rawdb.WriteCanonicalHash(hc.headerDb, head.Hash(), head.NumberU64(hc.NodeCtx())) - err := hc.AppendBlock(head) - if err != nil { - rawdb.DeleteCanonicalHash(hc.headerDb, head.NumberU64(hc.NodeCtx())) - return err + if nodeCtx == common.ZONE_CTX { + err := hc.AppendBlock(head) + if err != nil { + rawdb.DeleteCanonicalHash(hc.headerDb, head.NumberU64(hc.NodeCtx())) + return err + } } // write the head block hash to the db rawdb.WriteHeadBlockHash(hc.headerDb, head.Hash()) diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 0010b6b13e..bead2a5ce5 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -1457,3 +1457,43 @@ func WriteTrimmedUTXOs(db ethdb.KeyValueWriter, blockHash common.Hash, spentUTXO // And finally, store the data in the database under the appropriate key return db.Put(trimmedUTXOsKey(blockHash), data) } + +func DeleteTrimmedUTXOs(db ethdb.KeyValueWriter, blockHash common.Hash) { + if err := db.Delete(trimmedUTXOsKey(blockHash)); err != nil { + db.Logger().WithField("err", err).Fatal("Failed to delete trimmed utxos") + } +} + +func ReadTrimDepths(db ethdb.Reader, blockHash common.Hash) (map[uint8]uint64, error) { + data, _ := db.Get(trimDepthsKey(blockHash)) + if len(data) == 0 { + return nil, nil + } + protoTrimDepths := new(types.ProtoTrimDepths) + if err := proto.Unmarshal(data, protoTrimDepths); err != nil { + return nil, err + } + trimDepths := make(map[uint8]uint64, len(protoTrimDepths.TrimDepths)) + for denomination, depth := range protoTrimDepths.TrimDepths { + trimDepths[uint8(denomination)] = depth + } + return trimDepths, nil +} + +func WriteTrimDepths(db ethdb.KeyValueWriter, blockHash common.Hash, trimDepths map[uint8]uint64) error { + protoTrimDepths := &types.ProtoTrimDepths{TrimDepths: make(map[uint32]uint64, len(trimDepths))} + for denomination, depth := range trimDepths { + protoTrimDepths.TrimDepths[uint32(denomination)] = depth + } + data, err := proto.Marshal(protoTrimDepths) + if err != nil { + db.Logger().WithField("err", err).Fatal("Failed to rlp encode utxo") + } + return db.Put(trimDepthsKey(blockHash), data) +} + +func DeleteTrimDepths(db ethdb.KeyValueWriter, blockHash common.Hash) { + if err := db.Delete(trimDepthsKey(blockHash)); err != nil { + db.Logger().WithField("err", err).Fatal("Failed to delete trim depths") + } +} diff --git a/core/rawdb/db.pb.go b/core/rawdb/db.pb.go index 525bfc27ab..0c54a7f092 100644 --- a/core/rawdb/db.pb.go +++ b/core/rawdb/db.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2 -// protoc v5.28.1 +// protoc v5.28.2 // source: core/rawdb/db.proto package rawdb diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 02edbc3ced..3bc9dc9597 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -87,6 +87,7 @@ var ( utxoPrefix = []byte("ut") // outpointPrefix + hash -> types.Outpoint spentUTXOsPrefix = []byte("sutxo") // spentUTXOsPrefix + hash -> []types.SpentTxOut trimmedUTXOsPrefix = []byte("tutxo") // trimmedUTXOsPrefix + hash -> []types.SpentTxOut + trimDepthsPrefix = []byte("td") // trimDepthsPrefix + hash -> uint64 createdUTXOsPrefix = []byte("cutxo") // createdUTXOsPrefix + hash -> []common.Hash prunedUTXOKeysPrefix = []byte("putxo") // prunedUTXOKeysPrefix + num (uint64 big endian) -> hash utxoSetSizePrefix = []byte("us") // utxoSetSizePrefix + hash -> uint64 @@ -344,3 +345,7 @@ func prunedUTXOsKey(blockHeight uint64) []byte { func lastTrimmedBlockKey(hash common.Hash) []byte { return append(lastTrimmedBlockPrefix, hash.Bytes()...) } + +func trimDepthsKey(hash common.Hash) []byte { + return append(trimDepthsPrefix, hash.Bytes()...) +} diff --git a/core/state_processor.go b/core/state_processor.go index dc522441c1..ab5db0c6c1 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -736,7 +736,6 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty if err := rawdb.WriteCreatedUTXOKeys(batch, blockHash, utxosCreatedDeleted.UtxosCreatedKeys); err != nil { // Could do this in Apply instead return nil, nil, nil, nil, 0, 0, 0, nil, err } - utxosCreatedDeleted = nil // Release memory return receipts, emittedEtxs, allLogs, statedb, *usedGas, *usedState, utxoSetSize, multiSet, nil } diff --git a/core/types/proto_block.pb.go b/core/types/proto_block.pb.go index 0cc3d79c56..bd6fd75683 100644 --- a/core/types/proto_block.pb.go +++ b/core/types/proto_block.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2 -// protoc v5.28.1 +// protoc v5.28.2 // source: core/types/proto_block.proto package types @@ -2473,6 +2473,53 @@ func (x *ProtoKeys) GetKeys() [][]byte { return nil } +type ProtoTrimDepths struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TrimDepths map[uint32]uint64 `protobuf:"bytes,1,rep,name=trim_depths,json=trimDepths,proto3" json:"trim_depths,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` +} + +func (x *ProtoTrimDepths) Reset() { + *x = ProtoTrimDepths{} + if protoimpl.UnsafeEnabled { + mi := &file_core_types_proto_block_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProtoTrimDepths) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProtoTrimDepths) ProtoMessage() {} + +func (x *ProtoTrimDepths) ProtoReflect() protoreflect.Message { + mi := &file_core_types_proto_block_proto_msgTypes[36] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProtoTrimDepths.ProtoReflect.Descriptor instead. +func (*ProtoTrimDepths) Descriptor() ([]byte, []int) { + return file_core_types_proto_block_proto_rawDescGZIP(), []int{36} +} + +func (x *ProtoTrimDepths) GetTrimDepths() map[uint32]uint64 { + if x != nil { + return x.TrimDepths + } + return nil +} + var File_core_types_proto_block_proto protoreflect.FileDescriptor var file_core_types_proto_block_proto_rawDesc = []byte{ @@ -3006,10 +3053,20 @@ var file_core_types_proto_block_proto_rawDesc = []byte{ 0x6f, 0x53, 0x70, 0x65, 0x6e, 0x74, 0x55, 0x54, 0x58, 0x4f, 0x52, 0x06, 0x73, 0x75, 0x74, 0x78, 0x6f, 0x73, 0x22, 0x1f, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x6b, - 0x65, 0x79, 0x73, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x64, 0x6f, 0x6d, 0x69, 0x6e, 0x61, 0x6e, 0x74, 0x2d, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x67, 0x69, 0x65, 0x73, 0x2f, 0x67, 0x6f, 0x2d, 0x71, 0x75, 0x61, 0x69, 0x2f, 0x63, 0x6f, - 0x72, 0x65, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x79, 0x73, 0x22, 0x99, 0x01, 0x0a, 0x0f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x54, 0x72, 0x69, + 0x6d, 0x44, 0x65, 0x70, 0x74, 0x68, 0x73, 0x12, 0x47, 0x0a, 0x0b, 0x74, 0x72, 0x69, 0x6d, 0x5f, + 0x64, 0x65, 0x70, 0x74, 0x68, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x62, + 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x54, 0x72, 0x69, 0x6d, 0x44, 0x65, + 0x70, 0x74, 0x68, 0x73, 0x2e, 0x54, 0x72, 0x69, 0x6d, 0x44, 0x65, 0x70, 0x74, 0x68, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x74, 0x72, 0x69, 0x6d, 0x44, 0x65, 0x70, 0x74, 0x68, 0x73, + 0x1a, 0x3d, 0x0a, 0x0f, 0x54, 0x72, 0x69, 0x6d, 0x44, 0x65, 0x70, 0x74, 0x68, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, + 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x6f, + 0x6d, 0x69, 0x6e, 0x61, 0x6e, 0x74, 0x2d, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x69, 0x65, + 0x73, 0x2f, 0x67, 0x6f, 0x2d, 0x71, 0x75, 0x61, 0x69, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, + 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3024,7 +3081,7 @@ func file_core_types_proto_block_proto_rawDescGZIP() []byte { return file_core_types_proto_block_proto_rawDescData } -var file_core_types_proto_block_proto_msgTypes = make([]protoimpl.MessageInfo, 38) +var file_core_types_proto_block_proto_msgTypes = make([]protoimpl.MessageInfo, 40) var file_core_types_proto_block_proto_goTypes = []any{ (*ProtoHeader)(nil), // 0: block.ProtoHeader (*ProtoTransaction)(nil), // 1: block.ProtoTransaction @@ -3062,52 +3119,54 @@ var file_core_types_proto_block_proto_goTypes = []any{ (*ProtoSpentUTXO)(nil), // 33: block.ProtoSpentUTXO (*ProtoSpentUTXOs)(nil), // 34: block.ProtoSpentUTXOs (*ProtoKeys)(nil), // 35: block.ProtoKeys - nil, // 36: block.ProtoAddressOutPoints.OutPointsEntry - nil, // 37: block.ProtoOutPointsMap.EntriesEntry - (*common.ProtoHash)(nil), // 38: common.ProtoHash - (*common.ProtoLocation)(nil), // 39: common.ProtoLocation - (*common.ProtoAddress)(nil), // 40: common.ProtoAddress - (*common.ProtoHashes)(nil), // 41: common.ProtoHashes + (*ProtoTrimDepths)(nil), // 36: block.ProtoTrimDepths + nil, // 37: block.ProtoAddressOutPoints.OutPointsEntry + nil, // 38: block.ProtoOutPointsMap.EntriesEntry + nil, // 39: block.ProtoTrimDepths.TrimDepthsEntry + (*common.ProtoHash)(nil), // 40: common.ProtoHash + (*common.ProtoLocation)(nil), // 41: common.ProtoLocation + (*common.ProtoAddress)(nil), // 42: common.ProtoAddress + (*common.ProtoHashes)(nil), // 43: common.ProtoHashes } var file_core_types_proto_block_proto_depIdxs = []int32{ - 38, // 0: block.ProtoHeader.parent_hash:type_name -> common.ProtoHash - 38, // 1: block.ProtoHeader.uncle_hash:type_name -> common.ProtoHash - 38, // 2: block.ProtoHeader.evm_root:type_name -> common.ProtoHash - 38, // 3: block.ProtoHeader.tx_hash:type_name -> common.ProtoHash - 38, // 4: block.ProtoHeader.outbound_etx_hash:type_name -> common.ProtoHash - 38, // 5: block.ProtoHeader.etx_rollup_hash:type_name -> common.ProtoHash - 38, // 6: block.ProtoHeader.manifest_hash:type_name -> common.ProtoHash - 38, // 7: block.ProtoHeader.receipt_hash:type_name -> common.ProtoHash - 39, // 8: block.ProtoHeader.location:type_name -> common.ProtoLocation - 38, // 9: block.ProtoHeader.mix_hash:type_name -> common.ProtoHash - 38, // 10: block.ProtoHeader.utxo_root:type_name -> common.ProtoHash - 38, // 11: block.ProtoHeader.etx_set_root:type_name -> common.ProtoHash - 38, // 12: block.ProtoHeader.etx_eligible_slices:type_name -> common.ProtoHash - 38, // 13: block.ProtoHeader.prime_terminus_hash:type_name -> common.ProtoHash - 38, // 14: block.ProtoHeader.interlink_root_hash:type_name -> common.ProtoHash + 40, // 0: block.ProtoHeader.parent_hash:type_name -> common.ProtoHash + 40, // 1: block.ProtoHeader.uncle_hash:type_name -> common.ProtoHash + 40, // 2: block.ProtoHeader.evm_root:type_name -> common.ProtoHash + 40, // 3: block.ProtoHeader.tx_hash:type_name -> common.ProtoHash + 40, // 4: block.ProtoHeader.outbound_etx_hash:type_name -> common.ProtoHash + 40, // 5: block.ProtoHeader.etx_rollup_hash:type_name -> common.ProtoHash + 40, // 6: block.ProtoHeader.manifest_hash:type_name -> common.ProtoHash + 40, // 7: block.ProtoHeader.receipt_hash:type_name -> common.ProtoHash + 41, // 8: block.ProtoHeader.location:type_name -> common.ProtoLocation + 40, // 9: block.ProtoHeader.mix_hash:type_name -> common.ProtoHash + 40, // 10: block.ProtoHeader.utxo_root:type_name -> common.ProtoHash + 40, // 11: block.ProtoHeader.etx_set_root:type_name -> common.ProtoHash + 40, // 12: block.ProtoHeader.etx_eligible_slices:type_name -> common.ProtoHash + 40, // 13: block.ProtoHeader.prime_terminus_hash:type_name -> common.ProtoHash + 40, // 14: block.ProtoHeader.interlink_root_hash:type_name -> common.ProtoHash 5, // 15: block.ProtoTransaction.access_list:type_name -> block.ProtoAccessList - 38, // 16: block.ProtoTransaction.originating_tx_hash:type_name -> common.ProtoHash + 40, // 16: block.ProtoTransaction.originating_tx_hash:type_name -> common.ProtoHash 25, // 17: block.ProtoTransaction.tx_ins:type_name -> block.ProtoTxIns 26, // 18: block.ProtoTransaction.tx_outs:type_name -> block.ProtoTxOuts - 38, // 19: block.ProtoTransaction.parent_hash:type_name -> common.ProtoHash - 38, // 20: block.ProtoTransaction.mix_hash:type_name -> common.ProtoHash + 40, // 19: block.ProtoTransaction.parent_hash:type_name -> common.ProtoHash + 40, // 20: block.ProtoTransaction.mix_hash:type_name -> common.ProtoHash 1, // 21: block.ProtoTransactions.transactions:type_name -> block.ProtoTransaction 0, // 22: block.ProtoHeaders.headers:type_name -> block.ProtoHeader - 38, // 23: block.ProtoManifest.manifest:type_name -> common.ProtoHash + 40, // 23: block.ProtoManifest.manifest:type_name -> common.ProtoHash 15, // 24: block.ProtoAccessList.access_tuples:type_name -> block.ProtoAccessTuple - 38, // 25: block.ProtoWorkObjectHeader.header_hash:type_name -> common.ProtoHash - 38, // 26: block.ProtoWorkObjectHeader.parent_hash:type_name -> common.ProtoHash - 38, // 27: block.ProtoWorkObjectHeader.tx_hash:type_name -> common.ProtoHash - 39, // 28: block.ProtoWorkObjectHeader.location:type_name -> common.ProtoLocation - 38, // 29: block.ProtoWorkObjectHeader.mix_hash:type_name -> common.ProtoHash - 40, // 30: block.ProtoWorkObjectHeader.primary_coinbase:type_name -> common.ProtoAddress + 40, // 25: block.ProtoWorkObjectHeader.header_hash:type_name -> common.ProtoHash + 40, // 26: block.ProtoWorkObjectHeader.parent_hash:type_name -> common.ProtoHash + 40, // 27: block.ProtoWorkObjectHeader.tx_hash:type_name -> common.ProtoHash + 41, // 28: block.ProtoWorkObjectHeader.location:type_name -> common.ProtoLocation + 40, // 29: block.ProtoWorkObjectHeader.mix_hash:type_name -> common.ProtoHash + 42, // 30: block.ProtoWorkObjectHeader.primary_coinbase:type_name -> common.ProtoAddress 6, // 31: block.ProtoWorkObjectHeaders.wo_headers:type_name -> block.ProtoWorkObjectHeader 0, // 32: block.ProtoWorkObjectBody.header:type_name -> block.ProtoHeader 2, // 33: block.ProtoWorkObjectBody.transactions:type_name -> block.ProtoTransactions 7, // 34: block.ProtoWorkObjectBody.uncles:type_name -> block.ProtoWorkObjectHeaders 2, // 35: block.ProtoWorkObjectBody.outbound_etxs:type_name -> block.ProtoTransactions 4, // 36: block.ProtoWorkObjectBody.manifest:type_name -> block.ProtoManifest - 41, // 37: block.ProtoWorkObjectBody.interlink_hashes:type_name -> common.ProtoHashes + 43, // 37: block.ProtoWorkObjectBody.interlink_hashes:type_name -> common.ProtoHashes 6, // 38: block.ProtoWorkObject.wo_header:type_name -> block.ProtoWorkObjectHeader 8, // 39: block.ProtoWorkObject.wo_body:type_name -> block.ProtoWorkObjectBody 1, // 40: block.ProtoWorkObject.tx:type_name -> block.ProtoTransaction @@ -3116,19 +3175,19 @@ var file_core_types_proto_block_proto_depIdxs = []int32{ 11, // 43: block.ProtoWorkObjectBlocksView.work_objects:type_name -> block.ProtoWorkObjectBlockView 9, // 44: block.ProtoWorkObjectHeaderView.work_object:type_name -> block.ProtoWorkObject 9, // 45: block.ProtoWorkObjectShareView.work_object:type_name -> block.ProtoWorkObject - 38, // 46: block.ProtoAccessTuple.storage_key:type_name -> common.ProtoHash + 40, // 46: block.ProtoAccessTuple.storage_key:type_name -> common.ProtoHash 19, // 47: block.ProtoReceiptForStorage.logs:type_name -> block.ProtoLogsForStorage - 38, // 48: block.ProtoReceiptForStorage.tx_hash:type_name -> common.ProtoHash - 40, // 49: block.ProtoReceiptForStorage.contract_address:type_name -> common.ProtoAddress + 40, // 48: block.ProtoReceiptForStorage.tx_hash:type_name -> common.ProtoHash + 42, // 49: block.ProtoReceiptForStorage.contract_address:type_name -> common.ProtoAddress 2, // 50: block.ProtoReceiptForStorage.outbound_etxs:type_name -> block.ProtoTransactions 16, // 51: block.ProtoReceiptsForStorage.receipts:type_name -> block.ProtoReceiptForStorage - 40, // 52: block.ProtoLogForStorage.address:type_name -> common.ProtoAddress - 38, // 53: block.ProtoLogForStorage.topics:type_name -> common.ProtoHash + 42, // 52: block.ProtoLogForStorage.address:type_name -> common.ProtoAddress + 40, // 53: block.ProtoLogForStorage.topics:type_name -> common.ProtoHash 18, // 54: block.ProtoLogsForStorage.logs:type_name -> block.ProtoLogForStorage 9, // 55: block.ProtoPendingHeader.wo:type_name -> block.ProtoWorkObject 21, // 56: block.ProtoPendingHeader.termini:type_name -> block.ProtoTermini - 38, // 57: block.ProtoTermini.dom_termini:type_name -> common.ProtoHash - 38, // 58: block.ProtoTermini.sub_termini:type_name -> common.ProtoHash + 40, // 57: block.ProtoTermini.dom_termini:type_name -> common.ProtoHash + 40, // 58: block.ProtoTermini.sub_termini:type_name -> common.ProtoHash 9, // 59: block.ProtoPendingEtxs.header:type_name -> block.ProtoWorkObject 2, // 60: block.ProtoPendingEtxs.outbound_etxs:type_name -> block.ProtoTransactions 9, // 61: block.ProtoPendingEtxsRollup.header:type_name -> block.ProtoWorkObject @@ -3136,20 +3195,21 @@ var file_core_types_proto_block_proto_depIdxs = []int32{ 27, // 63: block.ProtoTxIns.tx_ins:type_name -> block.ProtoTxIn 29, // 64: block.ProtoTxOuts.tx_outs:type_name -> block.ProtoTxOut 28, // 65: block.ProtoTxIn.previous_out_point:type_name -> block.ProtoOutPoint - 38, // 66: block.ProtoOutPoint.hash:type_name -> common.ProtoHash - 38, // 67: block.ProtoOutPointAndDenomination.hash:type_name -> common.ProtoHash - 36, // 68: block.ProtoAddressOutPoints.out_points:type_name -> block.ProtoAddressOutPoints.OutPointsEntry - 37, // 69: block.ProtoOutPointsMap.entries:type_name -> block.ProtoOutPointsMap.EntriesEntry + 40, // 66: block.ProtoOutPoint.hash:type_name -> common.ProtoHash + 40, // 67: block.ProtoOutPointAndDenomination.hash:type_name -> common.ProtoHash + 37, // 68: block.ProtoAddressOutPoints.out_points:type_name -> block.ProtoAddressOutPoints.OutPointsEntry + 38, // 69: block.ProtoOutPointsMap.entries:type_name -> block.ProtoOutPointsMap.EntriesEntry 28, // 70: block.ProtoSpentUTXO.outpoint:type_name -> block.ProtoOutPoint 29, // 71: block.ProtoSpentUTXO.sutxo:type_name -> block.ProtoTxOut 33, // 72: block.ProtoSpentUTXOs.sutxos:type_name -> block.ProtoSpentUTXO - 30, // 73: block.ProtoAddressOutPoints.OutPointsEntry.value:type_name -> block.ProtoOutPointAndDenomination - 31, // 74: block.ProtoOutPointsMap.EntriesEntry.value:type_name -> block.ProtoAddressOutPoints - 75, // [75:75] is the sub-list for method output_type - 75, // [75:75] is the sub-list for method input_type - 75, // [75:75] is the sub-list for extension type_name - 75, // [75:75] is the sub-list for extension extendee - 0, // [0:75] is the sub-list for field type_name + 39, // 73: block.ProtoTrimDepths.trim_depths:type_name -> block.ProtoTrimDepths.TrimDepthsEntry + 30, // 74: block.ProtoAddressOutPoints.OutPointsEntry.value:type_name -> block.ProtoOutPointAndDenomination + 31, // 75: block.ProtoOutPointsMap.EntriesEntry.value:type_name -> block.ProtoAddressOutPoints + 76, // [76:76] is the sub-list for method output_type + 76, // [76:76] is the sub-list for method input_type + 76, // [76:76] is the sub-list for extension type_name + 76, // [76:76] is the sub-list for extension extendee + 0, // [0:76] is the sub-list for field type_name } func init() { file_core_types_proto_block_proto_init() } @@ -3590,6 +3650,18 @@ func file_core_types_proto_block_proto_init() { return nil } } + file_core_types_proto_block_proto_msgTypes[36].Exporter = func(v any, i int) any { + switch v := v.(*ProtoTrimDepths); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_core_types_proto_block_proto_msgTypes[0].OneofWrappers = []any{} file_core_types_proto_block_proto_msgTypes[1].OneofWrappers = []any{} @@ -3614,7 +3686,7 @@ func file_core_types_proto_block_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_core_types_proto_block_proto_rawDesc, NumEnums: 0, - NumMessages: 38, + NumMessages: 40, NumExtensions: 0, NumServices: 0, }, diff --git a/core/types/proto_block.proto b/core/types/proto_block.proto index fdf662a9d3..3f913dd10e 100644 --- a/core/types/proto_block.proto +++ b/core/types/proto_block.proto @@ -223,4 +223,8 @@ message ProtoSpentUTXOs { } message ProtoKeys { repeated bytes keys = 1; +} + +message ProtoTrimDepths { + map trim_depths = 1; } \ No newline at end of file diff --git a/core/types/utxo.go b/core/types/utxo.go index 0ff0cd272c..edaf82c83c 100644 --- a/core/types/utxo.go +++ b/core/types/utxo.go @@ -22,6 +22,8 @@ var MaxQi = new(big.Int).Mul(big.NewInt(math.MaxInt64), big.NewInt(params.Ether) // Denominations is a map of denomination to number of Qi var Denominations map[uint8]*big.Int +var TrimDepths map[uint8]uint64 + func init() { // Initialize denominations Denominations = make(map[uint8]*big.Int) @@ -42,6 +44,17 @@ func init() { Denominations[14] = big.NewInt(10000000) // 10000 Qi Denominations[15] = big.NewInt(100000000) // 100000 Qi Denominations[16] = big.NewInt(1000000000) // 1000000 Qi + + TrimDepths = make(map[uint8]uint64) + TrimDepths[0] = 100 + TrimDepths[1] = 200 + TrimDepths[2] = 300 + TrimDepths[3] = 400 + TrimDepths[4] = 500 + TrimDepths[5] = 600 + TrimDepths[6] = 700 + TrimDepths[7] = 800 + TrimDepths[8] = 900 } type TxIns []TxIn diff --git a/p2p/node/peerManager/peerdb/peer_info.pb.go b/p2p/node/peerManager/peerdb/peer_info.pb.go index c7c6c6ccf2..ca1882ad1f 100644 --- a/p2p/node/peerManager/peerdb/peer_info.pb.go +++ b/p2p/node/peerManager/peerdb/peer_info.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2 -// protoc v5.28.1 +// protoc v5.28.2 // source: p2p/node/peerManager/peerdb/peer_info.proto package peerdb diff --git a/p2p/pb/quai_messages.pb.go b/p2p/pb/quai_messages.pb.go index d09c5aa20a..a1fc057b18 100644 --- a/p2p/pb/quai_messages.pb.go +++ b/p2p/pb/quai_messages.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2 -// protoc v5.28.1 +// protoc v5.28.2 // source: p2p/pb/quai_messages.proto package pb From 30794ee691c402f46c2a63dd992494a5e4ab5f53 Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Tue, 3 Sep 2024 17:15:37 -0400 Subject: [PATCH 3/6] Added key sorting and concurrent trimming to reduce overhead --- consensus/blake3pow/consensus.go | 202 +++++++++++++++++---- consensus/consensus.go | 2 +- consensus/progpow/consensus.go | 257 ++++++++++++++++++++++++++- core/chain_indexer.go | 48 +++-- core/headerchain.go | 10 +- core/rawdb/accessors_chain.go | 73 ++++++-- core/rawdb/accessors_chain_test.go | 5 +- core/rawdb/schema.go | 31 +++- core/state_processor.go | 25 +-- core/tx_pool.go | 15 +- core/types/qi_tx.go | 13 +- core/types/utxo.go | 20 +-- core/worker.go | 47 ++--- internal/quaiapi/quai_api.go | 24 ++- internal/quaiapi/transaction_args.go | 4 +- params/protocol_params.go | 9 +- 16 files changed, 648 insertions(+), 137 deletions(-) diff --git a/consensus/blake3pow/consensus.go b/consensus/blake3pow/consensus.go index 5cf7e6266a..84afbdd816 100644 --- a/consensus/blake3pow/consensus.go +++ b/consensus/blake3pow/consensus.go @@ -5,6 +5,8 @@ import ( "math/big" "runtime" "runtime/debug" + "sort" + "sync" "time" mapset "github.com/deckarep/golang-set" @@ -605,6 +607,7 @@ func (blake3pow *Blake3pow) Prepare(chain consensus.ChainHeaderReader, header *t // Finalize implements consensus.Engine, accumulating the block and uncle rewards, // setting the final state on the header +// Finalize returns the new MuHash of the UTXO set, the new size of the UTXO set and an error if any func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) { nodeLocation := blake3pow.config.NodeLocation nodeCtx := blake3pow.config.NodeLocation.Context() @@ -659,7 +662,7 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch et utxoSetSize += uint64(len(utxosCreate)) utxoSetSize -= uint64(len(utxosDelete)) trimDepths := types.TrimDepths - if utxoSetSize > params.MaxUTXOSetSize/2 { + if utxoSetSize > params.SoftMaxUTXOSetSize/2 { var err error trimDepths, err = rawdb.ReadTrimDepths(chain.Database(), header.ParentHash(nodeCtx)) if err != nil || trimDepths == nil { @@ -677,16 +680,65 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch et } } start := time.Now() + collidingKeys, err := rawdb.ReadCollidingKeys(chain.Database(), header.ParentHash(nodeCtx)) + if err != nil { + blake3pow.logger.Errorf("Failed to read colliding keys for block %s: %+v", header.ParentHash(nodeCtx).String(), err) + } + newCollidingKeys := make([][]byte, 0) trimmedUtxos := make([]*types.SpentUtxoEntry, 0) + var wg sync.WaitGroup + var lock sync.Mutex for denomination, depth := range trimDepths { - if header.NumberU64(nodeCtx) > depth+1 { - nextBlockToTrim := rawdb.ReadCanonicalHash(chain.Database(), header.NumberU64(nodeCtx)-depth) - TrimBlock(chain, batch, true, denomination, header.NumberU64(nodeCtx)-depth, nextBlockToTrim, &utxosDelete, &trimmedUtxos, &utxoSetSize, !setRoots, blake3pow.logger) // setRoots is false when we are processing the block + if denomination <= types.MaxTrimDenomination && header.NumberU64(nodeCtx) > depth+1 { + wg.Add(1) + go func(denomination uint8, depth uint64) { + defer func() { + if r := recover(); r != nil { + blake3pow.logger.WithFields(log.Fields{ + "error": r, + "stacktrace": string(debug.Stack()), + }).Error("Go-Quai Panicked") + } + }() + nextBlockToTrim := rawdb.ReadCanonicalHash(chain.Database(), header.NumberU64(nodeCtx)-depth) + collisions := TrimBlock(chain, batch, denomination, true, header.NumberU64(nodeCtx)-depth, nextBlockToTrim, &utxosDelete, &trimmedUtxos, nil, &utxoSetSize, !setRoots, &lock, blake3pow.logger) // setRoots is false when we are processing the block + if len(collisions) > 0 { + lock.Lock() + newCollidingKeys = append(newCollidingKeys, collisions...) + lock.Unlock() + } + wg.Done() + }(denomination, depth) } } + if len(collidingKeys) > 0 { + wg.Add(1) + go func() { + defer func() { + if r := recover(); r != nil { + blake3pow.logger.WithFields(log.Fields{ + "error": r, + "stacktrace": string(debug.Stack()), + }).Error("Go-Quai Panicked") + } + }() + // Trim colliding/duplicate keys here - an optimization could be to do this above in parallel with the other trims + collisions := TrimBlock(chain, batch, 0, false, 0, common.Hash{}, &utxosDelete, &trimmedUtxos, collidingKeys, &utxoSetSize, !setRoots, &lock, blake3pow.logger) + if len(collisions) > 0 { + lock.Lock() + newCollidingKeys = append(newCollidingKeys, collisions...) + lock.Unlock() + } + wg.Done() + }() + } + wg.Wait() blake3pow.logger.Infof("Trimmed %d UTXOs from db in %s", len(trimmedUtxos), common.PrettyDuration(time.Since(start))) if !setRoots { rawdb.WriteTrimmedUTXOs(batch, header.Hash(), trimmedUtxos) + if len(newCollidingKeys) > 0 { + rawdb.WriteCollidingKeys(batch, header.Hash(), newCollidingKeys) + } } for _, hash := range utxosCreate { multiSet.Add(hash.Bytes()) @@ -709,29 +761,81 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch et return multiSet, utxoSetSize, nil } -type UtxoEntryWithIndex struct { - *types.UtxoEntry - Index uint16 - Key []byte -} - -func TrimBlock(chain consensus.ChainHeaderReader, batch ethdb.Batch, checkDenomination bool, denomination uint8, blockHeight uint64, blockHash common.Hash, utxosDelete *[]common.Hash, trimmedUtxos *[]*types.SpentUtxoEntry, utxoSetSize *uint64, deleteFromDb bool, logger *log.Logger) { - utxosCreated, _ := rawdb.ReadCreatedUTXOKeys(chain.Database(), blockHash) - if utxosCreated == nil { - // This is likely always going to be the case, as the prune depth will almost always be shorter than the trim depth - utxosCreated, _ = rawdb.ReadPrunedUTXOKeys(chain.Database(), blockHeight) +// TrimBlock trims all UTXOs of a given denomination that were created in a given block. +// In the event of an attacker intentionally creating too many 9-byte keys that collide, we return the colliding keys to be trimmed in the next block. +func TrimBlock(chain consensus.ChainHeaderReader, batch ethdb.Batch, denomination uint8, checkDenom bool, blockHeight uint64, blockHash common.Hash, utxosDelete *[]common.Hash, trimmedUtxos *[]*types.SpentUtxoEntry, collidingKeys [][]byte, utxoSetSize *uint64, deleteFromDb bool, lock *sync.Mutex, logger *log.Logger) [][]byte { + utxosCreated, err := rawdb.ReadPrunedUTXOKeys(chain.Database(), blockHeight) + if err != nil { + logger.Errorf("Failed to read pruned UTXOs for block %d: %+v", blockHeight, err) + } + if len(utxosCreated) == 0 { + // This should almost never happen, but we need to handle it + utxosCreated, err = rawdb.ReadCreatedUTXOKeys(chain.Database(), blockHash) + if err != nil { + logger.Errorf("Failed to read created UTXOs for block %d: %+v", blockHeight, err) + } + logger.Infof("Reading non-pruned UTXOs for block %d", blockHeight) + for i, key := range utxosCreated { + if len(key) == rawdb.UtxoKeyWithDenominationLength { + if key[len(key)-1] > types.MaxTrimDenomination { + // Don't keep it if the denomination is not trimmed + // The keys are sorted in order of denomination, so we can break here + break + } + key[rawdb.PrunedUtxoKeyWithDenominationLength+len(rawdb.UtxoPrefix)-1] = key[len(key)-1] // place the denomination at the end of the pruned key (11th byte will become 9th byte) + } + // Reduce key size to 9 bytes and cut off the prefix + key = key[len(rawdb.UtxoPrefix) : rawdb.PrunedUtxoKeyWithDenominationLength+len(rawdb.UtxoPrefix)] + utxosCreated[i] = key + } } + logger.Infof("UTXOs created in block %d: %d", blockHeight, len(utxosCreated)) - utxos := make(map[common.Hash][]*UtxoEntryWithIndex) + if len(collidingKeys) > 0 { + logger.Infof("Colliding keys: %d", len(collidingKeys)) + utxosCreated = append(utxosCreated, collidingKeys...) + sort.Slice(utxosCreated, func(i, j int) bool { + return utxosCreated[i][len(utxosCreated[i])-1] < utxosCreated[j][len(utxosCreated[j])-1] + }) + } + newCollisions := make([][]byte, 0) + duplicateKeys := make(map[[36]byte]bool) // cannot use rawdb.UtxoKeyLength for map as it's not const // Start by grabbing all the UTXOs created in the block (that are still in the UTXO set) for _, key := range utxosCreated { - if len(key) == 0 { + if len(key) != rawdb.PrunedUtxoKeyWithDenominationLength { continue } + if checkDenom { + if key[len(key)-1] != denomination { + if key[len(key)-1] > denomination { + break // The keys are stored in order of denomination, so we can stop checking here + } else { + continue + } + } else { + key = append(rawdb.UtxoPrefix, key...) // prepend the db prefix + key = key[:len(key)-1] // remove the denomination byte + } + } + // Check key in database + i := 0 it := chain.Database().NewIterator(key, nil) for it.Next() { data := it.Value() if len(data) == 0 { + logger.Infof("Empty key found, denomination: %d", denomination) + continue + } + // Check if the key is a duplicate + if len(it.Key()) == rawdb.UtxoKeyLength { + key36 := [36]byte(it.Key()) + if duplicateKeys[key36] { + continue + } else { + duplicateKeys[key36] = true + } + } else { + logger.Errorf("Invalid key length: %d", len(it.Key())) continue } utxoProto := new(types.ProtoTxOut) @@ -749,7 +853,7 @@ func TrimBlock(chain consensus.ChainHeaderReader, batch ethdb.Batch, checkDenomi }).Error("Invalid utxo Proto") continue } - if checkDenomination && utxo.Denomination != denomination { + if checkDenom && utxo.Denomination != denomination { continue } txHash, index, err := rawdb.ReverseUtxoKey(it.Key()) @@ -757,51 +861,71 @@ func TrimBlock(chain consensus.ChainHeaderReader, batch ethdb.Batch, checkDenomi logger.WithField("err", err).Error("Failed to parse utxo key") continue } - utxos[txHash] = append(utxos[txHash], &UtxoEntryWithIndex{utxo, index, it.Key()}) - } - it.Release() - } - - // Next, check if they are eligible for deletion and delete them - for txHash, utxoEntries := range utxos { - blockNumberForTx := rawdb.ReadTxLookupEntry(chain.Database(), txHash) - if blockNumberForTx != nil && *blockNumberForTx != blockHeight { // collision, wrong tx - logger.Infof("Collision: tx %s was created in block %d, but is in block %d", txHash.String(), *blockNumberForTx, blockHeight) - continue - } - for _, utxo := range utxoEntries { - *utxosDelete = append(*utxosDelete, types.UTXOHash(txHash, utxo.Index, utxo.UtxoEntry)) + lock.Lock() + *utxosDelete = append(*utxosDelete, types.UTXOHash(txHash, index, utxo)) if deleteFromDb { - batch.Delete(utxo.Key) - *trimmedUtxos = append(*trimmedUtxos, &types.SpentUtxoEntry{OutPoint: types.OutPoint{txHash, utxo.Index}, UtxoEntry: utxo.UtxoEntry}) + batch.Delete(it.Key()) + *trimmedUtxos = append(*trimmedUtxos, &types.SpentUtxoEntry{OutPoint: types.OutPoint{txHash, index}, UtxoEntry: utxo}) } *utxoSetSize-- + lock.Unlock() + i++ + if i >= types.MaxTrimCollisionsPerKeyPerBlock { + // This will rarely ever happen, but if it does, we should continue trimming this key in the next block + logger.WithField("blockHeight", blockHeight).Error("MaxTrimCollisionsPerBlock exceeded") + newCollisions = append(newCollisions, key) + break + } } + it.Release() } + return newCollisions } func UpdateTrimDepths(trimDepths map[uint8]uint64, utxoSetSize uint64) bool { switch { - case utxoSetSize > params.MaxUTXOSetSize/2 && trimDepths[255] == 0: // 50% full + case utxoSetSize > params.SoftMaxUTXOSetSize/2 && trimDepths[255] == 0: // 50% full for denomination, depth := range trimDepths { trimDepths[denomination] = depth - (depth / 10) // decrease lifespan of this denomination by 10% } trimDepths[255] = 1 // level 1 - case utxoSetSize > params.MaxUTXOSetSize-(params.MaxUTXOSetSize/4) && trimDepths[255] == 1: // 75% full + case utxoSetSize > params.SoftMaxUTXOSetSize-(params.SoftMaxUTXOSetSize/4) && trimDepths[255] == 1: // 75% full for denomination, depth := range trimDepths { trimDepths[denomination] = depth - (depth / 5) // decrease lifespan of this denomination by an additional 20% } trimDepths[255] = 2 // level 2 - case utxoSetSize > params.MaxUTXOSetSize-(params.MaxUTXOSetSize/10) && trimDepths[255] == 2: // 90% full + case utxoSetSize > params.SoftMaxUTXOSetSize-(params.SoftMaxUTXOSetSize/10) && trimDepths[255] == 2: // 90% full for denomination, depth := range trimDepths { trimDepths[denomination] = depth - (depth / 2) // decrease lifespan of this denomination by an additional 50% } trimDepths[255] = 3 // level 3 - case utxoSetSize > params.MaxUTXOSetSize && trimDepths[255] == 3: + case utxoSetSize > params.SoftMaxUTXOSetSize && trimDepths[255] == 3: for denomination, depth := range trimDepths { trimDepths[denomination] = depth - (depth / 2) // decrease lifespan of this denomination by an additional 50% } trimDepths[255] = 4 // level 4 + + // Resets + case utxoSetSize <= params.SoftMaxUTXOSetSize/2 && trimDepths[255] == 1: // Below 50% full + for denomination, depth := range types.TrimDepths { // reset to the default trim depths + trimDepths[denomination] = depth + } + trimDepths[255] = 0 // level 0 + case utxoSetSize <= params.SoftMaxUTXOSetSize-(params.SoftMaxUTXOSetSize/4) && trimDepths[255] == 2: // Below 75% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth + (depth / 5) // increase lifespan of this denomination by 20% + } + trimDepths[255] = 1 // level 1 + case utxoSetSize <= params.SoftMaxUTXOSetSize-(params.SoftMaxUTXOSetSize/10) && trimDepths[255] == 3: // Below 90% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth + (depth / 2) // increase lifespan of this denomination by 50% + } + trimDepths[255] = 2 // level 2 + case utxoSetSize <= params.SoftMaxUTXOSetSize && trimDepths[255] == 4: // Below 100% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth + (depth / 2) // increase lifespan of this denomination by 50% + } + trimDepths[255] = 3 // level 3 default: return false } @@ -810,7 +934,7 @@ func UpdateTrimDepths(trimDepths map[uint8]uint64, utxoSetSize uint64) bool { // FinalizeAndAssemble implements consensus.Engine, accumulating the block and // uncle rewards, setting the final state and assembling the block. -func (blake3pow *Blake3pow) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.WorkObject, state *state.StateDB, txs []*types.Transaction, uncles []*types.WorkObjectHeader, etxs []*types.Transaction, subManifest types.BlockManifest, receipts []*types.Receipt, utxosCreate, utxosDelete []common.Hash) (*types.WorkObject, error) { +func (blake3pow *Blake3pow) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.WorkObject, state *state.StateDB, txs []*types.Transaction, uncles []*types.WorkObjectHeader, etxs []*types.Transaction, subManifest types.BlockManifest, receipts []*types.Receipt, parentUtxoSetSize uint64, utxosCreate, utxosDelete []common.Hash) (*types.WorkObject, error) { nodeCtx := blake3pow.config.NodeLocation.Context() if nodeCtx == common.ZONE_CTX && chain.ProcessingState() { // Finalize block diff --git a/consensus/consensus.go b/consensus/consensus.go index 477d9eaa23..0f519d9e4a 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -196,7 +196,7 @@ type Engine interface { // // Note: The block header and state database might be updated to reflect any // consensus rules that happen at finalization (e.g. block rewards). - FinalizeAndAssemble(chain ChainHeaderReader, woHeader *types.WorkObject, state *state.StateDB, txs []*types.Transaction, uncles []*types.WorkObjectHeader, etxs []*types.Transaction, subManifest types.BlockManifest, receipts []*types.Receipt, utxosCreate, utxosDelete []common.Hash) (*types.WorkObject, error) + FinalizeAndAssemble(chain ChainHeaderReader, woHeader *types.WorkObject, state *state.StateDB, txs []*types.Transaction, uncles []*types.WorkObjectHeader, etxs []*types.Transaction, subManifest types.BlockManifest, receipts []*types.Receipt, parentUtxoSetSize uint64, utxosCreate, utxosDelete []common.Hash) (*types.WorkObject, error) // Seal generates a new sealing request for the given input block and pushes // the result into the given channel. diff --git a/consensus/progpow/consensus.go b/consensus/progpow/consensus.go index fe307279c3..4bb2cdf92f 100644 --- a/consensus/progpow/consensus.go +++ b/consensus/progpow/consensus.go @@ -6,6 +6,8 @@ import ( "math/big" "runtime" "runtime/debug" + "sort" + "sync" "time" mapset "github.com/deckarep/golang-set" @@ -22,6 +24,7 @@ import ( "github.com/dominant-strategies/go-quai/multiset" "github.com/dominant-strategies/go-quai/params" "github.com/dominant-strategies/go-quai/trie" + "google.golang.org/protobuf/proto" "modernc.org/mathutil" ) @@ -662,12 +665,13 @@ func (progpow *Progpow) Prepare(chain consensus.ChainHeaderReader, header *types // Finalize implements consensus.Engine, accumulating the block and uncle rewards, // setting the final state on the header +// Finalize returns the new MuHash of the UTXO set, the new size of the UTXO set and an error if any func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) { nodeLocation := progpow.NodeLocation() nodeCtx := progpow.NodeLocation().Context() var multiSet *multiset.MultiSet var utxoSetSize uint64 - if nodeCtx == common.ZONE_CTX && chain.IsGenesisHash(header.ParentHash(nodeCtx)) { + if chain.IsGenesisHash(header.ParentHash(nodeCtx)) { multiSet = multiset.New() // Create the lockup contract account lockupContract, err := vm.LockupContractAddresses[[2]byte{nodeLocation[0], nodeLocation[1]}].InternalAndQuaiAddress() @@ -703,6 +707,7 @@ func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb. core.AddGenesisUtxos(chain.Database(), &utxosCreate, nodeLocation, addressOutpointMap, progpow.logger) if chain.Config().IndexAddressUtxos { chain.WriteAddressOutpoints(addressOutpointMap) + progpow.logger.Info("Indexed genesis utxos") } } else { multiSet = rawdb.ReadMultiSet(chain.Database(), header.ParentHash(nodeCtx)) @@ -711,17 +716,99 @@ func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb. if multiSet == nil { return nil, 0, fmt.Errorf("Multiset is nil for block %s", header.ParentHash(nodeCtx).String()) } + utxoSetSize += uint64(len(utxosCreate)) + utxoSetSize -= uint64(len(utxosDelete)) + trimDepths := types.TrimDepths + if utxoSetSize > params.SoftMaxUTXOSetSize/2 { + var err error + trimDepths, err = rawdb.ReadTrimDepths(chain.Database(), header.ParentHash(nodeCtx)) + if err != nil || trimDepths == nil { + progpow.logger.Errorf("Failed to read trim depths for block %s: %+v", header.ParentHash(nodeCtx).String(), err) + trimDepths = make(map[uint8]uint64, len(types.TrimDepths)) + for denomination, depth := range types.TrimDepths { // copy the default trim depths + trimDepths[denomination] = depth + } + } + if UpdateTrimDepths(trimDepths, utxoSetSize) { + progpow.logger.Infof("Updated trim depths at height %d new depths: %+v", header.NumberU64(nodeCtx), trimDepths) + } + if !setRoots { + rawdb.WriteTrimDepths(batch, header.Hash(), trimDepths) + } + } + start := time.Now() + collidingKeys, err := rawdb.ReadCollidingKeys(chain.Database(), header.ParentHash(nodeCtx)) + if err != nil { + progpow.logger.Errorf("Failed to read colliding keys for block %s: %+v", header.ParentHash(nodeCtx).String(), err) + } + newCollidingKeys := make([][]byte, 0) + trimmedUtxos := make([]*types.SpentUtxoEntry, 0) + var wg sync.WaitGroup + var lock sync.Mutex + for denomination, depth := range trimDepths { + if denomination <= types.MaxTrimDenomination && header.NumberU64(nodeCtx) > depth+1 { + wg.Add(1) + go func(denomination uint8, depth uint64) { + defer func() { + if r := recover(); r != nil { + progpow.logger.WithFields(log.Fields{ + "error": r, + "stacktrace": string(debug.Stack()), + }).Error("Go-Quai Panicked") + } + }() + nextBlockToTrim := rawdb.ReadCanonicalHash(chain.Database(), header.NumberU64(nodeCtx)-depth) + collisions := TrimBlock(chain, batch, denomination, true, header.NumberU64(nodeCtx)-depth, nextBlockToTrim, &utxosDelete, &trimmedUtxos, nil, &utxoSetSize, !setRoots, &lock, progpow.logger) // setRoots is false when we are processing the block + if len(collisions) > 0 { + lock.Lock() + newCollidingKeys = append(newCollidingKeys, collisions...) + lock.Unlock() + } + wg.Done() + }(denomination, depth) + } + } + if len(collidingKeys) > 0 { + wg.Add(1) + go func() { + defer func() { + if r := recover(); r != nil { + progpow.logger.WithFields(log.Fields{ + "error": r, + "stacktrace": string(debug.Stack()), + }).Error("Go-Quai Panicked") + } + }() + // Trim colliding/duplicate keys here - an optimization could be to do this above in parallel with the other trims + collisions := TrimBlock(chain, batch, 0, false, 0, common.Hash{}, &utxosDelete, &trimmedUtxos, collidingKeys, &utxoSetSize, !setRoots, &lock, progpow.logger) + if len(collisions) > 0 { + lock.Lock() + newCollidingKeys = append(newCollidingKeys, collisions...) + lock.Unlock() + } + wg.Done() + }() + } + wg.Wait() + progpow.logger.Infof("Trimmed %d UTXOs from db in %s", len(trimmedUtxos), common.PrettyDuration(time.Since(start))) + if !setRoots { + rawdb.WriteTrimmedUTXOs(batch, header.Hash(), trimmedUtxos) + if len(newCollidingKeys) > 0 { + rawdb.WriteCollidingKeys(batch, header.Hash(), newCollidingKeys) + } + } for _, hash := range utxosCreate { multiSet.Add(hash.Bytes()) } for _, hash := range utxosDelete { multiSet.Remove(hash.Bytes()) } + progpow.logger.Infof("Parent hash: %s, header hash: %s, muhash: %s, block height: %d, setroots: %t, UtxosCreated: %d, UtxosDeleted: %d, UTXOs Trimmed from DB: %d, UTXO Set Size: %d", header.ParentHash(nodeCtx).String(), header.Hash().String(), multiSet.Hash().String(), header.NumberU64(nodeCtx), setRoots, len(utxosCreate), len(utxosDelete), len(trimmedUtxos), utxoSetSize) + if utxoSetSize < uint64(len(utxosDelete)) { return nil, 0, fmt.Errorf("UTXO set size is less than the number of utxos to delete. This is a bug. UTXO set size: %d, UTXOs to delete: %d", utxoSetSize, len(utxosDelete)) } - utxoSetSize += uint64(len(utxosCreate)) - utxoSetSize -= uint64(len(utxosDelete)) + if setRoots { header.Header().SetUTXORoot(multiSet.Hash()) header.Header().SetEVMRoot(state.IntermediateRoot(true)) @@ -731,9 +818,171 @@ func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb. return multiSet, utxoSetSize, nil } +// TrimBlock trims all UTXOs of a given denomination that were created in a given block. +// In the event of an attacker intentionally creating too many 9-byte keys that collide, we return the colliding keys to be trimmed in the next block. +func TrimBlock(chain consensus.ChainHeaderReader, batch ethdb.Batch, denomination uint8, checkDenom bool, blockHeight uint64, blockHash common.Hash, utxosDelete *[]common.Hash, trimmedUtxos *[]*types.SpentUtxoEntry, collidingKeys [][]byte, utxoSetSize *uint64, deleteFromDb bool, lock *sync.Mutex, logger *log.Logger) [][]byte { + utxosCreated, _ := rawdb.ReadPrunedUTXOKeys(chain.Database(), blockHeight) + if len(utxosCreated) == 0 { + // This should almost never happen, but we need to handle it + utxosCreated, _ = rawdb.ReadCreatedUTXOKeys(chain.Database(), blockHash) + logger.Infof("Reading non-pruned UTXOs for block %d", blockHeight) + for i, key := range utxosCreated { + if len(key) == rawdb.UtxoKeyWithDenominationLength { + if key[len(key)-1] > types.MaxTrimDenomination { + // Don't keep it if the denomination is not trimmed + // The keys are sorted in order of denomination, so we can break here + break + } + key[rawdb.PrunedUtxoKeyWithDenominationLength+len(rawdb.UtxoPrefix)-1] = key[len(key)-1] // place the denomination at the end of the pruned key (11th byte will become 9th byte) + } + // Reduce key size to 9 bytes and cut off the prefix + key = key[len(rawdb.UtxoPrefix) : rawdb.PrunedUtxoKeyWithDenominationLength+len(rawdb.UtxoPrefix)] + utxosCreated[i] = key + } + } + + logger.Infof("UTXOs created in block %d: %d", blockHeight, len(utxosCreated)) + if len(collidingKeys) > 0 { + logger.Infof("Colliding keys: %d", len(collidingKeys)) + utxosCreated = append(utxosCreated, collidingKeys...) + sort.Slice(utxosCreated, func(i, j int) bool { + return utxosCreated[i][len(utxosCreated[i])-1] < utxosCreated[j][len(utxosCreated[j])-1] + }) + } + newCollisions := make([][]byte, 0) + duplicateKeys := make(map[[36]byte]bool) // cannot use rawdb.UtxoKeyLength for map as it's not const + // Start by grabbing all the UTXOs created in the block (that are still in the UTXO set) + for _, key := range utxosCreated { + if len(key) != rawdb.PrunedUtxoKeyWithDenominationLength { + continue + } + if checkDenom { + if key[len(key)-1] != denomination { + if key[len(key)-1] > denomination { + break // The keys are stored in order of denomination, so we can stop checking here + } else { + continue + } + } else { + key = append(rawdb.UtxoPrefix, key...) // prepend the db prefix + key = key[:len(key)-1] // remove the denomination byte + } + } + // Check key in database + i := 0 + it := chain.Database().NewIterator(key, nil) + for it.Next() { + data := it.Value() + if len(data) == 0 { + logger.Infof("Empty key found, denomination: %d", denomination) + continue + } + // Check if the key is a duplicate + if len(it.Key()) == rawdb.UtxoKeyLength { + key36 := [36]byte(it.Key()) + if duplicateKeys[key36] { + continue + } else { + duplicateKeys[key36] = true + } + } + utxoProto := new(types.ProtoTxOut) + if err := proto.Unmarshal(data, utxoProto); err != nil { + logger.Errorf("Failed to unmarshal ProtoTxOut: %+v data: %+v key: %+v", err, data, key) + continue + } + + utxo := new(types.UtxoEntry) + if err := utxo.ProtoDecode(utxoProto); err != nil { + logger.WithFields(log.Fields{ + "key": key, + "data": data, + "err": err, + }).Error("Invalid utxo Proto") + continue + } + if checkDenom && utxo.Denomination != denomination { + continue + } + txHash, index, err := rawdb.ReverseUtxoKey(it.Key()) + if err != nil { + logger.WithField("err", err).Error("Failed to parse utxo key") + continue + } + lock.Lock() + *utxosDelete = append(*utxosDelete, types.UTXOHash(txHash, index, utxo)) + if deleteFromDb { + batch.Delete(it.Key()) + *trimmedUtxos = append(*trimmedUtxos, &types.SpentUtxoEntry{OutPoint: types.OutPoint{txHash, index}, UtxoEntry: utxo}) + } + *utxoSetSize-- + lock.Unlock() + i++ + if i >= types.MaxTrimCollisionsPerKeyPerBlock { + // This will rarely ever happen, but if it does, we should continue trimming this key in the next block + logger.WithField("blockHeight", blockHeight).Error("MaxTrimCollisionsPerBlock exceeded") + newCollisions = append(newCollisions, key) + break + } + } + it.Release() + } + return newCollisions +} + +func UpdateTrimDepths(trimDepths map[uint8]uint64, utxoSetSize uint64) bool { + switch { + case utxoSetSize > params.SoftMaxUTXOSetSize/2 && trimDepths[255] == 0: // 50% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth - (depth / 10) // decrease lifespan of this denomination by 10% + } + trimDepths[255] = 1 // level 1 + case utxoSetSize > params.SoftMaxUTXOSetSize-(params.SoftMaxUTXOSetSize/4) && trimDepths[255] == 1: // 75% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth - (depth / 5) // decrease lifespan of this denomination by an additional 20% + } + trimDepths[255] = 2 // level 2 + case utxoSetSize > params.SoftMaxUTXOSetSize-(params.SoftMaxUTXOSetSize/10) && trimDepths[255] == 2: // 90% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth - (depth / 2) // decrease lifespan of this denomination by an additional 50% + } + trimDepths[255] = 3 // level 3 + case utxoSetSize > params.SoftMaxUTXOSetSize && trimDepths[255] == 3: + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth - (depth / 2) // decrease lifespan of this denomination by an additional 50% + } + trimDepths[255] = 4 // level 4 + + // Resets + case utxoSetSize <= params.SoftMaxUTXOSetSize/2 && trimDepths[255] == 1: // Below 50% full + for denomination, depth := range types.TrimDepths { // reset to the default trim depths + trimDepths[denomination] = depth + } + trimDepths[255] = 0 // level 0 + case utxoSetSize <= params.SoftMaxUTXOSetSize-(params.SoftMaxUTXOSetSize/4) && trimDepths[255] == 2: // Below 75% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth + (depth / 5) // increase lifespan of this denomination by 20% + } + trimDepths[255] = 1 // level 1 + case utxoSetSize <= params.SoftMaxUTXOSetSize-(params.SoftMaxUTXOSetSize/10) && trimDepths[255] == 3: // Below 90% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth + (depth / 2) // increase lifespan of this denomination by 50% + } + trimDepths[255] = 2 // level 2 + case utxoSetSize <= params.SoftMaxUTXOSetSize && trimDepths[255] == 4: // Below 100% full + for denomination, depth := range trimDepths { + trimDepths[denomination] = depth + (depth / 2) // increase lifespan of this denomination by 50% + } + trimDepths[255] = 3 // level 3 + default: + return false + } + return true +} + // FinalizeAndAssemble implements consensus.Engine, accumulating the block and // uncle rewards, setting the final state and assembling the block. -func (progpow *Progpow) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.WorkObject, state *state.StateDB, txs []*types.Transaction, uncles []*types.WorkObjectHeader, etxs []*types.Transaction, subManifest types.BlockManifest, receipts []*types.Receipt, utxosCreate, utxosDelete []common.Hash) (*types.WorkObject, error) { +func (progpow *Progpow) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.WorkObject, state *state.StateDB, txs []*types.Transaction, uncles []*types.WorkObjectHeader, etxs []*types.Transaction, subManifest types.BlockManifest, receipts []*types.Receipt, parentUtxoSetSize uint64, utxosCreate, utxosDelete []common.Hash) (*types.WorkObject, error) { nodeCtx := progpow.config.NodeLocation.Context() if nodeCtx == common.ZONE_CTX && chain.ProcessingState() { // Finalize block diff --git a/core/chain_indexer.go b/core/chain_indexer.go index d3e46822a9..6cc0371f21 100644 --- a/core/chain_indexer.go +++ b/core/chain_indexer.go @@ -104,9 +104,9 @@ type ChainIndexer struct { throttling time.Duration // Disk throttling to prevent a heavy upgrade from hogging resources - logger *log.Logger - lock sync.Mutex - + logger *log.Logger + lock sync.Mutex + pruneLock sync.Mutex indexAddressUtxos bool } @@ -275,7 +275,8 @@ func (c *ChainIndexer) indexerLoop(currentHeader *types.WorkObject, qiIndexerCh // TODO: This seems a bit brittle, can we detect this case explicitly? if rawdb.ReadCanonicalHash(c.chainDb, prevHeader.NumberU64(nodeCtx)) != prevHash { - if h := rawdb.FindCommonAncestor(c.chainDb, prevHeader, block, nodeCtx); h != nil { + h, err := rawdb.FindCommonAncestor(c.chainDb, prevHeader, block, nodeCtx) + if h != nil { // If indexAddressUtxos flag is enabled, update the address utxo map // TODO: Need to be able to turn on/off indexer and fix corrupted state @@ -318,6 +319,9 @@ func (c *ChainIndexer) indexerLoop(currentHeader *types.WorkObject, qiIndexerCh time5 = time.Since(start) c.newHead(h.NumberU64(nodeCtx), true) + } else if err != nil { + c.logger.WithField("err", err).Error("Failed to index: failed to find common ancestor") + continue } } } @@ -370,24 +374,43 @@ func (c *ChainIndexer) indexerLoop(currentHeader *types.WorkObject, qiIndexerCh } func (c *ChainIndexer) PruneOldBlockData(blockHeight uint64) { + c.pruneLock.Lock() blockHash := rawdb.ReadCanonicalHash(c.chainDb, blockHeight) + if rawdb.ReadAlreadyPruned(c.chainDb, blockHash) { + return + } + rawdb.WriteAlreadyPruned(c.chainDb, blockHash) // Pruning can only happen once per block + c.pruneLock.Unlock() + rawdb.DeleteInboundEtxs(c.chainDb, blockHash) rawdb.DeletePendingEtxs(c.chainDb, blockHash) rawdb.DeletePendingEtxsRollup(c.chainDb, blockHash) rawdb.DeleteManifest(c.chainDb, blockHash) rawdb.DeletePbCacheBody(c.chainDb, blockHash) createdUtxos, _ := rawdb.ReadCreatedUTXOKeys(c.chainDb, blockHash) - createdUtxosToKeep := make([][]byte, 0, len(createdUtxos)) - for _, key := range createdUtxos { - // Reduce key size to 8 bytes - key = key[:8] - createdUtxosToKeep = append(createdUtxosToKeep, key) + if len(createdUtxos) > 0 { + createdUtxosToKeep := make([][]byte, 0, len(createdUtxos)/2) + for _, key := range createdUtxos { + if len(key) == rawdb.UtxoKeyWithDenominationLength { + if key[len(key)-1] > types.MaxTrimDenomination { + // Don't keep it if the denomination is not trimmed + // The keys are sorted in order of denomination, so we can break here + break + } + key[rawdb.PrunedUtxoKeyWithDenominationLength+len(rawdb.UtxoPrefix)-1] = key[len(key)-1] // place the denomination at the end of the pruned key (11th byte will become 9th byte) + } + // Reduce key size to 9 bytes and cut off the prefix + key = key[len(rawdb.UtxoPrefix) : rawdb.PrunedUtxoKeyWithDenominationLength+len(rawdb.UtxoPrefix)] + createdUtxosToKeep = append(createdUtxosToKeep, key) + } + c.logger.Infof("Removed %d utxo keys from block %d", len(createdUtxos)-len(createdUtxosToKeep), blockHeight) + rawdb.WritePrunedUTXOKeys(c.chainDb, blockHeight, createdUtxosToKeep) } - rawdb.WritePrunedUTXOKeys(c.chainDb, blockHeight, createdUtxosToKeep) rawdb.DeleteCreatedUTXOKeys(c.chainDb, blockHash) rawdb.DeleteSpentUTXOs(c.chainDb, blockHash) rawdb.DeleteTrimmedUTXOs(c.chainDb, blockHash) rawdb.DeleteTrimDepths(c.chainDb, blockHash) + rawdb.DeleteCollidingKeys(c.chainDb, blockHash) } func compareMinLength(a, b []byte) bool { @@ -396,8 +419,9 @@ func compareMinLength(a, b []byte) bool { minLen = len(b) } - // Compare the slices up to the length of the shorter slice - for i := 0; i < minLen; i++ { + // Compare the slices up to the length of the pruned key + // The 9th byte (position 8) is the denomination in the pruned utxo key + for i := 0; i < rawdb.PrunedUtxoKeyWithDenominationLength-1; i++ { if a[i] != b[i] { return false } diff --git a/core/headerchain.go b/core/headerchain.go index cc6f8957db..085e966abc 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -461,8 +461,11 @@ func (hc *HeaderChain) SetCurrentHeader(head *types.WorkObject) error { return nil } - //Find a common header - commonHeader := hc.findCommonAncestor(head) + //Find a common header between the current header and the new head + commonHeader, err := rawdb.FindCommonAncestor(hc.headerDb, prevHeader, head, nodeCtx) + if err != nil { + return err + } newHeader := types.CopyWorkObject(head) // Delete each header and rollback state processor until common header @@ -508,6 +511,9 @@ func (hc *HeaderChain) SetCurrentHeader(head *types.WorkObject) error { return err } for _, key := range utxoKeys { + if len(key) == rawdb.UtxoKeyWithDenominationLength { + key = key[:rawdb.UtxoKeyLength] // The last byte of the key is the denomination (but only in CreatedUTXOKeys) + } hc.headerDb.Delete(key) } } diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index bead2a5ce5..2f37cb051e 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -18,6 +18,8 @@ package rawdb import ( "encoding/binary" + "fmt" + "sort" "github.com/dominant-strategies/go-quai/common" "github.com/dominant-strategies/go-quai/core/types" @@ -745,39 +747,44 @@ func IsGenesisHash(db ethdb.Reader, hash common.Hash) bool { } // FindCommonAncestor returns the last common ancestor of two block headers -func FindCommonAncestor(db ethdb.Reader, a, b *types.WorkObject, nodeCtx int) *types.WorkObject { +func FindCommonAncestor(db ethdb.Reader, a, b *types.WorkObject, nodeCtx int) (*types.WorkObject, error) { for bn := b.NumberU64(nodeCtx); a.NumberU64(nodeCtx) > bn; { a = ReadHeader(db, a.NumberU64(nodeCtx)-1, a.ParentHash(nodeCtx)) - if IsGenesisHash(db, b.ParentHash(nodeCtx)) { - return nil + if IsGenesisHash(db, a.ParentHash(nodeCtx)) { + return nil, fmt.Errorf("no common ancestor found") } if a == nil { - return nil + return nil, fmt.Errorf("unable to find hash %s", a.ParentHash(nodeCtx).String()) } } for an := a.NumberU64(nodeCtx); an < b.NumberU64(nodeCtx); { b = ReadHeader(db, b.NumberU64(nodeCtx)-1, b.ParentHash(nodeCtx)) if IsGenesisHash(db, b.ParentHash(nodeCtx)) { - return nil + return nil, fmt.Errorf("no common ancestor found") } if b == nil { - return nil + return nil, fmt.Errorf("unable to find hash %s", b.ParentHash(nodeCtx).String()) } } for a.Hash() != b.Hash() { a = ReadHeader(db, a.NumberU64(nodeCtx)-1, a.ParentHash(nodeCtx)) if a == nil { - return nil + return nil, fmt.Errorf("unable to find hash %s", a.ParentHash(nodeCtx).String()) } b = ReadHeader(db, b.NumberU64(nodeCtx)-1, b.ParentHash(nodeCtx)) if b == nil { - return nil + return nil, fmt.Errorf("unable to find hash %s", b.ParentHash(nodeCtx).String()) } - if IsGenesisHash(db, a.ParentHash(nodeCtx)) || IsGenesisHash(db, b.ParentHash(nodeCtx)) { - return nil + if IsGenesisHash(db, a.ParentHash(nodeCtx)) && IsGenesisHash(db, b.ParentHash(nodeCtx)) { + number := ReadHeaderNumber(db, a.ParentHash(nodeCtx)) + header := ReadHeader(db, *number, a.ParentHash(nodeCtx)) + if header == nil { + return nil, fmt.Errorf("unable to find hash %s", a.ParentHash(nodeCtx).String()) + } + return header, nil } } - return a + return a, nil } // ReadHeadBlock returns the current canonical head block. @@ -1325,6 +1332,10 @@ func DeleteSpentUTXOs(db ethdb.KeyValueWriter, blockHash common.Hash) { } func WriteCreatedUTXOKeys(db ethdb.KeyValueWriter, blockHash common.Hash, createdUTXOKeys [][]byte) error { + // Sort each key by the denomination in the key + sort.Slice(createdUTXOKeys, func(i, j int) bool { + return createdUTXOKeys[i][len(createdUTXOKeys[i])-1] < createdUTXOKeys[j][len(createdUTXOKeys[j])-1] // the last byte is the denomination + }) protoKeys := &types.ProtoKeys{Keys: make([][]byte, 0, len(createdUTXOKeys))} protoKeys.Keys = append(protoKeys.Keys, createdUTXOKeys...) @@ -1497,3 +1508,43 @@ func DeleteTrimDepths(db ethdb.KeyValueWriter, blockHash common.Hash) { db.Logger().WithField("err", err).Fatal("Failed to delete trim depths") } } + +func ReadCollidingKeys(db ethdb.Reader, blockHash common.Hash) ([][]byte, error) { + data, _ := db.Get(collidingKeysKey(blockHash)) + if len(data) == 0 { + return nil, nil + } + protoKeys := new(types.ProtoKeys) + if err := proto.Unmarshal(data, protoKeys); err != nil { + return nil, err + } + return protoKeys.Keys, nil +} + +func WriteCollidingKeys(db ethdb.KeyValueWriter, blockHash common.Hash, keys [][]byte) error { + protoKeys := &types.ProtoKeys{Keys: make([][]byte, 0, len(keys))} + protoKeys.Keys = append(protoKeys.Keys, keys...) + + data, err := proto.Marshal(protoKeys) + if err != nil { + db.Logger().WithField("err", err).Fatal("Failed to rlp encode utxo") + } + return db.Put(collidingKeysKey(blockHash), data) +} + +func DeleteCollidingKeys(db ethdb.KeyValueWriter, blockHash common.Hash) { + if err := db.Delete(collidingKeysKey(blockHash)); err != nil { + db.Logger().WithField("err", err).Fatal("Failed to delete colliding keys") + } +} + +func ReadAlreadyPruned(db ethdb.Reader, blockHash common.Hash) bool { + data, _ := db.Get(alreadyPrunedKey(blockHash)) + return len(data) > 0 +} + +func WriteAlreadyPruned(db ethdb.KeyValueWriter, blockHash common.Hash) { + if err := db.Put(alreadyPrunedKey(blockHash), []byte{1}); err != nil { + db.Logger().WithField("err", err).Fatal("Failed to store already pruned") + } +} diff --git a/core/rawdb/accessors_chain_test.go b/core/rawdb/accessors_chain_test.go index 8a56e190e6..58c8786fb1 100644 --- a/core/rawdb/accessors_chain_test.go +++ b/core/rawdb/accessors_chain_test.go @@ -372,7 +372,10 @@ func TestCommonAncestor(t *testing.T) { zone1Block2 := types.NewWorkObject(zone1WoHeader2, zone1Body2, nil) WriteWorkObject(db, zone1Block2.Hash(), zone1Block2, types.BlockObject, common.ZONE_CTX) - ancestor := FindCommonAncestor(db, zone0Block, zone1Block2, common.ZONE_CTX) + ancestor, err := FindCommonAncestor(db, zone0Block, zone1Block2, common.ZONE_CTX) + if err != nil { + t.Fatalf("Error finding common ancestor: %v", err) + } if ancestor == nil || ancestor.Hash() != regionBlock.Hash() { t.Fatalf("Common ancestor not found: %v", ancestor) } diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 3bc9dc9597..723376d067 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -84,12 +84,14 @@ var ( AddressUtxosPrefix = []byte("au") // addressUtxosPrefix + hash -> []types.UtxoEntry processedStatePrefix = []byte("ps") // processedStatePrefix + hash -> boolean multiSetPrefix = []byte("ms") // multiSetPrefix + hash -> multiset - utxoPrefix = []byte("ut") // outpointPrefix + hash -> types.Outpoint + UtxoPrefix = []byte("ut") // outpointPrefix + hash -> types.Outpoint spentUTXOsPrefix = []byte("sutxo") // spentUTXOsPrefix + hash -> []types.SpentTxOut trimmedUTXOsPrefix = []byte("tutxo") // trimmedUTXOsPrefix + hash -> []types.SpentTxOut trimDepthsPrefix = []byte("td") // trimDepthsPrefix + hash -> uint64 + collidingKeysPrefix = []byte("ck") // collidingKeysPrefix + hash -> [][]byte createdUTXOsPrefix = []byte("cutxo") // createdUTXOsPrefix + hash -> []common.Hash prunedUTXOKeysPrefix = []byte("putxo") // prunedUTXOKeysPrefix + num (uint64 big endian) -> hash + prunedPrefix = []byte("pru") // prunedPrefix + hash -> pruned utxoSetSizePrefix = []byte("us") // utxoSetSizePrefix + hash -> uint64 blockReceiptsPrefix = []byte("r") // blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts pendingEtxsPrefix = []byte("pe") // pendingEtxsPrefix + hash -> PendingEtxs at block @@ -301,20 +303,31 @@ func addressUtxosKey(address string) []byte { return append(AddressUtxosPrefix, address[:]...) } +var UtxoKeyLength = len(UtxoPrefix) + common.HashLength + 2 + // This can be optimized via VLQ encoding as btcd has done // this key is 36 bytes long and can probably be reduced to 32 bytes func UtxoKey(hash common.Hash, index uint16) []byte { indexBytes := make([]byte, 2) binary.BigEndian.PutUint16(indexBytes, index) - return append(utxoPrefix, append(hash.Bytes(), indexBytes...)...) + return append(UtxoPrefix, append(hash.Bytes(), indexBytes...)...) +} + +var UtxoKeyWithDenominationLength = len(UtxoPrefix) + common.HashLength + 3 +var PrunedUtxoKeyWithDenominationLength = 9 + +func UtxoKeyWithDenomination(hash common.Hash, index uint16, denomination uint8) []byte { + indexBytes := make([]byte, 2) + binary.BigEndian.PutUint16(indexBytes, index) + return append(UtxoPrefix, append(hash.Bytes(), append(indexBytes, denomination)...)...) } func ReverseUtxoKey(key []byte) (common.Hash, uint16, error) { - if len(key) != len(utxoPrefix)+common.HashLength+2 { + if len(key) != len(UtxoPrefix)+common.HashLength+2 { return common.Hash{}, 0, fmt.Errorf("invalid key length %d", len(key)) } - hash := common.BytesToHash(key[len(utxoPrefix) : common.HashLength+len(utxoPrefix)]) - index := binary.BigEndian.Uint16(key[common.HashLength+len(utxoPrefix):]) + hash := common.BytesToHash(key[len(UtxoPrefix) : common.HashLength+len(UtxoPrefix)]) + index := binary.BigEndian.Uint16(key[common.HashLength+len(UtxoPrefix):]) return hash, index, nil } @@ -349,3 +362,11 @@ func lastTrimmedBlockKey(hash common.Hash) []byte { func trimDepthsKey(hash common.Hash) []byte { return append(trimDepthsPrefix, hash.Bytes()...) } + +func collidingKeysKey(hash common.Hash) []byte { + return append(collidingKeysPrefix, hash.Bytes()...) +} + +func alreadyPrunedKey(hash common.Hash) []byte { + return append(prunedPrefix, hash.Bytes()...) +} diff --git a/core/state_processor.go b/core/state_processor.go index ab5db0c6c1..d29352bd7d 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -242,11 +242,14 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty parentEvmRoot := parent.Header().EVMRoot() parentEtxSetRoot := parent.Header().EtxSetRoot() parentQuaiStateSize := parent.QuaiStateSize() + parentUtxoSetSize := rawdb.ReadUTXOSetSize(p.hc.bc.db, header.ParentHash(nodeCtx)) if p.hc.IsGenesisHash(parent.Hash()) { parentEvmRoot = types.EmptyRootHash parentEtxSetRoot = types.EmptyRootHash parentQuaiStateSize = big.NewInt(0) + parentUtxoSetSize = 0 } + qiScalingFactor := math.Log(float64(parentUtxoSetSize)) // Initialize a statedb statedb, err := state.New(parentEvmRoot, parentEtxSetRoot, parentQuaiStateSize, p.stateCache, p.etxCache, p.snaps, nodeLocation, p.logger) if err != nil { @@ -334,7 +337,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty if _, ok := senders[tx.Hash()]; ok { checkSig = false } - qiTxFee, fees, etxs, err, timing := ProcessQiTx(tx, p.hc, checkSig, firstQiTx, header, batch, p.hc.headerDb, gp, usedGas, p.hc.pool.signer, p.hc.NodeLocation(), *p.config.ChainID, &etxRLimit, &etxPLimit, utxosCreatedDeleted) + qiTxFee, fees, etxs, err, timing := ProcessQiTx(tx, p.hc, checkSig, firstQiTx, header, batch, p.hc.headerDb, gp, usedGas, p.hc.pool.signer, p.hc.NodeLocation(), *p.config.ChainID, qiScalingFactor, &etxRLimit, &etxPLimit, utxosCreatedDeleted) if err != nil { return nil, nil, nil, nil, 0, 0, 0, nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } @@ -351,7 +354,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty // convert the fee to quai qiTxFeeInQuai := misc.QiToQuai(primeTerminus.WorkObjectHeader(), qiTxFee) // get the gas price by dividing the fee by qiTxGas - qiGasPrice := new(big.Int).Div(qiTxFeeInQuai, big.NewInt(int64(types.CalculateBlockQiTxGas(tx, p.hc.NodeLocation())))) + qiGasPrice := new(big.Int).Div(qiTxFeeInQuai, big.NewInt(int64(types.CalculateBlockQiTxGas(tx, qiScalingFactor, p.hc.NodeLocation())))) if minGasPrice == nil { minGasPrice = new(big.Int).Set(qiGasPrice) } else { @@ -446,8 +449,8 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty return nil, nil, nil, nil, 0, 0, 0, nil, err } utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, types.UTXOHash(etx.Hash(), outputIndex, utxo)) - utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKey(etx.Hash(), outputIndex)) - p.logger.Debugf("Creating UTXO for coinbase %032x with denomination %d index %d and lock %d\n", tx.Hash(), denomination, outputIndex, lockup.Int64()) + utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKeyWithDenomination(etx.Hash(), outputIndex, utxo.Denomination)) + p.logger.Debugf("Creating UTXO for coinbase %032x with denomination %d index %d\n", tx.Hash(), denomination, outputIndex) outputIndex++ } } @@ -516,7 +519,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty return nil, nil, nil, nil, 0, 0, 0, nil, err } utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, types.UTXOHash(etx.Hash(), outputIndex, utxo)) - utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKey(etx.Hash(), outputIndex)) + utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKeyWithDenomination(etx.Hash(), outputIndex, utxo.Denomination)) p.logger.Infof("Converting Quai to Qi %032x with denomination %d index %d lock %d\n", tx.Hash(), denomination, outputIndex, lock) outputIndex++ } @@ -528,7 +531,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty return nil, nil, nil, nil, 0, 0, 0, nil, err } utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, types.UTXOHash(etx.OriginatingTxHash(), etx.ETXIndex(), utxo)) - utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKey(etx.OriginatingTxHash(), etx.ETXIndex())) + utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKeyWithDenomination(etx.OriginatingTxHash(), etx.ETXIndex(), utxo.Denomination)) // This Qi ETX should cost more gas if err := gp.SubGas(params.CallValueTransferGas); err != nil { return nil, nil, nil, nil, 0, 0, 0, nil, err @@ -860,9 +863,9 @@ func ValidateQiTxInputs(tx *types.Transaction, chain ChainContext, db ethdb.Read } -func ValidateQiTxOutputsAndSignature(tx *types.Transaction, chain ChainContext, totalQitIn *big.Int, currentHeader *types.WorkObject, signer types.Signer, location common.Location, chainId big.Int, etxRLimit, etxPLimit int) (*big.Int, error) { +func ValidateQiTxOutputsAndSignature(tx *types.Transaction, chain ChainContext, totalQitIn *big.Int, currentHeader *types.WorkObject, signer types.Signer, location common.Location, chainId big.Int, qiScalingFactor float64, etxRLimit, etxPLimit int) (*big.Int, error) { - intrinsicGas := types.CalculateIntrinsicQiTxGas(tx) + intrinsicGas := types.CalculateIntrinsicQiTxGas(tx, qiScalingFactor) usedGas := intrinsicGas primeTerminusHash := currentHeader.PrimeTerminusHash() @@ -1011,7 +1014,7 @@ func ValidateQiTxOutputsAndSignature(tx *types.Transaction, chain ChainContext, return txFee, nil } -func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFirstQiTx bool, currentHeader *types.WorkObject, batch ethdb.Batch, db ethdb.Reader, gp *types.GasPool, usedGas *uint64, signer types.Signer, location common.Location, chainId big.Int, etxRLimit, etxPLimit *int, utxosCreatedDeleted *UtxosCreatedDeleted) (*big.Int, *big.Int, []*types.ExternalTx, error, map[string]time.Duration) { +func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFirstQiTx bool, currentHeader *types.WorkObject, batch ethdb.Batch, db ethdb.Reader, gp *types.GasPool, usedGas *uint64, signer types.Signer, location common.Location, chainId big.Int, qiScalingFactor float64, etxRLimit, etxPLimit *int, utxosCreatedDeleted *UtxosCreatedDeleted) (*big.Int, *big.Int, []*types.ExternalTx, error, map[string]time.Duration) { var elapsedTime time.Duration stepTimings := make(map[string]time.Duration) @@ -1028,7 +1031,7 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFir if currentHeader == nil || batch == nil || gp == nil || usedGas == nil || signer == nil || etxRLimit == nil || etxPLimit == nil { return nil, nil, nil, errors.New("one of the parameters is nil"), nil } - intrinsicGas := types.CalculateIntrinsicQiTxGas(tx) + intrinsicGas := types.CalculateIntrinsicQiTxGas(tx, qiScalingFactor) *usedGas += intrinsicGas if err := gp.SubGas(intrinsicGas); err != nil { return nil, nil, nil, err, nil @@ -1181,7 +1184,7 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFir return nil, nil, nil, err, nil } utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, types.UTXOHash(tx.Hash(), uint16(txOutIdx), utxo)) - utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKey(tx.Hash(), uint16(txOutIdx))) + utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKeyWithDenomination(tx.Hash(), uint16(txOutIdx), utxo.Denomination)) } } elapsedTime = time.Since(stepStart) diff --git a/core/tx_pool.go b/core/tx_pool.go index d68f0b6b66..7b39026492 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -30,6 +30,7 @@ import ( "github.com/dominant-strategies/go-quai/common/hexutil" "github.com/dominant-strategies/go-quai/common/prque" "github.com/dominant-strategies/go-quai/consensus" + "github.com/dominant-strategies/go-quai/core/rawdb" "github.com/dominant-strategies/go-quai/core/state" "github.com/dominant-strategies/go-quai/core/types" "github.com/dominant-strategies/go-quai/ethdb" @@ -315,10 +316,11 @@ type TxPool struct { signer types.Signer mu sync.RWMutex - currentState *state.StateDB // Current state in the blockchain head - db ethdb.Reader - pendingNonces *txNoncer // Pending state tracking virtual nonces - currentMaxGas uint64 // Current gas limit for transaction caps + currentState *state.StateDB // Current state in the blockchain head + qiGasScalingFactor float64 + db ethdb.Reader + pendingNonces *txNoncer // Pending state tracking virtual nonces + currentMaxGas uint64 // Current gas limit for transaction caps locals *accountSet // Set of local transaction to exempt from eviction rules journal *txJournal // Journal of local transaction to back up to disk @@ -1227,7 +1229,7 @@ func (pool *TxPool) addQiTxs(txs types.Transactions) []error { errs = append(errs, err) continue } - txFee, err := ValidateQiTxOutputsAndSignature(tx, pool.chain, totalQitIn, currentBlock, pool.signer, pool.chainconfig.Location, *pool.chainconfig.ChainID, etxRLimit, etxPLimit) + txFee, err := ValidateQiTxOutputsAndSignature(tx, pool.chain, totalQitIn, currentBlock, pool.signer, pool.chainconfig.Location, *pool.chainconfig.ChainID, pool.qiGasScalingFactor, etxRLimit, etxPLimit) if err != nil { pool.logger.WithFields(logrus.Fields{ "tx": tx.Hash().String(), @@ -1299,7 +1301,7 @@ func (pool *TxPool) addQiTxsWithoutValidationLocked(txs types.Transactions) { }).Debug("Invalid Qi transaction, skipping re-inject") continue } - fee, err = ValidateQiTxOutputsAndSignature(tx, pool.chain, totalQitIn, currentBlock, pool.signer, pool.chainconfig.Location, *pool.chainconfig.ChainID, etxRLimit, etxPLimit) + fee, err = ValidateQiTxOutputsAndSignature(tx, pool.chain, totalQitIn, currentBlock, pool.signer, pool.chainconfig.Location, *pool.chainconfig.ChainID, pool.qiGasScalingFactor, etxRLimit, etxPLimit) if err != nil { pool.logger.WithFields(logrus.Fields{ "tx": tx.Hash().String(), @@ -1852,6 +1854,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.WorkObject) { return } pool.currentState = statedb + pool.qiGasScalingFactor = math.Log(float64(rawdb.ReadUTXOSetSize(pool.db, newHead.Hash()))) pool.pendingNonces = newTxNoncer(statedb) pool.currentMaxGas = newHead.GasLimit() diff --git a/core/types/qi_tx.go b/core/types/qi_tx.go index 67eab8dce3..6fa89852c7 100644 --- a/core/types/qi_tx.go +++ b/core/types/qi_tx.go @@ -132,11 +132,11 @@ func (tx *QiTx) isCoinbase() bool { } // CalculateQiTxGas calculates the total amount of gas a Qi tx uses (for fee calculation) -func CalculateQiTxGas(transaction *Transaction, location common.Location) uint64 { +func CalculateQiTxGas(transaction *Transaction, qiScalingFactor float64, location common.Location) uint64 { if transaction.Type() != QiTxType { panic("CalculateQiTxGas called on a transaction that is not a Qi transaction") } - txGas := CalculateIntrinsicQiTxGas(transaction) + txGas := CalculateIntrinsicQiTxGas(transaction, qiScalingFactor) for _, output := range transaction.TxOut() { toAddr := common.AddressBytes(output.Address) if !location.Equal(*toAddr.Location()) { @@ -151,11 +151,11 @@ func CalculateQiTxGas(transaction *Transaction, location common.Location) uint64 } // CalculateBlockQiTxGas calculates the amount of gas a Qi tx uses in a block (for block gas limit calculation) -func CalculateBlockQiTxGas(transaction *Transaction, location common.Location) uint64 { +func CalculateBlockQiTxGas(transaction *Transaction, qiScalingFactor float64, location common.Location) uint64 { if transaction.Type() != QiTxType { panic("CalculateQiTxGas called on a transaction that is not a Qi transaction") } - txGas := CalculateIntrinsicQiTxGas(transaction) + txGas := CalculateIntrinsicQiTxGas(transaction, qiScalingFactor) for _, output := range transaction.TxOut() { toAddr := common.AddressBytes(output.Address) if !location.Equal(*toAddr.Location()) { @@ -170,11 +170,12 @@ func CalculateBlockQiTxGas(transaction *Transaction, location common.Location) u } // CalculateIntrinsicQiTxGas calculates the intrinsic gas for a Qi tx without ETXs -func CalculateIntrinsicQiTxGas(transaction *Transaction) uint64 { +func CalculateIntrinsicQiTxGas(transaction *Transaction, scalingFactor float64) uint64 { if transaction.Type() != QiTxType { panic("CalculateIntrinsicQiTxGas called on a transaction that is not a Qi transaction") } - return uint64(len(transaction.TxIn()))*params.SloadGas + uint64(len(transaction.TxOut()))*params.CallValueTransferGas + params.EcrecoverGas + baseRate := uint64(len(transaction.TxIn()))*params.SloadGas + uint64(len(transaction.TxOut()))*params.CallValueTransferGas + params.EcrecoverGas + return params.CalculateQiGasWithUTXOSetSizeScalingFactor(scalingFactor, baseRate) } func (tx *QiTx) setTo(to common.Address) { diff --git a/core/types/utxo.go b/core/types/utxo.go index edaf82c83c..26dad56c76 100644 --- a/core/types/utxo.go +++ b/core/types/utxo.go @@ -14,7 +14,9 @@ import ( const ( MaxDenomination = 16 - MaxOutputIndex = math.MaxUint16 + MaxOutputIndex = math.MaxUint16 + MaxTrimDenomination = 6 + MaxTrimCollisionsPerKeyPerBlock = 1000 ) var MaxQi = new(big.Int).Mul(big.NewInt(math.MaxInt64), big.NewInt(params.Ether)) // This is just a default; determine correct value later @@ -46,15 +48,13 @@ func init() { Denominations[16] = big.NewInt(1000000000) // 1000000 Qi TrimDepths = make(map[uint8]uint64) - TrimDepths[0] = 100 - TrimDepths[1] = 200 - TrimDepths[2] = 300 - TrimDepths[3] = 400 - TrimDepths[4] = 500 - TrimDepths[5] = 600 - TrimDepths[6] = 700 - TrimDepths[7] = 800 - TrimDepths[8] = 900 + TrimDepths[0] = 720 // 2 hours + TrimDepths[1] = 720 // 2 hours + TrimDepths[2] = 1080 // 3 hours + TrimDepths[3] = 1080 // 3 hours + TrimDepths[4] = 2160 // 6 hours + TrimDepths[5] = 4320 // 12 hours + TrimDepths[6] = 8640 // 24 hours } type TxIns []TxIn diff --git a/core/worker.go b/core/worker.go index 81400d4155..b012c7bdc0 100644 --- a/core/worker.go +++ b/core/worker.go @@ -85,6 +85,8 @@ type environment struct { parentStateSize *big.Int quaiCoinbaseEtxs map[[21]byte]*big.Int deletedUtxos map[common.Hash]struct{} + qiGasScalingFactor float64 + utxoSetSize uint64 } // unclelist returns the contained uncles as the list format. @@ -619,7 +621,7 @@ func (w *worker) GeneratePendingHeader(block *types.WorkObject, fill bool, txs t } // Create a local environment copy, avoid the data race with snapshot state. - newWo, err := w.FinalizeAssemble(w.hc, work.wo, block, work.state, work.txs, uncles, work.etxs, work.subManifest, work.receipts, work.utxosCreate, work.utxosDelete) + newWo, err := w.FinalizeAssemble(w.hc, work.wo, block, work.state, work.txs, uncles, work.etxs, work.subManifest, work.receipts, work.utxoSetSize, work.utxosCreate, work.utxosDelete) if err != nil { return nil, err } @@ -721,6 +723,7 @@ func (w *worker) OrderTransactionSet(txs []*types.Transaction, gasUsedAfterTrans return } } + utxoSetSize := rawdb.ReadUTXOSetSize(w.workerDb, block.Hash()) // gas price and the gas used is extracted from each of these non external // transactions for the sorting txInfos := make([]TransactionInfo, 0) @@ -736,7 +739,7 @@ func (w *worker) OrderTransactionSet(txs []*types.Transaction, gasUsedAfterTrans } qiFeeInQuai := misc.QiToQuai(primeTerminus.WorkObjectHeader(), fee) // Divide the fee by the gas used by the Qi Tx - gasPrice = new(big.Int).Div(qiFeeInQuai, new(big.Int).SetInt64(int64(types.CalculateBlockQiTxGas(tx, w.hc.NodeLocation())))) + gasPrice = new(big.Int).Div(qiFeeInQuai, new(big.Int).SetInt64(int64(types.CalculateBlockQiTxGas(tx, math.Log(float64(utxoSetSize)), w.hc.NodeLocation())))) } else { gasPrice = new(big.Int).Set(tx.GasPrice()) } @@ -873,10 +876,12 @@ func (w *worker) makeEnv(parent *types.WorkObject, proposedWo *types.WorkObject, evmRoot := parent.EVMRoot() etxRoot := parent.EtxSetRoot() quaiStateSize := parent.QuaiStateSize() + utxoSetSize := rawdb.ReadUTXOSetSize(w.workerDb, parent.Hash()) if w.hc.IsGenesisHash(parent.Hash()) { evmRoot = types.EmptyRootHash etxRoot = types.EmptyRootHash quaiStateSize = big.NewInt(0) + utxoSetSize = 0 } state, err := w.hc.bc.processor.StateAt(evmRoot, etxRoot, quaiStateSize) if err != nil { @@ -893,19 +898,21 @@ func (w *worker) makeEnv(parent *types.WorkObject, proposedWo *types.WorkObject, } // Note the passed coinbase may be different with header.Coinbase. env := &environment{ - signer: types.MakeSigner(w.chainConfig, proposedWo.Number(w.hc.NodeCtx())), - state: state, - primaryCoinbase: primaryCoinbase, - secondaryCoinbase: secondaryCoinbase, - ancestors: mapset.NewSet(), - family: mapset.NewSet(), - wo: proposedWo, - uncles: make(map[common.Hash]*types.WorkObjectHeader), - etxRLimit: etxRLimit, - etxPLimit: etxPLimit, - parentStateSize: quaiStateSize, - quaiCoinbaseEtxs: make(map[[21]byte]*big.Int), - deletedUtxos: make(map[common.Hash]struct{}), + signer: types.MakeSigner(w.chainConfig, proposedWo.Number(w.hc.NodeCtx())), + state: state, + primaryCoinbase: primaryCoinbase, + secondaryCoinbase: secondaryCoinbase, + ancestors: mapset.NewSet(), + family: mapset.NewSet(), + wo: proposedWo, + uncles: make(map[common.Hash]*types.WorkObjectHeader), + etxRLimit: etxRLimit, + etxPLimit: etxPLimit, + parentStateSize: quaiStateSize, + quaiCoinbaseEtxs: make(map[[21]byte]*big.Int), + deletedUtxos: make(map[common.Hash]struct{}), + qiGasScalingFactor: math.Log(float64(utxoSetSize)), + utxoSetSize: utxoSetSize, } // Keep track of transactions which return errors so they can be removed env.tcount = 0 @@ -1182,7 +1189,7 @@ func (w *worker) commitTransactions(env *environment, primeTerminus *types.WorkO break } if tx.Type() == types.QiTxType { - txGas := types.CalculateBlockQiTxGas(tx, w.hc.NodeLocation()) + txGas := types.CalculateBlockQiTxGas(tx, env.qiGasScalingFactor, w.hc.NodeLocation()) if env.gasPool.Gas() < txGas { w.logger.WithFields(log.Fields{ "have": env.gasPool, @@ -1715,7 +1722,7 @@ func (w *worker) fillTransactions(env *environment, primeTerminus *types.WorkObj for _, tx := range pendingQiTxs { // update the fee qiFeeInQuai := misc.QiToQuai(primeTerminus, tx.MinerFee()) - minerFeeInQuai := new(big.Int).Div(qiFeeInQuai, big.NewInt(int64(types.CalculateBlockQiTxGas(tx.Tx(), w.hc.NodeLocation())))) + minerFeeInQuai := new(big.Int).Div(qiFeeInQuai, big.NewInt(int64(types.CalculateBlockQiTxGas(tx.Tx(), env.qiGasScalingFactor, w.hc.NodeLocation())))) if minerFeeInQuai.Cmp(big.NewInt(0)) == 0 { w.logger.Error("rejecting qi tx that has zero gas price") continue @@ -1785,9 +1792,9 @@ func (w *worker) ComputeManifestHash(header *types.WorkObject) common.Hash { return manifestHash } -func (w *worker) FinalizeAssemble(chain consensus.ChainHeaderReader, newWo *types.WorkObject, parent *types.WorkObject, state *state.StateDB, txs []*types.Transaction, uncles []*types.WorkObjectHeader, etxs []*types.Transaction, subManifest types.BlockManifest, receipts []*types.Receipt, utxosCreate, utxosDelete []common.Hash) (*types.WorkObject, error) { +func (w *worker) FinalizeAssemble(chain consensus.ChainHeaderReader, newWo *types.WorkObject, parent *types.WorkObject, state *state.StateDB, txs []*types.Transaction, uncles []*types.WorkObjectHeader, etxs []*types.Transaction, subManifest types.BlockManifest, receipts []*types.Receipt, parentUtxoSetSize uint64, utxosCreate, utxosDelete []common.Hash) (*types.WorkObject, error) { nodeCtx := w.hc.NodeCtx() - wo, err := w.engine.FinalizeAndAssemble(chain, newWo, state, txs, uncles, etxs, subManifest, receipts, utxosCreate, utxosDelete) + wo, err := w.engine.FinalizeAndAssemble(chain, newWo, state, txs, uncles, etxs, subManifest, receipts, parentUtxoSetSize, utxosCreate, utxosDelete) if err != nil { return nil, err } @@ -1909,7 +1916,7 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment, primeTermi return fmt.Errorf("tx %032x has wrong chain ID", tx.Hash()) } gasUsed := env.wo.GasUsed() - intrinsicGas := types.CalculateIntrinsicQiTxGas(tx) + intrinsicGas := types.CalculateIntrinsicQiTxGas(tx, env.qiGasScalingFactor) gasUsed += intrinsicGas // the amount of block gas used in this transaction is only the txGas, regardless of ETXs emitted if err := env.gasPool.SubGas(intrinsicGas); err != nil { return err diff --git a/internal/quaiapi/quai_api.go b/internal/quaiapi/quai_api.go index 0db0049959..b4e4042c3e 100644 --- a/internal/quaiapi/quai_api.go +++ b/internal/quaiapi/quai_api.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "math" "math/big" "time" @@ -27,6 +28,7 @@ import ( "github.com/dominant-strategies/go-quai/common/hexutil" "github.com/dominant-strategies/go-quai/consensus/misc" "github.com/dominant-strategies/go-quai/core" + "github.com/dominant-strategies/go-quai/core/rawdb" "github.com/dominant-strategies/go-quai/core/types" "github.com/dominant-strategies/go-quai/crypto" "github.com/dominant-strategies/go-quai/log" @@ -435,7 +437,15 @@ func (s *PublicBlockChainQuaiAPI) EstimateGas(ctx context.Context, args Transact } switch args.TxType { case types.QiTxType: - return args.CalculateQiTxGas(s.b.NodeLocation()) + block, err := s.b.BlockByNumberOrHash(ctx, bNrOrHash) + if err != nil { + return 0, err + } + if block == nil { + return 0, errors.New("block not found: " + fmt.Sprintf("%v", bNrOrHash)) + } + scalingFactor := math.Log(float64(rawdb.ReadUTXOSetSize(s.b.Database(), block.Hash()))) + return args.CalculateQiTxGas(scalingFactor, s.b.NodeLocation()) case types.QuaiTxType: return DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap()) default: @@ -497,11 +507,6 @@ func (s *PublicBlockChainQuaiAPI) BaseFee(ctx context.Context, txType bool) (*he // EstimateFeeForQi returns an estimate of the amount of Qi in qits needed to execute the // given transaction against the current pending block. func (s *PublicBlockChainQuaiAPI) EstimateFeeForQi(ctx context.Context, args TransactionArgs) (*hexutil.Big, error) { - // Estimate the gas - gas, err := args.CalculateQiTxGas(s.b.NodeLocation()) - if err != nil { - return nil, err - } header := s.b.CurrentBlock() if header == nil { return nil, errors.New("no header available") @@ -511,6 +516,13 @@ func (s *PublicBlockChainQuaiAPI) EstimateFeeForQi(ctx context.Context, args Tra if chainCfg == nil { return nil, errors.New("no chain config available") } + scalingFactor := math.Log(float64(rawdb.ReadUTXOSetSize(s.b.Database(), header.Hash()))) + // Estimate the gas + gas, err := args.CalculateQiTxGas(scalingFactor, s.b.NodeLocation()) + if err != nil { + return nil, err + } + // Calculate the base fee quaiBaseFee := header.BaseFee() feeInQuai := new(big.Int).Mul(new(big.Int).SetUint64(uint64(gas)), quaiBaseFee) diff --git a/internal/quaiapi/transaction_args.go b/internal/quaiapi/transaction_args.go index 41cd61bd98..84e75907c1 100644 --- a/internal/quaiapi/transaction_args.go +++ b/internal/quaiapi/transaction_args.go @@ -187,7 +187,7 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int, no } // CalculateQiTxGas calculates the gas usage of a Qi transaction. -func (args *TransactionArgs) CalculateQiTxGas(location common.Location) (hexutil.Uint64, error) { +func (args *TransactionArgs) CalculateQiTxGas(qiScalingFactor float64, location common.Location) (hexutil.Uint64, error) { if args.TxType != types.QiTxType { return 0, errors.New("not a Qi transaction") } @@ -202,5 +202,5 @@ func (args *TransactionArgs) CalculateQiTxGas(location common.Location) (hexutil } tx := types.NewTx(qiTx) - return hexutil.Uint64(types.CalculateQiTxGas(tx, location)), nil + return hexutil.Uint64(types.CalculateQiTxGas(tx, qiScalingFactor, location)), nil } diff --git a/params/protocol_params.go b/params/protocol_params.go index b98dfc2cae..72a6e29a55 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -153,7 +153,7 @@ const ( ConversionLockPeriod uint64 = 10 // The number of zone blocks that a conversion output is locked for MinQiConversionDenomination = 1 ConversionConfirmationContext = common.PRIME_CTX // A conversion requires a single coincident Dom confirmation - MaxUTXOSetSize = 1000000 // The maximum number of UTXOs that can be stored in the UTXO set + SoftMaxUTXOSetSize = 1000000 // The maximum number of UTXOs that can be stored in the UTXO set ) var ( @@ -277,3 +277,10 @@ func CalculateCoinbaseValueWithLockup(value *big.Int, lockupByte uint8) *big.Int } return new(big.Int).Add(value, new(big.Int).Div(value, LockupByteToRewardsRatio[lockupByte])) } + +func CalculateQiGasWithUTXOSetSizeScalingFactor(scalingFactor float64, baseRate uint64) uint64 { + if scalingFactor < 15 { + return baseRate + } + return uint64(scalingFactor*float64(baseRate)) / 15 +} From 9744cf3aaed41d923be68ad69b9dbd67f0c0860e Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Thu, 19 Sep 2024 12:45:43 -0500 Subject: [PATCH 4/6] Qi tx gas cost increases relative to log of set size --- consensus/blake3pow/consensus.go | 14 ++++++-------- consensus/consensus.go | 2 +- consensus/progpow/consensus.go | 15 +++++++-------- core/state_processor.go | 2 +- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/consensus/blake3pow/consensus.go b/consensus/blake3pow/consensus.go index 84afbdd816..882e5be033 100644 --- a/consensus/blake3pow/consensus.go +++ b/consensus/blake3pow/consensus.go @@ -608,11 +608,10 @@ func (blake3pow *Blake3pow) Prepare(chain consensus.ChainHeaderReader, header *t // Finalize implements consensus.Engine, accumulating the block and uncle rewards, // setting the final state on the header // Finalize returns the new MuHash of the UTXO set, the new size of the UTXO set and an error if any -func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) { +func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, utxoSetSize uint64, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) { nodeLocation := blake3pow.config.NodeLocation nodeCtx := blake3pow.config.NodeLocation.Context() var multiSet *multiset.MultiSet - var utxoSetSize uint64 if chain.IsGenesisHash(header.ParentHash(nodeCtx)) { multiSet = multiset.New() // Create the lockup contract account @@ -653,14 +652,17 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch et } } else { multiSet = rawdb.ReadMultiSet(chain.Database(), header.ParentHash(nodeCtx)) - utxoSetSize = rawdb.ReadUTXOSetSize(chain.Database(), header.ParentHash(nodeCtx)) } if multiSet == nil { return nil, 0, fmt.Errorf("Multiset is nil for block %s", header.ParentHash(nodeCtx).String()) } utxoSetSize += uint64(len(utxosCreate)) + if utxoSetSize < uint64(len(utxosDelete)) { + return nil, 0, fmt.Errorf("UTXO set size is less than the number of utxos to delete. This is a bug. UTXO set size: %d, UTXOs to delete: %d", utxoSetSize, len(utxosDelete)) + } utxoSetSize -= uint64(len(utxosDelete)) + trimDepths := types.TrimDepths if utxoSetSize > params.SoftMaxUTXOSetSize/2 { var err error @@ -748,10 +750,6 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch et } blake3pow.logger.Infof("Parent hash: %s, header hash: %s, muhash: %s, block height: %d, setroots: %t, UtxosCreated: %d, UtxosDeleted: %d, UTXOs Trimmed from DB: %d, UTXO Set Size: %d", header.ParentHash(nodeCtx).String(), header.Hash().String(), multiSet.Hash().String(), header.NumberU64(nodeCtx), setRoots, len(utxosCreate), len(utxosDelete), len(trimmedUtxos), utxoSetSize) - if utxoSetSize < uint64(len(utxosDelete)) { - return nil, 0, fmt.Errorf("UTXO set size is less than the number of utxos to delete. This is a bug. UTXO set size: %d, UTXOs to delete: %d", utxoSetSize, len(utxosDelete)) - } - if setRoots { header.Header().SetUTXORoot(multiSet.Hash()) header.Header().SetEVMRoot(state.IntermediateRoot(true)) @@ -938,7 +936,7 @@ func (blake3pow *Blake3pow) FinalizeAndAssemble(chain consensus.ChainHeaderReade nodeCtx := blake3pow.config.NodeLocation.Context() if nodeCtx == common.ZONE_CTX && chain.ProcessingState() { // Finalize block - if _, _, err := blake3pow.Finalize(chain, nil, header, state, true, utxosCreate, utxosDelete); err != nil { + if _, _, err := blake3pow.Finalize(chain, nil, header, state, true, parentUtxoSetSize, utxosCreate, utxosDelete); err != nil { return nil, err } } diff --git a/consensus/consensus.go b/consensus/consensus.go index 0f519d9e4a..0d6a1a888e 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -189,7 +189,7 @@ type Engine interface { // // Note: The block header and state database might be updated to reflect any // consensus rules that happen at finalization (e.g. block rewards). - Finalize(chain ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) + Finalize(chain ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, parentUtxoSetSize uint64, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) // FinalizeAndAssemble runs any post-transaction state modifications (e.g. block // rewards) and assembles the final block. diff --git a/consensus/progpow/consensus.go b/consensus/progpow/consensus.go index 4bb2cdf92f..eb72a4e867 100644 --- a/consensus/progpow/consensus.go +++ b/consensus/progpow/consensus.go @@ -666,11 +666,10 @@ func (progpow *Progpow) Prepare(chain consensus.ChainHeaderReader, header *types // Finalize implements consensus.Engine, accumulating the block and uncle rewards, // setting the final state on the header // Finalize returns the new MuHash of the UTXO set, the new size of the UTXO set and an error if any -func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) { +func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb.Batch, header *types.WorkObject, state *state.StateDB, setRoots bool, utxoSetSize uint64, utxosCreate, utxosDelete []common.Hash) (*multiset.MultiSet, uint64, error) { nodeLocation := progpow.NodeLocation() nodeCtx := progpow.NodeLocation().Context() var multiSet *multiset.MultiSet - var utxoSetSize uint64 if chain.IsGenesisHash(header.ParentHash(nodeCtx)) { multiSet = multiset.New() // Create the lockup contract account @@ -711,13 +710,17 @@ func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb. } } else { multiSet = rawdb.ReadMultiSet(chain.Database(), header.ParentHash(nodeCtx)) - utxoSetSize = rawdb.ReadUTXOSetSize(chain.Database(), header.ParentHash(nodeCtx)) } if multiSet == nil { return nil, 0, fmt.Errorf("Multiset is nil for block %s", header.ParentHash(nodeCtx).String()) } + utxoSetSize += uint64(len(utxosCreate)) + if utxoSetSize < uint64(len(utxosDelete)) { + return nil, 0, fmt.Errorf("UTXO set size is less than the number of utxos to delete. This is a bug. UTXO set size: %d, UTXOs to delete: %d", utxoSetSize, len(utxosDelete)) + } utxoSetSize -= uint64(len(utxosDelete)) + trimDepths := types.TrimDepths if utxoSetSize > params.SoftMaxUTXOSetSize/2 { var err error @@ -805,10 +808,6 @@ func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb. } progpow.logger.Infof("Parent hash: %s, header hash: %s, muhash: %s, block height: %d, setroots: %t, UtxosCreated: %d, UtxosDeleted: %d, UTXOs Trimmed from DB: %d, UTXO Set Size: %d", header.ParentHash(nodeCtx).String(), header.Hash().String(), multiSet.Hash().String(), header.NumberU64(nodeCtx), setRoots, len(utxosCreate), len(utxosDelete), len(trimmedUtxos), utxoSetSize) - if utxoSetSize < uint64(len(utxosDelete)) { - return nil, 0, fmt.Errorf("UTXO set size is less than the number of utxos to delete. This is a bug. UTXO set size: %d, UTXOs to delete: %d", utxoSetSize, len(utxosDelete)) - } - if setRoots { header.Header().SetUTXORoot(multiSet.Hash()) header.Header().SetEVMRoot(state.IntermediateRoot(true)) @@ -986,7 +985,7 @@ func (progpow *Progpow) FinalizeAndAssemble(chain consensus.ChainHeaderReader, h nodeCtx := progpow.config.NodeLocation.Context() if nodeCtx == common.ZONE_CTX && chain.ProcessingState() { // Finalize block - if _, _, err := progpow.Finalize(chain, nil, header, state, true, utxosCreate, utxosDelete); err != nil { + if _, _, err := progpow.Finalize(chain, nil, header, state, true, parentUtxoSetSize, utxosCreate, utxosDelete); err != nil { return nil, err } } diff --git a/core/state_processor.go b/core/state_processor.go index d29352bd7d..13d7b1f822 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -691,7 +691,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty time4 := common.PrettyDuration(time.Since(start)) // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) - multiSet, utxoSetSize, err := p.engine.Finalize(p.hc, batch, block, statedb, false, utxosCreatedDeleted.UtxosCreatedHashes, utxosCreatedDeleted.UtxosDeletedHashes) + multiSet, utxoSetSize, err := p.engine.Finalize(p.hc, batch, block, statedb, false, parentUtxoSetSize, utxosCreatedDeleted.UtxosCreatedHashes, utxosCreatedDeleted.UtxosDeletedHashes) if err != nil { return nil, nil, nil, nil, 0, 0, 0, nil, err } From 7a8bcd51b396614d3c9a0388bbfd8079bf140d8f Mon Sep 17 00:00:00 2001 From: gop Date: Wed, 9 Oct 2024 10:44:03 -0500 Subject: [PATCH 5/6] bugfix: if we reach genesis, return the genesis as common --- core/rawdb/accessors_chain.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 2f37cb051e..f900a32695 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -750,8 +750,8 @@ func IsGenesisHash(db ethdb.Reader, hash common.Hash) bool { func FindCommonAncestor(db ethdb.Reader, a, b *types.WorkObject, nodeCtx int) (*types.WorkObject, error) { for bn := b.NumberU64(nodeCtx); a.NumberU64(nodeCtx) > bn; { a = ReadHeader(db, a.NumberU64(nodeCtx)-1, a.ParentHash(nodeCtx)) - if IsGenesisHash(db, a.ParentHash(nodeCtx)) { - return nil, fmt.Errorf("no common ancestor found") + if IsGenesisHash(db, a.Hash()) { + return a, nil } if a == nil { return nil, fmt.Errorf("unable to find hash %s", a.ParentHash(nodeCtx).String()) @@ -759,8 +759,8 @@ func FindCommonAncestor(db ethdb.Reader, a, b *types.WorkObject, nodeCtx int) (* } for an := a.NumberU64(nodeCtx); an < b.NumberU64(nodeCtx); { b = ReadHeader(db, b.NumberU64(nodeCtx)-1, b.ParentHash(nodeCtx)) - if IsGenesisHash(db, b.ParentHash(nodeCtx)) { - return nil, fmt.Errorf("no common ancestor found") + if IsGenesisHash(db, b.Hash()) { + return b, nil } if b == nil { return nil, fmt.Errorf("unable to find hash %s", b.ParentHash(nodeCtx).String()) From 53eeb53f2b320540bf39fcb013f02218f2517490 Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Sat, 12 Oct 2024 13:41:12 -0500 Subject: [PATCH 6/6] Set minimum trim depth to MaxInt blocks and set soft max UTXO set size to 10m --- consensus/blake3pow/consensus.go | 6 ++++-- consensus/progpow/consensus.go | 6 ++++-- params/protocol_params.go | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/consensus/blake3pow/consensus.go b/consensus/blake3pow/consensus.go index 882e5be033..8ac7ec9c7a 100644 --- a/consensus/blake3pow/consensus.go +++ b/consensus/blake3pow/consensus.go @@ -691,7 +691,7 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch et var wg sync.WaitGroup var lock sync.Mutex for denomination, depth := range trimDepths { - if denomination <= types.MaxTrimDenomination && header.NumberU64(nodeCtx) > depth+1 { + if denomination <= types.MaxTrimDenomination && header.NumberU64(nodeCtx) > depth+params.MinimumTrimDepth { wg.Add(1) go func(denomination uint8, depth uint64) { defer func() { @@ -735,7 +735,9 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, batch et }() } wg.Wait() - blake3pow.logger.Infof("Trimmed %d UTXOs from db in %s", len(trimmedUtxos), common.PrettyDuration(time.Since(start))) + if len(trimmedUtxos) > 0 { + blake3pow.logger.Infof("Trimmed %d UTXOs from db in %s", len(trimmedUtxos), common.PrettyDuration(time.Since(start))) + } if !setRoots { rawdb.WriteTrimmedUTXOs(batch, header.Hash(), trimmedUtxos) if len(newCollidingKeys) > 0 { diff --git a/consensus/progpow/consensus.go b/consensus/progpow/consensus.go index eb72a4e867..02915998df 100644 --- a/consensus/progpow/consensus.go +++ b/consensus/progpow/consensus.go @@ -749,7 +749,7 @@ func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb. var wg sync.WaitGroup var lock sync.Mutex for denomination, depth := range trimDepths { - if denomination <= types.MaxTrimDenomination && header.NumberU64(nodeCtx) > depth+1 { + if denomination <= types.MaxTrimDenomination && header.NumberU64(nodeCtx) > depth+params.MinimumTrimDepth { wg.Add(1) go func(denomination uint8, depth uint64) { defer func() { @@ -793,7 +793,9 @@ func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, batch ethdb. }() } wg.Wait() - progpow.logger.Infof("Trimmed %d UTXOs from db in %s", len(trimmedUtxos), common.PrettyDuration(time.Since(start))) + if len(trimmedUtxos) > 0 { + progpow.logger.Infof("Trimmed %d UTXOs from db in %s", len(trimmedUtxos), common.PrettyDuration(time.Since(start))) + } if !setRoots { rawdb.WriteTrimmedUTXOs(batch, header.Hash(), trimmedUtxos) if len(newCollidingKeys) > 0 { diff --git a/params/protocol_params.go b/params/protocol_params.go index 72a6e29a55..3747704d4a 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -153,7 +153,8 @@ const ( ConversionLockPeriod uint64 = 10 // The number of zone blocks that a conversion output is locked for MinQiConversionDenomination = 1 ConversionConfirmationContext = common.PRIME_CTX // A conversion requires a single coincident Dom confirmation - SoftMaxUTXOSetSize = 1000000 // The maximum number of UTXOs that can be stored in the UTXO set + SoftMaxUTXOSetSize = 10000000 // The soft maximum number of UTXOs that can be stored in the UTXO set + MinimumTrimDepth = math.MaxInt // The minimum block depth of the chain to begin trimming ) var (