This modifies the utxoset in the database and related UtxoViewpoint to
store and work with unspent transaction outputs on a per-output basis
instead of at a transaction level.
The primary motivation is to simplify the code, pave the way for a
utxo cache, and generally focus on optimizing runtime performance.
The tradeoff is that this approach does somewhat increase the size of
the serialized utxoset since it means that the transaction hash is
duplicated for each output as a part of the key and some additional
details are duplicated in each output. The details duplicated in each
output include flags encoded into a single byte that specify whether the
containing transaction is a coinbase, whether the containing transaction
has an expiry, and the transaction type. Additionally, the containing
block height and index are stored in each output.
However, in practice, the size difference isn't all that large, disk
space is relatively cheap, certainly cheaper than memory, and it is much
more important to provide more efficient runtime operation since that is
the ultimate purpose of the daemon.
While performing this conversion, it also simplifies the code to remove
the transaction version information from the utxoset as well as the
spend journal. The logic for only serializing it under certain
circumstances is complicated, and it was only used for the gettxout RPC,
where it has already been removed.
The utxo set and spend journal in the database are automatically
migrated to the new format with this commit and it is possible to
interrupt and resume the migration process.
Finally, it also updates all references and tests that previously dealt
with transaction hashes to use outpoints instead.
An overview of the changes are as follows:
- Remove transaction version from both spent and unspent output entries
- Update utxo serialization format to exclude the version
- Update spend journal serialization format to exclude the version
- Convert UtxoEntry to represent a specific utxo instead of a
transaction with all remaining utxos
- Optimize for memory usage with an eye toward a utxo cache
- Combine fields such as whether the containing transaction is a
coinbase, whether the containing transaction has an expiry, and
the transaction type into a single byte
- Align entry fields to eliminate extra padding since ultimately
there will be a lot of these in memory
- Introduce a free list for serializing an outpoint to the database
key format to significantly reduce pressure on the GC
- Update entries to be keyed by a <hash><tree><output index> outpoint
rather than just a tx hash
- Update all related functions that previously dealt with transaction
hashes to accept outpoints instead
- Update all callers accordingly
- Only add individually requested outputs from the mempool when
constructing a mempool view
- Modify the spend journal to always store the encoded flags with every
spent txout
- Combine fields such as whether the containing transaction is a
coinbase, whether the containing transaction has an expiry, and the
transaction type into a single byte
- Use 4 bits instead of 3 for the transaction type to be consistent
with utxos. The extra bit was already unused so this doesn't take
any additional space
- Remove the fully spent flag
- Introduce ticketMinOuts in place of stakeExtra
- Renamed stakeExtra as ticketMinOuts and updated all comments to make
the purpose of the field clearer
- Only store ticketMinOuts for ticket submission outputs
- Add TicketMinimalOutputs function on UtxoEntry in place of
ConvertUtxosToMinimalOutputs
- Always decompress data loaded from the database now that a utxo entry
only consists of a specific output
- Introduce upgrade code to migrate the utxo set and spend journal to
the new format
- Update current database version to 9
- Update current utxo set version to 3
- Update current spend journal version to 3
- Introduce the ability to run upgrades after the block index has been
loaded
- Update all tests to expect the correct encodings, remove tests that no
longer apply, and add new ones for the new expected behavior
- Convert old tests for the legacy utxo format deserialization code to
test the new function that is used during upgrade
- Introduce a few new functions on UtxoViewpoint
- AddTxOut for adding an individual txout versus all of them
- addTxOut to handle the common code between the new AddTxOut and
existing AddTxOuts
- RemoveEntry for removing an individual txout
- Remove the ErrDiscordantTxTree error
- Since utxos are now retrieved using an outpoint, which includes the
tree, it is no longer possible to hit this error path
218 lines
9.1 KiB
Go
218 lines
9.1 KiB
Go
// Copyright (c) 2017-2020 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package blockchain
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/decred/dcrd/blockchain/stake/v4"
|
|
"github.com/decred/dcrd/blockchain/standalone/v2"
|
|
"github.com/decred/dcrd/dcrutil/v4"
|
|
"github.com/decred/dcrd/wire"
|
|
)
|
|
|
|
// SequenceLock represents the minimum timestamp and minimum block height after
|
|
// which a transaction can be included into a block while satisfying the
|
|
// relative lock times of all of its input sequence numbers. It is calculated
|
|
// via the CalcSequenceLock function. Each field may be -1 if none of the input
|
|
// sequence numbers require a specific relative lock time for the respective
|
|
// type. Since all valid heights and times are larger than -1, this implies
|
|
// that it will not prevent a transaction from being included due to the
|
|
// sequence lock, which is the desired behavior.
|
|
type SequenceLock struct {
|
|
MinHeight int64
|
|
MinTime int64
|
|
}
|
|
|
|
// isStakeBaseTx determines whether or not a transaction is a stakebase (also
|
|
// known as a vote). A stakebase is a special transaction created by the
|
|
// proof-of-stake system that creates subsidy. This function simply delegates
|
|
// to the IsSSGen function in the stake package and exists to make calling code
|
|
// that does not care about the specific reason the transaction is not a
|
|
// stakebase, rather only if it is one or not.
|
|
func isStakeBaseTx(tx *wire.MsgTx, isTreasuryEnabled bool) bool {
|
|
return stake.IsSSGen(tx, isTreasuryEnabled)
|
|
}
|
|
|
|
// calcSequenceLock computes the relative lock times for the passed transaction
|
|
// from the point of view of the block node passed in as the first argument.
|
|
//
|
|
// See the CalcSequenceLock comments for more details.
|
|
func (b *BlockChain) calcSequenceLock(node *blockNode, tx *dcrutil.Tx, view *UtxoViewpoint, isActive bool) (*SequenceLock, error) {
|
|
isTreasuryEnabled, err := b.isTreasuryAgendaActive(node.parent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// A value of -1 for each lock type allows a transaction to be included
|
|
// in a block at any given height or time.
|
|
sequenceLock := &SequenceLock{MinHeight: -1, MinTime: -1}
|
|
|
|
// Sequence locks do not apply if they are not yet active, the tx
|
|
// version is less than 2, or the tx is a coinbase or stakebase, so
|
|
// return now with a sequence lock that indicates the tx can possibly be
|
|
// included in a block at any given height or time.
|
|
msgTx := tx.MsgTx()
|
|
enforce := isActive && msgTx.Version >= 2
|
|
if !enforce || standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) ||
|
|
isStakeBaseTx(msgTx, isTreasuryEnabled) {
|
|
return sequenceLock, nil
|
|
}
|
|
|
|
for txInIndex, txIn := range msgTx.TxIn {
|
|
// Nothing to calculate for this input when relative time locks
|
|
// are disabled for it.
|
|
sequenceNum := txIn.Sequence
|
|
if sequenceNum&wire.SequenceLockTimeDisabled != 0 {
|
|
continue
|
|
}
|
|
|
|
utxo := view.LookupEntry(txIn.PreviousOutPoint)
|
|
if utxo == nil {
|
|
str := fmt.Sprintf("output %v referenced from "+
|
|
"transaction %s:%d either does not exist or "+
|
|
"has already been spent", txIn.PreviousOutPoint,
|
|
tx.Hash(), txInIndex)
|
|
return sequenceLock, ruleError(ErrMissingTxOut, str)
|
|
}
|
|
|
|
// Calculate the sequence locks from the point of view of the
|
|
// next block for inputs that are in the mempool.
|
|
inputHeight := utxo.BlockHeight()
|
|
if inputHeight == 0x7fffffff {
|
|
inputHeight = node.height + 1
|
|
}
|
|
|
|
// Mask off the value portion of the sequence number to obtain
|
|
// the time lock delta required before this input can be spent.
|
|
// The relative lock can be time based or block based.
|
|
relativeLock := int64(sequenceNum & wire.SequenceLockTimeMask)
|
|
|
|
if sequenceNum&wire.SequenceLockTimeIsSeconds != 0 {
|
|
// This input requires a time based relative lock
|
|
// expressed in seconds before it can be spent and time
|
|
// based locks are calculated relative to the earliest
|
|
// possible time the block that contains the referenced
|
|
// output could have been. That time is the past
|
|
// median time of the block before it (technically one
|
|
// second after that, but that complexity is ignored for
|
|
// time based locks which already have a granularity
|
|
// associated with them anyways). Therefore, the block
|
|
// prior to the one in which the referenced output was
|
|
// included is needed to compute its past median time.
|
|
prevInputHeight := inputHeight - 1
|
|
if prevInputHeight < 0 {
|
|
prevInputHeight = 0
|
|
}
|
|
blockNode := node.Ancestor(prevInputHeight)
|
|
medianTime := blockNode.CalcPastMedianTime()
|
|
|
|
// Calculate the minimum required timestamp based on the
|
|
// sum of the aforementioned past median time and
|
|
// required relative number of seconds. Since time
|
|
// based relative locks have a granularity associated
|
|
// with them, shift left accordingly in order to convert
|
|
// to the proper number of relative seconds. Also,
|
|
// subtract one from the relative lock to maintain the
|
|
// original lock time semantics.
|
|
relativeSecs := relativeLock << wire.SequenceLockTimeGranularity
|
|
minTime := medianTime.Unix() + relativeSecs - 1
|
|
if minTime > sequenceLock.MinTime {
|
|
sequenceLock.MinTime = minTime
|
|
}
|
|
} else {
|
|
// This input requires a relative lock expressed in
|
|
// blocks before it can be spent. Therefore, calculate
|
|
// the minimum required height based on the sum of the
|
|
// input height and required relative number of blocks.
|
|
// Also, subtract one from the relative lock in order to
|
|
// maintain the original lock time semantics.
|
|
minHeight := inputHeight + relativeLock - 1
|
|
if minHeight > sequenceLock.MinHeight {
|
|
sequenceLock.MinHeight = minHeight
|
|
}
|
|
}
|
|
}
|
|
|
|
return sequenceLock, nil
|
|
}
|
|
|
|
// CalcSequenceLock computes the minimum block height and time after which the
|
|
// passed transaction can be included into a block while satisfying the relative
|
|
// lock times of all of its input sequence numbers. The passed view is used to
|
|
// obtain the past median time and block heights of the blocks in which the
|
|
// referenced outputs of the inputs to the transaction were included. The
|
|
// generated sequence lock can be used in conjunction with a block height and
|
|
// median time to determine if all inputs to the transaction have reached the
|
|
// required maturity allowing it to be included in a block.
|
|
//
|
|
// NOTE: This will calculate the sequence locks regardless of the state of the
|
|
// agenda which conditionally activates it. This is acceptable for standard
|
|
// transactions, however, callers which are intending to perform any type of
|
|
// consensus checking must check the status of the agenda first.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (b *BlockChain) CalcSequenceLock(tx *dcrutil.Tx, view *UtxoViewpoint) (*SequenceLock, error) {
|
|
b.chainLock.Lock()
|
|
seqLock, err := b.calcSequenceLock(b.bestChain.Tip(), tx, view, true)
|
|
b.chainLock.Unlock()
|
|
return seqLock, err
|
|
}
|
|
|
|
// LockTimeToSequence converts the passed relative lock time to a sequence
|
|
// number in accordance with DCP0003.
|
|
//
|
|
// A sequence number is defined as follows:
|
|
//
|
|
// - bit 31 is the disable bit
|
|
// - the next 8 bits are reserved
|
|
// - bit 22 is the relative lock type (unset = block height, set = seconds)
|
|
// - the next 6 bites are reserved
|
|
// - the least significant 16 bits represent the value
|
|
// - value has a granularity of 512 when interpreted as seconds (bit 22 set)
|
|
//
|
|
// ---------------------------------------------------
|
|
// | Disable | Reserved | Type | Reserved | Value |
|
|
// ---------------------------------------------------
|
|
// | 1 bit | 8 bits | 1 bit | 6 bits | 16 bits |
|
|
// ---------------------------------------------------
|
|
// | [31] | [30-23] | [22] | [21-16] | [15-0] |
|
|
// ---------------------------------------------------
|
|
//
|
|
// The above implies that the maximum relative block height that can be encoded
|
|
// is 65535 and the maximum relative number of seconds that can be encoded is
|
|
// 65535*512 = 33,553,920 seconds (~1.06 years). It also means that seconds are
|
|
// truncated to the nearest granularity towards 0 (e.g. 536 seconds will end up
|
|
// round tripping as 512 seconds and 1500 seconds will end up round tripping as
|
|
// 1024 seconds).
|
|
//
|
|
// An error will be returned for values that are larger than can be represented.
|
|
func LockTimeToSequence(isSeconds bool, lockTime uint32) (uint32, error) {
|
|
// The corresponding sequence number is simply the desired input age
|
|
// when expressing the relative lock time in blocks.
|
|
if !isSeconds {
|
|
if lockTime > wire.SequenceLockTimeMask {
|
|
return 0, fmt.Errorf("max relative block height a "+
|
|
"sequence number can represent is %d",
|
|
wire.SequenceLockTimeMask)
|
|
}
|
|
return lockTime, nil
|
|
}
|
|
|
|
maxSeconds := uint32(wire.SequenceLockTimeMask <<
|
|
wire.SequenceLockTimeGranularity)
|
|
if lockTime > maxSeconds {
|
|
return 0, fmt.Errorf("max relative seconds a sequence number "+
|
|
"can represent is %d", maxSeconds)
|
|
}
|
|
|
|
// Set the 22nd bit which indicates the lock time is in seconds, then
|
|
// shift the lock time over by 9 since the time granularity is in
|
|
// 512-second intervals (2^9). This results in a max lock time of
|
|
// 33,553,920 seconds (~1.06 years).
|
|
return wire.SequenceLockTimeIsSeconds |
|
|
lockTime>>wire.SequenceLockTimeGranularity, nil
|
|
}
|