Skip to content

feat: support for Bitcoin RBF (Replace-By-Fee) and CPFP (Child-Pays-for-Parent)#3306

Closed
ws4charlie wants to merge 27 commits intodevelopfrom
feat-bitcoin-Replace-By-Fee
Closed

feat: support for Bitcoin RBF (Replace-By-Fee) and CPFP (Child-Pays-for-Parent)#3306
ws4charlie wants to merge 27 commits intodevelopfrom
feat-bitcoin-Replace-By-Fee

Conversation

@ws4charlie
Copy link
Contributor

@ws4charlie ws4charlie commented Dec 16, 2024

Description

The goals:

  1. Implement BIP-125, Replace-By-Fee so that every outbound is replaceable when stuck in mempool.
  2. Use the Child-Pays-for-Parent (CPFP) to bump tx fees and unblock stuck outbounds.

The general idea:

  1. Enable RBF flag in every Bitcoin outbound.
  2. Have zetacore feed latest fee rate (every 10 mins) to pending CCTXs in the newly created method
    CheckAndUpdateCctxGasPriceBTC.
  3. Spwan a new go routine WatchMempoolTxs in zetaclient to monitor pending outbound txs, so we know how long the txs have been sitting in the Bitcoin mempool.
  4. If the pending period of the outbounds exceeds a pre-defined threshold (30 mins and 3 blocks), zetaclient will mark the outbound status as stuck and trigger tx replacement using RBF and CPFP.
  5. After tx replacement, the outbound status will be set back to normal.
  6. After tx replacement, if the outbound still doesn't move forward for another threshold period of time, RBF will triggered again with even higher fee rate until reaching a feeRateCap = 100
  7. zetaclient will always use most recent fee rate (feed by zetacore) to initiate new outbound transactions. The reason is that using an outdated GasPrice in CCTX struct is usually the root cause (not the Bitcoin network traffic) of stuck transactions, the low-fee problem needs to be solved at the first place to reduce the possibility of stuck txs.

Some concepts and parameters:

  1. LastPendingOutbound:
    Bitcoin outbounds are sequentially chained transactions by nonce. Given N pending txs [TX1, TX2, TX3, TX4] in the mempool, zetaclient only need to watch and bump the fee of TX4 in order to clear all of them. According to Bitcoin CPFP strategy, the chained pending txs are treated as a package by miners. Bumping TX4 will increase the average fee rate of the whole txs package and make it more attractive to miners.
  2. minCPFPFeeBumpPercent:
    It is set to 20% as an exercise for the initial play. It is designed to balance effectiveness in replacing stuck tx while avoiding excessive sensitivity to fee market fluctuations. For example, given a paidRate == 10, RBF will not happen until the market rate goes up to liveRate==12.
  3. feeRateCap:
    It is the maximum average fee rate for fee bumping. 100 sat/vB is a chosen heuristic based on Bitcoin mempool statistics to avoid excessive (or accidental) fees.
  4. reservedRBFFees:
    It is the amount of BTC reserved in the outbound transaction for fee bumping. It is set to 0.01 BTC by default, which can bmp 10 transactions (1KB each) by 100 sat/vB. Most of the time, we have just 1 or 2 stuck transactions in the mempool and the signers will automatically stop signing new transactions by design, so the number 0.01 BTC is good enough.

Closes: #1695

How Has This Been Tested?

  • Tested CCTX in localnet
  • Tested in development environment
  • Go unit tests
  • Go integration tests
  • Tested via GitHub Actions

Summary by CodeRabbit

Based on the comprehensive summary, here are the release notes:

Release Notes

  • New Features

    • Added support for Bitcoin Replace-By-Fee (RBF) functionality
    • Enhanced Bitcoin transaction handling with improved fee bumping and mempool management
    • Introduced new methods for monitoring and processing Bitcoin outbound transactions
  • Improvements

    • Refactored Bitcoin signer and observer components for better modularity
    • Added more robust error handling in Bitcoin transaction processing
    • Improved logging and tracking of cross-chain transactions
  • Testing

    • Expanded test coverage for Bitcoin transaction signing and RBF scenarios
    • Added comprehensive unit tests for Bitcoin RPC and signer functionality
  • Bug Fixes

    • Resolved issues with transaction fee estimation
    • Improved handling of stuck transactions in the Bitcoin mempool
  • Chores

    • Updated naming conventions for cross-chain transaction scheduling methods
    • Refactored code to improve readability and maintainability

@ws4charlie ws4charlie added zetaclient Issues related to ZetaClient zetacore Issues related to ZetaCore chain:bitcoin Bitcoin chain related labels Dec 16, 2024
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 16, 2024

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

This pull request introduces comprehensive support for Bitcoin Replace-By-Fee (RBF) functionality across the ZetaChain node implementation. The changes span multiple packages and modules, focusing on enhancing Bitcoin transaction handling, fee management, and end-to-end testing capabilities. The implementation includes new methods for fee bumping, transaction signing, mempool monitoring, and robust error handling for RBF scenarios.

Changes

File Change Summary
changelog.md Added "Unreleased" section documenting Bitcoin RBF support
cmd/zetae2e/local/local.go Commented out Bitcoin RBF withdrawal test
contrib/localnet/docker-compose.yml Updated Bitcoin Core Docker image version
e2e/ files Added new E2E tests and helper functions for Bitcoin RBF
zetaclient/chains/bitcoin/ Extensive additions for RBF implementation, including observer, signer, and RPC modules
go.mod Updated dependency visibility

Sequence Diagram

sequenceDiagram
    participant Observer
    participant Signer
    participant RPCClient
    participant Mempool
    participant ZetaCore

    Observer->>Mempool: Monitor Stuck Transaction
    Mempool-->>Observer: Identify Stuck Transaction
    Observer->>Signer: Request Fee Bump
    Signer->>RPCClient: Fetch Current Fee Rates
    RPCClient-->>Signer: Return Fee Rates
    Signer->>Mempool: Broadcast Replacement Transaction
    Mempool-->>ZetaCore: Update Transaction Status
Loading

Possibly Related PRs

Suggested Labels

bitcoin, replace-by-fee, transaction-management, e2e-tests

Suggested Reviewers

  • kingpinXD
  • fbac
  • skosito
  • brewmaster012
  • lumtis
  • swift1337

The implementation demonstrates a robust approach to implementing Replace-By-Fee functionality, with comprehensive test coverage and careful consideration of various edge cases in Bitcoin transaction management.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ws4charlie ws4charlie changed the title WIP: (DON'T review, code will change a lot) bitcoin transaction replace by fee feat: WIP (DON'T review, code will change a lot) bitcoin transaction replace by fee Dec 16, 2024
@gitguardian
Copy link

gitguardian bot commented Dec 31, 2024

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@github-actions
Copy link

github-actions bot commented Dec 31, 2024

!!!WARNING!!!
nosec detected in the following files: zetaclient/chains/bitcoin/observer/db.go, zetaclient/chains/bitcoin/observer/gas_price.go, zetaclient/chains/bitcoin/observer/mempool.go, zetaclient/chains/bitcoin/signer/outbound_data.go, zetaclient/chains/bitcoin/signer/sign.go, zetaclient/chains/bitcoin/client/helpers.go, zetaclient/chains/bitcoin/common/fee.go

Be very careful about using #nosec in code. It can be a quick way to suppress security warnings and move forward with development, it should be employed with caution. Suppressing warnings with #nosec can hide potentially serious vulnerabilities. Only use #nosec when you're absolutely certain that the security issue is either a false positive or has been mitigated in another way.

Only suppress a single rule (or a specific set of rules) within a section of code, while continuing to scan for other problems. To do this, you can list the rule(s) to be suppressed within the #nosec annotation, e.g: /* #nosec G401 */ or //#nosec G201 G202 G203
Broad #nosec annotations should be avoided, as they can hide other vulnerabilities. The CI will block you from merging this PR until you remove #nosec annotations that do not target specific rules.

Pay extra attention to the way #nosec is being used in the files listed above.

@github-actions github-actions bot added the nosec label Dec 31, 2024
@lumtis
Copy link
Contributor

lumtis commented Jan 6, 2025

Context #3279 (comment)

@codecov
Copy link

codecov bot commented Jan 10, 2025

Codecov Report

Attention: Patch coverage is 60.95482% with 458 lines in your changes missing coverage. Please review.

Project coverage is 63.13%. Comparing base (7eea20e) to head (03d737c).

Files with missing lines Patch % Lines
zetaclient/chains/bitcoin/signer/sign.go 47.82% 78 Missing and 6 partials ⚠️
zetaclient/chains/bitcoin/rpc/rpc.go 19.60% 81 Missing and 1 partial ⚠️
zetaclient/chains/bitcoin/observer/gas_price.go 0.00% 72 Missing ⚠️
zetaclient/chains/bitcoin/observer/outbound.go 23.07% 68 Missing and 2 partials ⚠️
zetaclient/chains/bitcoin/observer/utxos.go 64.28% 43 Missing and 7 partials ⚠️
zetaclient/chains/bitcoin/signer/signer.go 47.77% 45 Missing and 2 partials ⚠️
zetaclient/orchestrator/orchestrator.go 0.00% 14 Missing ⚠️
x/crosschain/keeper/abci.go 78.00% 9 Missing and 2 partials ⚠️
zetaclient/chains/bitcoin/observer/db.go 78.57% 7 Missing and 2 partials ⚠️
zetaclient/chains/bitcoin/observer/mempool.go 92.63% 7 Missing ⚠️
... and 4 more
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #3306      +/-   ##
===========================================
+ Coverage    62.43%   63.13%   +0.70%     
===========================================
  Files          449      458       +9     
  Lines        31706    32236     +530     
===========================================
+ Hits         19795    20353     +558     
+ Misses       11024    10980      -44     
- Partials       887      903      +16     
Files with missing lines Coverage Δ
pkg/math/integer.go 100.00% <100.00%> (ø)
x/observer/types/crosschain_flags.go 100.00% <ø> (ø)
zetaclient/chains/bitcoin/bitcoin.go 40.12% <100.00%> (+1.16%) ⬆️
zetaclient/chains/bitcoin/signer/fee_bumper.go 100.00% <100.00%> (ø)
zetaclient/chains/bitcoin/signer/outbound_data.go 100.00% <100.00%> (ø)
zetaclient/common/env.go 88.88% <ø> (ø)
zetaclient/orchestrator/v2_bootstrap.go 60.86% <100.00%> (ø)
zetaclient/testutils/testdata.go 87.02% <100.00%> (+0.36%) ⬆️
zetaclient/testutils/testdata_naming.go 80.85% <100.00%> (+0.85%) ⬆️
zetaclient/zetacore/broadcast.go 59.16% <100.00%> (+0.34%) ⬆️
... and 14 more

@ws4charlie ws4charlie changed the title feat: WIP (DON'T review, code will change a lot) bitcoin transaction replace by fee feat: support for Bitcoin RBF (Replace-By-Fee) and CPFP (Child-Pays-for-Parent) Jan 10, 2025
Copy link
Contributor

@swift1337 swift1337 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid work! 🔥

To prevent complex refactoring, please wait until this will be merged (it's 60% done). #3349 (the new client is a major improvement to BTC O+S)

Then I can help resolve conflicts.

@ws4charlie ws4charlie requested a review from swift1337 January 15, 2025 18:40
) (math.Uint, math.Uint, error) {
// zetacore simply update 'GasPriorityFee', and zetaclient will use it to schedule RBF tx
// there is no priority fee in Bitcoin, the 'GasPriorityFee' is repurposed to store latest fee rate in sat/vB
cctx.GetCurrentOutboundParam().GasPriorityFee = medianGasPrice.String()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does the additional fees are taken from? If it's fully inferred by the protocol there might be a insolvency issue
For EVM with use gas stability pool. For Bitcoin we can have a gas stability pool as well, it would be not autoamatically filled like Ethereum, but could still be filled manually by sending BTC to the pool

Copy link
Contributor

@swift1337 swift1337 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 60% of the PR. I agree with @lumtis that it have too many changes in too many areas and thus needs to be split into multiple PRs. It also missed some context that can be expressed in .md docs in zetaclient/chains/bitcoin/`.

I would split this into:

  1. Introducing new client methods, feature doc
  2. Zetacore changes
  3. Refactoring signer
  4. Implementing observer
  5. E2E tests and E2E framework improvements

Also, imo RBF and CPFP should be separate PRs.

// wgDeposit is a wait group for deposit runner to finish
var wgDepositRunner sync.WaitGroup

func init() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is opaque and brittle. Let's implement a common logic of test dependencies. e.g. "B can be invoked only after A passes"

// wgDeposit is a wait group for deposit runner to finish
var wgDepositRunner sync.WaitGroup

func init() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To solve this, we simply need to:

  1. Add E2ETest.Dependencies []string (e.g. []string{TestBitcoinWithdrawRestrictedName})
  2. Update this RunE2ETests:
    • It should keep track of "pending tests" []string and "completed tests" map[string]struct{}
    • If a test has no dependencies, then run(), otherwise skip and check later in the loop

type E2ETest struct {

NewE2ETest(
    name, description string, 
    argsDefinition []ArgDefinition, 
    e2eTestFunc E2ETestFunc,
     ...deps string // NEW
) E2ETest

func (r *E2ERunner) RunE2ETests(e2eTests []E2ETest) (err error) {

require.Len(r, args, 2)

// wait for block mining to stop
wgDepositRunner.Wait()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop this

)

// MustHaveDroppedTx ensures the given tx has been dropped
func MustHaveDroppedTx(ctx context.Context, client *rpcclient.Client, txHash *chainhash.Hash) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear that this is (a) related to BTC, (b) related to the mempool

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the following pattern:

type m struct {
  Bitcoin Bitcoin
  // for zetacore.go Zetacore Zetacore
}

var Must m

type Bitcoin string

(Bitcoin) MempoolTxDropped(ctx context.Context, client *rpcclient.Client, txHash *chainhash.Hash) {
  ...
}

This trick effectively allows callers to do this:

// do some btc e2e stuff in e2etests/....

utils.Must.Bitcoin.MempoolTxDropped(...)

As a result, we will have a clear and easy-to-read testing library.


memplEntry, err := client.GetMempoolEntry(txHash)
if err != nil {
if strings.Contains(err.Error(), "Transaction not in mempool") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use sentinel errors, don't use strings. The new client also can solve this because it parses error as struct thus you can retrieve error code and cast to smth like ErrNotInMempool

for {
memplEntry, err := client.GetMempoolEntry(parentHash)
if err != nil {
if strings.Contains(err.Error(), "Transaction not in mempool") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

client interfaces.BTCRPCClient,
childHash string,
timeout time.Duration,
) (int64, float64, int64, int64, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should return FeesSomething{} struct instead of 4 numbers

RPCStatusCheckInterval = time.Minute

// MempoolStuckTxCheckInterval is the interval to check for stuck transactions in the mempool
MempoolStuckTxCheckInterval = 30 * time.Second
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be BitcoinMempoolStuckTxDuration ?

}

// PkScriptTSS returns the TSS pkScript
func (signer *Signer) PkScriptTSS() ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (signer *Signer) PkScriptTSS() ([]byte, error) {
func (signer *Signer) TSSToPkScript() ([]byte, error) {

)

// WatchUTXOs watches bitcoin chain for UTXOs owned by the TSS address
func (ob *Observer) WatchUTXOs(ctx context.Context) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use scheduler

nonce uint64,
consolidateRank uint16,
test bool,
) ([]btcjson.ListUnspentResult, float64, uint16, float64, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should return struct

@lumtis lumtis marked this pull request as draft January 21, 2025 07:08
@ws4charlie
Copy link
Contributor Author

replaced by #3396, close this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking:cli chain:bitcoin Bitcoin chain related nosec zetaclient Issues related to ZetaClient zetacore Issues related to ZetaCore

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider using RBF (Replace-by-Fee) for Bitcoin outbound transactions

3 participants