From 0493112768d88369a350665a14ef0228f6ab68ac Mon Sep 17 00:00:00 2001 From: Kyle Santiago Date: Wed, 28 Oct 2026 22:00:03 -0400 Subject: [PATCH 1/3] ci: test against Knots 29.4.3 --- .github/scripts/install-bitcoind.sh | 4 ++-- .github/workflows/ci.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/install-bitcoind.sh b/.github/scripts/install-bitcoind.sh index f39d55fb7e4a..f915a308aac3 100755 --- a/.github/scripts/install-bitcoind.sh +++ b/.github/scripts/install-bitcoind.sh @@ -31,14 +31,14 @@ if [ "$TEST_NETWORK" = "liquid-regtest" ]; then rm -rf "${EFILENAME}" "${EDIRNAME}" elif [ "${BLAKE2B_CI:-0}" = "1" ]; then # Exercise unified signatures on a backend that enforces Blake2b consensus. - KNOTS_VERSION=29.4.1.knots20260508 + KNOTS_VERSION=29.4.3.knots20260508 KNOTS_FILE="bitcoin-${KNOTS_VERSION}-x86_64-linux-gnu.tar.gz" if [ -f "$1/${KNOTS_FILE}" ]; then cp "$1/${KNOTS_FILE}" . else wget "https://github.com/bitcoinknots/bitcoin/releases/download/v${KNOTS_VERSION}/${KNOTS_FILE}" fi - echo "0d0b435ae67dd38d150c048a388be821ad0ca8b46d6dcace5c34d4ba2977801b ${KNOTS_FILE}" | sha256sum -c - + echo "4c67f1139b668fc3b98e339dd16112c636b2c0f42c2294b98fb92d1d27943e24 ${KNOTS_FILE}" | sha256sum -c - tar -xf "${KNOTS_FILE}" [ "$1" = "" ] || cp "${KNOTS_FILE}" "$1"/ sudo install -D -m 755 "bitcoin-${KNOTS_VERSION}/bin/bitcoind" /usr/local/libexec/paperclip-bitcoind diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9bd37224531b..98b18f2820cc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -894,7 +894,7 @@ jobs: bash -x .github/scripts/setup.sh - name: Verify Blake2b backend - run: bitcoind --version | grep '29.4.1.knots20260508' + run: bitcoind --version | grep '29.4.3.knots20260508' - name: Download build uses: actions/download-artifact@v8 From e7a8f6ddcd3a7ff5c05e9c023761248b1c7a5b3d Mon Sep 17 00:00:00 2001 From: Kyle Santiago Date: Wed, 28 Oct 2026 22:00:04 -0400 Subject: [PATCH 2/3] wallet: hold a coinbase from block 979919 for Knots' 25920-block maturity --- bitcoin/chainparams.c | 25 +++-- bitcoin/chainparams.h | 4 + common/test/run-utxo_is_immature.c | 146 +++++++++++++++++++++++++++++ common/utxo.c | 9 +- 4 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 common/test/run-utxo_is_immature.c diff --git a/bitcoin/chainparams.c b/bitcoin/chainparams.c index 2c74cc78b4d4..a3fc31d38347 100644 --- a/bitcoin/chainparams.c +++ b/bitcoin/chainparams.c @@ -53,17 +53,24 @@ const struct chainparams networks[] = { .max_supply = AMOUNT_SAT_INIT(2100000000000000), /* "Lightning Charge Powers Developers & Blockstream Store" */ .when_lightning_became_cool = 504500, - /* Bitcoin Knots deploys a temporary longer coinbase maturity on this - * chain: an upgraded node requires this depth of every coinbase spend - * it will accept into its mempool, whatever height the coinbase was - * mined at, and keeps requiring it after the consensus window closes. + /* Bitcoin Knots deploys a longer coinbase maturity on this chain: an + * upgraded node requires this depth of every coinbase spend it will + * accept into its mempool for a coinbase mined before + * long_coinbase_maturity_height, and keeps requiring it after the + * consensus window closes. * - * Read off v29.4.2.knots20260508, where CoinbaseMaturityLong is - * CoinbaseMaturityLongReleaseHeight (979920) minus - * CoinbaseMaturityLongStartHeight (973440). Temporary in their words - * rather than ours, and a full year is under discussion, so re-check it - * against the release you run. */ + * Read off v29.4.3.knots20260508, where CoinbaseMaturityBeforeRolling + * is CoinbaseMaturityHistoricalReleaseHeight (979920) minus + * CoinbaseMaturityHistoricalStartHeight (973440). Re-check it against + * the release you run. */ .relay_coinbase_maturity = 6480, + /* v29.4.3.knots20260508: CoinbaseMaturityLongStartHeight and + * CoinbaseMaturityLong (180 * 144). Knots drops a coinbase mined once + * its parent's median time passes RDTS expiry (2027-09-01) back to + * COINBASE_MATURITY; we keep the long depth for those, which only + * reports them immature for longer, until that is handled here. */ + .long_coinbase_maturity_height = 979919, + .long_coinbase_maturity = 25920, .p2pkh_version = 0, .p2sh_version = 5, .testnet = false, diff --git a/bitcoin/chainparams.h b/bitcoin/chainparams.h index 7a9b648faa07..7ae2ac72a17d 100644 --- a/bitcoin/chainparams.h +++ b/bitcoin/chainparams.h @@ -61,6 +61,10 @@ struct chainparams { * is all of us, only ever wants the relay answer. It is * COINBASE_MATURITY on any chain without such a rule. */ const u32 relay_coinbase_maturity; + /* A coinbase mined at or after long_coinbase_maturity_height needs + * long_coinbase_maturity instead. Zero height means no such rule. */ + const u32 long_coinbase_maturity_height; + const u32 long_coinbase_maturity; const u8 p2pkh_version; const u8 p2sh_version; diff --git a/common/test/run-utxo_is_immature.c b/common/test/run-utxo_is_immature.c new file mode 100644 index 000000000000..920476729fcb --- /dev/null +++ b/common/test/run-utxo_is_immature.c @@ -0,0 +1,146 @@ +#include "config.h" +#include "../utxo.c" +#include +#include +#include +#include +#include +#include + +/* AUTOGENERATED MOCKS START */ +/* Generated stub for amount_asset_is_main */ +bool amount_asset_is_main(struct amount_asset *asset UNNEEDED) +{ fprintf(stderr, "amount_asset_is_main called!\n"); abort(); } +/* Generated stub for amount_asset_to_sat */ +struct amount_sat amount_asset_to_sat(struct amount_asset *asset UNNEEDED) +{ fprintf(stderr, "amount_asset_to_sat called!\n"); abort(); } +/* Generated stub for amount_feerate */ + bool amount_feerate(u32 *feerate UNNEEDED, struct amount_sat fee UNNEEDED, size_t weight UNNEEDED) +{ fprintf(stderr, "amount_feerate called!\n"); abort(); } +/* Generated stub for amount_sat */ +struct amount_sat amount_sat(u64 satoshis UNNEEDED) +{ fprintf(stderr, "amount_sat called!\n"); abort(); } +/* Generated stub for amount_sat_add */ + bool amount_sat_add(struct amount_sat *val UNNEEDED, + struct amount_sat a UNNEEDED, + struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_add called!\n"); abort(); } +/* Generated stub for amount_sat_eq */ +bool amount_sat_eq(struct amount_sat a UNNEEDED, struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_eq called!\n"); abort(); } +/* Generated stub for amount_sat_greater_eq */ +bool amount_sat_greater_eq(struct amount_sat a UNNEEDED, struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_greater_eq called!\n"); abort(); } +/* Generated stub for amount_sat_sub */ + bool amount_sat_sub(struct amount_sat *val UNNEEDED, + struct amount_sat a UNNEEDED, + struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_sub called!\n"); abort(); } +/* Generated stub for amount_sat_to_asset */ +struct amount_asset amount_sat_to_asset(struct amount_sat *sat UNNEEDED, const u8 *asset UNNEEDED) +{ fprintf(stderr, "amount_sat_to_asset called!\n"); abort(); } +/* Generated stub for amount_tx_fee */ +struct amount_sat amount_tx_fee(u32 fee_per_kw UNNEEDED, size_t weight UNNEEDED) +{ fprintf(stderr, "amount_tx_fee called!\n"); abort(); } +/* Generated stub for fromwire */ +const u8 *fromwire(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, void *copy UNNEEDED, size_t n UNNEEDED) +{ fprintf(stderr, "fromwire called!\n"); abort(); } +/* Generated stub for fromwire_bool */ +bool fromwire_bool(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_bool called!\n"); abort(); } +/* Generated stub for fromwire_fail */ +void *fromwire_fail(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_fail called!\n"); abort(); } +/* Generated stub for fromwire_secp256k1_ecdsa_signature */ +void fromwire_secp256k1_ecdsa_signature(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, + secp256k1_ecdsa_signature *signature UNNEEDED) +{ fprintf(stderr, "fromwire_secp256k1_ecdsa_signature called!\n"); abort(); } +/* Generated stub for fromwire_sha256 */ +void fromwire_sha256(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, struct sha256 *sha256 UNNEEDED) +{ fprintf(stderr, "fromwire_sha256 called!\n"); abort(); } +/* Generated stub for fromwire_tal_arrn */ +u8 *fromwire_tal_arrn(const tal_t *ctx UNNEEDED, + const u8 **cursor UNNEEDED, size_t *max UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "fromwire_tal_arrn called!\n"); abort(); } +/* Generated stub for fromwire_u32 */ +u32 fromwire_u32(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_u32 called!\n"); abort(); } +/* Generated stub for fromwire_u64 */ +u64 fromwire_u64(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_u64 called!\n"); abort(); } +/* Generated stub for fromwire_u8 */ +u8 fromwire_u8(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_u8 called!\n"); abort(); } +/* Generated stub for fromwire_u8_array */ +void fromwire_u8_array(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, u8 *arr UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "fromwire_u8_array called!\n"); abort(); } +/* Generated stub for siphash_seed */ +const struct siphash_seed *siphash_seed(void) +{ fprintf(stderr, "siphash_seed called!\n"); abort(); } +/* Generated stub for towire */ +void towire(u8 **pptr UNNEEDED, const void *data UNNEEDED, size_t len UNNEEDED) +{ fprintf(stderr, "towire called!\n"); abort(); } +/* Generated stub for towire_bool */ +void towire_bool(u8 **pptr UNNEEDED, bool v UNNEEDED) +{ fprintf(stderr, "towire_bool called!\n"); abort(); } +/* Generated stub for towire_secp256k1_ecdsa_signature */ +void towire_secp256k1_ecdsa_signature(u8 **pptr UNNEEDED, + const secp256k1_ecdsa_signature *signature UNNEEDED) +{ fprintf(stderr, "towire_secp256k1_ecdsa_signature called!\n"); abort(); } +/* Generated stub for towire_sha256 */ +void towire_sha256(u8 **pptr UNNEEDED, const struct sha256 *sha256 UNNEEDED) +{ fprintf(stderr, "towire_sha256 called!\n"); abort(); } +/* Generated stub for towire_u32 */ +void towire_u32(u8 **pptr UNNEEDED, u32 v UNNEEDED) +{ fprintf(stderr, "towire_u32 called!\n"); abort(); } +/* Generated stub for towire_u64 */ +void towire_u64(u8 **pptr UNNEEDED, u64 v UNNEEDED) +{ fprintf(stderr, "towire_u64 called!\n"); abort(); } +/* Generated stub for towire_u8 */ +void towire_u8(u8 **pptr UNNEEDED, u8 v UNNEEDED) +{ fprintf(stderr, "towire_u8 called!\n"); abort(); } +/* Generated stub for towire_u8_array */ +void towire_u8_array(u8 **pptr UNNEEDED, const u8 *arr UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "towire_u8_array called!\n"); abort(); } +/* AUTOGENERATED MOCKS END */ + +static u32 immature(u32 mined, u32 now, bool coinbase) +{ + struct utxo utxo = { .is_in_coinbase = coinbase }; + + utxo.blockheight = &mined; + return utxo_is_immature(&utxo, now); +} + +/* A coinbase mined at @mined may be spent in block @mined + @depth, so it + * is mature once the tip is the block before. */ +static void check_depth(u32 mined, u32 depth) +{ + assert(immature(mined, mined + depth - 2, true) == 1); + assert(immature(mined, mined + depth - 1, true) == 0); + assert(immature(mined, mined + depth, true) == 0); + assert(immature(mined, mined, false) == 0); +} + +int main(int argc, char *argv[]) +{ + common_setup(argv[0]); + + chainparams = chainparams_for_network("bitcoin"); + check_depth(979917, 6480); + check_depth(979918, 6480); + check_depth(979919, 25920); + check_depth(979920, 25920); + check_depth(1100000, 25920); + check_depth(1, 6480); + + chainparams = chainparams_for_network("testnet4"); + check_depth(151405, 6705); + check_depth(200000, 6705); + + chainparams = chainparams_for_network("regtest"); + check_depth(101, COINBASE_MATURITY); + check_depth(2000000, COINBASE_MATURITY); + + common_shutdown(); +} diff --git a/common/utxo.c b/common/utxo.c index df8de1968f49..8843d0821c35 100644 --- a/common/utxo.c +++ b/common/utxo.c @@ -35,8 +35,13 @@ u32 utxo_is_immature(const struct utxo *utxo, u32 blockheight) * would make of it: we would build it, sign it, and have it * refused at broadcast. The two are the same number on every * chain without such a rule. */ - mature_at = *utxo->blockheight - + chainparams->relay_coinbase_maturity; + if (chainparams->long_coinbase_maturity_height + && *utxo->blockheight >= chainparams->long_coinbase_maturity_height) + mature_at = *utxo->blockheight + + chainparams->long_coinbase_maturity; + else + mature_at = *utxo->blockheight + + chainparams->relay_coinbase_maturity; if (blockheight < mature_at) return mature_at - 1 - blockheight; From dc53e97efd977219ae007392fc91928f0ad6754c Mon Sep 17 00:00:00 2001 From: Kyle Santiago Date: Wed, 28 Oct 2026 22:02:28 -0400 Subject: [PATCH 3/3] chainparams, wallet: update coinbase maturity comments for Knots 29.4.3 --- bitcoin/chainparams.c | 5 +++-- bitcoin/chainparams.h | 3 ++- wallet/wallet.h | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bitcoin/chainparams.c b/bitcoin/chainparams.c index a3fc31d38347..7070b2e070bc 100644 --- a/bitcoin/chainparams.c +++ b/bitcoin/chainparams.c @@ -178,8 +178,9 @@ const struct chainparams networks[] = { .max_funding = AMOUNT_SAT_INIT((1 << 24) - 1), .max_payment = AMOUNT_MSAT_INIT(0xFFFFFFFFULL), .max_supply = AMOUNT_SAT_INIT(2100000000000000), - /* As bitcoin above: 158111 minus 151406. Doubly provisional, since - * this chain's heights were rewritten at every release candidate. */ + /* v29.4.3.knots20260508: CoinbaseMaturityLongReleaseHeight (158111) + * minus CoinbaseMaturityLongStartHeight (151406). Doubly provisional, + * since this chain's heights were rewritten at every release candidate. */ .relay_coinbase_maturity = 6705, .p2pkh_version = 111, .p2sh_version = 196, diff --git a/bitcoin/chainparams.h b/bitcoin/chainparams.h index 7ae2ac72a17d..28cd4ff49772 100644 --- a/bitcoin/chainparams.h +++ b/bitcoin/chainparams.h @@ -52,7 +52,8 @@ struct chainparams { const struct amount_sat max_supply; const u32 when_lightning_became_cool; /* The depth a coinbase output must reach before this network will - * relay a transaction spending it. + * relay a transaction spending it, unless long_coinbase_maturity + * below applies. * * This is a relay rule, not a consensus one, and on a chain that * deploys a longer coinbase maturity the two do not agree: a spend diff --git a/wallet/wallet.h b/wallet/wallet.h index 12d1aa45327a..4e49e1b59901 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -814,7 +814,7 @@ u32 wallet_blocks_contig_minheight(struct wallet *w); * wallet_extract_owned_outputs - given a tx, extract all of our outputs * @w: wallet * @is_coinbase: true if this is output 0 (can't spend until the chain's - * relay_coinbase_maturity; see utxo_is_immature) + * relay depth; see utxo_is_immature) * @blockheight: non-NULL blockheight if known. * @outputs: if non-NULL, output numbers of owned outputs are appended to it. *