Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

blockchain: Validate allowed revokes in blk contxt. #1037

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 78 additions & 138 deletions blockchain/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,64 @@ func (b *BlockChain) checkBlockHeaderContext(header *wire.BlockHeader, prevNode
return nil
}

// checkAllowedVotes performs validation of all votes in the block to ensure
// they spend tickets that are actually allowed to vote per the lottery.
//
// This function is safe for concurrent access.
func (b *BlockChain) checkAllowedVotes(parentStakeNode *stake.Node, block *wire.MsgBlock) error {
// Determine the winning ticket hashes and create a map for faster lookup.
ticketsPerBlock := int(b.chainParams.TicketsPerBlock)
winningHashes := make(map[chainhash.Hash]struct{}, ticketsPerBlock)
for _, ticketHash := range parentStakeNode.Winners() {
winningHashes[ticketHash] = struct{}{}
}

for _, stx := range block.STransactions {
// Ignore non-vote stake transactions.
if !stake.IsSSGen(stx) {
continue
}

// Ensure the ticket being spent is actually eligible to vote in
// this block.
ticketHash := stx.TxIn[1].PreviousOutPoint.Hash
if _, ok := winningHashes[ticketHash]; !ok {
errStr := fmt.Sprintf("block contains vote for "+
"ineligible ticket %s (eligible tickets: %s)",
ticketHash, winningHashes)
return ruleError(ErrTicketUnavailable, errStr)
}
}

return nil
}

// checkAllowedRevocations performs validation of all revocations in the block
// to ensure they spend tickets that are actually allowed to be revoked per the
// lottery. Tickets are only eligible to be revoked if they were missed or have
// expired.
//
// This function is safe for concurrent access.
func (b *BlockChain) checkAllowedRevocations(parentStakeNode *stake.Node, block *wire.MsgBlock) error {
for _, stx := range block.STransactions {
// Ignore non-revocation stake transactions.
if !stake.IsSSRtx(stx) {
continue
}

// Ensure the ticket being spent is actually eligible to be
// revoked in this block.
ticketHash := stx.TxIn[0].PreviousOutPoint.Hash
if !parentStakeNode.ExistsMissedTicket(ticketHash) {
errStr := fmt.Sprintf("block contains revocation of "+
"ineligible ticket %s", ticketHash)
return ruleError(ErrInvalidSSRtx, errStr)
}
}

return nil
}

// checkBlockContext peforms several validation checks on the block which depend
// on its position within the block chain.
//
Expand Down Expand Up @@ -1090,6 +1148,26 @@ func (b *BlockChain) checkBlockContext(block *dcrutil.Block, prevNode *blockNode
return err
}
}

// Ensure that all votes are only for winning tickets and all
// revocations are actually eligible to be revoked once stake
// validation height has been reached.
if blockHeight >= b.chainParams.StakeValidationHeight {
parentStakeNode, err := b.fetchStakeNode(prevNode)
if err != nil {
return err
}
err = b.checkAllowedVotes(parentStakeNode, block.MsgBlock())
if err != nil {
return err
}

err = b.checkAllowedRevocations(parentStakeNode,
block.MsgBlock())
if err != nil {
return err
}
}
}

return nil
Expand Down Expand Up @@ -1137,136 +1215,6 @@ func (b *BlockChain) checkDupTxs(txSet []*dcrutil.Tx, view *UtxoViewpoint) error
return nil
}

// CheckBlockStakeSanity performs a series of checks on a block to ensure that
// the information from the block's header about stake is sane. For instance,
// the number of SSGen tx must be equal to voters.
// TODO: We can consider breaking this into two functions and making some of
// these checks go through in processBlock, however if a block has demonstrable
// PoW it seems unlikely that it will have stake errors (because the miner is
// then just wasting hash power).
func (b *BlockChain) CheckBlockStakeSanity(stakeValidationHeight int64, node *blockNode, block *dcrutil.Block, parent *dcrutil.Block, chainParams *chaincfg.Params) error {
// Setup variables.
stakeTransactions := block.STransactions()
blockHash := block.Hash()
ticketsPerBlock := int(b.chainParams.TicketsPerBlock)

parentStakeNode, err := b.fetchStakeNode(node.parent)
if err != nil {
return err
}

// Break if the stake system is otherwise disabled.
if block.Height() < stakeValidationHeight {
return nil
}

// -------------------------------------------------------------------
// SSGen Tx Handling
// -------------------------------------------------------------------
// PER SSGEN
// 1. Retrieve an emulated ticket database of SStxMemMaps from both the
// ticket database and the ticket store.
// 2. Check to ensure that the tickets included in the block are the
// ones that indeed should have been included according to the
// emulated ticket database.
// 3. Check to make sure that the SSGen votes on the correct
// block/height.

// PER BLOCK
// 4. Check and make sure that we have the same number of SSGen tx as
// we do votes.
// 5. Check for voters overflows (highly unlikely, but check anyway).
// 6. Ensure that the block votes on tx tree regular of the previous
// block in the way of the majority of the voters.

// 1. Retrieve an emulated ticket database of SStxMemMaps from both the
// ticket database and the ticket store.
ticketsWhichCouldBeUsed := make(map[chainhash.Hash]struct{},
ticketsPerBlock)
ticketSlice := parentStakeNode.Winners()

// 2. Obtain the tickets which could have been used on the block for
// votes and then check below to make sure that these were indeed
// the tickets used.
for _, ticketHash := range ticketSlice {
ticketsWhichCouldBeUsed[ticketHash] = struct{}{}
// Fetch utxo details for all of the transactions in this
// block. Typically, there will not be any utxos for any of
// the transactions.
}

for _, staketx := range stakeTransactions {
msgTx := staketx.MsgTx()
if stake.IsSSGen(msgTx) {
// Grab the input SStx hash from the inputs of the
// transaction.
sstxIn := msgTx.TxIn[1] // sstx input
sstxHash := sstxIn.PreviousOutPoint.Hash

// Check to make sure this was actually a ticket we
// were allowed to use.
_, ticketAvailable := ticketsWhichCouldBeUsed[sstxHash]
if !ticketAvailable {
errStr := fmt.Sprintf("Error in stake "+
"consensus: Ticket %v was not found "+
"to be available in the stake patch "+
"or database, yet block %v spends it!",
sstxHash, blockHash)
return ruleError(ErrTicketUnavailable, errStr)
}
}
}

// 3. Check to make sure that the SSGen votes on the correct
// block/height. Already checked in checkBlockSanity.

// 4. Check and make sure that we have the same number of SSGen tx as
// we do votes. Already checked in checkBlockSanity.

// 5. Check for too many voters. Already checked in checkBlockSanity.

// 6. Determine if TxTreeRegular should be valid or not, and then check
// it against what is provided in the block header. Already checked
// in checkBlockSanity.

// -------------------------------------------------------------------
// SSRtx Tx Handling
// -------------------------------------------------------------------
// PER SSRTX
// 1. Ensure that the SSRtx has been marked missed in the ticket patch
// data and, if not, ensure it has been marked missed in the ticket
// database.
// 2. Ensure that at least ticketMaturity many blocks has passed since
// the SStx it references was included in the blockchain.
// PER BLOCK
for _, staketx := range stakeTransactions {
msgTx := staketx.MsgTx()
if stake.IsSSRtx(msgTx) {
// Grab the input SStx hash from the inputs of the
// transaction.
sstxIn := msgTx.TxIn[0] // sstx input
sstxHash := sstxIn.PreviousOutPoint.Hash

ticketMissed := false

if parentStakeNode.ExistsMissedTicket(sstxHash) {
ticketMissed = true
}

if !ticketMissed {
errStr := fmt.Sprintf("Error in stake "+
"consensus: Ticket %v was not found "+
"to be missed in the stake patch or "+
"database, yet block %v spends it!",
sstxHash, blockHash)
return ruleError(ErrInvalidSSRtx, errStr)
}
}
}

return nil
}

// CheckTransactionInputs performs a series of checks on the inputs to a
// transaction to ensure they are valid. An example of some of the checks
// include verifying all inputs exist, ensuring the coinbase seasoning
Expand Down Expand Up @@ -2412,14 +2360,6 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block, parent *dcrutil.B
return err
}

err = b.CheckBlockStakeSanity(b.chainParams.StakeValidationHeight, node,
block, parent, b.chainParams)
if err != nil {
log.Tracef("CheckBlockStakeSanity failed for incoming node "+
"%v; error given: %v", node.hash, err)
return err
}

// Don't run scripts if this node is before the latest known good
// checkpoint since the validity is verified via the checkpoints (all
// transactions are included in the merkle root hash and any changes
Expand Down