kaspad/mining/txselection_test.go
Dan Aharoni ea6f7a28c2 [NOD-420] Process delayed blocks (#529)
* [NOD-420] Delay blocks with valid timestamp (non-delayed) that point to a delayed block.

* [NOD-420] Mark block as requested when setting as delayed.

* [NOD-420] Merge master; Use dag.timeSource.AdjustedTime() instead of time.Now;

* [NOD-420] Return nil when not expecting an error

* [NOD-420] Initialise delyaed blocks mapping

* [NOD-420] Trigger delayed blocks processing every time we process a block.

* [NOD-420] Hold the read lock in processDelayedBlocks

* [NOD-420] Add delayed blocks heap sorted by their process time so we could process them in order.

* [NOD-420] Update debug log

* [NOD-420] Fix process blocks loop

* [NOD-420] Add comment

* [NOD-420] Log error message

* [NOD-420] Implement peek method for delayed block heap. extract delayed block processing to another  function.

* [NOD-420] Trigger process delayed blocks only in process block

* [NOD-420] Move delayed block addition to process block

* [NOD-420] Use process block to make sure we fully process the delayed block and deal with orphans.

* [NOD-420] Unexport functions when not needed; Return isDelayed boolean from ProcessBlock instead of the delay duration

* [NOd-420] Remove redundant delayedBlocksLock

* [NOD-420] Resolve merge conflict; Return delay 0 instead of boolean

* [NOD-420] Do not treat delayed block as orphan

* [NOD-420] Make sure block is not processed if we have already sa delayed.

* [NOD-420] Process delayed block if parent is delayed to make sure it would not be treated as orphan.

* [NOD-420] Rename variable

* [NOD-420] Rename function. Move maxDelayOfParents to process.go

* [NOD-420] Fix typo

* [NOD-420] Handle errors from processDelayedBlocks properly

* [NOD-420] Return default values if err != nil from dag.addDelayedBlock

* [NOD-420] Return default values if err != nil from dag.addDelayedBlock in another place

Co-authored-by: Svarog <feanorr@gmail.com>
2020-01-08 15:28:52 +02:00

540 lines
12 KiB
Go

package mining
import (
"bou.ke/monkey"
"fmt"
"github.com/kaspanet/kaspad/blockdag"
"github.com/kaspanet/kaspad/dagconfig"
"github.com/kaspanet/kaspad/txscript"
"github.com/kaspanet/kaspad/util"
"github.com/kaspanet/kaspad/util/subnetworkid"
"github.com/kaspanet/kaspad/wire"
"math"
"testing"
)
type testTxDescDefinition struct {
fee uint64
mass uint64
gas uint64
expectedMinSelectedTimes uint64
expectedMaxSelectedTimes uint64
tx *util.Tx
}
func (dd testTxDescDefinition) String() string {
return fmt.Sprintf("[fee: %d, gas: %d, mass: %d]", dd.fee, dd.gas, dd.mass)
}
func TestSelectTxs(t *testing.T) {
params := dagconfig.SimNetParams
params.BlockCoinbaseMaturity = 0
dag, teardownFunc, err := blockdag.DAGSetup("TestSelectTxs", blockdag.Config{
DAGParams: &params,
})
if err != nil {
t.Fatalf("Failed to setup DAG instance: %v", err)
}
defer teardownFunc()
txSource := &fakeTxSource{
txDescs: []*TxDesc{},
}
blockTemplateGenerator := NewBlkTmplGenerator(&Policy{BlockMaxMass: 50000},
&params, txSource, dag, blockdag.NewMedianTime(), txscript.NewSigCache(100000))
OpTrueAddr, err := OpTrueAddress(params.Prefix)
if err != nil {
t.Fatalf("OpTrueAddress: %s", err)
}
template, err := blockTemplateGenerator.NewBlockTemplate(OpTrueAddr)
if err != nil {
t.Fatalf("NewBlockTemplate: %v", err)
}
isOrphan, isDelayed, err := dag.ProcessBlock(util.NewBlock(template.Block), blockdag.BFNoPoWCheck)
if err != nil {
t.Fatalf("ProcessBlock: %v", err)
}
if isDelayed {
t.Fatalf("ProcessBlock: template " +
"is too far in the future")
}
if isOrphan {
t.Fatalf("ProcessBlock: template got unexpectedly orphan")
}
fakeSubnetworkID := subnetworkid.SubnetworkID{250}
signatureScript, err := txscript.PayToScriptHashSignatureScript(blockdag.OpTrueScript, nil)
if err != nil {
t.Fatalf("Error creating signature script: %s", err)
}
scriptPubKey, err := txscript.NewScriptBuilder().AddOp(txscript.OpTrue).Script()
if err != nil {
t.Fatalf("Failed to create scriptPubKey: %v", err)
}
tests := []struct {
name string
runTimes int
massLimit uint64
gasLimit uint64
txDefinitions []*testTxDescDefinition
}{
{
name: "no source txs",
runTimes: 1,
massLimit: 10,
gasLimit: 10,
txDefinitions: []*testTxDescDefinition{},
},
{
name: "zero fee",
runTimes: 1,
massLimit: 10,
gasLimit: 10,
txDefinitions: []*testTxDescDefinition{
{
mass: 0,
gas: 0,
fee: 0,
// Expected probability: 0
expectedMinSelectedTimes: 0,
expectedMaxSelectedTimes: 0,
},
},
},
{
name: "single transaction",
runTimes: 1,
massLimit: 100,
gasLimit: 100,
txDefinitions: []*testTxDescDefinition{
{
mass: 10,
gas: 10,
fee: 10,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
},
},
{
name: "none fit, limited gas and mass",
runTimes: 1,
massLimit: 2,
gasLimit: 2,
txDefinitions: []*testTxDescDefinition{
{
mass: 10,
gas: 10,
fee: 100,
// Expected probability: 0
expectedMinSelectedTimes: 0,
expectedMaxSelectedTimes: 0,
},
{
mass: 5,
gas: 5,
fee: 50,
// Expected probability: 0
expectedMinSelectedTimes: 0,
expectedMaxSelectedTimes: 0,
},
},
},
{
name: "only one fits, limited gas and mass",
runTimes: 1,
massLimit: 2,
gasLimit: 2,
txDefinitions: []*testTxDescDefinition{
{
mass: 1,
gas: 1,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
{
mass: 10,
gas: 10,
fee: 100,
// Expected probability: 0
expectedMinSelectedTimes: 0,
expectedMaxSelectedTimes: 0,
},
{
mass: 10,
gas: 10,
fee: 100,
// Expected probability: 0
expectedMinSelectedTimes: 0,
expectedMaxSelectedTimes: 0,
},
},
},
{
name: "all fit, limited gas",
runTimes: 1,
massLimit: wire.MaxMassPerBlock,
gasLimit: 10,
txDefinitions: []*testTxDescDefinition{
{
mass: 100,
gas: 1,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
{
mass: 0,
gas: 1,
fee: 1,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
{
mass: 2,
gas: 1,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
{
mass: 3,
gas: 1,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
{
mass: 4,
gas: 1,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
},
},
{
name: "all fit, limited mass",
runTimes: 1,
massLimit: 10,
gasLimit: math.MaxUint64,
txDefinitions: []*testTxDescDefinition{
{
mass: 1,
gas: 100,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
{
mass: 1,
gas: 0,
fee: 1,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
{
mass: 1,
gas: 2,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
{
mass: 1,
gas: 3,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
{
mass: 1,
gas: 4,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 1,
expectedMaxSelectedTimes: 1,
},
},
},
{
name: "equal selection probability",
runTimes: 1000,
massLimit: 100,
gasLimit: 100,
txDefinitions: []*testTxDescDefinition{
{
mass: 75,
gas: 75,
fee: 100,
// Expected probability: 0.25
expectedMinSelectedTimes: 200,
expectedMaxSelectedTimes: 300,
},
{
mass: 75,
gas: 75,
fee: 100,
// Expected probability: 0.25
expectedMinSelectedTimes: 200,
expectedMaxSelectedTimes: 300,
},
{
mass: 75,
gas: 75,
fee: 100,
// Expected probability: 0.25
expectedMinSelectedTimes: 200,
expectedMaxSelectedTimes: 300,
},
{
mass: 75,
gas: 75,
fee: 100,
// Expected probability: 0.25
expectedMinSelectedTimes: 200,
expectedMaxSelectedTimes: 300,
},
},
},
{
name: "unequal selection probability",
runTimes: 1000,
massLimit: 100,
gasLimit: 100,
txDefinitions: []*testTxDescDefinition{
{
mass: 50,
gas: 50,
fee: 100,
// Expected probability: 0.33
expectedMinSelectedTimes: 230,
expectedMaxSelectedTimes: 430,
},
{
mass: 100,
gas: 0,
fee: 100,
// Expected probability: 0.50
expectedMinSelectedTimes: 400,
expectedMaxSelectedTimes: 600,
},
{
mass: 0,
gas: 100,
fee: 100,
// Expected probability: 0.50
expectedMinSelectedTimes: 400,
expectedMaxSelectedTimes: 600,
},
},
},
{
name: "distributed selection probability",
runTimes: 100,
massLimit: 32,
gasLimit: 32,
txDefinitions: []*testTxDescDefinition{
{
mass: 1,
gas: 1,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 95,
expectedMaxSelectedTimes: 100,
},
{
mass: 2,
gas: 2,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 95,
expectedMaxSelectedTimes: 100,
},
{
mass: 4,
gas: 4,
fee: 100,
// Expected probability: 1
expectedMinSelectedTimes: 95,
expectedMaxSelectedTimes: 100,
},
{
mass: 8,
gas: 8,
fee: 100,
// Expected probability: 0.98
expectedMinSelectedTimes: 90,
expectedMaxSelectedTimes: 100,
},
{
mass: 16,
gas: 16,
fee: 100,
// Expected probability: 0.90
expectedMinSelectedTimes: 75,
expectedMaxSelectedTimes: 100,
},
{
mass: 32,
gas: 32,
fee: 100,
// Expected probability: 0
expectedMinSelectedTimes: 0,
expectedMaxSelectedTimes: 5,
},
},
},
}
for _, test := range tests {
func() {
// Force the mass limit to always be test.massLimit
blockTemplateGenerator.policy.BlockMaxMass = test.massLimit
// Force the mass to be as defined in the definition.
// We use the first payload byte to resolve which definition to use.
massPatch := monkey.Patch(blockdag.CalcTxMassFromUTXOSet, func(tx *util.Tx, _ blockdag.UTXOSet) (uint64, error) {
if tx.IsCoinBase() {
return 0, nil
}
index := tx.MsgTx().Payload[0]
definition := test.txDefinitions[index]
return definition.mass, nil
})
defer massPatch.Unpatch()
// Force the gas limit to always be test.gasLimit
gasLimitPatch := monkey.Patch((*blockdag.SubnetworkStore).GasLimit, func(_ *blockdag.SubnetworkStore, subnetworkID *subnetworkid.SubnetworkID) (uint64, error) {
return test.gasLimit, nil
})
defer gasLimitPatch.Unpatch()
// Force the fee to be as defined in the definition.
// We use the first payload byte to resolve which definition to use.
feePatch := monkey.Patch(blockdag.CheckTransactionInputsAndCalulateFee, func(tx *util.Tx, _ uint64, _ blockdag.UTXOSet, _ *dagconfig.Params, _ bool) (txFeeInSompi uint64, err error) {
if tx.IsCoinBase() {
return 0, nil
}
index := tx.MsgTx().Payload[0]
definition := test.txDefinitions[index]
return definition.fee, nil
})
defer feePatch.Unpatch()
// Load the txSource with transactions as defined in test.txDefinitions.
// Note that we're saving the definition index in the msgTx payload
// so that we may use it in massPatch and feePatch.
// We also initialize a map that keeps track of how many times a tx
// has been selected.
txSource.txDescs = make([]*TxDesc, len(test.txDefinitions))
selectedTxCountMap := make(map[*util.Tx]uint64, len(test.txDefinitions))
for i, definition := range test.txDefinitions {
txIn := &wire.TxIn{
PreviousOutpoint: wire.Outpoint{
TxID: *template.Block.Transactions[util.CoinbaseTransactionIndex].TxID(),
Index: 0,
},
Sequence: wire.MaxTxInSequenceNum,
SignatureScript: signatureScript,
}
txOut := &wire.TxOut{
ScriptPubKey: scriptPubKey,
Value: 1,
}
msgTx := wire.NewSubnetworkMsgTx(
wire.TxVersion, []*wire.TxIn{txIn}, []*wire.TxOut{txOut},
&fakeSubnetworkID, definition.gas, []byte{byte(i)})
tx := util.NewTx(msgTx)
txDesc := TxDesc{
Fee: definition.fee,
Tx: tx,
}
txSource.txDescs[i] = &txDesc
definition.tx = tx
selectedTxCountMap[tx] = 0
}
// Run selectTxs test.runTimes times
for i := 0; i < test.runTimes; i++ {
result, err := blockTemplateGenerator.selectTxs(OpTrueAddr)
if err != nil {
t.Errorf("selectTxs unexpectedly failed in test '%s': %s",
test.name, err)
return
}
// Increment the counts of all the selected transactions.
// Ignore the first transactions because it's the coinbase.
for _, selectedTx := range result.selectedTxs[1:] {
selectedTxCountMap[selectedTx]++
}
}
// Make sure that each transaction has not been selected either
// too little or too much.
for i, definition := range test.txDefinitions {
tx := definition.tx
count := selectedTxCountMap[tx]
min := definition.expectedMinSelectedTimes
max := definition.expectedMaxSelectedTimes
if count < min || count > max {
t.Errorf("unexpected selected tx count "+
"in test '%s' for tx %d:%s. Want: %d <= count <= %d, got: %d. "+
"Note that this test is probabilistic and has a low chance to erroneously fail",
test.name, i, definition, min, max, count)
}
}
}()
}
}