Child-Pays-For-Parent (CPFP)

What is CPFP?

Stuck transactions in UTXO-based networks originating from your workspace can be released and successfully transmitted by initiating a new follow-up transaction with a higher fee that pays for itself and its correlated stuck transaction(s). The new transaction's inputs must be taken directly from the original transaction's unspent outputs. This type of transaction is called Child-Pays-For-Parent (CPFP).

Since the CPFP transaction is designed to release the original, stuck transaction, it needs to come with a relatively high fee that easily covers both transactions: the stuck one, as well as the CPFP transaction itself. Since this “release” is the CPFP transaction’s primary purpose, the transaction’s actual amount can be low in relation to its fee. The fee is the primary issue here, and it needs to serve both transactions.

Bitcoin transaction fees

On the Bitcoin network, miners select transactions to include in blocks. Typically, they prioritize transactions with higher fees because these fees are part of the reward they get for their work.

When the Bitcoin network is overly active, transactions with low fees may get left behind or take a long time to confirm since miners will pick transactions with higher fees first. This is where CPFP comes in.

When a transaction is stuck in the mempool, you can aggregate your CPFP transaction fee with the original transaction fee to help miners prioritize the original transaction and add it to the blockchain.

Supported assets for CPFP

  • Bitcoin
  • BCH
  • BSV
  • Cardano
  • Dash
  • Litecoin
  • ZCash

How to use CPFP as a transaction recipient

  1. Call the Get UTXO unspent inputs information endpoint to retrieve the original transaction's unspent outputs. Look for transactions with the Pending status in the transaction array. After you find your transaction, copy the txHash string. This is required for the CPFP transaction.
  2. Create the appropriate type of transaction. In the function, you will enter the assetId, amount, source.type, and source.id from the destination vault account in your workspace.
  3. Use the fee parameter to specify an amount that you estimate is high enough to cover the CPFP transaction fee as well as the fees of the original transactions it is designed to release. When an estimate is necessary, call the Estimate transaction fee endpoint to estimate the CPFP transaction fee.
  4. Use the extraParameters parameter to include the InputsSelection object of the original transaction. This indicates the unconfirmed input change created by the original transaction. Make sure it follows the specified structure because the inputsToSpend parameter populates the CPFP transaction with the original transaction's hash.
  5. Submit the transaction for approval and signing.

Important notes:

  1. The transaction you are trying to use CPFP with cannot be older than 10 days, otherwise it will no longer be present in the mempool. For older transactions which you want released, contact our Customer Support for assistance.
  2. The transaction must be marked as completed (confirmed) in our system, unless you are using the change UTXO.
  3. If the stuck transaction is older than 10 days and/or is not marked as completed, please contact Fireblocks Support.

How to switch the transaction to “Completed” status

You can switch the transaction to the Completed status via the Fireblocks Console or by using our set confirmation threshold API endpoint to set the threshold to zero. If it is not switched, the transaction will no longer be present in our mempool, and the CPFP transaction will fail.

Constructing the CPFP transaction

After verifying the transaction is not outdated and is in Completed status, you must use this endpoint to proceed with the CPFP operation. You must create a transaction payload with the following parameters:

  • Source: the source of the child transaction is the destination of the stuck transaction.
  • Destination: the destination of the child transaction can be the same as the source vault or any other destination you prefer.
  • Amount: must be the sum or less than the sum of the parent’s outputs that you want to use (you do not have to use all parent outputs; you can use only one output as an input for the child transaction).
  • FeeLevel: do not use FeeLevel, but instead see CustomFee below.
  • CustomFee: must be a high value, so the miner finds it attractive. You can use a CPFP calculator to get the desired fee (CPFP calculator example: Child Pays For Parent calculator).
  • ExtraParams: you must specify the inputs you want to use. These inputs serve as the outputs of the stuck parent transaction (you must use at least one). The selected inputs must be specified in the inputsToSend array inside an inputsSelection parameter, as is shown in the following example:

Example

let txPayload = {
  assetId: "BTC_TEST",
  amount: "0.001",
  fee: "1000",
  source: {
    type: TransferPeerPathType.VaultAccount,
    id: "0",
  },
  destination: {
    type: TransferPeerPathType.VaultAccount,
    id: "1",
  },
  extraParameters: {
    inputsSelection: {
      //grab input hash from UTXO endpoint
      inputsToSpend: [
        {
          txHash:
            "24f769af0c2b67965ae4b95583c049e0a8ba08f1c142676c9fc9fcaac5ad12a3",
          index: 0,
        },
      ],
    },
  },
  note: "Your CPFP transaction!",
};

const createTransaction = async (
  payload: TransactionRequest,
): Promise<CreateTransactionResponse | undefined> => {
  try {
    const result = await fireblocks.transactions.createTransaction({
      transactionRequest: payload,
    });
    console.log(JSON.stringify(result.data, null, 2));
    return result.data;
  } catch (error) {
    console.error(error);
  }
};
createTransaction(txPayload);

//prerequisites: API key from fireblocks, set up in console

const fs = require('fs');
const path = require('path');
const { FireblocksSDK } = require('fireblocks-sdk');
const { exit } = require('process');
const { inspect } = require('util');

//api secret key file created
const apiSecret = fs.readFileSync(path.resolve("../fireblocks_secret.key"), "utf8");
const apiKey = "<API_KEY HERE>"
// Choose the right api url for your workspace type 
const baseUrl = "https://api.fireblocks.io";
const fireblocks = new FireblocksSDK(apiSecret, apiKey, baseUrl);

(async () => {

    let newTx = await fireblocks.createTransaction({
        assetId: "BTC_TEST",
        amount: "0.001",
        fee: "1000",
        source: {
            type: "VAULT_ACCOUNT", 
            id: "2"
        },
        destination: { 
            type: "VAULT_ACCOUNT",
            id: "3"
        },
        extraParameters: {
            inputsSelection: {
                //grab input hash from UTXO endpoint
                inputsToSpend: [
                    {
                        "txHash": "24f769af0c2b67965ae4b95583c049e0a8ba08f1c142676c9fc9fcaac5ad12a3",
                        "index": 0
                    }, 
                ]
            }
        }
    });
    
    console.log(JSON.stringify(newTx));

    // let amountToConsolidate = await fireblocks.getMaxSpendableAmount("1", "BTC_TEST").maxSpendableAmount;
    // console.log(amountToConsolidate);

})().catch((e)=>{
    console.error(`Failed: ${e}`);
    exit(-1);
})
import json
from pathlib import Path

from fireblocks_sdk import FireblocksSDK
from fireblocks_sdk.api_types import *


def initialize_fireblocks(api_secret, api_key, base_url):
    try:
        return FireblocksSDK(private_key=api_secret, api_key=api_key, api_base_url=base_url)
    except Exception as e:
        raise ConnectionError(f"Error initializing Fireblocks SDK: {e}")


def create_transaction(fireblocks_instance):
    try:
        transaction_result = fireblocks_instance.create_transaction(
                asset_id="BTC_TEST",
                source=TransferPeerPath(peer_type=VAULT_ACCOUNT, peer_id="31"),
                destination=DestinationTransferPeerPath(peer_type=VAULT_ACCOUNT, peer_id="0"),
                amount="0.00009", fee="1000",
                extra_parameters={
                    "inputsSelection": {
                        "inputsToSpend": [
                            {"txHash": "0076f06b4f38dc2e14bc9abdc8e7caff6607f6f381e14e7db153ddd6267aabcd", "index": 1},
                            {"txHash": "b7496655bb71d04ee4a8dd9b13a5e78f420bc7deceddd6ae09afc14484f6abcd", "index": 1},
                            {"txHash": "2e3761fee5765d8c49bf13cf7bc4b32bed6804999bafe01c5605d64a897eabcd", "index": 0},
                            {"txHash": "436993c1a14e0ba824c7733399c8943fc8de0fdad1996a5dec53064045c4abcd", "index": 1},
                            {"txHash": "9f9af5bc9e5d57c9f8d0223bf2e1eb4c2ea45e95f2c1a0d0f02caaaa9956abcd", "index": 1},
                            ]
                        }
                    }
                )
        return transaction_result
    except Exception as e:
        raise RuntimeError(f"Error creating transaction: {e}")


def main():
    api_secret_path = ".../fireblocks_secret.key"
    api_key = "<API_KEY HERE>"
    base_url = "https://api.fireblocks.io"
    
    try:
        api_secret_file = Path(api_secret_path)
        api_secret = api_secret_file.read_text()
        fireblocks = initialize_fireblocks(api_secret, api_key, base_url)
        transaction_result = create_transaction(fireblocks)
        print(json.dumps(transaction_result, indent=2))
    except Exception as e:
        print(f"An error occurred: {e}")


if __name__ == "__main__":
    main()

Summary of the CPFP process

In order to carry out a CPFP transaction, the following requirements must be satisfied:

  1. The stuck transaction must not be older than 10 days.
  2. The transaction must be marked as Completed in our system.

Notes:

  1. If the parent transaction has a change output then it can still be used by the CPFP transaction to boost the original, even if the destination is not under the sender’s custody.
  2. If you use the change output you do not have to switch the stuck transaction to the Completed status.
  3. If the parent transaction is no longer in the Fireblocks mempool and the source of the transaction is within your workspace, you can perform a Transaction Replacement procedure instead.