Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support testmempoolaccept for both bitcoind and btcd #2053

Merged
merged 11 commits into from
Jan 16, 2024
Prev Previous commit
Next Next commit
btcd: add new RPC method testmempoolaccept
  • Loading branch information
yyforyongyu committed Jan 15, 2024
commit 6c9f7fe7b91072987c3ba54c3d22f8735fd8a1a6
121 changes: 121 additions & 0 deletions mempool/mocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package mempool

import (
"time"

"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/mock"
)

// MockTxMempool is a mock implementation of the TxMempool interface.
type MockTxMempool struct {
mock.Mock
}

// Ensure the MockTxMempool implements the TxMemPool interface.
var _ TxMempool = (*MockTxMempool)(nil)

// LastUpdated returns the last time a transaction was added to or removed from
// the source pool.
func (m *MockTxMempool) LastUpdated() time.Time {
args := m.Called()
return args.Get(0).(time.Time)
}

// TxDescs returns a slice of descriptors for all the transactions in the pool.
func (m *MockTxMempool) TxDescs() []*TxDesc {
args := m.Called()
return args.Get(0).([]*TxDesc)
}

// RawMempoolVerbose returns all the entries in the mempool as a fully
// populated btcjson result.
func (m *MockTxMempool) RawMempoolVerbose() map[string]*btcjson.
GetRawMempoolVerboseResult {

args := m.Called()
return args.Get(0).(map[string]*btcjson.GetRawMempoolVerboseResult)
}

// Count returns the number of transactions in the main pool. It does not
// include the orphan pool.
func (m *MockTxMempool) Count() int {
args := m.Called()
return args.Get(0).(int)
}

// FetchTransaction returns the requested transaction from the transaction
// pool. This only fetches from the main transaction pool and does not include
// orphans.
func (m *MockTxMempool) FetchTransaction(
txHash *chainhash.Hash) (*btcutil.Tx, error) {

args := m.Called(txHash)

if args.Get(0) == nil {
return nil, args.Error(1)
}

return args.Get(0).(*btcutil.Tx), args.Error(1)
}

// HaveTransaction returns whether or not the passed transaction already exists
// in the main pool or in the orphan pool.
func (m *MockTxMempool) HaveTransaction(hash *chainhash.Hash) bool {
args := m.Called(hash)
return args.Get(0).(bool)
}

// ProcessTransaction is the main workhorse for handling insertion of new
// free-standing transactions into the memory pool. It includes functionality
// such as rejecting duplicate transactions, ensuring transactions follow all
// rules, orphan transaction handling, and insertion into the memory pool.
func (m *MockTxMempool) ProcessTransaction(tx *btcutil.Tx, allowOrphan,
rateLimit bool, tag Tag) ([]*TxDesc, error) {

args := m.Called(tx, allowOrphan, rateLimit, tag)

if args.Get(0) == nil {
return nil, args.Error(1)
}

return args.Get(0).([]*TxDesc), args.Error(1)
}

// RemoveTransaction removes the passed transaction from the mempool. When the
// removeRedeemers flag is set, any transactions that redeem outputs from the
// removed transaction will also be removed recursively from the mempool, as
// they would otherwise become orphans.
func (m *MockTxMempool) RemoveTransaction(tx *btcutil.Tx,
removeRedeemers bool) {

m.Called(tx, removeRedeemers)
}

// CheckMempoolAcceptance behaves similarly to bitcoind's `testmempoolaccept`
// RPC method. It will perform a series of checks to decide whether this
// transaction can be accepted to the mempool. If not, the specific error is
// returned and the caller needs to take actions based on it.
func (m *MockTxMempool) CheckMempoolAcceptance(
tx *btcutil.Tx) (*MempoolAcceptResult, error) {

args := m.Called(tx)

if args.Get(0) == nil {
return nil, args.Error(1)
}

return args.Get(0).(*MempoolAcceptResult), args.Error(1)
}

// CheckSpend checks whether the passed outpoint is already spent by a
// transaction in the mempool. If that's the case the spending transaction will
// be returned, if not nil will be returned.
func (m *MockTxMempool) CheckSpend(op wire.OutPoint) *btcutil.Tx {
args := m.Called(op)

return args.Get(0).(*btcutil.Tx)
}
124 changes: 124 additions & 0 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ const (

// maxProtocolVersion is the max protocol version the server supports.
maxProtocolVersion = 70002

// defaultMaxFeeRate is the default value to use(0.1 BTC/kvB) when the
// `MaxFee` field is not set when calling `testmempoolaccept`.
defaultMaxFeeRate = 0.1
)

var (
Expand Down Expand Up @@ -179,6 +183,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{
"verifychain": handleVerifyChain,
"verifymessage": handleVerifyMessage,
"version": handleVersion,
"testmempoolaccept": handleTestMempoolAccept,
}

// list of commands that we recognize, but for which btcd has no support because
Expand Down Expand Up @@ -3806,6 +3811,125 @@ func handleVersion(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (in
return result, nil
}

// handleTestMempoolAccept implements the testmempoolaccept command.
func handleTestMempoolAccept(s *rpcServer, cmd interface{},
closeChan <-chan struct{}) (interface{}, error) {

c := cmd.(*btcjson.TestMempoolAcceptCmd)

// Create txns to hold the decoded tx.
txns := make([]*btcutil.Tx, 0, len(c.RawTxns))

// Iterate the raw hex slice and decode them.
for _, rawTx := range c.RawTxns {
rawBytes, err := hex.DecodeString(rawTx)
if err != nil {
return nil, rpcDecodeHexError(rawTx)
}

tx, err := btcutil.NewTxFromBytes(rawBytes)
if err != nil {
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCDeserialization,
Message: "TX decode failed: " + err.Error(),
}
}

txns = append(txns, tx)
}

results := make([]*btcjson.TestMempoolAcceptResult, 0, len(txns))
for _, tx := range txns {
// Create a test result item.
item := &btcjson.TestMempoolAcceptResult{
Txid: tx.Hash().String(),
Wtxid: tx.WitnessHash().String(),
}

// Check the mempool acceptance.
result, err := s.cfg.TxMemPool.CheckMempoolAcceptance(tx)

// If an error is returned, this tx is not allow, hence we
// record the reason.
if err != nil {
item.Allowed = false

// TODO(yy): differentiate the errors and put package
// error in `PackageError` field.
item.RejectReason = err.Error()

results = append(results, item)

// Move to the next transaction.
continue
}

// If this transaction is an orphan, it's not allowed.
if result.MissingParents != nil {
item.Allowed = false

// NOTE: "missing-inputs" is what bitcoind returns
// here, so we mimic the same error message.
item.RejectReason = "missing-inputs"

results = append(results, item)

// Move to the next transaction.
continue
}

// Otherwise this tx is allowed if its fee rate is below the
// max fee rate, we now patch the fields in
// `TestMempoolAcceptItem` as much as possible.
//
// Calculate the fee field and validate its fee rate.
item.Fees, item.Allowed = validateFeeRate(
result.TxFee, result.TxSize, c.MaxFeeRate,
)

// If the fee rate check passed, assign the corresponding
// fields.
if item.Allowed {
item.Vsize = int32(result.TxSize)
} else {
// NOTE: "max-fee-exceeded" is what bitcoind returns
// here, so we mimic the same error message.
item.RejectReason = "max-fee-exceeded"
}

results = append(results, item)
}

return results, nil
}

// validateFeeRate checks that the fee rate used by transaction doesn't exceed
// the max fee rate specified.
func validateFeeRate(feeSats btcutil.Amount, txSize int64,
maxFeeRate float64) (*btcjson.TestMempoolAcceptFees, bool) {

// Calculate fee rate in sats/kvB.
feeRateSatsPerKVB := feeSats * 1e3 / btcutil.Amount(txSize)

// Convert sats/vB to BTC/kvB.
feeRate := feeRateSatsPerKVB.ToBTC()

// Get the max fee rate, if not provided, default to 0.1 BTC/kvB.
if maxFeeRate == 0 {
maxFeeRate = defaultMaxFeeRate
}

// If the fee rate is above the max fee rate, this tx is not accepted.
if feeRate > maxFeeRate {
return nil, false
}

return &btcjson.TestMempoolAcceptFees{
Base: feeSats.ToBTC(),
EffectiveFeeRate: feeRate,
}, true
}

// rpcServer provides a concurrent safe RPC server to a chain server.
type rpcServer struct {
started int32
Expand Down
Loading