From 3c4900275c91ac236ab14db4e4b531e049c83712 Mon Sep 17 00:00:00 2001 From: eugene Date: Mon, 2 Aug 2021 11:14:52 -0400 Subject: [PATCH] mempool: export isDust for use in other projects This changes isDust to IsDust so other golang projects (btcwallet or lnd) can use the precise dust calculation used by btcd. --- mempool/policy.go | 6 +- mempool/policy_test.go | 512 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 515 insertions(+), 3 deletions(-) create mode 100644 mempool/policy_test.go diff --git a/mempool/policy.go b/mempool/policy.go index a58f110b..0197e231 100644 --- a/mempool/policy.go +++ b/mempool/policy.go @@ -172,12 +172,12 @@ func checkPkScriptStandard(pkScript []byte, scriptClass txscript.ScriptClass) er return nil } -// isDust returns whether or not the passed transaction output amount is +// IsDust returns whether or not the passed transaction output amount is // considered dust or not based on the passed minimum transaction relay fee. // Dust is defined in terms of the minimum transaction relay fee. In // particular, if the cost to the network to spend coins is more than 1/3 of the // minimum transaction relay fee, it is considered dust. -func isDust(txOut *wire.TxOut, minRelayTxFee btcutil.Amount) bool { +func IsDust(txOut *wire.TxOut, minRelayTxFee btcutil.Amount) bool { // Unspendable outputs are considered dust. if txscript.IsUnspendable(txOut.PkScript) { return true @@ -352,7 +352,7 @@ func checkTransactionStandard(tx *btcutil.Tx, height int32, // "dust". if scriptClass == txscript.NullDataTy { numNullDataOutputs++ - } else if isDust(txOut, minRelayTxFee) { + } else if IsDust(txOut, minRelayTxFee) { str := fmt.Sprintf("transaction output %d: payment "+ "of %d is dust", i, txOut.Value) return txRuleError(wire.RejectDust, str) diff --git a/mempool/policy_test.go b/mempool/policy_test.go new file mode 100644 index 00000000..b9cdbac4 --- /dev/null +++ b/mempool/policy_test.go @@ -0,0 +1,512 @@ +// Copyright (c) 2013-2016 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mempool + +import ( + "bytes" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" +) + +// TestCalcMinRequiredTxRelayFee tests the calcMinRequiredTxRelayFee API. +func TestCalcMinRequiredTxRelayFee(t *testing.T) { + tests := []struct { + name string // test description. + size int64 // Transaction size in bytes. + relayFee btcutil.Amount // minimum relay transaction fee. + want int64 // Expected fee. + }{ + { + // Ensure combination of size and fee that are less than 1000 + // produce a non-zero fee. + "250 bytes with relay fee of 3", + 250, + 3, + 3, + }, + { + "100 bytes with default minimum relay fee", + 100, + DefaultMinRelayTxFee, + 100, + }, + { + "max standard tx size with default minimum relay fee", + maxStandardTxWeight / 4, + DefaultMinRelayTxFee, + 100000, + }, + { + "max standard tx size with max satoshi relay fee", + maxStandardTxWeight / 4, + btcutil.MaxSatoshi, + btcutil.MaxSatoshi, + }, + { + "1500 bytes with 5000 relay fee", + 1500, + 5000, + 7500, + }, + { + "1500 bytes with 3000 relay fee", + 1500, + 3000, + 4500, + }, + { + "782 bytes with 5000 relay fee", + 782, + 5000, + 3910, + }, + { + "782 bytes with 3000 relay fee", + 782, + 3000, + 2346, + }, + { + "782 bytes with 2550 relay fee", + 782, + 2550, + 1994, + }, + } + + for _, test := range tests { + got := calcMinRequiredTxRelayFee(test.size, test.relayFee) + if got != test.want { + t.Errorf("TestCalcMinRequiredTxRelayFee test '%s' "+ + "failed: got %v want %v", test.name, got, + test.want) + continue + } + } +} + +// TestCheckPkScriptStandard tests the checkPkScriptStandard API. +func TestCheckPkScriptStandard(t *testing.T) { + var pubKeys [][]byte + for i := 0; i < 4; i++ { + pk, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Fatalf("TestCheckPkScriptStandard NewPrivateKey failed: %v", + err) + return + } + pubKeys = append(pubKeys, pk.PubKey().SerializeCompressed()) + } + + tests := []struct { + name string // test description. + script *txscript.ScriptBuilder + isStandard bool + }{ + { + "key1 and key2", + txscript.NewScriptBuilder().AddOp(txscript.OP_2). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_2).AddOp(txscript.OP_CHECKMULTISIG), + true, + }, + { + "key1 or key2", + txscript.NewScriptBuilder().AddOp(txscript.OP_1). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_2).AddOp(txscript.OP_CHECKMULTISIG), + true, + }, + { + "escrow", + txscript.NewScriptBuilder().AddOp(txscript.OP_2). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddData(pubKeys[2]). + AddOp(txscript.OP_3).AddOp(txscript.OP_CHECKMULTISIG), + true, + }, + { + "one of four", + txscript.NewScriptBuilder().AddOp(txscript.OP_1). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddData(pubKeys[2]).AddData(pubKeys[3]). + AddOp(txscript.OP_4).AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed1", + txscript.NewScriptBuilder().AddOp(txscript.OP_3). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_2).AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed2", + txscript.NewScriptBuilder().AddOp(txscript.OP_2). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_3).AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed3", + txscript.NewScriptBuilder().AddOp(txscript.OP_0). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_2).AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed4", + txscript.NewScriptBuilder().AddOp(txscript.OP_1). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_0).AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed5", + txscript.NewScriptBuilder().AddOp(txscript.OP_1). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed6", + txscript.NewScriptBuilder().AddOp(txscript.OP_1). + AddData(pubKeys[0]).AddData(pubKeys[1]), + false, + }, + } + + for _, test := range tests { + script, err := test.script.Script() + if err != nil { + t.Fatalf("TestCheckPkScriptStandard test '%s' "+ + "failed: %v", test.name, err) + continue + } + scriptClass := txscript.GetScriptClass(script) + got := checkPkScriptStandard(script, scriptClass) + if (test.isStandard && got != nil) || + (!test.isStandard && got == nil) { + + t.Fatalf("TestCheckPkScriptStandard test '%s' failed", + test.name) + return + } + } +} + +// TestDust tests the IsDust API. +func TestDust(t *testing.T) { + pkScript := []byte{0x76, 0xa9, 0x21, 0x03, 0x2f, 0x7e, 0x43, + 0x0a, 0xa4, 0xc9, 0xd1, 0x59, 0x43, 0x7e, 0x84, 0xb9, + 0x75, 0xdc, 0x76, 0xd9, 0x00, 0x3b, 0xf0, 0x92, 0x2c, + 0xf3, 0xaa, 0x45, 0x28, 0x46, 0x4b, 0xab, 0x78, 0x0d, + 0xba, 0x5e, 0x88, 0xac} + + tests := []struct { + name string // test description + txOut wire.TxOut + relayFee btcutil.Amount // minimum relay transaction fee. + isDust bool + }{ + { + // Any value is allowed with a zero relay fee. + "zero value with zero relay fee", + wire.TxOut{Value: 0, PkScript: pkScript}, + 0, + false, + }, + { + // Zero value is dust with any relay fee" + "zero value with very small tx fee", + wire.TxOut{Value: 0, PkScript: pkScript}, + 1, + true, + }, + { + "38 byte public key script with value 584", + wire.TxOut{Value: 584, PkScript: pkScript}, + 1000, + true, + }, + { + "38 byte public key script with value 585", + wire.TxOut{Value: 585, PkScript: pkScript}, + 1000, + false, + }, + { + // Maximum allowed value is never dust. + "max satoshi amount is never dust", + wire.TxOut{Value: btcutil.MaxSatoshi, PkScript: pkScript}, + btcutil.MaxSatoshi, + false, + }, + { + // Maximum int64 value causes overflow. + "maximum int64 value", + wire.TxOut{Value: 1<<63 - 1, PkScript: pkScript}, + 1<<63 - 1, + true, + }, + { + // Unspendable pkScript due to an invalid public key + // script. + "unspendable pkScript", + wire.TxOut{Value: 5000, PkScript: []byte{0x01}}, + 0, // no relay fee + true, + }, + } + for _, test := range tests { + res := IsDust(&test.txOut, test.relayFee) + if res != test.isDust { + t.Fatalf("Dust test '%s' failed: want %v got %v", + test.name, test.isDust, res) + continue + } + } +} + +// TestCheckTransactionStandard tests the checkTransactionStandard API. +func TestCheckTransactionStandard(t *testing.T) { + // Create some dummy, but otherwise standard, data for transactions. + prevOutHash, err := chainhash.NewHashFromStr("01") + if err != nil { + t.Fatalf("NewShaHashFromStr: unexpected error: %v", err) + } + dummyPrevOut := wire.OutPoint{Hash: *prevOutHash, Index: 1} + dummySigScript := bytes.Repeat([]byte{0x00}, 65) + dummyTxIn := wire.TxIn{ + PreviousOutPoint: dummyPrevOut, + SignatureScript: dummySigScript, + Sequence: wire.MaxTxInSequenceNum, + } + addrHash := [20]byte{0x01} + addr, err := btcutil.NewAddressPubKeyHash(addrHash[:], + &chaincfg.TestNet3Params) + if err != nil { + t.Fatalf("NewAddressPubKeyHash: unexpected error: %v", err) + } + dummyPkScript, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("PayToAddrScript: unexpected error: %v", err) + } + dummyTxOut := wire.TxOut{ + Value: 100000000, // 1 BTC + PkScript: dummyPkScript, + } + + tests := []struct { + name string + tx wire.MsgTx + height int32 + isStandard bool + code wire.RejectCode + }{ + { + name: "Typical pay-to-pubkey-hash transaction", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{&dummyTxOut}, + LockTime: 0, + }, + height: 300000, + isStandard: true, + }, + { + name: "Transaction version too high", + tx: wire.MsgTx{ + Version: wire.TxVersion + 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{&dummyTxOut}, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Transaction is not finalized", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: dummyPrevOut, + SignatureScript: dummySigScript, + Sequence: 0, + }}, + TxOut: []*wire.TxOut{&dummyTxOut}, + LockTime: 300001, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Transaction size is too large", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{{ + Value: 0, + PkScript: bytes.Repeat([]byte{0x00}, + (maxStandardTxWeight/4)+1), + }}, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Signature script size is too large", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: dummyPrevOut, + SignatureScript: bytes.Repeat([]byte{0x00}, + maxStandardSigScriptSize+1), + Sequence: wire.MaxTxInSequenceNum, + }}, + TxOut: []*wire.TxOut{&dummyTxOut}, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Signature script that does more than push data", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: dummyPrevOut, + SignatureScript: []byte{ + txscript.OP_CHECKSIGVERIFY}, + Sequence: wire.MaxTxInSequenceNum, + }}, + TxOut: []*wire.TxOut{&dummyTxOut}, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Valid but non standard public key script", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{{ + Value: 100000000, + PkScript: []byte{txscript.OP_TRUE}, + }}, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "More than one nulldata output", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{{ + Value: 0, + PkScript: []byte{txscript.OP_RETURN}, + }, { + Value: 0, + PkScript: []byte{txscript.OP_RETURN}, + }}, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Dust output", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{{ + Value: 0, + PkScript: dummyPkScript, + }}, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectDust, + }, + { + name: "One nulldata output with 0 amount (standard)", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{{ + Value: 0, + PkScript: []byte{txscript.OP_RETURN}, + }}, + LockTime: 0, + }, + height: 300000, + isStandard: true, + }, + } + + pastMedianTime := time.Now() + for _, test := range tests { + // Ensure standardness is as expected. + err := checkTransactionStandard(btcutil.NewTx(&test.tx), + test.height, pastMedianTime, DefaultMinRelayTxFee, 1) + if err == nil && test.isStandard { + // Test passes since function returned standard for a + // transaction which is intended to be standard. + continue + } + if err == nil && !test.isStandard { + t.Errorf("checkTransactionStandard (%s): standard when "+ + "it should not be", test.name) + continue + } + if err != nil && test.isStandard { + t.Errorf("checkTransactionStandard (%s): nonstandard "+ + "when it should not be: %v", test.name, err) + continue + } + + // Ensure error type is a TxRuleError inside of a RuleError. + rerr, ok := err.(RuleError) + if !ok { + t.Errorf("checkTransactionStandard (%s): unexpected "+ + "error type - got %T", test.name, err) + continue + } + txrerr, ok := rerr.Err.(TxRuleError) + if !ok { + t.Errorf("checkTransactionStandard (%s): unexpected "+ + "error type - got %T", test.name, rerr.Err) + continue + } + + // Ensure the reject code is the expected one. + if txrerr.RejectCode != test.code { + t.Errorf("checkTransactionStandard (%s): unexpected "+ + "error code - got %v, want %v", test.name, + txrerr.RejectCode, test.code) + continue + } + } +}