diff --git a/app/protocol/flows/v5/blockrelay/handle_ibd_block_locator.go b/app/protocol/flows/v5/blockrelay/handle_ibd_block_locator.go index 4e9cd0bc4..250c5cf9b 100644 --- a/app/protocol/flows/v5/blockrelay/handle_ibd_block_locator.go +++ b/app/protocol/flows/v5/blockrelay/handle_ibd_block_locator.go @@ -5,7 +5,6 @@ import ( "github.com/kaspanet/kaspad/app/protocol/peer" "github.com/kaspanet/kaspad/app/protocol/protocolerrors" "github.com/kaspanet/kaspad/domain" - "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/infrastructure/network/netadapter/router" ) @@ -34,7 +33,7 @@ func HandleIBDBlockLocator(context HandleIBDBlockLocatorContext, incomingRoute * if err != nil { return err } - if !blockInfo.Exists { + if !blockInfo.HasHeader() { return protocolerrors.Errorf(true, "received IBDBlockLocator "+ "with an unknown targetHash %s", targetHash) } @@ -47,7 +46,7 @@ func HandleIBDBlockLocator(context HandleIBDBlockLocatorContext, incomingRoute * } // The IBD block locator is checking only existing blocks with bodies. - if !blockInfo.Exists || blockInfo.BlockStatus == externalapi.StatusHeaderOnly { + if !blockInfo.HasBody() { continue } diff --git a/app/protocol/flows/v5/blockrelay/handle_ibd_block_requests.go b/app/protocol/flows/v5/blockrelay/handle_ibd_block_requests.go index fa6348581..aa69b82a0 100644 --- a/app/protocol/flows/v5/blockrelay/handle_ibd_block_requests.go +++ b/app/protocol/flows/v5/blockrelay/handle_ibd_block_requests.go @@ -4,7 +4,6 @@ import ( "github.com/kaspanet/kaspad/app/appmessage" "github.com/kaspanet/kaspad/app/protocol/protocolerrors" "github.com/kaspanet/kaspad/domain" - "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/infrastructure/network/netadapter/router" "github.com/pkg/errors" ) @@ -32,7 +31,7 @@ func HandleIBDBlockRequests(context HandleIBDBlockRequestsContext, incomingRoute if err != nil { return err } - if !blockInfo.Exists || blockInfo.BlockStatus == externalapi.StatusHeaderOnly { + if !blockInfo.HasBody() { return protocolerrors.Errorf(true, "block %s not found (v5)", hash) } block, err := context.Domain().Consensus().GetBlock(hash) diff --git a/app/protocol/flows/v5/blockrelay/handle_relay_block_requests.go b/app/protocol/flows/v5/blockrelay/handle_relay_block_requests.go index db4e54558..552f5f103 100644 --- a/app/protocol/flows/v5/blockrelay/handle_relay_block_requests.go +++ b/app/protocol/flows/v5/blockrelay/handle_relay_block_requests.go @@ -5,7 +5,6 @@ import ( peerpkg "github.com/kaspanet/kaspad/app/protocol/peer" "github.com/kaspanet/kaspad/app/protocol/protocolerrors" "github.com/kaspanet/kaspad/domain" - "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/infrastructure/network/netadapter/router" "github.com/pkg/errors" ) @@ -33,7 +32,7 @@ func HandleRelayBlockRequests(context RelayBlockRequestsContext, incomingRoute * if err != nil { return err } - if !blockInfo.Exists || blockInfo.BlockStatus == externalapi.StatusHeaderOnly { + if !blockInfo.HasBody() { return protocolerrors.Errorf(true, "block %s not found", hash) } block, err := context.Domain().Consensus().GetBlock(hash) diff --git a/app/protocol/flows/v5/blockrelay/handle_request_anticone.go b/app/protocol/flows/v5/blockrelay/handle_request_anticone.go index 267204953..208af8c6b 100644 --- a/app/protocol/flows/v5/blockrelay/handle_request_anticone.go +++ b/app/protocol/flows/v5/blockrelay/handle_request_anticone.go @@ -47,9 +47,9 @@ func (flow *handleRequestAnticoneFlow) start() error { // GetAnticone is expected to be called by the syncee for getting the anticone of the header selected tip // intersected by past of relayed block, and is thus expected to be bounded by mergeset limit since - // we relay blocks only if they enter virtual's mergeset. We add 2 for a small margin error. + // we relay blocks only if they enter virtual's mergeset. We add a 2 factor for possible sync gaps. blockHashes, err := flow.Domain().Consensus().GetAnticone(blockHash, contextHash, - flow.Config().ActiveNetParams.MergeSetSizeLimit+2) + flow.Config().ActiveNetParams.MergeSetSizeLimit*2) if err != nil { return protocolerrors.Wrap(true, err, "Failed querying anticone") } diff --git a/app/protocol/flows/v5/blockrelay/handle_request_headers.go b/app/protocol/flows/v5/blockrelay/handle_request_headers.go index c85fdc5cb..38225efb0 100644 --- a/app/protocol/flows/v5/blockrelay/handle_request_headers.go +++ b/app/protocol/flows/v5/blockrelay/handle_request_headers.go @@ -46,7 +46,25 @@ func (flow *handleRequestHeadersFlow) start() error { } log.Debugf("Received requestHeaders with lowHash: %s, highHash: %s", lowHash, highHash) - isLowSelectedAncestorOfHigh, err := flow.Domain().Consensus().IsInSelectedParentChainOf(lowHash, highHash) + consensus := flow.Domain().Consensus() + + lowHashInfo, err := consensus.GetBlockInfo(lowHash) + if err != nil { + return err + } + if !lowHashInfo.HasHeader() { + return protocolerrors.Errorf(true, "Block %s does not exist", lowHash) + } + + highHashInfo, err := consensus.GetBlockInfo(highHash) + if err != nil { + return err + } + if !highHashInfo.HasHeader() { + return protocolerrors.Errorf(true, "Block %s does not exist", highHash) + } + + isLowSelectedAncestorOfHigh, err := consensus.IsInSelectedParentChainOf(lowHash, highHash) if err != nil { return err } @@ -62,7 +80,7 @@ func (flow *handleRequestHeadersFlow) start() error { // in order to avoid locking the consensus for too long // maxBlocks MUST be >= MergeSetSizeLimit + 1 const maxBlocks = 1 << 10 - blockHashes, _, err := flow.Domain().Consensus().GetHashesBetween(lowHash, highHash, maxBlocks) + blockHashes, _, err := consensus.GetHashesBetween(lowHash, highHash, maxBlocks) if err != nil { return err } @@ -70,7 +88,7 @@ func (flow *handleRequestHeadersFlow) start() error { blockHeaders := make([]*appmessage.MsgBlockHeader, len(blockHashes)) for i, blockHash := range blockHashes { - blockHeader, err := flow.Domain().Consensus().GetBlockHeader(blockHash) + blockHeader, err := consensus.GetBlockHeader(blockHash) if err != nil { return err } diff --git a/app/protocol/flows/v5/blockrelay/ibd.go b/app/protocol/flows/v5/blockrelay/ibd.go index e1e68e4d6..fd5fcdcca 100644 --- a/app/protocol/flows/v5/blockrelay/ibd.go +++ b/app/protocol/flows/v5/blockrelay/ibd.go @@ -686,37 +686,28 @@ func (flow *handleIBDFlow) banIfBlockIsHeaderOnly(block *externalapi.DomainBlock } func (flow *handleIBDFlow) resolveVirtual(estimatedVirtualDAAScoreTarget uint64) error { - virtualDAAScoreStart, err := flow.Domain().Consensus().GetVirtualDAAScore() + err := flow.Domain().Consensus().ResolveVirtual(func(virtualDAAScoreStart uint64, virtualDAAScore uint64) { + var percents int + if estimatedVirtualDAAScoreTarget-virtualDAAScoreStart <= 0 { + percents = 100 + } else { + percents = int(float64(virtualDAAScore-virtualDAAScoreStart) / float64(estimatedVirtualDAAScoreTarget-virtualDAAScoreStart) * 100) + } + if percents < 0 { + percents = 0 + } else if percents > 100 { + percents = 100 + } + log.Infof("Resolving virtual. Estimated progress: %d%%", percents) + }) if err != nil { return err } - for i := 0; ; i++ { - if i%10 == 0 { - virtualDAAScore, err := flow.Domain().Consensus().GetVirtualDAAScore() - if err != nil { - return err - } - var percents int - if estimatedVirtualDAAScoreTarget-virtualDAAScoreStart <= 0 { - percents = 100 - } else { - percents = int(float64(virtualDAAScore-virtualDAAScoreStart) / float64(estimatedVirtualDAAScoreTarget-virtualDAAScoreStart) * 100) - } - log.Infof("Resolving virtual. Estimated progress: %d%%", percents) - } - isCompletelyResolved, err := flow.Domain().Consensus().ResolveVirtual() - if err != nil { - return err - } - - if isCompletelyResolved { - log.Infof("Resolved virtual") - err = flow.OnNewBlockTemplate() - if err != nil { - return err - } - return nil - } + log.Infof("Resolved virtual") + err = flow.OnNewBlockTemplate() + if err != nil { + return err } + return nil } diff --git a/app/rpc/rpchandlers/add_peer.go b/app/rpc/rpchandlers/add_peer.go index 7b6c83122..8563ebe42 100644 --- a/app/rpc/rpchandlers/add_peer.go +++ b/app/rpc/rpchandlers/add_peer.go @@ -9,6 +9,14 @@ import ( // HandleAddPeer handles the respectively named RPC command func HandleAddPeer(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) { + if context.Config.SafeRPC { + log.Warn("AddPeer RPC command called while node in safe RPC mode -- ignoring.") + response := appmessage.NewAddPeerResponseMessage() + response.Error = + appmessage.RPCErrorf("AddPeer RPC command called while node in safe RPC mode") + return response, nil + } + AddPeerRequest := request.(*appmessage.AddPeerRequestMessage) address, err := network.NormalizeAddress(AddPeerRequest.Address, context.Config.ActiveNetParams.DefaultPort) if err != nil { diff --git a/app/rpc/rpchandlers/ban.go b/app/rpc/rpchandlers/ban.go index 0a96b53e0..0d47d01b3 100644 --- a/app/rpc/rpchandlers/ban.go +++ b/app/rpc/rpchandlers/ban.go @@ -9,6 +9,14 @@ import ( // HandleBan handles the respectively named RPC command func HandleBan(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) { + if context.Config.SafeRPC { + log.Warn("Ban RPC command called while node in safe RPC mode -- ignoring.") + response := appmessage.NewBanResponseMessage() + response.Error = + appmessage.RPCErrorf("Ban RPC command called while node in safe RPC mode") + return response, nil + } + banRequest := request.(*appmessage.BanRequestMessage) ip := net.ParseIP(banRequest.IP) if ip == nil { diff --git a/app/rpc/rpchandlers/estimate_network_hashes_per_second.go b/app/rpc/rpchandlers/estimate_network_hashes_per_second.go index 505c62b2a..3e760bdd0 100644 --- a/app/rpc/rpchandlers/estimate_network_hashes_per_second.go +++ b/app/rpc/rpchandlers/estimate_network_hashes_per_second.go @@ -27,6 +27,27 @@ func HandleEstimateNetworkHashesPerSecond( } } + if context.Config.SafeRPC { + const windowSizeLimit = 10000 + if windowSize > windowSizeLimit { + response := &appmessage.EstimateNetworkHashesPerSecondResponseMessage{} + response.Error = + appmessage.RPCErrorf( + "Requested window size %d is larger than max allowed in RPC safe mode (%d)", + windowSize, windowSizeLimit) + return response, nil + } + } + + if uint64(windowSize) > context.Config.ActiveNetParams.PruningDepth() { + response := &appmessage.EstimateNetworkHashesPerSecondResponseMessage{} + response.Error = + appmessage.RPCErrorf( + "Requested window size %d is larger than pruning point depth %d", + windowSize, context.Config.ActiveNetParams.PruningDepth()) + return response, nil + } + networkHashesPerSecond, err := context.Domain.Consensus().EstimateNetworkHashesPerSecond(startHash, windowSize) if err != nil { response := &appmessage.EstimateNetworkHashesPerSecondResponseMessage{} diff --git a/app/rpc/rpchandlers/get_mempool_entries_by_addresses.go b/app/rpc/rpchandlers/get_mempool_entries_by_addresses.go index d40d42332..04e8ac30b 100644 --- a/app/rpc/rpchandlers/get_mempool_entries_by_addresses.go +++ b/app/rpc/rpchandlers/get_mempool_entries_by_addresses.go @@ -43,7 +43,7 @@ func HandleGetMempoolEntriesByAddresses(context *rpccontext.Context, _ *router.R if getMempoolEntriesByAddressesRequest.IncludeOrphanPool { orphanPoolTransactions := context.Domain.MiningManager().AllOrphanTransactions() - orphanPoolEntriesByAddresse, err := extractMempoolEntriesByAddressesFromTransactions( + orphanPoolEntriesByAddress, err := extractMempoolEntriesByAddressesFromTransactions( context, getMempoolEntriesByAddressesRequest.Addresses, orphanPoolTransactions, @@ -59,7 +59,7 @@ func HandleGetMempoolEntriesByAddresses(context *rpccontext.Context, _ *router.R return errorMessage, nil } - mempoolEntriesByAddresses = append(mempoolEntriesByAddresses, orphanPoolEntriesByAddresse...) + mempoolEntriesByAddresses = append(mempoolEntriesByAddresses, orphanPoolEntriesByAddress...) } return appmessage.NewGetMempoolEntriesByAddressesResponseMessage(mempoolEntriesByAddresses), nil @@ -80,9 +80,13 @@ func extractMempoolEntriesByAddressesFromTransactions(context *rpccontext.Contex for _, transaction := range transactions { for i, input := range transaction.Inputs { - // TODO: Fix this if input.UTXOEntry == nil { - log.Errorf("Couldn't find UTXO entry for input %d in mempool transaction %s. This is a bug and should be fixed.", i, consensushashing.TransactionID(transaction)) + if !areOrphans { // Orphans can legitimately have `input.UTXOEntry == nil` + // TODO: Fix the underlying cause of the bug for non-orphan entries + log.Debugf( + "Couldn't find UTXO entry for input %d in mempool transaction %s. This is a bug and should be fixed.", + i, consensushashing.TransactionID(transaction)) + } continue } diff --git a/app/rpc/rpchandlers/resolve_finality_conflict.go b/app/rpc/rpchandlers/resolve_finality_conflict.go index 0b403149b..1e3f88fb3 100644 --- a/app/rpc/rpchandlers/resolve_finality_conflict.go +++ b/app/rpc/rpchandlers/resolve_finality_conflict.go @@ -8,6 +8,14 @@ import ( // HandleResolveFinalityConflict handles the respectively named RPC command func HandleResolveFinalityConflict(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) { + if context.Config.SafeRPC { + log.Warn("ResolveFinalityConflict RPC command called while node in safe RPC mode -- ignoring.") + response := &appmessage.ResolveFinalityConflictResponseMessage{} + response.Error = + appmessage.RPCErrorf("ResolveFinalityConflict RPC command called while node in safe RPC mode") + return response, nil + } + response := &appmessage.ResolveFinalityConflictResponseMessage{} response.Error = appmessage.RPCErrorf("not implemented") return response, nil diff --git a/app/rpc/rpchandlers/shut_down.go b/app/rpc/rpchandlers/shut_down.go index c4479f01f..6b7bafdf0 100644 --- a/app/rpc/rpchandlers/shut_down.go +++ b/app/rpc/rpchandlers/shut_down.go @@ -12,6 +12,14 @@ const pauseBeforeShutDown = time.Second // HandleShutDown handles the respectively named RPC command func HandleShutDown(context *rpccontext.Context, _ *router.Router, _ appmessage.Message) (appmessage.Message, error) { + if context.Config.SafeRPC { + log.Warn("ShutDown RPC command called while node in safe RPC mode -- ignoring.") + response := appmessage.NewShutDownResponseMessage() + response.Error = + appmessage.RPCErrorf("ShutDown RPC command called while node in safe RPC mode") + return response, nil + } + log.Warn("ShutDown RPC called.") // Wait a second before shutting down, to allow time to return the response to the caller diff --git a/app/rpc/rpchandlers/unban.go b/app/rpc/rpchandlers/unban.go index 6077daf3e..db3e935d7 100644 --- a/app/rpc/rpchandlers/unban.go +++ b/app/rpc/rpchandlers/unban.go @@ -9,6 +9,14 @@ import ( // HandleUnban handles the respectively named RPC command func HandleUnban(context *rpccontext.Context, _ *router.Router, request appmessage.Message) (appmessage.Message, error) { + if context.Config.SafeRPC { + log.Warn("Unban RPC command called while node in safe RPC mode -- ignoring.") + response := appmessage.NewUnbanResponseMessage() + response.Error = + appmessage.RPCErrorf("Unban RPC command called while node in safe RPC mode") + return response, nil + } + unbanRequest := request.(*appmessage.UnbanRequestMessage) ip := net.ParseIP(unbanRequest.IP) if ip == nil { diff --git a/changelog.txt b/changelog.txt index 935439d42..a95c7bc2d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,14 @@ +Kaspad v0.12.4 - 2022-07-17 +=========================== + +* Crucial fix for the UTXO difference mechanism (#2114) +* Implement multi-layer auto-compound (#2115) + +Kaspad v0.12.3 - 2022-06-29 +=========================== + +* Fixes a few bugs which can lead to node crashes or out-of-memory errors + Kaspad v0.12.2 - 2022-06-17 =========================== diff --git a/cmd/kaspawallet/daemon/server/split_transaction.go b/cmd/kaspawallet/daemon/server/split_transaction.go index fd672b019..b2154c900 100644 --- a/cmd/kaspawallet/daemon/server/split_transaction.go +++ b/cmd/kaspawallet/daemon/server/split_transaction.go @@ -27,18 +27,10 @@ func (s *server) maybeAutoCompoundTransaction(transactionBytes []byte, toAddress return nil, err } - splitTransactions, err := s.maybeSplitTransaction(transaction, changeAddress) + splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress) if err != nil { return nil, err } - if len(splitTransactions) > 1 { - mergeTransaction, err := s.mergeTransaction(splitTransactions, transaction, toAddress, changeAddress, changeWalletAddress) - if err != nil { - return nil, err - } - splitTransactions = append(splitTransactions, mergeTransaction) - } - splitTransactionsBytes := make([][]byte, len(splitTransactions)) for i, splitTransaction := range splitTransactions { splitTransactionsBytes[i], err = serialization.SerializePartiallySignedTransaction(splitTransaction) @@ -113,8 +105,8 @@ func (s *server) mergeTransaction( return serialization.DeserializePartiallySignedTransaction(mergeTransactionBytes) } -func (s *server) maybeSplitTransaction(transaction *serialization.PartiallySignedTransaction, - changeAddress util.Address) ([]*serialization.PartiallySignedTransaction, error) { +func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address, + changeAddress util.Address, changeWalletAddress *walletAddress) ([]*serialization.PartiallySignedTransaction, error) { transactionMass, err := s.estimateMassAfterSignatures(transaction) if err != nil { @@ -141,6 +133,20 @@ func (s *server) maybeSplitTransaction(transaction *serialization.PartiallySigne } } + if len(splitTransactions) > 1 { + mergeTransaction, err := s.mergeTransaction(splitTransactions, transaction, toAddress, changeAddress, changeWalletAddress) + if err != nil { + return nil, err + } + // Recursion will be 2-3 iterations deep even in the rarest` cases, so considered safe.. + splitMergeTransaction, err := s.maybeSplitAndMergeTransaction(mergeTransaction, toAddress, changeAddress, changeWalletAddress) + if err != nil { + return nil, err + } + splitTransactions = append(splitTransactions, splitMergeTransaction...) + + } + return splitTransactions, nil } diff --git a/domain/consensus/consensus.go b/domain/consensus/consensus.go index aaddf88e5..8177389b7 100644 --- a/domain/consensus/consensus.go +++ b/domain/consensus/consensus.go @@ -194,11 +194,33 @@ func (s *consensus) BuildBlockTemplate(coinbaseData *externalapi.DomainCoinbaseD // ValidateAndInsertBlock validates the given block and, if valid, applies it // to the current state -func (s *consensus) ValidateAndInsertBlock(block *externalapi.DomainBlock, shouldValidateAgainstUTXO bool) error { +func (s *consensus) ValidateAndInsertBlock(block *externalapi.DomainBlock, updateVirtual bool) error { + if updateVirtual { + s.lock.Lock() + if s.virtualNotUpdated { + s.lock.Unlock() + err := s.ResolveVirtual(nil) + if err != nil { + return err + } + return s.validateAndInsertBlockWithLock(block, updateVirtual) + } + defer s.lock.Unlock() + _, err := s.validateAndInsertBlockNoLock(block, updateVirtual) + if err != nil { + return err + } + return nil + } + + return s.validateAndInsertBlockWithLock(block, updateVirtual) +} + +func (s *consensus) validateAndInsertBlockWithLock(block *externalapi.DomainBlock, updateVirtual bool) error { s.lock.Lock() defer s.lock.Unlock() - _, err := s.validateAndInsertBlockNoLock(block, shouldValidateAgainstUTXO) + _, err := s.validateAndInsertBlockNoLock(block, updateVirtual) if err != nil { return err } @@ -206,19 +228,6 @@ func (s *consensus) ValidateAndInsertBlock(block *externalapi.DomainBlock, shoul } func (s *consensus) validateAndInsertBlockNoLock(block *externalapi.DomainBlock, updateVirtual bool) (*externalapi.VirtualChangeSet, error) { - // If virtual is in non-updated state, and the caller requests updating virtual -- then we must first - // resolve virtual so that the new block can be fully processed properly - if updateVirtual && s.virtualNotUpdated { - for s.virtualNotUpdated { - // We use 10000 << finality interval. See comment in `ResolveVirtual`. - // We give up responsiveness of consensus in this rare case. - _, err := s.resolveVirtualNoLock(10000) // Note `s.virtualNotUpdated` is updated within the call - if err != nil { - return nil, err - } - } - } - virtualChangeSet, blockStatus, err := s.blockProcessor.ValidateAndInsertBlock(block, updateVirtual) if err != nil { return nil, err @@ -257,7 +266,7 @@ func (s *consensus) sendBlockAddedEvent(block *externalapi.DomainBlock, blockSta } func (s *consensus) sendVirtualChangedEvent(virtualChangeSet *externalapi.VirtualChangeSet, wasVirtualUpdated bool) error { - if !wasVirtualUpdated || s.consensusEventsChan == nil { + if !wasVirtualUpdated || s.consensusEventsChan == nil || virtualChangeSet == nil { return nil } @@ -888,41 +897,68 @@ func (s *consensus) PopulateMass(transaction *externalapi.DomainTransaction) { s.transactionValidator.PopulateMass(transaction) } -func (s *consensus) ResolveVirtual() (bool, error) { +func (s *consensus) ResolveVirtual(progressReportCallback func(uint64, uint64)) error { + virtualDAAScoreStart, err := s.GetVirtualDAAScore() + if err != nil { + return err + } + + for i := 0; ; i++ { + if i%10 == 0 && progressReportCallback != nil { + virtualDAAScore, err := s.GetVirtualDAAScore() + if err != nil { + return err + } + progressReportCallback(virtualDAAScoreStart, virtualDAAScore) + } + + // In order to prevent a situation that the consensus lock is held for too much time, we + // release the lock each time we resolve 100 blocks. + // Note: maxBlocksToResolve should be smaller than `params.FinalityDuration` in order to avoid a situation + // where UpdatePruningPointByVirtual skips a pruning point. + _, isCompletelyResolved, err := s.resolveVirtualChunkWithLock(100) + if err != nil { + return err + } + if isCompletelyResolved { + break + } + } + + return nil +} + +func (s *consensus) resolveVirtualChunkWithLock(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) { s.lock.Lock() defer s.lock.Unlock() - // In order to prevent a situation that the consensus lock is held for too much time, we - // release the lock each time resolve 100 blocks. - // Note: maxBlocksToResolve should be smaller than finality interval in order to avoid a situation - // where UpdatePruningPointByVirtual skips a pruning point. - return s.resolveVirtualNoLock(100) + return s.resolveVirtualChunkNoLock(maxBlocksToResolve) } -func (s *consensus) resolveVirtualNoLock(maxBlocksToResolve uint64) (bool, error) { +func (s *consensus) resolveVirtualChunkNoLock(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) { virtualChangeSet, isCompletelyResolved, err := s.consensusStateManager.ResolveVirtual(maxBlocksToResolve) if err != nil { - return false, err + return nil, false, err } s.virtualNotUpdated = !isCompletelyResolved stagingArea := model.NewStagingArea() err = s.pruningManager.UpdatePruningPointByVirtual(stagingArea) if err != nil { - return false, err + return nil, false, err } err = staging.CommitAllChanges(s.databaseContext, stagingArea) if err != nil { - return false, err + return nil, false, err } err = s.sendVirtualChangedEvent(virtualChangeSet, true) if err != nil { - return false, err + return nil, false, err } - return isCompletelyResolved, nil + return virtualChangeSet, isCompletelyResolved, nil } func (s *consensus) BuildPruningPointProof() (*externalapi.PruningPointProof, error) { diff --git a/domain/consensus/factory.go b/domain/consensus/factory.go index f5167fc7e..3b92ecb93 100644 --- a/domain/consensus/factory.go +++ b/domain/consensus/factory.go @@ -515,6 +515,7 @@ func (f *factory) NewConsensus(config *Config, db infrastructuredatabase.Databas blocksWithTrustedDataDAAWindowStore: daaWindowStore, consensusEventsChan: consensusEventsChan, + virtualNotUpdated: true, } if isOldReachabilityInitialized { diff --git a/domain/consensus/finality_test.go b/domain/consensus/finality_test.go index 5e4e9824b..cf7644288 100644 --- a/domain/consensus/finality_test.go +++ b/domain/consensus/finality_test.go @@ -589,18 +589,13 @@ func TestFinalityResolveVirtual(t *testing.T) { } } - for i := 0; ; i++ { - isCompletelyResolved, err := tc.ResolveVirtual() - if err != nil { - panic(err) - } - - if isCompletelyResolved { - t.Log("Resolved virtual") - break - } + err = tc.ResolveVirtual(nil) + if err != nil { + panic(err) } + t.Log("Resolved virtual") + sideChainTipGHOSTDAGData, err = tc.GHOSTDAGDataStore().Get(tc.DatabaseContext(), stagingArea, sideChainTipHash, false) if err != nil { panic(err) diff --git a/domain/consensus/model/externalapi/blockinfo.go b/domain/consensus/model/externalapi/blockinfo.go index c7f75a2c3..43c914f92 100644 --- a/domain/consensus/model/externalapi/blockinfo.go +++ b/domain/consensus/model/externalapi/blockinfo.go @@ -13,6 +13,16 @@ type BlockInfo struct { MergeSetReds []*DomainHash } +// HasHeader returns whether the block exists and has a valid header +func (bi *BlockInfo) HasHeader() bool { + return bi.Exists && bi.BlockStatus != StatusInvalid +} + +// HasBody returns whether the block exists and has a valid body +func (bi *BlockInfo) HasBody() bool { + return bi.Exists && bi.BlockStatus != StatusInvalid && bi.BlockStatus != StatusHeaderOnly +} + // Clone returns a clone of BlockInfo func (bi *BlockInfo) Clone() *BlockInfo { return &BlockInfo{ diff --git a/domain/consensus/model/externalapi/consensus.go b/domain/consensus/model/externalapi/consensus.go index adaa81bc4..d7aadc203 100644 --- a/domain/consensus/model/externalapi/consensus.go +++ b/domain/consensus/model/externalapi/consensus.go @@ -5,7 +5,7 @@ type Consensus interface { Init(skipAddingGenesis bool) error BuildBlock(coinbaseData *DomainCoinbaseData, transactions []*DomainTransaction) (*DomainBlock, error) BuildBlockTemplate(coinbaseData *DomainCoinbaseData, transactions []*DomainTransaction) (*DomainBlockTemplate, error) - ValidateAndInsertBlock(block *DomainBlock, shouldValidateAgainstUTXO bool) error + ValidateAndInsertBlock(block *DomainBlock, updateVirtual bool) error ValidateAndInsertBlockWithTrustedData(block *BlockWithTrustedData, validateUTXO bool) error ValidateTransactionAndPopulateWithConsensusData(transaction *DomainTransaction) error ImportPruningPoints(pruningPoints []BlockHeader) error @@ -48,7 +48,7 @@ type Consensus interface { Anticone(blockHash *DomainHash) ([]*DomainHash, error) EstimateNetworkHashesPerSecond(startHash *DomainHash, windowSize int) (uint64, error) PopulateMass(transaction *DomainTransaction) - ResolveVirtual() (bool, error) + ResolveVirtual(progressReportCallback func(uint64, uint64)) error BlockDAAWindowHashes(blockHash *DomainHash) ([]*DomainHash, error) TrustedDataDataDAAHeader(trustedBlockHash, daaBlockHash *DomainHash, daaBlockWindowIndex uint64) (*TrustedDataDataDAAHeader, error) TrustedBlockAssociatedGHOSTDAGDataBlockHashes(blockHash *DomainHash) ([]*DomainHash, error) diff --git a/domain/consensus/model/testapi/test_consensus.go b/domain/consensus/model/testapi/test_consensus.go index 828cabd1a..f7be59406 100644 --- a/domain/consensus/model/testapi/test_consensus.go +++ b/domain/consensus/model/testapi/test_consensus.go @@ -49,7 +49,7 @@ type TestConsensus interface { *externalapi.VirtualChangeSet, error) UpdatePruningPointByVirtual() error - ResolveVirtualWithMaxParam(maxBlocksToResolve uint64) (bool, error) + ResolveVirtualWithMaxParam(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) MineJSON(r io.Reader, blockType MineJSONBlockType) (tips []*externalapi.DomainHash, err error) ToJSON(w io.Writer) error diff --git a/domain/consensus/processes/consensusstatemanager/resolve.go b/domain/consensus/processes/consensusstatemanager/resolve.go index 3a3624881..85528c9b3 100644 --- a/domain/consensus/processes/consensusstatemanager/resolve.go +++ b/domain/consensus/processes/consensusstatemanager/resolve.go @@ -5,22 +5,22 @@ import ( "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/infrastructure/logger" "github.com/kaspanet/kaspad/util/staging" + "github.com/pkg/errors" "sort" ) -func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) { - onEnd := logger.LogAndMeasureExecutionTime(log, "csm.ResolveVirtual") - defer onEnd() - - readStagingArea := model.NewStagingArea() - tips, err := csm.consensusStateStore.Tips(readStagingArea, csm.databaseContext) +// tipsInDecreasingGHOSTDAGParentSelectionOrder returns the current DAG tips in decreasing parent selection order. +// This means that the first tip in the resulting list would be the GHOSTDAG selected parent, and if removed from the list, +// the second tip would be the selected parent, and so on. +func (csm *consensusStateManager) tipsInDecreasingGHOSTDAGParentSelectionOrder(stagingArea *model.StagingArea) ([]*externalapi.DomainHash, error) { + tips, err := csm.consensusStateStore.Tips(stagingArea, csm.databaseContext) if err != nil { - return nil, false, err + return nil, err } var sortErr error sort.Slice(tips, func(i, j int) bool { - selectedParent, err := csm.ghostdagManager.ChooseSelectedParent(readStagingArea, tips[i], tips[j]) + selectedParent, err := csm.ghostdagManager.ChooseSelectedParent(stagingArea, tips[i], tips[j]) if err != nil { sortErr = err return false @@ -29,16 +29,22 @@ func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*ex return selectedParent.Equal(tips[i]) }) if sortErr != nil { - return nil, false, sortErr + return nil, sortErr + } + return tips, nil +} + +func (csm *consensusStateManager) findNextPendingTip(stagingArea *model.StagingArea) (*externalapi.DomainHash, externalapi.BlockStatus, error) { + orderedTips, err := csm.tipsInDecreasingGHOSTDAGParentSelectionOrder(stagingArea) + if err != nil { + return nil, externalapi.StatusInvalid, err } - var selectedTip *externalapi.DomainHash - isCompletelyResolved := true - for _, tip := range tips { + for _, tip := range orderedTips { log.Debugf("Resolving tip %s", tip) - isViolatingFinality, shouldNotify, err := csm.isViolatingFinality(readStagingArea, tip) + isViolatingFinality, shouldNotify, err := csm.isViolatingFinality(stagingArea, tip) if err != nil { - return nil, false, err + return nil, externalapi.StatusInvalid, err } if isViolatingFinality { @@ -49,55 +55,147 @@ func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*ex continue } - resolveStagingArea := model.NewStagingArea() - unverifiedBlocks, err := csm.getUnverifiedChainBlocks(resolveStagingArea, tip) + status, err := csm.blockStatusStore.Get(csm.databaseContext, stagingArea, tip) if err != nil { - return nil, false, err + return nil, externalapi.StatusInvalid, err } - - resolveTip := tip - hasMoreUnverifiedThanMax := maxBlocksToResolve != 0 && uint64(len(unverifiedBlocks)) > maxBlocksToResolve - if hasMoreUnverifiedThanMax { - resolveTip = unverifiedBlocks[uint64(len(unverifiedBlocks))-maxBlocksToResolve] - log.Debugf("Has more than %d blocks to resolve. Changing the resolve tip to %s", maxBlocksToResolve, resolveTip) - } - - blockStatus, reversalData, err := csm.resolveBlockStatus(resolveStagingArea, resolveTip, true) - if err != nil { - return nil, false, err - } - - if blockStatus == externalapi.StatusUTXOValid { - selectedTip = resolveTip - isCompletelyResolved = !hasMoreUnverifiedThanMax - - err = staging.CommitAllChanges(csm.databaseContext, resolveStagingArea) - if err != nil { - return nil, false, err - } - - if reversalData != nil { - err = csm.ReverseUTXODiffs(resolveTip, reversalData) - if err != nil { - return nil, false, err - } - } - break + if status == externalapi.StatusUTXOValid || status == externalapi.StatusUTXOPendingVerification { + return tip, status, nil } } - if selectedTip == nil { - log.Warnf("Non of the DAG tips are valid") - return nil, true, nil + return nil, externalapi.StatusInvalid, nil +} + +// getGHOSTDAGLowerTips returns the set of tips which are lower in GHOSTDAG parent selection order than `pendingTip`. i.e., +// they can be added to virtual parents but `pendingTip` will remain the virtual selected parent +func (csm *consensusStateManager) getGHOSTDAGLowerTips(stagingArea *model.StagingArea, pendingTip *externalapi.DomainHash) ([]*externalapi.DomainHash, error) { + tips, err := csm.consensusStateStore.Tips(stagingArea, csm.databaseContext) + if err != nil { + return nil, err } - oldVirtualGHOSTDAGData, err := csm.ghostdagDataStore.Get(csm.databaseContext, readStagingArea, model.VirtualBlockHash, false) + lowerTips := []*externalapi.DomainHash{pendingTip} + for _, tip := range tips { + if tip.Equal(pendingTip) { + continue + } + selectedParent, err := csm.ghostdagManager.ChooseSelectedParent(stagingArea, tip, pendingTip) + if err != nil { + return nil, err + } + if selectedParent.Equal(pendingTip) { + lowerTips = append(lowerTips, tip) + } + } + return lowerTips, nil +} + +func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) { + onEnd := logger.LogAndMeasureExecutionTime(log, "csm.ResolveVirtual") + defer onEnd() + + // We use a read-only staging area for some read-only actions, to avoid + // confusion with the resolve/updateVirtual staging areas below + readStagingArea := model.NewStagingArea() + + pendingTip, pendingTipStatus, err := csm.findNextPendingTip(readStagingArea) if err != nil { return nil, false, err } + if pendingTip == nil { + log.Warnf("None of the DAG tips are valid") + return nil, true, nil + } + + previousVirtualSelectedParent, err := csm.virtualSelectedParent(readStagingArea) + if err != nil { + return nil, false, err + } + + if pendingTipStatus == externalapi.StatusUTXOValid && previousVirtualSelectedParent.Equal(pendingTip) { + return nil, true, nil + } + + // Resolve a chunk from the pending chain + resolveStagingArea := model.NewStagingArea() + unverifiedBlocks, err := csm.getUnverifiedChainBlocks(resolveStagingArea, pendingTip) + if err != nil { + return nil, false, err + } + + // Initially set the resolve processing point to the pending tip + processingPoint := pendingTip + + // Too many blocks to verify, so we only process a chunk and return + if maxBlocksToResolve != 0 && uint64(len(unverifiedBlocks)) > maxBlocksToResolve { + processingPointIndex := uint64(len(unverifiedBlocks)) - maxBlocksToResolve + processingPoint = unverifiedBlocks[processingPointIndex] + isNewVirtualSelectedParent, err := csm.isNewSelectedTip(readStagingArea, processingPoint, previousVirtualSelectedParent) + if err != nil { + return nil, false, err + } + + // We must find a processing point which wins previous virtual selected parent + // even if we process more than `maxBlocksToResolve` for that. + // Otherwise, internal UTXO diff logic gets all messed up + for !isNewVirtualSelectedParent { + if processingPointIndex == 0 { + return nil, false, errors.Errorf( + "Expecting the pending tip %s to overcome the previous selected parent %s", pendingTip, previousVirtualSelectedParent) + } + processingPointIndex-- + processingPoint = unverifiedBlocks[processingPointIndex] + isNewVirtualSelectedParent, err = csm.isNewSelectedTip(readStagingArea, processingPoint, previousVirtualSelectedParent) + if err != nil { + return nil, false, err + } + } + log.Debugf("Has more than %d blocks to resolve. Setting the resolve processing point to %s", maxBlocksToResolve, processingPoint) + } + + processingPointStatus, reversalData, err := csm.resolveBlockStatus( + resolveStagingArea, processingPoint, true) + if err != nil { + return nil, false, err + } + + if processingPointStatus == externalapi.StatusUTXOValid { + err = staging.CommitAllChanges(csm.databaseContext, resolveStagingArea) + if err != nil { + return nil, false, err + } + + if reversalData != nil { + err = csm.ReverseUTXODiffs(processingPoint, reversalData) + if err != nil { + return nil, false, err + } + } + } + + isActualTip := processingPoint.Equal(pendingTip) + isCompletelyResolved := isActualTip && processingPointStatus == externalapi.StatusUTXOValid + updateVirtualStagingArea := model.NewStagingArea() - virtualUTXODiff, err := csm.updateVirtualWithParents(updateVirtualStagingArea, []*externalapi.DomainHash{selectedTip}) + + virtualParents := []*externalapi.DomainHash{processingPoint} + // If `isCompletelyResolved`, set virtual correctly with all tips which have less blue work than pending + if isCompletelyResolved { + lowerTips, err := csm.getGHOSTDAGLowerTips(readStagingArea, pendingTip) + if err != nil { + return nil, false, err + } + log.Debugf("Picking virtual parents from relevant tips len: %d", len(lowerTips)) + + virtualParents, err = csm.pickVirtualParents(readStagingArea, lowerTips) + if err != nil { + return nil, false, err + } + log.Debugf("Picked virtual parents: %s", virtualParents) + } + virtualUTXODiff, err := csm.updateVirtualWithParents(updateVirtualStagingArea, virtualParents) if err != nil { return nil, false, err } @@ -108,12 +206,12 @@ func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*ex } selectedParentChainChanges, err := csm.dagTraversalManager. - CalculateChainPath(readStagingArea, oldVirtualGHOSTDAGData.SelectedParent(), selectedTip) + CalculateChainPath(updateVirtualStagingArea, previousVirtualSelectedParent, processingPoint) if err != nil { return nil, false, err } - virtualParents, err := csm.dagTopologyManager.Parents(readStagingArea, model.VirtualBlockHash) + virtualParentsOutcome, err := csm.dagTopologyManager.Parents(updateVirtualStagingArea, model.VirtualBlockHash) if err != nil { return nil, false, err } @@ -121,6 +219,6 @@ func (csm *consensusStateManager) ResolveVirtual(maxBlocksToResolve uint64) (*ex return &externalapi.VirtualChangeSet{ VirtualSelectedParentChainChanges: selectedParentChainChanges, VirtualUTXODiff: virtualUTXODiff, - VirtualParents: virtualParents, + VirtualParents: virtualParentsOutcome, }, isCompletelyResolved, nil } diff --git a/domain/consensus/processes/consensusstatemanager/resolve_block_status.go b/domain/consensus/processes/consensusstatemanager/resolve_block_status.go index 860a94472..631cbeee5 100644 --- a/domain/consensus/processes/consensusstatemanager/resolve_block_status.go +++ b/domain/consensus/processes/consensusstatemanager/resolve_block_status.go @@ -233,7 +233,7 @@ func (csm *consensusStateManager) resolveSingleBlockStatus(stagingArea *model.St return externalapi.StatusUTXOValid, nil, nil } - oldSelectedTip, err := csm.selectedTip(stagingArea) + oldSelectedTip, err := csm.virtualSelectedParent(stagingArea) if err != nil { return 0, nil, err } @@ -298,7 +298,7 @@ func (csm *consensusStateManager) isNewSelectedTip(stagingArea *model.StagingAre return blockHash.Equal(newSelectedTip), nil } -func (csm *consensusStateManager) selectedTip(stagingArea *model.StagingArea) (*externalapi.DomainHash, error) { +func (csm *consensusStateManager) virtualSelectedParent(stagingArea *model.StagingArea) (*externalapi.DomainHash, error) { virtualGHOSTDAGData, err := csm.ghostdagDataStore.Get(csm.databaseContext, stagingArea, model.VirtualBlockHash, false) if err != nil { return nil, err diff --git a/domain/consensus/processes/consensusstatemanager/resolve_virtual_test.go b/domain/consensus/processes/consensusstatemanager/resolve_virtual_test.go index 4af0c337c..5257c45b8 100644 --- a/domain/consensus/processes/consensusstatemanager/resolve_virtual_test.go +++ b/domain/consensus/processes/consensusstatemanager/resolve_virtual_test.go @@ -1,6 +1,9 @@ package consensusstatemanager_test import ( + "fmt" + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/testapi" "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" "testing" @@ -21,11 +24,14 @@ func TestAddBlockBetweenResolveVirtualCalls(t *testing.T) { } defer teardown(false) + hashes := []*externalapi.DomainHash{consensusConfig.GenesisHash} + // Create a chain of blocks const initialChainLength = 10 previousBlockHash := consensusConfig.GenesisHash for i := 0; i < initialChainLength; i++ { previousBlockHash, _, err = tc.AddBlock([]*externalapi.DomainHash{previousBlockHash}, nil, nil) + hashes = append(hashes, previousBlockHash) if err != nil { t.Fatalf("Error mining block no. %d in initial chain: %+v", i, err) } @@ -40,6 +46,7 @@ func TestAddBlockBetweenResolveVirtualCalls(t *testing.T) { t.Fatalf("Error mining block no. %d in re-org chain: %+v", i, err) } previousBlockHash = consensushashing.BlockHash(previousBlock) + hashes = append(hashes, previousBlockHash) // Do not UTXO validate in order to resolve virtual later err = tc.ValidateAndInsertBlock(previousBlock, false) @@ -49,7 +56,7 @@ func TestAddBlockBetweenResolveVirtualCalls(t *testing.T) { } // Resolve one step - _, err = tc.ResolveVirtualWithMaxParam(2) + _, _, err = tc.ResolveVirtualWithMaxParam(2) if err != nil { t.Fatalf("Error resolving virtual in re-org chain: %+v", err) } @@ -68,7 +75,7 @@ func TestAddBlockBetweenResolveVirtualCalls(t *testing.T) { } // Resolve one more step - isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2) + _, isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2) if err != nil { t.Fatalf("Error resolving virtual in re-org chain: %+v", err) } @@ -78,14 +85,17 @@ func TestAddBlockBetweenResolveVirtualCalls(t *testing.T) { if err != nil { t.Fatalf("Error mining block during virtual resolution of reorg: %+v", err) } + hashes = append(hashes, consensushashing.BlockHash(blockTemplate.Block)) // Complete resolving virtual for !isCompletelyResolved { - isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2) + _, isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2) if err != nil { t.Fatalf("Error resolving virtual in re-org chain: %+v", err) } } + + verifyUtxoDiffPaths(t, tc, hashes) }) } @@ -100,11 +110,14 @@ func TestAddGenesisChildAfterOneResolveVirtualCall(t *testing.T) { } defer teardown(false) + hashes := []*externalapi.DomainHash{consensusConfig.GenesisHash} + // Create a chain of blocks const initialChainLength = 6 previousBlockHash := consensusConfig.GenesisHash for i := 0; i < initialChainLength; i++ { previousBlockHash, _, err = tc.AddBlock([]*externalapi.DomainHash{previousBlockHash}, nil, nil) + hashes = append(hashes, previousBlockHash) if err != nil { t.Fatalf("Error mining block no. %d in initial chain: %+v", i, err) } @@ -119,6 +132,7 @@ func TestAddGenesisChildAfterOneResolveVirtualCall(t *testing.T) { t.Fatalf("Error mining block no. %d in re-org chain: %+v", i, err) } previousBlockHash = consensushashing.BlockHash(previousBlock) + hashes = append(hashes, previousBlockHash) // Do not UTXO validate in order to resolve virtual later err = tc.ValidateAndInsertBlock(previousBlock, false) @@ -128,7 +142,7 @@ func TestAddGenesisChildAfterOneResolveVirtualCall(t *testing.T) { } // Resolve one step - isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2) + _, isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2) if err != nil { t.Fatalf("Error resolving virtual in re-org chain: %+v", err) } @@ -140,11 +154,13 @@ func TestAddGenesisChildAfterOneResolveVirtualCall(t *testing.T) { // Complete resolving virtual for !isCompletelyResolved { - isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2) + _, isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2) if err != nil { t.Fatalf("Error resolving virtual in re-org chain: %+v", err) } } + + verifyUtxoDiffPaths(t, tc, hashes) }) } @@ -159,11 +175,14 @@ func TestAddGenesisChildAfterTwoResolveVirtualCalls(t *testing.T) { } defer teardown(false) + hashes := []*externalapi.DomainHash{consensusConfig.GenesisHash} + // Create a chain of blocks const initialChainLength = 6 previousBlockHash := consensusConfig.GenesisHash for i := 0; i < initialChainLength; i++ { previousBlockHash, _, err = tc.AddBlock([]*externalapi.DomainHash{previousBlockHash}, nil, nil) + hashes = append(hashes, previousBlockHash) if err != nil { t.Fatalf("Error mining block no. %d in initial chain: %+v", i, err) } @@ -178,6 +197,7 @@ func TestAddGenesisChildAfterTwoResolveVirtualCalls(t *testing.T) { t.Fatalf("Error mining block no. %d in re-org chain: %+v", i, err) } previousBlockHash = consensushashing.BlockHash(previousBlock) + hashes = append(hashes, previousBlockHash) // Do not UTXO validate in order to resolve virtual later err = tc.ValidateAndInsertBlock(previousBlock, false) @@ -187,13 +207,13 @@ func TestAddGenesisChildAfterTwoResolveVirtualCalls(t *testing.T) { } // Resolve one step - _, err = tc.ResolveVirtualWithMaxParam(2) + _, _, err = tc.ResolveVirtualWithMaxParam(2) if err != nil { t.Fatalf("Error resolving virtual in re-org chain: %+v", err) } // Resolve one more step - isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2) + _, isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(2) if err != nil { t.Fatalf("Error resolving virtual in re-org chain: %+v", err) } @@ -205,10 +225,220 @@ func TestAddGenesisChildAfterTwoResolveVirtualCalls(t *testing.T) { // Complete resolving virtual for !isCompletelyResolved { - isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2) + _, isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(2) if err != nil { t.Fatalf("Error resolving virtual in re-org chain: %+v", err) } } + + verifyUtxoDiffPaths(t, tc, hashes) }) } + +func TestResolveVirtualBackAndForthReorgs(t *testing.T) { + + testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) { + factory := consensus.NewFactory() + + tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestAddGenesisChildAfterTwoResolveVirtualCalls") + if err != nil { + t.Fatalf("Error setting up consensus: %+v", err) + } + defer teardown(false) + + hashes := []*externalapi.DomainHash{consensusConfig.GenesisHash} + blocks := make(map[externalapi.DomainHash]string) + blocks[*consensusConfig.GenesisHash] = "g" + blocks[*model.VirtualBlockHash] = "v" + printfDebug("%s\n\n", consensusConfig.GenesisHash) + + // Create a chain of blocks + const initialChainLength = 6 + previousBlockHash := consensusConfig.GenesisHash + for i := 0; i < initialChainLength; i++ { + previousBlockHash, _, err = tc.AddBlock([]*externalapi.DomainHash{previousBlockHash}, nil, nil) + blocks[*previousBlockHash] = fmt.Sprintf("A_%d", i) + hashes = append(hashes, previousBlockHash) + printfDebug("A_%d: %s\n", i, previousBlockHash) + + if err != nil { + t.Fatalf("Error mining block no. %d in initial chain: %+v", i, err) + } + } + + printfDebug("\n") + verifyUtxoDiffPaths(t, tc, hashes) + + firstChainTip := previousBlockHash + + // Mine a chain with more blocks, to re-organize the DAG + const reorgChainLength = 12 // initialChainLength + 1 + previousBlockHash = consensusConfig.GenesisHash + for i := 0; i < reorgChainLength; i++ { + previousBlock, _, err := tc.BuildBlockWithParents([]*externalapi.DomainHash{previousBlockHash}, nil, nil) + if err != nil { + t.Fatalf("Error mining block no. %d in re-org chain: %+v", i, err) + } + previousBlockHash = consensushashing.BlockHash(previousBlock) + blocks[*previousBlockHash] = fmt.Sprintf("B_%d", i) + hashes = append(hashes, previousBlockHash) + printfDebug("B_%d: %s\n", i, previousBlockHash) + + // Do not UTXO validate in order to resolve virtual later + err = tc.ValidateAndInsertBlock(previousBlock, false) + if err != nil { + t.Fatalf("Error mining block no. %d in re-org chain: %+v", i, err) + } + } + + printfDebug("\n") + + printUtxoDiffChildren(t, tc, hashes, blocks) + verifyUtxoDiffPaths(t, tc, hashes) + + previousVirtualSelectedParent, err := tc.GetVirtualSelectedParent() + if err != nil { + t.Fatal(err) + } + + // Resolve one step + virtualChangeSet, _, err := tc.ResolveVirtualWithMaxParam(3) + if err != nil { + printUtxoDiffChildren(t, tc, hashes, blocks) + t.Fatalf("Error resolving virtual in re-org chain: %+v", err) + } + + newVirtualSelectedParent, err := tc.GetVirtualSelectedParent() + if err != nil { + t.Fatal(err) + } + + // Make sure the reported change-set is compatible with actual changes. + // Checking this for one call should suffice to avoid possible bugs. + reportedPreviousVirtualSelectedParent := virtualChangeSet.VirtualSelectedParentChainChanges.Removed[0] + reportedNewVirtualSelectedParent := virtualChangeSet.VirtualSelectedParentChainChanges. + Added[len(virtualChangeSet.VirtualSelectedParentChainChanges.Added)-1] + + if !previousVirtualSelectedParent.Equal(reportedPreviousVirtualSelectedParent) { + t.Fatalf("The reported changeset is incompatible with actual changes") + } + if !newVirtualSelectedParent.Equal(reportedNewVirtualSelectedParent) { + t.Fatalf("The reported changeset is incompatible with actual changes") + } + + // Resolve one more step + _, isCompletelyResolved, err := tc.ResolveVirtualWithMaxParam(3) + if err != nil { + t.Fatalf("Error resolving virtual in re-org chain: %+v", err) + } + + // Complete resolving virtual + for !isCompletelyResolved { + _, isCompletelyResolved, err = tc.ResolveVirtualWithMaxParam(3) + if err != nil { + t.Fatalf("Error resolving virtual in re-org chain: %+v", err) + } + } + + printUtxoDiffChildren(t, tc, hashes, blocks) + verifyUtxoDiffPaths(t, tc, hashes) + + // Now get the first chain back to the wining position + previousBlockHash = firstChainTip + for i := 0; i < reorgChainLength; i++ { + previousBlockHash, _, err = tc.AddBlock([]*externalapi.DomainHash{previousBlockHash}, nil, nil) + blocks[*previousBlockHash] = fmt.Sprintf("A_%d", initialChainLength+i) + hashes = append(hashes, previousBlockHash) + printfDebug("A_%d: %s\n", initialChainLength+i, previousBlockHash) + + if err != nil { + t.Fatalf("Error mining block no. %d in initial chain: %+v", initialChainLength+i, err) + } + } + + printfDebug("\n") + + printUtxoDiffChildren(t, tc, hashes, blocks) + verifyUtxoDiffPaths(t, tc, hashes) + }) +} + +func verifyUtxoDiffPathToRoot(t *testing.T, tc testapi.TestConsensus, stagingArea *model.StagingArea, block, utxoDiffRoot *externalapi.DomainHash) { + current := block + for !current.Equal(utxoDiffRoot) { + hasUTXODiffChild, err := tc.UTXODiffStore().HasUTXODiffChild(tc.DatabaseContext(), stagingArea, current) + if err != nil { + t.Fatalf("Error while reading utxo diff store: %+v", err) + } + if !hasUTXODiffChild { + t.Fatalf("%s is expected to have a UTXO diff child", current) + } + current, err = tc.UTXODiffStore().UTXODiffChild(tc.DatabaseContext(), stagingArea, current) + if err != nil { + t.Fatalf("Error while reading utxo diff store: %+v", err) + } + } +} + +func verifyUtxoDiffPaths(t *testing.T, tc testapi.TestConsensus, hashes []*externalapi.DomainHash) { + stagingArea := model.NewStagingArea() + + virtualGHOSTDAGData, err := tc.GHOSTDAGDataStore().Get(tc.DatabaseContext(), stagingArea, model.VirtualBlockHash, false) + if err != nil { + t.Fatal(err) + } + + utxoDiffRoot := virtualGHOSTDAGData.SelectedParent() + hasUTXODiffChild, err := tc.UTXODiffStore().HasUTXODiffChild(tc.DatabaseContext(), stagingArea, utxoDiffRoot) + if err != nil { + t.Fatalf("Error while reading utxo diff store: %+v", err) + } + if hasUTXODiffChild { + t.Fatalf("Virtual selected parent is not expected to have an explicit diff child") + } + _, err = tc.UTXODiffStore().UTXODiff(tc.DatabaseContext(), stagingArea, utxoDiffRoot) + if err != nil { + t.Fatalf("Virtual selected parent is expected to have a utxo diff: %+v", err) + } + + for _, block := range hashes { + hasUTXODiffChild, err = tc.UTXODiffStore().HasUTXODiffChild(tc.DatabaseContext(), stagingArea, block) + if err != nil { + t.Fatalf("Error while reading utxo diff store: %+v", err) + } + isOnVirtualSelectedChain, err := tc.DAGTopologyManager().IsInSelectedParentChainOf(stagingArea, block, utxoDiffRoot) + if err != nil { + t.Fatal(err) + } + // We expect a valid path to root in both cases: (i) block has a diff child, (ii) block is on the virtual selected chain + if hasUTXODiffChild || isOnVirtualSelectedChain { + verifyUtxoDiffPathToRoot(t, tc, stagingArea, block, utxoDiffRoot) + } + } +} + +func printfDebug(format string, a ...any) { + // Uncomment below when debugging the test + //fmt.Printf(format, a...) +} + +func printUtxoDiffChildren(t *testing.T, tc testapi.TestConsensus, hashes []*externalapi.DomainHash, blocks map[externalapi.DomainHash]string) { + printfDebug("\n===============================\nBlock\t\tDiff child\n") + stagingArea := model.NewStagingArea() + for _, block := range hashes { + hasUTXODiffChild, err := tc.UTXODiffStore().HasUTXODiffChild(tc.DatabaseContext(), stagingArea, block) + if err != nil { + t.Fatalf("Error while reading utxo diff store: %+v", err) + } + if hasUTXODiffChild { + utxoDiffChild, err := tc.UTXODiffStore().UTXODiffChild(tc.DatabaseContext(), stagingArea, block) + if err != nil { + t.Fatalf("Error while reading utxo diff store: %+v", err) + } + printfDebug("%s\t\t\t%s\n", blocks[*block], blocks[*utxoDiffChild]) + } else { + printfDebug("%s\n", blocks[*block]) + } + } + printfDebug("\n===============================\n") +} diff --git a/domain/consensus/processes/consensusstatemanager/reverse_utxo_diffs.go b/domain/consensus/processes/consensusstatemanager/reverse_utxo_diffs.go index b8f388d20..49d2b24bd 100644 --- a/domain/consensus/processes/consensusstatemanager/reverse_utxo_diffs.go +++ b/domain/consensus/processes/consensusstatemanager/reverse_utxo_diffs.go @@ -56,12 +56,6 @@ func (csm *consensusStateManager) ReverseUTXODiffs(tipHash *externalapi.DomainHa return err } - // We stop reversing when current's UTXODiffChild is not current's SelectedParent - if !currentBlockGHOSTDAGData.SelectedParent().Equal(currentBlockUTXODiffChild) { - log.Debugf("Block %s's UTXODiffChild is not it's selected parent - finish reversing", currentBlock) - break - } - currentUTXODiff := previousUTXODiff.Reversed() // retrieve current utxoDiff for Bi, to be used by next block @@ -75,6 +69,12 @@ func (csm *consensusStateManager) ReverseUTXODiffs(tipHash *externalapi.DomainHa return err } + // We stop reversing when current's UTXODiffChild is not current's SelectedParent + if !currentBlockGHOSTDAGData.SelectedParent().Equal(currentBlockUTXODiffChild) { + log.Debugf("Block %s's UTXODiffChild is not it's selected parent - finish reversing", currentBlock) + break + } + previousBlock = currentBlock previousBlockGHOSTDAGData = currentBlockGHOSTDAGData diff --git a/domain/consensus/processes/consensusstatemanager/update_virtual.go b/domain/consensus/processes/consensusstatemanager/update_virtual.go index 8cb63c751..9b38bc942 100644 --- a/domain/consensus/processes/consensusstatemanager/update_virtual.go +++ b/domain/consensus/processes/consensusstatemanager/update_virtual.go @@ -110,7 +110,7 @@ func (csm *consensusStateManager) updateSelectedTipUTXODiff( onEnd := logger.LogAndMeasureExecutionTime(log, "updateSelectedTipUTXODiff") defer onEnd() - selectedTip, err := csm.selectedTip(stagingArea) + selectedTip, err := csm.virtualSelectedParent(stagingArea) if err != nil { return err } diff --git a/domain/consensus/test_consensus.go b/domain/consensus/test_consensus.go index 0eda211c9..6f67c47a3 100644 --- a/domain/consensus/test_consensus.go +++ b/domain/consensus/test_consensus.go @@ -112,11 +112,11 @@ func (tc *testConsensus) AddUTXOInvalidBlock(parentHashes []*externalapi.DomainH return consensushashing.BlockHash(block), virtualChangeSet, nil } -func (tc *testConsensus) ResolveVirtualWithMaxParam(maxBlocksToResolve uint64) (bool, error) { +func (tc *testConsensus) ResolveVirtualWithMaxParam(maxBlocksToResolve uint64) (*externalapi.VirtualChangeSet, bool, error) { tc.lock.Lock() defer tc.lock.Unlock() - return tc.resolveVirtualNoLock(maxBlocksToResolve) + return tc.resolveVirtualChunkNoLock(maxBlocksToResolve) } // jsonBlock is a json representation of a block in mine format diff --git a/domain/migrate.go b/domain/migrate.go index c1804862e..ef75a0458 100644 --- a/domain/migrate.go +++ b/domain/migrate.go @@ -214,34 +214,19 @@ func syncConsensuses(syncer, syncee externalapi.Consensus) error { return err } - virtualDAAScoreStart, err := syncee.GetVirtualDAAScore() + err = syncer.ResolveVirtual(func(virtualDAAScoreStart uint64, virtualDAAScore uint64) { + if estimatedVirtualDAAScoreTarget-virtualDAAScoreStart <= 0 { + percents = 100 + } else { + percents = int(float64(virtualDAAScore-virtualDAAScoreStart) / float64(estimatedVirtualDAAScoreTarget-virtualDAAScoreStart) * 100) + } + log.Infof("Resolving virtual. Estimated progress: %d%%", percents) + }) if err != nil { return err } - percents = 0 - for i := 0; ; i++ { - if i%10 == 0 { - virtualDAAScore, err := syncee.GetVirtualDAAScore() - if err != nil { - return err - } - newPercents := int(float64(virtualDAAScore-virtualDAAScoreStart) / float64(estimatedVirtualDAAScoreTarget-virtualDAAScoreStart) * 100) - if newPercents > percents { - percents = newPercents - log.Infof("Resolving virtual. Estimated progress: %d%%", percents) - } - } - isCompletelyResolved, err := syncee.ResolveVirtual() - if err != nil { - return err - } - - if isCompletelyResolved { - log.Infof("Resolved virtual") - break - } - } + log.Infof("Resolved virtual") return nil } diff --git a/infrastructure/config/config.go b/infrastructure/config/config.go index bba91219b..f5e280e3c 100644 --- a/infrastructure/config/config.go +++ b/infrastructure/config/config.go @@ -98,6 +98,7 @@ type Flags struct { RPCMaxWebsockets int `long:"rpcmaxwebsockets" description:"Max number of RPC websocket connections"` RPCMaxConcurrentReqs int `long:"rpcmaxconcurrentreqs" description:"Max number of concurrent RPC requests that may be processed concurrently"` DisableRPC bool `long:"norpc" description:"Disable built-in RPC server"` + SafeRPC bool `long:"saferpc" description:"Disable RPC commands which affect the state of the node"` DisableDNSSeed bool `long:"nodnsseed" description:"Disable DNS seeding for peers"` DNSSeed string `long:"dnsseed" description:"Override DNS seeds with specified hostname (Only 1 hostname allowed)"` GRPCSeed string `long:"grpcseed" description:"Hostname of gRPC server for seeding peers"` diff --git a/version/version.go b/version/version.go index 1df34d5d6..61ee5cc3a 100644 --- a/version/version.go +++ b/version/version.go @@ -11,7 +11,7 @@ const validCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs const ( appMajor uint = 0 appMinor uint = 12 - appPatch uint = 2 + appPatch uint = 4 ) // appBuild is defined as a variable so it can be overridden during the build