chaingen: Add revocation generation infrastructure.

This adds infrastructure to track missed votes and automatically
generate revocations for them along with a new assertion helper named
AssertTipNumRevocations which, as it name suggests, allows the caller to
assert the header commits to the specified number of revocations.

This will be useful for porting the rest of the block validation tests
to the fullblocktests framework.
This commit is contained in:
Dave Collins 2018-03-04 21:32:31 -06:00
parent 33ee347667
commit a0bdb17891
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2

View File

@ -147,12 +147,13 @@ type Generator struct {
spendableOuts [][]SpendableOut
prevCollectedHash chainhash.Hash
// Used for tracking the live ticket pool.
// Used for tracking the live ticket pool and revocations.
originalParents map[chainhash.Hash]chainhash.Hash
immatureTickets []*stakeTicket
liveTickets []*stakeTicket
wonTickets map[chainhash.Hash][]*stakeTicket
expiredTickets []*stakeTicket
missedVotes map[chainhash.Hash][]*stakeTicket
}
// MakeGenerator returns a generator instance initialized with the genesis block
@ -183,6 +184,7 @@ func MakeGenerator(params *chaincfg.Params) (Generator, error) {
p2shOpTrueScript: p2shOpTrueScript,
originalParents: make(map[chainhash.Hash]chainhash.Hash),
wonTickets: make(map[chainhash.Hash][]*stakeTicket),
missedVotes: make(map[chainhash.Hash][]*stakeTicket),
}, nil
}
@ -653,6 +655,40 @@ func (g *Generator) createVoteTx(parentBlock *wire.MsgBlock, ticket *stakeTicket
return tx
}
// createRevocationTx returns a new transaction (ssrtx) refunding the ticket
// price for a ticket which either missed its vote or expired.
//
// The transaction consists of the following inputs:
// - The outpoint of the ticket that was missed or expired.
//
// The transaction consists of the following outputs:
// - The payouts according to the ticket commitments.
func (g *Generator) createRevocationTx(ticket *stakeTicket) *wire.MsgTx {
// The outputs pay the original commitment amounts. This impl uses the
// standard pay-to-script-hash to an OP_TRUE.
revokeScript, err := txscript.PayToSSRtx(g.p2shOpTrueAddr)
if err != nil {
panic(err)
}
// Generate and return the transaction spending from the provided ticket
// along with the previously described outputs.
ticketPrice := ticket.tx.TxOut[0].Value
ticketHash := ticket.tx.CachedTxHash()
tx := wire.NewMsgTx()
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: *wire.NewOutPoint(ticketHash, 0,
wire.TxTreeStake),
Sequence: wire.MaxTxInSequenceNum,
ValueIn: ticketPrice,
BlockHeight: ticket.blockHeight,
BlockIndex: ticket.blockIndex,
SignatureScript: opTrueRedeemScript,
})
tx.AddTxOut(wire.NewTxOut(ticketPrice, revokeScript))
return tx
}
// ancestorBlock returns the ancestor block at the provided height by following
// the chain backwards from the given block. The returned block will be nil
// when a height is requested that is after the height of the passed block.
@ -1658,15 +1694,58 @@ func (g *Generator) connectLiveTickets(blockHash *chainhash.Hash, height uint32,
g.immatureTickets = append(g.immatureTickets, purchases...)
}
// addMissedVotes adds any of the passed winning tickets as missed votes if the
// passed block does not cast those votes.
func (g *Generator) addMissedVotes(blockHash *chainhash.Hash, stakeTxns []*wire.MsgTx, winners []*stakeTicket) {
// Nothing to do before there are any winning tickets.
if len(winners) == 0 {
return
}
// Assume all of the winning tickets were missed.
missedVotes := make(map[chainhash.Hash]*stakeTicket)
for _, ticket := range winners {
missedVotes[ticket.tx.TxHash()] = ticket
}
// Remove the entries for which the block actually contains votes.
for _, stx := range stakeTxns {
// Ignore all stake transactions that are not votes.
if !isVoteTx(stx) {
continue
}
// Ignore the vote if it is not for one of the winning tickets.
ticketInput := stx.TxIn[1]
ticketHash := ticketInput.PreviousOutPoint.Hash
missedVote, ok := missedVotes[ticketHash]
if !ok || missedVote.blockHeight != ticketInput.BlockHeight ||
missedVote.blockIndex != ticketInput.BlockIndex {
continue
}
delete(missedVotes, ticketHash)
}
// Add the missed votes to the generator state so future blocks will
// generate revocations for them.
for _, missedVote := range missedVotes {
g.missedVotes[*blockHash] = append(g.missedVotes[*blockHash],
missedVote)
}
}
// connectBlockTickets updates the live ticket pool and associated data structs
// by for the passed block. It will panic if the specified block does not
// connect to the current tip block.
func (g *Generator) connectBlockTickets(b *wire.MsgBlock) {
// Attempt to prevent misuse of this function by ensuring the provided
// block connects to the current tip.
blockHash := b.BlockHash()
if b.Header.PrevBlock != g.tip.BlockHash() {
panic(fmt.Sprintf("attempt to connect block %s with parent %s "+
"that is not the current tip %s", b.BlockHash(),
"that is not the current tip %s", blockHash,
b.Header.PrevBlock, g.tip.BlockHash()))
}
@ -1677,10 +1756,12 @@ func (g *Generator) connectBlockTickets(b *wire.MsgBlock) {
panic(err)
}
// Keep track of any missed votes.
g.addMissedVotes(&blockHash, b.STransactions, winners)
// Extract the ticket purchases (sstx) from the block.
var purchases []*stakeTicket
blockHash := b.BlockHash()
blockHeight := g.blockHeight(b.BlockHash())
blockHeight := g.blockHeight(blockHash)
for txIdx, tx := range b.STransactions {
if isTicketPurchaseTx(tx) {
ticket := &stakeTicket{tx, blockHeight, uint32(txIdx)}
@ -1698,14 +1779,14 @@ func (g *Generator) connectBlockTickets(b *wire.MsgBlock) {
func (g *Generator) disconnectBlockTickets(b *wire.MsgBlock) {
// Attempt to prevent misuse of this function by ensuring the provided
// block is the current tip.
blockHash := b.BlockHash()
if b != g.tip {
panic(fmt.Sprintf("attempt to disconnect block %s that is not "+
"the current tip %s", b.BlockHash(), g.tip.BlockHash()))
"the current tip %s", blockHash, g.tip.BlockHash()))
}
// Remove tickets created in the block from the immature ticket pool.
blockHash := b.BlockHash()
blockHeight := g.blockHeight(b.BlockHash())
blockHeight := g.blockHeight(blockHash)
for i := 0; i < len(g.immatureTickets); i++ {
ticket := g.immatureTickets[i]
if ticket.blockHeight == blockHeight {
@ -1759,6 +1840,9 @@ func (g *Generator) disconnectBlockTickets(b *wire.MsgBlock) {
g.liveTickets = append(g.liveTickets, g.wonTickets[blockHash]...)
delete(g.wonTickets, blockHash)
// Remove any votes missed by the block.
delete(g.missedVotes, blockHash)
// Resort the ticket pool now that all live ticket pool manipulations
// are done.
sort.Sort(stakeTicketSorter(g.liveTickets))
@ -1942,6 +2026,14 @@ func (g *Generator) NextBlock(blockName string, spend *SpendableOut, ticketSpend
stakeTxns = append(stakeTxns, purchaseTx)
}
}
// Generate and add revocations for any missed tickets.
for _, missedVotes := range g.missedVotes {
for _, missedVote := range missedVotes {
revocationTx := g.createRevocationTx(missedVote)
stakeTxns = append(stakeTxns, revocationTx)
}
}
}
// Create stake tickets for the ticket purchases (sstx), count the
@ -2096,6 +2188,7 @@ func (g *Generator) NextBlock(blockName string, spend *SpendableOut, ticketSpend
// it.
g.originalParents[blockHash] = prevHash
}
g.addMissedVotes(&blockHash, block.STransactions, ticketWinners)
g.connectLiveTickets(&blockHash, nextHeight, ticketWinners,
ticketPurchases)
g.blocks[blockHash] = &block
@ -2379,3 +2472,15 @@ func (g *Generator) AssertBlockVersion(expected int32) {
"expected %d", g.tipName, blockVersion, expected))
}
}
// AssertTipNumRevocations panics if the number of revocations in header of the
// current tip block associated with the generator does not match the specified
// value.
func (g *Generator) AssertTipNumRevocations(expected uint8) {
numRevocations := g.tip.Header.Revocations
if numRevocations != expected {
panic(fmt.Sprintf("number of revocations in block %q (height "+
"%d) is %d instead of expected %d", g.tipName,
g.tip.Header.Height, numRevocations, expected))
}
}