stake: Add func to create revocation from ticket.
This adds a new function, CreateRevocationFromTicket, and associated tests for creating a revocation transaction for a given ticket.
This commit is contained in:
parent
b4861dd9f0
commit
24a7bed319
@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2014 Conformal Systems LLC.
|
||||
// Copyright (c) 2015-2020 The Decred developers
|
||||
// Copyright (c) 2015-2021 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@ -139,10 +139,13 @@ const (
|
||||
// the stake tx tree.
|
||||
ErrSSRtxWrongTxTree = ErrorKind("ErrSSRtxWrongTxTree")
|
||||
|
||||
// ErrSSRtxBadGenOuts indicates that there was a non-SSRtx tagged output
|
||||
// ErrSSRtxBadOuts indicates that there was a non-SSRtx tagged output
|
||||
// present in an SSRtx.
|
||||
ErrSSRtxBadOuts = ErrorKind("ErrSSRtxBadOuts")
|
||||
|
||||
// ErrSSRtxInvalidFee indicates that a given SSRtx contains an invalid fee.
|
||||
ErrSSRtxInvalidFee = ErrorKind("ErrSSRtxInvalidFee")
|
||||
|
||||
// ErrVerSStxAmts indicates there was an error verifying the calculated
|
||||
// SStx out amounts and the actual SStx out amounts.
|
||||
ErrVerSStxAmts = ErrorKind("ErrVerSStxAmts")
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2014 Conformal Systems LLC.
|
||||
// Copyright (c) 2015-2020 The Decred developers
|
||||
// Copyright (c) 2015-2021 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@ -49,6 +49,7 @@ func TestErrorKindStringer(t *testing.T) {
|
||||
{ErrSSRtxNoOutputs, "ErrSSRtxNoOutputs"},
|
||||
{ErrSSRtxWrongTxTree, "ErrSSRtxWrongTxTree"},
|
||||
{ErrSSRtxBadOuts, "ErrSSRtxBadOuts"},
|
||||
{ErrSSRtxInvalidFee, "ErrSSRtxInvalidFee"},
|
||||
{ErrVerSStxAmts, "ErrVerSStxAmts"},
|
||||
{ErrVerifyInput, "ErrVerifyInput"},
|
||||
{ErrVerifyOutType, "ErrVerifyOutType"},
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"math/big"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/chaincfg/v3"
|
||||
"github.com/decred/dcrd/dcrutil/v4"
|
||||
"github.com/decred/dcrd/txscript/v4"
|
||||
"github.com/decred/dcrd/txscript/v4/stdaddr"
|
||||
@ -1221,3 +1222,122 @@ func FindSpentTicketsInBlock(block *wire.MsgBlock) *SpentTicketsInBlock {
|
||||
RevokedTickets: revocations,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateRevocationFromTicket creates a revocation transaction for the provided
|
||||
// ticket. The provided fee is applied to the first output of the created
|
||||
// revocation unless it does not adhere to the fee limit imposed by the ticket,
|
||||
// in which case an error is returned.
|
||||
func CreateRevocationFromTicket(ticketHash *chainhash.Hash,
|
||||
ticketMinOuts []*MinimalOutput, revocationTxFee dcrutil.Amount,
|
||||
revocationTxVersion uint16, params *chaincfg.Params) (*wire.MsgTx, error) {
|
||||
|
||||
// Validate that the provided ticket minimal outputs have a non-zero length.
|
||||
if len(ticketMinOuts) == 0 {
|
||||
return nil, stakeRuleError(ErrSStxNoOutputs, "no minimal outputs")
|
||||
}
|
||||
|
||||
// Create the revocation transaction and add the input. The only input for a
|
||||
// revocation transaction is a ticket submission (OP_SSTX tagged) output.
|
||||
revocationTx := wire.NewMsgTx()
|
||||
const ticketSubmissionOutput = 0
|
||||
const tree = wire.TxTreeStake
|
||||
ticketSubmissionAmount := ticketMinOuts[ticketSubmissionOutput].Value
|
||||
prevOut := wire.NewOutPoint(ticketHash, ticketSubmissionOutput, tree)
|
||||
txIn := wire.NewTxIn(prevOut, ticketSubmissionAmount, nil)
|
||||
revocationTx.AddTxIn(txIn)
|
||||
|
||||
// Check to make sure that all scripts are the consensus version.
|
||||
for i, ticketMinOut := range ticketMinOuts {
|
||||
if ticketMinOut.Version != consensusVersion {
|
||||
str := fmt.Sprintf("invalid script version found in ticket minimal "+
|
||||
"outputs idx %d", i)
|
||||
return nil, stakeRuleError(ErrSStxInvalidOutputs, str)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the ticket commitment output details.
|
||||
isP2SH, payToHashes, amounts, _, hasFeeLimit, feeLimits :=
|
||||
SStxStakeOutputInfo(ticketMinOuts)
|
||||
|
||||
// In the returned ticket stake output values, vote fee limit info is in index
|
||||
// 0 and revocation fee limit info is in index 1.
|
||||
const revocationFeeLimitIndex = 1
|
||||
|
||||
// Calculate the revocation output values from this data.
|
||||
const revocationSubsidy = 0
|
||||
revocationOutputAmounts := CalculateRewards(amounts, ticketSubmissionAmount,
|
||||
revocationSubsidy)
|
||||
|
||||
// Add all the SSRtx-tagged transaction outputs to the transaction after
|
||||
// performing some validity checks.
|
||||
feeApplied := false
|
||||
for i, payToHash := range payToHashes {
|
||||
// Ensure amount is in the valid range for monetary amounts.
|
||||
if amounts[i] <= 0 || amounts[i] > dcrutil.MaxAmount {
|
||||
str := fmt.Sprintf("invalid output amount: %v (min: 0, max: %v)",
|
||||
amounts[i], dcrutil.MaxAmount)
|
||||
return nil, stakeRuleError(ErrSStxBadCommitAmount, str)
|
||||
}
|
||||
|
||||
// Create a new script which pays to the provided address specified in
|
||||
// the original ticket tx.
|
||||
var ssrtxOutScript []byte
|
||||
switch isP2SH[i] {
|
||||
case false: // P2PKH
|
||||
addr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(payToHash,
|
||||
params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, ssrtxOutScript = addr.PayRevokeCommitmentScript()
|
||||
case true: // P2SH
|
||||
addr, err := stdaddr.NewAddressScriptHashV0FromHash(payToHash, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, ssrtxOutScript = addr.PayRevokeCommitmentScript()
|
||||
}
|
||||
|
||||
// Validate that the provided fee adheres to the fee limit imposed by the
|
||||
// ticket commitment.
|
||||
hasFeeLimit := hasFeeLimit[i][revocationFeeLimitIndex]
|
||||
feeLimitLog2 := feeLimits[i][revocationFeeLimitIndex]
|
||||
if hasFeeLimit {
|
||||
// Notice that, since the fee limit is in terms of a log2 value, and
|
||||
// amounts are int64, anything greater than or equal to 63 is interpreted
|
||||
// to mean allow spending the entire amount as a fee. This allows fast
|
||||
// left shifts to be used to calculate the fee limit while preventing
|
||||
// degenerate cases such as negative numbers for 63.
|
||||
if feeLimitLog2 < 63 {
|
||||
feeLimit := int64(1 << uint64(feeLimitLog2))
|
||||
if !feeApplied && int64(revocationTxFee) > feeLimit {
|
||||
str := fmt.Sprintf("fee %v is higher than the imposed fee limit %v",
|
||||
int64(revocationTxFee), feeLimit)
|
||||
return nil, stakeRuleError(ErrSSRtxInvalidFee, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The fee must be zero when not encumbered with a fee limit.
|
||||
if !hasFeeLimit && revocationTxFee != 0 {
|
||||
str := "fee must be zero when not encumbered with a fee limit"
|
||||
return nil, stakeRuleError(ErrSSRtxInvalidFee, str)
|
||||
}
|
||||
|
||||
// Apply the fee to the output if the fee has not already been applied.
|
||||
amt := revocationOutputAmounts[i]
|
||||
if !feeApplied && int64(revocationTxFee) < amt {
|
||||
amt -= int64(revocationTxFee)
|
||||
feeApplied = true
|
||||
}
|
||||
|
||||
// Add the txout to our SSRtx tx.
|
||||
txOut := wire.NewTxOut(amt, ssrtxOutScript)
|
||||
revocationTx.AddTxOut(txOut)
|
||||
}
|
||||
|
||||
// Set the version of the revocation transaction.
|
||||
revocationTx.Version = revocationTxVersion
|
||||
|
||||
return revocationTx, nil
|
||||
}
|
||||
|
||||
@ -6,12 +6,14 @@ package stake
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/decred/dcrd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/chaincfg/v3"
|
||||
"github.com/decred/dcrd/dcrutil/v4"
|
||||
"github.com/decred/dcrd/txscript/v4"
|
||||
"github.com/decred/dcrd/txscript/v4/stdaddr"
|
||||
@ -30,6 +32,31 @@ const (
|
||||
withTreasury = true
|
||||
)
|
||||
|
||||
// hexToBytes converts the passed hex string into bytes and will panic if there
|
||||
// is an error. This is only provided for the hard-coded constants so errors in
|
||||
// the source code can be detected. It will only (and must only) be called with
|
||||
// hard-coded values.
|
||||
func hexToBytes(s string) []byte {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic("invalid hex in source file: " + s)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// mustParseHash converts the passed big-endian hex string into a
|
||||
// chainhash.Hash and will panic if there is an error. It only differs from the
|
||||
// one available in chainhash in that it will panic so errors in the source code
|
||||
// be detected. It will only (and must only) be called with hard-coded, and
|
||||
// therefore known good, hashes.
|
||||
func mustParseHash(s string) *chainhash.Hash {
|
||||
hash, err := chainhash.NewHashFromStr(s)
|
||||
if err != nil {
|
||||
panic("invalid hash in source file: " + s)
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// SSTX TESTING -------------------------------------------------------------------
|
||||
|
||||
// TestSStx ensures the CheckSStx and IsSStx functions correctly recognize stake
|
||||
@ -1326,6 +1353,202 @@ func TestIsNullDataScript(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateRevocationFromTicket validates that revocation transactions are
|
||||
// created correctly under a variety of conditions.
|
||||
func TestCreateRevocationFromTicket(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Default network parameters to use for tests.
|
||||
params := chaincfg.RegNetParams()
|
||||
|
||||
// The following variables are derived from a mainnet ticket that was
|
||||
// purchased in block 135375 and revoked in block 140709.
|
||||
ticketHash := mustParseHash("dad48ac8c59ee97d1a6fd04ad4f1c8392357a6ee78d39f" +
|
||||
"fc00fb467a0cdba695")
|
||||
ticketOut1 := &MinimalOutput{
|
||||
PkScript: hexToBytes("ba76a914097e847d49c6806f6933e806a350f43b97ac70d088a" +
|
||||
"c"),
|
||||
Value: 4126629682,
|
||||
Version: 0,
|
||||
}
|
||||
ticketOut2 := &MinimalOutput{
|
||||
PkScript: hexToBytes("6a1e86c6da62556f5e21fbce3564b7374724d65f0cbb51c66d0" +
|
||||
"0000000800058"),
|
||||
Value: 0,
|
||||
Version: 0,
|
||||
}
|
||||
ticketOut3 := &MinimalOutput{
|
||||
PkScript: hexToBytes("bd76a914000000000000000000000000000000000000000088a" +
|
||||
"c"),
|
||||
Value: 0,
|
||||
Version: 0,
|
||||
}
|
||||
ticketOut4 := &MinimalOutput{
|
||||
PkScript: hexToBytes("6a1e7e8efe653b374ba7ad950c873e483fc53c5bafa9c1c439f" +
|
||||
"8000000000058"),
|
||||
Value: 0,
|
||||
Version: 0,
|
||||
}
|
||||
ticketOut5 := &MinimalOutput{
|
||||
PkScript: hexToBytes("bd76a914000000000000000000000000000000000000000088a" +
|
||||
"c"),
|
||||
Value: 0,
|
||||
Version: 0,
|
||||
}
|
||||
ticketMinOuts := []*MinimalOutput{
|
||||
ticketOut1,
|
||||
ticketOut2,
|
||||
ticketOut3,
|
||||
ticketOut4,
|
||||
ticketOut5,
|
||||
}
|
||||
revocationHash := mustParseHash("46ae5f78174c6c6e3675d0bbfec27e25c40f3a119e" +
|
||||
"df9183b96261db5cda7a4f")
|
||||
revocationTxFee := dcrutil.Amount(285000)
|
||||
revocationTxVersion := uint16(1)
|
||||
|
||||
// Invalid script version.
|
||||
ticketOutInvalidScriptVersion := &MinimalOutput{
|
||||
PkScript: hexToBytes("6a1e86c6da62556f5e21fbce3564b7374724d65f0cbb51c66d0" +
|
||||
"0000000800058"),
|
||||
Value: 0,
|
||||
Version: 1,
|
||||
}
|
||||
ticketMinOutsInvalidScriptVersion := []*MinimalOutput{
|
||||
ticketOut1,
|
||||
ticketOutInvalidScriptVersion,
|
||||
ticketOut3,
|
||||
ticketOut4,
|
||||
ticketOut5,
|
||||
}
|
||||
|
||||
// Invalid ticket commitment amount.
|
||||
ticketOutInvalidCommitmentAmt := &MinimalOutput{
|
||||
PkScript: hexToBytes("6a1e86c6da62556f5e21fbce3564b7374724d65f0cbbfffffff" +
|
||||
"fffffffff0058"),
|
||||
Value: 0,
|
||||
Version: 0,
|
||||
}
|
||||
ticketMinOutsInvalidCommitmentAmt := []*MinimalOutput{
|
||||
ticketOut1,
|
||||
ticketOutInvalidCommitmentAmt,
|
||||
ticketOut3,
|
||||
ticketOut4,
|
||||
ticketOut5,
|
||||
}
|
||||
|
||||
// No fee limit.
|
||||
ticketOut2NoFeeLimit := &MinimalOutput{
|
||||
PkScript: hexToBytes("6a1e86c6da62556f5e21fbce3564b7374724d65f0cbb51c66d0" +
|
||||
"0000000800000"),
|
||||
Value: 0,
|
||||
Version: 0,
|
||||
}
|
||||
ticketMinOutsNoFeeLimit := []*MinimalOutput{
|
||||
ticketOut1,
|
||||
ticketOut2NoFeeLimit,
|
||||
ticketOut3,
|
||||
ticketOut4,
|
||||
ticketOut5,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ticketHash *chainhash.Hash
|
||||
ticketMinOuts []*MinimalOutput
|
||||
revocationTxFee dcrutil.Amount
|
||||
revocationTxVersion uint16
|
||||
wantTxHash chainhash.Hash
|
||||
wantErr error
|
||||
}{{
|
||||
name: "valid with P2SH and P2PKH outputs",
|
||||
ticketHash: ticketHash,
|
||||
ticketMinOuts: ticketMinOuts,
|
||||
revocationTxFee: revocationTxFee,
|
||||
revocationTxVersion: revocationTxVersion,
|
||||
wantTxHash: *revocationHash,
|
||||
}, {
|
||||
name: "invalid ticket minimal outputs",
|
||||
ticketHash: ticketHash,
|
||||
ticketMinOuts: nil,
|
||||
revocationTxFee: revocationTxFee,
|
||||
revocationTxVersion: revocationTxVersion,
|
||||
wantErr: ErrSStxNoOutputs,
|
||||
}, {
|
||||
name: "invalid script version",
|
||||
ticketHash: ticketHash,
|
||||
ticketMinOuts: ticketMinOutsInvalidScriptVersion,
|
||||
revocationTxFee: revocationTxFee,
|
||||
revocationTxVersion: revocationTxVersion,
|
||||
wantErr: ErrSStxInvalidOutputs,
|
||||
}, {
|
||||
name: "invalid ticket commitment amount",
|
||||
ticketHash: ticketHash,
|
||||
ticketMinOuts: ticketMinOutsInvalidCommitmentAmt,
|
||||
revocationTxFee: revocationTxFee,
|
||||
revocationTxVersion: revocationTxVersion,
|
||||
wantErr: ErrSStxBadCommitAmount,
|
||||
}, {
|
||||
name: "invalid fee",
|
||||
ticketHash: ticketHash,
|
||||
ticketMinOuts: ticketMinOuts,
|
||||
revocationTxFee: 16777217,
|
||||
revocationTxVersion: revocationTxVersion,
|
||||
wantErr: ErrSSRtxInvalidFee,
|
||||
}, {
|
||||
name: "invalid fee (no fee limit)",
|
||||
ticketHash: ticketHash,
|
||||
ticketMinOuts: ticketMinOutsNoFeeLimit,
|
||||
revocationTxFee: revocationTxFee,
|
||||
revocationTxVersion: revocationTxVersion,
|
||||
wantErr: ErrSSRtxInvalidFee,
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
// Create a revocation transaction with the given test parameters.
|
||||
revocationTx, err := CreateRevocationFromTicket(test.ticketHash,
|
||||
test.ticketMinOuts, test.revocationTxFee, test.revocationTxVersion,
|
||||
params)
|
||||
|
||||
// Validate that the expected error was returned for negative tests.
|
||||
if test.wantErr != nil {
|
||||
if !errors.Is(err, test.wantErr) {
|
||||
t.Errorf("%q: mismatched error -- got %T, want %T", test.name, err,
|
||||
test.wantErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate that an unexpected error was not returned.
|
||||
if err != nil {
|
||||
t.Fatalf("%q: unexpected error creating revocation: %v", test.name, err)
|
||||
}
|
||||
|
||||
// Validate that the revocation transaction was created correctly.
|
||||
err = CheckSSRtx(revocationTx)
|
||||
if err != nil {
|
||||
t.Errorf("%q: unexpected error checking revocation: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate the revocation transaction version.
|
||||
if revocationTx.Version != test.revocationTxVersion {
|
||||
t.Errorf("%q: mismatched tx version -- got %d, want %d", test.name,
|
||||
revocationTx.Version, test.revocationTxVersion)
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate that the resulting revocation transaction hash matches what is
|
||||
// expected.
|
||||
revocationTxHash := revocationTx.TxHash()
|
||||
if revocationTxHash != test.wantTxHash {
|
||||
t.Errorf("%q: mismatched tx hash -- got %v, want %v", test.name,
|
||||
revocationTxHash, test.wantTxHash)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// TESTING VARIABLES BEGIN HERE
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user