mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-09-15 05:50:10 +00:00
425 lines
18 KiB
Go
425 lines
18 KiB
Go
package consensus_test
|
|
|
|
import (
|
|
"github.com/kaspanet/kaspad/domain/consensus"
|
|
"github.com/kaspanet/kaspad/domain/consensus/model"
|
|
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
|
"github.com/kaspanet/kaspad/domain/consensus/model/testapi"
|
|
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/testutils"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/transactionhelper"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
|
|
"github.com/pkg/errors"
|
|
"testing"
|
|
)
|
|
|
|
// TestCheckSequenceVerifyConditionedByBlockHeight verifies that locked output (by CSV script) is spendable
|
|
// only after a certain number of blocks have been added relative to the time the UTXO was mined.
|
|
// CSV - check sequence verify.
|
|
func TestCheckSequenceVerifyConditionedByBlockHeight(t *testing.T) {
|
|
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
|
consensusConfig.BlockCoinbaseMaturity = 0
|
|
factory := consensus.NewFactory()
|
|
testConsensus, teardown, err := factory.NewTestConsensus(consensusConfig, "TestCheckSequenceVerifyConditionedByBlockHeight")
|
|
if err != nil {
|
|
t.Fatalf("Error setting up consensus: %+v", err)
|
|
}
|
|
defer teardown(false)
|
|
|
|
blockAHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{testConsensus.DAGParams().GenesisHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockA: %v", err)
|
|
}
|
|
blockBHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockAHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockB: %v", err)
|
|
}
|
|
blockCHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockBHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockC: %v", err)
|
|
}
|
|
blockC, err := testConsensus.GetBlock(blockCHash)
|
|
if err != nil {
|
|
t.Fatalf("Failed getting blockC: %v", err)
|
|
}
|
|
fees := uint64(1)
|
|
fundingTransaction, err := testutils.CreateTransaction(blockC.Transactions[transactionhelper.CoinbaseTransactionIndex], fees)
|
|
if err != nil {
|
|
t.Fatalf("Error creating foundingTransaction: %v", err)
|
|
}
|
|
blockDHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockCHash}, nil,
|
|
[]*externalapi.DomainTransaction{fundingTransaction})
|
|
if err != nil {
|
|
t.Fatalf("Failed creating blockD: %v", err)
|
|
}
|
|
//create a CSV script
|
|
numOfBlocksToWait := int64(10)
|
|
if numOfBlocksToWait > 0xffff {
|
|
t.Fatalf("More than the maximum number of blocks allowed.")
|
|
}
|
|
redeemScriptCSV, err := createCheckSequenceVerifyScript(numOfBlocksToWait)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create a script using createCheckSequenceVerifyScript: %v", err)
|
|
}
|
|
p2shScriptCSV, err := txscript.PayToScriptHashScript(redeemScriptCSV)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create a pay-to-script-hash script : %v", err)
|
|
}
|
|
scriptPublicKeyCSV := externalapi.ScriptPublicKey{
|
|
Version: constants.MaxScriptPublicKeyVersion,
|
|
Script: p2shScriptCSV,
|
|
}
|
|
transactionWithLockedOutput, err := createTransactionWithLockedOutput(fundingTransaction, fees, &scriptPublicKeyCSV)
|
|
if err != nil {
|
|
t.Fatalf("Error in createTransactionWithLockedOutput: %v", err)
|
|
}
|
|
// BlockE contains the locked output (locked by CSV).
|
|
// This block should be valid since CSV script locked only the output.
|
|
blockEHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockDHash}, nil,
|
|
[]*externalapi.DomainTransaction{transactionWithLockedOutput})
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockE: %v", err)
|
|
}
|
|
// The 23-bit of sequence defines if it's conditioned by block height(set to 0) or by time (set to 1).
|
|
sequenceFlag := 0
|
|
// Create a transaction that tries to spend the locked output.
|
|
transactionThatSpentTheLockedOutput, err := createTransactionThatSpentTheLockedOutput(transactionWithLockedOutput,
|
|
fees, redeemScriptCSV, uint64(numOfBlocksToWait), sequenceFlag, blockEHash, &testConsensus)
|
|
if err != nil {
|
|
t.Fatalf("Error creating transactionThatSpentTheLockedOutput: %v", err)
|
|
}
|
|
// Add a block that contains a transaction that spends the locked output before the time, and therefore should be failed.
|
|
_, _, err = testConsensus.AddBlock([]*externalapi.DomainHash{blockEHash}, nil,
|
|
[]*externalapi.DomainTransaction{transactionThatSpentTheLockedOutput})
|
|
if err == nil || !errors.Is(err, ruleerrors.ErrUnfinalizedTx) {
|
|
t.Fatalf("Expected block to be invalid with err: %v, instead found: %v", ruleerrors.ErrUnfinalizedTx, err)
|
|
}
|
|
//Add x blocks to release the locked output, where x = 'numOfBlocksToWait'.
|
|
tipHash := blockEHash
|
|
for i := int64(0); i < numOfBlocksToWait; i++ {
|
|
tipHash, _, err = testConsensus.AddBlock([]*externalapi.DomainHash{tipHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating tip: %v", err)
|
|
}
|
|
}
|
|
// Tries to spend the output that should be no longer locked.
|
|
_, _, err = testConsensus.AddBlock([]*externalapi.DomainHash{tipHash}, nil,
|
|
[]*externalapi.DomainTransaction{transactionThatSpentTheLockedOutput})
|
|
if err != nil {
|
|
t.Fatalf("The block should be valid since the output is not locked anymore. but got an error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestCheckSequenceVerifyConditionedByRelativeTime verifies that locked output (by CSV script) is spendable only after
|
|
// the time is reached to the set target relative to the time the UTXO was mined (compared to the past median time).
|
|
func TestCheckSequenceVerifyConditionedByRelativeTime(t *testing.T) {
|
|
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
|
consensusConfig.BlockCoinbaseMaturity = 0
|
|
factory := consensus.NewFactory()
|
|
testConsensus, teardown, err := factory.NewTestConsensus(consensusConfig, "TestCheckSequenceVerifyConditionedByRelativeTime")
|
|
if err != nil {
|
|
t.Fatalf("Error setting up consensus: %+v", err)
|
|
}
|
|
defer teardown(false)
|
|
|
|
blockAHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{testConsensus.DAGParams().GenesisHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockA: %v", err)
|
|
}
|
|
blockBHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockAHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockB: %v", err)
|
|
}
|
|
blockCHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockBHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockC: %v", err)
|
|
}
|
|
blockC, err := testConsensus.GetBlock(blockCHash)
|
|
if err != nil {
|
|
t.Fatalf("Failed getting blockC: %v", err)
|
|
}
|
|
fees := uint64(1)
|
|
fundingTransaction, err := testutils.CreateTransaction(blockC.Transactions[transactionhelper.CoinbaseTransactionIndex], fees)
|
|
if err != nil {
|
|
t.Fatalf("Error creating foundingTransaction: %v", err)
|
|
}
|
|
blockDHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockCHash}, nil,
|
|
[]*externalapi.DomainTransaction{fundingTransaction})
|
|
if err != nil {
|
|
t.Fatalf("Failed creating blockD: %v", err)
|
|
}
|
|
//create a CSV script
|
|
timeToWait := int64(14 * 1000)
|
|
if timeToWait > 0xffff {
|
|
t.Fatalf("More than the allowed time to set.")
|
|
}
|
|
redeemScriptCSV, err := createCheckSequenceVerifyScript(timeToWait)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create a script using createCheckSequenceVerifyScript: %v", err)
|
|
}
|
|
p2shScriptCSV, err := txscript.PayToScriptHashScript(redeemScriptCSV)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create a pay-to-script-hash script : %v", err)
|
|
}
|
|
scriptPublicKeyCSV := externalapi.ScriptPublicKey{
|
|
Version: constants.MaxScriptPublicKeyVersion,
|
|
Script: p2shScriptCSV,
|
|
}
|
|
transactionWithLockedOutput, err := createTransactionWithLockedOutput(fundingTransaction, fees, &scriptPublicKeyCSV)
|
|
if err != nil {
|
|
t.Fatalf("Error in createTransactionWithLockedOutput: %v", err)
|
|
}
|
|
// BlockE contains the locked output (locked by CSV).
|
|
// This block should be valid since CSV script locked only the output.
|
|
blockEHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockDHash}, nil,
|
|
[]*externalapi.DomainTransaction{transactionWithLockedOutput})
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockE: %v", err)
|
|
}
|
|
// The 23-bit of sequence defines if it's conditioned by block height(set to 0) or by time (set to 1).
|
|
sequenceFlag := 1
|
|
// Create a transaction that tries to spend the locked output.
|
|
transactionThatSpentTheLockedOutput, err := createTransactionThatSpentTheLockedOutput(transactionWithLockedOutput,
|
|
fees, redeemScriptCSV, uint64(timeToWait), sequenceFlag, blockEHash, &testConsensus)
|
|
if err != nil {
|
|
t.Fatalf("Error creating transactionThatSpentTheLockedOutput: %v", err)
|
|
}
|
|
// Add a block that contains a transaction that spends the locked output before the time, and therefore should be failed.
|
|
_, _, err = testConsensus.AddBlock([]*externalapi.DomainHash{blockEHash}, nil,
|
|
[]*externalapi.DomainTransaction{transactionThatSpentTheLockedOutput})
|
|
if err == nil || !errors.Is(err, ruleerrors.ErrUnfinalizedTx) {
|
|
t.Fatalf("Expected block to be invalid with err: %v, instead found: %v", ruleerrors.ErrUnfinalizedTx, err)
|
|
}
|
|
emptyCoinbase := externalapi.DomainCoinbaseData{
|
|
ScriptPublicKey: &externalapi.ScriptPublicKey{
|
|
Script: nil,
|
|
Version: 0,
|
|
},
|
|
}
|
|
var tipHash *externalapi.DomainHash
|
|
blockE, err := testConsensus.GetBlock(blockEHash)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get blockE: %v", err)
|
|
}
|
|
timeStampBlockE := blockE.Header.TimeInMilliseconds()
|
|
stagingArea := model.NewStagingArea()
|
|
// Make sure the time limitation has passed.
|
|
lockTimeTarget := blockE.Header.TimeInMilliseconds() + timeToWait
|
|
for i := int64(0); ; i++ {
|
|
tipBlock, err := testConsensus.BuildBlock(&emptyCoinbase, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating tip using BuildBlock: %v", err)
|
|
}
|
|
blockHeader := tipBlock.Header.ToMutable()
|
|
blockHeader.SetTimeInMilliseconds(timeStampBlockE + i*1000)
|
|
tipBlock.Header = blockHeader.ToImmutable()
|
|
_, err = testConsensus.ValidateAndInsertBlock(tipBlock)
|
|
if err != nil {
|
|
t.Fatalf("Error validating and inserting tip block: %v", err)
|
|
}
|
|
tipHash = consensushashing.BlockHash(tipBlock)
|
|
pastMedianTime, err := testConsensus.PastMedianTimeManager().PastMedianTime(stagingArea, tipHash)
|
|
if err != nil {
|
|
t.Fatalf("Failed getting pastMedianTime: %v", err)
|
|
}
|
|
if pastMedianTime > lockTimeTarget {
|
|
break
|
|
}
|
|
}
|
|
// Tries to spend the output that should be no longer locked
|
|
_, _, err = testConsensus.AddBlock([]*externalapi.DomainHash{tipHash}, nil,
|
|
[]*externalapi.DomainTransaction{transactionThatSpentTheLockedOutput})
|
|
if err != nil {
|
|
t.Fatalf("The block should be valid since the output is not locked anymore. but got an error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
//TestRelativeTimeOnCheckSequenceVerify verifies that if the relative target is set to X blocks to wait, and the absolute height
|
|
// will be X before adding all the blocks, then the output will remain locked.
|
|
func TestRelativeTimeOnCheckSequenceVerify(t *testing.T) {
|
|
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
|
consensusConfig.BlockCoinbaseMaturity = 0
|
|
factory := consensus.NewFactory()
|
|
testConsensus, teardown, err := factory.NewTestConsensus(consensusConfig, "TestRelativeTimeOnCheckSequenceVerify")
|
|
if err != nil {
|
|
t.Fatalf("Error setting up consensus: %+v", err)
|
|
}
|
|
defer teardown(false)
|
|
|
|
currentNumOfBlocks := int64(0)
|
|
blockAHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{testConsensus.DAGParams().GenesisHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockA: %v", err)
|
|
}
|
|
currentNumOfBlocks++
|
|
blockBHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockAHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockB: %v", err)
|
|
}
|
|
currentNumOfBlocks++
|
|
blockCHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockBHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockC: %v", err)
|
|
}
|
|
currentNumOfBlocks++
|
|
blockC, err := testConsensus.GetBlock(blockCHash)
|
|
if err != nil {
|
|
t.Fatalf("Failed getting blockC: %v", err)
|
|
}
|
|
fees := uint64(1)
|
|
fundingTransaction, err := testutils.CreateTransaction(blockC.Transactions[transactionhelper.CoinbaseTransactionIndex], fees)
|
|
if err != nil {
|
|
t.Fatalf("Error creating foundingTransaction: %v", err)
|
|
}
|
|
blockDHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockCHash}, nil,
|
|
[]*externalapi.DomainTransaction{fundingTransaction})
|
|
if err != nil {
|
|
t.Fatalf("Failed creating blockD: %v", err)
|
|
}
|
|
currentNumOfBlocks++
|
|
//create a CSV script
|
|
numOfBlocksToWait := int64(10)
|
|
if numOfBlocksToWait > 0xffff {
|
|
t.Fatalf("More than the max number of blocks that allowed to set.")
|
|
}
|
|
redeemScriptCSV, err := createCheckSequenceVerifyScript(numOfBlocksToWait)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create a script using createCheckSequenceVerifyScript: %v", err)
|
|
}
|
|
p2shScriptCSV, err := txscript.PayToScriptHashScript(redeemScriptCSV)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create a pay-to-script-hash script : %v", err)
|
|
}
|
|
scriptPublicKeyCSV := externalapi.ScriptPublicKey{
|
|
Version: constants.MaxScriptPublicKeyVersion,
|
|
Script: p2shScriptCSV,
|
|
}
|
|
transactionWithLockedOutput, err := createTransactionWithLockedOutput(fundingTransaction, fees, &scriptPublicKeyCSV)
|
|
if err != nil {
|
|
t.Fatalf("Error in createTransactionWithLockedOutput: %v", err)
|
|
}
|
|
// BlockE contains the locked output (locked by CSV).
|
|
// This block should be valid since CSV script locked only the output.
|
|
blockEHash, _, err := testConsensus.AddBlock([]*externalapi.DomainHash{blockDHash}, nil,
|
|
[]*externalapi.DomainTransaction{transactionWithLockedOutput})
|
|
if err != nil {
|
|
t.Fatalf("Error creating blockE: %v", err)
|
|
}
|
|
currentNumOfBlocks++
|
|
// The 23-bit of sequence defines if it's conditioned by block height(set to 0) or by time (set to 1).
|
|
sequenceFlag := 0
|
|
// Create a transaction that tries to spend the locked output.
|
|
transactionThatSpentTheLockedOutput, err := createTransactionThatSpentTheLockedOutput(transactionWithLockedOutput,
|
|
fees, redeemScriptCSV, uint64(numOfBlocksToWait), sequenceFlag, blockEHash, &testConsensus)
|
|
if err != nil {
|
|
t.Fatalf("Error creating transactionThatSpentTheLockedOutput: %v", err)
|
|
}
|
|
// Mines blocks so the block height will be the same as the relative number(but not enough to reach the relative target)
|
|
// and verify that the output is still locked.
|
|
// For unlocked the output the blocks should be count from the block that contains the locked output and not as an absolute height.
|
|
tipHash := blockEHash
|
|
for currentNumOfBlocks == numOfBlocksToWait {
|
|
tipHash, _, err = testConsensus.AddBlock([]*externalapi.DomainHash{tipHash}, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("Error creating tip: %v", err)
|
|
}
|
|
currentNumOfBlocks++
|
|
}
|
|
_, _, err = testConsensus.AddBlock([]*externalapi.DomainHash{tipHash}, nil,
|
|
[]*externalapi.DomainTransaction{transactionThatSpentTheLockedOutput})
|
|
if err == nil || !errors.Is(err, ruleerrors.ErrUnfinalizedTx) {
|
|
t.Fatalf("Expected block to be invalid with err: %v, instead found: %v", ruleerrors.ErrUnfinalizedTx, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func createCheckSequenceVerifyScript(numOfBlocks int64) ([]byte, error) {
|
|
scriptBuilder := txscript.NewScriptBuilder()
|
|
scriptBuilder.AddOp(txscript.OpCheckSequenceVerify)
|
|
scriptBuilder.AddInt64(numOfBlocks)
|
|
scriptBuilder.AddOp(txscript.OpTrue)
|
|
return scriptBuilder.Script()
|
|
}
|
|
|
|
func createTransactionWithLockedOutput(txToSpend *externalapi.DomainTransaction, fee uint64,
|
|
scriptPublicKeyCSV *externalapi.ScriptPublicKey) (*externalapi.DomainTransaction, error) {
|
|
|
|
_, redeemScript := testutils.OpTrueScript()
|
|
signatureScript, err := txscript.PayToScriptHashSignatureScript(redeemScript, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
input := &externalapi.DomainTransactionInput{
|
|
PreviousOutpoint: externalapi.DomainOutpoint{
|
|
TransactionID: *consensushashing.TransactionID(txToSpend),
|
|
Index: 0,
|
|
},
|
|
SignatureScript: signatureScript,
|
|
Sequence: constants.MaxTxInSequenceNum,
|
|
}
|
|
output := &externalapi.DomainTransactionOutput{
|
|
ScriptPublicKey: scriptPublicKeyCSV,
|
|
Value: txToSpend.Outputs[0].Value - fee,
|
|
}
|
|
return &externalapi.DomainTransaction{
|
|
Version: constants.MaxTransactionVersion,
|
|
Inputs: []*externalapi.DomainTransactionInput{input},
|
|
Outputs: []*externalapi.DomainTransactionOutput{output},
|
|
Payload: []byte{},
|
|
}, nil
|
|
}
|
|
|
|
func createTransactionThatSpentTheLockedOutput(txToSpend *externalapi.DomainTransaction, fee uint64,
|
|
redeemScript []byte, lockTime uint64, sequenceFlag23Bit int, lockedOutputBlockHash *externalapi.DomainHash,
|
|
testConsensus *testapi.TestConsensus) (*externalapi.DomainTransaction, error) {
|
|
|
|
// the 31bit is off since its relative timelock.
|
|
sequence := uint64(0)
|
|
sequence |= lockTime
|
|
// conditioned by absolute time:
|
|
if sequenceFlag23Bit == 1 {
|
|
sequence |= 1 << 23
|
|
lockedOutputBlock, err := (*testConsensus).GetBlock(lockedOutputBlockHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lockTime += uint64(lockedOutputBlock.Header.TimeInMilliseconds())
|
|
} else {
|
|
// conditioned by block height:
|
|
blockDAAScore, err := (*testConsensus).DAABlocksStore().DAAScore((*testConsensus).DatabaseContext(),
|
|
model.NewStagingArea(), lockedOutputBlockHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lockTime += blockDAAScore
|
|
}
|
|
signatureScript, err := txscript.PayToScriptHashSignatureScript(redeemScript, []byte{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scriptPublicKeyOutput, _ := testutils.OpTrueScript()
|
|
input := &externalapi.DomainTransactionInput{
|
|
PreviousOutpoint: externalapi.DomainOutpoint{
|
|
TransactionID: *consensushashing.TransactionID(txToSpend),
|
|
Index: 0,
|
|
},
|
|
SignatureScript: signatureScript,
|
|
Sequence: sequence,
|
|
}
|
|
output := &externalapi.DomainTransactionOutput{
|
|
ScriptPublicKey: scriptPublicKeyOutput,
|
|
Value: txToSpend.Outputs[0].Value - fee,
|
|
}
|
|
return &externalapi.DomainTransaction{
|
|
Version: constants.MaxTransactionVersion,
|
|
Inputs: []*externalapi.DomainTransactionInput{input},
|
|
Outputs: []*externalapi.DomainTransactionOutput{output},
|
|
Payload: []byte{},
|
|
LockTime: lockTime,
|
|
}, nil
|
|
}
|