Get rid of genesis's UTXO dump (#1867)

* Get rid of genesis's UTXO dump

* Allow known orphans when AllowSubmitBlockWhenNotSynced=true

* gofmt

* Avoid IBD without changing the pruning point when only genesis is available

* Add DisallowDirectBlocksOnTopOfGenesis=true for mainnet

* Remove any mention to nobanning to let stability tests run

* Rename ifGenesisSetUtxoSet to loadUTXODataForGenesis

Co-authored-by: Ori Newman <>
This commit is contained in:
Ori Newman 2021-12-05 12:46:41 +02:00 committed by GitHub
parent 606b781ca0
commit 11103a36d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 103 additions and 122 deletions

View File

@ -25,6 +25,10 @@ func (f *FlowContext) ShouldMine() (bool, error) {
return false, err
}
if virtualSelectedParent.Equal(f.Config().NetParams().GenesisHash) {
return false, nil
}
virtualSelectedParentHeader, err := f.domain.Consensus().GetBlockHeader(virtualSelectedParent)
if err != nil {
return false, err

View File

@ -81,7 +81,18 @@ func (flow *handleRelayInvsFlow) start() error {
continue
}
isGenesisVirtualSelectedParent, err := flow.isGenesisVirtualSelectedParent()
if err != nil {
return err
}
if flow.IsOrphan(inv.Hash) {
if flow.Config().NetParams().DisallowDirectBlocksOnTopOfGenesis && !flow.Config().AllowSubmitBlockWhenNotSynced && isGenesisVirtualSelectedParent {
log.Infof("Cannot process orphan %s for a node with only the genesis block. The node needs to IBD "+
"to the recent pruning point before normal operation can resume.", inv.Hash)
continue
}
log.Debugf("Block %s is a known orphan. Requesting its missing ancestors", inv.Hash)
err := flow.AddOrphanRootsToQueue(inv.Hash)
if err != nil {
@ -111,6 +122,11 @@ func (flow *handleRelayInvsFlow) start() error {
return err
}
if flow.Config().NetParams().DisallowDirectBlocksOnTopOfGenesis && !flow.Config().AllowSubmitBlockWhenNotSynced && !flow.Config().Devnet && flow.isChildOfGenesis(block) {
log.Infof("Cannot process %s because it's a direct child of genesis.", consensushashing.BlockHash(block))
continue
}
log.Debugf("Processing block %s", inv.Hash)
missingParents, blockInsertionResult, err := flow.processBlock(block)
if err != nil {
@ -265,6 +281,19 @@ func (flow *handleRelayInvsFlow) processOrphan(block *externalapi.DomainBlock) e
return err
}
if isBlockInOrphanResolutionRange {
if flow.Config().NetParams().DisallowDirectBlocksOnTopOfGenesis && !flow.Config().AllowSubmitBlockWhenNotSynced {
isGenesisVirtualSelectedParent, err := flow.isGenesisVirtualSelectedParent()
if err != nil {
return err
}
if isGenesisVirtualSelectedParent {
log.Infof("Cannot process orphan %s for a node with only the genesis block. The node needs to IBD "+
"to the recent pruning point before normal operation can resume.", blockHash)
return nil
}
}
log.Debugf("Block %s is within orphan resolution range. "+
"Adding it to the orphan set", blockHash)
flow.AddOrphan(block)
@ -278,6 +307,20 @@ func (flow *handleRelayInvsFlow) processOrphan(block *externalapi.DomainBlock) e
return flow.runIBDIfNotRunning(block)
}
func (flow *handleRelayInvsFlow) isGenesisVirtualSelectedParent() (bool, error) {
virtualSelectedParent, err := flow.Domain().Consensus().GetVirtualSelectedParent()
if err != nil {
return false, err
}
return virtualSelectedParent.Equal(flow.Config().NetParams().GenesisHash), nil
}
func (flow *handleRelayInvsFlow) isChildOfGenesis(block *externalapi.DomainBlock) bool {
parents := block.Header.DirectParents()
return len(parents) == 1 && parents[0].Equal(flow.Config().NetParams().GenesisHash)
}
// isBlockInOrphanResolutionRange finds out whether the given blockHash should be
// retrieved via the unorphaning mechanism or via IBD. This method sends a
// getBlockLocator request to the peer with a limit of orphanResolutionRange.

View File

@ -55,6 +55,19 @@ func (flow *handleRelayInvsFlow) runIBDIfNotRunning(block *externalapi.DomainBlo
return err
}
} else {
if flow.Config().NetParams().DisallowDirectBlocksOnTopOfGenesis && !flow.Config().AllowSubmitBlockWhenNotSynced {
isGenesisVirtualSelectedParent, err := flow.isGenesisVirtualSelectedParent()
if err != nil {
return err
}
if isGenesisVirtualSelectedParent {
log.Infof("Cannot IBD to %s because it won't change the pruning point. The node needs to IBD "+
"to the recent pruning point before normal operation can resume.", highHash)
return nil
}
}
err = flow.syncPruningPointFutureHeaders(flow.Domain().Consensus(), highestSharedBlockHash, highHash)
if err != nil {
return err

View File

@ -1,32 +1,22 @@
package blockprocessor
import (
"bytes"
"compress/gzip"
"github.com/kaspanet/go-muhash"
// we need to embed the utxoset of mainnet genesis here
_ "embed"
"fmt"
"github.com/kaspanet/kaspad/domain/consensus/utils/multiset"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/infrastructure/db/database"
"github.com/kaspanet/kaspad/util/difficulty"
"github.com/kaspanet/kaspad/util/staging"
"io"
"sync"
"github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/consensus/utils/multiset"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/infrastructure/db/database"
"github.com/kaspanet/kaspad/infrastructure/logger"
"github.com/kaspanet/kaspad/util/difficulty"
"github.com/kaspanet/kaspad/util/staging"
"github.com/pkg/errors"
)
//go:embed resources/utxos.gz
var utxoDumpFile []byte
func (bp *blockProcessor) setBlockStatusAfterBlockValidation(
stagingArea *model.StagingArea, block *externalapi.DomainBlock, isPruningPoint bool) error {
@ -139,11 +129,7 @@ func (bp *blockProcessor) validateAndInsertBlock(stagingArea *model.StagingArea,
}
}
err = bp.ifGenesisSetUtxoSet(block)
if err != nil {
return nil, err
}
bp.loadUTXODataForGenesis(stagingArea, block)
var selectedParentChainChanges *externalapi.SelectedChainPath
var virtualUTXODiff externalapi.UTXODiff
var reversalData *model.UTXODiffReversalData
@ -231,91 +217,25 @@ func (bp *blockProcessor) validateAndInsertBlock(stagingArea *model.StagingArea,
}, nil
}
var mainnetGenesisUTXOSet externalapi.UTXODiff
var mainnetGenesisMultiSet model.Multiset
var mainnetGenesisOnce sync.Once
var mainnetGenesisErr error
func deserializeMainnetUTXOSet() (externalapi.UTXODiff, model.Multiset, error) {
mainnetGenesisOnce.Do(func() {
toAdd := make(map[externalapi.DomainOutpoint]externalapi.UTXOEntry)
mainnetGenesisMultiSet = multiset.New()
file, err := gzip.NewReader(bytes.NewReader(utxoDumpFile))
if err != nil {
mainnetGenesisErr = err
return
}
for i := 0; ; i++ {
size := make([]byte, 1)
_, err = io.ReadFull(file, size)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
mainnetGenesisErr = err
return
}
serializedUTXO := make([]byte, size[0])
_, err = io.ReadFull(file, serializedUTXO)
if err != nil {
mainnetGenesisErr = err
return
}
mainnetGenesisMultiSet.Add(serializedUTXO)
entry, outpoint, err := utxo.DeserializeUTXO(serializedUTXO)
if err != nil {
mainnetGenesisErr = err
return
}
toAdd[*outpoint] = entry
}
mainnetGenesisUTXOSet, mainnetGenesisErr = utxo.NewUTXODiffFromCollections(utxo.NewUTXOCollection(toAdd), utxo.NewUTXOCollection(make(map[externalapi.DomainOutpoint]externalapi.UTXOEntry)))
})
return mainnetGenesisUTXOSet, mainnetGenesisMultiSet, mainnetGenesisErr
}
func (bp *blockProcessor) ifGenesisSetUtxoSet(block *externalapi.DomainBlock) error {
func (bp *blockProcessor) loadUTXODataForGenesis(stagingArea *model.StagingArea, block *externalapi.DomainBlock) {
isGenesis := len(block.Header.DirectParents()) == 0
if !isGenesis {
return nil
return
}
blockHash := consensushashing.BlockHash(block)
if !block.Header.UTXOCommitment().Equal(externalapi.NewDomainHashFromByteArray(muhash.EmptyMuHashHash.AsArray())) {
log.Infof("Loading checkpoint UTXO set")
diff, utxoSetMultiset, err := deserializeMainnetUTXOSet()
if err != nil {
return err
}
log.Infof("Finished loading checkpoint UTXO set")
utxoSetHash := utxoSetMultiset.Hash()
if !utxoSetHash.Equal(block.Header.UTXOCommitment()) {
return errors.New("Invalid UTXO set dump")
}
area := model.NewStagingArea()
bp.consensusStateStore.StageVirtualUTXODiff(area, diff)
bp.utxoDiffStore.Stage(area, blockHash, diff, nil)
// commit the multiset of genesis
bp.multisetStore.Stage(area, blockHash, utxoSetMultiset)
err = staging.CommitAllChanges(bp.databaseContext, area)
if err != nil {
return err
}
} else {
// if it's genesis but has an empty muhash then commit an empty multiset and an empty diff
area := model.NewStagingArea()
bp.consensusStateStore.StageVirtualUTXODiff(area, utxo.NewUTXODiff())
bp.utxoDiffStore.Stage(area, blockHash, utxo.NewUTXODiff(), nil)
bp.multisetStore.Stage(area, blockHash, multiset.New())
err := staging.CommitAllChanges(bp.databaseContext, area)
if err != nil {
return err
}
}
return nil
// Note: The applied UTXO set and multiset do not satisfy the UTXO commitment
// of Mainnet's genesis. This is why any block that will be built on top of genesis
// will have a wrong UTXO commitment as well, and will not be able to get to a consensus
// with the rest of the network.
// This is why getting direct blocks on top of genesis is forbidden, and the only way to
// get a newer state for a node with genesis only is by requesting a proof for a recent
// pruning point.
// The actual UTXO set that fits Mainnet's genesis' UTXO commitment was removed from the codebase in order
// to make reduce the consensus initialization time and the compiled binary size, but can be still
// found here for anyone to verify: https://github.com/kaspanet/kaspad/blob/dbf18d8052f000ba0079be9e79b2d6f5a98b74ca/domain/consensus/processes/blockprocessor/resources/utxos.gz
bp.consensusStateStore.StageVirtualUTXODiff(stagingArea, utxo.NewUTXODiff())
bp.utxoDiffStore.Stage(stagingArea, blockHash, utxo.NewUTXODiff(), nil)
bp.multisetStore.Stage(stagingArea, blockHash, multiset.New())
}
func isHeaderOnlyBlock(block *externalapi.DomainBlock) bool {

View File

@ -533,12 +533,7 @@ func TestGetPruningPointUTXOs(t *testing.T) {
}
}
const mainnetUTXOSize = 1232643
expected := len(outputs) + 1
if consensusConfig.Name == "kaspa-mainnet" {
expected += mainnetUTXOSize
}
// Make sure the length of the UTXOs is exactly spendingTransaction.Outputs + 1 coinbase
// output (includingBlock's coinbase)
if len(allOutpointAndUTXOEntryPairs) != expected {

View File

@ -117,6 +117,10 @@ func (csm *consensusStateManager) validateUTXOCommitment(
log.Tracef("validateUTXOCommitment start for block %s", blockHash)
defer log.Tracef("validateUTXOCommitment end for block %s", blockHash)
if blockHash.Equal(csm.genesisHash) {
return nil
}
multisetHash := multiset.Hash()
if !block.Header.UTXOCommitment().Equal(multisetHash) {
return errors.Wrapf(ruleerrors.ErrBadUTXOCommitment, "block %s UTXO commitment is invalid - block "+

View File

@ -40,12 +40,12 @@ func TestBlockWindow(t *testing.T) {
{
parents: []string{"C", "D"},
id: "E",
expectedWindow: []string{"D", "C", "B"},
expectedWindow: []string{"C", "D", "B"},
},
{
parents: []string{"C", "D"},
id: "F",
expectedWindow: []string{"D", "C", "B"},
expectedWindow: []string{"C", "D", "B"},
},
{
parents: []string{"A"},
@ -60,38 +60,38 @@ func TestBlockWindow(t *testing.T) {
{
parents: []string{"H", "F"},
id: "I",
expectedWindow: []string{"F", "D", "H", "C", "G", "B"},
expectedWindow: []string{"F", "C", "H", "D", "B", "G"},
},
{
parents: []string{"I"},
id: "J",
expectedWindow: []string{"I", "F", "D", "H", "C", "G", "B"},
expectedWindow: []string{"I", "F", "C", "H", "D", "B", "G"},
},
//
{
parents: []string{"J"},
id: "K",
expectedWindow: []string{"J", "I", "F", "D", "H", "C", "G", "B"},
expectedWindow: []string{"J", "I", "F", "C", "H", "D", "B", "G"},
},
{
parents: []string{"K"},
id: "L",
expectedWindow: []string{"K", "J", "I", "F", "D", "H", "C", "G", "B"},
expectedWindow: []string{"K", "J", "I", "F", "C", "H", "D", "B", "G"},
},
{
parents: []string{"L"},
id: "M",
expectedWindow: []string{"L", "K", "J", "I", "F", "D", "H", "C", "G", "B"},
expectedWindow: []string{"L", "K", "J", "I", "F", "C", "H", "D", "B", "G"},
},
{
parents: []string{"M"},
id: "N",
expectedWindow: []string{"M", "L", "K", "J", "I", "F", "D", "H", "C", "G"},
expectedWindow: []string{"M", "L", "K", "J", "I", "F", "C", "H", "D", "B"},
},
{
parents: []string{"N"},
id: "O",
expectedWindow: []string{"N", "M", "L", "K", "J", "I", "F", "D", "H", "C"},
expectedWindow: []string{"N", "M", "L", "K", "J", "I", "F", "C", "H", "D"},
},
},
dagconfig.TestnetParams.Name: {

View File

@ -38,7 +38,7 @@ var genesisTxPayload = []byte{
0xd7, 0x91, 0xd7, 0x93, 0xd7, 0x95, 0xd7, 0x9f,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Bitcoin block hash 0000000000000000000b1f8e1c17b0133d439174e52efbb0c41c3583a8aa66b0
0x00, 0x0b, 0x1f, 0x8e, 0x1c, 0x17, 0xb0, 0x13,
0x3d, 0x43, 0x91, 0x74 ,0xe5, 0x2e, 0xfb, 0xb0,
0x3d, 0x43, 0x91, 0x74, 0xe5, 0x2e, 0xfb, 0xb0,
0xc4, 0x1c, 0x35, 0x83, 0xa8, 0xaa, 0x66, 0xb0,
0x0f, 0xca, 0x37, 0xca, 0x66, 0x7c, 0x2d, 0x55, // Checkpoint block hash 0fca37ca667c2d550a6c4416dad9717e50927128c424fa4edbebc436ab13aeef
0x0a, 0x6c, 0x44, 0x16, 0xda, 0xd9, 0x71, 0x7e,

View File

@ -186,6 +186,8 @@ type Params struct {
FixedSubsidySwitchPruningPointInterval uint64
FixedSubsidySwitchHashRateThreshold *big.Int
DisallowDirectBlocksOnTopOfGenesis bool
}
// NormalizeRPCServerAddress returns addr with the current network default
@ -272,6 +274,7 @@ var MainnetParams = Params{
PruningProofM: defaultPruningProofM,
FixedSubsidySwitchPruningPointInterval: defaultFixedSubsidySwitchPruningPointInterval,
FixedSubsidySwitchHashRateThreshold: big.NewInt(150_000_000_000),
DisallowDirectBlocksOnTopOfGenesis: true,
}
// TestnetParams defines the network parameters for the test Kaspa network.

View File

@ -85,8 +85,8 @@
; Maximum number of inbound and outbound peers.
; maxinpeers=125
; Disable banning of misbehaving peers.
; nobanning=1
; Enable banning of misbehaving peers.
; enablebanning=1
; Maximum allowed ban score before disconnecting and banning misbehaving peers.
; banthreshold=100

View File

@ -1,7 +1,7 @@
#!/bin/bash
rm -rf /tmp/kaspad-temp
kaspad --devnet --nobanning --appdir=/tmp/kaspad-temp --profile=6061 --loglevel=debug &
kaspad --devnet --appdir=/tmp/kaspad-temp --profile=6061 --loglevel=debug &
KASPAD_PID=$!
KASPAD_KILLED=0
function killKaspadIfNotKilled() {

View File

@ -1,7 +1,7 @@
#!/bin/bash
rm -rf /tmp/kaspad-temp
kaspad --devnet --nobanning --appdir=/tmp/kaspad-temp --profile=6061 &
kaspad --devnet --appdir=/tmp/kaspad-temp --profile=6061 &
KASPAD_PID=$!
sleep 1

View File

@ -1,2 +1 @@
--devnet
--devnet --nobanning
--devnet