kaspad/domain/consensus/test_consensus.go
Michael Sutton 1c9bb54cc2
Crucial fix for the UTXO difference mechanism (#2114)
* Illustrate the bug through prints

* Change consensus API to a single ResolveVirtual call

* nil changeset is not expected when err=nil

* Fixes a deep bug in the resolve virtual process

* Be more defensive at resolving virtual when adding a block

* When finally resolved, set virtual parents properly

* Return nil changeset when nothing happened

* Make sure the block at the split point is reversed to new chain as well

* bump to version 0.12.4

* Avoid locking consensus twice in the common case of adding block with updateVirtual=true

* check err

* Parents must be picked first before set as virtual parents

* Keep the flag for tracking virtual state, since tip sorting perf is high with many tips

* Improve and clarify resolve virtual tests

* Addressing minor review comments

* Fixed a bug in the reported virtual changeset, and modified the test to verify it

* Addressing review comments
2022-07-15 12:35:20 +03:00

282 lines
7.6 KiB
Go

package consensus
import (
"encoding/json"
"fmt"
"github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/utils/hashset"
"github.com/kaspanet/kaspad/util/staging"
"io"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/model/testapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/dagconfig"
"github.com/kaspanet/kaspad/infrastructure/db/database"
"github.com/pkg/errors"
)
type testConsensus struct {
*consensus
dagParams *dagconfig.Params
database database.Database
testBlockBuilder testapi.TestBlockBuilder
testReachabilityManager testapi.TestReachabilityManager
testConsensusStateManager testapi.TestConsensusStateManager
testTransactionValidator testapi.TestTransactionValidator
buildBlockConsensus *consensus
}
func (tc *testConsensus) DAGParams() *dagconfig.Params {
return tc.dagParams
}
func (tc *testConsensus) BuildBlockWithParents(parentHashes []*externalapi.DomainHash,
coinbaseData *externalapi.DomainCoinbaseData, transactions []*externalapi.DomainTransaction) (
*externalapi.DomainBlock, externalapi.UTXODiff, error) {
// Require write lock because BuildBlockWithParents stages temporary data
tc.lock.Lock()
defer tc.lock.Unlock()
block, diff, err := tc.testBlockBuilder.BuildBlockWithParents(parentHashes, coinbaseData, transactions)
if err != nil {
return nil, nil, err
}
return block, diff, nil
}
func (tc *testConsensus) AddBlock(parentHashes []*externalapi.DomainHash, coinbaseData *externalapi.DomainCoinbaseData,
transactions []*externalapi.DomainTransaction) (*externalapi.DomainHash, *externalapi.VirtualChangeSet, error) {
// Require write lock because BuildBlockWithParents stages temporary data
tc.lock.Lock()
defer tc.lock.Unlock()
block, _, err := tc.testBlockBuilder.BuildBlockWithParents(parentHashes, coinbaseData, transactions)
if err != nil {
return nil, nil, err
}
virtualChangeSet, err := tc.validateAndInsertBlockNoLock(block, true)
if err != nil {
return nil, nil, err
}
return consensushashing.BlockHash(block), virtualChangeSet, nil
}
func (tc *testConsensus) AddUTXOInvalidHeader(parentHashes []*externalapi.DomainHash) (*externalapi.DomainHash,
*externalapi.VirtualChangeSet, error) {
// Require write lock because BuildBlockWithParents stages temporary data
tc.lock.Lock()
defer tc.lock.Unlock()
header, err := tc.testBlockBuilder.BuildUTXOInvalidHeader(parentHashes)
if err != nil {
return nil, nil, err
}
virtualChangeSet, err := tc.validateAndInsertBlockNoLock(&externalapi.DomainBlock{
Header: header,
Transactions: nil,
}, true)
if err != nil {
return nil, nil, err
}
return consensushashing.HeaderHash(header), virtualChangeSet, nil
}
func (tc *testConsensus) AddUTXOInvalidBlock(parentHashes []*externalapi.DomainHash) (*externalapi.DomainHash,
*externalapi.VirtualChangeSet, error) {
// Require write lock because BuildBlockWithParents stages temporary data
tc.lock.Lock()
defer tc.lock.Unlock()
block, err := tc.testBlockBuilder.BuildUTXOInvalidBlock(parentHashes)
if err != nil {
return nil, nil, err
}
virtualChangeSet, err := tc.validateAndInsertBlockNoLock(block, true)
if err != nil {
return nil, nil, err
}
return consensushashing.BlockHash(block), virtualChangeSet, nil
}
func (tc *testConsensus) ResolveVirtualWithMaxParam(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) {
tc.lock.Lock()
defer tc.lock.Unlock()
return tc.resolveVirtualChunkNoLock(maxBlocksToResolve)
}
// jsonBlock is a json representation of a block in mine format
type jsonBlock struct {
ID string `json:"id"`
Parents []string `json:"parents"`
}
func (tc *testConsensus) MineJSON(r io.Reader, blockType testapi.MineJSONBlockType) (tips []*externalapi.DomainHash, err error) {
tipSet := map[externalapi.DomainHash]*externalapi.DomainHash{}
tipSet[*tc.dagParams.GenesisHash] = tc.dagParams.GenesisHash
parentsMap := make(map[string]*externalapi.DomainHash)
parentsMap["0"] = tc.dagParams.GenesisHash
decoder := json.NewDecoder(r)
// read open bracket
_, err = decoder.Token()
if err != nil {
return nil, err
}
// while the array contains values
for decoder.More() {
var block jsonBlock
// decode an array value (Message)
err := decoder.Decode(&block)
if err != nil {
return nil, err
}
if block.ID == "0" {
continue
}
parentHashes := make([]*externalapi.DomainHash, len(block.Parents))
var ok bool
for i, parentID := range block.Parents {
parentHashes[i], ok = parentsMap[parentID]
if !ok {
return nil, errors.Errorf("Couldn't find blockID: %s", parentID)
}
delete(tipSet, *parentHashes[i])
}
var blockHash *externalapi.DomainHash
switch blockType {
case testapi.MineJSONBlockTypeUTXOValidBlock:
blockHash, _, err = tc.AddBlock(parentHashes, nil, nil)
if err != nil {
return nil, err
}
case testapi.MineJSONBlockTypeUTXOInvalidBlock:
blockHash, _, err = tc.AddUTXOInvalidBlock(parentHashes)
if err != nil {
return nil, err
}
case testapi.MineJSONBlockTypeUTXOInvalidHeader:
blockHash, _, err = tc.AddUTXOInvalidHeader(parentHashes)
if err != nil {
return nil, err
}
default:
return nil, errors.Errorf("unknwon block type %v", blockType)
}
parentsMap[block.ID] = blockHash
tipSet[*blockHash] = blockHash
}
tips = make([]*externalapi.DomainHash, len(tipSet))
i := 0
for _, v := range tipSet {
tips[i] = v
i++
}
return tips, nil
}
func (tc *testConsensus) ToJSON(w io.Writer) error {
hashToID := make(map[externalapi.DomainHash]string)
lastID := 0
encoder := json.NewEncoder(w)
visited := hashset.New()
queue := tc.dagTraversalManager.NewUpHeap(model.NewStagingArea())
err := queue.Push(tc.dagParams.GenesisHash)
if err != nil {
return err
}
blocksToAdd := make([]jsonBlock, 0)
for queue.Len() > 0 {
current := queue.Pop()
if visited.Contains(current) {
continue
}
visited.Add(current)
if current.Equal(model.VirtualBlockHash) {
continue
}
header, err := tc.blockHeaderStore.BlockHeader(tc.databaseContext, model.NewStagingArea(), current)
if err != nil {
return err
}
directParents := header.DirectParents()
parentIDs := make([]string, len(directParents))
for i, parent := range directParents {
parentIDs[i] = hashToID[*parent]
}
lastIDStr := fmt.Sprintf("%d", lastID)
blocksToAdd = append(blocksToAdd, jsonBlock{
ID: lastIDStr,
Parents: parentIDs,
})
hashToID[*current] = lastIDStr
lastID++
children, err := tc.dagTopologyManagers[0].Children(model.NewStagingArea(), current)
if err != nil {
return err
}
err = queue.PushSlice(children)
if err != nil {
return err
}
}
return encoder.Encode(blocksToAdd)
}
func (tc *testConsensus) BuildUTXOInvalidBlock(parentHashes []*externalapi.DomainHash) (*externalapi.DomainBlock, error) {
// Require write lock because BuildBlockWithParents stages temporary data
tc.lock.Lock()
defer tc.lock.Unlock()
return tc.testBlockBuilder.BuildUTXOInvalidBlock(parentHashes)
}
func (tc *testConsensus) BuildHeaderWithParents(parentHashes []*externalapi.DomainHash) (externalapi.BlockHeader, error) {
// Require write lock because BuildUTXOInvalidHeader stages temporary data
tc.lock.Lock()
defer tc.lock.Unlock()
return tc.testBlockBuilder.BuildUTXOInvalidHeader(parentHashes)
}
func (tc *testConsensus) UpdatePruningPointByVirtual() error {
tc.lock.Lock()
defer tc.lock.Unlock()
stagingArea := model.NewStagingArea()
err := tc.pruningManager.UpdatePruningPointByVirtual(stagingArea)
if err != nil {
return err
}
return staging.CommitAllChanges(tc.databaseContext, stagingArea)
}