mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-05-21 06:16:45 +00:00
467 lines
16 KiB
Go
467 lines
16 KiB
Go
package blockrelay
|
|
|
|
import (
|
|
"github.com/kaspanet/kaspad/app/appmessage"
|
|
"github.com/kaspanet/kaspad/app/protocol/common"
|
|
"github.com/kaspanet/kaspad/app/protocol/flowcontext"
|
|
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"
|
|
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
|
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/hashset"
|
|
"github.com/kaspanet/kaspad/infrastructure/config"
|
|
"github.com/kaspanet/kaspad/infrastructure/network/netadapter/router"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// orphanResolutionRange is the maximum amount of blockLocator hashes
|
|
// to search for known blocks. See isBlockInOrphanResolutionRange for
|
|
// further details
|
|
var orphanResolutionRange uint32 = 5
|
|
|
|
// RelayInvsContext is the interface for the context needed for the HandleRelayInvs flow.
|
|
type RelayInvsContext interface {
|
|
Domain() domain.Domain
|
|
Config() *config.Config
|
|
OnNewBlock(block *externalapi.DomainBlock) error
|
|
OnNewBlockTemplate() error
|
|
OnPruningPointUTXOSetOverride() error
|
|
SharedRequestedBlocks() *flowcontext.SharedRequestedBlocks
|
|
Broadcast(message appmessage.Message) error
|
|
AddOrphan(orphanBlock *externalapi.DomainBlock)
|
|
GetOrphanRoots(orphanHash *externalapi.DomainHash) ([]*externalapi.DomainHash, bool, error)
|
|
IsOrphan(blockHash *externalapi.DomainHash) bool
|
|
IsIBDRunning() bool
|
|
IsRecoverableError(err error) bool
|
|
IsNearlySynced() (bool, error)
|
|
}
|
|
|
|
type invRelayBlock struct {
|
|
Hash *externalapi.DomainHash
|
|
IsOrphanRoot bool
|
|
}
|
|
|
|
type handleRelayInvsFlow struct {
|
|
RelayInvsContext
|
|
incomingRoute, outgoingRoute *router.Route
|
|
peer *peerpkg.Peer
|
|
invsQueue []invRelayBlock
|
|
}
|
|
|
|
// HandleRelayInvs listens to appmessage.MsgInvRelayBlock messages, requests their corresponding blocks if they
|
|
// are missing, adds them to the DAG and propagates them to the rest of the network.
|
|
func HandleRelayInvs(context RelayInvsContext, incomingRoute *router.Route, outgoingRoute *router.Route,
|
|
peer *peerpkg.Peer) error {
|
|
|
|
flow := &handleRelayInvsFlow{
|
|
RelayInvsContext: context,
|
|
incomingRoute: incomingRoute,
|
|
outgoingRoute: outgoingRoute,
|
|
peer: peer,
|
|
invsQueue: make([]invRelayBlock, 0),
|
|
}
|
|
err := flow.start()
|
|
// Currently, HandleRelayInvs flow is the only place where IBD is triggered, so the channel can be closed now
|
|
close(peer.IBDRequestChannel())
|
|
return err
|
|
}
|
|
|
|
func (flow *handleRelayInvsFlow) start() error {
|
|
for {
|
|
log.Debugf("Waiting for inv")
|
|
inv, err := flow.readInv()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debugf("Got relay inv for block %s", inv.Hash)
|
|
|
|
blockInfo, err := flow.Domain().Consensus().GetBlockInfo(inv.Hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if blockInfo.Exists && blockInfo.BlockStatus != externalapi.StatusHeaderOnly {
|
|
if blockInfo.BlockStatus == externalapi.StatusInvalid {
|
|
return protocolerrors.Errorf(true, "sent inv of an invalid block %s",
|
|
inv.Hash)
|
|
}
|
|
log.Debugf("Block %s already exists. continuing...", inv.Hash)
|
|
continue
|
|
}
|
|
|
|
isGenesisVirtualSelectedParent, err := flow.isGenesisVirtualSelectedParent()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if flow.IsOrphan(inv.Hash) {
|
|
if flow.Config().NetParams().DisallowDirectBlocksOnTopOfGenesis && !flow.Config().AllowSubmitBlockWhenNotSynced && isGenesisVirtualSelectedParent {
|
|
log.Infof("Cannot process orphan %s for a node with only the genesis block. The node needs to IBD "+
|
|
"to the recent pruning point before normal operation can resume.", inv.Hash)
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Block %s is a known orphan. Requesting its missing ancestors", inv.Hash)
|
|
err := flow.AddOrphanRootsToQueue(inv.Hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Block relay is disabled if the node is already during IBD AND considered out of sync
|
|
if flow.IsIBDRunning() {
|
|
isNearlySynced, err := flow.IsNearlySynced()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isNearlySynced {
|
|
log.Debugf("Got block %s while in IBD and the node is out of sync. Continuing...", inv.Hash)
|
|
continue
|
|
}
|
|
}
|
|
|
|
log.Debugf("Requesting block %s", inv.Hash)
|
|
block, exists, err := flow.requestBlock(inv.Hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
log.Debugf("Aborting requesting block %s because it already exists", inv.Hash)
|
|
continue
|
|
}
|
|
|
|
err = flow.banIfBlockIsHeaderOnly(block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if flow.Config().NetParams().DisallowDirectBlocksOnTopOfGenesis && !flow.Config().AllowSubmitBlockWhenNotSynced && !flow.Config().Devnet && flow.isChildOfGenesis(block) {
|
|
log.Infof("Cannot process %s because it's a direct child of genesis.", consensushashing.BlockHash(block))
|
|
continue
|
|
}
|
|
|
|
// Note we do not apply the heuristic below if inv was queued as an orphan root, since
|
|
// that means the process started by a proper and relevant relay block
|
|
if !inv.IsOrphanRoot {
|
|
// Check bounded merge depth to avoid requesting irrelevant data which cannot be merged under virtual
|
|
virtualMergeDepthRoot, err := flow.Domain().Consensus().VirtualMergeDepthRoot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !virtualMergeDepthRoot.Equal(model.VirtualGenesisBlockHash) {
|
|
mergeDepthRootHeader, err := flow.Domain().Consensus().GetBlockHeader(virtualMergeDepthRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Since `BlueWork` respects topology, this condition means that the relay
|
|
// block is not in the future of virtual's merge depth root, and thus cannot be merged unless
|
|
// other valid blocks Kosherize it, in which case it will be obtained once the merger is relayed
|
|
if block.Header.BlueWork().Cmp(mergeDepthRootHeader.BlueWork()) <= 0 {
|
|
log.Debugf("Block %s has lower blue work than virtual's merge root %s (%d <= %d), hence we are skipping it",
|
|
inv.Hash, virtualMergeDepthRoot, block.Header.BlueWork(), mergeDepthRootHeader.BlueWork())
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Debugf("Processing block %s", inv.Hash)
|
|
oldVirtualInfo, err := flow.Domain().Consensus().GetVirtualInfo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
missingParents, err := flow.processBlock(block)
|
|
if err != nil {
|
|
if errors.Is(err, ruleerrors.ErrPrunedBlock) {
|
|
log.Infof("Ignoring pruned block %s", inv.Hash)
|
|
continue
|
|
}
|
|
|
|
if errors.Is(err, ruleerrors.ErrDuplicateBlock) {
|
|
log.Infof("Ignoring duplicate block %s", inv.Hash)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
if len(missingParents) > 0 {
|
|
log.Debugf("Block %s is orphan and has missing parents: %s", inv.Hash, missingParents)
|
|
err := flow.processOrphan(block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
oldVirtualParents := hashset.New()
|
|
for _, parent := range oldVirtualInfo.ParentHashes {
|
|
oldVirtualParents.Add(parent)
|
|
}
|
|
|
|
newVirtualInfo, err := flow.Domain().Consensus().GetVirtualInfo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
virtualHasNewParents := false
|
|
for _, parent := range newVirtualInfo.ParentHashes {
|
|
if oldVirtualParents.Contains(parent) {
|
|
continue
|
|
}
|
|
virtualHasNewParents = true
|
|
block, found, err := flow.Domain().Consensus().GetBlock(parent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !found {
|
|
return protocolerrors.Errorf(false, "Virtual parent %s not found", parent)
|
|
}
|
|
blockHash := consensushashing.BlockHash(block)
|
|
log.Debugf("Relaying block %s", blockHash)
|
|
err = flow.relayBlock(block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if virtualHasNewParents {
|
|
log.Debugf("Virtual %d has new parents, raising new block template event", newVirtualInfo.DAAScore)
|
|
err = flow.OnNewBlockTemplate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
log.Infof("Accepted block %s via relay", inv.Hash)
|
|
err = flow.OnNewBlock(block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (flow *handleRelayInvsFlow) banIfBlockIsHeaderOnly(block *externalapi.DomainBlock) error {
|
|
if len(block.Transactions) == 0 {
|
|
return protocolerrors.Errorf(true, "sent header of %s block where expected block with body",
|
|
consensushashing.BlockHash(block))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (flow *handleRelayInvsFlow) readInv() (invRelayBlock, error) {
|
|
if len(flow.invsQueue) > 0 {
|
|
var inv invRelayBlock
|
|
inv, flow.invsQueue = flow.invsQueue[0], flow.invsQueue[1:]
|
|
return inv, nil
|
|
}
|
|
|
|
msg, err := flow.incomingRoute.Dequeue()
|
|
if err != nil {
|
|
return invRelayBlock{}, err
|
|
}
|
|
|
|
msgInv, ok := msg.(*appmessage.MsgInvRelayBlock)
|
|
if !ok {
|
|
return invRelayBlock{}, protocolerrors.Errorf(true, "unexpected %s message in the block relay handleRelayInvsFlow while "+
|
|
"expecting an inv message", msg.Command())
|
|
}
|
|
return invRelayBlock{Hash: msgInv.Hash, IsOrphanRoot: false}, nil
|
|
}
|
|
|
|
func (flow *handleRelayInvsFlow) requestBlock(requestHash *externalapi.DomainHash) (*externalapi.DomainBlock, bool, error) {
|
|
exists := flow.SharedRequestedBlocks().AddIfNotExists(requestHash)
|
|
if exists {
|
|
return nil, true, nil
|
|
}
|
|
|
|
// In case the function returns earlier than expected, we want to make sure flow.SharedRequestedBlocks() is
|
|
// clean from any pending blocks.
|
|
defer flow.SharedRequestedBlocks().Remove(requestHash)
|
|
|
|
getRelayBlocksMsg := appmessage.NewMsgRequestRelayBlocks([]*externalapi.DomainHash{requestHash})
|
|
err := flow.outgoingRoute.Enqueue(getRelayBlocksMsg)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
msgBlock, err := flow.readMsgBlock()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
block := appmessage.MsgBlockToDomainBlock(msgBlock)
|
|
blockHash := consensushashing.BlockHash(block)
|
|
if !blockHash.Equal(requestHash) {
|
|
return nil, false, protocolerrors.Errorf(true, "got unrequested block %s", blockHash)
|
|
}
|
|
|
|
return block, false, nil
|
|
}
|
|
|
|
// readMsgBlock returns the next msgBlock in msgChan, and populates invsQueue with any inv messages that meanwhile arrive.
|
|
//
|
|
// Note: this function assumes msgChan can contain only appmessage.MsgInvRelayBlock and appmessage.MsgBlock messages.
|
|
func (flow *handleRelayInvsFlow) readMsgBlock() (msgBlock *appmessage.MsgBlock, err error) {
|
|
for {
|
|
message, err := flow.incomingRoute.DequeueWithTimeout(common.DefaultTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch message := message.(type) {
|
|
case *appmessage.MsgInvRelayBlock:
|
|
flow.invsQueue = append(flow.invsQueue, invRelayBlock{Hash: message.Hash, IsOrphanRoot: false})
|
|
case *appmessage.MsgBlock:
|
|
return message, nil
|
|
default:
|
|
return nil, errors.Errorf("unexpected message %s", message.Command())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (flow *handleRelayInvsFlow) processBlock(block *externalapi.DomainBlock) ([]*externalapi.DomainHash, error) {
|
|
blockHash := consensushashing.BlockHash(block)
|
|
err := flow.Domain().Consensus().ValidateAndInsertBlock(block, true)
|
|
if err != nil {
|
|
if !errors.As(err, &ruleerrors.RuleError{}) {
|
|
return nil, errors.Wrapf(err, "failed to process block %s", blockHash)
|
|
}
|
|
|
|
missingParentsError := &ruleerrors.ErrMissingParents{}
|
|
if errors.As(err, missingParentsError) {
|
|
return missingParentsError.MissingParentHashes, nil
|
|
}
|
|
// A duplicate block should not appear to the user as a warning and is already reported in the calling function
|
|
if !errors.Is(err, ruleerrors.ErrDuplicateBlock) {
|
|
log.Warnf("Rejected block %s from %s: %s", blockHash, flow.peer, err)
|
|
}
|
|
return nil, protocolerrors.Wrapf(true, err, "got invalid block %s from relay", blockHash)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (flow *handleRelayInvsFlow) relayBlock(block *externalapi.DomainBlock) error {
|
|
blockHash := consensushashing.BlockHash(block)
|
|
return flow.Broadcast(appmessage.NewMsgInvBlock(blockHash))
|
|
}
|
|
|
|
func (flow *handleRelayInvsFlow) processOrphan(block *externalapi.DomainBlock) error {
|
|
blockHash := consensushashing.BlockHash(block)
|
|
|
|
// Return if the block has been orphaned from elsewhere already
|
|
if flow.IsOrphan(blockHash) {
|
|
log.Debugf("Skipping orphan processing for block %s because it is already an orphan", blockHash)
|
|
return nil
|
|
}
|
|
|
|
// Add the block to the orphan set if it's within orphan resolution range
|
|
isBlockInOrphanResolutionRange, err := flow.isBlockInOrphanResolutionRange(blockHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isBlockInOrphanResolutionRange {
|
|
if flow.Config().NetParams().DisallowDirectBlocksOnTopOfGenesis && !flow.Config().AllowSubmitBlockWhenNotSynced {
|
|
isGenesisVirtualSelectedParent, err := flow.isGenesisVirtualSelectedParent()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if isGenesisVirtualSelectedParent {
|
|
log.Infof("Cannot process orphan %s for a node with only the genesis block. The node needs to IBD "+
|
|
"to the recent pruning point before normal operation can resume.", blockHash)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
log.Debugf("Block %s is within orphan resolution range. "+
|
|
"Adding it to the orphan set", blockHash)
|
|
flow.AddOrphan(block)
|
|
log.Debugf("Requesting block %s missing ancestors", blockHash)
|
|
return flow.AddOrphanRootsToQueue(blockHash)
|
|
}
|
|
|
|
// Start IBD unless we already are in IBD
|
|
log.Debugf("Block %s is out of orphan resolution range. "+
|
|
"Attempting to start IBD against it.", blockHash)
|
|
|
|
// Send the block to IBD flow via the IBDRequestChannel.
|
|
// Note that this is a non-blocking send, since if IBD is already running, there is no need to trigger it
|
|
select {
|
|
case flow.peer.IBDRequestChannel() <- block:
|
|
default:
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (flow *handleRelayInvsFlow) isGenesisVirtualSelectedParent() (bool, error) {
|
|
virtualSelectedParent, err := flow.Domain().Consensus().GetVirtualSelectedParent()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return virtualSelectedParent.Equal(flow.Config().NetParams().GenesisHash), nil
|
|
}
|
|
|
|
func (flow *handleRelayInvsFlow) isChildOfGenesis(block *externalapi.DomainBlock) bool {
|
|
parents := block.Header.DirectParents()
|
|
return len(parents) == 1 && parents[0].Equal(flow.Config().NetParams().GenesisHash)
|
|
}
|
|
|
|
// isBlockInOrphanResolutionRange finds out whether the given blockHash should be
|
|
// retrieved via the unorphaning mechanism or via IBD. This method sends a
|
|
// getBlockLocator request to the peer with a limit of orphanResolutionRange.
|
|
// In the response, if we know none of the hashes, we should retrieve the given
|
|
// blockHash via IBD. Otherwise, via unorphaning.
|
|
func (flow *handleRelayInvsFlow) isBlockInOrphanResolutionRange(blockHash *externalapi.DomainHash) (bool, error) {
|
|
err := flow.sendGetBlockLocator(blockHash, orphanResolutionRange)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
blockLocatorHashes, err := flow.receiveBlockLocator()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, blockLocatorHash := range blockLocatorHashes {
|
|
blockInfo, err := flow.Domain().Consensus().GetBlockInfo(blockLocatorHash)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if blockInfo.Exists && blockInfo.BlockStatus != externalapi.StatusHeaderOnly {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (flow *handleRelayInvsFlow) AddOrphanRootsToQueue(orphan *externalapi.DomainHash) error {
|
|
orphanRoots, orphanExists, err := flow.GetOrphanRoots(orphan)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !orphanExists {
|
|
log.Infof("Orphan block %s was missing from the orphan pool while requesting for its roots. This "+
|
|
"probably happened because it was randomly evicted immediately after it was added.", orphan)
|
|
}
|
|
|
|
if len(orphanRoots) == 0 {
|
|
// In some rare cases we get here when there are no orphan roots already
|
|
return nil
|
|
}
|
|
log.Infof("Block %s has %d missing ancestors. Adding them to the invs queue...", orphan, len(orphanRoots))
|
|
|
|
invMessages := make([]invRelayBlock, len(orphanRoots))
|
|
for i, root := range orphanRoots {
|
|
log.Debugf("Adding block %s missing ancestor %s to the invs queue", orphan, root)
|
|
invMessages[i] = invRelayBlock{Hash: root, IsOrphanRoot: true}
|
|
}
|
|
|
|
flow.invsQueue = append(invMessages, flow.invsQueue...)
|
|
return nil
|
|
}
|