Repository: kaspanet/kaspad
Pull request #1011 (2020-11-08 14:14:10)

[NOD-1519] Add TestAPI for BuildBlockWithParents

ci-dag: Rebuild after update in master
ci-dag: Rebuild after update in master
ci-dag: Rebuild after update in master
ci-dag: Rebuild after update in master
ci-dag: Rebuild after update in master

Patch:
diff --git a/domain/consensus/consensus.go b/domain/consensus/consensus.go
index f19720bc..ca5a8a3a 100644
--- a/domain/consensus/consensus.go
+++ b/domain/consensus/consensus.go
@@ -5,30 +5,11 @@ import (
 	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
 )
 
-// Consensus maintains the current core state of the node
-type Consensus interface {
-	BuildBlock(coinbaseData *externalapi.DomainCoinbaseData, transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error)
-	ValidateAndInsertBlock(block *externalapi.DomainBlock) error
-	ValidateTransactionAndPopulateWithConsensusData(transaction *externalapi.DomainTransaction) error
-
-	GetBlock(blockHash *externalapi.DomainHash) (*externalapi.DomainBlock, error)
-	GetBlockHeader(blockHash *externalapi.DomainHash) (*externalapi.DomainBlockHeader, error)
-	GetBlockInfo(blockHash *externalapi.DomainHash) (*externalapi.BlockInfo, error)
-
-	GetHashesBetween(lowHash, highHash *externalapi.DomainHash) ([]*externalapi.DomainHash, error)
-	GetMissingBlockBodyHashes(highHash *externalapi.DomainHash) ([]*externalapi.DomainHash, error)
-	GetPruningPointUTXOSet() ([]byte, error)
-	SetPruningPointUTXOSet(serializedUTXOSet []byte) error
-	GetVirtualSelectedParent() (*externalapi.DomainBlock, error)
-	CreateBlockLocator(lowHash, highHash *externalapi.DomainHash) (externalapi.BlockLocator, error)
-	FindNextBlockLocatorBoundaries(blockLocator externalapi.BlockLocator) (lowHash, highHash *externalapi.DomainHash, err error)
-	GetSyncInfo() (*externalapi.SyncInfo, error)
-}
-
 type consensus struct {
 	databaseContext model.DBReader
 
 	blockProcessor        model.BlockProcessor
+	blockBuilder          model.BlockBuilder
 	consensusStateManager model.ConsensusStateManager
 	transactionValidator  model.TransactionValidator
 	syncManager           model.SyncManager
@@ -46,7 +27,7 @@ type consensus struct {
 func (s *consensus) BuildBlock(coinbaseData *externalapi.DomainCoinbaseData,
 	transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error) {
 
-	return s.blockProcessor.BuildBlock(coinbaseData, transactions)
+	return s.blockBuilder.BuildBlock(coinbaseData, transactions)
 }
 
 // ValidateAndInsertBlock validates the given block and, if valid, applies it
diff --git a/domain/consensus/factory.go b/domain/consensus/factory.go
index a2e0d6fe..f5138e07 100644
--- a/domain/consensus/factory.go
+++ b/domain/consensus/factory.go
@@ -16,6 +16,8 @@ import (
 	"github.com/kaspanet/kaspad/domain/consensus/datastructures/utxodiffstore"
 	"github.com/kaspanet/kaspad/domain/consensus/model"
 	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
+	"github.com/kaspanet/kaspad/domain/consensus/model/testapi"
+	"github.com/kaspanet/kaspad/domain/consensus/processes/blockbuilder"
 	"github.com/kaspanet/kaspad/domain/consensus/processes/blockprocessor"
 	"github.com/kaspanet/kaspad/domain/consensus/processes/blockvalidator"
 	"github.com/kaspanet/kaspad/domain/consensus/processes/coinbasemanager"
@@ -37,13 +39,19 @@ import (
 
 // Factory instantiates new Consensuses
 type Factory interface {
-	NewConsensus(dagParams *dagconfig.Params, db infrastructuredatabase.Database) (Consensus, error)
+	NewConsensus(dagParams *dagconfig.Params, db infrastructuredatabase.Database) (externalapi.Consensus, error)
+	NewTestConsensus(dagParams *dagconfig.Params, db infrastructuredatabase.Database) (testapi.TestConsensus, error)
 }
 
 type factory struct{}
 
+// NewFactory creates a new Consensus factory
+func NewFactory() Factory {
+	return &factory{}
+}
+
 // NewConsensus instantiates a new Consensus
-func (f *factory) NewConsensus(dagParams *dagconfig.Params, db infrastructuredatabase.Database) (Consensus, error) {
+func (f *factory) NewConsensus(dagParams *dagconfig.Params, db infrastructuredatabase.Database) (externalapi.Consensus, error) {
 	// Data Structures
 	acceptanceDataStore := acceptancedatastore.New()
 	blockStore := blockstore.New()
@@ -193,6 +201,19 @@ func (f *factory) NewConsensus(dagParams *dagconfig.Params, db infrastructuredat
 		blockHeaderStore,
 		headerTipsStore)
 
+	blockBuilder := blockbuilder.New(
+		dbManager,
+		difficultyManager,
+		pastMedianTimeManager,
+		coinbaseManager,
+		consensusStateManager,
+		ghostdagManager,
+		acceptanceDataStore,
+		blockRelationStore,
+		multisetStore,
+		ghostdagDataStore,
+	)
+
 	blockProcessor := blockprocessor.New(
 		dagParams,
 		dbManager,
@@ -225,6 +246,7 @@ func (f *factory) NewConsensus(dagParams *dagconfig.Params, db infrastructuredat
 		databaseContext: dbManager,
 
 		blockProcessor:        blockProcessor,
+		blockBuilder:          blockBuilder,
 		consensusStateManager: consensusStateManager,
 		transactionValidator:  transactionValidator,
 		syncManager:           syncManager,
@@ -252,7 +274,18 @@ func (f *factory) NewConsensus(dagParams *dagconfig.Params, db infrastructuredat
 	return c, nil
 }
 
-// NewFactory creates a new Consensus factory
-func NewFactory() Factory {
-	return &factory{}
+func (f *factory) NewTestConsensus(dagParams *dagconfig.Params, db infrastructuredatabase.Database) (
+	testapi.TestConsensus, error) {
+
+	consensusAsInterface, err := f.NewConsensus(dagParams, db)
+	if err != nil {
+		return nil, err
+	}
+
+	consensusAsImplementation := consensusAsInterface.(*consensus)
+
+	return &testConsensus{
+		consensus:        consensusAsImplementation,
+		testBlockBuilder: blockbuilder.NewTestBlockBuilder(consensusAsImplementation.blockBuilder),
+	}, nil
 }
diff --git a/domain/consensus/model/externalapi/consensus.go b/domain/consensus/model/externalapi/consensus.go
new file mode 100644
index 00000000..2dabcb92
--- /dev/null
+++ b/domain/consensus/model/externalapi/consensus.go
@@ -0,0 +1,21 @@
+package externalapi
+
+// Consensus maintains the current core state of the node
+type Consensus interface {
+	BuildBlock(coinbaseData *DomainCoinbaseData, transactions []*DomainTransaction) (*DomainBlock, error)
+	ValidateAndInsertBlock(block *DomainBlock) error
+	ValidateTransactionAndPopulateWithConsensusData(transaction *DomainTransaction) error
+
+	GetBlock(blockHash *DomainHash) (*DomainBlock, error)
+	GetBlockHeader(blockHash *DomainHash) (*DomainBlockHeader, error)
+	GetBlockInfo(blockHash *DomainHash) (*BlockInfo, error)
+
+	GetHashesBetween(lowHash, highHash *DomainHash) ([]*DomainHash, error)
+	GetMissingBlockBodyHashes(highHash *DomainHash) ([]*DomainHash, error)
+	GetPruningPointUTXOSet() ([]byte, error)
+	SetPruningPointUTXOSet(serializedUTXOSet []byte) error
+	GetVirtualSelectedParent() (*DomainBlock, error)
+	CreateBlockLocator(lowHash, highHash *DomainHash) (BlockLocator, error)
+	FindNextBlockLocatorBoundaries(blockLocator BlockLocator) (lowHash, highHash *DomainHash, err error)
+	GetSyncInfo() (*SyncInfo, error)
+}
diff --git a/domain/consensus/model/interface_processes_blockbuilder.go b/domain/consensus/model/interface_processes_blockbuilder.go
new file mode 100644
index 00000000..469c176a
--- /dev/null
+++ b/domain/consensus/model/interface_processes_blockbuilder.go
@@ -0,0 +1,15 @@
+package model
+
+import "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
+
+// BlockBuilder is responsible for creating blocks from the current state
+type BlockBuilder interface {
+	BuildBlock(coinbaseData *externalapi.DomainCoinbaseData, transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error)
+}
+
+// TestBlockBuilder adds to the main BlockBuilder methods required by tests
+type TestBlockBuilder interface {
+	BlockBuilder
+	BuildBlockWithParents(parentHashes []*externalapi.DomainHash, coinbaseData *externalapi.DomainCoinbaseData,
+		transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error)
+}
diff --git a/domain/consensus/model/interface_processes_blockprocessor.go b/domain/consensus/model/interface_processes_blockprocessor.go
index ee2c1dff..0f7df809 100644
--- a/domain/consensus/model/interface_processes_blockprocessor.go
+++ b/domain/consensus/model/interface_processes_blockprocessor.go
@@ -3,8 +3,6 @@ package model
 import "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
 
 // BlockProcessor is responsible for processing incoming blocks
-// and creating blocks from the current state
 type BlockProcessor interface {
-	BuildBlock(coinbaseData *externalapi.DomainCoinbaseData, transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error)
 	ValidateAndInsertBlock(block *externalapi.DomainBlock) error
 }
diff --git a/domain/consensus/model/interface_processes_consensusstatemanager.go b/domain/consensus/model/interface_processes_consensusstatemanager.go
index cc36e856..5f4dc783 100644
--- a/domain/consensus/model/interface_processes_consensusstatemanager.go
+++ b/domain/consensus/model/interface_processes_consensusstatemanager.go
@@ -9,4 +9,5 @@ type ConsensusStateManager interface {
 	SetPruningPointUTXOSet(serializedUTXOSet []byte) error
 	RestorePastUTXOSetIterator(blockHash *externalapi.DomainHash) (ReadOnlyUTXOSetIterator, error)
 	HeaderTipsPruningPoint() (*externalapi.DomainHash, error)
+	CalculatePastUTXOAndAcceptanceData(blockHash *externalapi.DomainHash) (*UTXODiff, AcceptanceData, Multiset, error)
 }
diff --git a/domain/consensus/processes/blockbuilder/block_builder.go b/domain/consensus/processes/blockbuilder/block_builder.go
new file mode 100644
index 00000000..66f4cfab
--- /dev/null
+++ b/domain/consensus/processes/blockbuilder/block_builder.go
@@ -0,0 +1,210 @@
+package blockbuilder
+
+import (
+	"sort"
+
+	"github.com/kaspanet/kaspad/domain/consensus/model"
+	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
+	"github.com/kaspanet/kaspad/domain/consensus/utils/consensusserialization"
+	"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
+	"github.com/kaspanet/kaspad/domain/consensus/utils/merkle"
+	"github.com/kaspanet/kaspad/domain/consensus/utils/transactionid"
+	"github.com/kaspanet/kaspad/infrastructure/logger"
+	"github.com/kaspanet/kaspad/util/mstime"
+)
+
+type blockBuilder struct {
+	databaseContext model.DBManager
+
+	difficultyManager     model.DifficultyManager
+	pastMedianTimeManager model.PastMedianTimeManager
+	coinbaseManager       model.CoinbaseManager
+	consensusStateManager model.ConsensusStateManager
+	ghostdagManager       model.GHOSTDAGManager
+
+	acceptanceDataStore model.AcceptanceDataStore
+	blockRelationStore  model.BlockRelationStore
+	multisetStore       model.MultisetStore
+	ghostdagDataStore   model.GHOSTDAGDataStore
+}
+
+// New creates a new instance of a BlockBuilder
+func New(
+	databaseContext model.DBManager,
+
+	difficultyManager model.DifficultyManager,
+	pastMedianTimeManager model.PastMedianTimeManager,
+	coinbaseManager model.CoinbaseManager,
+	consensusStateManager model.ConsensusStateManager,
+	ghostdagManager model.GHOSTDAGManager,
+
+	acceptanceDataStore model.AcceptanceDataStore,
+	blockRelationStore model.BlockRelationStore,
+	multisetStore model.MultisetStore,
+	ghostdagDataStore model.GHOSTDAGDataStore,
+) model.BlockBuilder {
+
+	return &blockBuilder{
+		databaseContext:       databaseContext,
+		difficultyManager:     difficultyManager,
+		pastMedianTimeManager: pastMedianTimeManager,
+		coinbaseManager:       coinbaseManager,
+		consensusStateManager: consensusStateManager,
+		ghostdagManager:       ghostdagManager,
+		acceptanceDataStore:   acceptanceDataStore,
+		blockRelationStore:    blockRelationStore,
+		multisetStore:         multisetStore,
+		ghostdagDataStore:     ghostdagDataStore,
+	}
+}
+
+// BuildBlock builds a block over the current state, with the given
+// coinbaseData and the given transactions
+func (bb *blockBuilder) BuildBlock(coinbaseData *externalapi.DomainCoinbaseData,
+	transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error) {
+
+	onEnd := logger.LogAndMeasureExecutionTime(log, "BuildBlock")
+	defer onEnd()
+
+	return bb.buildBlock(coinbaseData, transactions)
+}
+
+func (bb *blockBuilder) buildBlock(coinbaseData *externalapi.DomainCoinbaseData,
+	transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error) {
+
+	coinbase, err := bb.newBlockCoinbaseTransaction(coinbaseData)
+	if err != nil {
+		return nil, err
+	}
+	transactionsWithCoinbase := append([]*externalapi.DomainTransaction{coinbase}, transactions...)
+
+	header, err := bb.buildHeader(transactionsWithCoinbase)
+	if err != nil {
+		return nil, err
+	}
+
+	return &externalapi.DomainBlock{
+		Header:       header,
+		Transactions: transactionsWithCoinbase,
+	}, nil
+}
+
+func (bb *blockBuilder) newBlockCoinbaseTransaction(
+	coinbaseData *externalapi.DomainCoinbaseData) (*externalapi.DomainTransaction, error) {
+
+	return bb.coinbaseManager.ExpectedCoinbaseTransaction(model.VirtualBlockHash, coinbaseData)
+}
+
+func (bb blockBuilder) buildHeader(transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlockHeader, error) {
+	parentHashes, err := bb.newBlockParentHashes()
+	if err != nil {
+		return nil, err
+	}
+	virtualGHOSTDAGData, err := bb.ghostdagDataStore.Get(bb.databaseContext, model.VirtualBlockHash)
+	if err != nil {
+		return nil, err
+	}
+	timeInMilliseconds, err := bb.newBlockTime(virtualGHOSTDAGData)
+	if err != nil {
+		return nil, err
+	}
+	bits, err := bb.newBlockDifficulty(virtualGHOSTDAGData)
+	if err != nil {
+		return nil, err
+	}
+	hashMerkleRoot := bb.newBlockHashMerkleRoot(transactions)
+	acceptedIDMerkleRoot, err := bb.newBlockAcceptedIDMerkleRoot()
+	if err != nil {
+		return nil, err
+	}
+	utxoCommitment, err := bb.newBlockUTXOCommitment()
+	if err != nil {
+		return nil, err
+	}
+
+	return &externalapi.DomainBlockHeader{
+		Version:              constants.BlockVersion,
+		ParentHashes:         parentHashes,
+		HashMerkleRoot:       *hashMerkleRoot,
+		AcceptedIDMerkleRoot: *acceptedIDMerkleRoot,
+		UTXOCommitment:       *utxoCommitment,
+		TimeInMilliseconds:   timeInMilliseconds,
+		Bits:                 bits,
+	}, nil
+}
+
+func (bb blockBuilder) newBlockParentHashes() ([]*externalapi.DomainHash, error) {
+	virtualBlockRelations, err := bb.blockRelationStore.BlockRelation(bb.databaseContext, model.VirtualBlockHash)
+	if err != nil {
+		return nil, err
+	}
+
+	return virtualBlockRelations.Parents, nil
+}
+
+func (bb blockBuilder) newBlockTime(virtualGHOSTDAGData *model.BlockGHOSTDAGData) (int64, error) {
+	// The timestamp for the block must not be before the median timestamp
+	// of the last several blocks. Thus, choose the maximum between the
+	// current time and one second after the past median time. The current
+	// timestamp is truncated to a millisecond boundary before comparison since a
+	// block timestamp does not supported a precision greater than one
+	// millisecond.
+	newTimestamp := mstime.Now().UnixMilliseconds() + 1
+	minTimestamp, err := bb.pastMedianTimeManager.PastMedianTime(virtualGHOSTDAGData.SelectedParent)
+	if err != nil {
+		return 0, err
+	}
+	if newTimestamp < minTimestamp {
+		newTimestamp = minTimestamp
+	}
+	return newTimestamp, nil
+}
+
+func (bb blockBuilder) newBlockDifficulty(virtualGHOSTDAGData *model.BlockGHOSTDAGData) (uint32, error) {
+	virtualGHOSTDAGData, err := bb.ghostdagDataStore.Get(bb.databaseContext, model.VirtualBlockHash)
+	if err != nil {
+		return 0, err
+	}
+	return bb.difficultyManager.RequiredDifficulty(virtualGHOSTDAGData.SelectedParent)
+}
+
+func (bb blockBuilder) newBlockHashMerkleRoot(transactions []*externalapi.DomainTransaction) *externalapi.DomainHash {
+	return merkle.CalculateHashMerkleRoot(transactions)
+}
+
+func (bb blockBuilder) newBlockAcceptedIDMerkleRoot() (*externalapi.DomainHash, error) {
+	newBlockAcceptanceData, err := bb.acceptanceDataStore.Get(bb.databaseContext, model.VirtualBlockHash)
+	if err != nil {
+		return nil, err
+	}
+
+	return bb.calculateAcceptedIDMerkleRoot(newBlockAcceptanceData)
+}
+
+func (bb blockBuilder) calculateAcceptedIDMerkleRoot(acceptanceData model.AcceptanceData) (*externalapi.DomainHash, error) {
+	var acceptedTransactions []*externalapi.DomainTransaction
+	for _, blockAcceptanceData := range acceptanceData {
+		for _, transactionAcceptance := range blockAcceptanceData.TransactionAcceptanceData {
+			if !transactionAcceptance.IsAccepted {
+				continue
+			}
+			acceptedTransactions = append(acceptedTransactions, transactionAcceptance.Transaction)
+		}
+	}
+	sort.Slice(acceptedTransactions, func(i, j int) bool {
+		acceptedTransactionIID := consensusserialization.TransactionID(acceptedTransactions[i])
+		acceptedTransactionJID := consensusserialization.TransactionID(acceptedTransactions[j])
+		return transactionid.Less(acceptedTransactionIID, acceptedTransactionJID)
+	})
+
+	return merkle.CalculateIDMerkleRoot(acceptedTransactions), nil
+}
+
+func (bb blockBuilder) newBlockUTXOCommitment() (*externalapi.DomainHash, error) {
+	newBlockMultiset, err := bb.multisetStore.Get(bb.databaseContext, model.VirtualBlockHash)
+	if err != nil {
+		return nil, err
+	}
+	newBlockUTXOCommitment := newBlockMultiset.Hash()
+	return newBlockUTXOCommitment, nil
+}
diff --git a/domain/consensus/processes/blockbuilder/log.go b/domain/consensus/processes/blockbuilder/log.go
new file mode 100644
index 00000000..c7af295e
--- /dev/null
+++ b/domain/consensus/processes/blockbuilder/log.go
@@ -0,0 +1,7 @@
+package blockbuilder
+
+import (
+	"github.com/kaspanet/kaspad/infrastructure/logger"
+)
+
+var log, _ = logger.Get(logger.SubsystemTags.BDAG)
diff --git a/domain/consensus/processes/blockprocessor/blockprocessor.go b/domain/consensus/processes/blockprocessor/blockprocessor.go
index 1775de19..805892a2 100644
--- a/domain/consensus/processes/blockprocessor/blockprocessor.go
+++ b/domain/consensus/processes/blockprocessor/blockprocessor.go
@@ -116,17 +116,6 @@ func New(
 	}
 }
 
-// BuildBlock builds a block over the current state, with the given
-// coinbaseData and the given transactions
-func (bp *blockProcessor) BuildBlock(coinbaseData *externalapi.DomainCoinbaseData,
-	transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error) {
-
-	onEnd := logger.LogAndMeasureExecutionTime(log, "BuildBlock")
-	defer onEnd()
-
-	return bp.buildBlock(coinbaseData, transactions)
-}
-
 // ValidateAndInsertBlock validates the given block and, if valid, applies it
 // to the current state
 func (bp *blockProcessor) ValidateAndInsertBlock(block *externalapi.DomainBlock) error {
diff --git a/domain/consensus/processes/blockprocessor/buildblock.go b/domain/consensus/processes/blockprocessor/buildblock.go
deleted file mode 100644
index bf56b13d..00000000
--- a/domain/consensus/processes/blockprocessor/buildblock.go
+++ /dev/null
@@ -1,150 +0,0 @@
-package blockprocessor
-
-import (
-	"sort"
-
-	"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
-
-	"github.com/kaspanet/kaspad/domain/consensus/model"
-	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
-	"github.com/kaspanet/kaspad/domain/consensus/utils/consensusserialization"
-	"github.com/kaspanet/kaspad/domain/consensus/utils/merkle"
-	"github.com/kaspanet/kaspad/domain/consensus/utils/transactionid"
-	"github.com/kaspanet/kaspad/util/mstime"
-)
-
-func (bp *blockProcessor) buildBlock(coinbaseData *externalapi.DomainCoinbaseData,
-	transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error) {
-
-	coinbase, err := bp.newBlockCoinbaseTransaction(coinbaseData)
-	if err != nil {
-		return nil, err
-	}
-	transactionsWithCoinbase := append([]*externalapi.DomainTransaction{coinbase}, transactions...)
-
-	header, err := bp.buildHeader(transactionsWithCoinbase)
-	if err != nil {
-		return nil, err
-	}
-
-	return &externalapi.DomainBlock{
-		Header:       header,
-		Transactions: transactionsWithCoinbase,
-	}, nil
-}
-
-func (bp *blockProcessor) newBlockCoinbaseTransaction(
-	coinbaseData *externalapi.DomainCoinbaseData) (*externalapi.DomainTransaction, error) {
-
-	return bp.coinbaseManager.ExpectedCoinbaseTransaction(model.VirtualBlockHash, coinbaseData)
-}
-
-func (bp *blockProcessor) buildHeader(transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlockHeader, error) {
-	parentHashes, err := bp.newBlockParentHashes()
-	if err != nil {
-		return nil, err
-	}
-	virtualGHOSTDAGData, err := bp.ghostdagDataStore.Get(bp.databaseContext, model.VirtualBlockHash)
-	if err != nil {
-		return nil, err
-	}
-	timeInMilliseconds, err := bp.newBlockTime(virtualGHOSTDAGData)
-	if err != nil {
-		return nil, err
-	}
-	bits, err := bp.newBlockDifficulty(virtualGHOSTDAGData)
-	if err != nil {
-		return nil, err
-	}
-	hashMerkleRoot := bp.newBlockHashMerkleRoot(transactions)
-	acceptedIDMerkleRoot, err := bp.newBlockAcceptedIDMerkleRoot()
-	if err != nil {
-		return nil, err
-	}
-	utxoCommitment, err := bp.newBlockUTXOCommitment()
-	if err != nil {
-		return nil, err
-	}
-
-	return &externalapi.DomainBlockHeader{
-		Version:              constants.BlockVersion,
-		ParentHashes:         parentHashes,
-		HashMerkleRoot:       *hashMerkleRoot,
-		AcceptedIDMerkleRoot: *acceptedIDMerkleRoot,
-		UTXOCommitment:       *utxoCommitment,
-		TimeInMilliseconds:   timeInMilliseconds,
-		Bits:                 bits,
-	}, nil
-}
-
-func (bp *blockProcessor) newBlockParentHashes() ([]*externalapi.DomainHash, error) {
-	virtualBlockRelations, err := bp.blockRelationStore.BlockRelation(bp.databaseContext, model.VirtualBlockHash)
-	if err != nil {
-		return nil, err
-	}
-
-	return virtualBlockRelations.Parents, nil
-}
-
-func (bp *blockProcessor) newBlockTime(virtualGHOSTDAGData *model.BlockGHOSTDAGData) (int64, error) {
-	// The timestamp for the block must not be before the median timestamp
-	// of the last several blocks. Thus, choose the maximum between the
-	// current time and one second after the past median time. The current
-	// timestamp is truncated to a millisecond boundary before comparison since a
-	// block timestamp does not supported a precision greater than one
-	// millisecond.
-	newTimestamp := mstime.Now().UnixMilliseconds() + 1
-	minTimestamp, err := bp.pastMedianTimeManager.PastMedianTime(virtualGHOSTDAGData.SelectedParent)
-	if err != nil {
-		return 0, err
-	}
-	if newTimestamp < minTimestamp {
-		newTimestamp = minTimestamp
-	}
-	return newTimestamp, nil
-}
-
-func (bp *blockProcessor) newBlockDifficulty(virtualGHOSTDAGData *model.BlockGHOSTDAGData) (uint32, error) {
-	virtualGHOSTDAGData, err := bp.ghostdagDataStore.Get(bp.databaseContext, model.VirtualBlockHash)
-	if err != nil {
-		return 0, err
-	}
-	return bp.difficultyManager.RequiredDifficulty(virtualGHOSTDAGData.SelectedParent)
-}
-
-func (bp *blockProcessor) newBlockHashMerkleRoot(transactions []*externalapi.DomainTransaction) *externalapi.DomainHash {
-	return merkle.CalculateHashMerkleRoot(transactions)
-}
-
-func (bp *blockProcessor) newBlockAcceptedIDMerkleRoot() (*externalapi.DomainHash, error) {
-	newBlockAcceptanceData, err := bp.acceptanceDataStore.Get(bp.databaseContext, model.VirtualBlockHash)
-	if err != nil {
-		return nil, err
-	}
-
-	var acceptedTransactions []*externalapi.DomainTransaction
-	for _, blockAcceptanceData := range newBlockAcceptanceData {
-		for _, transactionAcceptance := range blockAcceptanceData.TransactionAcceptanceData {
-			if !transactionAcceptance.IsAccepted {
-				continue
-			}
-			acceptedTransactions = append(acceptedTransactions, transactionAcceptance.Transaction)
-		}
-	}
-	sort.Slice(acceptedTransactions, func(i, j int) bool {
-		acceptedTransactionIID := consensusserialization.TransactionID(acceptedTransactions[i])
-		acceptedTransactionJID := consensusserialization.TransactionID(acceptedTransactions[j])
-		return transactionid.Less(acceptedTransactionIID, acceptedTransactionJID)
-	})
-
-	return merkle.CalculateIDMerkleRoot(acceptedTransactions), nil
-}
-
-func (bp *blockProcessor) newBlockUTXOCommitment() (*externalapi.DomainHash, error) {
-	newBlockMultiset, err := bp.multisetStore.Get(bp.databaseContext, model.VirtualBlockHash)
-	if err != nil {
-		return nil, err
-	}
-	newBlockUTXOCommitment := newBlockMultiset.Hash()
-	return newBlockUTXOCommitment, nil
-}
diff --git a/domain/consensus/processes/consensusstatemanager/calculate_past_utxo.go b/domain/consensus/processes/consensusstatemanager/calculate_past_utxo.go
index 27f81b4c..5b42bd2a 100644
--- a/domain/consensus/processes/consensusstatemanager/calculate_past_utxo.go
+++ b/domain/consensus/processes/consensusstatemanager/calculate_past_utxo.go
@@ -12,7 +12,7 @@ import (
 	"github.com/kaspanet/kaspad/domain/consensus/utils/transactionhelper"
 )
 
-func (csm *consensusStateManager) calculatePastUTXOAndAcceptanceData(blockHash *externalapi.DomainHash) (
+func (csm *consensusStateManager) CalculatePastUTXOAndAcceptanceData(blockHash *externalapi.DomainHash) (
 	*model.UTXODiff, model.AcceptanceData, model.Multiset, error) {
 
 	blockGHOSTDAGData, err := csm.ghostdagDataStore.Get(csm.databaseContext, blockHash)
@@ -167,7 +167,7 @@ func (csm *consensusStateManager) checkTransactionMass(
 }
 
 func (csm *consensusStateManager) RestorePastUTXOSetIterator(blockHash *externalapi.DomainHash) (model.ReadOnlyUTXOSetIterator, error) {
-	diff, _, _, err := csm.calculatePastUTXOAndAcceptanceData(blockHash)
+	diff, _, _, err := csm.CalculatePastUTXOAndAcceptanceData(blockHash)
 	if err != nil {
 		return nil, err
 	}
diff --git a/domain/consensus/processes/consensusstatemanager/resolve_block_status.go b/domain/consensus/processes/consensusstatemanager/resolve_block_status.go
index 27620c0f..f184f633 100644
--- a/domain/consensus/processes/consensusstatemanager/resolve_block_status.go
+++ b/domain/consensus/processes/consensusstatemanager/resolve_block_status.go
@@ -63,7 +63,7 @@ func (csm *consensusStateManager) getUnverifiedChainBlocksAndSelectedParentStatu
 }
 
 func (csm *consensusStateManager) resolveSingleBlockStatus(blockHash *externalapi.DomainHash) (externalapi.BlockStatus, error) {
-	pastUTXODiff, acceptanceData, multiset, err := csm.calculatePastUTXOAndAcceptanceData(blockHash)
+	pastUTXODiff, acceptanceData, multiset, err := csm.CalculatePastUTXOAndAcceptanceData(blockHash)
 	if err != nil {
 		return 0, err
 	}
diff --git a/domain/consensus/processes/consensusstatemanager/update_virtual.go b/domain/consensus/processes/consensusstatemanager/update_virtual.go
index 6cb28817..39541096 100644
--- a/domain/consensus/processes/consensusstatemanager/update_virtual.go
+++ b/domain/consensus/processes/consensusstatemanager/update_virtual.go
@@ -23,7 +23,7 @@ func (csm *consensusStateManager) updateVirtual(newBlockHash *externalapi.Domain
 		return err
 	}
 
-	virtualUTXODiff, _, _, err := csm.calculatePastUTXOAndAcceptanceData(model.VirtualBlockHash)
+	virtualUTXODiff, _, _, err := csm.CalculatePastUTXOAndAcceptanceData(model.VirtualBlockHash)
 	if err != nil {
 		return err
 	}
diff --git a/domain/consensus/utils/subnetworks/compare.go b/domain/consensus/utils/subnetworks/compare.go
index 12cda20d..af1a0635 100644
--- a/domain/consensus/utils/subnetworks/compare.go
+++ b/domain/consensus/utils/subnetworks/compare.go
@@ -21,3 +21,14 @@ func cmp(a, b externalapi.DomainSubnetworkID) int {
 func Less(a, b externalapi.DomainSubnetworkID) bool {
 	return cmp(a, b) < 0
 }
+
+// IsEqual returns true if a and b are equal or both nil
+func IsEqual(a, b *externalapi.DomainSubnetworkID) bool {
+	if a == nil && b == nil {
+		return true
+	}
+	if a == nil || b == nil {
+		return false
+	}
+	return *a == *b
+}
diff --git a/domain/domain.go b/domain/domain.go
index e8319e21..1565a9ef 100644
--- a/domain/domain.go
+++ b/domain/domain.go
@@ -2,6 +2,7 @@ package domain
 
 import (
 	"github.com/kaspanet/kaspad/domain/consensus"
+	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
 	"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
 	"github.com/kaspanet/kaspad/domain/dagconfig"
 	"github.com/kaspanet/kaspad/domain/miningmanager"
@@ -11,15 +12,15 @@ import (
 // Domain provides a reference to the domain's external aps
 type Domain interface {
 	MiningManager() miningmanager.MiningManager
-	Consensus() consensus.Consensus
+	Consensus() externalapi.Consensus
 }
 
 type domain struct {
 	miningManager miningmanager.MiningManager
-	consensus     consensus.Consensus
+	consensus     externalapi.Consensus
 }
 
-func (d domain) Consensus() consensus.Consensus {
+func (d domain) Consensus() externalapi.Consensus {
 	return d.consensus
 }
 
diff --git a/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go b/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go
index be0f1eec..3e22f59b 100644
--- a/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go
+++ b/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go
@@ -1,15 +1,15 @@
 package blocktemplatebuilder
 
 import (
-	"github.com/kaspanet/kaspad/domain/consensus"
+	"math"
+	"sort"
+
 	consensusexternalapi "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
 	"github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
 	"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
 	miningmanagerapi "github.com/kaspanet/kaspad/domain/miningmanager/model"
 	"github.com/kaspanet/kaspad/util"
 	"github.com/pkg/errors"
-	"math"
-	"sort"
 )
 
 type candidateTx struct {
@@ -26,13 +26,13 @@ type candidateTx struct {
 
 // blockTemplateBuilder creates block templates for a miner to consume
 type blockTemplateBuilder struct {
-	consensus consensus.Consensus
+	consensus consensusexternalapi.Consensus
 	mempool   miningmanagerapi.Mempool
 	policy    policy
 }
 
 // New creates a new blockTemplateBuilder
-func New(consensus consensus.Consensus, mempool miningmanagerapi.Mempool, blockMaxMass uint64) miningmanagerapi.BlockTemplateBuilder {
+func New(consensus consensusexternalapi.Consensus, mempool miningmanagerapi.Mempool, blockMaxMass uint64) miningmanagerapi.BlockTemplateBuilder {
 	return &blockTemplateBuilder{
 		consensus: consensus,
 		mempool:   mempool,
diff --git a/domain/miningmanager/factory.go b/domain/miningmanager/factory.go
index 4590e46c..30ad455e 100644
--- a/domain/miningmanager/factory.go
+++ b/domain/miningmanager/factory.go
@@ -1,20 +1,20 @@
 package miningmanager
 
 import (
-	"github.com/kaspanet/kaspad/domain/consensus"
+	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
 	"github.com/kaspanet/kaspad/domain/miningmanager/blocktemplatebuilder"
 	mempoolpkg "github.com/kaspanet/kaspad/domain/miningmanager/mempool"
 )
 
 // Factory instantiates new mining managers
 type Factory interface {
-	NewMiningManager(consensus consensus.Consensus, blockMaxMass uint64) MiningManager
+	NewMiningManager(consensus externalapi.Consensus, blockMaxMass uint64) MiningManager
 }
 
 type factory struct{}
 
 // NewMiningManager instantiate a new mining manager
-func (f *factory) NewMiningManager(consensus consensus.Consensus, blockMaxMass uint64) MiningManager {
+func (f *factory) NewMiningManager(consensus externalapi.Consensus, blockMaxMass uint64) MiningManager {
 	mempool := mempoolpkg.New(consensus)
 	blockTemplateBuilder := blocktemplatebuilder.New(consensus, mempool, blockMaxMass)
 
diff --git a/domain/miningmanager/mempool/mempool.go b/domain/miningmanager/mempool/mempool.go
index ef66ca5f..d048c3bd 100644
--- a/domain/miningmanager/mempool/mempool.go
+++ b/domain/miningmanager/mempool/mempool.go
@@ -12,7 +12,6 @@ import (
 
 	"github.com/kaspanet/kaspad/domain/consensus/utils/transactionhelper"
 
-	"github.com/kaspanet/kaspad/domain/consensus"
 	consensusexternalapi "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
 	"github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
 	"github.com/kaspanet/kaspad/domain/consensus/utils/consensusserialization"
@@ -75,7 +74,7 @@ type mempool struct {
 	orphansByPrev map[consensusexternalapi.DomainOutpoint]map[consensusexternalapi.DomainTransactionID]*consensusexternalapi.DomainTransaction
 
 	mempoolUTXOSet *mempoolUTXOSet
-	consensus      consensus.Consensus
+	consensus      consensusexternalapi.Consensus
 
 	// nextExpireScan is the time after which the orphan pool will be
 	// scanned in order to evict orphans. This is NOT a hard deadline as
@@ -89,7 +88,7 @@ type mempool struct {
 
 // New returns a new memory pool for validating and storing standalone
 // transactions until they are mined into a block.
-func New(consensus consensus.Consensus) miningmanagermodel.Mempool {
+func New(consensus consensusexternalapi.Consensus) miningmanagermodel.Mempool {
 	policy := policy{
 		MaxTxVersion:    0,
 		AcceptNonStd:    false,


Test patch:
diff --git a/domain/consensus/model/testapi/test_consensus.go b/domain/consensus/model/testapi/test_consensus.go
new file mode 100644
index 00000000..ce291126
--- /dev/null
+++ b/domain/consensus/model/testapi/test_consensus.go
@@ -0,0 +1,11 @@
+package testapi
+
+import "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
+
+// TestConsensus wraps the Consensus interface with some methods that are needed by tests only
+type TestConsensus interface {
+	externalapi.Consensus
+
+	BuildBlockWithParents(parentHashes []*externalapi.DomainHash, coinbaseData *externalapi.DomainCoinbaseData,
+		transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error)
+}
diff --git a/domain/consensus/processes/blockbuilder/test_block_builder.go b/domain/consensus/processes/blockbuilder/test_block_builder.go
new file mode 100644
index 00000000..1690709b
--- /dev/null
+++ b/domain/consensus/processes/blockbuilder/test_block_builder.go
@@ -0,0 +1,100 @@
+package blockbuilder
+
+import (
+	"github.com/kaspanet/kaspad/domain/consensus/model"
+	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
+	"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
+	"github.com/kaspanet/kaspad/infrastructure/logger"
+)
+
+type testBlockBuilder struct {
+	*blockBuilder
+}
+
+var tempBlockHash = &externalapi.DomainHash{
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
+
+// NewTestBlockBuilder creates an instance of a TestBlockBuilder
+func NewTestBlockBuilder(baseBlockBuilder model.BlockBuilder) model.TestBlockBuilder {
+	return &testBlockBuilder{blockBuilder: baseBlockBuilder.(*blockBuilder)}
+}
+
+func (bb *testBlockBuilder) BuildBlockWithParents(parentHashes []*externalapi.DomainHash, coinbaseData *externalapi.DomainCoinbaseData,
+	transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error) {
+
+	onEnd := logger.LogAndMeasureExecutionTime(log, "BuildBlockWithParents")
+	defer onEnd()
+
+	return bb.buildBlockWithParents(parentHashes, coinbaseData, transactions)
+}
+
+func (bb testBlockBuilder) buildHeaderWithParents(parentHashes []*externalapi.DomainHash,
+	transactions []*externalapi.DomainTransaction, acceptanceData model.AcceptanceData, multiset model.Multiset) (
+	*externalapi.DomainBlockHeader, error) {
+
+	ghostdagData, err := bb.ghostdagDataStore.Get(bb.databaseContext, tempBlockHash)
+	if err != nil {
+		return nil, err
+	}
+	timeInMilliseconds, err := bb.newBlockTime(ghostdagData)
+	if err != nil {
+		return nil, err
+	}
+	bits, err := bb.newBlockDifficulty(ghostdagData)
+	if err != nil {
+		return nil, err
+	}
+	hashMerkleRoot := bb.newBlockHashMerkleRoot(transactions)
+	acceptedIDMerkleRoot, err := bb.calculateAcceptedIDMerkleRoot(acceptanceData)
+	if err != nil {
+		return nil, err
+	}
+	utxoCommitment := multiset.Hash()
+
+	return &externalapi.DomainBlockHeader{
+		Version:              constants.BlockVersion,
+		ParentHashes:         parentHashes,
+		HashMerkleRoot:       *hashMerkleRoot,
+		AcceptedIDMerkleRoot: *acceptedIDMerkleRoot,
+		UTXOCommitment:       *utxoCommitment,
+		TimeInMilliseconds:   timeInMilliseconds,
+		Bits:                 bits,
+	}, nil
+}
+
+func (bb *testBlockBuilder) buildBlockWithParents(
+	parentHashes []*externalapi.DomainHash, coinbaseData *externalapi.DomainCoinbaseData,
+	transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error) {
+
+	bb.blockRelationStore.StageBlockRelation(tempBlockHash, &model.BlockRelations{Parents: parentHashes})
+	defer bb.blockRelationStore.Discard()
+
+	err := bb.ghostdagManager.GHOSTDAG(tempBlockHash)
+	if err != nil {
+		return nil, err
+	}
+	defer bb.ghostdagDataStore.Discard()
+
+	_, acceptanceData, multiset, err := bb.consensusStateManager.CalculatePastUTXOAndAcceptanceData(tempBlockHash)
+	if err != nil {
+		return nil, err
+	}
+	bb.acceptanceDataStore.Stage(tempBlockHash, acceptanceData)
+	defer bb.acceptanceDataStore.Discard()
+
+	coinbase, err := bb.newBlockCoinbaseTransaction(coinbaseData)
+	if err != nil {
+		return nil, err
+	}
+	transactionsWithCoinbase := append([]*externalapi.DomainTransaction{coinbase}, transactions...)
+
+	header, err := bb.buildHeaderWithParents(parentHashes, transactions, acceptanceData, multiset)
+	if err != nil {
+		return nil, err
+	}
+
+	return &externalapi.DomainBlock{
+		Header:       header,
+		Transactions: transactionsWithCoinbase,
+	}, nil
+}
diff --git a/domain/consensus/test_consensus.go b/domain/consensus/test_consensus.go
new file mode 100644
index 00000000..5f38622f
--- /dev/null
+++ b/domain/consensus/test_consensus.go
@@ -0,0 +1,17 @@
+package consensus
+
+import (
+	"github.com/kaspanet/kaspad/domain/consensus/model"
+	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
+)
+
+type testConsensus struct {
+	*consensus
+	testBlockBuilder model.TestBlockBuilder
+}
+
+func (tc *testConsensus) BuildBlockWithParents(parentHashes []*externalapi.DomainHash, coinbaseData *externalapi.DomainCoinbaseData,
+	transactions []*externalapi.DomainTransaction) (*externalapi.DomainBlock, error) {
+
+	return tc.testBlockBuilder.BuildBlockWithParents(parentHashes, coinbaseData, transactions)
+}