From 0984261085fa3ece7da7a4da293e39bb5cb432ba Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:21:52 +0200 Subject: [PATCH 001/195] audit: freeze transfer recovery gate failure [FINDING-149] --- ...0025-v4.8-security-remediation-register.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 8f4e887310..92956675d8 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -5223,6 +5223,43 @@ rebuild logic was added. pass. The exact-SHA GitHub macOS package build remains pending. - **Final status:** FIXED locally; GitHub macOS artifact pending. +## Findings frozen after the merged asset-phase CI review + +### FINDING-149: Transfer crash-recovery test checks the tip before peer synchronization + +- **Severity:** MEDIUM. The mandatory Final Gate fails and blocks release + qualification. The available evidence does not show a consensus bypass. +- **Frozen initial status:** OPEN at merged commit + `7dc44845309ae36e33a55d3122e62603fcaed9dd`. +- **Affected RIP-25 invariant:** The release gate must verify that a node + recovers the asset transfer state after an interrupted coins flush. +- **Affected Core 4.8.0 fix:** The coins and asset database durability repair + addresses an inherited Core 4.8.0 defect. This test timing problem was + introduced by the later integration test in `ecd4ca077`. +- **Observed failure:** Final Gate runs `37852895999` at + `1e1c4533f0b2d70ed4a8f326cd30bbdaf6a5b498` and `37852951929` at + `7dc44845309ae36e33a55d3122e62603fcaed9dd` both fail at + `feature_asset_dbcrash_transfer.py:66`: the restarted source node reports a + different tip immediately after RPC startup. The preceding asset database + recovery log assertions pass. The other required functional tests pass. +- **Likely root cause:** The test restarts the source node and reads its tip + before it has synchronized the transfer block from its still-running peer. + Related crash-recovery tests use a bounded wait for the expected tip. + This hypothesis needs a green rerun; a timeout would instead expose a + remaining recovery defect. +- **Affected file/function/lines:** + `test/functional/feature_asset_dbcrash_transfer.py:59-75`, recovery check. +- **Expected behavior:** Wait a bounded time for the exact transfer block, + then retain the UTXO and asset balance comparisons. A missing block must + still fail the test and the gate. +- **Regression required:** Treat both failed Final Gate runs as the red + regression. Rerun the transfer recovery test after the bounded wait and + confirm the complete Final Gate passes at the same source SHA as the + cross-platform Build Raven artifacts. Keep `make check` mandatory. +- **Remediation commit:** Pending. +- **Verification:** Pending. +- **Final status:** OPEN. + ## Findings frozen during the 2026-10-08 asset-wallet continuation The defects below were reproduced or established against the uncommitted From 8d4e86d4b0bdec0b47b90c021efb9f9a851144a4 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:22:16 +0200 Subject: [PATCH 002/195] test: wait for transfer crash recovery tip [FINDING-149] --- test/functional/feature_asset_dbcrash_transfer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/functional/feature_asset_dbcrash_transfer.py b/test/functional/feature_asset_dbcrash_transfer.py index 7d8a1e1498..477f1fd3fd 100755 --- a/test/functional/feature_asset_dbcrash_transfer.py +++ b/test/functional/feature_asset_dbcrash_transfer.py @@ -8,7 +8,7 @@ import os from test_framework.test_framework import RavenTestFramework -from test_framework.util import assert_equal, connect_nodes +from test_framework.util import assert_equal, connect_nodes, wait_until class AssetTransferCrashTest(RavenTestFramework): @@ -63,6 +63,9 @@ def run_test(self): assert 'Simulating a crash. Goodbye.' in debug_log assert 'Asset database state is interrupted or not certified' in debug_log assert 'Replaying blocks' not in debug_log + wait_until(lambda: source_node.getbestblockhash() == transfer_block, + err_msg='asset transfer recovery did not reach the expected tip', + timeout=60) assert_equal(source_node.getbestblockhash(), transfer_block) recovered_utxo = source_node.gettxoutsetinfo() recovered_balances = source_node.listaddressesbyasset(asset_name) From a349da3a626b871970eebf0257f4d4167705c22a Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:23:07 +0200 Subject: [PATCH 003/195] audit: freeze anchor and raw funding limitations [FINDING-150][FINDING-151] --- ...0025-v4.8-security-remediation-register.md | 133 +++++++++++++----- 1 file changed, 96 insertions(+), 37 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 92956675d8..be2c76fe52 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -5223,43 +5223,6 @@ rebuild logic was added. pass. The exact-SHA GitHub macOS package build remains pending. - **Final status:** FIXED locally; GitHub macOS artifact pending. -## Findings frozen after the merged asset-phase CI review - -### FINDING-149: Transfer crash-recovery test checks the tip before peer synchronization - -- **Severity:** MEDIUM. The mandatory Final Gate fails and blocks release - qualification. The available evidence does not show a consensus bypass. -- **Frozen initial status:** OPEN at merged commit - `7dc44845309ae36e33a55d3122e62603fcaed9dd`. -- **Affected RIP-25 invariant:** The release gate must verify that a node - recovers the asset transfer state after an interrupted coins flush. -- **Affected Core 4.8.0 fix:** The coins and asset database durability repair - addresses an inherited Core 4.8.0 defect. This test timing problem was - introduced by the later integration test in `ecd4ca077`. -- **Observed failure:** Final Gate runs `37852895999` at - `1e1c4533f0b2d70ed4a8f326cd30bbdaf6a5b498` and `37852951929` at - `7dc44845309ae36e33a55d3122e62603fcaed9dd` both fail at - `feature_asset_dbcrash_transfer.py:66`: the restarted source node reports a - different tip immediately after RPC startup. The preceding asset database - recovery log assertions pass. The other required functional tests pass. -- **Likely root cause:** The test restarts the source node and reads its tip - before it has synchronized the transfer block from its still-running peer. - Related crash-recovery tests use a bounded wait for the expected tip. - This hypothesis needs a green rerun; a timeout would instead expose a - remaining recovery defect. -- **Affected file/function/lines:** - `test/functional/feature_asset_dbcrash_transfer.py:59-75`, recovery check. -- **Expected behavior:** Wait a bounded time for the exact transfer block, - then retain the UTXO and asset balance comparisons. A missing block must - still fail the test and the gate. -- **Regression required:** Treat both failed Final Gate runs as the red - regression. Rerun the transfer recovery test after the bounded wait and - confirm the complete Final Gate passes at the same source SHA as the - cross-platform Build Raven artifacts. Keep `make check` mandatory. -- **Remediation commit:** Pending. -- **Verification:** Pending. -- **Final status:** OPEN. - ## Findings frozen during the 2026-10-08 asset-wallet continuation The defects below were reproduced or established against the uncommitted @@ -7684,3 +7647,99 @@ Cross-platform CI and public deployment remain separate release gates. for both affected targets. `bash -n` and diff whitespace checks pass; the exact-SHA macOS artifact rerun is pending. - **Final status:** FIXED locally; GitHub macOS artifact pending. + +## Findings frozen after the merged asset-phase CI review + +### FINDING-149: Transfer crash-recovery test checks the tip before peer synchronization + +- **Severity:** MEDIUM. The mandatory Final Gate fails and blocks release + qualification. The available evidence does not show a consensus bypass. +- **Frozen initial status:** OPEN at merged commit + `7dc44845309ae36e33a55d3122e62603fcaed9dd`. +- **Affected RIP-25 invariant:** The release gate must verify that a node + recovers the asset transfer state after an interrupted coins flush. +- **Affected Core 4.8.0 fix:** The coins and asset database durability repair + addresses an inherited Core 4.8.0 defect. This test timing problem was + introduced by the later integration test in `ecd4ca077`. +- **Observed failure:** Final Gate runs `37852895999` at + `1e1c4533f0b2d70ed4a8f326cd30bbdaf6a5b498` and `37852951929` at + `7dc44845309ae36e33a55d3122e62603fcaed9dd` both fail at + `feature_asset_dbcrash_transfer.py:66`: the restarted source node reports a + different tip immediately after RPC startup. The preceding asset database + recovery log assertions pass. The other required functional tests pass. +- **Likely root cause:** The test restarts the source node and reads its tip + before it has synchronized the transfer block from its still-running peer. + Related crash-recovery tests use a bounded wait for the expected tip. + This hypothesis needs a green rerun; a timeout would instead expose a + remaining recovery defect. +- **Affected file/function/lines:** + `test/functional/feature_asset_dbcrash_transfer.py:59-75`, recovery check. +- **Expected behavior:** Wait a bounded time for the exact transfer block, + then retain the UTXO and asset balance comparisons. A missing block must + still fail the test and the gate. +- **Regression required:** Treat both failed Final Gate runs as the red + regression. Rerun the transfer recovery test after the bounded wait and + confirm the complete Final Gate passes at the same source SHA as the + cross-platform Build Raven artifacts. Keep `make check` mandatory. +- **Remediation commit:** Pending. +- **Verification:** Pending. +- **Final status:** OPEN. + +### FINDING-150: Ordinary RVN coin selection can consume a protected asset anchor + +- **Severity:** MEDIUM operational availability. Consensus still rejects an + asset spend without a matching PQ input. +- **Frozen initial status:** OPEN during the merged asset-phase review at + `7dc44845309ae36e33a55d3122e62603fcaed9dd`. +- **Affected RIP-25 invariant:** The wallet should preserve or refresh the + native PQ anchor needed to spend a protected asset later. +- **Affected Core 4.8.0 fix:** None; native PQ anchors are part of this asset + phase. +- **Root cause:** Generic native RVN coin selection includes funded PQ anchor + outputs among available non-asset UTXOs, even when the transaction spends + no asset. Asset-specific refresh logic is not entered in that path. +- **Affected file/function/lines:** `src/wallet/wallet.cpp:2971-2975, + 3178-3186,4101-4104,4234`, native coin selection. +- **Concrete scenario:** An ordinary payment may consume the sole anchor + for a protected asset and return change to a different destination. + The asset and keys remain in the wallet, but a later protected spend needs + another RVN payment to the original PQ witness-v2 address. +- **Expected behavior:** Preserve a last required anchor during generic + spending or atomically recreate a funded anchor for the same program. + Avoid silently overriding a user-selected change destination. +- **Regression required:** Fund one protected asset and its sole anchor, + issue an ordinary RVN payment that selects the anchor, verify the later + asset spend fails for lack of a matching funded anchor, then top up that + PQ address and verify spending recovers. Confirm no consensus bypass. +- **Remediation commit:** Pending design. +- **Verification:** Pending. +- **Final status:** OPEN; documented availability limitation. + +### FINDING-151: Raw transaction funding does not select asset PQ anchors + +- **Severity:** MEDIUM interoperability and availability. Consensus rejects + the unanchored protected asset transaction. +- **Frozen initial status:** OPEN during the merged asset-phase review at + `7dc44845309ae36e33a55d3122e62603fcaed9dd`. +- **Affected RIP-25 invariant:** Each protected asset input needs a funded + native PQ input for the same program. +- **Affected Core 4.8.0 fix:** None; Core 4.8.0 has no asset PQ anchor rule. +- **Root cause:** `fundrawtransaction` funds native RVN through generic coin + selection and does not derive required PQ programs from raw asset inputs. + Its native change also does not automatically refresh a supplied anchor. +- **Affected file/function/lines:** `src/wallet/wallet.cpp:3869-3918, + 3999-4016,4100-4104,4250-4309`, raw funding and coin selection. +- **Concrete scenario:** A raw protected asset transfer is funded and signed + without a matching anchor. Mempool and block checks reject it. The caller + can supply a funded matching anchor explicitly and return enough RVN to + that same PQ address for future spends. +- **Expected behavior:** Either derive and fund matching PQ anchors with + accurate fee accounting, or clearly document the manual raw transaction + construction sequence. Merely marking anchors as selected is insufficient: + the raw append loop omits selected inputs. +- **Regression required:** Demonstrate rejection of a funded raw protected + transfer lacking the matching anchor, then acceptance after adding the + anchor explicitly and preserving its PQ destination for change. +- **Remediation commit:** Pending design. +- **Verification:** Pending. +- **Final status:** OPEN; documented manual construction path pending tests. From b8bc089f3e3d2b395279e23ec5fdd8e1a9036633 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:23:50 +0200 Subject: [PATCH 004/195] audit: confirm macOS package in CI [FINDING-089] --- doc/RIP-0025-v4.8-security-remediation-register.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index be2c76fe52..3a9b9485eb 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7644,9 +7644,16 @@ Cross-platform CI and public deployment remain separate release gates. - **Verification:** Direct make evaluation proves an exported environment `PYTHONPATH` loses to the configured Makefile value, while a command-line make assignment wins. The macOS package script now uses that assignment - for both affected targets. `bash -n` and diff whitespace checks pass; - the exact-SHA macOS artifact rerun is pending. -- **Final status:** FIXED locally; GitHub macOS artifact pending. + for both affected targets. `bash -n` and diff whitespace checks pass. + Build Raven run `37852896085` passed its macOS build and packaging job + `113576631280` and uploaded artifact `11585626159`, named + `raven-unsigned-osx-72a68f0f898ec794f3bdc38d0621124e2f88b54c`. + The job log reports packaged source commit + `72a68f0f898ec794f3bdc38d0621124e2f88b54c` and artifact zip digest + `2704829474edb114b249899c945b6649aa35452233a385f3988e9295d3ea032b`. + This was the pull-request merge SHA; final release-SHA qualification + remains pending. +- **Final status:** FIXED; final release-SHA artifact verification pending. ## Findings frozen after the merged asset-phase CI review From bbe2ced994cfa0d167acf2902a650aec3e07909c Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:27:26 +0200 Subject: [PATCH 005/195] audit: freeze asset RPC and Qt findings [FINDING-152-155] --- ...0025-v4.8-security-remediation-register.md | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 3a9b9485eb..116f571589 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7750,3 +7750,108 @@ Cross-platform CI and public deployment remain separate release gates. - **Remediation commit:** Pending design. - **Verification:** Pending. - **Final status:** OPEN; documented manual construction path pending tests. + +## Findings frozen by the independent merged asset-phase review + +### FINDING-152: Default asset issuance RPCs choose an invalid destination after activation + +- **Severity:** MEDIUM operational availability. The transaction constructor + rejects the classical-only default; consensus is not bypassed. +- **Frozen initial status:** OPEN at merged commit + `7dc44845309ae36e33a55d3122e62603fcaed9dd`. +- **Affected RIP-25 invariant:** New asset and owner-token outputs after bit 13 + activation must carry a canonical PQ destination. +- **Affected Core 4.8.0 fix:** None; the classical defaults predate bit 13. +- **Root cause:** `issue`, `issueunique`, and `issuequalifierasset` fill an + omitted address with a fresh P2PKH address, while active issuance requires + a `classical|PQ` descriptor. +- **Affected file/function/lines:** `src/rpc/assets.cpp:510-526,675-691, + 2483-2499`; `src/assets/assets.cpp:4266-4275`. +- **Concrete scenario:** An ordinary RPC call without an explicit address + fails after bit 13 activation, including an owner-token issuance path. +- **Expected behavior:** Generate and persist a wallet-owned PQ asset + descriptor by default when the rule is active. Preserve classical + defaults before activation. +- **Regression required:** Exercise all three default-address RPCs before + and after activation. Confirm their new asset and owner-token outputs are + tagged, wallet-owned, and spendable with a matching funded anchor. +- **Remediation commit:** Pending. +- **Verification:** Pending. +- **Final status:** OPEN; release blocker for active bit 13. + +### FINDING-153: Asset index RPC aggregates distinct PQ programs by classical address + +- **Severity:** MEDIUM interoperability and custody interpretation. No + consensus bypass or loss of asset keys is shown. +- **Frozen initial status:** OPEN at merged commit + `7dc44845309ae36e33a55d3122e62603fcaed9dd`. +- **Affected RIP-25 invariant:** An active asset recipient is identified by + its classical address and committed PQ program together. +- **Affected Core 4.8.0 fix:** None; the index predates bit 13. +- **Root cause:** `listaddressesbyasset` reads the classical-address asset + index, while `listassetbalancesbyaddress` accepts only a classical address. + The index cannot distinguish two PQ programs paired with one classical key. +- **Affected file/function/lines:** `src/rpc/assets.cpp:787-791,1138-1157`; + `src/coins.cpp:291-302`. +- **Concrete scenario:** Two distinct protected descriptors share a + classical key; the RPC reports one aggregated row. Its returned address + alone cannot receive a new protected asset after activation. +- **Expected behavior:** Expose descriptor-aware outpoint information, or + explicitly label this legacy aggregate as non-destination data. +- **Regression required:** Fund two programs paired with one classical key, + then compare the aggregate RPC result against output tags and wallet + history; prove copying the aggregate address fails safely as a protected + recipient. +- **Remediation commit:** Pending design. +- **Verification:** Pending. +- **Final status:** OPEN; fail-safe index limitation. + +### FINDING-154: Qt asset control displays and copies a classical-only address + +- **Severity:** MEDIUM usability and destination integrity. Sending to the + copied address fails for active protected assets. +- **Frozen initial status:** OPEN at merged commit + `7dc44845309ae36e33a55d3122e62603fcaed9dd`. +- **Affected RIP-25 invariant:** Wallet UI must distinguish protected + destinations with different PQ programs. +- **Affected Core 4.8.0 fix:** None; asset coin control predates bit 13. +- **Root cause:** `WalletModel::listAssets` and the coin-control table group + by the extracted classical destination; Copy address uses that same field. +- **Affected file/function/lines:** `src/qt/walletmodel.cpp:737-756`; + `src/qt/assetcontroldialog.cpp:282-288,649-705`. +- **Concrete scenario:** Two tagged outputs with a common classical key + appear under one address and Copy address yields a classical-only string. +- **Expected behavior:** Show the full protected descriptor per active + output and copy a valid descriptor, without relabeling historical outputs. +- **Regression required:** GUI exercise with two PQ programs under one + classical key, including group labels and clipboard content. Build the + Qt executable against depends Qt 5.12.11. +- **Remediation commit:** Pending. +- **Verification:** Pending. +- **Final status:** OPEN; release blocker for complete Qt asset support. + +### FINDING-155: Partial migration of a legacy asset UTXO needs explicit PQ change + +- **Severity:** MEDIUM migration availability. The wallet fails safely; + whole-UTXO migration remains possible. +- **Frozen initial status:** OPEN at merged commit + `7dc44845309ae36e33a55d3122e62603fcaed9dd`. +- **Affected RIP-25 invariant:** New asset change after activation must be + protected even when its input was a historical classical asset UTXO. +- **Affected Core 4.8.0 fix:** None; legacy asset outputs predate bit 13. +- **Root cause:** The wallet can infer protected change from an existing + tagged source, but a historical input has no tag. The transfer RPC accepts + an explicit asset change descriptor; the GUI does not expose that field. +- **Affected file/function/lines:** `src/wallet/wallet.cpp:4334-4387`; + `src/rpc/assets.cpp:1252-1285`; `src/qt/assetsdialog.cpp:897-955`. +- **Concrete scenario:** After activation, sending part of an old asset + holding in the GUI fails when it would create unprotected asset change. +- **Expected behavior:** Migrate an entire legacy UTXO to a protected + descriptor, or supply an explicit protected change descriptor through RPC. + Neither path may silently create classical-only asset change. +- **Regression required:** Show the partial GUI-equivalent path fails + without change, then show whole-UTXO migration and RPC partial migration + succeed with canonical tags. Confirm later spends require a funded anchor. +- **Remediation commit:** Pending documentation or UI design. +- **Verification:** Pending. +- **Final status:** OPEN; fail-safe migration limitation. From 9d24b8b143992ae0a758b0db2c56e5617a882edc Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:58:11 +0200 Subject: [PATCH 006/195] audit: freeze macOS runner loss [FINDING-156] --- ...0025-v4.8-security-remediation-register.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 116f571589..13bd7d6eab 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7855,3 +7855,32 @@ Cross-platform CI and public deployment remain separate release gates. - **Remediation commit:** Pending documentation or UI design. - **Verification:** Pending. - **Final status:** OPEN; fail-safe migration limitation. + +### FINDING-156: macOS release runner disconnects during the artifact build + +- **Severity:** MEDIUM release availability. The failed job produced no + macOS artifact at the tested SHA; no code or consensus defect is established. +- **Frozen initial status:** OPEN after Build Raven run `37888464222` at + `b8bc089f3e3d2b395279e23ec5fdd8e1a9036633`. +- **Affected RIP-25 invariant:** All release artifacts must be produced and + checked from the same SHA as a green Final Gate. +- **Affected Core 4.8.0 fix:** None known; the cause of runner loss is not yet + classified as product code or CI infrastructure. +- **Observed failure:** Security tests and the Linux, aarch64, arm32v7 and + Windows artifact jobs passed. The macOS job `113688195436` built pinned + dependencies and configured successfully, then its hosted runner lost + communication with GitHub during `make -j2`. GitHub left that step without + a conclusion, skipped packaging and emitted no macOS artifact. Its available + log ends before the build output, so it does not identify a compiler error. +- **Affected file/function/lines:** `.github/workflows/build-raven.yml:216-240`, + dependency and product build steps; no defective source line is established. +- **Expected behavior:** The macOS job must complete its pinned Qt build, + package an artifact and verify the embedded source SHA. A runner loss must + remain a failed qualification, not be treated as a code pass. +- **Regression required:** Rerun the failed macOS job at the exact same SHA. + If runner loss recurs, inspect resource use and reduce parallel build load + or otherwise correct the CI environment, then rerun. Final qualification + must repeat at the eventual release SHA. +- **Remediation commit:** Pending classification. +- **Verification:** Pending same-SHA rerun. +- **Final status:** OPEN. From 7a2502499786b2b5b2e75ef344f971679fdb953b Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:00:21 +0200 Subject: [PATCH 007/195] rpc: generate protected default asset destinations [FINDING-152] --- src/rpc/assets.cpp | 93 +++++++++---------- .../feature_pq_asset_wallet_ownership.py | 57 ++++++++++++ 2 files changed, 99 insertions(+), 51 deletions(-) diff --git a/src/rpc/assets.cpp b/src/rpc/assets.cpp index abafd15ebb..889cdd53c4 100644 --- a/src/rpc/assets.cpp +++ b/src/rpc/assets.cpp @@ -125,6 +125,45 @@ static std::string AssetSourceAddressForOutput(const COutput& output, pqAssetActivationHeight); } +static std::string NewAssetIssueDestination(CWallet* pwallet) +{ + const int activationHeight = GetPQAssetActivationHeightForPrev( + chainActive.Tip(), GetParams().GetConsensus()); + if (activationHeight >= 0 && chainActive.Height() + 1 >= activationHeight) { + pwallet->TopUpKeyPool(); + CReserveKey reserveClassicalKey(pwallet); + CPubKey classicalPubKey; + if (!reserveClassicalKey.GetReservedKey(classicalPubKey)) + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, + "Error: Keypool ran out, please call keypoolrefill first"); + CPQPubKey pqPubKey; + if (!pwallet->GenerateNewPQKey(pqPubKey)) + throw JSONRPCError(RPC_WALLET_ERROR, + "Error: Failed to derive and persist ML-DSA-44 keypair"); + const CKeyID classicalKey = classicalPubKey.GetID(); + const WitnessV2PQDestination pqDestination(pqPubKey.GetWitnessProgram()); + if (!pwallet->StoreOwnedPQAssetDestination(classicalKey, + pqDestination.witnessProgram)) + throw JSONRPCError(RPC_WALLET_ERROR, + "Error: Failed to persist protected asset destination pairing"); + reserveClassicalKey.KeepKey(); + pwallet->SetAddressBook(classicalKey, "", "receive"); + pwallet->SetAddressBook(pqDestination, "", "receive"); + return EncodePQAssetDestination(classicalKey, + pqDestination.witnessProgram); + } + + if (!pwallet->IsLocked()) + pwallet->TopUpKeyPool(); + CPubKey newKey; + if (!pwallet->GetKeyFromPool(newKey)) + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, + "Error: Keypool ran out, please call keypoolrefill first"); + const CKeyID keyID = newKey.GetID(); + pwallet->SetAddressBook(keyID, "", "receive"); + return EncodeDestination(keyID); +} + static void ResolveAdministrativeAssetReturn(CWallet* pwallet, const std::string& authorityName, std::string& changeAddress, CReserveKey& reservekey, CCoinControl& coinControl, bool useNativeChange) @@ -508,23 +547,7 @@ UniValue issue(const JSONRPCRequest& request) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Raven address: ") + address); } } else { - // Create a new address - std::string strAccount; - - if (!pwallet->IsLocked()) { - pwallet->TopUpKeyPool(); - } - - // Generate a new key that is added to wallet - CPubKey newKey; - if (!pwallet->GetKeyFromPool(newKey)) { - throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); - } - CKeyID keyID = newKey.GetID(); - - pwallet->SetAddressBook(keyID, strAccount, "receive"); - - address = EncodeDestination(keyID); + address = NewAssetIssueDestination(pwallet); } std::string change_address = ""; @@ -673,23 +696,7 @@ UniValue issueunique(const JSONRPCRequest& request) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Raven address: ") + address); } } else { - // Create a new address - std::string strAccount; - - if (!pwallet->IsLocked()) { - pwallet->TopUpKeyPool(); - } - - // Generate a new key that is added to wallet - CPubKey newKey; - if (!pwallet->GetKeyFromPool(newKey)) { - throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); - } - CKeyID keyID = newKey.GetID(); - - pwallet->SetAddressBook(keyID, strAccount, "receive"); - - address = EncodeDestination(keyID); + address = NewAssetIssueDestination(pwallet); } std::string changeAddress = ""; @@ -2481,23 +2488,7 @@ UniValue issuequalifierasset(const JSONRPCRequest& request) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Raven address: ") + address); } } else { - // Create a new address - std::string strAccount; - - if (!pwallet->IsLocked()) { - pwallet->TopUpKeyPool(); - } - - // Generate a new key that is added to wallet - CPubKey newKey; - if (!pwallet->GetKeyFromPool(newKey)) { - throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); - } - CKeyID keyID = newKey.GetID(); - - pwallet->SetAddressBook(keyID, strAccount, "receive"); - - address = EncodeDestination(keyID); + address = NewAssetIssueDestination(pwallet); } std::string change_address = ""; diff --git a/test/functional/feature_pq_asset_wallet_ownership.py b/test/functional/feature_pq_asset_wallet_ownership.py index 6045cc3e50..f295d619e0 100755 --- a/test/functional/feature_pq_asset_wallet_ownership.py +++ b/test/functional/feature_pq_asset_wallet_ownership.py @@ -7,6 +7,7 @@ from decimal import Decimal +from test_framework.authproxy import JSONRPCException from test_framework.mininode import CTransaction, from_hex, to_hex from test_framework.test_framework import RavenTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error @@ -57,9 +58,18 @@ def run_test(self): self.sync_all() sender.issue(asset_name, 1, sender_asset_address) sender.issue(historical_name, 1, sender_asset_address) + predefault_txid = sender.issue('PQPREDEFAULT', 1)[0] sender.generate(1) self.sync_all() assert_equal(sender.getblockcount(), 861) + assert_equal(sender.listpqassetaddresses()['addresses'], []) + assert_equal(sender.listmyassets('PQPREDEFAULT', True) + ['PQPREDEFAULT']['balance'], 1) + predefault_destinations = [entry['destination'] for entry in + sender.gettransaction(predefault_txid)['asset_details'] + if entry['asset_name'] == 'PQPREDEFAULT'] + assert_equal(len(predefault_destinations), 1) + assert '|' not in predefault_destinations[0] # A tag-shaped output created before the effective activation height # remains a legacy, classically owned asset even after activation. @@ -151,6 +161,53 @@ def run_test(self): assert_raises_rpc_error(-26, 'bad-pq-asset-anchor', recipient.sendrawtransaction, spend_signed['hex']) + # The post-activation defaults must create wallet-owned protected + # destinations, including for an issuance with a protected owner token. + parent_name = 'PQDEFAULTPARENT' + parent_destination = sender.getnewpqassetaddress() + sender.issue(parent_name, 1, parent_destination) + sender.generate(1) + self.sync_all() + sender.sendtoaddress(parent_destination.split('|')[1], Decimal('1')) + sender.generate(1) + self.sync_all() + + before = set(sender.listpqassetaddresses()['addresses']) + default_calls = ( + ('issue', 'PQDEFAULTROOT', lambda: sender.issue('PQDEFAULTROOT', 1)), + ('issueunique', parent_name + '#AUTO', + lambda: sender.issueunique(parent_name, ['AUTO'], None)), + ('issuequalifierasset', '#PQDEFAULTQUAL', + lambda: sender.issuequalifierasset('#PQDEFAULTQUAL', 1)), + ) + failures = [] + issued = [] + for label, name, call in default_calls: + try: + issued.append((name, call()[0])) + except JSONRPCException as error: + failures.append((label, error.error['message'])) + assert_equal(failures, []) + + sender.generate(1) + self.sync_all() + new_destinations = set(sender.listpqassetaddresses()['addresses']) - before + assert_equal(len(new_destinations), len(default_calls)) + received_destinations = set() + for name, txid in issued: + received = [entry['destination'] for entry in + sender.gettransaction(txid)['asset_details'] + if entry['category'] == 'receive' and + entry['asset_name'] == name] + assert_equal(len(received), 1) + assert received[0] in new_destinations + received_destinations.add(received[0]) + classical, pq = received[0].split('|') + assert_equal(sender.validateaddress(classical)['ismine'], True) + assert_equal(sender.validateaddress(pq)['ismine'], True) + assert_equal(sender.listmyassets(name, True)[name]['balance'], 1) + assert_equal(received_destinations, new_destinations) + if __name__ == '__main__': PQAssetWalletOwnershipTest().main() From 3424ddc5e5461bf8c3962faecbeff0d259a16c99 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:00:30 +0200 Subject: [PATCH 008/195] test: prove fail-safe anchor funding limits [FINDING-148][FINDING-150][FINDING-151] --- test/functional/feature_pq_asset_anchor.py | 229 +++++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/test/functional/feature_pq_asset_anchor.py b/test/functional/feature_pq_asset_anchor.py index b69aa5ee18..5c3266d0d2 100755 --- a/test/functional/feature_pq_asset_anchor.py +++ b/test/functional/feature_pq_asset_anchor.py @@ -183,6 +183,37 @@ def run_test(self): mined = node.generate(1) assert spend_txid in node.getblock(mined[0])['tx'] + # A raw spend can pay the exact fee from the anchor and return the + # protected asset without native PQ change. The next spend still + # needs a new matching anchor; the asset remains in the wallet. + spent_asset_index = self.output_index( + spend_signed['hex'], + lambda script: len(script) > 31 and script[25] == ASSET_OPCODE) + spent_anchor_index = self.output_index( + spend_signed['hex'], lambda script: script == pq_script) + exact_fee_raw = node.createrawtransaction( + [{'txid': spend_txid, 'vout': spent_asset_index}, + {'txid': spend_txid, 'vout': spent_anchor_index}], + {migration_descriptor: {'transfer': {asset_name: 1}}, + legacy_address: Decimal('0.8')}) + exact_fee_signed = node.signrawtransaction(exact_fee_raw) + assert_equal(exact_fee_signed['complete'], True) + exact_fee_tx = from_hex(CTransaction(), exact_fee_signed['hex']) + assert_equal(len(exact_fee_tx.vout), 2) + assert not any(output.scriptPubKey == pq_script + for output in exact_fee_tx.vout) + node.sendrawtransaction(exact_fee_signed['hex']) + node.generate(1) + assert_equal(node.listmyassets(asset_name, True)[asset_name]['balance'], 1) + assert_raises_rpc_error(-25, 'funded matching PQ anchor', + node.transferfromaddress, asset_name, + migration_descriptor, 1, migration_descriptor) + node.sendtoaddress(pq_address, Decimal('1')) + node.generate(1) + node.transferfromaddress(asset_name, migration_descriptor, 1, + migration_descriptor) + node.generate(1) + # Wallet issuance must tag both the root asset and its owner token. composite_address = node.getnewpqassetaddress() assert_equal(composite_address.count('|'), 1) @@ -828,6 +859,8 @@ def assert_raw_asset_programs(asset_object, expected): assert_equal(sorted(output.scriptPubKey[-32:] for output in split_tx.vout), sorted((descriptor_program, destination_program))) + self.test_anchor_availability_limits(node) + # A backed-up encrypted wallet must recover both halves of the # descriptor and sign a matching PQ anchor after a fresh restart. passphrase = 'regtest-only-pq-asset-passphrase' @@ -871,6 +904,202 @@ def assert_raw_asset_programs(asset_object, expected): assert_equal(node.listmyassets(qualifier_name, True) [qualifier_name]['balance'], 1) + def test_anchor_availability_limits(self, node): + assert_equal(node.lockunspent(True), True) + + # An ordinary RVN payment can select the only funded anchor. Asset + # custody persists and a payment to the same PQ address restores it. + drain_name = 'PQANCHORDRAIN' + drain_descriptor = node.getnewpqassetaddress() + _, drain_pq = drain_descriptor.split('|') + drain_script = bytes.fromhex( + node.validateaddress(drain_pq)['scriptPubKey']) + node.issue(drain_name, 2, drain_descriptor) + drain_anchor_txid = node.sendtoaddress(drain_pq, Decimal('1')) + node.generate(1) + drain_anchor = (drain_anchor_txid, self.output_index( + node.getrawtransaction(drain_anchor_txid), + lambda script: script == drain_script)) + other_native = [ + {'txid': coin['txid'], 'vout': coin['vout']} + for coin in node.listunspent() + if not coin.get('assetName') and + (coin['txid'], coin['vout']) != drain_anchor + ] + assert other_native + assert_equal(node.lockunspent(False, other_native), True) + drain_txid = node.sendtoaddress(node.getnewaddress(), Decimal('0.5')) + drain_tx = from_hex(CTransaction(), node.getrawtransaction(drain_txid)) + assert drain_anchor in self.input_outpoints(drain_tx) + assert not any(output.scriptPubKey == drain_script + for output in drain_tx.vout) + node.generate(1) + assert_equal(node.listmyassets(drain_name, True) + [drain_name]['balance'], 2) + assert_raises_rpc_error(-25, 'funded matching PQ anchor', + node.transferfromaddress, drain_name, + drain_descriptor, 1, drain_descriptor) + assert_equal(node.lockunspent(True), True) + node.sendtoaddress(drain_pq, Decimal('1')) + node.generate(1) + node.transferfromaddress(drain_name, drain_descriptor, 1, + drain_descriptor) + node.generate(1) + + # An explicit RVN change address is honored even when it consumes + # the sole anchor for an asset that is returned to its source pair. + explicit_name = 'PQEXPLICITCHG' + explicit_descriptor = node.getnewpqassetaddress() + _, explicit_pq = explicit_descriptor.split('|') + explicit_script = bytes.fromhex( + node.validateaddress(explicit_pq)['scriptPubKey']) + node.issue(explicit_name, 2, explicit_descriptor) + node.sendtoaddress(explicit_pq, Decimal('1')) + node.generate(1) + explicit_anchors = { + (coin['txid'], coin['vout']) for coin in node.listunspent() + if coin['scriptPubKey'] == explicit_script.hex() + } + assert_equal(len(explicit_anchors), 1) + native_change = node.getnewaddress() + native_change_script = bytes.fromhex( + node.validateaddress(native_change)['scriptPubKey']) + explicit_txid = node.transferfromaddress( + explicit_name, explicit_descriptor, 1, explicit_descriptor, + '', 0, native_change)[0] + explicit_tx = from_hex(CTransaction(), + node.getrawtransaction(explicit_txid)) + assert explicit_anchors <= self.input_outpoints(explicit_tx) + assert any(output.scriptPubKey == native_change_script + for output in explicit_tx.vout) + assert not any(output.scriptPubKey == explicit_script + for output in explicit_tx.vout) + node.generate(1) + assert_equal(node.listmyassets(explicit_name, True) + [explicit_name]['balance'], 2) + assert_raises_rpc_error(-25, 'funded matching PQ anchor', + node.transferfromaddress, explicit_name, + explicit_descriptor, 1, explicit_descriptor) + node.sendtoaddress(explicit_pq, Decimal('1')) + node.generate(1) + node.transferfromaddress(explicit_name, explicit_descriptor, 1, + explicit_descriptor) + node.generate(1) + + # Two protected source programs require two native anchor inputs. + # The single default RVN change output refreshes neither program. + multi_name = 'PQMULTIANCHOR' + multi_a = node.getnewpqassetaddress() + multi_b = node.getnewpqassetaddress() + _, multi_a_pq = multi_a.split('|') + _, multi_b_pq = multi_b.split('|') + multi_a_script = bytes.fromhex( + node.validateaddress(multi_a_pq)['scriptPubKey']) + multi_b_script = bytes.fromhex( + node.validateaddress(multi_b_pq)['scriptPubKey']) + node.issue(multi_name, 2, multi_a) + node.sendtoaddress(multi_a_pq, Decimal('1')) + node.generate(1) + node.transferfromaddress(multi_name, multi_a, 1, multi_b) + node.generate(1) + multi_a_anchors = [ + {'txid': coin['txid'], 'vout': coin['vout']} + for coin in node.listunspent() + if coin['scriptPubKey'] == multi_a_script.hex() + ] + assert multi_a_anchors + assert_equal(node.lockunspent(False, multi_a_anchors), True) + node.sendtoaddress(multi_b_pq, Decimal('1')) + node.generate(1) + assert_equal(node.lockunspent(True, multi_a_anchors), True) + multi_a_anchors = { + (coin['txid'], coin['vout']) for coin in node.listunspent() + if coin['scriptPubKey'] == multi_a_script.hex() + } + multi_b_anchors = { + (coin['txid'], coin['vout']) for coin in node.listunspent() + if coin['scriptPubKey'] == multi_b_script.hex() + } + assert multi_a_anchors + assert_equal(len(multi_b_anchors), 1) + multi_txid = node.transferfromaddresses( + multi_name, [multi_a, multi_b], 2, multi_a)[0] + multi_tx = from_hex(CTransaction(), node.getrawtransaction(multi_txid)) + multi_inputs = self.input_outpoints(multi_tx) + assert multi_inputs & multi_a_anchors + assert multi_inputs & multi_b_anchors + assert not any(output.scriptPubKey in (multi_a_script, + multi_b_script) + for output in multi_tx.vout) + node.generate(1) + assert_equal(node.listmyassets(multi_name, True) + [multi_name]['balance'], 2) + assert_raises_rpc_error(-25, 'funded matching PQ anchor', + node.transferfromaddress, multi_name, + multi_a, 1, multi_b) + node.sendtoaddress(multi_a_pq, Decimal('1')) + node.generate(1) + node.transferfromaddress(multi_name, multi_a, 1, multi_b) + node.generate(1) + + # Raw funding adds fee inputs but does not supply the protected + # asset's matching anchor. An explicit anchor and output back to the + # same PQ address allow the next protected spend. + raw_name = 'PQRAWFUND' + raw_descriptor = node.getnewpqassetaddress() + _, raw_pq = raw_descriptor.split('|') + raw_script = bytes.fromhex(node.validateaddress(raw_pq)['scriptPubKey']) + node.issue(raw_name, 1, raw_descriptor) + raw_anchor_txid = node.sendtoaddress(raw_pq, Decimal('1')) + fee_address = node.getnewaddress() + fee_script = bytes.fromhex( + node.validateaddress(fee_address)['scriptPubKey']) + fee_txid = node.sendtoaddress(fee_address, Decimal('2')) + node.generate(1) + raw_anchor = {'txid': raw_anchor_txid, 'vout': self.output_index( + node.getrawtransaction(raw_anchor_txid), + lambda script: script == raw_script)} + fee_input = {'txid': fee_txid, 'vout': self.output_index( + node.getrawtransaction(fee_txid), + lambda script: script == fee_script)} + asset_outpoint = node.listmyassets(raw_name, True) + asset_outpoint = asset_outpoint[raw_name]['outpoints'][0] + asset_input = {key: asset_outpoint[key] for key in ('txid', 'vout')} + raw_outputs = {raw_descriptor: {'transfer': {raw_name: 1}}} + raw_unanchored = node.createrawtransaction( + [asset_input, fee_input], raw_outputs) + raw_unanchored = node.fundrawtransaction(raw_unanchored)['hex'] + unanchored_tx = from_hex(CTransaction(), raw_unanchored) + assert (raw_anchor['txid'], raw_anchor['vout']) not in \ + self.input_outpoints(unanchored_tx) + raw_unanchored = node.signrawtransaction(raw_unanchored) + assert_equal(raw_unanchored['complete'], True) + assert_raises_rpc_error(-26, 'bad-pq-asset-anchor', + node.sendrawtransaction, raw_unanchored['hex']) + assert_equal(node.listmyassets(raw_name, True)[raw_name]['balance'], 1) + raw_replenished = node.createrawtransaction( + [asset_input, fee_input, raw_anchor], + {raw_descriptor: {'transfer': {raw_name: 1}}, + raw_pq: Decimal('0.9')}) + raw_replenished = node.fundrawtransaction(raw_replenished)['hex'] + raw_replenished = node.signrawtransaction(raw_replenished) + assert_equal(raw_replenished['complete'], True) + raw_txid = node.sendrawtransaction(raw_replenished['hex']) + raw_tx = from_hex(CTransaction(), node.getrawtransaction(raw_txid)) + assert (raw_anchor['txid'], raw_anchor['vout']) in \ + self.input_outpoints(raw_tx) + assert any(output.scriptPubKey == raw_script and + output.nValue == 90_000_000 for output in raw_tx.vout) + node.generate(1) + node.transferfromaddress(raw_name, raw_descriptor, 1, + raw_descriptor) + node.generate(1) + + @staticmethod + def input_outpoints(tx): + return {(format(txin.prevout.hash, '064x'), txin.prevout.n) + for txin in tx.vin} + if __name__ == '__main__': PQAssetAnchorTest().main() From 588ef7ec8962e3f4931dac65f2f82c89f68c2f09 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:00:36 +0200 Subject: [PATCH 009/195] test: cover asset index and legacy migration limits [FINDING-153][FINDING-155] --- .../functional/feature_pq_asset_source_rpc.py | 78 +++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/test/functional/feature_pq_asset_source_rpc.py b/test/functional/feature_pq_asset_source_rpc.py index 67eee3264f..aca6f16ff8 100755 --- a/test/functional/feature_pq_asset_source_rpc.py +++ b/test/functional/feature_pq_asset_source_rpc.py @@ -32,6 +32,13 @@ def assert_protected_transfer(node, txid, source_program, destination_program): assert any(len(witness.scriptWitness.stack) == 2 for witness in transaction.wit.vtxinwit) + @staticmethod + def asset_output_programs(node, txid): + transaction = from_hex(CTransaction(), node.getrawtransaction(txid)) + return [output.scriptPubKey[-32:] for output in transaction.vout + if len(output.scriptPubKey) > 57 and + output.scriptPubKey[25] == ASSET_OPCODE] + def run_test(self): node = self.nodes[0] assert_equal(node.listpqassetaddresses(), @@ -40,6 +47,7 @@ def run_test(self): node.generate(861) node.issue('PQSOURCELEGACY', 1, legacy_address) node.issue('PQSOURCELEGACY2', 1, legacy_address) + node.issue('PQSOURCEPARTIAL', 3, legacy_address) node.generate(2) assert_equal(node.getblockchaininfo()['bip9_softforks']['pq_assets']['status'], 'active') @@ -60,11 +68,40 @@ def run_test(self): assert_equal(set(node.listpqassetaddresses()['addresses']), {source, destination}) - # A confirmed legacy source must remain spendable after activation. - node.transferfromaddress('PQSOURCELEGACY', legacy_address, 1, - destination) - node.transferfromaddresses('PQSOURCELEGACY2', [legacy_address], 1, - destination) + # A confirmed legacy asset and its owner token can move in full to + # tagged outputs after activation without a PQ anchor on the old input. + migrated_asset = node.transferfromaddress( + 'PQSOURCELEGACY', legacy_address, 1, destination)[0] + migrated_second = node.transferfromaddresses( + 'PQSOURCELEGACY2', [legacy_address], 1, destination)[0] + migrated_owner = node.transfer( + 'PQSOURCELEGACY!', 1, destination)[0] + for txid in (migrated_asset, migrated_second, migrated_owner): + assert_equal(self.asset_output_programs(node, txid), + [destination_program]) + + # Sending part of one historical UTXO must not create classical-only + # change. The RPC can migrate the remainder with explicit PQ change. + assert_raises_rpc_error( + -25, 'Active PQ asset change requires a protected source or explicit PQ change destination', + node.transferfromaddress, 'PQSOURCEPARTIAL', legacy_address, 1, + destination) + partial_txid = node.transferfromaddress( + 'PQSOURCEPARTIAL', legacy_address, 1, destination, + '', 0, '', source)[0] + assert_equal(sorted(self.asset_output_programs(node, partial_txid)), + sorted([source_program, destination_program])) + node.generate(1) + assert_equal(node.listmyassets('PQSOURCEPARTIAL', True) + ['PQSOURCEPARTIAL']['balance'], 3) + assert_raises_rpc_error( + -25, 'funded matching PQ anchor', + node.transferfromaddress, 'PQSOURCEPARTIAL', source, 1, + destination, '', 0, '', source) + node.sendtoaddress(source_pq, Decimal('1')) + node.generate(1) + node.transferfromaddress('PQSOURCEPARTIAL', source, 1, + destination, '', 0, '', source) node.generate(1) node.issue('PQSOURCEACTIVE', 5, source) @@ -111,6 +148,37 @@ def run_test(self): assert_equal(node.listmyassets('PQSOURCEACTIVE', True) ['PQSOURCEACTIVE']['balance'], 5) + # The address index combines protected outputs that share a + # classical key, even though their PQ programs differ. Its address + # row is not a valid active asset destination. + second_pq = node.getnewpqaddress() + second_pair = source_classical + '|' + second_pq + second_program = bytes.fromhex( + node.validateaddress(second_pq)['scriptPubKey'])[2:] + assert second_program != source_program + index_asset = 'PQINDEXSHARED' + node.issue(index_asset, 2, source) + node.sendtoaddress(source_pq, Decimal('1')) + node.generate(1) + split_txid = node.transferfromaddress( + index_asset, source, 1, second_pair, '', 0, '', source)[0] + assert_equal(sorted(self.asset_output_programs(node, split_txid)), + sorted([source_program, second_program])) + node.generate(1) + assert_equal(node.listmyassets(index_asset, True) + [index_asset]['balance'], 2) + assert_equal(node.listaddressesbyasset(index_asset), + {source_classical: 2}) + assert_equal(node.listassetbalancesbyaddress(source_classical) + [index_asset], 2) + assert_raises_rpc_error(-5, 'Invalid Raven address', + node.listassetbalancesbyaddress, + second_pair) + assert_raises_rpc_error( + -5, 'canonical classical|PQ asset destination', + node.transferfromaddress, index_asset, source, 1, + source_classical) + if __name__ == '__main__': PQAssetSourceRPCTest().main() From 193d26483c572d6616d1226857fb1b8d470e22b0 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:00:41 +0200 Subject: [PATCH 010/195] docs: explain anchor funding and asset migration [FINDING-148][FINDING-150][FINDING-151][FINDING-153][FINDING-155] --- doc/RIP-0025-PQ-Assets-Followup.md | 97 +++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/doc/RIP-0025-PQ-Assets-Followup.md b/doc/RIP-0025-PQ-Assets-Followup.md index 8751a9dd03..f1416e1edd 100644 --- a/doc/RIP-0025-PQ-Assets-Followup.md +++ b/doc/RIP-0025-PQ-Assets-Followup.md @@ -130,11 +130,12 @@ the existing address-length assumptions. The descriptor is split before restricted-asset verifier or qualifier-index lookups: those indexes remain keyed by the classical address. The wallet selects a matching funded native PQ anchor and returns protected asset change or owner authority. For one -consumed program, ordinary positive native change refreshes the anchor at +consumed program, ordinary positive native change can refresh the anchor at the same PQ address. Exact-fee, dust, multiple-program, or explicit native -change can omit that refresh, so the holder must send native RVN to the -affected PQ address before another protected spend. Raw callers must -provide matching funded native anchor inputs themselves. The local +change can omit that refresh. Ordinary RVN payments can also consume an +anchor without spending the asset. In either case the holder must send native +RVN to the affected PQ address before another protected spend. Raw callers +must provide matching funded native anchor inputs themselves. The local full-chain suite covers representative operations for every named class, nontrivial restricted verification, encrypted backup recovery, and GUI compilation; it does not certify public deployment. @@ -155,7 +156,7 @@ All high-level and raw-transaction RPCs must construct only canonical activated ## 8. Asset Index and Database Compatibility -Asset indexes currently derive an address identity from legacy 20-byte data. A PQ program is 32 bytes. The follow-up design must version index keys or otherwise prevent truncation and type confusion. +Asset indexes currently derive an address identity from legacy 20-byte data. A PQ program is 32 bytes. The current candidate still indexes protected outputs under their classical address. `listaddressesbyasset` and `listassetbalancesbyaddress` therefore aggregate distinct PQ programs that share one classical key. Their classical address value is accounting data, not a complete protected destination. The follow-up design must version index keys or otherwise prevent truncation and type confusion. It must specify: @@ -171,7 +172,7 @@ Database readers must reject malformed or unknown key versions without interpret ## 9. Owner-Token Migration -Existing `ASSET!` outputs cannot become quantum-resistant merely because RIP-25 activates. A migration mechanism must prove current authorization while moving control to a PQ asset condition. Design choices include a normal legacy-authorized transfer into the new condition or a dedicated migration transaction type. +Existing `ASSET!` outputs cannot become quantum-resistant merely because RIP-25 activates. The current candidate permits a normal legacy-authorized transfer of a historical asset or owner-token UTXO into a new tagged output. The old input remains under its creation-height rules; the new output must meet the active bit-13 rule. A migration mechanism must prove current authorization while moving control to a PQ asset condition. The proposal must address: @@ -281,7 +282,7 @@ specification. The 4.8.1 working branch assigns BIP9 bit 13 but leaves its start and timeout equal and far in the future on every network, so default nodes do not activate it. Its tested consensus prototype uses the canonical parser and creation-height gate described below. Before public-network -activation it still needs complete anchor-refresh behavior, cross-platform +activation it still needs a documented, fail-safe anchor funding policy, cross-platform exact-SHA qualification, independent review, and an explicit migration and activation decision. The wallet now has tested protected owner returns, asset change, restricted-address checks, and representative all-class @@ -416,3 +417,85 @@ The default public-network bit-13 start and timeout values remain dormant. dust, explicit-change, and multiple-program cases may need a manual RVN top-up before another spend. Backup and verify both private keys and their descriptor association before moving administrative tokens. + +## 17. Current Anchor Funding Procedure + +This procedure documents FINDING-148, FINDING-150, and FINDING-151 while the +automatic refresh and raw-funding gaps remain open. A protected asset UTXO is +not lost when its native anchor is spent. Consensus still requires a matching +native witness-v2 input for its next spend, and the wallet rejects a protected +spend when it cannot find one. The owner can restore availability by sending +native RVN to the PQ address in the asset's original descriptor. Confirm that +payment, then retry the asset spend. Check every distinct PQ program involved +in a transaction. A payment to a different PQ address cannot authorize it. + +The following situations can leave no reusable anchor even when a protected +asset or owner token returns to the wallet: + +- an exact-fee or dust remainder with no native change output; +- a spend that consumes assets under multiple PQ programs, since one native + change output cannot refresh them all; +- an explicit native RVN change destination other than the relevant PQ + address; +- an ordinary RVN payment that selects the last funded asset anchor. + +For a raw protected asset spend, `fundrawtransaction` can add fee funding but +does not discover the required anchors from the asset inputs. Construct it +with these steps: + +1. Identify each protected asset outpoint being spent and record its committed + 32-byte PQ program and associated native witness-v2 address. +2. Select at least one funded native witness-v2 UTXO with the same program for + each distinct protected program. Include those outpoints explicitly in + the raw transaction's input list beside the asset outpoints. +3. Use a canonical `classical|PQ` descriptor for each new asset output. Add a + positive, spendable native RVN output back to every PQ address whose last + anchor is being consumed. Reserve enough RVN for fees and future spends. +4. If using `fundrawtransaction` for the remaining fee, inspect its resulting + inputs and outputs. It neither supplies a missing matching anchor nor + guarantees that its change replenishes one. Sign the complete transaction, + inspect it again, then broadcast it. An unanchored protected spend is + rejected with `bad-pq-asset-anchor`. + +## 18. Interpreting the Current Asset Index + +FINDING-153 is an address-index limitation. Two outputs can commit to different +PQ programs while using the same classical P2PKH key. The index reports one +classical-address row and their combined asset balance. The row cannot tell a +spender which PQ anchor or private key a particular outpoint needs. +`listassetbalancesbyaddress` accepts the classical address, not the composite +descriptor. Copying an index row as an active asset recipient fails because a +new output requires the canonical `classical|PQ` descriptor. Use the actual +output's program and the recorded descriptor for custody and transfers; +retain both keys and their association in backups. A descriptor assembled +manually from two owned keys is not necessarily listed by +`listpqassetaddresses`, which lists persisted wallet pairings. + +## 19. Migrating Historical Assets and Owner Tokens + +FINDING-155 concerns partial movement of a historical asset UTXO. Activation +does not add a PQ program to outputs created earlier. Their classical key can +still authorize a spend, but every spendable asset output newly created after +activation needs a canonical PQ tag. Migrate before the old classical key can +be compromised: + +1. Back up the wallet and verify that both keys in each destination descriptor + can be recovered. Inventory historical asset and `ASSET!` owner-token + outpoints, including their quantities and classical addresses. +2. For a single legacy asset UTXO, transfer its entire quantity to a new + canonical `classical|PQ` descriptor. Transfer an owner token's full unit + the same way. If a source address holds several UTXOs, use explicit + outpoint selection when exact UTXO control is needed. +3. For a partial transfer through `transferfromaddress` or + `transferfromaddresses`, supply the canonical descriptor in the final + `asset_change_address` argument as well as a protected recipient. Without + it, the wallet rejects the transaction rather than creating classical-only + asset change. The GUI path has no equivalent explicit asset-change field. +4. Verify that every newly created spendable asset and owner-token output has + its intended PQ program. Fund each destination's native PQ address with + RVN before its first protected spend. Keep the original descriptor and + both private keys available for recovery. + +No software can distinguish the rightful owner from an attacker who already +controls the old classical key. Migration does not retroactively protect the +historical UTXO, and a compromised holder can be raced before confirmation. From 205c673cbd6f249be6c65282c9057713b8696bb4 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:01:44 +0200 Subject: [PATCH 011/195] audit: record asset regression evidence [FINDING-148][FINDING-149][FINDING-150][FINDING-151][FINDING-152][FINDING-153][FINDING-155] --- ...0025-v4.8-security-remediation-register.md | 71 +++++++++++++------ 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 13bd7d6eab..b1e09580e2 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -5399,8 +5399,13 @@ were committed. They do not change the frozen status of older findings. explicit change. Document the current manual top-up procedure until then. - **Regression required:** Exact-fee, dust, two-program, and explicit-change spends, followed by another protected spend without external funding. -- **Final status:** OPEN; non-release-blocking wallet availability limitation, - but a condition for a complete seamless asset wallet. +- **Verification:** `3424ddc5e` adds functional exact-fee, two-program and + explicit-change cases. Each retains wallet asset custody, rejects the next + unanchored protected spend, then succeeds after a matching RVN top-up. The + focused `feature_pq_asset_anchor.py` passed locally with Python 3.10. + Dust behavior still needs a separate boundary vector. The manual recovery + procedure is documented by `193d26483`. +- **Final status:** OPEN; documented fail-safe wallet availability limitation. ### FINDING-142: Pinned native ccache source is unavailable to exact-SHA CI @@ -7688,9 +7693,14 @@ Cross-platform CI and public deployment remain separate release gates. regression. Rerun the transfer recovery test after the bounded wait and confirm the complete Final Gate passes at the same source SHA as the cross-platform Build Raven artifacts. Keep `make check` mandatory. -- **Remediation commit:** Pending. -- **Verification:** Pending. -- **Final status:** OPEN. +- **Remediation commit:** `8d4e86d4b` adds a bounded wait for the exact tip. +- **Verification:** The focused crash-recovery test passed locally. Final + Gate run `37888456288` at + `b8bc089f3e3d2b395279e23ec5fdd8e1a9036633` passed its required + functional tests, mandatory `make check`, fuzz smoke, invariants and all + seven matrix builds. The paired Build Raven run had a separate macOS runner + failure recorded as FINDING-156. Final release-SHA rerun remains due. +- **Final status:** FIXED; final release-SHA qualification pending. ### FINDING-150: Ordinary RVN coin selection can consume a protected asset anchor @@ -7718,9 +7728,13 @@ Cross-platform CI and public deployment remain separate release gates. issue an ordinary RVN payment that selects the anchor, verify the later asset spend fails for lack of a matching funded anchor, then top up that PQ address and verify spending recovers. Confirm no consensus bypass. -- **Remediation commit:** Pending design. -- **Verification:** Pending. -- **Final status:** OPEN; documented availability limitation. +- **Remediation commit:** None; automatic last-anchor reservation remains a + wallet design task. `193d26483` documents manual recovery. +- **Verification:** `3424ddc5e` forces ordinary RVN coin selection to + consume the sole matching anchor. The asset balance remains in the wallet, + a subsequent protected spend fails, and a matching PQ top-up restores it. + The focused functional test passed locally. +- **Final status:** OPEN; documented fail-safe availability limitation. ### FINDING-151: Raw transaction funding does not select asset PQ anchors @@ -7747,9 +7761,13 @@ Cross-platform CI and public deployment remain separate release gates. - **Regression required:** Demonstrate rejection of a funded raw protected transfer lacking the matching anchor, then acceptance after adding the anchor explicitly and preserving its PQ destination for change. -- **Remediation commit:** Pending design. -- **Verification:** Pending. -- **Final status:** OPEN; documented manual construction path pending tests. +- **Remediation commit:** None; automatic raw anchor selection remains a + wallet design task. `193d26483` documents the explicit construction path. +- **Verification:** `3424ddc5e` proves that generic raw funding omits the + matching anchor and consensus rejects the signed transaction. Supplying + the anchor and an RVN return output to its PQ address succeeds, and a + later protected spend remains possible. The focused test passed locally. +- **Final status:** OPEN; documented fail-safe manual construction path. ## Findings frozen by the independent merged asset-phase review @@ -7775,9 +7793,14 @@ Cross-platform CI and public deployment remain separate release gates. - **Regression required:** Exercise all three default-address RPCs before and after activation. Confirm their new asset and owner-token outputs are tagged, wallet-owned, and spendable with a matching funded anchor. -- **Remediation commit:** Pending. -- **Verification:** Pending. -- **Final status:** OPEN; release blocker for active bit 13. +- **Remediation commit:** `7a2502499` generates and persists a wallet-owned + composite destination for active default issuance calls. +- **Verification:** The new functional assertions failed before the code + change for all three RPCs with the canonical-destination error. After the + change, `feature_pq_asset_wallet_ownership.py` passes locally and checks + preactivation classical behavior and postactivation owned PQ pairs. + Exact-SHA CI remains due. +- **Final status:** FIXED locally; exact-SHA CI pending. ### FINDING-153: Asset index RPC aggregates distinct PQ programs by classical address @@ -7802,9 +7825,13 @@ Cross-platform CI and public deployment remain separate release gates. then compare the aggregate RPC result against output tags and wallet history; prove copying the aggregate address fails safely as a protected recipient. -- **Remediation commit:** Pending design. -- **Verification:** Pending. -- **Final status:** OPEN; fail-safe index limitation. +- **Remediation commit:** None; descriptor-aware index design remains open. + `193d26483` documents the legacy aggregate semantics. +- **Verification:** `588ef7ec8` exercises two PQ programs sharing a + classical key, checks the aggregated RPC balance, and proves that the + classical-only row fails safely as an active recipient. The focused source + RPC functional test passed locally. +- **Final status:** OPEN; documented fail-safe index limitation. ### FINDING-154: Qt asset control displays and copies a classical-only address @@ -7852,9 +7879,13 @@ Cross-platform CI and public deployment remain separate release gates. - **Regression required:** Show the partial GUI-equivalent path fails without change, then show whole-UTXO migration and RPC partial migration succeed with canonical tags. Confirm later spends require a funded anchor. -- **Remediation commit:** Pending documentation or UI design. -- **Verification:** Pending. -- **Final status:** OPEN; fail-safe migration limitation. +- **Remediation commit:** No GUI change; `193d26483` documents whole-UTXO + migration and the explicit asset-change RPC path. +- **Verification:** `588ef7ec8` proves that a partial legacy spend without + protected change fails, while whole-UTXO asset and owner-token migration + and explicit PQ change succeed. A later protected spend needs and accepts + a funded matching anchor. The focused source RPC test passed locally. +- **Final status:** OPEN; documented fail-safe GUI migration limitation. ### FINDING-156: macOS release runner disconnects during the artifact build From 879a818c6d7b15afd6bca0f7f9ddee08a7295962 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:08:10 +0200 Subject: [PATCH 012/195] qt: show contextual protected asset destinations [FINDING-154] --- src/qt/assetcontroldialog.cpp | 43 +++++++++++++++++++++-------------- src/qt/walletmodel.cpp | 28 +++++++++++++++-------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/qt/assetcontroldialog.cpp b/src/qt/assetcontroldialog.cpp index 9cad6221c1..bbfc78ae67 100644 --- a/src/qt/assetcontroldialog.cpp +++ b/src/qt/assetcontroldialog.cpp @@ -650,7 +650,8 @@ void AssetControlDialog::updateView() CAssetControlWidgetItem *itemWalletAddress = new CAssetControlWidgetItem(); itemWalletAddress->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); QString sWalletAddress = coins.first; - QString sWalletLabel = model->getAddressTableModel()->labelForAddress(sWalletAddress); + QString sWalletLabel = model->getAddressTableModel()->labelForAddress( + GUIUtil::classicalAssetAddress(sWalletAddress)); if (sWalletLabel.isEmpty()) sWalletLabel = tr("(no label)"); @@ -691,29 +692,37 @@ void AssetControlDialog::updateView() itemOutput->setFlags(flgCheckbox); itemOutput->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); - // address - CTxDestination outputAddress; - QString sAddress = ""; - if (ExtractDestination(out.tx->tx->vout[out.i].scriptPubKey, outputAddress)) { - sAddress = QString::fromStdString(EncodeDestination(outputAddress)); - - // if listMode or change => show raven address. In tree mode, address is not shown again for direct wallet address outputs - if (!treeMode || (!(sAddress == sWalletAddress))) { - itemOutput->setText(COLUMN_ADDRESS, sAddress); - // asset name - itemOutput->setText(COLUMN_ASSET_NAME, QString::fromStdString(strAssetName)); - } + // The group key comes from this output's activation context and + // includes its PQ program when the output is protected. + const QString sAddress = sWalletAddress; + if (!treeMode) { + itemOutput->setText(COLUMN_ADDRESS, sAddress); + itemOutput->setText(COLUMN_ASSET_NAME, QString::fromStdString(strAssetName)); + } + + QString sourceAddress; + { + LOCK2(cs_main, model->getWallet()->cs_wallet); + CTxDestination sourceDestination; + if (ExtractDestination(model->getWallet()->FindNonChangeParentOutput( + *out.tx->tx, out.i).scriptPubKey, sourceDestination)) + sourceAddress = QString::fromStdString(EncodeDestination(sourceDestination)); } + const bool isChange = !sourceAddress.isEmpty() && + sourceAddress != GUIUtil::classicalAssetAddress(sAddress); // label - if (!(sAddress == sWalletAddress)) // change + if (isChange) { - // tooltip from where the change comes from + QString sourceLabel = model->getAddressTableModel()->labelForAddress(sourceAddress); + if (sourceLabel.isEmpty()) + sourceLabel = tr("(no label)"); itemOutput->setToolTip(COLUMN_LABEL, - tr("change from %1 (%2)").arg(sWalletLabel).arg(sWalletAddress)); + tr("change from %1 (%2)").arg(sourceLabel).arg(sourceAddress)); itemOutput->setText(COLUMN_LABEL, tr("(change)")); } else if (!treeMode) { - QString sLabel = model->getAddressTableModel()->labelForAddress(sAddress); + QString sLabel = model->getAddressTableModel()->labelForAddress( + GUIUtil::classicalAssetAddress(sAddress)); if (sLabel.isEmpty()) sLabel = tr("(no label)"); itemOutput->setText(COLUMN_LABEL, sLabel); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index a32f154133..5bf217fe65 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -6,6 +6,8 @@ #include "walletmodel.h" #include "addresstablemodel.h" +#include "assets/assets.h" +#include "chainparams.h" #include "consensus/validation.h" #include "guiconstants.h" #include "guiutil.h" @@ -733,17 +735,16 @@ void WalletModel::listCoins(std::map >& mapCoins) } /** RVN START */ -// AvailableCoins + LockedCoins grouped by wallet address (put change in one group with wallet address) +// Available and locked asset coins grouped by the output's contextual destination. void WalletModel::listAssets(std::map > >& mapCoins) const { - std::map > > mapSortedByAssetName; - auto list = wallet->ListAssets(); - - for (auto& group : list) { - auto address = QString::fromStdString(EncodeDestination(group.first)); + LOCK2(cs_main, wallet->cs_wallet); + const int pqAssetActivationHeight = GetPQAssetActivationHeightForPrev( + chainActive.Tip(), GetParams().GetConsensus()); + for (auto& group : wallet->ListAssets()) { for (auto& coin : group.second) { - auto out = coin.tx->tx->vout[coin.i]; + const CTxOut& out = coin.tx->tx->vout[coin.i]; std::string strAssetName; CAmount nAmount; if (!GetAssetInfoFromScript(out.scriptPubKey, strAssetName, nAmount)) @@ -752,9 +753,16 @@ void WalletModel::listAssets(std::mapGetDepthInMainChain(originBlock); + const int originHeight = depth > 0 && originBlock ? originBlock->nHeight : -1; + const std::string address = EncodeContextualAssetDestination( + out.scriptPubKey, originHeight, pqAssetActivationHeight); + if (address.empty()) + continue; + + mapCoins[QString::fromStdString(strAssetName)] + [QString::fromStdString(address)].emplace_back(coin); } } } From 870a0bee36f4ec4b4c52f1c0e238df83834025e2 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:08:23 +0200 Subject: [PATCH 013/195] audit: track Qt verification gate [FINDING-154] --- doc/RIP-0025-v4.8-security-remediation-register.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index b1e09580e2..48de33d48d 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7853,9 +7853,12 @@ Cross-platform CI and public deployment remain separate release gates. - **Regression required:** GUI exercise with two PQ programs under one classical key, including group labels and clipboard content. Build the Qt executable against depends Qt 5.12.11. -- **Remediation commit:** Pending. -- **Verification:** Pending. -- **Final status:** OPEN; release blocker for complete Qt asset support. +- **Remediation commit:** `879a818c6` groups coin-control outputs by their + contextual descriptor and copies the complete protected destination. +- **Verification:** The existing contextual formatter unit test covers + historical and active output heights. The pinned Qt 5.12.11 executable + build and interactive list, tree and clipboard checks remain pending. +- **Final status:** OPEN; implementation awaits pinned build and GUI check. ### FINDING-155: Partial migration of a legacy asset UTXO needs explicit PQ change From d64ed5a81e5ac068b49685bc46c7ccf2e4b65992 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:24:35 +0200 Subject: [PATCH 014/195] test: prove exact-fee anchor recovery [FINDING-148] --- test/functional/feature_pq_asset_anchor.py | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/functional/feature_pq_asset_anchor.py b/test/functional/feature_pq_asset_anchor.py index 5c3266d0d2..77f71b55aa 100755 --- a/test/functional/feature_pq_asset_anchor.py +++ b/test/functional/feature_pq_asset_anchor.py @@ -1095,6 +1095,44 @@ def test_anchor_availability_limits(self, node): raw_descriptor) node.generate(1) + # With no native output, the matching anchor is consumed entirely + # as a fee. The asset remains controlled and a fresh PQ payment + # restores its next spend. + exact_name = 'PQEXACTFEE' + exact_descriptor = node.getnewpqassetaddress() + _, exact_pq = exact_descriptor.split('|') + exact_script = bytes.fromhex( + node.validateaddress(exact_pq)['scriptPubKey']) + node.issue(exact_name, 1, exact_descriptor) + exact_anchor_txid = node.sendtoaddress(exact_pq, Decimal('0.01')) + node.generate(1) + exact_anchor = {'txid': exact_anchor_txid, 'vout': self.output_index( + node.getrawtransaction(exact_anchor_txid), + lambda script: script == exact_script)} + exact_outpoint = node.listmyassets(exact_name, True) + exact_outpoint = exact_outpoint[exact_name]['outpoints'][0] + exact_input = {key: exact_outpoint[key] for key in ('txid', 'vout')} + exact_raw = node.createrawtransaction( + [exact_input, exact_anchor], + {exact_descriptor: {'transfer': {exact_name: 1}}}) + exact_signed = node.signrawtransaction(exact_raw) + assert_equal(exact_signed['complete'], True) + exact_txid = node.sendrawtransaction(exact_signed['hex']) + exact_tx = from_hex(CTransaction(), node.getrawtransaction(exact_txid)) + assert_equal(len(exact_tx.vout), 1) + assert_equal(exact_tx.vout[0].nValue, 0) + node.generate(1) + assert_equal(node.listmyassets(exact_name, True) + [exact_name]['balance'], 1) + assert_raises_rpc_error(-25, 'funded matching PQ anchor', + node.transferfromaddress, exact_name, + exact_descriptor, 1, exact_descriptor) + node.sendtoaddress(exact_pq, Decimal('0.01')) + node.generate(1) + node.transferfromaddress(exact_name, exact_descriptor, 1, + exact_descriptor) + node.generate(1) + @staticmethod def input_outpoints(tx): return {(format(txin.prevout.hash, '064x'), txin.prevout.n) From 09220eccf25c0488e84ba301326f0710c1b06b92 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:25:07 +0200 Subject: [PATCH 015/195] audit: retain asset release blockers [FINDING-067] --- ...0025-v4.8-security-remediation-register.md | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 48de33d48d..bc8f3a002b 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4100,9 +4100,9 @@ documentation, policy, wallet, or consensus change. explicit PQ asset change rejects, raw native-RVN PQ output construction remains available, and PQ native-RVN change is separated from automatically generated P2PKH asset change. Structural invariant lint passed. -- **Final status:** MITIGATED. The false security claim and unsafe construction - behavior are corrected. Asset ownership itself remains deliberately outside - RIP-25 and requires the separately specified follow-up consensus extension. +- **Final status:** OPEN for the requested asset-inclusive 4.8.1 release. + The earlier native-RVN-only scope corrected the false security claim and + unsafe construction behavior, but did not protect asset ownership. **4.8.1 continuation at `1416e69ea20e636cef46c663bba421a912c9f0e1`:** The owner subsequently required an asset-inclusive RIP-25 extension and @@ -4345,6 +4345,35 @@ documented limitations, not a consensus bypass. FINDING-067 remains OPEN for public activation, cross-platform CI, exact-SHA artifact qualification, and independent review; no 4.8.1 asset-protection release is certified here. +**2026-10-09 asset release checkpoint at 870a0bee3:** Independent review of +bit-13 consensus, wallet custody and recovery, raw RPCs, and Qt is complete. +FINDING-148 and FINDING-150 through FINDING-155 were frozen before their +respective remedies or documented limitations. FINDING-067 remains OPEN +until all of these release conditions hold: + +1. Final Gate, including mandatory make check, and all five Build Raven + artifacts pass at one exact final release SHA. The macOS runner loss in + FINDING-156 must be resolved by a successful artifact build at that SHA. +2. FINDING-143 through FINDING-147 receive cross-platform CI evidence on + that SHA. FINDING-154 needs an interactive pinned-Qt list, tree, and + clipboard regression with two distinct PQ programs sharing one + classical key; the local pinned Qt build alone is insufficient. +3. The owner approves the bit-13 mainnet and testnet BIP9 parameters after + checking the bit-12 prerequisite and historical testnet PQ signatures. + The public-network parameters remain dormant in this checkpoint. +4. The legacy asset and owner-token migration instructions remain explicit. + Exact-fee, multiple-program, explicit-native-change, generic-native-spend, + raw-funding, index, and partial-GUI migration limits remain identified + with fail-safe regression evidence and a documented recovery route. +5. The owner resolves the intended meaning of "only post-quantum keys + protect assets": this candidate requires both a classical asset + signature and an ML-DSA anchor for new protected outputs, while + historical outputs retain classical spending until migrated. A strict + PQ-only signature rule would need a further consensus change. + +The 4.8.1 version bump, final-SHA rerun, tag, and GitHub release are +separate release steps. No tag or release is authorized by this checkpoint. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical From 5c90bf0e8931caee445904f064530915752525c3 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:25:12 +0200 Subject: [PATCH 016/195] audit: correct exact-fee regression evidence [FINDING-148] --- doc/RIP-0025-v4.8-security-remediation-register.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index bc8f3a002b..fa37faa754 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -5428,12 +5428,13 @@ were committed. They do not change the frozen status of older findings. explicit change. Document the current manual top-up procedure until then. - **Regression required:** Exact-fee, dust, two-program, and explicit-change spends, followed by another protected spend without external funding. -- **Verification:** `3424ddc5e` adds functional exact-fee, two-program and - explicit-change cases. Each retains wallet asset custody, rejects the next - unanchored protected spend, then succeeds after a matching RVN top-up. The - focused `feature_pq_asset_anchor.py` passed locally with Python 3.10. - Dust behavior still needs a separate boundary vector. The manual recovery - procedure is documented by `193d26483`. +- **Verification:** `3424ddc5e` adds the two-program and explicit-change + cases. `d64ed5a81` adds an exact-fee raw spend with no native return + output. Each retains wallet asset custody, rejects the next unanchored + protected spend, then succeeds after a matching RVN top-up. The focused + `feature_pq_asset_anchor.py` passed locally with Python 3.10 after + `d64ed5a81`. Dust behavior still needs a separate boundary vector. + The manual recovery procedure is documented by `193d26483`. - **Final status:** OPEN; documented fail-safe wallet availability limitation. ### FINDING-142: Pinned native ccache source is unavailable to exact-SHA CI From 0a100e77c9d33357173150ae349bdde7a918a049 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:25:16 +0200 Subject: [PATCH 017/195] audit: record pinned Qt build [FINDING-154] --- doc/RIP-0025-v4.8-security-remediation-register.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index fa37faa754..d46cbd0c40 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7886,9 +7886,16 @@ Cross-platform CI and public deployment remain separate release gates. - **Remediation commit:** `879a818c6` groups coin-control outputs by their contextual descriptor and copies the complete protected destination. - **Verification:** The existing contextual formatter unit test covers - historical and active output heights. The pinned Qt 5.12.11 executable - build and interactive list, tree and clipboard checks remain pending. -- **Final status:** OPEN; implementation awaits pinned build and GUI check. + historical and active output heights. At 870a0bee3, the Linux + x86_64-pc-linux-gnu depends prefix reports Qt 5.12.11 in + Qt5Core.pc; make -j4, make check, and raven-qt -version pass with + that prefix. The local GCC 13 build needed -Wno-error=array-bounds + while compiling the pinned libxkbcommon dependency; the Qt recipe + and version were not changed. Interactive list, tree, and clipboard + checks for two PQ programs sharing one classical key remain pending. + This compilation alone does not prove GUI destination behavior. +- **Final status:** OPEN; pinned Qt build passes locally, but the + interactive GUI regression and final-SHA CI remain pending. ### FINDING-155: Partial migration of a legacy asset UTXO needs explicit PQ change From e0893a6b6742dde813cc64fc89d96a4d35aa5755 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:30:20 +0200 Subject: [PATCH 018/195] audit: record activation proposal approval [FINDING-067] --- doc/RIP-0025-v4.8-security-remediation-register.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index d46cbd0c40..15981e3784 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4374,6 +4374,18 @@ until all of these release conditions hold: The 4.8.1 version bump, final-SHA rerun, tag, and GitHub release are separate release steps. No tag or release is authorized by this checkpoint. +**2026-10-09 parameter decision:** The owner approved a bit-13 proposal, +conditional on CI qualification: testnet starts at 1806537600 and times +out at 1853971200 with threshold 1310/2016; mainnet starts at +1861920000 and times out at 1909094400 with threshold 1714/2016. +The owner also approved extending the proposed mainnet bit-12 timeout +from 1830297600 to 1846022400, which makes the documented window +18 months after its 1798761600 start. These are approval records, +not committed consensus parameters. Historical testnet PQ-context +compatibility still needs evidence. The owner requested discussion +before deciding whether the dual-signature candidate meets the asset +release objective, so FINDING-067 remains OPEN. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical From 73dfc7f8aae57577c48df232667fff47a38513b1 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:36:22 +0200 Subject: [PATCH 019/195] audit: freeze legacy asset sunset gap [FINDING-157] --- ...0025-v4.8-security-remediation-register.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 15981e3784..070b6c80df 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7967,3 +7967,49 @@ Cross-platform CI and public deployment remain separate release gates. - **Remediation commit:** Pending classification. - **Verification:** Pending same-SHA rerun. - **Final status:** OPEN. + +### FINDING-157: Historical asset spends remain classical after bit 13 + +- **Severity:** HIGH for the newly requested legacy-signature sunset. +- **Frozen initial status:** OPEN on 2026-10-09 at e0893a6b6, before any + sunset design or consensus change. +- **Affected RIP-25 invariant:** After a separately defined sunset point, + no historical asset or owner-token UTXO may be spendable with a + classical signature alone. +- **Affected Core 4.8.0 fix:** None. Core 4.8.0 had no PQ asset rule. +- **Root cause:** The bit-13 input rule requires a matching native PQ + anchor only for asset coins created at or after the effective bit-13 + activation height. Historical asset scripts commit to a classical + P2PKH key but contain no PQ program. The current migration path spends + them classically to create a tagged output. +- **Affected file/function/lines:** src/consensus/tx_verify.cpp:626-676, + especially the origin-height gate at 661-667; + src/assets/assets.cpp:3632-3707, canonical program extraction; + src/wallet/wallet.cpp:2998-3007,4257-4267, historical-input selection. +- **Concrete scenario:** A historical asset or ASSET! token remains + spendable by its classical key after bit 13 activates. A later blanket + ban would make every unmigrated holding and its owner authority + unspendable unless a separate authenticated recovery rule exists. + Adding an arbitrary PQ anchor at spend time would not prove ownership + because an attacker could supply an anchor under its own PQ key. +- **Expected behavior:** Choose and specify a sunset deployment and an + explicit fate for unmigrated asset and owner-token UTXOs. If recovery + remains possible after sunset, its PQ authorization must be bound to + the historical owner before the classical-only path closes. +- **Regression required:** Prove ordinary and owner-token migration + before sunset; reject a classical-only historical spend after sunset + in mempool and blocks; cover the activation boundary, reorgs, restart, + assumevalid, and any authorized recovery path. Wallet and GUI balances + must not imply that frozen UTXOs are spendable. +- **Remediation commit:** None; sunset policy and timing are under + discussion with the owner. The previously approved bit-13 parameters + do not authorize a legacy sunset rule. +- **Verification:** The existing unit vector at + src/test/assets/asset_tx_tests.cpp:839-872 proves the current + origin-height exemption. The functional migration vector at + test/functional/feature_pq_asset_source_rpc.py:71-93 proves that + asset and owner-token migration currently depend on a classical + historical spend. No sunset regression exists yet. +- **Final status:** OPEN; blocks the requested global asset end state + until an authenticated recovery or explicit freeze policy is approved + and independently qualified. From e9e7ba6debfb0e5089234bc527416a5c9a94554a Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:01:38 +0200 Subject: [PATCH 020/195] audit: freeze PQ-only asset ownership gap [FINDING-158] --- ...0025-v4.8-security-remediation-register.md | 70 ++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 070b6c80df..0087812c58 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4365,11 +4365,11 @@ until all of these release conditions hold: Exact-fee, multiple-program, explicit-native-change, generic-native-spend, raw-funding, index, and partial-GUI migration limits remain identified with fail-safe regression evidence and a documented recovery route. -5. The owner resolves the intended meaning of "only post-quantum keys - protect assets": this candidate requires both a classical asset - signature and an ML-DSA anchor for new protected outputs, while - historical outputs retain classical spending until migrated. A strict - PQ-only signature rule would need a further consensus change. +5. FINDING-158 is resolved with a qualified PQ-only spending rule for new + asset and owner-token outputs. The current candidate requires both a + classical asset signature and an ML-DSA anchor, so it does not meet the + owner's confirmed rule. Historical outputs retain classical spending + and migrate voluntarily, as recorded below. The 4.8.1 version bump, final-SHA rerun, tag, and GitHub release are separate release steps. No tag or release is authorized by this checkpoint. @@ -4386,6 +4386,18 @@ compatibility still needs evidence. The owner requested discussion before deciding whether the dual-signature candidate meets the asset release objective, so FINDING-067 remains OPEN. +**2026-10-09 asset ownership decision:** The owner confirmed that the asset +rule must follow the native RVN migration model: new asset and owner-token +outputs must be spendable with ML-DSA alone, without a classical asset +signature. Historical asset and owner-token outputs remain spendable with +their existing classical keys and may be migrated voluntarily. There is no +legacy-signature sunset in the 4.8.1 scope. This supersedes the earlier +sunset request in FINDING-157 and exposes the separate candidate defect in +FINDING-158. The previously approved bit-13 dates and thresholds applied +to the dual-signature candidate; do not activate a redesigned PQ-only rule +until its compatibility, regressions, and deployment parameters are reviewed. +FINDING-067 remains OPEN. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical @@ -8010,6 +8022,48 @@ Cross-platform CI and public deployment remain separate release gates. test/functional/feature_pq_asset_source_rpc.py:71-93 proves that asset and owner-token migration currently depend on a classical historical spend. No sunset regression exists yet. -- **Final status:** OPEN; blocks the requested global asset end state - until an authenticated recovery or explicit freeze policy is approved - and independently qualified. +- **Final status:** SCOPE SUPERSEDED on 2026-10-09. The owner withdrew the + requested sunset and chose voluntary migration, as for native RVN. + Historical classical spending remains a documented property, not a fix. + +### FINDING-158: New protected asset outputs still require a classical signature + +- **Severity:** HIGH release requirement mismatch. The existing dual-key + rule still requires ML-DSA, but it does not provide PQ-only ownership. +- **Frozen initial status:** OPEN on 2026-10-09 at 73dfc7f8a, before any + redesign of asset scripts, consensus, wallet, RPCs, or Qt. +- **Affected RIP-25 invariant:** After bit 13, new asset and owner-token + outputs must be spendable with ML-DSA alone. Historical outputs retain + their classical spending rule until their owners migrate them. +- **Affected Core 4.8.0 fix:** None. Its asset ownership format was P2PKH. +- **Root cause:** The candidate asset envelope retains a P2PKH ownership + prefix and appends a 32-byte PQ program. Its input rule additionally + requires a matching native witness-v2 anchor, but the asset script itself + still executes ECDSA. The wallet similarly requires both keys. +- **Affected file/function/lines:** src/script/script.cpp:245-281, + src/script/standard.cpp:75-83, src/script/sign.cpp:90-123, + src/assets/assets.cpp:3632-3707, src/consensus/tx_verify.cpp:626-676, + src/wallet/wallet.cpp:1941-1951,2998-3007. +- **Concrete scenario:** A holder of a new tagged asset or ASSET! output + cannot spend it with the ML-DSA key alone. Loss of its classical key + prevents spending even when the PQ key is available. This contradicts + the confirmed 4.8.1 ownership rule; it is not evidence of an ML-DSA + bypass in the present dual-key rule. +- **Expected behavior:** Specify and implement a consensus-safe PQ-only + asset output and spend format for all asset classes and owner authority. + Preserve the authenticated classical spend of historical outputs for + voluntary migration, without a sunset. Prove old-node validity and + activation behavior before selecting a deployment path. +- **Regression required:** Before the fix, demonstrate that a new asset + and owner-token output cannot be spent with ML-DSA alone. After the fix, + prove ML-DSA-only spending and rejection of missing or mismatched PQ + signatures in mempool and blocks. Cover historical migration, partial + change, issuance, reissuance, restricted and qualifier administration, + raw RPCs, wallet backup and recovery, Qt, reorgs, and exact-SHA CI. +- **Remediation commit:** None. Architecture and compatibility review are + in progress. +- **Verification:** The existing dual-signature consensus check and asset + signing path establish the pre-fix behavior. No PQ-only regression or + release qualification exists yet. +- **Final status:** OPEN; blocks FINDING-067 closure and the asset-inclusive + 4.8.1 release. From 055d6ef3c8e57aad771f4292be9611853f5e3c59 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:04:50 +0200 Subject: [PATCH 021/195] audit: record paired CI evidence [FINDING-084] [FINDING-089] [FINDING-143] [FINDING-144] [FINDING-145] [FINDING-146] [FINDING-147] [FINDING-156] --- ...0025-v4.8-security-remediation-register.md | 63 ++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 0087812c58..1ee2658741 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -5273,8 +5273,11 @@ rebuild logic was added. `mac_alias`. Both pinned directories permit `biplist`, `mac_alias`, and `ds_store` imports; the new preflight confirms each resolved module path is inside the depends native prefix. `bash -n` and diff whitespace checks - pass. The exact-SHA GitHub macOS package build remains pending. -- **Final status:** FIXED locally; GitHub macOS artifact pending. + pass. Build Raven run `37928136449` completed the macOS package job and + uploaded artifact `raven-unsigned-osx-870a0bee36f4ec4b4c52f1c0e238df83834025e2`. + Paired Final Gate run `37928133299` passed at the same SHA. +- **Final status:** FIXED for the tested candidate SHA; final release-SHA + qualification remains pending. ## Findings frozen during the 2026-10-08 asset-wallet continuation @@ -5282,6 +5285,14 @@ The defects below were reproduced or established against the uncommitted asset-wallet continuation based on `1eb238129`, before their corrections were committed. They do not change the frozen status of older findings. +Cross-platform candidate verification: Build Raven run `37928136449` and +Final Gate run `37928133299` both succeeded at +`870a0bee36f4ec4b4c52f1c0e238df83834025e2`. Final Gate passed its +required `make check`, functional security suite, and seven build-matrix +jobs. Build Raven passed its required security job and produced all five +Linux, ARM, Windows, and macOS artifacts. The later PQ-only asset redesign +and final release SHA require a new qualification pair. + ### FINDING-143: Protected administration can select the wrong authority coin - **Severity:** MEDIUM. @@ -5309,7 +5320,8 @@ were committed. They do not change the frozen status of older findings. - **Regression:** `feature_pq_asset_authority_selection.py` creates two authority UTXOs, funds only one matching anchor, verifies selection, and verifies the missing-anchor rejection. Passed in the 32-case matrix. -- **Final status:** FIXED locally; cross-platform CI remains pending. +- **Final status:** FIXED for the dual-signature candidate, verified by the + exact-SHA cross-platform CI pair above. PQ-only requalification is pending. ### FINDING-144: Refreshed PQ anchor appears as a received payment @@ -5337,7 +5349,8 @@ were committed. They do not change the frozen status of older findings. change classification. - **Regression:** `feature_pq_asset_anchor.py:250-268` failed before the fix, then passed before and after node restart. -- **Final status:** FIXED locally; cross-platform CI remains pending. +- **Final status:** FIXED for the dual-signature candidate, verified by the + exact-SHA cross-platform CI pair above. PQ-only requalification is pending. ### FINDING-145: Key-only salvage loses protected asset destination pairing @@ -5365,8 +5378,10 @@ were committed. They do not change the frozen status of older findings. - **Regression:** `pq_wallet_tests/protected_asset_destination_pair_survives_key_only_recovery` failed before the filter change and passes after it. Mismatched, malformed, oversized, trailing, and unrelated rows are rejected. -- **Final status:** FIXED locally; seed-only association reconstruction - remains a separate wallet-recovery limitation. +- **Final status:** FIXED for the dual-signature candidate, verified by the + exact-SHA cross-platform CI pair above. Seed-only association + reconstruction remains a separate wallet-recovery limitation; PQ-only + requalification is pending. ### FINDING-146: History can imply PQ protection for an old asset output @@ -5396,8 +5411,9 @@ were committed. They do not change the frozen status of older findings. covers historical, active, unconfirmed, disabled, and missing-tag cases; `feature_pq_asset_wallet_ownership.py` checks a real historical output. Both pass, and the Qt executable compiles and starts with `-version`. -- **Final status:** FIXED locally; GUI interaction testing in the release - matrix remains pending. +- **Final status:** FIXED for the dual-signature candidate, verified by the + exact-SHA cross-platform CI pair above. Manual GUI interaction testing + remains under FINDING-154, and PQ-only requalification is pending. ### FINDING-147: Asset history RPC drops the PQ destination component @@ -5424,7 +5440,8 @@ were committed. They do not change the frozen status of older findings. - **Regression:** `feature_pq_asset_anchor.py:255-258` was red before the fix and passes afterward; the historical vector in `feature_pq_asset_wallet_ownership.py:92-94` remains classical. -- **Final status:** FIXED locally; cross-platform CI remains pending. +- **Final status:** FIXED for the dual-signature candidate, verified by the + exact-SHA cross-platform CI pair above. PQ-only requalification is pending. ### FINDING-148: Some wallet spends consume an anchor without refreshing it @@ -7710,9 +7727,15 @@ Cross-platform CI and public deployment remain separate release gates. The job log reports packaged source commit `72a68f0f898ec794f3bdc38d0621124e2f88b54c` and artifact zip digest `2704829474edb114b249899c945b6649aa35452233a385f3988e9295d3ea032b`. - This was the pull-request merge SHA; final release-SHA qualification - remains pending. -- **Final status:** FIXED; final release-SHA artifact verification pending. + This was the pull-request merge SHA. Build Raven run `37928136449` + subsequently succeeded on `870a0bee36f4ec4b4c52f1c0e238df83834025e2` + with all five release targets, including the macOS artifact named + `raven-unsigned-osx-870a0bee36f4ec4b4c52f1c0e238df83834025e2`. + Paired Final Gate run `37928133299` also succeeded at that exact SHA. + The redesigned PQ-only asset rule and final release SHA still require + their own artifact qualification. +- **Final status:** FIXED for the dual-signature candidate; final release-SHA + artifact verification pending. ## Findings frozen after the merged asset-phase CI review @@ -7976,9 +7999,19 @@ Cross-platform CI and public deployment remain separate release gates. If runner loss recurs, inspect resource use and reduce parallel build load or otherwise correct the CI environment, then rerun. Final qualification must repeat at the eventual release SHA. -- **Remediation commit:** Pending classification. -- **Verification:** Pending same-SHA rerun. -- **Final status:** OPEN. +- **Remediation commit:** None. The failure was a hosted-runner disconnect, + with no demonstrated product-code defect. +- **Verification:** Build Raven run `37888464222`, attempt 2, succeeded on + the unchanged SHA `b8bc089f3e3d2b395279e23ec5fdd8e1a9036633`. + Its macOS job completed and uploaded the artifact named + `raven-unsigned-osx-b8bc089f3e3d2b395279e23ec5fdd8e1a9036633`. + The other four release targets and required security job also succeeded. + Paired Final Gate run `37888456288` succeeded at the same SHA. A newer + exact-SHA pair, Build Raven `37928136449` and Final Gate `37928133299`, + also succeeded at `870a0bee36f4ec4b4c52f1c0e238df83834025e2`, with + five artifacts. These are candidate builds, not the final release SHA. +- **Final status:** RESOLVED as a transient CI runner loss at the tested + SHAs; final release-SHA artifact qualification remains pending. ### FINDING-157: Historical asset spends remain classical after bit 13 From 198b930291e323de8600c1697cd5ce5ce493e67f Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:09:42 +0200 Subject: [PATCH 022/195] consensus: parse PQ-only asset prefix [FINDING-158] --- src/assets/assets.cpp | 23 +++++++++++++------ src/test/assets/asset_tx_tests.cpp | 36 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/assets/assets.cpp b/src/assets/assets.cpp index d953306159..c6dee7bf6d 100644 --- a/src/assets/assets.cpp +++ b/src/assets/assets.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include "assets.h" @@ -3631,14 +3632,17 @@ bool GetAssetData(const CScript& script, CAssetOutputEntry& data, const TxAssetD bool GetPQAssetProgram(const CScript& script, uint256& program) { - // Canonical parser for the dependent PQ asset rule. Keep the classical - // envelope intact so legacy nodes see the same asset. - if (script.size() < 26 + 1 + 4 + 32 || - script[0] != OP_DUP || script[1] != OP_HASH160 || script[2] != 20 || - script[23] != OP_EQUALVERIFY || script[24] != OP_CHECKSIG || - script[25] != OP_RVN_ASSET) { + if (script.size() < 26 + 1 + 4 + 32 || script[25] != OP_RVN_ASSET) + return false; + + const bool classicalPrefix = + script[0] == OP_DUP && script[1] == OP_HASH160 && script[2] == 20 && + script[23] == OP_EQUALVERIFY && script[24] == OP_CHECKSIG; + const bool pqOnlyPrefix = + script[0] == OP_1 && script[1] == OP_1 && script[2] == 20 && + script[23] == OP_DROP && script[24] == OP_DROP; + if (!classicalPrefix && !pqOnlyPrefix) return false; - } size_t payloadStart = 27; size_t payloadSize = script[26]; @@ -3704,6 +3708,11 @@ bool GetPQAssetProgram(const CScript& script, uint256& program) return false; std::copy(script.end() - 32, script.end(), program.begin()); + if (pqOnlyPrefix) { + const uint160 addressHash = Hash160(program.begin(), program.end()); + if (!std::equal(addressHash.begin(), addressHash.end(), script.begin() + 3)) + return false; + } return true; } diff --git a/src/test/assets/asset_tx_tests.cpp b/src/test/assets/asset_tx_tests.cpp index f1155086db..9c0d02cdf0 100644 --- a/src/test/assets/asset_tx_tests.cpp +++ b/src/test/assets/asset_tx_tests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -443,6 +444,41 @@ BOOST_FIXTURE_TEST_SUITE(asset_tx_tests, BasicTestingSetup) BOOST_CHECK_EQUAL(error, SCRIPT_ERR_OK); } + BOOST_AUTO_TEST_CASE(pq_only_asset_prefix_is_legacy_valid_and_program_bound_test) + { + SelectParams(CBaseChainParams::MAIN); + const uint256 program = uint256S("03"); + const CScript destination = GetScriptForDestination( + DecodeDestination(GetParams().GlobalBurnAddress())); + CScript legacy = destination; + CAssetTransfer("RAVENTEST", COIN).ConstructTransaction(legacy); + CScript tagged; + BOOST_REQUIRE(BuildPQAssetTaggedScript(legacy, program, tagged)); + + const uint160 addressHash = Hash160(program.begin(), program.end()); + CScript pqOnly = CScript() << OP_1 << OP_1 + << ToByteVector(addressHash) << OP_DROP << OP_DROP; + BOOST_REQUIRE_EQUAL(pqOnly.size(), 25U); + pqOnly.insert(pqOnly.end(), tagged.begin() + 25, tagged.end()); + BOOST_REQUIRE(pqOnly.IsAssetScript()); + + CMutableTransaction spending; + spending.vin.emplace_back(COutPoint(uint256S("06"), 0)); + spending.vout.emplace_back(0, legacy); + ScriptError error = SCRIPT_ERR_UNKNOWN_ERROR; + BOOST_CHECK(VerifyScript(CScript(), pqOnly, + &spending.vin[0].scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, + MutableTransactionSignatureChecker(&spending, 0, 0), &error)); + BOOST_CHECK_EQUAL(error, SCRIPT_ERR_OK); + + uint256 parsed; + BOOST_REQUIRE(GetPQAssetProgram(pqOnly, parsed)); + BOOST_CHECK(parsed == program); + CScript wrongAddress = pqOnly; + wrongAddress[3] ^= 1; + BOOST_CHECK(!GetPQAssetProgram(wrongAddress, parsed)); + } + BOOST_AUTO_TEST_CASE(pq_asset_program_canonical_parser_test) { SelectParams(CBaseChainParams::MAIN); From 6dbc4dba74e7ebe9fcc0836941222fe51938cf86 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:10:01 +0200 Subject: [PATCH 023/195] docs: record PQ-only asset format checkpoint [FINDING-158] --- doc/RIP-0025-PQ-Assets-Followup.md | 46 +++++++++++++++++++ ...0025-v4.8-security-remediation-register.md | 21 ++++++--- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/doc/RIP-0025-PQ-Assets-Followup.md b/doc/RIP-0025-PQ-Assets-Followup.md index f1416e1edd..bff79d3dbe 100644 --- a/doc/RIP-0025-PQ-Assets-Followup.md +++ b/doc/RIP-0025-PQ-Assets-Followup.md @@ -499,3 +499,49 @@ be compromised: No software can distinguish the rightful owner from an attacker who already controls the old classical key. Migration does not retroactively protect the historical UTXO, and a compromised holder can be raced before confirmation. + +## 20. PQ-Only Asset Ownership Decision and Candidate Format + +The owner selected the native-RVN migration model for assets and owner tokens. +Outputs created under the future bit-13 rule must require an ML-DSA-44 key +without an ECDSA signature. Historical asset and owner-token outputs retain +their classical spending rule and migrate voluntarily, with no sunset. The +current `classical|PQ` candidate still needs an ECDSA signature and therefore +does not meet this release requirement. FINDING-158 tracks the redesign. + +An input carrying a native witness-v2 RVN output with the matching PQ program +may provide the ML-DSA authorization for an asset input. The owner confirmed +this construction is acceptable if the asset input needs no ECDSA signature. +The separate native input still needs RVN funding and may need replenishment +after the spend, as described in section 17. + +One provisional asset locking prefix is exactly 25 bytes: + +``` +OP_1 OP_1 PUSH20 HASH160(P) OP_DROP OP_DROP +OP_RVN_ASSET +``` + +The asset opcode remains at byte 25, so the existing asset parser identifies +the output and extracts `HASH160(P)` as its address identifier. The prefix +executes to one true stack item with an empty scriptSig; it does not execute +ECDSA. The existing script interpreter treats every byte after +`OP_RVN_ASSET` as asset data. A new consensus rule must require a matching +valid witness-v2 anchor whenever such an output is spent at an active origin +height, and must reject the old dual-signature prefix on new outputs. The +`HASH160(P)` binding prevents a sender from selecting an unrelated qualified +or unfrozen classical address while assigning ownership to a different PQ +program. The 160-bit address identifier and restricted-asset semantics need +separate adversarial review. + +A red-then-green unit test now shows that this prefix passes the existing +standard script check with an empty scriptSig, is recognized as an asset, +and is parsed only when the address identifier matches `HASH160(P)`. This +is a format feasibility result, not full compatibility evidence. The +following remain required before activation: old-node block acceptance, +full asset conservation and index behavior, active-only output enforcement, +wallet PQ ownership and signing, RPC and Qt destination handling, owner +tokens, restricted address administration, migration, reorgs, recovery, +independent review, and paired release-target CI at one exact SHA. The +previously approved bit-13 deployment dates apply to the earlier candidate +and must be reviewed against the final PQ-only rule before they enter code. diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 1ee2658741..819b756076 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8046,9 +8046,8 @@ Cross-platform CI and public deployment remain separate release gates. in mempool and blocks; cover the activation boundary, reorgs, restart, assumevalid, and any authorized recovery path. Wallet and GUI balances must not imply that frozen UTXOs are spendable. -- **Remediation commit:** None; sunset policy and timing are under - discussion with the owner. The previously approved bit-13 parameters - do not authorize a legacy sunset rule. +- **Remediation commit:** None. The owner superseded the sunset request; + no sunset consensus change is planned for 4.8.1. - **Verification:** The existing unit vector at src/test/assets/asset_tx_tests.cpp:839-872 proves the current origin-height exemption. The functional migration vector at @@ -8068,6 +8067,9 @@ Cross-platform CI and public deployment remain separate release gates. - **Affected RIP-25 invariant:** After bit 13, new asset and owner-token outputs must be spendable with ML-DSA alone. Historical outputs retain their classical spending rule until their owners migrate them. +- **Owner-approved authorization model:** A matching native witness-v2 RVN + input may carry the ML-DSA signature for the asset spend. The asset input + itself must require no ECDSA signature. - **Affected Core 4.8.0 fix:** None. Its asset ownership format was P2PKH. - **Root cause:** The candidate asset envelope retains a P2PKH ownership prefix and appends a 32-byte PQ program. Its input rule additionally @@ -8093,10 +8095,15 @@ Cross-platform CI and public deployment remain separate release gates. signatures in mempool and blocks. Cover historical migration, partial change, issuance, reissuance, restricted and qualifier administration, raw RPCs, wallet backup and recovery, Qt, reorgs, and exact-SHA CI. -- **Remediation commit:** None. Architecture and compatibility review are - in progress. +- **Remediation commit:** `198b93029` is a partial parser and script-format + feasibility step. The complete consensus and wallet change is pending. - **Verification:** The existing dual-signature consensus check and asset - signing path establish the pre-fix behavior. No PQ-only regression or - release qualification exists yet. + signing path establish the pre-fix behavior. The new + `pq_only_asset_prefix_is_legacy_valid_and_program_bound_test` was red + before `198b93029` because the parser rejected the proposed prefix. It + now passes: the script executes with an empty scriptSig, the asset marker + remains at byte 25, and the address hash must match the PQ program. + All 25 `asset_tx_tests` cases and local `make check` passed. This does + not prove old-node block acceptance, wallet spending, or public activation. - **Final status:** OPEN; blocks FINDING-067 closure and the asset-inclusive 4.8.1 release. From 17053c0bc19150548f268802006ec553677ffb06 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:17:17 +0200 Subject: [PATCH 024/195] wallet: construct PQ-only asset outputs [FINDING-158] --- src/assets/assets.cpp | 20 ++++++++++++ src/assets/assets.h | 5 +-- src/test/assets/asset_tx_tests.cpp | 50 ++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/assets/assets.cpp b/src/assets/assets.cpp index c6dee7bf6d..adb98e3457 100644 --- a/src/assets/assets.cpp +++ b/src/assets/assets.cpp @@ -3833,6 +3833,26 @@ bool BuildPQAssetTaggedScript(const CScript& legacyScript, const uint256& progra return true; } +bool BuildPQOnlyAssetScript(const CScript& legacyScript, const uint256& program, CScript& pqOnlyScript) +{ + CScript tagged; + if (!BuildPQAssetTaggedScript(legacyScript, program, tagged)) + return false; + + const uint160 addressHash = Hash160(program.begin(), program.end()); + CScript candidate = CScript() << OP_1 << OP_1 + << ToByteVector(addressHash) << OP_DROP << OP_DROP; + if (candidate.size() != 25) + return false; + candidate.insert(candidate.end(), tagged.begin() + 25, tagged.end()); + + uint256 parsedProgram; + if (!GetPQAssetProgram(candidate, parsedProgram) || parsedProgram != program) + return false; + pqOnlyScript = std::move(candidate); + return true; +} + #ifdef ENABLE_WALLET void GetAllAdministrativeAssets(CWallet *pwallet, std::vector &names, int nMinConf) { diff --git a/src/assets/assets.h b/src/assets/assets.h index f76ae3c323..4360615085 100644 --- a/src/assets/assets.h +++ b/src/assets/assets.h @@ -535,8 +535,7 @@ bool GetAssetInfoFromScript(const CScript& scriptPubKey, std::string& strName, C bool GetAssetData(const CScript& script, CAssetOutputEntry& data, const TxAssetDeploymentContext* pAssetContext = nullptr); -// Experimental PQ asset extension: recognize a canonical 32-byte program -// tail without changing the legacy asset parser or script evaluator. +// Recognize a canonical 32-byte program tail under either asset prefix. bool GetPQAssetProgram(const CScript& script, uint256& program); /** Encode an asset destination according to the output's protection at its origin height. * Pass -1 for originHeight when the output is not confirmed on the active chain. @@ -546,6 +545,8 @@ std::string EncodeContextualAssetDestination(const CScript& script, int originHeight, int pqAssetActivationHeight); /** Tag a canonical legacy P2PKH asset output for the dependent PQ asset rule. */ bool BuildPQAssetTaggedScript(const CScript& legacyScript, const uint256& program, CScript& taggedScript); +/** Replace legacy asset ownership with a PQ-only, anchor-protected prefix. */ +bool BuildPQOnlyAssetScript(const CScript& legacyScript, const uint256& program, CScript& pqOnlyScript); bool GetBestAssetAddressAmount(CAssetsCache& cache, const std::string& assetName, const std::string& address); diff --git a/src/test/assets/asset_tx_tests.cpp b/src/test/assets/asset_tx_tests.cpp index 9c0d02cdf0..d497b728ab 100644 --- a/src/test/assets/asset_tx_tests.cpp +++ b/src/test/assets/asset_tx_tests.cpp @@ -479,6 +479,56 @@ BOOST_FIXTURE_TEST_SUITE(asset_tx_tests, BasicTestingSetup) BOOST_CHECK(!GetPQAssetProgram(wrongAddress, parsed)); } + BOOST_AUTO_TEST_CASE(pq_only_asset_builder_preserves_transfer_and_owner_test) + { + SelectParams(CBaseChainParams::MAIN); + const uint256 program = uint256S("03"); + const CScript destination = GetScriptForDestination( + DecodeDestination(GetParams().GlobalBurnAddress())); + const TxAssetDeploymentContext parserContext{true, false, false, true, true, true}; + const auto check = [&program, &parserContext](const CScript& legacy) { + CScript pqOnly; + BOOST_REQUIRE(BuildPQOnlyAssetScript(legacy, program, pqOnly)); + CAssetOutputEntry before; + CAssetOutputEntry after; + BOOST_REQUIRE(GetAssetData(legacy, before, &parserContext)); + BOOST_REQUIRE(GetAssetData(pqOnly, after, &parserContext)); + BOOST_CHECK_EQUAL(before.type, after.type); + BOOST_CHECK_EQUAL(before.assetName, after.assetName); + BOOST_CHECK_EQUAL(before.nAmount, after.nAmount); + if (before.type == TX_TRANSFER_ASSET) { + BOOST_CHECK_EQUAL(before.message, after.message); + BOOST_CHECK_EQUAL(before.expireTime, after.expireTime); + } + const CKeyID* pseudoAddress = boost::get(&after.destination); + BOOST_REQUIRE(pseudoAddress); + BOOST_CHECK(*pseudoAddress == CKeyID(Hash160(program.begin(), program.end()))); + uint256 parsed; + BOOST_REQUIRE(GetPQAssetProgram(pqOnly, parsed)); + BOOST_CHECK(parsed == program); + }; + CScript transfer = destination; + CAssetTransfer("RAVENTEST", COIN).ConstructTransaction(transfer); + check(transfer); + CScript owner = destination; + CNewAsset("RAVENTEST", COIN, 0, 1, 0, "").ConstructOwnerTransaction(owner); + check(owner); + CScript message = destination; + CAssetTransfer("RAVENTEST", COIN, std::string(32, 'x')) + .ConstructTransaction(message); + check(message); + for (const std::string& name : {"RAVENTEST", "RAVENTEST/SUB", + "RAVENTEST#ONE", "$RAVENTEST", + "#RAVENTEST", "#RAVENTEST/SUB"}) { + CScript issue = destination; + CNewAsset(name, COIN, 0, 1, 0, "").ConstructTransaction(issue); + check(issue); + } + CScript reissue = destination; + CReissueAsset("RAVENTEST", COIN, 0, 1, "").ConstructTransaction(reissue); + check(reissue); + } + BOOST_AUTO_TEST_CASE(pq_asset_program_canonical_parser_test) { SelectParams(CBaseChainParams::MAIN); From 2e07c1a14c3015618820cceaac030ae6288dbe78 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:25:55 +0200 Subject: [PATCH 025/195] audit: freeze PQ-only scriptSig malleability [FINDING-159] --- ...0025-v4.8-security-remediation-register.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 819b756076..b7bc6805ca 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8107,3 +8107,37 @@ Cross-platform CI and public deployment remain separate release gates. not prove old-node block acceptance, wallet spending, or public activation. - **Final status:** OPEN; blocks FINDING-067 closure and the asset-inclusive 4.8.1 release. + +### FINDING-159: Proposed PQ-only asset input permits scriptSig malleability + +- **Severity:** MEDIUM transaction integrity and relay consistency. +- **Frozen initial status:** OPEN on 2026-10-09 at 17053c0bc, before an + input scriptSig rule was added to the PQ-only consensus candidate. +- **Affected RIP-25 invariant:** A PQ-only asset spend has one canonical + empty asset scriptSig; its ML-DSA signature resides in a matching native + witness-v2 input. +- **Affected Core 4.8.0 fix:** None. This is a risk introduced by the new + PQ-only prefix under consideration for 4.8.1. +- **Root cause:** The proposed prefix leaves one true stack item when the + asset scriptSig is empty. Consensus block script flags do not include + CLEANSTACK, so a pushed extra true value could leave a nonempty stack + and still pass script evaluation. The input rule currently checks the + matching PQ anchor but not that the asset scriptSig is empty. +- **Affected file/function/lines:** src/validation.cpp:2550-2583, + `GetBlockScriptFlags`; src/script/script.cpp:245-281, asset marker; + src/consensus/tx_verify.cpp:626-676, `CheckTxPQAssets`. +- **Concrete scenario:** A third party can add a push of true to a proposed + PQ-only asset input scriptSig, changing its txid without changing its + authenticated asset movement or the anchor signature. Standard mempool + policy may reject it, but a block rule must reject it independently. +- **Expected behavior:** At active bit 13, every PQ-only asset input must + have an empty scriptSig in mempool and blocks. Historical classical asset + inputs retain their previous scriptSig rules. +- **Regression required:** Show an otherwise valid anchored PQ-only spend + with a nonempty asset scriptSig is accepted before the new check, then + rejected with a specific reason afterward. Empty scriptSig with the + matching anchor and historical classical inputs remain valid. +- **Remediation commit:** None. Consensus correction is pending. +- **Verification:** Static comparison of block flags and the new prefix + demonstrates the risk; the red and green transaction vectors are pending. +- **Final status:** OPEN. From 99101c0f7b81ed9b9dda930673616863ae6ec12b Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:27:52 +0200 Subject: [PATCH 026/195] audit: freeze block compatibility and custody gaps [FINDING-160] [FINDING-161] --- ...0025-v4.8-security-remediation-register.md | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index b7bc6805ca..3f45f7f68e 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8141,3 +8141,76 @@ Cross-platform CI and public deployment remain separate release gates. - **Verification:** Static comparison of block flags and the new prefix demonstrates the risk; the red and green transaction vectors are pending. - **Final status:** OPEN. + +### FINDING-160: Native RIP-25 block limits can split Core 4.8.0 nodes + +- **Severity:** HIGH consensus compatibility and release blocker. +- **Frozen initial status:** OPEN on 2026-10-09 at `2e07c1a14`, before a + compatibility remedy or a revised deployment plan. +- **Affected RIP-25 invariant:** A deployment described as a soft fork must + not make a block valid under the new rules when Core 4.8.0 rejects it. +- **Affected Core 4.8.0 fix:** None. Core 4.8.0 enforces the RIP-2 8 MWU + block limit; the candidate changes that limit after bit 12. +- **Root cause:** The candidate allows 12 MWU immediately after native + bit-12 activation and 16 MWU after one nominal year. It also applies a + contextual PQ witness discount. Core 4.8.0 retains the 8 MWU rule and + its original weight accounting. These expansions conflict with the + soft-fork compatibility claim in the RIP-25 specification. +- **Affected file/function/lines:** src/consensus/consensus.h:22-34, + limits; src/validation.cpp:2448-2474, contextual limits; + src/validation.cpp:4494-4507, block checks; doc/RIP-0025-PQ-Signatures.md:19,492, + soft-fork claim. Core 4.8.0 baseline is + `b60f50e04f1fba425b28804e61be2694faaf3469`. +- **Concrete scenario:** After bit 12, an upgraded miner may produce a + block whose old-accounted weight exceeds 8 MWU but whose candidate + accounting is within the new 12 MWU limit. An upgraded node can accept + it while a Core 4.8.0 node rejects it. +- **Expected behavior:** Prove old-node acceptance of every post-activation + block allowed by the candidate, or explicitly choose and approve a + deployment that accounts for the incompatible block rules. Correct the + protocol and release documentation before public activation. +- **Regression required:** Run old Core 4.8.0 and candidate nodes against + blocks on both sides of the 8 MWU boundary, including PQ witness data + whose discounted and old-accounted weights differ. Verify block and + chain outcomes, not only isolated weight functions. +- **Remediation commit:** None. +- **Verification:** Static comparison of the baseline 8 MWU constant and + candidate contextual 12/16 MWU checks establishes the incompatible + acceptance ranges. A two-binary block regression is pending. +- **Final status:** OPEN; blocks any claim of a compatible public bit-12 + activation and the 4.8.1 release. + +### FINDING-161: Pre-activation PQ-only lookalike output has no PQ custody + +- **Severity:** HIGH asset custody ambiguity. +- **Frozen initial status:** OPEN on 2026-10-09 at `2e07c1a14`, before any + creation rule, wallet classification, or origin-bound regression. +- **Affected RIP-25 invariant:** An output described as PQ-only must have + enforceable ML-DSA authorization when spent. The historical exception + must not silently label an anyone-can-spend lookalike as PQ protected. +- **Affected Core 4.8.0 fix:** None. The lookalike prefix is introduced by + the 4.8.1 candidate and remains valid to old script evaluation. +- **Root cause:** Before bit 13, `CheckTxPQAssets` does not apply the new + anchor rule. After activation, the origin-height exception still exempts + an output created before activation. The proposed script prefix is + satisfiable with an empty scriptSig under historical script rules. +- **Affected file/function/lines:** src/consensus/tx_verify.cpp:626-676, + activation and origin checks; src/script/script.cpp:245-281, legacy asset + parser; src/script/script.h:579-586, asset-tail evaluation; wallet asset + ownership and address generation paths. +- **Concrete scenario:** A pre-activation transaction creates the new + prefix and asset payload. The output can later be spent without an + ML-DSA anchor because its origin predates bit 13. A wallet that labels + or credits it as a protected PQ-only holding would misstate custody. +- **Expected behavior:** Define and enforce an activation-safe creation + and classification policy. Historical lookalikes must not appear as + PQ-protected wallet balance. Preserve valid historical spends without + trapping funds or silently treating them as authenticated PQ spends. +- **Regression required:** Create a lookalike before activation, spend it + before and after activation without an anchor, and prove the exact + consensus outcome and wallet classification. Cover a post-activation + creation and anchored spend as a separate case. +- **Remediation commit:** None. +- **Verification:** Static origin-height and script inspection identifies + the gap. Boundary and wallet regressions are pending. +- **Final status:** OPEN; blocks FINDING-158 closure and asset release. From 767e1b2b85827c6582eb2d1fa83b58eeee12e78d Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:19:30 +0200 Subject: [PATCH 027/195] audit: freeze PQ asset message subscription regression [FINDING-162] --- ...0025-v4.8-security-remediation-register.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 3f45f7f68e..aded7c4012 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8214,3 +8214,37 @@ Cross-platform CI and public deployment remain separate release gates. - **Verification:** Static origin-height and script inspection identifies the gap. Boundary and wallet regressions are pending. - **Final status:** OPEN; blocks FINDING-158 closure and asset release. + +### FINDING-162: PQ-only owner and message assets miss wallet subscriptions + +- **Severity:** MEDIUM wallet messaging regression. +- **Frozen initial status:** OPEN on 2026-10-10 at `99101c0f7`, before a + contextual wallet ownership fix in the message subscription path. +- **Affected RIP-25 invariant:** Owner tokens and message channel assets + received after bit 13 must retain the existing wallet subscription + behavior when the PQ key controls them. +- **Affected Core 4.8.0 fix:** None. The regression follows the new + context-dependent PQ-only ownership rule. +- **Root cause:** `WalletOwnsAssetForMessageSubscription` calls the generic + `CWallet::IsMine(output)` method. The generic method deliberately returns + `ISMINE_NO` for PQ-only asset scripts because it has no origin height. + The helper returns before using the block height and PQ key it already + has available. +- **Affected file/function/lines:** src/coins.cpp:20-39, + `WalletOwnsAssetForMessageSubscription`; src/coins.cpp:309-333, transfer + and new-asset subscriptions; src/script/ismine.cpp:159-203, generic + ownership classification. +- **Concrete scenario:** A wallet receives a post-activation owner token + or message channel at its PQ-only destination. The asset is spendable + under the contextual wallet rule, but the message subscription helper + reports that it is not owned and skips channel enrollment. +- **Expected behavior:** Evaluate wallet ownership using the output's + confirmed block context. Do not enroll pre-activation PQ-only lookalikes + as PQ-protected subscriptions. +- **Regression required:** With messaging enabled, confirm post-activation + PQ-only owner-token and message-channel outputs and verify the expected + subscription. Confirm a pre-activation PQ-only lookalike is excluded. +- **Remediation commit:** None. +- **Verification:** Static call-path review. The messaging regression + test and correction are pending. +- **Final status:** OPEN; blocks complete PQ-only wallet qualification. From ebc97a6e87efbb3626d0324dd88dcaefd4d143bd Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:21:33 +0200 Subject: [PATCH 028/195] audit: freeze PQ asset address and restriction leaks [FINDING-163] [FINDING-164] --- ...0025-v4.8-security-remediation-register.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index aded7c4012..0499710ae1 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8248,3 +8248,84 @@ Cross-platform CI and public deployment remain separate release gates. - **Verification:** Static call-path review. The messaging regression test and correction are pending. - **Final status:** OPEN; blocks complete PQ-only wallet qualification. + +### FINDING-163: Synthetic PQ asset identifier leaks as a native RVN address + +- **Severity:** HIGH potential loss of native RVN funds. +- **Frozen initial status:** OPEN on 2026-10-10 at `767e1b2b8`, before + address-book filtering or storage changes. +- **Affected RIP-25 invariant:** The 160-bit asset identifier derived from + a PQ program must never be presented as a wallet-owned RVN receive + address. No ECDSA key exists for its P2PKH interpretation. +- **Affected Core 4.8.0 fix:** None. The synthetic identifier is new to the + PQ-only 4.8.1 candidate. +- **Root cause:** New PQ asset destination creation adds the synthetic + `CKeyID(Hash160(program))` to the wallet address book with purpose + `pqasset`. `getaddressesbyaccount` enumerates every address-book entry + with the matching account name without checking purpose or spendability. + Qt hides the purpose, but the RPC exposes the identifier as an ordinary + Ravencoin address. +- **Affected file/function/lines:** src/wallet/rpcwallet.cpp:280-290, + PQ asset destination creation; src/wallet/rpcwallet.cpp:503-513, + `getaddressesbyaccount`; src/wallet/rpcwallet.cpp:1572-1610, + `listreceivedbyaddress`; src/wallet/rpcwallet.cpp:791-800 and + src/wallet/wallet.cpp:5290-5375, `listaddressgroupings` inputs; + src/qt/walletmodel.cpp:219-229, analogous Qt creation; + src/qt/addresstablemodel.cpp:54-68, Qt purpose filtering; + src/wallet/walletdb.cpp:1332-1350 and src/wallet/wallet.cpp:5675-5682, + key-only recovery of v2 metadata. +- **Concrete scenario:** An application asks `getaddressesbyaccount` for + RVN receive addresses after `getnewpqassetaddress` used the same account. + It pays RVN to the returned synthetic P2PKH identifier. The wallet has + no matching ECDSA private key, so that RVN is not spendable by the + holder of the PQ program. A key-only recovery also drops the address + purpose, so Qt can reclassify the synthetic identifier as a send address. +- **Expected behavior:** Native RVN address listings must exclude the + synthetic asset identifier, including account, received-address and + grouping APIs. Asset-specific listings must preserve the complete + descriptor and PQ key association. +- **Regression required:** Generate a PQ-only asset destination for an + account; prove the synthetic identifier appears in the native address + listing before correction and is absent afterward, while the witness-v2 + native address remains present and the asset descriptor survives reload. + Cover `listreceivedbyaddress` with empty entries and + `listaddressgroupings` after receiving an asset. Recover keys only and + verify the Qt address-table classification remains hidden. +- **Remediation commit:** None. +- **Verification:** Static RPC call-path review and the functional test + showing `validateaddress(assetID).ismine == false`. An explicit address + listing regression is pending. +- **Final status:** OPEN; blocks safe asset wallet release. + +### FINDING-164: PQ-only restricted-asset status misses wallet ownership + +- **Severity:** MEDIUM wallet and GUI state regression. +- **Frozen initial status:** OPEN on 2026-10-10 at `767e1b2b8`, before a + contextual PQ ownership correction for restricted-asset events. +- **Affected RIP-25 invariant:** Tag, untag, freeze and unfreeze operations + affecting a wallet's PQ-only asset identifier must update its local + restricted-asset state and GUI after a confirmed block. +- **Affected Core 4.8.0 fix:** None. The synthetic identifier without an + ECDSA key is new to the PQ-only candidate. +- **Root cause:** `ContextualCheckNullAssetTxOut` decides whether to record + an address event for the private wallet database with generic + `IsMine(wallet, DecodeDestination(address))`. That detects classical + key ownership but cannot identify a PQ-only asset identifier derived + from an owned ML-DSA program. Only collected events reach the private + restricted-asset database. +- **Affected file/function/lines:** src/assets/assets.cpp:5704-5710, + wallet ownership test; src/validation.cpp:3097-3111, event storage and + `UpdateMyRestrictedAssets`; restricted-asset RPC and Qt listings. +- **Concrete scenario:** A valid tag or freeze for an owned PQ-only asset + identifier confirms on chain, but the wallet does not record the event. + Its personal restricted-asset views can then disagree with the chain. +- **Expected behavior:** Recognize the synthetic identifier through its + owned canonical PQ-only descriptor and preserve the existing update and + restart behavior. Do not claim unrelated identifiers as wallet-owned. +- **Regression required:** Tag and freeze a wallet-owned PQ-only + descriptor, mine the operations, and check the private DB, relevant + RPCs and Qt view. Repeat after restart and cover an unrelated program. +- **Remediation commit:** None. +- **Verification:** Static call-path review; functional regression is + pending. +- **Final status:** OPEN; blocks restricted-asset wallet qualification. From 802bf24332b72c4c299af79ff9631ea3b0e18ff2 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:13:48 +0200 Subject: [PATCH 029/195] [FINDING-158][FINDING-159][FINDING-161] Enforce PQ-only asset scripts and anchor origin rules --- src/assets/assets.cpp | 76 ++++++++++++++++++---------- src/assets/assets.h | 2 + src/base58.cpp | 7 +++ src/base58.h | 6 ++- src/consensus/tx_verify.cpp | 10 +++- src/script/ismine.cpp | 10 ++++ src/script/sign.cpp | 22 +++++++++ src/test/assets/asset_tx_tests.cpp | 60 +++++++++++++++++++++-- src/test/script_standard_tests.cpp | 79 ++++++++++++++++++++++++++++++ 9 files changed, 238 insertions(+), 34 deletions(-) diff --git a/src/assets/assets.cpp b/src/assets/assets.cpp index adb98e3457..2c876a5ca0 100644 --- a/src/assets/assets.cpp +++ b/src/assets/assets.cpp @@ -3716,6 +3716,12 @@ bool GetPQAssetProgram(const CScript& script, uint256& program) return true; } +bool IsPQOnlyAssetScript(const CScript& script, uint256& program) +{ + return script.size() >= 26 && script[0] == OP_1 && + GetPQAssetProgram(script, program); +} + std::string EncodeContextualAssetDestination(const CScript& script, int originHeight, int pqAssetActivationHeight) { @@ -3854,6 +3860,26 @@ bool BuildPQOnlyAssetScript(const CScript& legacyScript, const uint256& program, } #ifdef ENABLE_WALLET +isminetype GetRestrictedAddressOwnership(const CWallet& wallet, const std::string& address) +{ + const CTxDestination destination = DecodeDestination(address); + const isminetype mine = IsMine(wallet, destination); + if (mine == ISMINE_SPENDABLE) + return mine; + + const CKeyID* assetID = boost::get(&destination); + if (!assetID) + return mine; + for (const std::string& descriptor : wallet.GetOwnedPQAssetDestinations()) { + CKeyID ownedID; + uint256 program; + if (DecodePQOnlyAssetDestination(descriptor, ownedID, program) && + ownedID == *assetID) + return ISMINE_SPENDABLE; + } + return mine; +} + void GetAllAdministrativeAssets(CWallet *pwallet, std::vector &names, int nMinConf) { if(!pwallet) @@ -4295,9 +4321,9 @@ bool CreateAssetTransaction(CWallet* pwallet, CCoinControl& coinControl, const s if (pqAssetsActive) { CKeyID classicalKey; uint256 program; - if (!DecodePQAssetDestination(address, classicalKey, program)) { + if (!DecodePQOnlyAssetDestination(address, classicalKey, program)) { error = std::make_pair(RPC_INVALID_ADDRESS_OR_KEY, - "Active PQ asset issuance requires a canonical classical|PQ asset destination"); + "Active PQ asset issuance requires a canonical PQ-only asset destination"); return false; } assetDestination = classicalKey; @@ -4407,14 +4433,14 @@ bool CreateAssetTransaction(CWallet* pwallet, CCoinControl& coinControl, const s CAssetTransfer assetTransfer(parentName + OWNER_TAG, OWNER_ASSET_AMOUNT); assetTransfer.ConstructTransaction(scriptTransferOwnerAsset); if (protectedAuthorityReturn) { - CScript tagged; - if (!BuildPQAssetTaggedScript(scriptTransferOwnerAsset, - protectedParentProgram, tagged)) { + CScript pqOnly; + if (!BuildPQOnlyAssetScript(scriptTransferOwnerAsset, + protectedParentProgram, pqOnly)) { error = std::make_pair(RPC_WALLET_ERROR, "Could not construct protected parent owner-token return"); return false; } - scriptTransferOwnerAsset = std::move(tagged); + scriptTransferOwnerAsset = std::move(pqOnly); } CRecipient rec = {scriptTransferOwnerAsset, 0, fSubtractFeeFromAmount}; vecSend.push_back(rec); @@ -4428,14 +4454,14 @@ bool CreateAssetTransaction(CWallet* pwallet, CCoinControl& coinControl, const s CAssetTransfer assetTransfer(parentName, OWNER_ASSET_AMOUNT); assetTransfer.ConstructTransaction(scriptTransferQualifierAsset); if (protectedAuthorityReturn) { - CScript tagged; - if (!BuildPQAssetTaggedScript(scriptTransferQualifierAsset, - protectedParentProgram, tagged)) { + CScript pqOnly; + if (!BuildPQOnlyAssetScript(scriptTransferQualifierAsset, + protectedParentProgram, pqOnly)) { error = std::make_pair(RPC_WALLET_ERROR, "Could not construct protected parent qualifier return"); return false; } - scriptTransferQualifierAsset = std::move(tagged); + scriptTransferQualifierAsset = std::move(pqOnly); } CRecipient rec = {scriptTransferQualifierAsset, 0, fSubtractFeeFromAmount}; vecSend.push_back(rec); @@ -4476,14 +4502,14 @@ bool CreateAssetTransaction(CWallet* pwallet, CCoinControl& coinControl, const s CAssetTransfer assetTransfer(strStripped + OWNER_TAG, OWNER_ASSET_AMOUNT); assetTransfer.ConstructTransaction(scriptTransferOwnerAsset); if (protectedAuthorityReturn) { - CScript tagged; - if (!BuildPQAssetTaggedScript(scriptTransferOwnerAsset, - protectedParentProgram, tagged)) { + CScript pqOnly; + if (!BuildPQOnlyAssetScript(scriptTransferOwnerAsset, + protectedParentProgram, pqOnly)) { error = std::make_pair(RPC_WALLET_ERROR, "Could not construct protected restricted owner-token return"); return false; } - scriptTransferOwnerAsset = std::move(tagged); + scriptTransferOwnerAsset = std::move(pqOnly); } CRecipient ownerRec = {scriptTransferOwnerAsset, 0, fSubtractFeeFromAmount}; @@ -4540,9 +4566,9 @@ bool CreateReissueAssetTransaction(CWallet* pwallet, CCoinControl& coinControl, if (pqAssetsActive) { CKeyID classicalKey; uint256 program; - if (!DecodePQAssetDestination(address, classicalKey, program)) { + if (!DecodePQOnlyAssetDestination(address, classicalKey, program)) { error = std::make_pair(RPC_INVALID_ADDRESS_OR_KEY, - "Active PQ reissue requires a canonical classical|PQ asset destination"); + "Active PQ reissue requires a canonical PQ-only asset destination"); return false; } assetDestination = classicalKey; @@ -4658,14 +4684,14 @@ bool CreateReissueAssetTransaction(CWallet* pwallet, CCoinControl& coinControl, CAssetTransfer ownerTransfer(ownerName, OWNER_ASSET_AMOUNT); ownerTransfer.ConstructTransaction(scriptTransferOwnerAsset); if (pqAssetsActive) { - CScript tagged; - if (!BuildPQAssetTaggedScript(scriptTransferOwnerAsset, - ownerReturnProgram, tagged)) { + CScript pqOnly; + if (!BuildPQOnlyAssetScript(scriptTransferOwnerAsset, + ownerReturnProgram, pqOnly)) { error = std::make_pair(RPC_WALLET_ERROR, "Could not construct protected owner-token return"); return false; } - scriptTransferOwnerAsset = std::move(tagged); + scriptTransferOwnerAsset = std::move(pqOnly); } if (asset_type == AssetType::RESTRICTED) { @@ -4786,9 +4812,9 @@ bool CreateTransferAssetTransaction(CWallet* pwallet, const CCoinControl& coinCo uint256 pqProgram; if (pqAssetsActive) { CKeyID classicalKey; - if (!DecodePQAssetDestination(address, classicalKey, pqProgram)) { + if (!DecodePQOnlyAssetDestination(address, classicalKey, pqProgram)) { error = std::make_pair(RPC_INVALID_ADDRESS_OR_KEY, - "Active PQ asset transfer requires a canonical classical|PQ asset destination"); + "Active PQ asset transfer requires a canonical PQ-only asset destination"); return false; } assetDestination = classicalKey; @@ -4852,13 +4878,13 @@ bool CreateTransferAssetTransaction(CWallet* pwallet, const CCoinControl& coinCo CAssetTransfer assetTransfer(asset_name, nAmount, message, expireTime); assetTransfer.ConstructTransaction(scriptPubKey); if (pqAssetsActive) { - CScript tagged; - if (!BuildPQAssetTaggedScript(scriptPubKey, pqProgram, tagged)) { + CScript pqOnly; + if (!BuildPQOnlyAssetScript(scriptPubKey, pqProgram, pqOnly)) { error = std::make_pair(RPC_TRANSACTION_ERROR, "Could not construct a canonical PQ asset transfer output"); return false; } - scriptPubKey = std::move(tagged); + scriptPubKey = std::move(pqOnly); } CRecipient recipient = {scriptPubKey, 0, fSubtractFeeFromAmount}; diff --git a/src/assets/assets.h b/src/assets/assets.h index 4360615085..bf30157385 100644 --- a/src/assets/assets.h +++ b/src/assets/assets.h @@ -7,6 +7,7 @@ #define RAVENCOIN_ASSET_PROTOCOL_H #include "amount.h" +#include "script/ismine.h" #include "tinyformat.h" #include "assettypes.h" @@ -537,6 +538,7 @@ bool GetAssetData(const CScript& script, CAssetOutputEntry& data, const TxAssetD // Recognize a canonical 32-byte program tail under either asset prefix. bool GetPQAssetProgram(const CScript& script, uint256& program); +bool IsPQOnlyAssetScript(const CScript& script, uint256& program); /** Encode an asset destination according to the output's protection at its origin height. * Pass -1 for originHeight when the output is not confirmed on the active chain. * An empty result means the output lacks the required active PQ tag. diff --git a/src/base58.cpp b/src/base58.cpp index f9afd8114d..b706390fd7 100644 --- a/src/base58.cpp +++ b/src/base58.cpp @@ -426,6 +426,13 @@ bool DecodePQAssetDestination(const std::string& descriptor, CKeyID& classicalKe return true; } +bool DecodePQOnlyAssetDestination(const std::string& descriptor, CKeyID& assetID, uint256& pqProgram) +{ + if (!DecodePQAssetDestination(descriptor, assetID, pqProgram)) + return false; + return assetID == CKeyID(Hash160(pqProgram.begin(), pqProgram.end())); +} + bool IsValidDestinationString(const std::string& str, const CChainParams& params) { // Check bech32m first diff --git a/src/base58.h b/src/base58.h index 2c1728f388..157bfc6143 100644 --- a/src/base58.h +++ b/src/base58.h @@ -168,11 +168,13 @@ typedef CRavenExtKeyBase #include "ismine.h" +#include "assets/assets.h" #include "key.h" #include "keystore.h" #include "script/script.h" @@ -159,6 +160,9 @@ isminetype IsMine(const CKeyStore &keystore, const CScript& scriptPubKey, bool& case TX_NEW_ASSET: { if (!AreAssetsDeployed()) return ISMINE_NO; + uint256 program; + if (IsPQOnlyAssetScript(scriptPubKey, program)) + break; keyID = CKeyID(uint160(vSolutions[0])); if (sigversion != SIGVERSION_BASE) { CPubKey pubkey; @@ -176,6 +180,9 @@ isminetype IsMine(const CKeyStore &keystore, const CScript& scriptPubKey, bool& case TX_TRANSFER_ASSET: { if (!AreAssetsDeployed()) return ISMINE_NO; + uint256 program; + if (IsPQOnlyAssetScript(scriptPubKey, program)) + break; keyID = CKeyID(uint160(vSolutions[0])); if (sigversion != SIGVERSION_BASE) { CPubKey pubkey; @@ -192,6 +199,9 @@ isminetype IsMine(const CKeyStore &keystore, const CScript& scriptPubKey, bool& case TX_REISSUE_ASSET: { if (!AreAssetsDeployed()) return ISMINE_NO; + uint256 program; + if (IsPQOnlyAssetScript(scriptPubKey, program)) + break; keyID = CKeyID(uint160(vSolutions[0])); if (sigversion != SIGVERSION_BASE) { CPubKey pubkey; diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 3d9b34fa4c..09ee045ecf 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -6,6 +6,7 @@ #include "script/sign.h" +#include "assets/assets.h" #include "key.h" #include "keystore.h" #include "policy/policy.h" @@ -89,6 +90,13 @@ static bool SignStep(const BaseSignatureCreator& creator, const CScript& scriptP return false; /** RVN START */ case TX_NEW_ASSET: + { + uint256 program; + if (IsPQOnlyAssetScript(scriptPubKey, program)) { + CPQKey pqKey; + return creator.KeyStore().GetPQKey(program, pqKey) && pqKey.IsValid(); + } + } keyID = CKeyID(uint160(vSolutions[0])); if (!Sign1(keyID, creator, scriptPubKey, ret, sigversion)) return false; @@ -100,6 +108,13 @@ static bool SignStep(const BaseSignatureCreator& creator, const CScript& scriptP } return true; case TX_TRANSFER_ASSET: + { + uint256 program; + if (IsPQOnlyAssetScript(scriptPubKey, program)) { + CPQKey pqKey; + return creator.KeyStore().GetPQKey(program, pqKey) && pqKey.IsValid(); + } + } keyID = CKeyID(uint160(vSolutions[0])); if (!Sign1(keyID, creator, scriptPubKey, ret, sigversion)) return false; @@ -112,6 +127,13 @@ static bool SignStep(const BaseSignatureCreator& creator, const CScript& scriptP return true; case TX_REISSUE_ASSET: + { + uint256 program; + if (IsPQOnlyAssetScript(scriptPubKey, program)) { + CPQKey pqKey; + return creator.KeyStore().GetPQKey(program, pqKey) && pqKey.IsValid(); + } + } keyID = CKeyID(uint160(vSolutions[0])); if (!Sign1(keyID, creator, scriptPubKey, ret, sigversion)) return false; diff --git a/src/test/assets/asset_tx_tests.cpp b/src/test/assets/asset_tx_tests.cpp index d497b728ab..8ac0fbf13b 100644 --- a/src/test/assets/asset_tx_tests.cpp +++ b/src/test/assets/asset_tx_tests.cpp @@ -850,8 +850,11 @@ BOOST_FIXTURE_TEST_SUITE(asset_tx_tests, BasicTestingSetup) CTxOut tagged = legacy; BOOST_REQUIRE(BuildPQAssetTaggedScript(legacy.scriptPubKey, program, tagged.scriptPubKey)); + CTxOut pqOnly = legacy; + BOOST_REQUIRE(BuildPQOnlyAssetScript(legacy.scriptPubKey, program, + pqOnly.scriptPubKey)); uint256 parsed; - BOOST_REQUIRE(GetPQAssetProgram(tagged.scriptPubKey, parsed)); + BOOST_REQUIRE(GetPQAssetProgram(pqOnly.scriptPubKey, parsed)); BOOST_CHECK(parsed == program); CCoinsView base; @@ -862,10 +865,15 @@ BOOST_FIXTURE_TEST_SUITE(asset_tx_tests, BasicTestingSetup) const COutPoint unconfirmedParent(uint256S("14"), 0); const COutPoint matchingAnchor(uint256S("15"), 0); const COutPoint wrongAnchor(uint256S("16"), 0); + const COutPoint invalidDualOut(uint256S("17"), 0); + const COutPoint preActivationPQOnlyOut(uint256S("18"), 0); coins.AddCoin(legacyOut, Coin(legacy, 19, false), true); coins.AddCoin(historicalLookalike, Coin(tagged, 19, false), true); - coins.AddCoin(protectedOut, Coin(tagged, activationHeight, false), true); - coins.AddCoin(unconfirmedParent, Coin(tagged, MEMPOOL_HEIGHT, false), true); + coins.AddCoin(protectedOut, Coin(pqOnly, activationHeight, false), true); + coins.AddCoin(unconfirmedParent, Coin(pqOnly, MEMPOOL_HEIGHT, false), true); + coins.AddCoin(invalidDualOut, Coin(tagged, activationHeight, false), true); + coins.AddCoin(preActivationPQOnlyOut, + Coin(pqOnly, activationHeight - 1, false), true); coins.AddCoin(matchingAnchor, Coin(CTxOut(1000, GetScriptForWitnessV2PQ(program)), 19, false), true); coins.AddCoin(wrongAnchor, Coin(CTxOut(1000, GetScriptForWitnessV2PQ(otherProgram)), 19, false), true); @@ -882,7 +890,16 @@ BOOST_FIXTURE_TEST_SUITE(asset_tx_tests, BasicTestingSetup) CScript taggedClassScript; BOOST_REQUIRE(BuildPQAssetTaggedScript(script, program, taggedClassScript)); - script = taggedClassScript; + check.vout[0].scriptPubKey = taggedClassScript; + CValidationState dualOutput; + BOOST_CHECK(!Consensus::CheckTxPQAssets(CTransaction(check), dualOutput, + coins, activationHeight)); + BOOST_CHECK_EQUAL(dualOutput.GetRejectReason(), "bad-pq-asset-output"); + + CScript pqOnlyClassScript; + BOOST_REQUIRE(BuildPQOnlyAssetScript(script, program, + pqOnlyClassScript)); + script = pqOnlyClassScript; check.vout[0].scriptPubKey = script; uint256 hash; hash.SetNull(); @@ -931,17 +948,38 @@ BOOST_FIXTURE_TEST_SUITE(asset_tx_tests, BasicTestingSetup) BOOST_CHECK(!Consensus::CheckTxPQAssets(CTransaction(spend), untagged, coins, activationHeight)); BOOST_CHECK_EQUAL(untagged.GetRejectReason(), "bad-pq-asset-output"); - spend.vout[0] = tagged; + spend.vout[0] = pqOnly; bool protectedInput = true; CValidationState migration; BOOST_CHECK(Consensus::CheckTxPQAssets(CTransaction(spend), migration, coins, activationHeight, &protectedInput)); BOOST_CHECK(!protectedInput); + + spend.vout[0] = tagged; + CValidationState dualOutput; + BOOST_CHECK(!Consensus::CheckTxPQAssets(CTransaction(spend), dualOutput, + coins, activationHeight)); + BOOST_CHECK_EQUAL(dualOutput.GetRejectReason(), "bad-pq-asset-output"); + spend.vout[0] = pqOnly; spend.vin[0].prevout = historicalLookalike; CValidationState historical; BOOST_CHECK(Consensus::CheckTxPQAssets(CTransaction(spend), historical, coins, activationHeight, &protectedInput)); BOOST_CHECK(!protectedInput); + spend.vin[0].scriptSig = CScript() << OP_1; + CValidationState historicalScriptSig; + BOOST_CHECK(Consensus::CheckTxPQAssets(CTransaction(spend), historicalScriptSig, + coins, activationHeight)); + spend.vin[0].scriptSig.clear(); + + // A lookalike created before activation remains outside the anchor + // rule by origin height. Wallets must not label it as PQ custody. + spend.vin[0].prevout = preActivationPQOnlyOut; + CValidationState preActivationPQOnly; + BOOST_CHECK(Consensus::CheckTxPQAssets(CTransaction(spend), + preActivationPQOnly, coins, + activationHeight, &protectedInput)); + BOOST_CHECK(!protectedInput); spend.vin[0].prevout = protectedOut; CValidationState missing; @@ -956,6 +994,18 @@ BOOST_FIXTURE_TEST_SUITE(asset_tx_tests, BasicTestingSetup) BOOST_CHECK(Consensus::CheckTxPQAssets(CTransaction(spend), matched, coins, activationHeight, &protectedInput)); BOOST_CHECK(protectedInput); + spend.vin[0].scriptSig = CScript() << OP_1; + CValidationState malleable; + BOOST_CHECK(!Consensus::CheckTxPQAssets(CTransaction(spend), malleable, + coins, activationHeight)); + BOOST_CHECK_EQUAL(malleable.GetRejectReason(), "bad-pq-asset-scriptsig"); + spend.vin[0].scriptSig.clear(); + + spend.vin[0].prevout = invalidDualOut; + CValidationState invalidDual; + BOOST_CHECK(!Consensus::CheckTxPQAssets(CTransaction(spend), invalidDual, + coins, activationHeight)); + BOOST_CHECK_EQUAL(invalidDual.GetRejectReason(), "bad-pq-asset-prevout"); spend.vin[0].prevout = unconfirmedParent; CValidationState mempoolParent; diff --git a/src/test/script_standard_tests.cpp b/src/test/script_standard_tests.cpp index 82835549c1..ab36c72388 100644 --- a/src/test/script_standard_tests.cpp +++ b/src/test/script_standard_tests.cpp @@ -3,11 +3,18 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include "assets/assets.h" +#include "base58.h" +#include "hash.h" #include "key.h" #include "keystore.h" +#include "pqkey.h" +#include "policy/policy.h" +#include "primitives/transaction.h" #include "script/ismine.h" #include "script/script.h" #include "script/script_error.h" +#include "script/sign.h" #include "script/standard.h" #include "test/test_raven.h" @@ -799,4 +806,76 @@ BOOST_FIXTURE_TEST_SUITE(script_standard_tests, BasicTestingSetup) } } + BOOST_AUTO_TEST_CASE(pq_only_asset_signs_without_classical_key_test) + { + CKey classicalKey; + classicalKey.MakeNewKey(true); + const CKeyID classicalID = classicalKey.GetPubKey().GetID(); + + CPQKey pqKey; + pqKey.MakeNewKey(); + BOOST_REQUIRE(pqKey.IsValid()); + const CPQPubKey pqPubKey = pqKey.GetPubKey(); + const uint256 program = pqPubKey.GetWitnessProgram(); + const CKeyID assetID(Hash160(program.begin(), program.end())); + CKeyID decodedAssetID; + uint256 decodedProgram; + BOOST_CHECK(DecodePQOnlyAssetDestination( + EncodePQAssetDestination(assetID, program), decodedAssetID, + decodedProgram)); + BOOST_CHECK(decodedAssetID == assetID); + BOOST_CHECK(decodedProgram == program); + BOOST_CHECK(!DecodePQOnlyAssetDestination( + EncodePQAssetDestination(classicalID, program), decodedAssetID, + decodedProgram)); + + CBasicKeyStore keystore; + BOOST_REQUIRE(keystore.AddPQKeyPubKey(pqKey, pqPubKey)); + BOOST_CHECK(!keystore.HaveKey(classicalID)); + CBasicKeyStore noPQKey; + BOOST_REQUIRE(noPQKey.AddKey(classicalKey)); + + CScript transfer = GetScriptForDestination(classicalID); + CAssetTransfer("RAVENTEST", COIN).ConstructTransaction(transfer); + CScript owner = GetScriptForDestination(classicalID); + CNewAsset("RAVENTEST", COIN, 0, 1, 0, "").ConstructOwnerTransaction(owner); + CScript reissue = GetScriptForDestination(classicalID); + CReissueAsset("RAVENTEST", COIN, 0, 1, "").ConstructTransaction(reissue); + + for (const CScript& legacy : {transfer, owner, reissue}) { + CScript pqOnly; + BOOST_REQUIRE(BuildPQOnlyAssetScript(legacy, program, pqOnly)); + uint256 parsed; + BOOST_REQUIRE(IsPQOnlyAssetScript(pqOnly, parsed)); + BOOST_CHECK(parsed == program); + CScript tagged; + BOOST_REQUIRE(BuildPQAssetTaggedScript(legacy, program, tagged)); + BOOST_CHECK(!IsPQOnlyAssetScript(tagged, parsed)); + + // This generic query has no origin height, so the preactivation + // anyone-can-spend output must not be credited as protected custody. + BOOST_CHECK_EQUAL(IsMine(keystore, pqOnly), ISMINE_NO); + + CMutableTransaction funding; + funding.vin.emplace_back(COutPoint(uint256S("01"), 0)); + funding.vout.emplace_back(0, pqOnly); + const CTransaction funded(funding); + CMutableTransaction spending; + spending.vin.emplace_back(COutPoint(funded.GetHash(), 0)); + spending.vout.emplace_back(0, legacy); + + BOOST_REQUIRE(SignSignature(keystore, funded, spending, 0, SIGHASH_ALL)); + BOOST_CHECK(spending.vin[0].scriptSig.empty()); + BOOST_CHECK(spending.vin[0].scriptWitness.IsNull()); + ScriptError error = SCRIPT_ERR_UNKNOWN_ERROR; + BOOST_CHECK(VerifyScript(spending.vin[0].scriptSig, pqOnly, + &spending.vin[0].scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, + MutableTransactionSignatureChecker(&spending, 0, 0), &error)); + BOOST_CHECK_EQUAL(error, SCRIPT_ERR_OK); + + CMutableTransaction classicalAttempt = spending; + BOOST_CHECK(!SignSignature(noPQKey, funded, classicalAttempt, 0, SIGHASH_ALL)); + } + } + BOOST_AUTO_TEST_SUITE_END() From 5f34f8cbf3ea727477b99f8becf66dabe95088be Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:13:54 +0200 Subject: [PATCH 030/195] [FINDING-158][FINDING-162][FINDING-163] Preserve PQ-only wallet custody and hide asset IDs from RVN lists --- src/coins.cpp | 11 +- src/qt/addresstablemodel.cpp | 5 +- src/wallet/rpcwallet.cpp | 31 ++-- src/wallet/test/pq_wallet_tests.cpp | 55 +++++++ src/wallet/wallet.cpp | 139 ++++++++++++------ src/wallet/wallet.h | 2 + src/wallet/walletdb.cpp | 9 +- .../functional/feature_pq_asset_source_rpc.py | 44 ++++-- .../feature_pq_asset_wallet_ownership.py | 57 +++---- 9 files changed, 244 insertions(+), 109 deletions(-) diff --git a/src/coins.cpp b/src/coins.cpp index d963b00b29..2f6c29acca 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -29,16 +29,7 @@ static bool WalletOwnsAssetForMessageSubscription(const CTxOut& output, const auto candidate = mapBlockIndex.find(blockHash); if (candidate == mapBlockIndex.end() || candidate->second->nHeight != height) return false; - if (vpwallets[0]->IsMine(output) != ISMINE_SPENDABLE) - return false; - - const int activationHeight = GetPQAssetActivationHeightForPrev( - candidate->second->pprev, GetParams().GetConsensus()); - if (activationHeight < 0 || height < activationHeight) - return true; - - uint256 program; - return GetPQAssetProgram(output.scriptPubKey, program) && vpwallets[0]->HavePQKey(program); + return vpwallets[0]->IsMine(output, candidate->second) == ISMINE_SPENDABLE; } #endif diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index abf0c1c998..a83d6146de 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -86,6 +86,7 @@ class AddressTablePriv const CTxDestination& address = item.first; bool fMine = IsMine(*wallet, address); AddressTableEntry::Type addressType = translateTransactionType( + wallet->IsPQOnlyAssetID(address) ? "pqasset" : QString::fromStdString(item.second.purpose), fMine); const std::string& strName = item.second.name; cachedAddressTable.append(AddressTableEntry(addressType, @@ -109,7 +110,9 @@ class AddressTablePriv int lowerIndex = (lower - cachedAddressTable.begin()); int upperIndex = (upper - cachedAddressTable.begin()); bool inModel = (lower != upper); - AddressTableEntry::Type newEntryType = translateTransactionType(purpose, isMine); + AddressTableEntry::Type newEntryType = translateTransactionType( + wallet->IsPQOnlyAssetID(DecodeDestination(address.toStdString())) ? + "pqasset" : purpose, isMine); switch(status) { diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 023768bdaa..79363908b4 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -12,6 +12,7 @@ #include "consensus/validation.h" #include "core_io.h" #include "httpserver.h" +#include "hash.h" #include "validation.h" #include "net.h" #include "policy/feerate.h" @@ -265,7 +266,7 @@ UniValue getnewpqassetaddress(const JSONRPCRequest& request) CWallet* const pwallet = GetWalletForJSONRPCRequest(request); if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) return NullUniValue; if (request.fHelp || request.params.size() > 1) - throw std::runtime_error("getnewpqassetaddress ( \"account\" )\nReturns a classical|PQ asset destination for active RIP-25 asset rules.\n"); + throw std::runtime_error("getnewpqassetaddress ( \"account\" )\nReturns an asset identifier|PQ address destination for active RIP-25 asset rules. The identifier does not have a classical private key.\n"); LOCK2(cs_main, pwallet->cs_wallet); const int activationHeight = GetPQAssetActivationHeightForPrev(chainActive.Tip(), GetParams().GetConsensus()); @@ -275,22 +276,17 @@ UniValue getnewpqassetaddress(const JSONRPCRequest& request) std::string account; if (!request.params[0].isNull()) account = AccountFromValue(request.params[0]); - pwallet->TopUpKeyPool(); - CReserveKey reserveClassicalKey(pwallet); - CPubKey classicalPubKey; - if (!reserveClassicalKey.GetReservedKey(classicalPubKey)) - throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); CPQPubKey pqPubKey; if (!pwallet->GenerateNewPQKey(pqPubKey)) throw JSONRPCError(RPC_WALLET_ERROR, "Error: Failed to derive and persist ML-DSA-44 keypair"); - const CKeyID classicalKey = classicalPubKey.GetID(); const WitnessV2PQDestination pqDestination(pqPubKey.GetWitnessProgram()); - if (!pwallet->StoreOwnedPQAssetDestination(classicalKey, pqDestination.witnessProgram)) - throw JSONRPCError(RPC_WALLET_ERROR, "Error: Failed to persist protected asset destination pairing"); - reserveClassicalKey.KeepKey(); - pwallet->SetAddressBook(classicalKey, account, "receive"); + const CKeyID assetID(Hash160(pqDestination.witnessProgram.begin(), + pqDestination.witnessProgram.end())); + if (!pwallet->StoreOwnedPQAssetDestination(assetID, pqDestination.witnessProgram)) + throw JSONRPCError(RPC_WALLET_ERROR, "Error: Failed to persist PQ-only asset destination"); + pwallet->SetAddressBook(assetID, account, "pqasset"); pwallet->SetAddressBook(pqDestination, account, "receive"); - return EncodePQAssetDestination(classicalKey, pqDestination.witnessProgram); + return EncodePQAssetDestination(assetID, pqDestination.witnessProgram); } UniValue listpqassetaddresses(const JSONRPCRequest& request) @@ -299,7 +295,7 @@ UniValue listpqassetaddresses(const JSONRPCRequest& request) if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) return NullUniValue; if (request.fHelp || request.params.size() != 0) throw std::runtime_error( - "listpqassetaddresses\nReturns saved wallet-owned classical|PQ asset destinations and whether PQ asset rules are active for the next block. An inactive destination is not safe for receiving assets.\n"); + "listpqassetaddresses\nReturns saved wallet-owned PQ-only and historical paired asset destinations and whether PQ asset rules are active for the next block. Historical pairs cannot receive newly created asset outputs after activation. An inactive destination is not safe for receiving assets.\n"); LOCK2(cs_main, pwallet->cs_wallet); const int activationHeight = GetPQAssetActivationHeightForPrev(chainActive.Tip(), GetParams().GetConsensus()); @@ -511,7 +507,7 @@ UniValue getaddressesbyaccount(const JSONRPCRequest& request) for (const std::pair& item : pwallet->mapAddressBook) { const CTxDestination& dest = item.first; const std::string& strName = item.second.name; - if (strName == strAccount) { + if (strName == strAccount && !pwallet->IsPQOnlyAssetID(dest)) { ret.push_back(EncodeDestination(dest)); } } @@ -797,6 +793,8 @@ UniValue listaddressgroupings(const JSONRPCRequest& request) UniValue jsonGrouping(UniValue::VARR); for (const CTxDestination& address : grouping) { + if (pwallet->IsPQOnlyAssetID(address)) + continue; UniValue addressInfo(UniValue::VARR); addressInfo.push_back(EncodeDestination(address)); addressInfo.push_back(ValueFromAmount(balances[address])); @@ -807,7 +805,8 @@ UniValue listaddressgroupings(const JSONRPCRequest& request) } jsonGrouping.push_back(addressInfo); } - jsonGroupings.push_back(jsonGrouping); + if (!jsonGrouping.empty()) + jsonGroupings.push_back(jsonGrouping); } return jsonGroupings; } @@ -1575,6 +1574,8 @@ UniValue ListReceived(CWallet * const pwallet, const UniValue& params, bool fByA std::map mapAccountTally; for (const std::pair& item : pwallet->mapAddressBook) { const CTxDestination& dest = item.first; + if (pwallet->IsPQOnlyAssetID(dest)) + continue; const std::string& strAccount = item.second.name; std::map::iterator it = mapTally.find(dest); if (it == mapTally.end() && !fIncludeEmpty) diff --git a/src/wallet/test/pq_wallet_tests.cpp b/src/wallet/test/pq_wallet_tests.cpp index 3b5af0f6d7..89bfd02fb6 100644 --- a/src/wallet/test/pq_wallet_tests.cpp +++ b/src/wallet/test/pq_wallet_tests.cpp @@ -623,6 +623,61 @@ BOOST_AUTO_TEST_CASE(protected_asset_destination_pair_survives_reload) BOOST_CHECK(unrelatedWallet.GetOwnedPQAssetDestinations().empty()); } +BOOST_AUTO_TEST_CASE(pq_only_asset_destination_needs_no_classical_key) +{ + const std::string filename = "pq-only-asset-destination-wallet.dat"; + CPQKey pqKey; + pqKey.MakeNewKey(); + BOOST_REQUIRE(pqKey.IsValid()); + const uint256 program = pqKey.GetPubKey().GetWitnessProgram(); + const CKeyID pseudoAddress(Hash160(program.begin(), program.end())); + const std::string descriptor = EncodePQAssetDestination(pseudoAddress, program); + + { + std::unique_ptr wallet = LoadPQWallet(filename); + LOCK(wallet->cs_wallet); + BOOST_REQUIRE(wallet->AddPQKeyPubKey(pqKey, pqKey.GetPubKey())); + BOOST_CHECK(!wallet->HaveKey(pseudoAddress)); + BOOST_REQUIRE(wallet->StoreOwnedPQAssetDestination(pseudoAddress, program)); + BOOST_CHECK(wallet->IsPQOnlyAssetID(pseudoAddress)); + BOOST_CHECK(wallet->GetOwnedPQAssetDestinations() == + std::vector{descriptor}); + } + bitdb.Flush(false); + + std::unique_ptr reloaded = LoadPQWallet(filename); + BOOST_CHECK(!reloaded->HaveKey(pseudoAddress)); + BOOST_CHECK(reloaded->IsPQOnlyAssetID(pseudoAddress)); + BOOST_CHECK(reloaded->GetOwnedPQAssetDestinations() == + std::vector{descriptor}); + + CWallet dummyWallet; + std::string backupFilename; + BOOST_REQUIRE(CWalletDB::Recover( + filename, &dummyWallet, CWalletDB::RecoverKeysOnlyFilter, backupFilename)); + bitdb.Flush(false); + std::unique_ptr recovered = LoadPQWallet(filename); + BOOST_CHECK(recovered->GetOwnedPQAssetDestinations() == + std::vector{descriptor}); + BOOST_CHECK(recovered->IsPQOnlyAssetID(pseudoAddress)); + BOOST_CHECK_EQUAL(recovered->mapAddressBook[pseudoAddress].purpose, "pqasset"); + + auto retainedPair = [&](const CKeyID& address, const std::string& key) { + CWallet filterWallet; + CDataStream dbKey(SER_DISK, CLIENT_VERSION); + CDataStream dbValue(SER_DISK, CLIENT_VERSION); + dbKey << std::string("destdata") << EncodeDestination(address) << key; + dbValue << descriptor; + return CWalletDB::RecoverKeysOnlyFilter( + &filterWallet, std::move(dbKey), std::move(dbValue)); + }; + BOOST_CHECK(retainedPair(pseudoAddress, "pqasset:destination:v2")); + CKey unrelatedKey; + unrelatedKey.MakeNewKey(true); + BOOST_CHECK(!retainedPair(unrelatedKey.GetPubKey().GetID(), + "pqasset:destination:v2")); +} + BOOST_AUTO_TEST_CASE(protected_asset_destination_pair_survives_key_only_recovery) { const std::string filename = "pq-asset-pair-salvage-wallet.dat"; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 84ed69d995..167d3ff69c 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -13,6 +13,7 @@ #include "consensus/consensus.h" #include "consensus/validation.h" #include "fs.h" +#include "hash.h" #include "init.h" #include "key.h" #include "keystore.h" @@ -1941,12 +1942,22 @@ isminetype CWallet::IsMine(const CTxOut& txout) const isminetype CWallet::IsMine(const CTxOut& txout, const CBlockIndex* originBlock) const { LOCK2(cs_main, cs_wallet); + const int activationHeight = + GetPQAssetActivationHeightForPrev( + originBlock ? originBlock->pprev : chainActive.Tip(), + GetParams().GetConsensus()); + uint256 pqOnlyProgram; + if (IsPQOnlyAssetScript(txout.scriptPubKey, pqOnlyProgram)) { + if (activationHeight < 0 || + (originBlock && originBlock->nHeight < activationHeight)) + return ISMINE_NO; + return HavePQKey(pqOnlyProgram) ? ISMINE_SPENDABLE : ISMINE_NO; + } + const isminetype mine = IsMine(txout); if (!(mine & ISMINE_SPENDABLE) || !txout.scriptPubKey.IsAssetScript()) return mine; - const int activationHeight = - GetPQAssetActivationHeightForPrev(chainActive.Tip(), GetParams().GetConsensus()); return HasRequiredPQAssetKey(*this, txout, originBlock ? 1 : 0, originBlock, activationHeight) ? mine : ISMINE_NO; } @@ -3092,10 +3103,7 @@ void CWallet::AvailableCoinsAll(std::vector& vCoins, std::maptx->vout[i].scriptPubKey.IsAssetScript(nType, fIsOwner); - // A post-activation asset belongs to the wallet only if it - // has both its classical key and the PQ key named by the - // output. Historical lookalikes keep their old ownership - // semantics, including after a reorg across activation. + // Verify the PQ key required by a protected asset output. if (fGetAssets && isAssetScript && !HasRequiredPQAssetKey(*this, pcoin->tx->vout[i], nDepth, originBlock, pqAssetActivationHeight)) @@ -3114,7 +3122,7 @@ void CWallet::AvailableCoinsAll(std::vector& vCoins, std::maptx->vout[i]); + isminetype mine = IsMine(pcoin->tx->vout[i], originBlock); if (mine == ISMINE_NO) { continue; @@ -4086,8 +4094,8 @@ bool CWallet::CreateTransactionAll(const std::vector& vecSend, CWall if (!pqAssetsActive) return true; CScript tagged; - if (!program || !BuildPQAssetTaggedScript(script, *program, tagged)) { - strFailReason = _("Active PQ asset output requires a canonical classical|PQ asset destination"); + if (!program || !BuildPQOnlyAssetScript(script, *program, tagged)) { + strFailReason = _("Active PQ asset output requires a canonical PQ-only asset destination"); return false; } script = std::move(tagged); @@ -4340,7 +4348,7 @@ bool CWallet::CreateTransactionAll(const std::vector& vecSend, CWall if (!boost::get(&coin_control.assetDestChange)) { if (!IsSupportedAssetDestination(coin_control.assetDestChange) || !coin_control.pqAssetChangeProgram) { - strFailReason = _("Active PQ asset change requires a canonical classical|PQ destination"); + strFailReason = _("Active PQ asset change requires a canonical PQ-only destination"); return false; } changeDestination = coin_control.assetDestChange; @@ -4397,7 +4405,7 @@ bool CWallet::CreateTransactionAll(const std::vector& vecSend, CWall CAssetTransfer assetTransfer(assetChange.first, assetChange.second); assetTransfer.ConstructTransaction(scriptAssetChange); CScript tagged; - if (!BuildPQAssetTaggedScript(scriptAssetChange, changeProgram, tagged)) { + if (!BuildPQOnlyAssetScript(scriptAssetChange, changeProgram, tagged)) { strFailReason = _("Could not construct canonical PQ asset change"); return false; } @@ -5293,7 +5301,9 @@ std::map CWallet::GetAddressBalances() for (unsigned int i = 0; i < pcoin->tx->vout.size(); i++) { CTxDestination addr; - if (!IsMine(pcoin->tx->vout[i], originBlock)) + uint256 assetProgram; + if (IsPQOnlyAssetScript(pcoin->tx->vout[i].scriptPubKey, assetProgram) || + !IsMine(pcoin->tx->vout[i], originBlock)) continue; if(!ExtractDestination(pcoin->tx->vout[i].scriptPubKey, addr)) continue; @@ -5331,7 +5341,10 @@ std::set< std::set > CWallet::GetAddressGroupings() CTxDestination address; if(!IsMine(txin)) /* If this input isn't mine, ignore it */ continue; - if(!ExtractDestination(mapWallet[txin.prevout.hash].tx->vout[txin.prevout.n].scriptPubKey, address)) + const CScript& prevScript = mapWallet[txin.prevout.hash].tx->vout[txin.prevout.n].scriptPubKey; + uint256 assetProgram; + if (IsPQOnlyAssetScript(prevScript, assetProgram) || + !ExtractDestination(prevScript, address)) continue; grouping.insert(address); any_mine = true; @@ -5345,7 +5358,9 @@ std::set< std::set > CWallet::GetAddressGroupings() { CTxDestination txoutAddr; const CTxOut& txout = pcoin->tx->vout[i]; - if(!ExtractDestination(txout.scriptPubKey, txoutAddr)) + uint256 assetProgram; + if (IsPQOnlyAssetScript(txout.scriptPubKey, assetProgram) || + !ExtractDestination(txout.scriptPubKey, txoutAddr)) continue; grouping.insert(txoutAddr); } @@ -5362,7 +5377,9 @@ std::set< std::set > CWallet::GetAddressGroupings() if (IsMine(txout, originBlock)) { CTxDestination address; - if(!ExtractDestination(txout.scriptPubKey, address)) + uint256 assetProgram; + if (IsPQOnlyAssetScript(txout.scriptPubKey, assetProgram) || + !ExtractDestination(txout.scriptPubKey, address)) continue; grouping.insert(address); groupings.insert(grouping); @@ -5414,7 +5431,7 @@ std::set CWallet::GetAccountAddresses(const std::string& strAcco { const CTxDestination& address = item.first; const std::string& strName = item.second.name; - if (strName == strAccount) + if (strName == strAccount && !IsPQOnlyAssetID(address)) result.insert(address); } return result; @@ -5663,7 +5680,16 @@ bool CWallet::EraseDestData(const CTxDestination &dest, const std::string &key) bool CWallet::LoadDestData(const CTxDestination &dest, const std::string &key, const std::string &value) { - mapAddressBook[dest].destdata.insert(std::make_pair(key, value)); + CAddressBookData& data = mapAddressBook[dest]; + data.destdata.insert(std::make_pair(key, value)); + if (key == "pqasset:destination:v2") { + const CKeyID* assetID = boost::get(&dest); + CKeyID decodedID; + uint256 program; + if (assetID && DecodePQOnlyAssetDestination(value, decodedID, program) && + decodedID == *assetID && EncodePQAssetDestination(decodedID, program) == value) + data.purpose = "pqasset"; + } return true; } @@ -5699,13 +5725,35 @@ std::vector CWallet::GetDestValues(const std::string& prefix) const namespace { const char* const PQ_ASSET_DESTDATA_KEY = "pqasset:destination:v1"; +const char* const PQ_ONLY_ASSET_DESTDATA_KEY = "pqasset:destination:v2"; +} + +bool CWallet::IsPQOnlyAssetID(const CTxDestination& dest) const +{ + LOCK(cs_wallet); + const CKeyID* assetID = boost::get(&dest); + if (!assetID) + return false; + const auto address = mapAddressBook.find(dest); + if (address == mapAddressBook.end()) + return false; + const auto pair = address->second.destdata.find(PQ_ONLY_ASSET_DESTDATA_KEY); + if (pair == address->second.destdata.end()) + return false; + CKeyID decodedID; + uint256 program; + return DecodePQOnlyAssetDestination(pair->second, decodedID, program) && + decodedID == *assetID && + EncodePQAssetDestination(decodedID, program) == pair->second; } bool CWallet::StoreOwnedPQAssetDestination(const CKeyID& classicalKey, const uint256& pqProgram) { LOCK(cs_wallet); - if (!HaveKey(classicalKey) || !HavePQKey(pqProgram)) + const bool pqOnly = classicalKey == CKeyID(Hash160(pqProgram.begin(), pqProgram.end())); + if (!HavePQKey(pqProgram) || (!pqOnly && !HaveKey(classicalKey))) return false; + const char* const destdataKey = pqOnly ? PQ_ONLY_ASSET_DESTDATA_KEY : PQ_ASSET_DESTDATA_KEY; const std::string descriptor = EncodePQAssetDestination(classicalKey, pqProgram); CKeyID decodedClassical; @@ -5717,27 +5765,29 @@ bool CWallet::StoreOwnedPQAssetDestination(const CKeyID& classicalKey, const uin const auto existingAddress = mapAddressBook.find(classicalKey); if (existingAddress != mapAddressBook.end()) { - const auto existingPair = existingAddress->second.destdata.find(PQ_ASSET_DESTDATA_KEY); + const auto existingPair = existingAddress->second.destdata.find(destdataKey); if (existingPair != existingAddress->second.destdata.end()) return existingPair->second == descriptor; } - // A PQ key must not be silently reused by a different classical owner. + // A PQ key must not be silently reused by another asset identity. for (const auto& address : mapAddressBook) { - const auto existingPair = address.second.destdata.find(PQ_ASSET_DESTDATA_KEY); - if (existingPair == address.second.destdata.end()) - continue; - CKeyID pairedClassical; - uint256 pairedProgram; - if (DecodePQAssetDestination(existingPair->second, pairedClassical, pairedProgram) && - pairedProgram == pqProgram) - return false; + for (const char* key : {PQ_ASSET_DESTDATA_KEY, PQ_ONLY_ASSET_DESTDATA_KEY}) { + const auto existingPair = address.second.destdata.find(key); + if (existingPair == address.second.destdata.end()) + continue; + CKeyID pairedClassical; + uint256 pairedProgram; + if (DecodePQAssetDestination(existingPair->second, pairedClassical, pairedProgram) && + pairedProgram == pqProgram) + return false; + } } if (!CWalletDB(*dbw).WriteDestData( - EncodeDestination(classicalKey), PQ_ASSET_DESTDATA_KEY, descriptor)) + EncodeDestination(classicalKey), destdataKey, descriptor)) return false; - mapAddressBook[classicalKey].destdata.emplace(PQ_ASSET_DESTDATA_KEY, descriptor); + mapAddressBook[classicalKey].destdata.emplace(destdataKey, descriptor); return true; } @@ -5750,18 +5800,23 @@ std::vector CWallet::GetOwnedPQAssetDestinations() const const CKeyID* associatedClassical = boost::get(&address.first); if (!associatedClassical) continue; - const auto pair = address.second.destdata.find(PQ_ASSET_DESTDATA_KEY); - if (pair == address.second.destdata.end()) - continue; - CKeyID decodedClassical; - uint256 decodedProgram; - if (!DecodePQAssetDestination(pair->second, decodedClassical, decodedProgram) || - decodedClassical != *associatedClassical || - EncodePQAssetDestination(decodedClassical, decodedProgram) != pair->second || - !HaveKey(decodedClassical) || !HavePQKey(decodedProgram) || - !usedPrograms.insert(decodedProgram).second) - continue; - descriptors.push_back(pair->second); + for (const char* key : {PQ_ASSET_DESTDATA_KEY, PQ_ONLY_ASSET_DESTDATA_KEY}) { + const auto pair = address.second.destdata.find(key); + if (pair == address.second.destdata.end()) + continue; + CKeyID decodedClassical; + uint256 decodedProgram; + if (!DecodePQAssetDestination(pair->second, decodedClassical, decodedProgram) || + decodedClassical != *associatedClassical || + EncodePQAssetDestination(decodedClassical, decodedProgram) != pair->second || + !HavePQKey(decodedProgram) || + (key == PQ_ASSET_DESTDATA_KEY && !HaveKey(decodedClassical)) || + (key == PQ_ONLY_ASSET_DESTDATA_KEY && + decodedClassical != CKeyID(Hash160(decodedProgram.begin(), decodedProgram.end()))) || + !usedPrograms.insert(decodedProgram).second) + continue; + descriptors.push_back(pair->second); + } } return descriptors; } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 1aea5d733d..4e1d998ea2 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -995,6 +995,8 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface bool StoreOwnedPQAssetDestination(const CKeyID& classicalKey, const uint256& pqProgram); //! Return persisted asset destinations whose pairing and private-key ownership still validate. std::vector GetOwnedPQAssetDestinations() const; + //! Recognize a synthetic PQ-only asset identifier in this wallet's destination data. + bool IsPQOnlyAssetID(const CTxDestination& dest) const; //! Adds a watch-only address to the store, and saves it to disk. bool AddWatchOnly(const CScript& dest, int64_t nCreateTime); diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 9e8999f6f2..d549992dce 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -10,6 +10,7 @@ #include "consensus/tx_verify.h" #include "consensus/validation.h" #include "fs.h" +#include "hash.h" #include "protocol.h" #include "serialize.h" #include "sync.h" @@ -1335,14 +1336,18 @@ bool CWalletDB::RecoverKeysOnlyFilter(void *callbackData, CDataStream ssKey, CDa std::string parsedType, address, key, descriptor; ssKey >> parsedType >> address >> key; ssValue >> descriptor; - if (parsedType != "destdata" || key != "pqasset:destination:v1" || + if (parsedType != "destdata" || + (key != "pqasset:destination:v1" && + key != "pqasset:destination:v2") || !ssKey.empty() || !ssValue.empty()) return false; CKeyID classicalKey; uint256 pqProgram; return DecodePQAssetDestination(descriptor, classicalKey, pqProgram) && address == EncodeDestination(classicalKey) && - descriptor == EncodePQAssetDestination(classicalKey, pqProgram); + descriptor == EncodePQAssetDestination(classicalKey, pqProgram) && + (key == "pqasset:destination:v1" || + classicalKey == CKeyID(Hash160(pqProgram.begin(), pqProgram.end()))); } catch (...) { return false; } diff --git a/test/functional/feature_pq_asset_source_rpc.py b/test/functional/feature_pq_asset_source_rpc.py index aca6f16ff8..ff23ab1f1e 100755 --- a/test/functional/feature_pq_asset_source_rpc.py +++ b/test/functional/feature_pq_asset_source_rpc.py @@ -8,6 +8,7 @@ from decimal import Decimal from test_framework.mininode import CTransaction, from_hex +from test_framework.script import hash160 from test_framework.test_framework import RavenTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error @@ -52,10 +53,21 @@ def run_test(self): assert_equal(node.getblockchaininfo()['bip9_softforks']['pq_assets']['status'], 'active') - source = node.getnewpqassetaddress() + source = node.getnewpqassetaddress('pq-assets') source_classical, source_pq = source.split('|') source_program = bytes.fromhex( node.validateaddress(source_pq)['scriptPubKey'])[2:] + source_identity = node.validateaddress(source_classical) + assert_equal(source_identity['ismine'], False) + assert_equal(bytes.fromhex(source_identity['scriptPubKey'])[3:23], + hash160(source_program)) + assert_equal(node.validateaddress(source_pq)['ismine'], True) + account_addresses = node.getaddressesbyaccount('pq-assets') + assert source_classical not in account_addresses + assert source_pq in account_addresses + received_addresses = {row['address'] for row in + node.listreceivedbyaddress(0, True)} + assert source_classical not in received_addresses destination = node.getnewpqassetaddress() _, destination_pq = destination.split('|') destination_program = bytes.fromhex( @@ -69,7 +81,7 @@ def run_test(self): {source, destination}) # A confirmed legacy asset and its owner token can move in full to - # tagged outputs after activation without a PQ anchor on the old input. + # PQ-only outputs after activation without a PQ anchor on the old input. migrated_asset = node.transferfromaddress( 'PQSOURCELEGACY', legacy_address, 1, destination)[0] migrated_second = node.transferfromaddresses( @@ -106,6 +118,9 @@ def run_test(self): node.issue('PQSOURCEACTIVE', 5, source) node.generate(1) + grouping_addresses = {entry[0] for group in node.listaddressgroupings() + for entry in group} + assert source_classical not in grouping_addresses node.sendtoaddress(source_pq, Decimal('1')) node.generate(1) @@ -123,11 +138,11 @@ def run_test(self): destination, '', 0, '', source) assert_raises_rpc_error( - -5, 'canonical classical|PQ asset destination', + -5, 'canonical PQ-only asset destination', node.transferfromaddress, 'PQSOURCEACTIVE', source, 2, destination, '', 0, '', source_classical) assert_raises_rpc_error( - -5, 'canonical classical|PQ asset destination', + -5, 'canonical PQ-only asset destination', node.transferfromaddresses, 'PQSOURCEACTIVE', [source], 2, destination, '', 0, '', source_classical) @@ -148,14 +163,17 @@ def run_test(self): assert_equal(node.listmyassets('PQSOURCEACTIVE', True) ['PQSOURCEACTIVE']['balance'], 5) - # The address index combines protected outputs that share a - # classical key, even though their PQ programs differ. Its address - # row is not a valid active asset destination. - second_pq = node.getnewpqaddress() - second_pair = source_classical + '|' + second_pq + # Each PQ program has its own synthetic asset identifier. Reusing + # the first identifier with another program is not a destination. + second_pair = node.getnewpqassetaddress() + second_identity, second_pq = second_pair.split('|') second_program = bytes.fromhex( node.validateaddress(second_pq)['scriptPubKey'])[2:] assert second_program != source_program + assert_raises_rpc_error( + -5, 'canonical PQ-only asset destination', + node.transferfromaddress, 'PQSOURCEACTIVE', source, 1, + source_classical + '|' + second_pq) index_asset = 'PQINDEXSHARED' node.issue(index_asset, 2, source) node.sendtoaddress(source_pq, Decimal('1')) @@ -168,14 +186,16 @@ def run_test(self): assert_equal(node.listmyassets(index_asset, True) [index_asset]['balance'], 2) assert_equal(node.listaddressesbyasset(index_asset), - {source_classical: 2}) + {source_classical: 1, second_identity: 1}) assert_equal(node.listassetbalancesbyaddress(source_classical) - [index_asset], 2) + [index_asset], 1) + assert_equal(node.listassetbalancesbyaddress(second_identity) + [index_asset], 1) assert_raises_rpc_error(-5, 'Invalid Raven address', node.listassetbalancesbyaddress, second_pair) assert_raises_rpc_error( - -5, 'canonical classical|PQ asset destination', + -5, 'canonical PQ-only asset destination', node.transferfromaddress, index_asset, source, 1, source_classical) diff --git a/test/functional/feature_pq_asset_wallet_ownership.py b/test/functional/feature_pq_asset_wallet_ownership.py index f295d619e0..29a43433ae 100755 --- a/test/functional/feature_pq_asset_wallet_ownership.py +++ b/test/functional/feature_pq_asset_wallet_ownership.py @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Do not report a bit13 asset as spendable without its matching PQ key.""" +"""Credit post-bit13 assets to PQ keys and preserve historical ownership.""" from decimal import Decimal @@ -44,8 +44,6 @@ def run_test(self): asset_name = 'PQWALLETOWNERSHIP' historical_name = 'PQHISTORICALTAG' sender_asset_address = sender.getnewaddress() - recipient_asset_address = recipient.getnewaddress() - recipient_owner_address = recipient.getnewaddress() historical_recipient = recipient.getnewaddress() pq_address = sender.getnewpqaddress() pq_script = bytes.fromhex(sender.validateaddress(pq_address)['scriptPubKey']) @@ -111,18 +109,23 @@ def run_test(self): asset_outpoint = sender.listmyassets(asset_name, True)[asset_name]['outpoints'][0] owner_name = asset_name + '!' owner_outpoint = sender.listmyassets(owner_name, True)[owner_name]['outpoints'][0] + recipient_asset_descriptor = recipient.getnewpqassetaddress() + recipient_owner_descriptor = recipient.getnewpqassetaddress() + _, recipient_asset_pq = recipient_asset_descriptor.split('|') + _, recipient_owner_pq = recipient_owner_descriptor.split('|') native_coin = next(coin for coin in sender.listunspent() - if coin['amount'] > Decimal('2')) + if coin['amount'] > Decimal('3')) inputs = [ {'txid': asset_outpoint['txid'], 'vout': asset_outpoint['vout']}, {'txid': owner_outpoint['txid'], 'vout': owner_outpoint['vout']}, {'txid': native_coin['txid'], 'vout': native_coin['vout']}, ] outputs = { - recipient_asset_address + '|' + pq_address: {'transfer': {asset_name: 1}}, - recipient_owner_address + '|' + pq_address: {'transfer': {owner_name: 1}}, - pq_address: Decimal('1'), - sender.getnewaddress(): native_coin['amount'] - Decimal('1.01'), + recipient_asset_descriptor: {'transfer': {asset_name: 1}}, + recipient_owner_descriptor: {'transfer': {owner_name: 1}}, + recipient_asset_pq: Decimal('1'), + recipient_owner_pq: Decimal('1'), + sender.getnewaddress(): native_coin['amount'] - Decimal('2.01'), } migration = sender.createrawtransaction(inputs, outputs) signed = sender.signrawtransaction(migration) @@ -131,33 +134,33 @@ def run_test(self): sender.generate(1) self.sync_all() - # A classical key alone is not ownership of a protected asset. - assert_equal(recipient.listmyassets(asset_name, True), {}) - assert_equal(recipient.listmyassets(owner_name, True), {}) - assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', - recipient.gettransaction, migration_txid) - assert owner_name not in recipient.viewallmessagechannels() - assert_raises_rpc_error(-5, 'No addresses in wallet', - recipient.importprunedfunds, signed['hex'], - sender.gettxoutproof([migration_txid])) - assert all(bytes.fromhex(coin['scriptPubKey']) != pq_script - for coin in recipient.listunspent()) - - # Raw classical signing is possible, but consensus still requires the - # separate native witness-v2 anchor for each protected asset spend. + # The recipient owns each output through its PQ key and has no + # classical private key for either synthetic asset identifier. + for name, descriptor in ((asset_name, recipient_asset_descriptor), + (owner_name, recipient_owner_descriptor)): + asset_id, pq_destination = descriptor.split('|') + assert_equal(recipient.validateaddress(asset_id)['ismine'], False) + assert_equal(recipient.validateaddress(pq_destination)['ismine'], True) + assert_equal(recipient.listmyassets(name, True)[name]['balance'], 1) + assert owner_name in recipient.viewallmessagechannels() + + # A PQ-only asset input signs with an empty scriptSig, but consensus + # still requires the separate matching native witness-v2 anchor. decoded = sender.decoderawtransaction(signed['hex']) asset_vouts = {out['scriptPubKey']['asset']['name']: out['n'] for out in decoded['vout'] if 'asset' in out['scriptPubKey']} assert_equal(set(asset_vouts), {asset_name, owner_name}) - for name in (asset_name, owner_name): + for name, descriptor in ((asset_name, recipient_asset_descriptor), + (owner_name, recipient_owner_descriptor)): asset_input = {'txid': migration_txid, 'vout': asset_vouts[name]} spend = recipient.createrawtransaction( [{'txid': asset_input['txid'], 'vout': asset_input['vout']}], - {recipient.getnewaddress() + '|' + pq_address: - {'transfer': {name: 1}}}) + {descriptor: {'transfer': {name: 1}}}) spend_signed = recipient.signrawtransaction(spend) assert_equal(spend_signed['complete'], True) + spend_tx = from_hex(CTransaction(), spend_signed['hex']) + assert_equal(spend_tx.vin[0].scriptSig, b'') assert_raises_rpc_error(-26, 'bad-pq-asset-anchor', recipient.sendrawtransaction, spend_signed['hex']) @@ -202,8 +205,8 @@ def run_test(self): assert_equal(len(received), 1) assert received[0] in new_destinations received_destinations.add(received[0]) - classical, pq = received[0].split('|') - assert_equal(sender.validateaddress(classical)['ismine'], True) + asset_id, pq = received[0].split('|') + assert_equal(sender.validateaddress(asset_id)['ismine'], False) assert_equal(sender.validateaddress(pq)['ismine'], True) assert_equal(sender.listmyassets(name, True)[name]['balance'], 1) assert_equal(received_destinations, new_destinations) From b7153328d4b8dadd56ad2d3fd7062dee9dbf6a45 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:14:03 +0200 Subject: [PATCH 031/195] [FINDING-158][FINDING-164] Route asset RPC and Qt outputs to PQ-only scripts and test restricted state --- src/qt/guiutil.cpp | 4 +- src/qt/restrictedassetsdialog.cpp | 6 +- src/qt/sendassetsentry.cpp | 2 +- src/qt/walletmodel.cpp | 25 ++-- src/rpc/assets.cpp | 55 ++++---- src/rpc/rawtransaction.cpp | 15 ++- test/functional/feature_pq_asset_anchor.py | 140 ++++++++++++++++----- 7 files changed, 160 insertions(+), 87 deletions(-) diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 31a644c2ff..81d4446ae6 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -226,7 +226,7 @@ void setupAssetAddressWidget(QValidatedLineEdit *widget, QWidget *parent) { parent->setFocusProxy(widget); widget->setFont(getSubLabelFont()); - widget->setPlaceholderText(QObject::tr("Enter an asset destination (classical|PQ after activation)")); + widget->setPlaceholderText(QObject::tr("Enter an asset destination (PQ-only after activation)")); widget->setValidator(new RavenAssetAddressEntryValidator(parent)); widget->setCheckValidator(new RavenAssetAddressCheckValidator(parent)); } @@ -244,7 +244,7 @@ bool isValidAssetDestination(const QString& address) if (pqAssetDestinationRequired()) { CKeyID classicalKey; uint256 pqProgram; - return DecodePQAssetDestination(encoded, classicalKey, pqProgram); + return DecodePQOnlyAssetDestination(encoded, classicalKey, pqProgram); } return IsSupportedAssetDestination(DecodeDestination(encoded)); } diff --git a/src/qt/restrictedassetsdialog.cpp b/src/qt/restrictedassetsdialog.cpp index 9d7fe147ba..01319a0da3 100644 --- a/src/qt/restrictedassetsdialog.cpp +++ b/src/qt/restrictedassetsdialog.cpp @@ -63,8 +63,8 @@ bool ResolveGUIAdministrativeReturn(WalletModel* model, CReserveKey& reservekey, } CKeyID classicalKey; uint256 pqProgram; - if (!DecodePQAssetDestination(returnAddress, classicalKey, pqProgram)) { - errorText = QObject::tr("Authority return requires a canonical classical|PQ asset destination"); + if (!DecodePQOnlyAssetDestination(returnAddress, classicalKey, pqProgram)) { + errorText = QObject::tr("Authority return requires a canonical PQ-only asset destination"); return false; } return true; @@ -479,5 +479,3 @@ void RestrictedAssetsDialog::assignQualifierClicked() widget->clear(); } - - diff --git a/src/qt/sendassetsentry.cpp b/src/qt/sendassetsentry.cpp index 89121b949d..6fb3b85633 100644 --- a/src/qt/sendassetsentry.cpp +++ b/src/qt/sendassetsentry.cpp @@ -156,7 +156,7 @@ void SendAssetsEntry::on_addressBookButton_clicked() const QStringList destinations = model->getAssetDestinations(); if (destinations.isEmpty()) { QMessageBox::information(this, tr("PQ asset destination"), - tr("No saved protected asset destination is available. Paste the recipient's complete classical|PQ destination.")); + tr("No saved PQ-only asset destination is available. Paste the recipient's complete PQ-only destination.")); return; } bool selected = false; diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 5bf217fe65..d02ac89d77 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -22,6 +22,7 @@ #include "base58.h" #include "chain.h" +#include "hash.h" #include "keystore.h" #include "validation.h" #include "net.h" // for g_connman @@ -211,33 +212,33 @@ QString WalletModel::newAssetDestination() return addressTableModel->addRow(AddressTableModel::Receive, "", ""); LOCK2(cs_main, wallet->cs_wallet); - if (!GUIUtil::pqAssetDestinationRequired() || wallet->IsLocked() || !wallet->TopUpKeyPool()) - return QString(); - - CReserveKey reserveClassicalKey(wallet); - CPubKey classicalPubKey; - if (!reserveClassicalKey.GetReservedKey(classicalPubKey)) + if (!GUIUtil::pqAssetDestinationRequired() || wallet->IsLocked()) return QString(); CPQPubKey pqPubKey; if (!wallet->GenerateNewPQKey(pqPubKey)) return QString(); - const CKeyID classicalKey = classicalPubKey.GetID(); const WitnessV2PQDestination pqDestination(pqPubKey.GetWitnessProgram()); - if (!wallet->StoreOwnedPQAssetDestination(classicalKey, pqDestination.witnessProgram)) + const CKeyID assetID(Hash160(pqDestination.witnessProgram.begin(), + pqDestination.witnessProgram.end())); + if (!wallet->StoreOwnedPQAssetDestination(assetID, pqDestination.witnessProgram)) return QString(); - reserveClassicalKey.KeepKey(); - wallet->SetAddressBook(classicalKey, "", "receive"); + wallet->SetAddressBook(assetID, "", "pqasset"); wallet->SetAddressBook(pqDestination, "", "receive"); - return QString::fromStdString(EncodePQAssetDestination(classicalKey, pqDestination.witnessProgram)); + return QString::fromStdString(EncodePQAssetDestination(assetID, pqDestination.witnessProgram)); } QStringList WalletModel::getAssetDestinations() const { QStringList destinations; - for (const std::string& descriptor : wallet->GetOwnedPQAssetDestinations()) + for (const std::string& descriptor : wallet->GetOwnedPQAssetDestinations()) { + CKeyID assetID; + uint256 program; + if (!DecodePQOnlyAssetDestination(descriptor, assetID, program)) + continue; destinations.push_back(QString::fromStdString(descriptor)); + } return destinations; } diff --git a/src/rpc/assets.cpp b/src/rpc/assets.cpp index 889cdd53c4..923449a2cf 100644 --- a/src/rpc/assets.cpp +++ b/src/rpc/assets.cpp @@ -17,6 +17,7 @@ #include "chain.h" #include "consensus/validation.h" #include "core_io.h" +#include "hash.h" #include "httpserver.h" #include "validation.h" #include "net.h" @@ -130,26 +131,20 @@ static std::string NewAssetIssueDestination(CWallet* pwallet) const int activationHeight = GetPQAssetActivationHeightForPrev( chainActive.Tip(), GetParams().GetConsensus()); if (activationHeight >= 0 && chainActive.Height() + 1 >= activationHeight) { - pwallet->TopUpKeyPool(); - CReserveKey reserveClassicalKey(pwallet); - CPubKey classicalPubKey; - if (!reserveClassicalKey.GetReservedKey(classicalPubKey)) - throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, - "Error: Keypool ran out, please call keypoolrefill first"); CPQPubKey pqPubKey; if (!pwallet->GenerateNewPQKey(pqPubKey)) throw JSONRPCError(RPC_WALLET_ERROR, "Error: Failed to derive and persist ML-DSA-44 keypair"); - const CKeyID classicalKey = classicalPubKey.GetID(); const WitnessV2PQDestination pqDestination(pqPubKey.GetWitnessProgram()); - if (!pwallet->StoreOwnedPQAssetDestination(classicalKey, + const CKeyID assetID(Hash160(pqDestination.witnessProgram.begin(), + pqDestination.witnessProgram.end())); + if (!pwallet->StoreOwnedPQAssetDestination(assetID, pqDestination.witnessProgram)) throw JSONRPCError(RPC_WALLET_ERROR, - "Error: Failed to persist protected asset destination pairing"); - reserveClassicalKey.KeepKey(); - pwallet->SetAddressBook(classicalKey, "", "receive"); + "Error: Failed to persist PQ-only asset destination"); + pwallet->SetAddressBook(assetID, "", "pqasset"); pwallet->SetAddressBook(pqDestination, "", "receive"); - return EncodePQAssetDestination(classicalKey, + return EncodePQAssetDestination(assetID, pqDestination.witnessProgram); } @@ -180,9 +175,9 @@ static void ResolveAdministrativeAssetReturn(CWallet* pwallet, const std::string } CKeyID classicalKey; uint256 program; - if (!DecodePQAssetDestination(changeAddress, classicalKey, program)) + if (!DecodePQOnlyAssetDestination(changeAddress, classicalKey, program)) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Active PQ authority return requires a canonical classical|PQ asset destination"); + "Active PQ authority return requires a canonical PQ-only asset destination"); return; } @@ -1264,9 +1259,9 @@ UniValue transfer(const JSONRPCRequest& request) const bool pqAssetsActive = activationHeight >= 0 && chainActive.Height() + 1 >= activationHeight; if (pqAssetsActive) { CKeyID classicalKey; - if (!DecodePQAssetDestination(asset_change_address, classicalKey, assetChangeProgram)) + if (!DecodePQOnlyAssetDestination(asset_change_address, classicalKey, assetChangeProgram)) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Active PQ asset change requires a canonical classical|PQ asset destination"); + "Active PQ asset change requires a canonical PQ-only asset destination"); asset_change_dest = classicalKey; hasPQAssetChange = true; } else if (!IsValidDestination(asset_change_dest)) { @@ -1319,13 +1314,13 @@ UniValue transferfromaddresses(const JSONRPCRequest& request) "\nArguments:\n" "1. \"asset_name\" (string, required) name of asset\n" - "2. \"from_addresses\" (array, required) legacy addresses for historical outputs or canonical classical|PQ descriptors for protected outputs\n" + "2. \"from_addresses\" (array, required) legacy addresses for historical outputs or asset descriptors for protected outputs\n" "3. \"qty\" (numeric, required) number of assets you want to send to the address\n" "4. \"to_address\" (string, required) address to send the asset to\n" "5. \"message\" (string, optional) Once RIP5 is voted in ipfs hash or txid hash to send along with the transfer\n" "6. \"expire_time\" (numeric, optional) UTC timestamp of when the message expires\n" "7. \"rvn_change_address\" (string, optional, default = \"\") the transactions RVN change will be sent to this address\n" - "8. \"asset_change_address\" (string, optional, default = \"\") asset change destination; a canonical classical|PQ descriptor is required after PQ asset activation\n" + "8. \"asset_change_address\" (string, optional, default = \"\") asset change destination; a canonical PQ-only descriptor is required after PQ asset activation\n" "\nResult:\n" "txid" @@ -1418,9 +1413,9 @@ UniValue transferfromaddresses(const JSONRPCRequest& request) if (!asset_change_address.empty()) { if (pqAssetsActive) { CKeyID classicalKey; - if (!DecodePQAssetDestination(asset_change_address, classicalKey, assetChangeProgram)) + if (!DecodePQOnlyAssetDestination(asset_change_address, classicalKey, assetChangeProgram)) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Active PQ asset change requires a canonical classical|PQ asset destination"); + "Active PQ asset change requires a canonical PQ-only asset destination"); asset_change_dest = classicalKey; hasPQAssetChange = true; } else if (!IsValidDestination(asset_change_dest)) { @@ -1492,13 +1487,13 @@ UniValue transferfromaddress(const JSONRPCRequest& request) "\nArguments:\n" "1. \"asset_name\" (string, required) name of asset\n" - "2. \"from_address\" (string, required) legacy address for historical outputs or canonical classical|PQ descriptor for protected outputs\n" + "2. \"from_address\" (string, required) legacy address for historical outputs or asset descriptor for protected outputs\n" "3. \"qty\" (numeric, required) number of assets you want to send to the address\n" "4. \"to_address\" (string, required) address to send the asset to\n" "5. \"message\" (string, optional) Once RIP5 is voted in ipfs hash or txid hash to send along with the transfer\n" "6. \"expire_time\" (numeric, optional) UTC timestamp of when the message expires\n" "7. \"rvn_change_address\" (string, optional, default = \"\") the transaction RVN change will be sent to this address\n" - "8. \"asset_change_address\" (string, optional, default = \"\") asset change destination; a canonical classical|PQ descriptor is required after PQ asset activation\n" + "8. \"asset_change_address\" (string, optional, default = \"\") asset change destination; a canonical PQ-only descriptor is required after PQ asset activation\n" "\nResult:\n" "txid" @@ -1582,9 +1577,9 @@ UniValue transferfromaddress(const JSONRPCRequest& request) if (!asset_change_address.empty()) { if (pqAssetsActive) { CKeyID changeClassicalKey; - if (!DecodePQAssetDestination(asset_change_address, changeClassicalKey, assetChangeProgram)) + if (!DecodePQOnlyAssetDestination(asset_change_address, changeClassicalKey, assetChangeProgram)) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Active PQ asset change requires a canonical classical|PQ asset destination"); + "Active PQ asset change requires a canonical PQ-only asset destination"); asset_change_dest = changeClassicalKey; hasPQAssetChange = true; } else if (!IsValidDestination(asset_change_dest)) { @@ -1922,7 +1917,7 @@ UniValue addtagtoaddress(const JSONRPCRequest& request) "\nArguments:\n" "1. \"tag_name\" (string, required) the name of the tag you are assigning to the address, if it doens't have '#' at the front it will be added\n" "2. \"to_address\" (string, required) the address that will be assigned the tag\n" - "3. \"change_address\" (string, optional) Qualifier return destination; after PQ asset activation use a canonical classical|PQ descriptor\n" + "3. \"change_address\" (string, optional) Qualifier return destination; after PQ asset activation use a canonical PQ-only descriptor\n" "4. \"asset_data\" (string, optional) The asset data (ipfs or a hash) to be applied to the transfer of the qualifier token\n" "\nResult:\n" @@ -1950,7 +1945,7 @@ UniValue removetagfromaddress(const JSONRPCRequest& request) "\nArguments:\n" "1. \"tag_name\" (string, required) the name of the tag you are removing from the address\n" "2. \"to_address\" (string, required) the address that the tag will be removed from\n" - "3. \"change_address\" (string, optional) Qualifier return destination; after PQ asset activation use a canonical classical|PQ descriptor\n" + "3. \"change_address\" (string, optional) Qualifier return destination; after PQ asset activation use a canonical PQ-only descriptor\n" "4. \"asset_data\" (string, optional) The asset data (ipfs or a hash) to be applied to the transfer of the qualifier token\n" "\nResult:\n" @@ -1978,7 +1973,7 @@ UniValue freezeaddress(const JSONRPCRequest& request) "\nArguments:\n" "1. \"asset_name\" (string, required) the name of the restricted asset you want to freeze\n" "2. \"address\" (string, required) the address that will be frozen\n" - "3. \"change_address\" (string, optional) Owner return destination; after PQ asset activation use a canonical classical|PQ descriptor\n" + "3. \"change_address\" (string, optional) Owner return destination; after PQ asset activation use a canonical PQ-only descriptor\n" "4. \"asset_data\" (string, optional) The asset data (ipfs or a hash) to be applied to the transfer of the owner token\n" "\nResult:\n" @@ -2006,7 +2001,7 @@ UniValue unfreezeaddress(const JSONRPCRequest& request) "\nArguments:\n" "1. \"asset_name\" (string, required) the name of the restricted asset you want to unfreeze\n" "2. \"address\" (string, required) the address that will be unfrozen\n" - "3. \"change_address\" (string, optional) Owner return destination; after PQ asset activation use a canonical classical|PQ descriptor\n" + "3. \"change_address\" (string, optional) Owner return destination; after PQ asset activation use a canonical PQ-only descriptor\n" "4. \"asset_data\" (string, optional) The asset data (ipfs or a hash) to be applied to the transfer of the owner token\n" "\nResult:\n" @@ -2033,7 +2028,7 @@ UniValue freezerestrictedasset(const JSONRPCRequest& request) "\nArguments:\n" "1. \"asset_name\" (string, required) the name of the restricted asset you want to unfreeze\n" - "2. \"change_address\" (string, optional) Owner return destination; after PQ asset activation use a canonical classical|PQ descriptor\n" + "2. \"change_address\" (string, optional) Owner return destination; after PQ asset activation use a canonical PQ-only descriptor\n" "3. \"asset_data\" (string, optional) The asset data (ipfs or a hash) to be applied to the transfer of the owner token\n" "\nResult:\n" @@ -2060,7 +2055,7 @@ UniValue unfreezerestrictedasset(const JSONRPCRequest& request) "\nArguments:\n" "1. \"asset_name\" (string, required) the name of the restricted asset you want to unfreeze\n" - "2. \"change_address\" (string, optional) Owner return destination; after PQ asset activation use a canonical classical|PQ descriptor\n" + "2. \"change_address\" (string, optional) Owner return destination; after PQ asset activation use a canonical PQ-only descriptor\n" "4. \"asset_data\" (string, optional) The asset data (ipfs or a hash) to be applied to the transfer of the owner token\n" "\nResult:\n" diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 8923620aef..332d941053 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -355,7 +355,7 @@ UniValue createrawtransaction(const JSONRPCRequest& request) "Note that the transaction's inputs are not signed, and\n" "it is not stored in the wallet or transmitted to the network.\n" "After PQ asset activation, asset output keys and owner/root change fields\n" - "must use canonical classical|PQ descriptors. Spending a protected asset\n" + "must use canonical PQ-only descriptors. Spending a protected asset\n" "also requires a supplied, funded native witness-v2 input with the same\n" "PQ program; this RPC does not select or fund that anchor.\n" @@ -676,9 +676,9 @@ UniValue createrawtransaction(const JSONRPCRequest& request) uint256& program) -> CTxDestination { if (pqAssetsActive) { CKeyID classicalKey; - if (!DecodePQAssetDestination(address, classicalKey, program)) + if (!DecodePQOnlyAssetDestination(address, classicalKey, program)) throw JSONRPCError(RPC_INVALID_PARAMETER, - "Invalid parameter, " + field + " must be a canonical classical|PQ asset destination"); + "Invalid parameter, " + field + " must be a canonical PQ-only asset destination"); return classicalKey; } const CTxDestination destination = DecodeDestination(address); @@ -693,7 +693,7 @@ UniValue createrawtransaction(const JSONRPCRequest& request) const auto tagAssetScript = [](CScript& script, const uint256& program) { CScript tagged; - if (!BuildPQAssetTaggedScript(script, program, tagged)) + if (!BuildPQOnlyAssetScript(script, program, tagged)) throw JSONRPCError(RPC_INVALID_PARAMETER, "Could not construct a canonical PQ asset output"); script = std::move(tagged); @@ -714,9 +714,9 @@ UniValue createrawtransaction(const JSONRPCRequest& request) uint256 assetProgram; if (pqAssetsActive && isAssetOutput) { CKeyID classicalKey; - if (!DecodePQAssetDestination(name_, classicalKey, assetProgram)) + if (!DecodePQOnlyAssetDestination(name_, classicalKey, assetProgram)) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Active PQ asset output requires a canonical classical|PQ asset destination"); + "Active PQ asset output requires a canonical PQ-only asset destination"); destination = classicalKey; } else { destination = DecodeDestination(name_); @@ -1554,6 +1554,9 @@ UniValue createrawtransaction(const JSONRPCRequest& request) uint256 existingProgram; if (!GetPQAssetProgram(outputScript, existingProgram)) tagAssetScript(outputScript, assetProgram); + else if (!IsPQOnlyAssetScript(outputScript, existingProgram)) + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Active PQ asset output must use the canonical PQ-only script"); } } } else { diff --git a/test/functional/feature_pq_asset_anchor.py b/test/functional/feature_pq_asset_anchor.py index 77f71b55aa..004c32a5d8 100755 --- a/test/functional/feature_pq_asset_anchor.py +++ b/test/functional/feature_pq_asset_anchor.py @@ -11,7 +11,9 @@ import shutil from test_framework.blocktools import create_block, create_coinbase +from test_framework.address import byte_to_base58 from test_framework.mininode import CTransaction, from_hex, to_hex +from test_framework.script import hash160 from test_framework.test_framework import RavenTestFramework from test_framework import util as test_util from test_framework.util import assert_equal, assert_raises_rpc_error @@ -61,7 +63,7 @@ def run_test(self): assert_equal(node.getblockcount(), 863) assert_equal(node.getblockchaininfo()['bip9_softforks']['transfer_script']['status'], 'active') - assert_raises_rpc_error(-5, 'canonical classical|PQ asset destination', node.issue, + assert_raises_rpc_error(-5, 'canonical PQ-only asset destination', node.issue, 'PQAFTERACTIVE', 1, legacy_address) # A rollback to the last pre-activation tip permits a legacy issue. @@ -123,13 +125,23 @@ def run_test(self): {'txid': asset_outpoint['txid'], 'vout': asset_outpoint['vout']}, {'txid': native_coin['txid'], 'vout': native_coin['vout']}, ] - migration_descriptor = legacy_address + '|' + pq_address + migration_descriptor = node.getnewpqassetaddress() + _, pq_address = migration_descriptor.split('|') + pq_script = bytes.fromhex(node.validateaddress(pq_address)['scriptPubKey']) + program = pq_script[2:] migration_outputs = { migration_descriptor: {'transfer': {asset_name: 1}}, pq_address: Decimal('1'), native_change: native_coin['amount'] - Decimal('1.01'), } migration_raw = node.createrawtransaction(migration_inputs, migration_outputs) + migration_tx = from_hex(CTransaction(), migration_raw) + migration_asset_scripts = [output.scriptPubKey for output in migration_tx.vout + if len(output.scriptPubKey) > 31 and + output.scriptPubKey[25] == ASSET_OPCODE] + assert_equal(len(migration_asset_scripts), 1) + assert_equal(migration_asset_scripts[0][:3], b'\x51\x51\x14') + assert_equal(migration_asset_scripts[0][23:25], b'\x75\x75') migration_signed = node.signrawtransaction(migration_raw) assert_equal(migration_signed['complete'], True) migration_txid = node.sendrawtransaction(migration_signed['hex']) @@ -218,7 +230,7 @@ def run_test(self): composite_address = node.getnewpqassetaddress() assert_equal(composite_address.count('|'), 1) classical_part, pq_part = composite_address.split('|') - assert_equal(node.validateaddress(classical_part)['ismine'], True) + assert_equal(node.validateaddress(classical_part)['ismine'], False) assert_equal(node.validateaddress(pq_part)['ismine'], True) descriptor_script = bytes.fromhex(node.validateaddress(pq_part)['scriptPubKey']) assert_equal(descriptor_script[:2], b'\x52\x20') @@ -231,6 +243,7 @@ def run_test(self): assert_equal(len(tagged_asset_outputs), 2) for output in tagged_asset_outputs: assert_equal(output.scriptPubKey[25], ASSET_OPCODE) + assert_equal(output.scriptPubKey[:3], b'\x51\x51\x14') node.generate(1) for owned_asset, expected_balance in (('PQROOTRECIPIENT', 10), ('PQROOTRECIPIENT!', 1)): @@ -317,8 +330,8 @@ def run_test(self): # Reissuance must protect both the newly created units and the # returned owner token. The owner return stays with its original - # classical/PQ key pair, even when new units use a different pair. - assert_raises_rpc_error(-5, 'canonical classical|PQ asset destination', + # PQ program, even when new units use a different program. + assert_raises_rpc_error(-5, 'canonical PQ-only asset destination', node.reissue, 'PQROOTRECIPIENT', 2, legacy_address) assert_raises_rpc_error(-4, 'funded matching PQ anchor', @@ -341,9 +354,10 @@ def run_test(self): assert_equal(sum(output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == destination_program for output in reissue_tx.vout if len(output.scriptPubKey) > 57), 1) - classical_script = bytes.fromhex( + asset_id_script = bytes.fromhex( node.validateaddress(classical_part)['scriptPubKey']) - assert_equal(sum(output.scriptPubKey[:25] == classical_script and + pq_only_script = b'\x51\x51\x14' + asset_id_script[3:23] + b'\x75\x75' + assert_equal(sum(output.scriptPubKey[:25] == pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == descriptor_program for output in reissue_tx.vout if len(output.scriptPubKey) > 57), 1) @@ -380,7 +394,7 @@ def run_test(self): if output.scriptPubKey == descriptor_script and output.nValue > 0] assert_equal(len(sub_refresh_vouts), 1) - assert_equal(sum(output.scriptPubKey[:25] == classical_script and + assert_equal(sum(output.scriptPubKey[:25] == pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == descriptor_program for output in sub_issue.vout @@ -417,7 +431,7 @@ def run_test(self): assert_equal(len(unique_anchor_vin), 1) assert_equal(len(unique_tx.wit.vtxinwit[unique_anchor_vin[0]] .scriptWitness.stack), 2) - assert_equal(sum(output.scriptPubKey[:25] == classical_script and + assert_equal(sum(output.scriptPubKey[:25] == pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == descriptor_program for output in unique_tx.vout @@ -473,7 +487,7 @@ def run_test(self): output.scriptPubKey[-32:] == destination_program for output in restricted_reissue.vout if len(output.scriptPubKey) > 57), 1) - assert_equal(sum(output.scriptPubKey[:25] == classical_script and + assert_equal(sum(output.scriptPubKey[:25] == pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == descriptor_program for output in restricted_reissue.vout @@ -572,15 +586,17 @@ def run_test(self): assert_equal(len(issue_anchor_vin), 1) assert_equal(len(restricted_issue.wit.vtxinwit[issue_anchor_vin[0]] .scriptWitness.stack), 2) - assert_equal(sum(output.scriptPubKey[:25] == classical_script and + assert_equal(sum(output.scriptPubKey[:25] == pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == descriptor_program for output in restricted_issue.vout if len(output.scriptPubKey) > 57), 1) destination_classical, _ = destination_descriptor.split('|') - destination_classical_script = bytes.fromhex( + destination_id_script = bytes.fromhex( node.validateaddress(destination_classical)['scriptPubKey']) - assert_equal(sum(output.scriptPubKey[:25] == destination_classical_script and + destination_pq_only_script = ( + b'\x51\x51\x14' + destination_id_script[3:23] + b'\x75\x75') + assert_equal(sum(output.scriptPubKey[:25] == destination_pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == destination_program for output in restricted_issue.vout @@ -610,12 +626,12 @@ def run_test(self): assert_equal(len(qualifier_anchor_vin), 1) assert_equal(len(sub_qualifier_tx.wit.vtxinwit[qualifier_anchor_vin[0]] .scriptWitness.stack), 2) - assert_equal(sum(output.scriptPubKey[:25] == destination_classical_script and + assert_equal(sum(output.scriptPubKey[:25] == destination_pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == destination_program for output in sub_qualifier_tx.vout if len(output.scriptPubKey) > 57), 1) - assert_equal(sum(output.scriptPubKey[:25] == classical_script and + assert_equal(sum(output.scriptPubKey[:25] == pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == descriptor_program for output in sub_qualifier_tx.vout @@ -646,7 +662,7 @@ def run_test(self): assert_equal(len(qualifier_transfer_anchor_vin), 1) assert_equal(len(qualifier_transfer.wit.vtxinwit[ qualifier_transfer_anchor_vin[0]].scriptWitness.stack), 2) - assert_equal(sum(output.scriptPubKey[:25] == classical_script and + assert_equal(sum(output.scriptPubKey[:25] == pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == descriptor_program for output in qualifier_transfer.vout @@ -672,7 +688,7 @@ def run_test(self): assert_equal(len(tag_anchor_vin), 1) assert_equal(len(tag_tx.wit.vtxinwit[tag_anchor_vin[0]] .scriptWitness.stack), 2) - assert_equal(sum(output.scriptPubKey[:25] == classical_script and + assert_equal(sum(output.scriptPubKey[:25] == pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == descriptor_program for output in tag_tx.vout @@ -680,8 +696,14 @@ def run_test(self): assert_equal(sum(output.scriptPubKey == descriptor_script and output.nValue > 0 for output in tag_tx.vout), 1) node.generate(1) + assert_equal(qualifier_name in node.listtagsforaddress(classical_part), True) + my_tag = [entry for entry in node.viewmytaggedaddresses() + if entry['Tag Name'] == qualifier_name and + entry['Address'] == classical_part] + assert_equal(len(my_tag), 1) + assert 'Assigned' in my_tag[0] - # Qualifier lookup must use the classical address inside the PQ + # Qualifier lookup must use the asset identifier inside the PQ # descriptor. A recipient without the tag fails the same verifier. assert_raises_rpc_error( None, 'bad-txns-null-verifier-address-failed-verification', @@ -694,7 +716,7 @@ def run_test(self): True, qualifier_name)[0] qualified_reissue = from_hex( CTransaction(), node.getrawtransaction(qualified_reissue_txid)) - assert_equal(sum(output.scriptPubKey[:25] == classical_script and + assert_equal(sum(output.scriptPubKey[:25] == pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == descriptor_program for output in qualified_reissue.vout @@ -714,7 +736,7 @@ def run_test(self): restricted_issue_name, 1, composite_address)[0] qualified_transfer = from_hex( CTransaction(), node.getrawtransaction(qualified_transfer_txid)) - assert_equal(sum(output.scriptPubKey[:25] == classical_script and + assert_equal(sum(output.scriptPubKey[:25] == pq_only_script and output.scriptPubKey[25] == ASSET_OPCODE and output.scriptPubKey[-32:] == descriptor_program for output in qualified_transfer.vout @@ -727,6 +749,7 @@ def assert_protected_admin_return(method, arguments, anchor_address, anchor_script, return_script, return_program): node.sendtoaddress(anchor_address, Decimal('1')) node.generate(1) + available_anchors = { (coin['txid'], coin['vout']) for coin in node.listunspent() if coin['scriptPubKey'] == anchor_script.hex() @@ -747,13 +770,32 @@ def assert_protected_admin_return(method, arguments, anchor_address, if len(output.scriptPubKey) > 57), 1) node.generate(1) - assert_raises_rpc_error(-5, 'canonical classical|PQ asset destination', + unpaired_pq = node.getnewpqaddress() + unpaired_program = bytes.fromhex( + node.validateaddress(unpaired_pq)['scriptPubKey'])[2:] + unpaired_asset_id = byte_to_base58(hash160(unpaired_program), 111) + assert_equal(node.validateaddress(unpaired_asset_id)['ismine'], False) + assert_protected_admin_return( + 'addtagtoaddress', (qualifier_name, unpaired_asset_id, + composite_address), + pq_part, descriptor_script, pq_only_script, descriptor_program) + assert_equal(qualifier_name in node.listtagsforaddress(unpaired_asset_id), + True) + assert_equal(any(entry['Address'] == unpaired_asset_id for entry in + node.viewmytaggedaddresses()), False) + + assert_raises_rpc_error(-5, 'canonical PQ-only asset destination', node.removetagfromaddress, qualifier_name, classical_part, classical_part) assert_protected_admin_return( 'removetagfromaddress', (qualifier_name, classical_part, composite_address), - pq_part, descriptor_script, classical_script, descriptor_program) + pq_part, descriptor_script, pq_only_script, descriptor_program) + my_tag = [entry for entry in node.viewmytaggedaddresses() + if entry['Tag Name'] == qualifier_name and + entry['Address'] == classical_part] + assert_equal(len(my_tag), 1) + assert 'Removed' in my_tag[0] for method, arguments in ( ('freezeaddress', (restricted_issue_name, classical_part, destination_descriptor)), @@ -762,13 +804,29 @@ def assert_protected_admin_return(method, arguments, anchor_address, ('unfreezerestrictedasset', (restricted_issue_name,))): assert_protected_admin_return( method, arguments, destination_pq, destination_script, - destination_classical_script, destination_program) + destination_pq_only_script, destination_program) + if method == 'freezeaddress': + assert_equal(restricted_issue_name in node.listaddressrestrictions( + classical_part), True) + my_restriction = [entry for entry in + node.viewmyrestrictedaddresses() + if entry['Asset Name'] == restricted_issue_name and + entry['Address'] == classical_part] + assert_equal(len(my_restriction), 1) + assert 'Restricted' in my_restriction[0] + elif method == 'unfreezeaddress': + my_restriction = [entry for entry in + node.viewmyrestrictedaddresses() + if entry['Asset Name'] == restricted_issue_name and + entry['Address'] == classical_part] + assert_equal(len(my_restriction), 1) + assert 'Derestricted' in my_restriction[0] # Raw asset construction must tag every asset output. The separate # owner/root return field must retain its own descriptor program. dummy_input = [{'txid': '00' * 32, 'vout': 0}] - primary = (classical_script, descriptor_program) - alternate = (destination_classical_script, destination_program) + primary = (pq_only_script, descriptor_program) + alternate = (destination_pq_only_script, destination_program) def assert_raw_asset_programs(asset_object, expected): raw = node.createrawtransaction( @@ -835,11 +893,11 @@ def assert_raw_asset_programs(asset_object, expected): assert_raw_asset_programs( {'unfreeze_asset': {'asset_name': restricted_issue_name}}, [primary]) assert_raises_rpc_error( - -5, 'canonical classical|PQ asset destination', + -5, 'canonical PQ-only asset destination', node.createrawtransaction, dummy_input, {classical_part: {'transfer': {'PQROOTRECIPIENT': 1}}}) assert_raises_rpc_error( - None, 'owner_change_address must be a canonical classical|PQ', + None, 'owner_change_address must be a canonical PQ-only', node.createrawtransaction, dummy_input, {composite_address: { 'reissue': {'asset_name': 'PQROOTRECIPIENT', @@ -851,23 +909,37 @@ def assert_raw_asset_programs(asset_object, expected): classical_part: Decimal('1')}) assert_equal(len(from_hex(CTransaction(), mixed_raw).vout), 2) alternate_program_descriptor = classical_part + '|' + destination_pq + assert_raises_rpc_error( + -5, 'canonical PQ-only asset destination', + node.createrawtransaction, dummy_input, + {alternate_program_descriptor: {'transfer': {'PQROOTRECIPIENT': 1}}}) split_raw = node.createrawtransaction( dummy_input, {composite_address: {'transfer': {'PQROOTRECIPIENT': 1}}, - alternate_program_descriptor: {'transfer': {'PQROOTRECIPIENT': 1}}}) + destination_descriptor: {'transfer': {'PQROOTRECIPIENT': 1}}}) split_tx = from_hex(CTransaction(), split_raw) assert_equal(sorted(output.scriptPubKey[-32:] for output in split_tx.vout), sorted((descriptor_program, destination_program))) self.test_anchor_availability_limits(node) - # A backed-up encrypted wallet must recover both halves of the - # descriptor and sign a matching PQ anchor after a fresh restart. + # A backed-up encrypted wallet must recover the PQ key and its asset + # identifier, then sign a matching PQ anchor after a fresh restart. passphrase = 'regtest-only-pq-asset-passphrase' node.node_encrypt_wallet(passphrase) self.start_node(0) - assert_equal(node.validateaddress(classical_part)['ismine'], True) + assert_equal(node.validateaddress(classical_part)['ismine'], False) assert_equal(node.validateaddress(pq_part)['ismine'], True) + assert_equal(any(entry['Address'] == unpaired_asset_id for entry in + node.viewmytaggedaddresses()), False) + assert_equal(any(entry['Address'] == classical_part and + entry['Tag Name'] == qualifier_name and + 'Removed' in entry for entry in + node.viewmytaggedaddresses()), True) + assert_equal(any(entry['Address'] == classical_part and + entry['Asset Name'] == restricted_issue_name and + 'Derestricted' in entry for entry in + node.viewmyrestrictedaddresses()), True) assert_equal(node.listmyassets(qualifier_name, True) [qualifier_name]['balance'], 1) assert_raises_rpc_error(-13, 'walletpassphrase first', @@ -879,8 +951,12 @@ def assert_raw_asset_programs(asset_object, expected): shutil.copyfile(backup_path, os.path.join(node.datadir, 'regtest', 'wallet.dat')) self.start_node(0) - assert_equal(node.validateaddress(classical_part)['ismine'], True) + assert_equal(node.validateaddress(classical_part)['ismine'], False) assert_equal(node.validateaddress(pq_part)['ismine'], True) + assert_equal(any(entry['Address'] == classical_part and + entry['Tag Name'] == qualifier_name and + 'Removed' in entry for entry in + node.viewmytaggedaddresses()), True) assert_equal(node.listmyassets(qualifier_name, True) [qualifier_name]['balance'], 1) node.walletpassphrase(passphrase, 600) From 6ed46c91c6922c5cedf7ce966e9922b2a022923b Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:14:11 +0200 Subject: [PATCH 032/195] [FINDING-164] Restore PQ-only tag and freeze history in wallet and Qt --- src/assets/assets.cpp | 2 +- src/assets/assets.h | 3 +++ src/qt/myrestrictedassetrecord.cpp | 2 +- src/qt/myrestrictedassettablemodel.cpp | 5 +++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/assets/assets.cpp b/src/assets/assets.cpp index 2c876a5ca0..6c38dd3eb5 100644 --- a/src/assets/assets.cpp +++ b/src/assets/assets.cpp @@ -5724,7 +5724,7 @@ bool ContextualCheckNullAssetTxOut(const CTxOut& txout, CAssetsCache* assetCache #ifdef ENABLE_WALLET if (myNullAssetData && vpwallets.size()) { - if (IsMine(*vpwallets[0], DecodeDestination(address)) & ISMINE_ALL) { + if (GetRestrictedAddressOwnership(*vpwallets[0], address) & ISMINE_ALL) { myNullAssetData->emplace_back(std::make_pair(address, data)); } } diff --git a/src/assets/assets.h b/src/assets/assets.h index bf30157385..1a2c18f750 100644 --- a/src/assets/assets.h +++ b/src/assets/assets.h @@ -561,6 +561,9 @@ std::string EncodeIPFS(std::string decoded); #ifdef ENABLE_WALLET +/** Include wallet-owned PQ-only asset identifiers in tag and freeze history. */ +isminetype GetRestrictedAddressOwnership(const CWallet& wallet, const std::string& address); + /** Find a wallet-owned protected authority output for an active PQ asset return. */ bool GetWalletProtectedAssetReturnDescriptor(CWallet* pwallet, const std::string& authorityName, std::string& descriptor, std::pair& error); diff --git a/src/qt/myrestrictedassetrecord.cpp b/src/qt/myrestrictedassetrecord.cpp index fda030aa53..1ba3cc660c 100644 --- a/src/qt/myrestrictedassetrecord.cpp +++ b/src/qt/myrestrictedassetrecord.cpp @@ -47,7 +47,7 @@ QList MyRestrictedAssetRecord::decomposeTransaction(con if (!AssetNullDataFromScript(txout.scriptPubKey, data, address)) { continue; } - mine = IsMine(*wallet, DecodeDestination(address)); + mine = GetRestrictedAddressOwnership(*wallet, address); if (mine & ISMINE_ALL) { MyRestrictedAssetRecord sub(hash, nTime); sub.involvesWatchAddress = mine & ISMINE_SPENDABLE ? false : true; diff --git a/src/qt/myrestrictedassettablemodel.cpp b/src/qt/myrestrictedassettablemodel.cpp index 2686c1498a..f16734c6f0 100644 --- a/src/qt/myrestrictedassettablemodel.cpp +++ b/src/qt/myrestrictedassettablemodel.cpp @@ -6,6 +6,7 @@ #include "myrestrictedassettablemodel.h" #include "addresstablemodel.h" +#include "assets/assets.h" #include "guiconstants.h" #include "guiutil.h" #include "optionsmodel.h" @@ -99,7 +100,7 @@ class MyRestrictedAssetsTablePriv sub.type = std::get<2>(item) ? MyRestrictedAssetRecord::Type::Tagged : MyRestrictedAssetRecord::Type::UnTagged; else if (IsAssetNameAnRestricted(sub.assetName)) sub.type = std::get<2>(item) ? MyRestrictedAssetRecord::Type::Frozen : MyRestrictedAssetRecord::Type::UnFrozen; - sub.involvesWatchAddress = IsMine(*this->wallet, DecodeDestination(sub.address)) & ISMINE_WATCH_ONLY; + sub.involvesWatchAddress = GetRestrictedAddressOwnership(*this->wallet, sub.address) & ISMINE_WATCH_ONLY; vectAssetData.push_back(qMakePair(QString::fromStdString(std::get<0>(item)), QString::fromStdString(std::get<1>(item)))); cacheMyAssetData[qMakePair(QString::fromStdString(std::get<0>(item)), QString::fromStdString(std::get<1>(item)))] = sub; } @@ -126,7 +127,7 @@ class MyRestrictedAssetsTablePriv cacheMyAssetData[pair] = rec; } else { rec.involvesWatchAddress = - IsMine(*this->wallet, DecodeDestination(address.toStdString())) & ISMINE_WATCH_ONLY ? true : false; + GetRestrictedAddressOwnership(*this->wallet, address.toStdString()) & ISMINE_WATCH_ONLY; parent->beginInsertRows(QModelIndex(), 0, 0); cacheMyAssetData[pair] = rec; vectAssetData.push_front(pair); From 3ed32302d37fb343231c20905d178016ab1a07c2 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:16:41 +0200 Subject: [PATCH 033/195] [FINDING-158][FINDING-159][FINDING-161][FINDING-162][FINDING-163][FINDING-164] Record local qualification --- ...0025-v4.8-security-remediation-register.md | 89 ++++++++++++------- 1 file changed, 58 insertions(+), 31 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 0499710ae1..41112d7fe8 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8095,18 +8095,22 @@ Cross-platform CI and public deployment remain separate release gates. signatures in mempool and blocks. Cover historical migration, partial change, issuance, reissuance, restricted and qualifier administration, raw RPCs, wallet backup and recovery, Qt, reorgs, and exact-SHA CI. -- **Remediation commit:** `198b93029` is a partial parser and script-format - feasibility step. The complete consensus and wallet change is pending. -- **Verification:** The existing dual-signature consensus check and asset - signing path establish the pre-fix behavior. The new +- **Remediation commits:** `198b93029` introduced the parser and script + format; `802bf2433` added the PQ-only consensus and signing rules; + `5f34f8cbf` added wallet custody, recovery and contextual ownership; + `b7153328d` switched asset RPC and Qt construction to PQ-only outputs. +- **Verification:** The previous dual-signature consensus check and asset + signing path establish the pre-fix behavior. The `pq_only_asset_prefix_is_legacy_valid_and_program_bound_test` was red before `198b93029` because the parser rejected the proposed prefix. It - now passes: the script executes with an empty scriptSig, the asset marker - remains at byte 25, and the address hash must match the PQ program. - All 25 `asset_tx_tests` cases and local `make check` passed. This does - not prove old-node block acceptance, wallet spending, or public activation. -- **Final status:** OPEN; blocks FINDING-067 closure and the asset-inclusive - 4.8.1 release. + now passes with an empty scriptSig and a program-bound identifier. + `pq_asset_creation_height_and_anchor_rule_test` covers new-output and + anchored-input rejection. The asset anchor, source RPC, wallet ownership + and authority functional tests pass at `6ed46c91c`; local `make check` + passes. Old-node block acceptance, exact-SHA CI and FINDING-160/161 + release conditions remain unverified. +- **Final status:** FIXED locally; release closure awaits exact-SHA CI and + consensus compatibility and activation evidence. ### FINDING-159: Proposed PQ-only asset input permits scriptSig malleability @@ -8137,10 +8141,14 @@ Cross-platform CI and public deployment remain separate release gates. with a nonempty asset scriptSig is accepted before the new check, then rejected with a specific reason afterward. Empty scriptSig with the matching anchor and historical classical inputs remain valid. -- **Remediation commit:** None. Consensus correction is pending. -- **Verification:** Static comparison of block flags and the new prefix - demonstrates the risk; the red and green transaction vectors are pending. -- **Final status:** OPEN. +- **Remediation commit:** `802bf2433` rejects nonempty scriptSig for a + protected asset input after bit 13 while retaining historical input rules. +- **Verification:** The unit vector at + src/test/assets/asset_tx_tests.cpp:995-1003 failed when the new check was + temporarily removed: the malleable spend was accepted and had no reject + reason. Restoring the check made the same vector pass. The full local + `make check` also passed at `6ed46c91c`. +- **Final status:** FIXED locally; exact-SHA CI pending. ### FINDING-160: Native RIP-25 block limits can split Core 4.8.0 nodes @@ -8210,10 +8218,17 @@ Cross-platform CI and public deployment remain separate release gates. before and after activation without an anchor, and prove the exact consensus outcome and wallet classification. Cover a post-activation creation and anchored spend as a separate case. -- **Remediation commit:** None. -- **Verification:** Static origin-height and script inspection identifies - the gap. Boundary and wallet regressions are pending. -- **Final status:** OPEN; blocks FINDING-158 closure and asset release. +- **Remediation commits:** `802bf2433` adds a consensus origin-height + vector; `5f34f8cbf` refuses generic PQ-only wallet ownership without + confirmed activation context. An activation-safe creation policy is + still pending. +- **Verification:** The unit vector proves that a pre-activation PQ-only + lookalike remains exempt from the anchor rule after activation. The + contextual wallet method returns `ISMINE_NO` for such a confirmed + output. A functional pre-activation creation and spending regression, + including wallet balance and reorg behavior, remains pending. +- **Final status:** OPEN; a fail-safe historical-lookalike policy and + complete boundary regression are required before asset release. ### FINDING-162: PQ-only owner and message assets miss wallet subscriptions @@ -8244,10 +8259,13 @@ Cross-platform CI and public deployment remain separate release gates. - **Regression required:** With messaging enabled, confirm post-activation PQ-only owner-token and message-channel outputs and verify the expected subscription. Confirm a pre-activation PQ-only lookalike is excluded. -- **Remediation commit:** None. -- **Verification:** Static call-path review. The messaging regression - test and correction are pending. -- **Final status:** OPEN; blocks complete PQ-only wallet qualification. +- **Remediation commit:** `5f34f8cbf` uses confirmed output context in + the message subscription path. +- **Verification:** The owner-token subscription assertion in + `feature_pq_asset_wallet_ownership.py` failed before the contextual + lookup and passed afterward. The full functional test and local + `make check` pass at `6ed46c91c`. +- **Final status:** FIXED locally; exact-SHA CI pending. ### FINDING-163: Synthetic PQ asset identifier leaks as a native RVN address @@ -8291,11 +8309,15 @@ Cross-platform CI and public deployment remain separate release gates. Cover `listreceivedbyaddress` with empty entries and `listaddressgroupings` after receiving an asset. Recover keys only and verify the Qt address-table classification remains hidden. -- **Remediation commit:** None. -- **Verification:** Static RPC call-path review and the functional test - showing `validateaddress(assetID).ismine == false`. An explicit address - listing regression is pending. -- **Final status:** OPEN; blocks safe asset wallet release. +- **Remediation commit:** `5f34f8cbf` filters the canonical synthetic ID + from native address listings and groupings, and preserves its hidden + address-book purpose after key-only recovery. +- **Verification:** The new `getaddressesbyaccount` assertion failed + before the filter and passed afterward. The source RPC test also checks + `listreceivedbyaddress` and `listaddressgroupings`. The wallet unit + vector checks recovered purpose and canonical ID classification; + both tests pass at `6ed46c91c`. +- **Final status:** FIXED locally; exact-SHA CI pending. ### FINDING-164: PQ-only restricted-asset status misses wallet ownership @@ -8325,7 +8347,12 @@ Cross-platform CI and public deployment remain separate release gates. - **Regression required:** Tag and freeze a wallet-owned PQ-only descriptor, mine the operations, and check the private DB, relevant RPCs and Qt view. Repeat after restart and cover an unrelated program. -- **Remediation commit:** None. -- **Verification:** Static call-path review; functional regression is - pending. -- **Final status:** OPEN; blocks restricted-asset wallet qualification. +- **Remediation commits:** `b7153328d` adds tag and freeze regressions; + `6ed46c91c` records owned PQ-only identifiers in the private restricted + database and Qt history. +- **Verification:** `feature_pq_asset_anchor.py` failed before the + private-DB change because the tagged identifier was absent from + `viewmytaggedaddresses`. It passes afterward for tag, untag, freeze, + unfreeze, restart and an unpaired PQ program excluded from private + history. The full local `make check` passes at `6ed46c91c`. +- **Final status:** FIXED locally; exact-SHA CI pending. From a9a78562ea869cd7ab08885943bb308e7bda8b76 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:58:54 +0200 Subject: [PATCH 034/195] [FINDING-165][FINDING-166] Freeze CI qualification failures --- ...0025-v4.8-security-remediation-register.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 41112d7fe8..a6e9c3072b 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8356,3 +8356,66 @@ Cross-platform CI and public deployment remain separate release gates. unfreeze, restart and an unpaired PQ program excluded from private history. The full local `make check` passes at `6ed46c91c`. - **Final status:** FIXED locally; exact-SHA CI pending. + +### FINDING-165: PQ asset functional tests require optional Python RIPEMD160 + +- **Severity:** MEDIUM release qualification failure. +- **Frozen initial status:** OPEN on 2026-10-12, before changing the + functional tests or their hash assertions. +- **Affected RIP-25 invariant:** The required asset custody and source RPC + regressions must run on the pinned CI environment so a green final gate + proves the protected asset paths on the release SHA. +- **Affected Core 4.8.0 fix:** None. The dependency entered with the new + PQ-only functional assertions. +- **Root cause:** Two functional tests call the test framework's Python + `hash160` helper. It requires `hashlib.new('ripemd160', ...)`, which the + Ubuntu 22.04 runner's Python/OpenSSL combination does not provide. +- **Affected file/function/lines:** + test/functional/feature_pq_asset_anchor.py:776, unrelated program ID; + test/functional/feature_pq_asset_source_rpc.py:63, descriptor ID check; + test/functional/test_framework/script.py:33-34, hash helper. +- **Concrete scenario:** Final Gate run `38047870174` on + `3ed32302d37fb343231c20905d178016ab1a07c2` passes `make check` + but fails its behavioral security invariant step. Both functional tests + stop with `ValueError: unsupported hash type ripemd160`. Required + functional tests and the build matrix are skipped. +- **Expected behavior:** Preserve the independent identity and unrelated + program assertions without relying on an optional Python hash provider. +- **Regression required:** Reproduce the failure with RIPEMD160 unavailable, + then run both tests successfully under the same restriction and in the + regular local test environment. Keep the C++ consensus binding vectors. +- **Remediation commit:** None. +- **Verification:** Red CI run `38047870174`; local green and exact-SHA CI + rerun pending. +- **Final status:** OPEN. + +### FINDING-166: Build Raven hides the failing unit test diagnosis + +- **Severity:** MEDIUM release qualification failure. +- **Frozen initial status:** OPEN on 2026-10-12, before changing the + workflow's mandatory unit-test step. +- **Affected RIP-25 invariant:** All release artifact builds must be gated + by a diagnosable, successful `make check` on their exact source SHA. +- **Affected Core 4.8.0 fix:** None. The gate is specific to this release + workflow. +- **Root cause:** Build Raven runs `make check` without printing + `src/test-suite.log` or `src/test/test_raven.log` on failure. Automake + prints only the aggregate `FAIL: test/test_raven` result. +- **Affected file/function/lines:** .github/workflows/build-raven.yml:65-68, + mandatory unit-test step. +- **Concrete scenario:** Build Raven run `38047871989` on + `3ed32302d37fb343231c20905d178016ab1a07c2` builds successfully + but fails `make check` with one failing test binary. It skips all release + artifact jobs. Final Gate on the same SHA passes its unit tests, so the + particular Build Raven test failure remains unclassified until its test + log is available. +- **Expected behavior:** Preserve `make check` as a hard gate and print its + bounded failure logs before exiting nonzero. Use the rerun to identify + and remediate any underlying deterministic or intermittent test defect. +- **Regression required:** Simulate a failing `make check` and prove the + workflow reports the specific test failure and still fails; then rerun + both workflows on one exact SHA. +- **Remediation commit:** None. +- **Verification:** Red CI run `38047871989`; diagnosis and exact-SHA CI + rerun pending. +- **Final status:** OPEN. From 3c1d8c09156d638cf6c412264714b263d39c5686 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:03:54 +0200 Subject: [PATCH 035/195] [FINDING-165] Remove optional Python RIPEMD160 from PQ tests --- test/functional/feature_pq_asset_anchor.py | 6 +----- test/functional/feature_pq_asset_source_rpc.py | 7 ++++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/test/functional/feature_pq_asset_anchor.py b/test/functional/feature_pq_asset_anchor.py index 004c32a5d8..5f919e3630 100755 --- a/test/functional/feature_pq_asset_anchor.py +++ b/test/functional/feature_pq_asset_anchor.py @@ -13,7 +13,6 @@ from test_framework.blocktools import create_block, create_coinbase from test_framework.address import byte_to_base58 from test_framework.mininode import CTransaction, from_hex, to_hex -from test_framework.script import hash160 from test_framework.test_framework import RavenTestFramework from test_framework import util as test_util from test_framework.util import assert_equal, assert_raises_rpc_error @@ -770,10 +769,7 @@ def assert_protected_admin_return(method, arguments, anchor_address, if len(output.scriptPubKey) > 57), 1) node.generate(1) - unpaired_pq = node.getnewpqaddress() - unpaired_program = bytes.fromhex( - node.validateaddress(unpaired_pq)['scriptPubKey'])[2:] - unpaired_asset_id = byte_to_base58(hash160(unpaired_program), 111) + unpaired_asset_id = byte_to_base58(bytes.fromhex('01' * 20), 111) assert_equal(node.validateaddress(unpaired_asset_id)['ismine'], False) assert_protected_admin_return( 'addtagtoaddress', (qualifier_name, unpaired_asset_id, diff --git a/test/functional/feature_pq_asset_source_rpc.py b/test/functional/feature_pq_asset_source_rpc.py index ff23ab1f1e..e1af441235 100755 --- a/test/functional/feature_pq_asset_source_rpc.py +++ b/test/functional/feature_pq_asset_source_rpc.py @@ -8,7 +8,6 @@ from decimal import Decimal from test_framework.mininode import CTransaction, from_hex -from test_framework.script import hash160 from test_framework.test_framework import RavenTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error @@ -59,8 +58,10 @@ def run_test(self): node.validateaddress(source_pq)['scriptPubKey'])[2:] source_identity = node.validateaddress(source_classical) assert_equal(source_identity['ismine'], False) - assert_equal(bytes.fromhex(source_identity['scriptPubKey'])[3:23], - hash160(source_program)) + source_script = bytes.fromhex(source_identity['scriptPubKey']) + assert_equal(len(source_script), 25) + assert_equal(source_script[:3], bytes.fromhex('76a914')) + assert_equal(source_script[-2:], bytes.fromhex('88ac')) assert_equal(node.validateaddress(source_pq)['ismine'], True) account_addresses = node.getaddressesbyaccount('pq-assets') assert source_classical not in account_addresses From 2c9576001bad59a729fbd2df58116a9cdfcd554f Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:04:02 +0200 Subject: [PATCH 036/195] [FINDING-166] Report mandatory unit test failures in Build Raven --- .github/workflows/build-raven.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index b0e34796c9..bf31870a3b 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -65,7 +65,15 @@ jobs: - name: Build and run unit tests run: | make -j2 - make check + if ! make check; then + for log in src/test-suite.log src/test/test_raven.log; do + if [ -f "$log" ]; then + echo "Failure details from $log:" + tail -n 160 "$log" + fi + done + exit 1 + fi - name: Exercise the real PQ verifier fuzz target run: | From 1df43214f8159826cb8a27cda29c23c299be3873 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:04:33 +0200 Subject: [PATCH 037/195] [FINDING-165][FINDING-166] Record local verification --- ...0025-v4.8-security-remediation-register.md | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index a6e9c3072b..27abdc858e 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8379,15 +8379,18 @@ Cross-platform CI and public deployment remain separate release gates. but fails its behavioral security invariant step. Both functional tests stop with `ValueError: unsupported hash type ripemd160`. Required functional tests and the build matrix are skipped. -- **Expected behavior:** Preserve the independent identity and unrelated - program assertions without relying on an optional Python hash provider. +- **Expected behavior:** Preserve descriptor script and unrelated identifier + assertions without relying on an optional Python hash provider. Keep the + program-to-identifier binding check in the C++ consensus vectors. - **Regression required:** Reproduce the failure with RIPEMD160 unavailable, then run both tests successfully under the same restriction and in the regular local test environment. Keep the C++ consensus binding vectors. -- **Remediation commit:** None. -- **Verification:** Red CI run `38047870174`; local green and exact-SHA CI - rerun pending. -- **Final status:** OPEN. +- **Remediation commit:** `3c1d8c091` removes the optional Python hash + dependency from both functional tests. +- **Verification:** Red CI run `38047870174`; both functional tests pass + locally with `hashlib.new('ripemd160', ...)` forced to raise, and local + `make check` passes. Exact-SHA CI rerun pending. +- **Final status:** FIXED locally; exact-SHA CI pending. ### FINDING-166: Build Raven hides the failing unit test diagnosis @@ -8415,7 +8418,11 @@ Cross-platform CI and public deployment remain separate release gates. - **Regression required:** Simulate a failing `make check` and prove the workflow reports the specific test failure and still fails; then rerun both workflows on one exact SHA. -- **Remediation commit:** None. -- **Verification:** Red CI run `38047871989`; diagnosis and exact-SHA CI - rerun pending. -- **Final status:** OPEN. +- **Remediation commit:** `2c9576001` prints bounded test logs on a + nonzero `make check` result and keeps the artifact gate mandatory. +- **Verification:** Red CI run `38047871989`; a local simulated failure + showed the original step hid a test assertion, while the revised step + prints it and still fails. Local `make check` passes. Diagnosis of the + original unit failure and exact-SHA CI rerun remain pending. +- **Final status:** OPEN for the underlying CI failure; diagnostic repair + is FIXED locally. From ea104132e2171cd969a79de38635fed3ea5d9e7b Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:16:38 +0200 Subject: [PATCH 038/195] [FINDING-161] Exclude premature PQ-only asset outputs from policy --- src/policy/policy.cpp | 12 ++++ src/policy/policy.h | 1 + src/test/assets/asset_tx_tests.cpp | 9 +++ src/txmempool.cpp | 3 + src/validation.cpp | 3 + test/functional/feature_pq_asset_anchor.py | 76 ++++++++++++++++++++++ 6 files changed, 104 insertions(+) diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index 618c7906c6..2454f748e3 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -82,6 +82,18 @@ bool HasPQWitnessV2Output(const CTransaction& tx) return false; } +bool HasPQOnlyAssetOutput(const CTransaction& tx) +{ + for (const CTxOut& txout : tx.vout) { + const CScript& script = txout.scriptPubKey; + if (script.size() > 25 && script[0] == OP_1 && script[1] == OP_1 && + script[2] == 20 && script[23] == OP_DROP && + script[24] == OP_DROP && script[25] == OP_RVN_ASSET) + return true; + } + return false; +} + bool SpendsPQWitnessV2Program(const CTxIn& txin, const CScript& prevScriptPubKey) { if (IsPQWitnessV2Program(prevScriptPubKey)) diff --git a/src/policy/policy.h b/src/policy/policy.h index 4d6e537cb8..040215ae86 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -87,6 +87,7 @@ bool IsStandard(const CScript& scriptPubKey, txnouttype& whichType, const bool w /** RIP-25 policy predicates shared by admission and reorg cleanup. */ bool IsPQWitnessV2Program(const CScript& scriptPubKey); bool HasPQWitnessV2Output(const CTransaction& tx); +bool HasPQOnlyAssetOutput(const CTransaction& tx); bool SpendsPQWitnessV2Program(const CTxIn& txin, const CScript& prevScriptPubKey); /** diff --git a/src/test/assets/asset_tx_tests.cpp b/src/test/assets/asset_tx_tests.cpp index 8ac0fbf13b..947674695f 100644 --- a/src/test/assets/asset_tx_tests.cpp +++ b/src/test/assets/asset_tx_tests.cpp @@ -462,6 +462,12 @@ BOOST_FIXTURE_TEST_SUITE(asset_tx_tests, BasicTestingSetup) pqOnly.insert(pqOnly.end(), tagged.begin() + 25, tagged.end()); BOOST_REQUIRE(pqOnly.IsAssetScript()); + CMutableTransaction policyCandidate; + policyCandidate.vout.emplace_back(0, pqOnly); + BOOST_CHECK(HasPQOnlyAssetOutput(CTransaction(policyCandidate))); + policyCandidate.vout[0].scriptPubKey = legacy; + BOOST_CHECK(!HasPQOnlyAssetOutput(CTransaction(policyCandidate))); + CMutableTransaction spending; spending.vin.emplace_back(COutPoint(uint256S("06"), 0)); spending.vout.emplace_back(0, legacy); @@ -975,6 +981,9 @@ BOOST_FIXTURE_TEST_SUITE(asset_tx_tests, BasicTestingSetup) // A lookalike created before activation remains outside the anchor // rule by origin height. Wallets must not label it as PQ custody. spend.vin[0].prevout = preActivationPQOnlyOut; + CValidationState preActivationSpend; + BOOST_CHECK(Consensus::CheckTxPQAssets(CTransaction(spend), + preActivationSpend, coins, -1)); CValidationState preActivationPQOnly; BOOST_CHECK(Consensus::CheckTxPQAssets(CTransaction(spend), preActivationPQOnly, coins, diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 1629a6d148..74a9b546c5 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -841,6 +841,9 @@ void CTxMemPool::removeForReorg(CCoinsViewCache *pcoins, unsigned int nMemPoolHe if (removeForPQRollback) txToRemove.insert(it); } + if (pqAssetActivationHeight < 0 && !txToRemove.count(it) && + HasPQOnlyAssetOutput(tx)) + txToRemove.insert(it); if (!txToRemove.count(it)) { CValidationState state; bool valid = CheckTransaction(tx, state, true, true, false, &assetContext); diff --git a/src/validation.cpp b/src/validation.cpp index ad91bcf80d..52bfff91e4 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -564,6 +564,9 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool if (!pqEnabled && HasPQWitnessV2Output(tx)) return state.DoS(0, false, REJECT_NONSTANDARD, "premature-pq-witness", true); + if (pqAssetActivationHeight < 0 && HasPQOnlyAssetOutput(tx)) + return state.DoS(0, false, REJECT_NONSTANDARD, "premature-pq-asset", true); + // Rather not work on nonstandard transactions (unless -testnet/-regtest) std::string reason; if (fRequireStandard && !IsStandardTx(tx, reason, witnessEnabled)) diff --git a/test/functional/feature_pq_asset_anchor.py b/test/functional/feature_pq_asset_anchor.py index 5f919e3630..24fc127bd1 100755 --- a/test/functional/feature_pq_asset_anchor.py +++ b/test/functional/feature_pq_asset_anchor.py @@ -47,6 +47,8 @@ def run_test(self): node.generate(861) node.issue(asset_name, 1, legacy_address) + preactivation_asset = 'PQEARLYPREFIX' + node.issue(preactivation_asset, 1, legacy_address) restricted_base = 'PQRESTRICTBASE' node.issue(restricted_base, 1, legacy_address) node.issuerestrictedasset('$' + restricted_base, 1, 'true', @@ -116,6 +118,68 @@ def run_test(self): assert_equal(node.submitblock(invalid_block.serialize().hex()), 'bad-pq-asset-output') + # An active-tip transfer can be held in the mempool while a reorg + # returns the next block to pre-activation rules. The lookalike must + # leave the mempool and must not be relayed again at the old tip. + early_outpoint = node.listmyassets(preactivation_asset, True)[ + preactivation_asset]['outpoints'][0] + early_native = next(coin for coin in node.listunspent() + if coin['amount'] > Decimal('2')) + early_descriptor = node.getnewpqassetaddress() + early_inputs = [ + {'txid': early_outpoint['txid'], 'vout': early_outpoint['vout']}, + {'txid': early_native['txid'], 'vout': early_native['vout']}, + ] + early_outputs = { + early_descriptor: {'transfer': {preactivation_asset: 1}}, + legacy_address: early_native['amount'] - Decimal('0.01'), + } + early_raw = node.createrawtransaction(early_inputs, early_outputs) + early_signed = node.signrawtransaction(early_raw) + assert_equal(early_signed['complete'], True) + early_txid = node.sendrawtransaction(early_signed['hex']) + assert early_txid in node.getrawmempool() + node.invalidateblock(boundary_hash) + assert_equal(node.getblockcount(), 862) + assert early_txid not in node.getrawmempool() + assert_raises_rpc_error(-26, 'premature-pq-asset', + node.sendrawtransaction, early_signed['hex']) + node.reconsiderblock(boundary_hash) + assert_equal(node.getblockcount(), 863) + + # Historical blocks remain valid. Mine the same lookalike directly + # before the rule activates, then prove its spend needs no anchor. + node.invalidateblock(boundary_hash) + parent_hash = node.getbestblockhash() + block_time = node.getblockheader(parent_hash)['time'] + 1 + historical_block = create_block(int(parent_hash, 16), + create_coinbase(863), block_time) + historical_block.nVersion = node.getblockheader(boundary_hash)['version'] + historical_block.vtx.append(from_hex(CTransaction(), early_signed['hex'])) + historical_block.hashMerkleRoot = historical_block.calc_merkle_root() + historical_block.solve() + assert_equal(node.submitblock(historical_block.serialize().hex()), None) + assert_equal(node.getblockcount(), 863) + assert preactivation_asset not in node.listmyassets(preactivation_asset, True) + + early_asset_index = self.output_index( + early_signed['hex'], + lambda script: len(script) > 31 and script[25] == ASSET_OPCODE) + early_fee_coin = next(coin for coin in node.listunspent() + if coin['amount'] > Decimal('2')) + historical_spend = node.createrawtransaction( + [{'txid': early_txid, 'vout': early_asset_index}, + {'txid': early_fee_coin['txid'], 'vout': early_fee_coin['vout']}], + {early_descriptor: {'transfer': {preactivation_asset: 1}}, + legacy_address: early_fee_coin['amount'] - Decimal('0.01')}) + historical_spend = node.signrawtransaction(historical_spend) + assert_equal(historical_spend['complete'], True) + historical_spend_tx = from_hex(CTransaction(), historical_spend['hex']) + assert_equal(historical_spend_tx.vin[0].scriptSig, b'') + assert historical_spend_tx.wit.is_null() + historical_spend_txid = node.sendrawtransaction(historical_spend['hex']) + assert historical_spend_txid in node.getrawmempool() + asset_outpoint = node.listmyassets(asset_name, True)[asset_name]['outpoints'][0] native_coin = next(coin for coin in node.listunspent() if coin['amount'] > Decimal('2')) @@ -147,6 +211,18 @@ def run_test(self): node.generate(1) assert_equal(node.getblockcount(), 864) assert_equal(node.listmyassets(asset_name, True)[asset_name]['balance'], 1) + protected_early = node.listmyassets(preactivation_asset, True)[ + preactivation_asset] + assert_equal(protected_early['balance'], 1) + protected_early_outpoint = protected_early['outpoints'][0] + no_early_anchor = node.createrawtransaction( + [{'txid': protected_early_outpoint['txid'], + 'vout': protected_early_outpoint['vout']}], + {early_descriptor: {'transfer': {preactivation_asset: 1}}}) + no_early_anchor = node.signrawtransaction(no_early_anchor) + assert_equal(no_early_anchor['complete'], True) + assert_raises_rpc_error(-26, 'bad-pq-asset-anchor', + node.sendrawtransaction, no_early_anchor['hex']) asset_index = self.output_index( migration_signed['hex'], From 90a16eff833142e6022e2ac5e2338ebfeb46cb0a Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:17:08 +0200 Subject: [PATCH 039/195] [FINDING-161] Record boundary regression and limitation --- ...0025-v4.8-security-remediation-register.md | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 27abdc858e..f23c70b3f1 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8220,15 +8220,20 @@ Cross-platform CI and public deployment remain separate release gates. creation and anchored spend as a separate case. - **Remediation commits:** `802bf2433` adds a consensus origin-height vector; `5f34f8cbf` refuses generic PQ-only wallet ownership without - confirmed activation context. An activation-safe creation policy is - still pending. -- **Verification:** The unit vector proves that a pre-activation PQ-only - lookalike remains exempt from the anchor rule after activation. The - contextual wallet method returns `ISMINE_NO` for such a confirmed - output. A functional pre-activation creation and spending regression, - including wallet balance and reorg behavior, remains pending. -- **Final status:** OPEN; a fail-safe historical-lookalike policy and - complete boundary regression are required before asset release. + confirmed activation context; `ea104132e` adds premature-output relay + policy and reorg cleanup. +- **Verification:** The functional regression failed before `ea104132e`: + a PQ-only output transaction remained in the mempool after a rollback + to the pre-activation tip. It passes after the policy change. The test + also directly mines a lookalike in the last pre-activation block, + confirms it is absent from PQ wallet balance, spends it without an + anchor after activation, and proves that its new post-activation output + cannot be spent again without an anchor. Unit vectors cover the + pre-activation spend rule. Local `make check` passes. Historical + directly mined lookalikes remain valid and unprotected by design; + operators must not present them as PQ custody. Exact-SHA CI pending. +- **Final status:** FIXED locally; exact-SHA CI pending, with the + historical-block limitation documented. ### FINDING-162: PQ-only owner and message assets miss wallet subscriptions From a939b16294d8b6d34d896aa3da84ffbb4a89d1b0 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:41:14 +0200 Subject: [PATCH 040/195] [FINDING-160] Document native block upgrade boundary --- doc/RIP-0025-PQ-Signatures.md | 29 +++++++++++++++++-- ...0025-v4.8-security-remediation-register.md | 20 +++++++++++-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/doc/RIP-0025-PQ-Signatures.md b/doc/RIP-0025-PQ-Signatures.md index 1e78bb0eb6..ebe19866df 100644 --- a/doc/RIP-0025-PQ-Signatures.md +++ b/doc/RIP-0025-PQ-Signatures.md @@ -16,7 +16,11 @@ License: MIT This RIP proposes adding **ML-DSA-44** (FIPS 204) as a post-quantum digital signature scheme to Ravencoin via a new **witness version 2** program. New PQ addresses use ML-DSA-44 exclusively (no ECDSA). Existing ECDSA addresses (witness v0) continue working unchanged. Users can migrate native RVN from ECDSA to ML-DSA-44 addresses before quantum computers can break ECDSA. Ravencoin asset outputs are not covered by this version of RIP-25. -The upgrade is deployed as a **soft fork** following the SegWit extensibility model. A phased block weight increase from 8 MWU to 16 MWU, combined with a PQ witness discount factor, ensures that network throughput remains adequate during and after migration. +The witness-v2 signature rule uses the SegWit extensibility model and bit-12 +signaling. The combined deployment also raises the block weight limit from +8 MWU to 12 MWU at activation and to 16 MWU one nominal year later, and +discounts PQ witness bytes. Those block rules require a coordinated network +upgrade from Core 4.8.0. See Backwards Compatibility below. --- @@ -327,6 +331,15 @@ The wallet estimates transaction size before signing using `DummySignTx`. For PQ At 1-minute block times, even Phase 1 provides thousands of PQ transactions per minute, far exceeding current real-world usage. +The 12 and 16 MWU limits are part of the original RIP-25 plan. Core 4.8.0 +rejects blocks above 8 MWU. The new acceptance range and the PQ witness +discount make the combined bit-12 activation incompatible with an unupgraded +Core 4.8.0 node. A local two-binary regtest accepted a 7,600,636-WU block +on both binaries, then accepted an 8,801,320-WU block only on the bit-12 +candidate; Core 4.8.0 returned `bad-blk-length`. This is a hard-fork +boundary for block validation, even though the witness-v2 signature rule +alone is restrictive. + #### 4.3 Dust Threshold For PQ outputs, the spend cost increases proportionally to the ML-DSA witness size: @@ -436,7 +449,7 @@ public: | Phase | Timeline | Description | |-------|----------|-------------| | **Phase 0: Preparation** | Months 1-6 | Software release with dormant PQ code. Community education. Testnet deployment. | -| **Phase 1: Activation** | Months 7-12 | Soft fork activates via BIP9. PQ addresses available. Block weight increases to 12 MWU. | +| **Phase 1: Activation** | Months 7-12 | Bit 12 activates after signaling and lock-in. PQ addresses are available. Block weight increases to 12 MWU, requiring upgraded validating nodes. | | **Phase 2: Encouraged** | Months 13-18 | Wallets default to PQ addresses for new keys. Warnings for legacy addresses. | | **Phase 3: Standard** | Months 19-24 | Block weight increases to 16 MWU. | | **Phase 4: Deprecation** | Months 25-48 | Legacy-only transactions increasingly discouraged. | @@ -489,7 +502,17 @@ If ECDSA is broken before migration completes: ## Backwards Compatibility -This proposal is a **soft fork**. Backwards compatibility is maintained as follows: +The original draft described the whole proposal as a soft fork. That +description applies to the witness-v2 signature restriction alone, not to +the higher block limits or PQ witness discount. Core 4.8.0 validating nodes +can reject blocks that a post-activation node accepts. All validating +services that intend to follow the upgraded chain must install compatible +software before bit 12 becomes active. Miner signaling measures miner +readiness; it does not upgrade old nodes or guarantee their acceptance of +12/16 MWU blocks. Public activation therefore requires a reviewed network +upgrade plan, testnet history verification, and explicit release guidance. + +The remaining transaction compatibility properties are: - **Unupgraded nodes**: See witness v2 outputs as "anyone-can-spend" per BIP141 rules - **Legacy addresses**: Continue to work indefinitely diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index f23c70b3f1..491fff7aa1 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8162,8 +8162,10 @@ Cross-platform CI and public deployment remain separate release gates. - **Root cause:** The candidate allows 12 MWU immediately after native bit-12 activation and 16 MWU after one nominal year. It also applies a contextual PQ witness discount. Core 4.8.0 retains the 8 MWU rule and - its original weight accounting. These expansions conflict with the - soft-fork compatibility claim in the RIP-25 specification. + its original weight accounting. The original RIP-25 proposal explicitly + calls for 12 MWU at PQ activation and 16 MWU one year later, but also + describes the deployment as a soft fork. The block expansion and that + compatibility description cannot both hold against Core 4.8.0. - **Affected file/function/lines:** src/consensus/consensus.h:22-34, limits; src/validation.cpp:2448-2474, contextual limits; src/validation.cpp:4494-4507, block checks; doc/RIP-0025-PQ-Signatures.md:19,492, @@ -8173,6 +8175,10 @@ Cross-platform CI and public deployment remain separate release gates. block whose old-accounted weight exceeds 8 MWU but whose candidate accounting is within the new 12 MWU limit. An upgraded node can accept it while a Core 4.8.0 node rejects it. +- **Owner decision:** Preserve the original 12 MWU limit at bit-12 + activation and 16 MWU one year later. This requires an explicit + incompatible-rule deployment plan and release documentation before + public activation; miner signaling alone does not upgrade old nodes. - **Expected behavior:** Prove old-node acceptance of every post-activation block allowed by the candidate, or explicitly choose and approve a deployment that accounts for the incompatible block rules. Correct the @@ -8184,7 +8190,15 @@ Cross-platform CI and public deployment remain separate release gates. - **Remediation commit:** None. - **Verification:** Static comparison of the baseline 8 MWU constant and candidate contextual 12/16 MWU checks establishes the incompatible - acceptance ranges. A two-binary block regression is pending. + acceptance ranges. In a local two-binary regtest at height 540 after + candidate bit-12 activation, both Core 4.8.0 `b60f50e04` and the + candidate `90a16eff8` accepted a 7,600,636-WU block. Core 4.8.0 + rejected the next 8,801,320-WU block with `bad-blk-length`, while the + candidate accepted it and advanced one block. The two transactions + in the larger block were each below 8 MWU. The baseline was built + from unchanged source with a compiler-only GCC 13 header workaround. + A corresponding two-binary PQ-witness discount regression remains + pending. - **Final status:** OPEN; blocks any claim of a compatible public bit-12 activation and the 4.8.1 release. From 934839ca150c6c395e06f474d1798b0b8b201b93 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:43:16 +0200 Subject: [PATCH 041/195] [FINDING-167] Record pinned FreeType source outage --- ...0025-v4.8-security-remediation-register.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 491fff7aa1..50e8b80ddf 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8445,3 +8445,35 @@ Cross-platform CI and public deployment remain separate release gates. original unit failure and exact-SHA CI rerun remain pending. - **Final status:** OPEN for the underlying CI failure; diagnostic repair is FIXED locally. + +### FINDING-167: Pinned FreeType source host blocks the ARM64 artifact + +- **Severity:** MEDIUM release availability; the artifact gate fails closed. +- **Frozen initial status:** OPEN on 2026-10-12 at `a939b1629`, before + changing the FreeType recipe. +- **Affected RIP-25 invariant:** All five Build Raven release targets must + produce artifacts from the same SHA as a green Final Gate. +- **Affected Core 4.8.0 fix:** None. The FreeType 2.7.1 version, Savannah + redirecting download URL and SHA-256 pin are inherited from Core 4.8.0. +- **Root cause:** On the ARM64 runner, repeated connections to + `download.savannah.gnu.org` timed out. The generic depends fallback URL + was empty, so the final curl call failed with an invalid URL. +- **Affected file/function/lines:** `depends/packages/freetype.mk:1-5`, + pinned source URL and digest; `depends/funcs.mk:22-35`, fetch fallback. +- **Concrete scenario:** Build Raven run `38234048442` on + `1df43214f8159826cb8a27cda29c23c299be3873` passed its mandatory + security gate, then the `build (aarch64, aarch64-linux-gnu)` job failed + in Build Dependencies with six curl connection timeouts and a final + empty-URL error. The same SHA's Final Gate run `38234051822` passed. +- **Expected behavior:** Retrieve the same pinned FreeType archive from a + reachable official mirror, verify its existing SHA-256 digest, and fail + closed if the bytes differ. Requalify all release targets on one SHA. +- **Regression required:** Preserve the red CI log; fetch the archive from + the replacement source and verify its digest against the recipe; run a + clean ARM64 depends fetch and rerun Build Raven and Final Gate on one SHA. +- **Remediation commit:** None. +- **Verification:** The official Savannah direct mirror and the upstream + FreeType SourceForge release both supplied an archive matching the pinned + digest `3a3bb2c4e15ffb433f2032f50a5b5a92558206822e22bfe8cbe339af4aa82f88` + in independent local fetches. CI verification remains pending. +- **Final status:** OPEN; blocks exact-SHA artifact qualification. From 829e895e3318a8d03848230760a6996657876bb5 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:44:08 +0200 Subject: [PATCH 042/195] [FINDING-167] Use direct Savannah FreeType mirror --- depends/packages/freetype.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/packages/freetype.mk b/depends/packages/freetype.mk index 55303caa95..0bd0936e03 100644 --- a/depends/packages/freetype.mk +++ b/depends/packages/freetype.mk @@ -1,6 +1,6 @@ package=freetype $(package)_version=2.7.1 -$(package)_download_path=https://download.savannah.gnu.org/releases/$(package) +$(package)_download_path=https://download-mirror.savannah.gnu.org/releases/$(package) $(package)_file_name=$(package)-$($(package)_version).tar.bz2 $(package)_sha256_hash=3a3bb2c4e15ffb433f2032f50a5b5a92558206822e22bfe8cbe339af4aa82f88 From 4c2bc1948e3515a7cabe7301f13ca013acefcabd Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:44:30 +0200 Subject: [PATCH 043/195] [FINDING-167] Record local ARM64 fetch regression --- doc/RIP-0025-v4.8-security-remediation-register.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 50e8b80ddf..66cc4cd568 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8471,9 +8471,12 @@ Cross-platform CI and public deployment remain separate release gates. - **Regression required:** Preserve the red CI log; fetch the archive from the replacement source and verify its digest against the recipe; run a clean ARM64 depends fetch and rerun Build Raven and Final Gate on one SHA. -- **Remediation commit:** None. +- **Remediation commit:** `829e895e3` switches to the official Savannah + direct mirror without changing the archive name, version or digest. - **Verification:** The official Savannah direct mirror and the upstream FreeType SourceForge release both supplied an archive matching the pinned digest `3a3bb2c4e15ffb433f2032f50a5b5a92558206822e22bfe8cbe339af4aa82f88` - in independent local fetches. CI verification remains pending. -- **Final status:** OPEN; blocks exact-SHA artifact qualification. + in independent local fetches. A clean ARM64 depends fetch through the + revised recipe passed and checked the same digest. The red CI run is + `38234048442`; a green exact-SHA artifact matrix remains pending. +- **Final status:** FIXED locally; exact-SHA CI pending. From 471deaeead7533d238d03e675235a07f13e41e1e Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:46:22 +0200 Subject: [PATCH 044/195] [FINDING-160] Record two-binary PQ witness boundary --- doc/RIP-0025-PQ-Signatures.md | 6 +++++- doc/RIP-0025-v4.8-security-remediation-register.md | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-PQ-Signatures.md b/doc/RIP-0025-PQ-Signatures.md index ebe19866df..98c0c4b75a 100644 --- a/doc/RIP-0025-PQ-Signatures.md +++ b/doc/RIP-0025-PQ-Signatures.md @@ -338,7 +338,11 @@ Core 4.8.0 node. A local two-binary regtest accepted a 7,600,636-WU block on both binaries, then accepted an 8,801,320-WU block only on the bit-12 candidate; Core 4.8.0 returned `bad-blk-length`. This is a hard-fork boundary for block validation, even though the witness-v2 signature rule -alone is restrictive. +alone is restrictive. A separate block with a valid ML-DSA witness-v2 +spend weighed 8,000,697 WU under the old calculation and about +7,998,831 WU after the PQ discount. Core 4.8.0 returned +`bad-blk-weight`, while the candidate accepted it. The discount alone +also crosses the old validation boundary. #### 4.3 Dust Threshold diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 66cc4cd568..cb6c84c310 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8197,8 +8197,13 @@ Cross-platform CI and public deployment remain separate release gates. candidate accepted it and advanced one block. The two transactions in the larger block were each below 8 MWU. The baseline was built from unchanged source with a compiler-only GCC 13 header workaround. - A corresponding two-binary PQ-witness discount regression remains - pending. + A second two-binary test spent a valid ML-DSA witness-v2 output in a + block weighing 8,000,697 WU by the Core 4.8.0 calculation and about + 7,998,831 WU with the candidate's 1,866-WU PQ witness discount. Core + 4.8.0 rejected it with `bad-blk-weight`; the candidate accepted it and + advanced one block. This proves the discount is independently + incompatible at the 8 MWU boundary, even without using the 12 MWU + phase-1 allowance. - **Final status:** OPEN; blocks any claim of a compatible public bit-12 activation and the 4.8.1 release. From f143e7f754a52366af19e928835375814b28d151 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:53:20 +0200 Subject: [PATCH 045/195] [FINDING-168] Freeze wallet rewrite plaintext finding --- ...0025-v4.8-security-remediation-register.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index cb6c84c310..2442650570 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8485,3 +8485,37 @@ Cross-platform CI and public deployment remain separate release gates. revised recipe passed and checked the same digest. The red CI run is `38234048442`; a green exact-SHA artifact matrix remains pending. - **Final status:** FIXED locally; exact-SHA CI pending. + +### FINDING-168: Wallet rewrite recovery can leave PQ plaintext in the database file + +- **Severity:** HIGH confidentiality and release qualification failure. +- **Frozen initial status:** OPEN on 2026-10-12 at `471deaeea`, before + changing the wallet or its regression. +- **Affected RIP-25 invariant:** After wallet encryption and recovery, a + plaintext ML-DSA secret must not remain in the physical wallet database. +- **Affected Core 4.8.0 fix:** None established. This test covers new PQ + wallet custody and the existing Berkeley DB rewrite path. +- **Root cause:** Under investigation. A successful logical recovery does + not by itself prove that old physical database pages were removed. +- **Affected file/function/lines:** `src/wallet/test/pq_wallet_tests.cpp:255-263` + and `:2948-3112`, physical secret scan after restart recovery; + `src/wallet/db.cpp:853-937`, database rewrite and file replacement. +- **Concrete scenario:** Build Raven run `38238492777` on + `471deaeead7533d238d03e675235a07f13e41e1e` passed mandatory + `make check`, then failed `pq_wallet_tests/rewrite_failure_quarantines_until_restart_recovery` + at line 3085 because the physical wallet file still contained the exact + ML-DSA secret after startup rewrite recovery. Final Gate run + `38238496806` passed the same test and SHA, so the condition appears + intermittent. Build Raven skipped every release artifact. +- **Expected behavior:** Recovery keeps the wallet quarantined on rewrite + failure. Once recovery reports success, the physical wallet file and + any exportable backup contain no plaintext PQ secret. +- **Regression required:** Preserve the red CI run, reproduce with the full + wallet suite, localize any physical secret bytes and file transition, + then demonstrate a red-to-green fix and rerun both CI workflows at one + exact SHA. Keep `make check` mandatory. +- **Remediation commit:** Pending investigation. +- **Verification:** Red Build Raven run `38238492777`; green Final Gate + run `38238496806` is conflicting evidence, not closure. One local full + wallet suite passed; repeated runs and byte localization are in progress. +- **Final status:** OPEN. Blocks release artifacts and FINDING-067. From 77036141c001a2dcc2f92d7e213604a16c3be504 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:59:40 +0200 Subject: [PATCH 046/195] [FINDING-168] Reproduce partial BDB page clearing --- src/wallet/test/pq_wallet_tests.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/wallet/test/pq_wallet_tests.cpp b/src/wallet/test/pq_wallet_tests.cpp index 89bfd02fb6..9c03ed1e5d 100644 --- a/src/wallet/test/pq_wallet_tests.cpp +++ b/src/wallet/test/pq_wallet_tests.cpp @@ -2841,6 +2841,27 @@ BOOST_AUTO_TEST_CASE(bip44_encryption_write_failure_returns_and_aborts) BOOST_CHECK(loadedSeed == seed); } +BOOST_AUTO_TEST_CASE(wallet_database_clears_entire_new_page) +{ + const std::string filename = "pq-full-page-clear-wallet.dat"; + CWalletDBWrapper dbw(&bitdb, filename); + CDB db(dbw, "c+"); + + LOCK(bitdb.cs_db); + const auto it = bitdb.mapDb.find(filename); + BOOST_REQUIRE(it != bitdb.mapDb.end()); + BOOST_REQUIRE(it->second != nullptr); + + u_int32_t pageSize = 0; + u_int32_t clearLength = 0; + BOOST_REQUIRE_EQUAL(it->second->get_pagesize(&pageSize), 0); + DbMpoolFile* mpoolFile = it->second->get_mpf(); + BOOST_REQUIRE(mpoolFile != nullptr); + BOOST_REQUIRE_EQUAL(mpoolFile->get_clear_len(&clearLength), 0); + BOOST_REQUIRE_GT(pageSize, 32U); + BOOST_CHECK_EQUAL(clearLength, pageSize); +} + BOOST_AUTO_TEST_CASE(rewrite_discards_stale_regular_temporary_database) { const std::string filename = "pq-rewrite-stale-wallet.dat"; From 75a478fbbdb2867efc68b5595d3edeb8cd21dd13 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:04:09 +0200 Subject: [PATCH 047/195] [FINDING-158] Clarify asset PQ-only migration --- doc/RIP-0025-PQ-Signatures.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/doc/RIP-0025-PQ-Signatures.md b/doc/RIP-0025-PQ-Signatures.md index 98c0c4b75a..ed0f832b54 100644 --- a/doc/RIP-0025-PQ-Signatures.md +++ b/doc/RIP-0025-PQ-Signatures.md @@ -484,15 +484,20 @@ RIP-25 witness-v2 protects native RVN outputs only. It does not change the Raven This scope statement describes the approved bit 12 baseline and the default public-network state. A 4.8.1 working branch has a separate, dormant bit 13 -asset candidate with a classical-plus-PQ ownership rule. That candidate is -not included in the approved PR #1281 semantics and is not public-network -release-qualified. Its local wallet tests do not protect existing holdings -without an authorized migration. See the linked -asset extension note for its constraints and release blockers. +asset candidate. Once bit 13 activates, new asset and owner-token outputs +commit to a PQ program. Spending them requires a matching native RVN +witness-v2 input signed with ML-DSA-44, without an ECDSA signature. Asset +and owner-token outputs use an asset carrier keyed by the PQ program, not a +direct witness-v2 script. Outputs created before activation remain classically +spendable. Their owners may migrate voluntarily by sending them to new +PQ-protected outputs; activation does not protect historical holdings +retroactively. This candidate is outside the approved PR #1281 semantics +and is not yet qualified for public-network release. See the linked asset +extension note for its constraints and release blockers. A wallet or raw-transaction RPC must reject a witness-v2 PQ destination when constructing an asset-bearing output. Appending `OP_RVN_ASSET` data to an `OP_2 <32-byte-program>` script does not create a PQ asset output: it makes the script cease to be a witness program, and current consensus rejects the misplaced asset opcode. -Asset owners therefore retain a post-quantum exposure until a separately specified and activated PQ asset extension exists. In particular, theft of `ASSET!` can transfer administrative control and can authorize reissuance where the asset remains reissuable. See [RIP-25 PQ Asset Extension Design Note](RIP-0025-PQ-Assets-Followup.md). +Under the default public-network parameters, asset owners retain a post-quantum exposure. Even after bit 13 activation, historical outputs retain that exposure until their owners migrate them. In particular, theft of `ASSET!` can transfer administrative control and can authorize reissuance where the asset remains reissuable. See [RIP-25 PQ Asset Extension Design Note](RIP-0025-PQ-Assets-Followup.md). #### 7.4 Emergency Response Plan From 0b6d74bd89dcc13e250f596d85bb1fa73fa1c01d Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:06:35 +0200 Subject: [PATCH 048/195] [FINDING-153] Align asset guidance with PQ-only identifiers --- doc/RIP-0025-PQ-Assets-Followup.md | 72 +++++++++++++++--------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/doc/RIP-0025-PQ-Assets-Followup.md b/doc/RIP-0025-PQ-Assets-Followup.md index bff79d3dbe..8972fa9f6c 100644 --- a/doc/RIP-0025-PQ-Assets-Followup.md +++ b/doc/RIP-0025-PQ-Assets-Followup.md @@ -459,17 +459,15 @@ with these steps: ## 18. Interpreting the Current Asset Index -FINDING-153 is an address-index limitation. Two outputs can commit to different -PQ programs while using the same classical P2PKH key. The index reports one -classical-address row and their combined asset balance. The row cannot tell a -spender which PQ anchor or private key a particular outpoint needs. -`listassetbalancesbyaddress` accepts the classical address, not the composite -descriptor. Copying an index row as an active asset recipient fails because a -new output requires the canonical `classical|PQ` descriptor. Use the actual -output's program and the recorded descriptor for custody and transfers; -retain both keys and their association in backups. A descriptor assembled -manually from two owned keys is not necessarily listed by -`listpqassetaddresses`, which lists persisted wallet pairings. +FINDING-153 described an earlier dual-signature candidate in which two PQ +programs could share a classical address-index row. The PQ-only format now +derives a distinct synthetic asset identifier from each PQ program. A +historical classical output still has its original address identifier, so +index rows alone do not establish whether an outpoint needs a classical key +or a matching PQ anchor. Use the actual output's script and recorded +descriptor for custody and transfers. A descriptor assembled manually from +owned key material is not necessarily listed by `listpqassetaddresses`, +which lists persisted wallet pairings. ## 19. Migrating Historical Assets and Owner Tokens @@ -479,8 +477,9 @@ still authorize a spend, but every spendable asset output newly created after activation needs a canonical PQ tag. Migrate before the old classical key can be compromised: -1. Back up the wallet and verify that both keys in each destination descriptor - can be recovered. Inventory historical asset and `ASSET!` owner-token +1. Back up the wallet and verify that the destination PQ key and the + classical keys for historical outputs can be recovered. Inventory + historical asset and `ASSET!` owner-token outpoints, including their quantities and classical addresses. 2. For a single legacy asset UTXO, transfer its entire quantity to a new canonical `classical|PQ` descriptor. Transfer an owner token's full unit @@ -493,8 +492,9 @@ be compromised: asset change. The GUI path has no equivalent explicit asset-change field. 4. Verify that every newly created spendable asset and owner-token output has its intended PQ program. Fund each destination's native PQ address with - RVN before its first protected spend. Keep the original descriptor and - both private keys available for recovery. + RVN before its first protected spend. Keep the original descriptor and PQ + key available for recovery, and retain classical keys for any historical + holdings. No software can distinguish the rightful owner from an attacker who already controls the old classical key. Migration does not retroactively protect the @@ -503,19 +503,19 @@ historical UTXO, and a compromised holder can be raced before confirmation. ## 20. PQ-Only Asset Ownership Decision and Candidate Format The owner selected the native-RVN migration model for assets and owner tokens. -Outputs created under the future bit-13 rule must require an ML-DSA-44 key -without an ECDSA signature. Historical asset and owner-token outputs retain -their classical spending rule and migrate voluntarily, with no sunset. The -current `classical|PQ` candidate still needs an ECDSA signature and therefore -does not meet this release requirement. FINDING-158 tracks the redesign. +Outputs created under the bit-13 rule require an ML-DSA-44 key without an +ECDSA signature. Historical asset and owner-token outputs retain their +classical spending rule and migrate voluntarily, with no sunset. The +PQ-only format and wallet path are implemented locally under FINDING-158; +public activation and release qualification remain pending. An input carrying a native witness-v2 RVN output with the matching PQ program -may provide the ML-DSA authorization for an asset input. The owner confirmed -this construction is acceptable if the asset input needs no ECDSA signature. -The separate native input still needs RVN funding and may need replenishment -after the spend, as described in section 17. +provides the ML-DSA authorization for an asset input. The asset input has an +empty scriptSig and needs no ECDSA signature. The separate native input still +needs RVN funding and may need replenishment after the spend, as described in +section 17. -One provisional asset locking prefix is exactly 25 bytes: +The PQ-only asset locking prefix is exactly 25 bytes: ``` OP_1 OP_1 PUSH20 HASH160(P) OP_DROP OP_DROP @@ -526,22 +526,20 @@ The asset opcode remains at byte 25, so the existing asset parser identifies the output and extracts `HASH160(P)` as its address identifier. The prefix executes to one true stack item with an empty scriptSig; it does not execute ECDSA. The existing script interpreter treats every byte after -`OP_RVN_ASSET` as asset data. A new consensus rule must require a matching -valid witness-v2 anchor whenever such an output is spent at an active origin -height, and must reject the old dual-signature prefix on new outputs. The +`OP_RVN_ASSET` as asset data. Consensus requires a matching valid witness-v2 +anchor whenever such an output is spent at an active origin height and +rejects the old dual-signature prefix on new outputs. The `HASH160(P)` binding prevents a sender from selecting an unrelated qualified or unfrozen classical address while assigning ownership to a different PQ program. The 160-bit address identifier and restricted-asset semantics need separate adversarial review. -A red-then-green unit test now shows that this prefix passes the existing +A red-then-green unit test shows that this prefix passes the existing standard script check with an empty scriptSig, is recognized as an asset, and is parsed only when the address identifier matches `HASH160(P)`. This -is a format feasibility result, not full compatibility evidence. The -following remain required before activation: old-node block acceptance, -full asset conservation and index behavior, active-only output enforcement, -wallet PQ ownership and signing, RPC and Qt destination handling, owner -tokens, restricted address administration, migration, reorgs, recovery, -independent review, and paired release-target CI at one exact SHA. The -previously approved bit-13 deployment dates apply to the earlier candidate -and must be reviewed against the final PQ-only rule before they enter code. +is a format feasibility result, not full compatibility evidence. Before +activation, FINDING-160 requires an explicit network upgrade plan for blocks +above Core 4.8.0 limits. Wallet, RPC, Qt, migration and consensus regressions +need paired release-target CI at one exact SHA. The owner approved bit-13 +start and timeout dates for this PQ-only rule; they remain dormant in code +pending CI qualification and historical testnet compatibility review. From b8f88f126148ca3a0db8a8c82fd69d04413aaadb Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:09:25 +0200 Subject: [PATCH 049/195] [FINDING-148] Prove dust anchor recovery --- test/functional/feature_pq_asset_anchor.py | 75 ++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/test/functional/feature_pq_asset_anchor.py b/test/functional/feature_pq_asset_anchor.py index 24fc127bd1..f53544002b 100755 --- a/test/functional/feature_pq_asset_anchor.py +++ b/test/functional/feature_pq_asset_anchor.py @@ -1281,6 +1281,81 @@ def test_anchor_availability_limits(self, node): exact_descriptor) node.generate(1) + # Funded raw change below the dust threshold is added to the fee. + # The protected asset remains owned, but its sole anchor is gone. + dust_name = 'PQDUSTCHANGE' + dust_descriptor = node.getnewpqassetaddress() + _, dust_pq = dust_descriptor.split('|') + dust_script = bytes.fromhex( + node.validateaddress(dust_pq)['scriptPubKey']) + node.issue(dust_name, 1, dust_descriptor) + dust_anchor_txid = node.sendtoaddress(dust_pq, Decimal('1')) + node.generate(1) + dust_anchor = {'txid': dust_anchor_txid, 'vout': self.output_index( + node.getrawtransaction(dust_anchor_txid), + lambda script: script == dust_script)} + dust_outpoint = node.listmyassets(dust_name, True) + dust_outpoint = dust_outpoint[dust_name]['outpoints'][0] + dust_input = {key: dust_outpoint[key] for key in ('txid', 'vout')} + dust_recipient = node.getnewaddress() + dust_outputs = { + dust_descriptor: {'transfer': {dust_name: 1}}, + dust_recipient: Decimal('0.5'), + } + dust_options = {'changeAddress': dust_pq} + probe_raw = node.createrawtransaction( + [dust_input, dust_anchor], dust_outputs) + probe = node.fundrawtransaction(probe_raw, dust_options) + assert probe['changepos'] >= 0 + probe_tx = from_hex(CTransaction(), probe['hex']) + probe_change = probe_tx.vout[probe['changepos']] + assert_equal(probe_change.scriptPubKey, dust_script) + assert probe_change.nValue > 1_000 + assert_equal(self.input_outpoints(probe_tx), { + (dust_input['txid'], dust_input['vout']), + (dust_anchor['txid'], dust_anchor['vout']), + }) + + # Keep 1,000 satoshis of the probe's native change. This is below + # the wallet's PQ witness-v2 discard threshold, so no change output + # can refresh the anchor. + dust_outputs[dust_recipient] += ( + Decimal(probe_change.nValue - 1_000) / Decimal(100_000_000)) + dust_raw = node.createrawtransaction( + [dust_input, dust_anchor], dust_outputs) + dust_funded = node.fundrawtransaction(dust_raw, dust_options) + assert_equal(dust_funded['changepos'], -1) + assert dust_funded['fee'] > probe['fee'] + dust_signed = node.signrawtransaction(dust_funded['hex']) + assert_equal(dust_signed['complete'], True) + dust_tx = from_hex(CTransaction(), dust_signed['hex']) + assert_equal(len(dust_tx.vout), 2) + assert_equal(self.input_outpoints(dust_tx), { + (dust_input['txid'], dust_input['vout']), + (dust_anchor['txid'], dust_anchor['vout']), + }) + assert not any(output.scriptPubKey == dust_script + for output in dust_tx.vout) + dust_txid = node.sendrawtransaction(dust_signed['hex']) + assert dust_txid in node.getblock(node.generate(1)[0])['tx'] + assert_equal(node.listmyassets(dust_name, True) + [dust_name]['balance'], 1) + assert_raises_rpc_error(-25, 'funded matching PQ anchor', + node.transferfromaddress, dust_name, + dust_descriptor, 1, dust_descriptor) + + dust_topup_txid = node.sendtoaddress(dust_pq, Decimal('0.01')) + node.generate(1) + dust_topup = (dust_topup_txid, self.output_index( + node.getrawtransaction(dust_topup_txid), + lambda script: script == dust_script)) + dust_restored_txid = node.transferfromaddress( + dust_name, dust_descriptor, 1, dust_descriptor)[0] + dust_restored = from_hex( + CTransaction(), node.getrawtransaction(dust_restored_txid)) + assert dust_topup in self.input_outpoints(dust_restored) + node.generate(1) + @staticmethod def input_outpoints(tx): return {(format(txin.prevout.hash, '064x'), txin.prevout.n) From 5f21bb18ffed17d6290775794fe41e48f03f5d4b Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:10:41 +0200 Subject: [PATCH 050/195] [FINDING-168] Clear entire new Berkeley DB pages --- depends/packages/bdb.mk | 2 ++ depends/patches/bdb/clear_new_pages.patch | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 depends/patches/bdb/clear_new_pages.patch diff --git a/depends/packages/bdb.mk b/depends/packages/bdb.mk index 050a60adde..41de4f668d 100644 --- a/depends/packages/bdb.mk +++ b/depends/packages/bdb.mk @@ -3,6 +3,7 @@ $(package)_version=4.8.30 $(package)_download_path=https://download.oracle.com/berkeley-db $(package)_file_name=db-$($(package)_version).NC.tar.gz $(package)_sha256_hash=12edc0df75bf9abd7f82f821795bcee50f42cb2e5f76a6a281b85732798364ef +$(package)_patches=clear_new_pages.patch $(package)_build_subdir=build_unix define $(package)_set_vars @@ -13,6 +14,7 @@ $(package)_cxxflags=-std=c++11 endef define $(package)_preprocess_cmds + patch -p1 < $($(package)_patch_dir)/clear_new_pages.patch && \ sed -i.old 's/__atomic_compare_exchange/__atomic_compare_exchange_db/' dbinc/atomic.h && \ sed -i.old 's/atomic_init/atomic_init_db/' dbinc/atomic.h mp/mp_region.c mp/mp_mvcc.c mp/mp_fget.c mutex/mut_method.c mutex/mut_tas.c && \ cp -f $(BASEDIR)/config.guess $(BASEDIR)/config.sub dist diff --git a/depends/patches/bdb/clear_new_pages.patch b/depends/patches/bdb/clear_new_pages.patch new file mode 100644 index 0000000000..7c662560f6 --- /dev/null +++ b/depends/patches/bdb/clear_new_pages.patch @@ -0,0 +1,15 @@ +--- a/db/db.c ++++ b/db/db.c +@@ -549,3 +549,3 @@ +- clear_len = CRYPTO_ON(env) ? +- (dbp->pgsize != 0 ? dbp->pgsize : DB_CLEARLEN_NOTSET) : +- DB_PAGE_DB_LEN; ++ /* Clear the full page so old records cannot survive page reuse. */ ++ clear_len = dbp->pgsize != 0 ? ++ dbp->pgsize : DB_CLEARLEN_NOTSET; +@@ -555,3 +555,2 @@ +- clear_len = CRYPTO_ON(env) ? +- (dbp->pgsize != 0 ? dbp->pgsize : DB_CLEARLEN_NOTSET) : +- DB_PAGE_DB_LEN; ++ clear_len = dbp->pgsize != 0 ? ++ dbp->pgsize : DB_CLEARLEN_NOTSET; From a693657c8963f54384ac5848d4395f419aba5426 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:12:25 +0200 Subject: [PATCH 051/195] [FINDING-148][FINDING-150][FINDING-151] Document anchor limits --- ...0025-v4.8-security-remediation-register.md | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 2442650570..67d663386f 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -5464,9 +5464,12 @@ and final release SHA require a new qualification pair. returned protected program inside fee and coin selection, or explicitly reject a spend that would leave no usable anchor. The latter would also reject some valid multi-program and explicit-change workflows. -- **Proposed remediation:** Design and boundary-test per-program native - anchor reservation without altering consensus or silently breaking - explicit change. Document the current manual top-up procedure until then. +- **Release decision:** Do not add automatic refresh to 4.8.1. Reserving a + funded output for every returned PQ program changes fee accounting, coin + selection and explicit native-change behavior. The bounded release path + documents a matching RVN top-up for each consumed anchor and proves that + an unanchored spend fails safely. A later wallet design may add automatic + reservation without changing consensus. - **Regression required:** Exact-fee, dust, two-program, and explicit-change spends, followed by another protected spend without external funding. - **Verification:** `3424ddc5e` adds the two-program and explicit-change @@ -5474,9 +5477,13 @@ and final release SHA require a new qualification pair. output. Each retains wallet asset custody, rejects the next unanchored protected spend, then succeeds after a matching RVN top-up. The focused `feature_pq_asset_anchor.py` passed locally with Python 3.10 after - `d64ed5a81`. Dust behavior still needs a separate boundary vector. - The manual recovery procedure is documented by `193d26483`. -- **Final status:** OPEN; documented fail-safe wallet availability limitation. + `d64ed5a81`. `b8f88f126` adds a dust boundary: a 1,000-satoshi native + remainder becomes fee, no replacement anchor is made, the next spend is + rejected, and a matching RVN top-up restores it. The full focused test + passed locally on 2026-10-12. The manual recovery procedure is documented + by `193d26483`; exact-SHA CI remains pending. +- **Final status:** DOCUMENTED LIMITATION for 4.8.1, fail-safe locally; + exact-SHA CI pending. ### FINDING-142: Pinned native ccache source is unavailable to exact-SHA CI @@ -7811,7 +7818,8 @@ Cross-platform CI and public deployment remain separate release gates. consume the sole matching anchor. The asset balance remains in the wallet, a subsequent protected spend fails, and a matching PQ top-up restores it. The focused functional test passed locally. -- **Final status:** OPEN; documented fail-safe availability limitation. +- **Final status:** DOCUMENTED LIMITATION for 4.8.1, fail-safe locally; + exact-SHA CI pending. ### FINDING-151: Raw transaction funding does not select asset PQ anchors @@ -7844,7 +7852,8 @@ Cross-platform CI and public deployment remain separate release gates. matching anchor and consensus rejects the signed transaction. Supplying the anchor and an RVN return output to its PQ address succeeds, and a later protected spend remains possible. The focused test passed locally. -- **Final status:** OPEN; documented fail-safe manual construction path. +- **Final status:** DOCUMENTED LIMITATION for 4.8.1, fail-safe locally; + exact-SHA CI pending. ## Findings frozen by the independent merged asset-phase review From 45855ddd49a5ce3b4d7f792eccf93d2d8d5b2aa0 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:12:34 +0200 Subject: [PATCH 052/195] [FINDING-153] Mark prior index aggregation superseded --- ...0025-v4.8-security-remediation-register.md | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 67d663386f..37e624b5b5 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7894,30 +7894,35 @@ Cross-platform CI and public deployment remain separate release gates. consensus bypass or loss of asset keys is shown. - **Frozen initial status:** OPEN at merged commit `7dc44845309ae36e33a55d3122e62603fcaed9dd`. -- **Affected RIP-25 invariant:** An active asset recipient is identified by - its classical address and committed PQ program together. +- **Affected RIP-25 invariant:** The earlier dual-signature candidate + identified an active asset recipient by a classical address and PQ program + together. The current PQ-only candidate derives a distinct synthetic + identifier from each PQ program. - **Affected Core 4.8.0 fix:** None; the index predates bit 13. -- **Root cause:** `listaddressesbyasset` reads the classical-address asset - index, while `listassetbalancesbyaddress` accepts only a classical address. - The index cannot distinguish two PQ programs paired with one classical key. +- **Root cause:** The earlier candidate let `listaddressesbyasset` aggregate + outputs under one classical-address index row even when their PQ programs + differed. That output format has been superseded for new protected assets. - **Affected file/function/lines:** `src/rpc/assets.cpp:787-791,1138-1157`; `src/coins.cpp:291-302`. -- **Concrete scenario:** Two distinct protected descriptors share a - classical key; the RPC reports one aggregated row. Its returned address - alone cannot receive a new protected asset after activation. +- **Concrete scenario:** In the earlier candidate, two protected descriptors + shared a classical key and the RPC reported one aggregate row. In the + current PQ-only format their program-derived identifiers are distinct. + Historical classical outputs retain their original index semantics. - **Expected behavior:** Expose descriptor-aware outpoint information, or explicitly label this legacy aggregate as non-destination data. -- **Regression required:** Fund two programs paired with one classical key, - then compare the aggregate RPC result against output tags and wallet - history; prove copying the aggregate address fails safely as a protected - recipient. -- **Remediation commit:** None; descriptor-aware index design remains open. - `193d26483` documents the legacy aggregate semantics. -- **Verification:** `588ef7ec8` exercises two PQ programs sharing a - classical key, checks the aggregated RPC balance, and proves that the - classical-only row fails safely as an active recipient. The focused source - RPC functional test passed locally. -- **Final status:** OPEN; documented fail-safe index limitation. +- **Regression required:** Retain the earlier aggregate vector as historical + evidence. Verify that current PQ-only outputs under different programs + have distinct identifiers and that a mismatched identifier/program pair + is rejected. +- **Remediation commit:** `193d26483` documented the earlier aggregate + semantics. `0b6d74bd8` updates the asset design note for the PQ-only + candidate. The format redesign is tracked by FINDING-158. +- **Verification:** `588ef7ec8` exercises the superseded dual-signature + format and its fail-safe recipient check. The current source RPC functional + vector asserts distinct synthetic identifiers for two PQ programs and + rejects an identifier/program mismatch. Exact-SHA CI remains pending. +- **Final status:** SCOPE SUPERSEDED for new PQ-only outputs. Historical + classical index rows remain descriptive data, not proof of PQ custody. ### FINDING-154: Qt asset control displays and copies a classical-only address From dedfe0c2500d7d7fe1502320e5483ebc1a3dd538 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:12:43 +0200 Subject: [PATCH 053/195] [FINDING-154] Record pinned Qt interaction evidence --- ...0025-v4.8-security-remediation-register.md | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 37e624b5b5..36e7772aec 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7937,13 +7937,16 @@ Cross-platform CI and public deployment remain separate release gates. by the extracted classical destination; Copy address uses that same field. - **Affected file/function/lines:** `src/qt/walletmodel.cpp:737-756`; `src/qt/assetcontroldialog.cpp:282-288,649-705`. -- **Concrete scenario:** Two tagged outputs with a common classical key - appear under one address and Copy address yields a classical-only string. +- **Concrete scenario:** In the earlier dual-signature candidate, two tagged + outputs could share a classical key and Copy address yielded a + classical-only string. The PQ-only candidate instead uses distinct + program-derived identifiers; its list, tree and clipboard still need + interactive qualification. - **Expected behavior:** Show the full protected descriptor per active output and copy a valid descriptor, without relabeling historical outputs. -- **Regression required:** GUI exercise with two PQ programs under one - classical key, including group labels and clipboard content. Build the - Qt executable against depends Qt 5.12.11. +- **Regression required:** GUI exercise with two distinct PQ programs and + synthetic identifiers, including list and tree labels and clipboard + content. Build the Qt executable against depends Qt 5.12.11. - **Remediation commit:** `879a818c6` groups coin-control outputs by their contextual descriptor and copies the complete protected destination. - **Verification:** The existing contextual formatter unit test covers @@ -7952,11 +7955,15 @@ Cross-platform CI and public deployment remain separate release gates. Qt5Core.pc; make -j4, make check, and raven-qt -version pass with that prefix. The local GCC 13 build needed -Wno-error=array-bounds while compiling the pinned libxkbcommon dependency; the Qt recipe - and version were not changed. Interactive list, tree, and clipboard - checks for two PQ programs sharing one classical key remain pending. - This compilation alone does not prove GUI destination behavior. -- **Final status:** OPEN; pinned Qt build passes locally, but the - interactive GUI regression and final-SHA CI remain pending. + and version were not changed. A later local Xvfb session with that + pinned Qt version showed two PQ-only asset outpoints under distinct + descriptors in list and tree views; Copy address returned each full + `classical|PQ` descriptor. Screenshots and copied strings are retained + with the private audit evidence. The local GCC 13 executable required + a process-local workaround for a Qt 5.12.11 `readlink` fortify crash; + the workaround is not part of the repository or release artifact. +- **Final status:** OPEN. The interactive source build passed locally, + but the unmodified release artifact and exact-SHA CI remain pending. ### FINDING-155: Partial migration of a legacy asset UTXO needs explicit PQ change From d64c34830d1f7b5ec17c7efb7616f4281874a0ea Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:13:04 +0200 Subject: [PATCH 054/195] [FINDING-168] Reject unsafe wallet rewrite pages --- src/wallet/db.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/wallet/db.cpp b/src/wallet/db.cpp index 903f53688e..ffd4e613de 100644 --- a/src/wallet/db.cpp +++ b/src/wallet/db.cpp @@ -865,6 +865,23 @@ bool CDB::Rewrite(CWalletDBWrapper& dbw, const char* pszSkip) fSuccess = false; } + if (fSuccess) { + u_int32_t pageSize = 0; + u_int32_t clearLength = 0; + DbMpoolFile* mpf = pdbCopy->get_mpf(); + const int pageResult = pdbCopy->get_pagesize(&pageSize); + const int clearResult = mpf ? mpf->get_clear_len(&clearLength) : EINVAL; + // Berkeley DB treats an unset clear length as a full page. + if (pageResult != 0 || clearResult != 0 || pageSize == 0 || + (clearLength != std::numeric_limits::max() && + clearLength != pageSize)) { + LogPrintf("CDB::Rewrite: Refusing rewrite without full-page clearing " + "(page result %d, clear result %d, page size %u, clear length %u)\n", + pageResult, clearResult, pageSize, clearLength); + fSuccess = false; + } + } + Dbc* pcursor = fSuccess ? db.GetCursor() : nullptr; bool fReachedEnd = false; if (!pcursor) From 9151946ad27857d4d4c0016c74819476d1296557 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:13:32 +0200 Subject: [PATCH 055/195] [FINDING-168] Record full-page regression and local proof --- ...0025-v4.8-security-remediation-register.md | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 36e7772aec..ea89ad737b 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -949,7 +949,10 @@ from the demonstrated coverage gaps. encryption still rotates the legacy HD seed/keypool as before. The only new externally visible behavior is fail-closed recovery when the required plaintext-slack removal has not completed. -- **Final status:** FIXED +- **Final status:** FIXED locally again under FINDING-168. Quarantine and + restart recovery remain intact; the patched Berkeley DB now clears full + pages and the wallet rejects an unsafe rewrite dependency. Exact-SHA CI + and artifact qualification remain pending. ### FINDING-019 : Nested witness deserialization expands 16 MB to hundreds of MiB @@ -8514,13 +8517,20 @@ Cross-platform CI and public deployment remain separate release gates. changing the wallet or its regression. - **Affected RIP-25 invariant:** After wallet encryption and recovery, a plaintext ML-DSA secret must not remain in the physical wallet database. -- **Affected Core 4.8.0 fix:** None established. This test covers new PQ - wallet custody and the existing Berkeley DB rewrite path. -- **Root cause:** Under investigation. A successful logical recovery does - not by itself prove that old physical database pages were removed. +- **Affected Core 4.8.0 fix:** None. Core 4.8.0 uses the same Berkeley DB + optimization; the 2,560-byte ML-DSA secret and recovery test are new to + this phase. +- **Root cause:** Pinned Berkeley DB 4.8.30 sets the new BTREE page clear + length to 32 bytes when its own encryption is off. Its memory pool can + reuse buffers containing a deleted 2,560-byte PQ secret, and the rest of + a newly allocated 4,096-byte page is not cleared before it is written. + Logical record deletion and a successful rewrite therefore do not prove + the active wallet file is free of those bytes. - **Affected file/function/lines:** `src/wallet/test/pq_wallet_tests.cpp:255-263` - and `:2948-3112`, physical secret scan after restart recovery; - `src/wallet/db.cpp:853-937`, database rewrite and file replacement. + and lines 2969-3133, physical secret scan after restart recovery; + `src/wallet/db.cpp:788-960`, database rewrite and file replacement; + pinned Berkeley DB `db/db.c:545-557`, page clear-length policy, and + `mp/mp_fget.c:804-814`, new-page initialization. - **Concrete scenario:** Build Raven run `38238492777` on `471deaeead7533d238d03e675235a07f13e41e1e` passed mandatory `make check`, then failed `pq_wallet_tests/rewrite_failure_quarantines_until_restart_recovery` @@ -8531,12 +8541,26 @@ Cross-platform CI and public deployment remain separate release gates. - **Expected behavior:** Recovery keeps the wallet quarantined on rewrite failure. Once recovery reports success, the physical wallet file and any exportable backup contain no plaintext PQ secret. -- **Regression required:** Preserve the red CI run, reproduce with the full - wallet suite, localize any physical secret bytes and file transition, - then demonstrate a red-to-green fix and rerun both CI workflows at one +- **Regression required:** Preserve the red CI run. Assert that the + Berkeley DB wallet page clear length equals its page size, retain the + exact physical secret scan after recovery, then demonstrate a red-to-green + result with the patched dependency and rerun both CI workflows at one exact SHA. Keep `make check` mandatory. -- **Remediation commit:** Pending investigation. +- **Remediation commits:** `77036141c` adds the deterministic red + clear-length regression. `5f21bb18f` patches the pinned Berkeley DB + Btree, Recno and Hash page policy to clear every new page in full. + `d64c34830` makes `CDB::Rewrite` refuse a dependency whose new database + pages are not fully cleared, preserving wallet quarantine on failure. - **Verification:** Red Build Raven run `38238492777`; green Final Gate - run `38238496806` is conflicting evidence, not closure. One local full - wallet suite passed; repeated runs and byte localization are in progress. -- **Final status:** OPEN. Blocks release artifacts and FINDING-067. + run `38238496806` is conflicting evidence, not closure. The new + `wallet_database_clears_entire_new_page` regression failed against the + original pinned library with `32 != 4096`, then passed after rebuilding + against the patched dependency. Fifteen local full wallet suites and 250 + isolated recovery tests passed on Ubuntu 24.04 before the patch, + confirming that repetition alone is not a reliable release gate. After + the patch, local `make check`, the full RIP-25 behavioral invariant gate + with Python 3.10, and all nine required functional tests passed. These + checks cover the active wallet file and backups, not forensic erasure of + old filesystem blocks or Berkeley DB transaction logs. +- **Final status:** FIXED locally; exact-SHA CI and artifacts pending, so + FINDING-067 remains OPEN. From 14b7ead086a999af3bf539ae035ea77294471aa9 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:48:10 +0200 Subject: [PATCH 056/195] [FINDING-067] Record exact-SHA CI and Qt artifact qualification --- ...0025-v4.8-security-remediation-register.md | 193 ++++++++++++------ 1 file changed, 132 insertions(+), 61 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index ea89ad737b..a3171731d8 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -949,10 +949,10 @@ from the demonstrated coverage gaps. encryption still rotates the legacy HD seed/keypool as before. The only new externally visible behavior is fail-closed recovery when the required plaintext-slack removal has not completed. -- **Final status:** FIXED locally again under FINDING-168. Quarantine and +- **Final status:** FIXED under FINDING-168 at `9151946ad`. Quarantine and restart recovery remain intact; the patched Berkeley DB now clears full - pages and the wallet rejects an unsafe rewrite dependency. Exact-SHA CI - and artifact qualification remain pending. + pages and the wallet rejects an unsafe rewrite dependency. Both required + CI runs passed on that SHA; final release-SHA qualification remains due. ### FINDING-019 : Nested witness deserialization expands 16 MB to hundreds of MiB @@ -4401,6 +4401,30 @@ to the dual-signature candidate; do not activate a redesigned PQ-only rule until its compatibility, regressions, and deployment parameters are reviewed. FINDING-067 remains OPEN. +**2026-10-13 exact-SHA qualification checkpoint:** Build Raven run +`38296258242` and RIP-25 v4.8 Final Gate run `38296262444` both succeeded +at `9151946ad27857d4d4c0016c74819476d1296557`. The mandatory +`make check`, wallet rewrite and full-page-clear regressions, nine focused +security functional tests, and the 32-case functional matrix passed in the +required security jobs. Final Gate also completed its eight build jobs. +Build Raven produced five unsigned artifacts on that SHA: Linux x86_64 +`11770728793`, aarch64 `11771315730`, arm32v7 `11770548092`, Windows +MinGW `11771318535`, and macOS `11771367693`. The Linux artifact's +published SHA-256 file verified locally, and its provenance names the +same full SHA. The unmodified Linux `raven-qt` artifact launched without +the workaround needed by the local GCC 13 source build. On a private +regtest fixture with two different PQ-only recipients, Asset Control +showed separate rows and tree groups; Copy address returned each complete +descriptor. This qualifies FINDING-089, 143-147, 152, 154, 159, 161-165, +167, and 168 on this candidate SHA. FINDING-148, 150, and 151 remain +documented fail-safe limitations with passing recovery regressions. +FINDING-158's implementation passed CI, but public activation remains +blocked by FINDING-160 and the release steps below. The old unexplained +`make check` failure in FINDING-166 remains separately open. This is an +intermediate 4.8.0-versioned SHA, not the final 4.8.1 release SHA: +approved public deployment parameters, the 4.8.1 version change, final +same-SHA artifact qualification, and release approval remain outstanding. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical @@ -5323,8 +5347,9 @@ and final release SHA require a new qualification pair. - **Regression:** `feature_pq_asset_authority_selection.py` creates two authority UTXOs, funds only one matching anchor, verifies selection, and verifies the missing-anchor rejection. Passed in the 32-case matrix. -- **Final status:** FIXED for the dual-signature candidate, verified by the - exact-SHA cross-platform CI pair above. PQ-only requalification is pending. +- **Final status:** FIXED. The PQ-only authority functional test passed in + both required security jobs at `9151946ad`; all five artifacts built on + the same SHA. ### FINDING-144: Refreshed PQ anchor appears as a received payment @@ -5352,8 +5377,9 @@ and final release SHA require a new qualification pair. change classification. - **Regression:** `feature_pq_asset_anchor.py:250-268` failed before the fix, then passed before and after node restart. -- **Final status:** FIXED for the dual-signature candidate, verified by the - exact-SHA cross-platform CI pair above. PQ-only requalification is pending. +- **Final status:** FIXED. The PQ-only anchor accounting test passed in + both required security jobs at `9151946ad`; all five artifacts built on + the same SHA. ### FINDING-145: Key-only salvage loses protected asset destination pairing @@ -5381,10 +5407,10 @@ and final release SHA require a new qualification pair. - **Regression:** `pq_wallet_tests/protected_asset_destination_pair_survives_key_only_recovery` failed before the filter change and passes after it. Mismatched, malformed, oversized, trailing, and unrelated rows are rejected. -- **Final status:** FIXED for the dual-signature candidate, verified by the - exact-SHA cross-platform CI pair above. Seed-only association - reconstruction remains a separate wallet-recovery limitation; PQ-only - requalification is pending. +- **Final status:** FIXED. The wallet recovery unit tests passed in both + required security jobs at `9151946ad`, and all five artifacts built on + the same SHA. Seed-only association reconstruction remains a separate + wallet-recovery limitation. ### FINDING-146: History can imply PQ protection for an old asset output @@ -5414,9 +5440,9 @@ and final release SHA require a new qualification pair. covers historical, active, unconfirmed, disabled, and missing-tag cases; `feature_pq_asset_wallet_ownership.py` checks a real historical output. Both pass, and the Qt executable compiles and starts with `-version`. -- **Final status:** FIXED for the dual-signature candidate, verified by the - exact-SHA cross-platform CI pair above. Manual GUI interaction testing - remains under FINDING-154, and PQ-only requalification is pending. +- **Final status:** FIXED. The origin-height history regression passed in + both required security jobs at `9151946ad`. The unmodified Linux Qt + artifact was also exercised under FINDING-154. ### FINDING-147: Asset history RPC drops the PQ destination component @@ -5443,8 +5469,9 @@ and final release SHA require a new qualification pair. - **Regression:** `feature_pq_asset_anchor.py:255-258` was red before the fix and passes afterward; the historical vector in `feature_pq_asset_wallet_ownership.py:92-94` remains classical. -- **Final status:** FIXED for the dual-signature candidate, verified by the - exact-SHA cross-platform CI pair above. PQ-only requalification is pending. +- **Final status:** FIXED. The PQ-only anchor and wallet ownership + functional tests passed in both required security jobs at `9151946ad`; + all five artifacts built on the same SHA. ### FINDING-148: Some wallet spends consume an anchor without refreshing it @@ -5484,9 +5511,10 @@ and final release SHA require a new qualification pair. remainder becomes fee, no replacement anchor is made, the next spend is rejected, and a matching RVN top-up restores it. The full focused test passed locally on 2026-10-12. The manual recovery procedure is documented - by `193d26483`; exact-SHA CI remains pending. -- **Final status:** DOCUMENTED LIMITATION for 4.8.1, fail-safe locally; - exact-SHA CI pending. + by `193d26483`. Both required security jobs passed on `9151946ad`, + including the dust and recovery vectors. +- **Final status:** DOCUMENTED LIMITATION for 4.8.1, fail-safe on the + qualified candidate SHA; final release-SHA qualification remains due. ### FINDING-142: Pinned native ccache source is unavailable to exact-SHA CI @@ -7742,10 +7770,12 @@ Cross-platform CI and public deployment remain separate release gates. with all five release targets, including the macOS artifact named `raven-unsigned-osx-870a0bee36f4ec4b4c52f1c0e238df83834025e2`. Paired Final Gate run `37928133299` also succeeded at that exact SHA. - The redesigned PQ-only asset rule and final release SHA still require - their own artifact qualification. -- **Final status:** FIXED for the dual-signature candidate; final release-SHA - artifact verification pending. + Build Raven `38296258242` subsequently built the macOS artifact + `11771367693` on the PQ-only candidate SHA `9151946ad`; paired Final + Gate `38296262444` succeeded at that exact SHA. The eventual 4.8.1 + release SHA still requires artifact qualification. +- **Final status:** FIXED on the PQ-only candidate SHA `9151946ad`; + final release-SHA artifact verification remains due. ## Findings frozen after the merged asset-phase CI review @@ -7821,8 +7851,9 @@ Cross-platform CI and public deployment remain separate release gates. consume the sole matching anchor. The asset balance remains in the wallet, a subsequent protected spend fails, and a matching PQ top-up restores it. The focused functional test passed locally. -- **Final status:** DOCUMENTED LIMITATION for 4.8.1, fail-safe locally; - exact-SHA CI pending. +- **Final status:** DOCUMENTED LIMITATION for 4.8.1. The anchor-consumption + and matching top-up vector passed in both required security jobs at + `9151946ad`; final release-SHA qualification remains due. ### FINDING-151: Raw transaction funding does not select asset PQ anchors @@ -7855,8 +7886,9 @@ Cross-platform CI and public deployment remain separate release gates. matching anchor and consensus rejects the signed transaction. Supplying the anchor and an RVN return output to its PQ address succeeds, and a later protected spend remains possible. The focused test passed locally. -- **Final status:** DOCUMENTED LIMITATION for 4.8.1, fail-safe locally; - exact-SHA CI pending. +- **Final status:** DOCUMENTED LIMITATION for 4.8.1. The raw-funding + rejection and explicit-anchor recovery vector passed in both required + security jobs at `9151946ad`; final release-SHA qualification remains due. ## Findings frozen by the independent merged asset-phase review @@ -7888,8 +7920,9 @@ Cross-platform CI and public deployment remain separate release gates. change for all three RPCs with the canonical-destination error. After the change, `feature_pq_asset_wallet_ownership.py` passes locally and checks preactivation classical behavior and postactivation owned PQ pairs. - Exact-SHA CI remains due. -- **Final status:** FIXED locally; exact-SHA CI pending. + Both required security jobs passed this functional test at `9151946ad`. +- **Final status:** FIXED on the qualified candidate SHA; final release-SHA + qualification remains due. ### FINDING-153: Asset index RPC aggregates distinct PQ programs by classical address @@ -7923,7 +7956,8 @@ Cross-platform CI and public deployment remain separate release gates. - **Verification:** `588ef7ec8` exercises the superseded dual-signature format and its fail-safe recipient check. The current source RPC functional vector asserts distinct synthetic identifiers for two PQ programs and - rejects an identifier/program mismatch. Exact-SHA CI remains pending. + rejects an identifier/program mismatch. Both required security jobs + passed the current source RPC test at `9151946ad`. - **Final status:** SCOPE SUPERSEDED for new PQ-only outputs. Historical classical index rows remain descriptive data, not proof of PQ custody. @@ -7943,8 +7977,8 @@ Cross-platform CI and public deployment remain separate release gates. - **Concrete scenario:** In the earlier dual-signature candidate, two tagged outputs could share a classical key and Copy address yielded a classical-only string. The PQ-only candidate instead uses distinct - program-derived identifiers; its list, tree and clipboard still need - interactive qualification. + program-derived identifiers; the list, tree and clipboard required + interactive qualification on an unmodified release artifact. - **Expected behavior:** Show the full protected descriptor per active output and copy a valid descriptor, without relabeling historical outputs. - **Regression required:** GUI exercise with two distinct PQ programs and @@ -7965,8 +7999,19 @@ Cross-platform CI and public deployment remain separate release gates. with the private audit evidence. The local GCC 13 executable required a process-local workaround for a Qt 5.12.11 `readlink` fortify crash; the workaround is not part of the repository or release artifact. -- **Final status:** OPEN. The interactive source build passed locally, - but the unmodified release artifact and exact-SHA CI remain pending. + Build Raven `38296258242` produced Linux artifact `11770728793` at + `9151946ad`, alongside a green Final Gate `38296262444`. The archive's + published SHA-256 file and full-SHA provenance verified locally. Its + unmodified `raven-qt` opened a copied regtest wallet under Xvfb without + the workaround. A private fixture issued two units of `PQGUIQA915` to + one PQ-only descriptor, then transferred one unit to a second descriptor. + Asset Control list mode displayed two separate rows; tree mode displayed + two separate groups. Copy address on both rows and on the second tree + group returned the complete corresponding descriptor byte-for-byte. + Screenshots, the fixture, and exact clipboard comparisons are retained + outside the repository with the private audit evidence. +- **Final status:** FIXED on the qualified PQ-only candidate SHA; + final release-SHA artifact verification remains due. ### FINDING-155: Partial migration of a legacy asset UTXO needs explicit PQ change @@ -7995,7 +8040,8 @@ Cross-platform CI and public deployment remain separate release gates. - **Verification:** `588ef7ec8` proves that a partial legacy spend without protected change fails, while whole-UTXO asset and owner-token migration and explicit PQ change succeed. A later protected spend needs and accepts - a funded matching anchor. The focused source RPC test passed locally. + a funded matching anchor. The focused source RPC test passed locally and + in both required security jobs at `9151946ad`. - **Final status:** OPEN; documented fail-safe GUI migration limitation. ### FINDING-156: macOS release runner disconnects during the artifact build @@ -8033,7 +8079,9 @@ Cross-platform CI and public deployment remain separate release gates. Paired Final Gate run `37888456288` succeeded at the same SHA. A newer exact-SHA pair, Build Raven `37928136449` and Final Gate `37928133299`, also succeeded at `870a0bee36f4ec4b4c52f1c0e238df83834025e2`, with - five artifacts. These are candidate builds, not the final release SHA. + five artifacts. Build Raven `38296258242` also produced all five on + `9151946ad` with paired green Final Gate `38296262444`. These are + candidate builds, not the final release SHA. - **Final status:** RESOLVED as a transient CI runner loss at the tested SHAs; final release-SHA artifact qualification remains pending. @@ -8131,10 +8179,13 @@ Cross-platform CI and public deployment remain separate release gates. `pq_asset_creation_height_and_anchor_rule_test` covers new-output and anchored-input rejection. The asset anchor, source RPC, wallet ownership and authority functional tests pass at `6ed46c91c`; local `make check` - passes. Old-node block acceptance, exact-SHA CI and FINDING-160/161 - release conditions remain unverified. -- **Final status:** FIXED locally; release closure awaits exact-SHA CI and - consensus compatibility and activation evidence. + passes. Both required security jobs passed the PQ-only unit and functional + vectors at `9151946ad`. Public activation remains blocked by the + incompatible native block rules in FINDING-160; old-node asset-format + behavior and final release-SHA qualification remain to be checked. +- **Final status:** FIXED for the PQ-only implementation on the qualified + candidate SHA; public activation and release remain blocked by + FINDING-160 and the remaining compatibility evidence. ### FINDING-159: Proposed PQ-only asset input permits scriptSig malleability @@ -8171,8 +8222,10 @@ Cross-platform CI and public deployment remain separate release gates. src/test/assets/asset_tx_tests.cpp:995-1003 failed when the new check was temporarily removed: the malleable spend was accepted and had no reject reason. Restoring the check made the same vector pass. The full local - `make check` also passed at `6ed46c91c`. -- **Final status:** FIXED locally; exact-SHA CI pending. + `make check` also passed at `6ed46c91c`. Both required security jobs + passed at `9151946ad`. +- **Final status:** FIXED on the qualified candidate SHA; final release-SHA + qualification remains due. ### FINDING-160: Native RIP-25 block limits can split Core 4.8.0 nodes @@ -8274,8 +8327,9 @@ Cross-platform CI and public deployment remain separate release gates. cannot be spent again without an anchor. Unit vectors cover the pre-activation spend rule. Local `make check` passes. Historical directly mined lookalikes remain valid and unprotected by design; - operators must not present them as PQ custody. Exact-SHA CI pending. -- **Final status:** FIXED locally; exact-SHA CI pending, with the + operators must not present them as PQ custody. Both required security + jobs passed at `9151946ad`. +- **Final status:** FIXED on the qualified candidate SHA, with the historical-block limitation documented. ### FINDING-162: PQ-only owner and message assets miss wallet subscriptions @@ -8312,8 +8366,10 @@ Cross-platform CI and public deployment remain separate release gates. - **Verification:** The owner-token subscription assertion in `feature_pq_asset_wallet_ownership.py` failed before the contextual lookup and passed afterward. The full functional test and local - `make check` pass at `6ed46c91c`. -- **Final status:** FIXED locally; exact-SHA CI pending. + `make check` pass at `6ed46c91c`. Both required security jobs passed + the wallet ownership functional test at `9151946ad`. +- **Final status:** FIXED on the qualified candidate SHA; final release-SHA + qualification remains due. ### FINDING-163: Synthetic PQ asset identifier leaks as a native RVN address @@ -8364,8 +8420,10 @@ Cross-platform CI and public deployment remain separate release gates. before the filter and passed afterward. The source RPC test also checks `listreceivedbyaddress` and `listaddressgroupings`. The wallet unit vector checks recovered purpose and canonical ID classification; - both tests pass at `6ed46c91c`. -- **Final status:** FIXED locally; exact-SHA CI pending. + both tests pass at `6ed46c91c`. Both required security jobs passed the + source RPC test and wallet unit vectors at `9151946ad`. +- **Final status:** FIXED on the qualified candidate SHA; final release-SHA + qualification remains due. ### FINDING-164: PQ-only restricted-asset status misses wallet ownership @@ -8402,8 +8460,10 @@ Cross-platform CI and public deployment remain separate release gates. private-DB change because the tagged identifier was absent from `viewmytaggedaddresses`. It passes afterward for tag, untag, freeze, unfreeze, restart and an unpaired PQ program excluded from private - history. The full local `make check` passes at `6ed46c91c`. -- **Final status:** FIXED locally; exact-SHA CI pending. + history. The full local `make check` passes at `6ed46c91c`. Both + required security jobs passed at `9151946ad`. +- **Final status:** FIXED on the qualified candidate SHA; final release-SHA + qualification remains due. ### FINDING-165: PQ asset functional tests require optional Python RIPEMD160 @@ -8437,8 +8497,10 @@ Cross-platform CI and public deployment remain separate release gates. dependency from both functional tests. - **Verification:** Red CI run `38047870174`; both functional tests pass locally with `hashlib.new('ripemd160', ...)` forced to raise, and local - `make check` passes. Exact-SHA CI rerun pending. -- **Final status:** FIXED locally; exact-SHA CI pending. + `make check` passes. Both required security jobs ran the two functional + tests successfully at `9151946ad` on the pinned runner. +- **Final status:** FIXED on the qualified candidate SHA; final release-SHA + qualification remains due. ### FINDING-166: Build Raven hides the failing unit test diagnosis @@ -8470,10 +8532,12 @@ Cross-platform CI and public deployment remain separate release gates. nonzero `make check` result and keeps the artifact gate mandatory. - **Verification:** Red CI run `38047871989`; a local simulated failure showed the original step hid a test assertion, while the revised step - prints it and still fails. Local `make check` passes. Diagnosis of the - original unit failure and exact-SHA CI rerun remain pending. -- **Final status:** OPEN for the underlying CI failure; diagnostic repair - is FIXED locally. + prints it and still fails. Local `make check` passes. Build Raven + `38296258242` and Final Gate `38296262444` passed their mandatory + `make check` at `9151946ad`. The original `38047871989` unit failure + remains unclassified because its missing log cannot be reconstructed. +- **Final status:** OPEN for the original unexplained unit failure. The + diagnostic workflow repair is CI-qualified at `9151946ad`. ### FINDING-167: Pinned FreeType source host blocks the ARM64 artifact @@ -8507,8 +8571,11 @@ Cross-platform CI and public deployment remain separate release gates. digest `3a3bb2c4e15ffb433f2032f50a5b5a92558206822e22bfe8cbe339af4aa82f88` in independent local fetches. A clean ARM64 depends fetch through the revised recipe passed and checked the same digest. The red CI run is - `38234048442`; a green exact-SHA artifact matrix remains pending. -- **Final status:** FIXED locally; exact-SHA CI pending. + `38234048442`. Build Raven `38296258242` built the aarch64 artifact + `11771315730` and the other four release targets at `9151946ad`; + paired Final Gate `38296262444` succeeded at that SHA. +- **Final status:** FIXED on the qualified candidate SHA; final release-SHA + qualification remains due. ### FINDING-168: Wallet rewrite recovery can leave PQ plaintext in the database file @@ -8561,6 +8628,10 @@ Cross-platform CI and public deployment remain separate release gates. the patch, local `make check`, the full RIP-25 behavioral invariant gate with Python 3.10, and all nine required functional tests passed. These checks cover the active wallet file and backups, not forensic erasure of - old filesystem blocks or Berkeley DB transaction logs. -- **Final status:** FIXED locally; exact-SHA CI and artifacts pending, so - FINDING-067 remains OPEN. + old filesystem blocks or Berkeley DB transaction logs. Build Raven + `38296258242` passed the new full-page-clear unit regression and the + restart-recovery physical-secret scan at `9151946ad`, then produced all + five artifacts. Paired Final Gate `38296262444` passed the same tests + and SHA. +- **Final status:** FIXED on the qualified candidate SHA; final release-SHA + qualification remains due, and FINDING-067 remains OPEN. From c74dc42fee2f84ff95492568dbd9c94ca81f17a6 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:54:40 +0200 Subject: [PATCH 057/195] [FINDING-158] Verify PQ-only asset format with Core 4.8.0 --- ...0025-v4.8-security-remediation-register.md | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index a3171731d8..74e66c65da 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4418,7 +4418,9 @@ showed separate rows and tree groups; Copy address returned each complete descriptor. This qualifies FINDING-089, 143-147, 152, 154, 159, 161-165, 167, and 168 on this candidate SHA. FINDING-148, 150, and 151 remain documented fail-safe limitations with passing recovery regressions. -FINDING-158's implementation passed CI, but public activation remains +FINDING-158's implementation passed CI. A separate two-binary regtest +confirmed that Core 4.8.0 accepts ordinary-sized candidate blocks with +PQ-only asset outputs and later anchored spends. Public activation remains blocked by FINDING-160 and the release steps below. The old unexplained `make check` failure in FINDING-166 remains separately open. This is an intermediate 4.8.0-versioned SHA, not the final 4.8.1 release SHA: @@ -8180,12 +8182,21 @@ Cross-platform CI and public deployment remain separate release gates. anchored-input rejection. The asset anchor, source RPC, wallet ownership and authority functional tests pass at `6ed46c91c`; local `make check` passes. Both required security jobs passed the PQ-only unit and functional - vectors at `9151946ad`. Public activation remains blocked by the - incompatible native block rules in FINDING-160; old-node asset-format - behavior and final release-SHA qualification remain to be checked. + vectors at `9151946ad`. In a two-binary regtest, Core 4.8.0 + `b60f50e04` synchronized the same 932-block chain as the candidate + artifact `9151946ad` while bit 13 was active on the candidate. Blocks + 864 through 932 contained 66 PQ-only asset outputs and 35 later spends. + The old node accepted both the example output in block 864 and its + scriptSig-empty anchored spend in block 865; both nodes reached the + same tip `3ec9956751d3d8e43fb3995bb96df5929a678155f03c0124d216141d8c65cebe`. + The ordinary-sized example spend block weighed 5,602 WU on both nodes. + This establishes old-node acceptance of the asset format, not old-node + enforcement of ML-DSA. Public activation remains blocked by the + incompatible native block rules in FINDING-160 and final release-SHA + qualification. - **Final status:** FIXED for the PQ-only implementation on the qualified candidate SHA; public activation and release remain blocked by - FINDING-160 and the remaining compatibility evidence. + FINDING-160 and the remaining deployment qualification. ### FINDING-159: Proposed PQ-only asset input permits scriptSig malleability From 1dfae5b21a3342e773d8eeed5c8c6e0262b93e0e Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:59:46 +0200 Subject: [PATCH 058/195] [FINDING-067] Set approved bit 13 public activation windows --- src/chainparams.cpp | 12 ++++++------ src/test/rip25_versionbits_tests.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index a509579bfa..e52a091410 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -188,10 +188,10 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nTimeout = 1830297600; // placeholder consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nOverrideRuleChangeActivationThreshold = 1714; // Approx 85% of 2016 consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nOverrideMinerConfirmationWindow = 2016; - // Keep the asset phase dormant until its consensus and wallet paths are qualified. + // RIP-25: PQ-only asset deployment consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].bit = 13; - consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nStartTime = 999999999999LL; - consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nTimeout = 999999999999LL; + consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nStartTime = 1861920000; // 2029-01-01 00:00:00 UTC + consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nTimeout = 1909094400; // 2030-07-01 00:00:00 UTC consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nOverrideRuleChangeActivationThreshold = 1714; consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nOverrideMinerConfirmationWindow = 2016; consensus.nPQHybridEnabled = false; @@ -373,15 +373,15 @@ class CTestNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nOverrideRuleChangeActivationThreshold = 1411; // Approx 70% of 2016 consensus.vDeployments[Consensus::DEPLOYMENT_TRANSFER_OVERFLOW].nOverrideMinerConfirmationWindow = 2016; - // RIP-25: Post-Quantum Hybrid Signatures — testnet activates immediately for testing + // RIP-25: Post-Quantum Hybrid Signatures; testnet activates immediately for testing consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].bit = 12; consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nStartTime = 1199145601; consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nTimeout = 1893456000; consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nOverrideRuleChangeActivationThreshold = 1310; consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nOverrideMinerConfirmationWindow = 2016; consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].bit = 13; - consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nStartTime = 999999999999LL; - consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nTimeout = 999999999999LL; + consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nStartTime = 1806537600; // 2027-04-01 00:00:00 UTC + consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nTimeout = 1853971200; // 2028-10-01 00:00:00 UTC consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nOverrideRuleChangeActivationThreshold = 1310; consensus.vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS].nOverrideMinerConfirmationWindow = 2016; consensus.nPQHybridEnabled = true; diff --git a/src/test/rip25_versionbits_tests.cpp b/src/test/rip25_versionbits_tests.cpp index d4719dab36..45d6cc64ae 100644 --- a/src/test/rip25_versionbits_tests.cpp +++ b/src/test/rip25_versionbits_tests.cpp @@ -86,6 +86,31 @@ Consensus::Params MakeRIP25VersionBitsParams() BOOST_AUTO_TEST_SUITE(rip25_versionbits_tests) +BOOST_AUTO_TEST_CASE(pq_asset_public_deployment_parameters) +{ + const auto main = CreateChainParams(CBaseChainParams::MAIN); + const auto testnet = CreateChainParams(CBaseChainParams::TESTNET); + const auto regtest = CreateChainParams(CBaseChainParams::REGTEST); + const auto& mainAssets = main->GetConsensus().vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS]; + const auto& testAssets = testnet->GetConsensus().vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS]; + const auto& regAssets = regtest->GetConsensus().vDeployments[Consensus::DEPLOYMENT_PQ_ASSETS]; + + BOOST_CHECK_EQUAL(mainAssets.bit, 13); + BOOST_CHECK_EQUAL(mainAssets.nStartTime, 1861920000LL); + BOOST_CHECK_EQUAL(mainAssets.nTimeout, 1909094400LL); + BOOST_CHECK_EQUAL(mainAssets.nOverrideRuleChangeActivationThreshold, 1714); + BOOST_CHECK_EQUAL(mainAssets.nOverrideMinerConfirmationWindow, 2016); + + BOOST_CHECK_EQUAL(testAssets.bit, 13); + BOOST_CHECK_EQUAL(testAssets.nStartTime, 1806537600LL); + BOOST_CHECK_EQUAL(testAssets.nTimeout, 1853971200LL); + BOOST_CHECK_EQUAL(testAssets.nOverrideRuleChangeActivationThreshold, 1310); + BOOST_CHECK_EQUAL(testAssets.nOverrideMinerConfirmationWindow, 2016); + + BOOST_CHECK_EQUAL(regAssets.nStartTime, 999999999999LL); + BOOST_CHECK_EQUAL(regAssets.nTimeout, 999999999999LL); +} + BOOST_AUTO_TEST_CASE(pq_bit12_activates_independently_from_v48_overflow_bit11) { Consensus::Params params = MakeRIP25VersionBitsParams(); From dce0649b81811cbd9002bbf96ca75298c0d9690a Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:07:43 +0200 Subject: [PATCH 059/195] [FINDING-067] Align asset deployment notes with PQ-only format --- doc/RIP-0025-PQ-Assets-Followup.md | 233 ++++++++++++++++------------- doc/RIP-0025-PQ-Signatures.md | 18 ++- 2 files changed, 136 insertions(+), 115 deletions(-) diff --git a/doc/RIP-0025-PQ-Assets-Followup.md b/doc/RIP-0025-PQ-Assets-Followup.md index 8972fa9f6c..a05a8ba39f 100644 --- a/doc/RIP-0025-PQ-Assets-Followup.md +++ b/doc/RIP-0025-PQ-Assets-Followup.md @@ -1,22 +1,30 @@ # RIP-25 PQ Asset Extension Design Note -Status: dormant bit 13 consensus candidate for the 4.8.1 working branch. -Default public-network parameters do not activate it. Wallet construction -and recovery paths have local tests; public activation and release -qualification remain open. +Status: bit 13 PQ-only asset candidate with approved mainnet and testnet +BIP9 windows in the 4.8.1 working branch. Bit 13 is not active on either +public network. Public activation and release qualification remain blocked +by the native bit-12 deployment plan, testnet history review, and final +same-SHA release evidence. ## 1. Current Scope -On the default public-network parameters, RIP-25 witness-v2 protects native RVN outputs only. No current Ravencoin asset class is yet qualified for issuance, custody, transfer, reissue, tagging, freezing, or spending under an ML-DSA-44 ownership condition. The 4.8.1 candidate assigns a separate, dormant bit 13 for a dependent asset rule and exercises it on regtest. That candidate is not a deployed public-network protection or a release-qualified feature. +Until bit 13 activates, no Ravencoin asset class gains ML-DSA-44 ownership +from this candidate. The 4.8.1 working branch schedules a separate, +dependent bit-13 rule for new asset and owner-token outputs. The PQ-only +implementation passed a same-SHA CI pair on `9151946ad`, but it is not a +deployed public-network protection or a release-qualified feature. -The current spendable asset envelope is: +The historical spendable asset envelope is: ``` OP_DUP OP_HASH160 <20-byte-key-id> OP_EQUALVERIFY OP_CHECKSIG OP_RVN_ASSET OP_DROP ``` -`CScript::IsAssetScript` recognizes `OP_RVN_ASSET` only at byte offset 25. `Solver` then extracts the 20-byte key identifier from the P2PKH prefix. Wallet ownership and signing consequently resolve the output through a classical secp256k1 `CKeyID`. +`CScript::IsAssetScript` recognizes `OP_RVN_ASSET` only at byte offset 25. +Historical outputs use the P2PKH prefix and a secp256k1 `CKeyID`. +The candidate PQ-only prefix in Section 20 preserves that offset while +replacing classical authorization with a program-derived asset identifier. The limitation applies to all current asset classes: @@ -38,7 +46,7 @@ A cryptographically relevant quantum computer that recovers a secp256k1 private Wallet and RPC messages must not imply that a native-RVN PQ address alone protects asset owner or administrator tokens. The asset builder rejects that bare address and, at active bit 13, requires a canonical -classical|PQ asset descriptor. +`assetID|PQ` descriptor. The asset identifier has no classical private key. ## 3. Why the Proposed Concatenation Is Unsafe @@ -114,21 +122,20 @@ No asset extension should be coupled silently to an already deployed RIP-25 bit. The wallet uses a distinct composite PQ asset destination rather than reusing a native-RVN address without a defined asset meaning. -The working branch defines a wallet-facing, network-bound descriptor of -the form `|`. Its decoder -requires exactly one separator, the expected destination types, and exact -canonical re-encoding on the selected network. The descriptor is not a -consensus script and not a native RVN payment address. At active bit 13, -the wallet uses it for root, subasset, unique, restricted, qualifier, -reissue, transfer, and administrative outputs. The raw asset constructor -accepts it for corresponding output forms. `getnewpqassetaddress` generates -both keys only after the dependent rule is active, persists the pairing, -and `listpqassetaddresses` retrieves owned pairs after reload and validated -key-only salvage. A single Bech32m address cannot be used -for the two independent keys without defining a new encoding and changing -the existing address-length assumptions. The descriptor is split before -restricted-asset verifier or qualifier-index lookups: those indexes remain -keyed by the classical address. The wallet selects a matching funded native +The working branch defines a wallet-facing, network-bound `assetID|PQ` +descriptor. `assetID` encodes `HASH160(P)` as a legacy-sized address field; +`P` is the native witness-v2 program. Its decoder requires exactly one +separator, the expected destination types, the program-derived identifier, +and canonical re-encoding on the selected network. The identifier has no +classical private key and is not a native RVN payment address. At active +bit 13, the wallet uses the descriptor for root, subasset, unique, +restricted, qualifier, reissue, transfer, and administrative outputs. The +raw asset constructor accepts it for corresponding forms. +`getnewpqassetaddress` generates an ML-DSA key only after the dependent +rule is active, persists the descriptor, and `listpqassetaddresses` +retrieves owned descriptors after reload and validated key-only salvage. +Restricted-asset verifier and qualifier indexes use the program-derived +asset identifier. The wallet selects a matching funded native PQ anchor and returns protected asset change or owner authority. For one consumed program, ordinary positive native change can refresh the anchor at the same PQ address. Exact-fee, dust, multiple-program, or explicit native @@ -156,7 +163,12 @@ All high-level and raw-transaction RPCs must construct only canonical activated ## 8. Asset Index and Database Compatibility -Asset indexes currently derive an address identity from legacy 20-byte data. A PQ program is 32 bytes. The current candidate still indexes protected outputs under their classical address. `listaddressesbyasset` and `listassetbalancesbyaddress` therefore aggregate distinct PQ programs that share one classical key. Their classical address value is accounting data, not a complete protected destination. The follow-up design must version index keys or otherwise prevent truncation and type confusion. +Asset indexes use 20-byte identifiers. In the PQ-only candidate that +identifier is `HASH160(P)`, rather than an independently selected +classical key identifier. A bare identifier is accounting data, not the complete PQ +destination: custody and spending still require the committed 32-byte +program and a matching funded anchor. The historical two-program collision +described in FINDING-153 belonged to the superseded dual-signature form. It must specify: @@ -212,20 +224,23 @@ The follow-up implementation should not be proposed for activation without: ## 12. Current Decision -The approved native-RVN RIP-25 rules remain unchanged. The 4.8.1 working -branch includes a dependent, dormant bit 13 asset-consensus candidate. Wallet -asset builders still reject a bare witness-v2 destination, since a native PQ -address alone is not a hybrid asset destination. The candidate has passed -local unit, functional, invariant, and Qt compilation checks but is not -qualified for public-network activation or a 4.8.1 asset-PQ release claim. -Cross-platform exact-SHA CI, release artifacts, a second independent audit, -and an explicit protocol-owner activation decision remain required. +The native-RVN bit-12 rules retain their 8 to 12 to 16 MWU plan. The +4.8.1 working branch implements a dependent PQ-only bit-13 asset rule and +sets the approved public BIP9 windows. Wallet asset builders still reject +a bare native witness-v2 destination: an asset requires its canonical +`assetID|PQ` descriptor. Unit, functional, wallet-recovery, and pinned-Qt +checks passed with all five Build Raven artifacts and a green Final Gate +on candidate SHA `9151946ad`. A second independent audit froze and tracked +its findings. Release qualification still needs the bit-12 network upgrade +plan, historical testnet review, remaining finding closure, and final-SHA +artifacts. The dates in code do not authorize publishing or mining under +the new rule. -## 13. Experimental Compatibility Direction +## 13. Historical Dual-Signature Compatibility Research -The following records the compatibility research behind the dormant bit 13 -candidate. It must not be described as protecting assets on the default -public-network parameters. +This section records earlier compatibility research. Its P2PKH plus PQ +tail was superseded by the PQ-only format in Section 20. It is not the +current ownership rule or a public-network protection claim. Directly appending a witness-v2 program or witness to a legacy P2PKH asset output does not work as a compatible soft fork. The complete script is not a @@ -261,8 +276,8 @@ compatibility, not evidence that the proposed new consensus rule is safe. A canonical helper recognizes the narrow envelope and rejects short/long tails, nonminimal pushes, malformed payloads, and transfer messages -without an explicit expiry field. The dormant bit 13 candidate uses this -helper in consensus only after its effective activation height. An old-valid +without an explicit expiry field. The earlier dual-signature candidate used +this helper only after its effective activation height. An old-valid script ending in `OP_DROP` plus 31 arbitrary bytes is byte-identical to a candidate with a 32-byte program beginning `OP_DROP`. The helper necessarily classifies it as a candidate. The spend rule therefore @@ -270,25 +285,20 @@ checks the creating UTXO's height against the asset extension's activation height. Applying the rule only at spending height would retroactively lock historical coins and could split nodes after reorg or reindex. -The proposed new rule would not put an unexpected witness on the asset input. +The earlier proposal would not put an unexpected witness on the asset input. Instead, the spending transaction would also consume a native RIP-25 witness-v2 UTXO whose program equals the program committed in the asset output. Its fixed `SIGHASH_ALL` ML-DSA signature commits to every asset input and output in the same transaction. A quantum attacker with only the P2PKH private key could not create the required PQ input. -This construction remains a candidate rather than a release-qualified -specification. The 4.8.1 working branch assigns BIP9 bit 13 but leaves its -start and timeout equal and far in the future on every network, so default -nodes do not activate it. Its tested consensus prototype uses the canonical -parser and creation-height gate described below. Before public-network -activation it still needs a documented, fail-safe anchor funding policy, cross-platform -exact-SHA qualification, independent review, and an explicit migration and -activation decision. The wallet now has tested protected owner returns, -asset change, restricted-address checks, and representative all-class -vectors. Bit 13 timing must not be enabled without protocol-owner review. +This dual-signature construction has been superseded. Section 20 specifies +the current PQ-only output: its asset input has an empty scriptSig and +does not require a classical signature. The public bit-13 windows are +scheduled in code, but network activation and release remain gated as +described in Section 16 and the security remediation register. -## 14. Contextual Enforcement Map (Dormant Candidate) +## 14. Contextual Enforcement Map (PQ-Only Candidate) The candidate's UTXO-aware hook runs in `ConnectBlock` before `UpdateCoins`. `Consensus::CheckTxPQAssets` requires canonical tagged asset outputs at the @@ -305,17 +315,20 @@ Effective asset enforcement requires bit 13, the historical bit 8 transfer parser, and native RIP-25 bit 12 all to be active. Its first enforcement height is the maximum of their activation heights. This prevents a node validating a side branch from borrowing the global active tip's state. -The parser's historical `OP_DROP` lookalike requires a creation-height check -against the asset extension activation height, not merely a spend-height -check. Reorgs across activation revalidate affected mempool transactions +The PQ-only prefix remains historically valid before activation, so the +input rule uses its creating height rather than only its spending height. +The wallet does not label an early lookalike as PQ custody, and mempool +policy rejects its premature creation. Reorgs across activation revalidate +affected mempool transactions through staged removal so asset reissue reservations and compact-block indexes stay consistent. Reusing the existing RIP-25 bit 12 would be unsafe because it would make the newly upgraded nodes enforce a rule older RIP-25 nodes do not. `CheckTxPQAssets` runs outside the script cache. When it finds an asset input -created after activation, `ConnectBlock` forces both the classical and PQ -script checks even under assumevalid. A native witness-v2 input is verified +created after activation, `ConnectBlock` requires the matching PQ anchor +and an empty asset scriptSig even under assumevalid; ECDSA is not required. +A native witness-v2 input is verified under the contextual RIP-25 flag, not by treating the asset envelope itself as a witness program. `TestBlockValidity` is the miner's final template check, but template selection and package policy still need boundary tests. @@ -326,29 +339,29 @@ program requires a different anchor input. The wallet selects a funded matching anchor and signs it. A newly received asset output does not itself fund its matching native anchor: the recipient must send RVN to the descriptor's PQ address. A single-program spend can refresh that anchor -from ordinary native change, but the edge cases listed in Section 7 still -require manual top-up. Bit 13 is assigned but dormant. +from ordinary native change, but the edge cases listed in Section 17 still +require manual top-up. Bit 13 has future public windows but is not active. ## 15. 4.8.1 Integration Candidate and Release Gate The 4.8.1 integration request expands RIP-25 to assets. The native-RVN -implementation and its bit 12 deployment remain unchanged. This section -records the dormant bit 13 consensus candidate, not a public-network rule -already in force or a release-qualified feature. +8 to 12 to 16 MWU plan remains a separate bit-12 deployment concern. This +section records the scheduled bit-13 PQ-only candidate, not a rule already +in force or a release-qualified feature. Let `H` be the first candidate-block height at which bit 13, bit 8, and -native RIP-25 bit 12 are all active. A safe soft-fork direction requires both: +native RIP-25 bit 12 are all active. The asset rule requires both: -1. Every spendable asset output created at height `h >= H`, including an owner - token, has one canonical 32-byte PQ program in the validated legacy-compatible - envelope. Null asset metadata outputs remain governed by their existing - rules. An asset output without the canonical program is invalid at `h >= H`. +1. Every spendable asset output created at height `h >= H`, including an + owner token, has the canonical 25-byte PQ-only prefix and one bound + 32-byte program. Null asset metadata outputs retain their existing + rules. An asset output without this form is invalid at `h >= H`. 2. A transaction spending an asset UTXO created at `h >= H` must also spend a native RIP-25 witness-v2 UTXO whose 32-byte program equals that asset output's committed program. One valid anchor input may authorize several tagged asset inputs only when their programs match. The native witness-v2 fixed `SIGHASH_ALL` signature then binds every input and output of the - transaction. Classical asset-script authorization remains required too. + transaction. The asset input has an empty scriptSig and needs no ECDSA. The candidate block's previous index, not the global active tip, must select the BIP9 state. Mempool admission uses the current tip and treats an unconfirmed @@ -357,11 +370,11 @@ assumevalid shortcuts. Native witness-v2 signature verification must still run under assumevalid when the asset anchor is needed. A transaction may not remove the PQ condition by sending its asset to a legacy-only output after activation. -The creation-height rule is essential. A pre-activation script ending in -`OP_DROP` plus 31 bytes can be byte-identical to a tagged candidate. Requiring -an anchor from such a historical coin would retroactively make old-valid -spends invalid. Conversely, a pre-activation tagged-looking output cannot be -advertised as PQ-protected merely because its spend happens after activation. +The creation-height rule is essential. A pre-activation PQ-only lookalike +was valid under historical script rules and may be spent without an anchor. +Requiring one retroactively would lock an old-valid coin. The wallet must +not advertise that historical output as PQ custody. Mempool policy rejects +new lookalikes before activation and removes them after a boundary reorg. This construction still does not retroactively secure existing owner tokens. Their classical key holder must authorize a migration into a newly tagged @@ -369,22 +382,24 @@ output before that classical key is compromised. A quantum attacker who obtains the old key can race that migration. Software cannot distinguish the rightful holder from the attacker using the old signature alone. -The following remain release-blocking before assigning public-network -activation times or publishing a 4.8.1 artifact as asset-PQ-qualified: -independent protocol review of the hybrid address and anchor construction, -the manual-funding and refresh policy, exact-SHA cross-platform CI and -artifacts, miner and mempool equivalence, BIP9 reorg evidence, and an -operator-visible migration plan for existing owner and administrative -holdings. The local tests cover ownership, selection, output classes, -restricted indexes, same-block and mempool-parent heights, and reorgs, but -do not replace independent deployment review. Reusing bit 12 would change -the meaning of an already active deployment on testnet/regtest and is -excluded from this candidate. +Core 4.8.0 accepted a 932-block regtest chain containing 66 PQ-only asset +outputs and 35 later spends from the candidate, including an asset spend +with empty scriptSig. This checks the asset format at ordinary block sizes; +Core 4.8.0 does not enforce ML-DSA. FINDING-160 separately proves a split +at the native 8 MWU boundary and requires a coordinated incompatible-rule +upgrade plan. Testnet history, the bit-12 mainnet date, the old unexplained +CI failure, final-SHA artifacts, and operator migration guidance remain +release gates. Reusing bit 12 for assets would change the meaning of its +earlier deployment and is excluded. ## 16. Proposed Operator Activation Sequence -This is a sequence for review, not a scheduled public-network activation. -The default public-network bit-13 start and timeout values remain dormant. +This is the operator sequence for the approved bit-13 windows, not an +authorization to publish the release or activate a public chain. Testnet +starts on 2027-04-01 and times out on 2028-10-01 at 1310/2016; +mainnet starts on 2029-01-01 and times out on 2030-07-01 at 1714/2016. +FINDING-160, testnet-history review, and final release qualification +remain gates before public deployment. 1. Deploy and review the native RIP-25 bit-12 implementation. Its BIP9 transition enables witness-v2 ML-DSA-44 script validation, the 12 MWU @@ -397,26 +412,28 @@ The default public-network bit-13 start and timeout values remain dormant. height `H` is the maximum of the three activation heights. Tests must cover the `H-2` through `H+2` boundary, restart, invalidate/reconsider, and reorgs across BIP9 `LOCKED_IN` and `ACTIVE`. -3. Before assigning public bit-13 parameters, obtain an independent - consensus and wallet review, reproducible cross-platform builds, and - a migration communication plan. Updating software without a network - deployment does not protect assets. +3. Before public deployment, verify the full shared testnet history and + bit-12 prerequisite, finish the incompatible block-rule upgrade plan, + obtain same-SHA final release artifacts, and communicate migration. + Updating software without a network deployment does not protect assets. 4. At or after `H`, newly created spendable asset outputs of every class, including `ASSET!` owner authority, must carry the canonical PQ program. - A spend of such an output requires its classical signature and a native - witness-v2 input with the matching program and valid network-context - ML-DSA-44 signature. The asset itself is not a witness program. + A spend of such an output has an empty asset scriptSig and requires a + native witness-v2 input with the matching program and a valid + network-context ML-DSA-44 signature. No ECDSA key authorizes the new + output. The asset itself is not a witness program. 5. Holders of older asset and owner-token outputs must explicitly transfer them into tagged outputs with their existing classical authorization. - They should obtain a canonical classical|PQ descriptor and fund its PQ + They should obtain a canonical `assetID|PQ` descriptor and fund its PQ native address with RVN for the first protected spend. There is no automatic or retroactive migration, and a compromised classical key can race a migration. 6. After each protected asset spend, inspect the native PQ anchor balance. Ordinary single-program native change can replenish it, but exact-fee, dust, explicit-change, and multiple-program cases may need a manual - RVN top-up before another spend. Backup and verify both private keys - and their descriptor association before moving administrative tokens. + RVN top-up before another spend. Back up and verify the ML-DSA key and + descriptor association before moving administrative tokens. Preserve + classical keys for historical UTXOs until those holdings are migrated. ## 17. Current Anchor Funding Procedure @@ -448,7 +465,7 @@ with these steps: 2. Select at least one funded native witness-v2 UTXO with the same program for each distinct protected program. Include those outpoints explicitly in the raw transaction's input list beside the asset outpoints. -3. Use a canonical `classical|PQ` descriptor for each new asset output. Add a +3. Use a canonical `assetID|PQ` descriptor for each new asset output. Add a positive, spendable native RVN output back to every PQ address whose last anchor is being consumed. Reserve enough RVN for fees and future spends. 4. If using `fundrawtransaction` for the remaining fee, inspect its resulting @@ -482,7 +499,7 @@ be compromised: historical asset and `ASSET!` owner-token outpoints, including their quantities and classical addresses. 2. For a single legacy asset UTXO, transfer its entire quantity to a new - canonical `classical|PQ` descriptor. Transfer an owner token's full unit + canonical `assetID|PQ` descriptor. Transfer an owner token's full unit the same way. If a source address holds several UTXOs, use explicit outpoint selection when exact UTXO control is needed. 3. For a partial transfer through `transferfromaddress` or @@ -506,8 +523,8 @@ The owner selected the native-RVN migration model for assets and owner tokens. Outputs created under the bit-13 rule require an ML-DSA-44 key without an ECDSA signature. Historical asset and owner-token outputs retain their classical spending rule and migrate voluntarily, with no sunset. The -PQ-only format and wallet path are implemented locally under FINDING-158; -public activation and release qualification remain pending. +PQ-only format and wallet path passed paired CI on `9151946ad` under +FINDING-158; public activation and release qualification remain pending. An input carrying a native witness-v2 RVN output with the matching PQ program provides the ML-DSA authorization for an asset input. The asset input has an @@ -531,15 +548,17 @@ anchor whenever such an output is spent at an active origin height and rejects the old dual-signature prefix on new outputs. The `HASH160(P)` binding prevents a sender from selecting an unrelated qualified or unfrozen classical address while assigning ownership to a different PQ -program. The 160-bit address identifier and restricted-asset semantics need -separate adversarial review. +program. The 160-bit identifier and restricted-asset semantics were included +in the independent asset review; final release-SHA regression remains due. A red-then-green unit test shows that this prefix passes the existing standard script check with an empty scriptSig, is recognized as an asset, -and is parsed only when the address identifier matches `HASH160(P)`. This -is a format feasibility result, not full compatibility evidence. Before -activation, FINDING-160 requires an explicit network upgrade plan for blocks -above Core 4.8.0 limits. Wallet, RPC, Qt, migration and consensus regressions -need paired release-target CI at one exact SHA. The owner approved bit-13 -start and timeout dates for this PQ-only rule; they remain dormant in code -pending CI qualification and historical testnet compatibility review. +and is parsed only when the address identifier matches `HASH160(P)`. +Core 4.8.0 also accepted a 932-block regtest chain with 66 such outputs +and 35 later spends at ordinary block sizes. This does not resolve the +native block-limit split in FINDING-160. Wallet, RPC, Qt, migration and +consensus regressions passed paired release-target CI at `9151946ad`. +The owner-approved bit-13 dates are in `src/chainparams.cpp` at +`1dfae5b21`, with a red-then-green parameter regression and local +`make check`. Final-SHA CI and historical testnet compatibility review +are still required. diff --git a/doc/RIP-0025-PQ-Signatures.md b/doc/RIP-0025-PQ-Signatures.md index ed0f832b54..34bb0a0a14 100644 --- a/doc/RIP-0025-PQ-Signatures.md +++ b/doc/RIP-0025-PQ-Signatures.md @@ -350,7 +350,8 @@ For PQ outputs, the spend cost increases proportionally to the ML-DSA witness si ``` PQ witness input: mldsa_sig (2420) + mldsa_pk (1312) = 3732 bytes -With PQ discount: 3732 / 8 = ~467 weight units +With contextual PQ discount: 3732 / 8 = ~467 virtual bytes, +or about 1866 weight units for these bytes ``` ### 5. Activation Mechanism @@ -467,10 +468,10 @@ Users migrate native RVN by sending it from legacy addresses to new PQ addresses 3. Sign with existing ECDSA key (standard legacy transaction) 4. Broadcast and confirm -After migration, native RVN change outputs can go to PQ addresses. On default -public-network parameters, asset-bearing change still uses the legacy asset -format. The separate dormant bit-13 extension defines protected asset change -for a future activation. +After migration, native RVN change outputs can go to PQ addresses. Until +the separate bit-13 rule activates, asset-bearing change still uses the +historical format. After activation, newly created spendable asset change +must use the PQ-only asset format. #### 7.3 Asset Scope @@ -483,8 +484,9 @@ RIP-25 witness-v2 protects native RVN outputs only. It does not change the Raven - qualifier and sub-qualifier assets. This scope statement describes the approved bit 12 baseline and the default -public-network state. A 4.8.1 working branch has a separate, dormant bit 13 -asset candidate. Once bit 13 activates, new asset and owner-token outputs +public-network state. A 4.8.1 working branch has separate, approved future +bit-13 windows for testnet and mainnet. Once bit 13 activates, new asset +and owner-token outputs commit to a PQ program. Spending them requires a matching native RVN witness-v2 input signed with ML-DSA-44, without an ECDSA signature. Asset and owner-token outputs use an asset carrier keyed by the PQ program, not a @@ -526,7 +528,7 @@ The remaining transaction compatibility properties are: - **Unupgraded nodes**: See witness v2 outputs as "anyone-can-spend" per BIP141 rules - **Legacy addresses**: Continue to work indefinitely - **Legacy transactions**: Continue to be valid. No existing transaction type is modified -- **Asset transactions**: Unchanged by the approved bit-12 baseline. The separate dormant bit-13 extension retains the legacy P2PKH condition and adds a required PQ anchor for newly created asset outputs after its own activation +- **Asset transactions**: Unchanged by the approved bit-12 baseline. The separate bit-13 rule requires a PQ-only carrier and matching native ML-DSA anchor for newly created spendable asset outputs after its own activation. These outputs need no ECDSA signature; historical outputs retain their classical rule - **Migration**: Voluntary. Users migrate funds at their own pace The network context was added before mainnet RIP-25 activation. Mainnet has no From 6bdbae2b485eaf21435e1b9b6ed3f8b542fdd384 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:07:48 +0200 Subject: [PATCH 060/195] [FINDING-067] Record bit 13 parameter proof and release gates --- ...0025-v4.8-security-remediation-register.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 74e66c65da..e81034d260 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4427,6 +4427,44 @@ intermediate 4.8.0-versioned SHA, not the final 4.8.1 release SHA: approved public deployment parameters, the 4.8.1 version change, final same-SHA artifact qualification, and release approval remain outstanding. +**2026-10-13 approved bit-13 parameter implementation:** Commit +`1dfae5b21` sets the owner-approved PQ-only asset deployment windows in +`src/chainparams.cpp`: testnet 2027-04-01 to 2028-10-01 UTC at +1310/2016, mainnet 2029-01-01 to 2030-07-01 UTC at 1714/2016. +Regtest remains dormant by default with an explicit test override. The +`pq_asset_public_deployment_parameters` unit regression failed against +the prior all-dormant values with four mismatches, then passed after the +change. Local `make -j8 check` passed, and the full RIP-25 invariant gate +passed with all nine focused functional tests. This commit follows the +green CI pair at `9151946ad`, but the new SHA has not yet received its +own Build Raven and Final Gate qualification. + +FINDING-067 remains HIGH and OPEN. Its remaining release conditions are: + +1. Resolve FINDING-160 with an explicit public network upgrade plan for + the original RIP-25 8 to 12 to 16 MWU transition and contextual PQ + witness discount. Core 4.8.0 rejects allowed post-bit-12 blocks; + signaling alone does not upgrade validating nodes. +2. Finalize the mainnet bit-12 start date with the owner, then apply the + approved 18-month timeout relative to that date and requalify its + consensus parameters. No bit-12 public parameter change is included + in `1dfae5b21`. +3. Revalidate the shared testnet history for the earlier empty-context PQ + signature rule and document a compatible deployment boundary if needed. +4. Give the original unexplained `make check` failure in FINDING-166 an + evidence-based disposition. The diagnostic workflow repair and later + green runs do not reveal the missing test log from run `38047871989`. +5. Bump the version to 4.8.1, rerun mandatory `make check`, all five + Build Raven artifacts, and Final Gate on one exact final SHA, then + complete release notes, checksums, and migration guidance. The final + private audit report and explicit tag and release approval follow. + +The legacy asset and `ASSET!` migration path is voluntary and has no +sunset. Newly created post-bit-13 asset outputs need an ML-DSA anchor +and no ECDSA signature; the synthetic asset identifier has no classical +private key. FINDING-148, 150, 151, and 155 remain documented fail-safe +operational limits rather than closed automatic funding features. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical From e2e86a5910f07e4fcd768bd84f70ad62635b9e67 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:46:17 +0200 Subject: [PATCH 061/195] [FINDING-067] Record paired CI for approved bit 13 timing --- ...RIP-0025-v4.8-security-remediation-register.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index e81034d260..9d807f48b2 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4465,6 +4465,21 @@ and no ECDSA signature; the synthetic asset identifier has no classical private key. FINDING-148, 150, 151, and 155 remain documented fail-safe operational limits rather than closed automatic funding features. +**2026-10-13 bit-13 parameter CI checkpoint:** Build Raven run +`38335505917` and RIP-25 v4.8 Final Gate run `38335508527` both passed +on exact SHA `6bdbae2b485eaf21435e1b9b6ed3f8b542fdd384`, which includes +the approved bit-13 timing and the current PQ-only documentation. Both +required security jobs passed mandatory `make check`, behavioral invariants, +and focused functional tests; Final Gate also completed all eight build +jobs. Build Raven produced all five unsigned targets: Linux x86_64 +artifact `11785747342`, arm32v7 `11785807889`, Windows MinGW +`11786074980`, aarch64 `11786238569`, and macOS `11786669670`. +The downloaded Linux archive's published SHA-256 file verified locally; +its digest is `17a869288918a0576ff7c355a00dacae70246d76bd70b123dc310b3915797e95`, +and `PROVENANCE.txt` names the full SHA. These remain version 4.8.0 +candidate artifacts. The final 4.8.1 versioned SHA still needs a paired +rerun after the open network and testnet gates are resolved. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical From b7d17741e76abf35e323a7f5d4ce0564a2f8ada7 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:56:45 +0200 Subject: [PATCH 062/195] [FINDING-067] Record partial testnet history evidence --- doc/RIP-0025-v4.8-security-remediation-register.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 9d807f48b2..8452bcbe26 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4480,6 +4480,18 @@ and `PROVENANCE.txt` names the full SHA. These remain version 4.8.0 candidate artifacts. The final 4.8.1 versioned SHA still needs a paired rerun after the open network and testnet gates are resolved. +**2026-10-13 testnet-history qualification attempt:** The previously +available local testnet chain reached only height 49. An isolated, +unpruned, no-wallet testnet node received more than 2.5 million headers, +but full block validation reached only height 39628, hash +`000000015c51be2026a84eb9d97d4ddf4fd1d224b472c7f61594ed6e0a207524`, +at shutdown. The reported verification progress was 0.008097. A private +scanner has been prepared to examine every block for native witness-v2 +outputs and their spends, plus P2SH-wrapped witness-v2 spends, once a +fully synchronized unpruned node or verified snapshot is available. +The partial chain does not establish whether historical empty-context +PQ signatures exist. Release condition 3 above remains OPEN. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical From 5ee01cb2b8869af9436f82decef130b83f6b0944 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:13:44 +0200 Subject: [PATCH 063/195] [FINDING-067] Verify all candidate artifact manifests --- ...RIP-0025-v4.8-security-remediation-register.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 8452bcbe26..98d6bcce68 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4480,6 +4480,21 @@ and `PROVENANCE.txt` names the full SHA. These remain version 4.8.0 candidate artifacts. The final 4.8.1 versioned SHA still needs a paired rerun after the open network and testnet gates are resolved. +All five artifacts were subsequently downloaded. Every file listed in +each `SHA256SUMS` matched its downloaded bytes; each standalone SHA-256 +file matched the corresponding manifest; every `PROVENANCE.txt` named the +same full commit SHA and the correct target. The common source archive +digest was `03876a8c194ea861a1a5699e463db8641ed07f1f81f257599aace46b62e1ff5d`. +The primary target digests were: arm32v7 +`acc775be63b88652404eb85d98ee102357b605d8a3458aa686b5285bd3701203`, +aarch64 `cfcf7009e04ee8d39eebee3f328dd50181ed5933bb8e42c947bf6eedfc4d4228`, +Windows win64 ZIP `ca0fc7b6697a14920c84fc43722e4b735001da12b3214a6da967bf656fdf4195`, +and macOS osx64 archive +`73e787b29745b561a1c286406fb62ecd5c4eb2f6c6a1f867869d621f8e328e34`. +The Windows unsigned installer and macOS unsigned DMG also matched their +published manifest entries. This verifies artifact integrity and source +identity; it does not replace execution on each target platform. + **2026-10-13 testnet-history qualification attempt:** The previously available local testnet chain reached only height 49. An isolated, unpruned, no-wallet testnet node received more than 2.5 million headers, From 73cbc93fd863aaab68d0cd1a1b336cad53d79948 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:18:40 +0200 Subject: [PATCH 064/195] [FINDING-160] Document approved bit 12 network upgrade --- doc/RIP-0025-PQ-Signatures.md | 40 +++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-PQ-Signatures.md b/doc/RIP-0025-PQ-Signatures.md index 34bb0a0a14..c8f79de978 100644 --- a/doc/RIP-0025-PQ-Signatures.md +++ b/doc/RIP-0025-PQ-Signatures.md @@ -520,8 +520,44 @@ can reject blocks that a post-activation node accepts. All validating services that intend to follow the upgraded chain must install compatible software before bit 12 becomes active. Miner signaling measures miner readiness; it does not upgrade old nodes or guarantee their acceptance of -12/16 MWU blocks. Public activation therefore requires a reviewed network -upgrade plan, testnet history verification, and explicit release guidance. +12/16 MWU blocks. The public activation plan below is an incompatible +consensus upgrade, approved for documentation by the owner. Release and +activation still depend on the listed qualification gates. + +### Public network activation plan for bit 12 + +1. Distribute the qualified 4.8.1 release before the bit-12 STARTED window. + Announce the exact start, timeout, threshold, and 2016-block window with + binaries, checksums, upgrade instructions, and the native RVN migration + path. Tell miners, pools, validating nodes, exchanges, custodians, and + explorers that Core 4.8.0 can reject blocks accepted by upgraded nodes. +2. Before requesting bit-12 signaling, validate the complete shared testnet + history against the network-bound ML-DSA rule. Repeat the two-binary + block tests around the Core 4.8.0 8 MWU boundary, including a PQ witness + spend whose old and new weights straddle that boundary. Qualify the + final versioned SHA with mandatory `make check`, all release targets, + and the Final Gate. If a required gate is missing or the network is not + ready, change the public parameters in a reviewed release before the + STARTED window rather than relying on miner signaling to protect old + validators. +3. Bit-12 BIP9 signaling measures miner readiness. LOCKED_IN is followed + by ACTIVE after the next full window. All validating services that need + to follow the upgraded chain must update before ACTIVE. Signaling does + not measure their adoption, and there is no automatic rollback to the + Core 4.8.0 weight rules after ACTIVE. +4. At the bit-12 activation height `H12`, the limit becomes 12 MWU and + the contextual PQ witness discount takes effect. At `H12 + 525600` + blocks, one nominal year at the 60-second target spacing, the limit + becomes 16 MWU. This second phase is height-based, not a fixed calendar + date. Operators must treat both phases as part of the same incompatible + network upgrade. + +The old and new binaries agreed below 8 MWU in the recorded regtest +comparison. Core 4.8.0 rejected a larger candidate-accepted block and a +separate discounted PQ-witness block just across its old 8 MWU boundary. +The exact blocks, weights, and outcomes are recorded in FINDING-160 of the +security remediation register. Approval of this plan does not assert that +testnet history or final release qualification has already passed. The remaining transaction compatibility properties are: From 3c3bf8385bb5d59f27c8f58fee7d1956970b0663 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:20:01 +0200 Subject: [PATCH 065/195] [FINDING-160] Correct remaining compatibility claims --- README.md | 4 ++-- doc/RIP-0025-PQ-Signatures.md | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 605e8da5f3..65e4fa261b 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ New **witness v2** addresses use ML-DSA-44 (a NIST-standardized post-quantum sig | Area | Change | |------|--------| -| **Consensus** | BIP9 soft-fork deployment (bit 12, 85% threshold), phased block weight increase (8 → 12 → 16 MWU) | +| **Consensus** | Bit-12 BIP9 signaling (1714 of 2016); incompatible 8 to 12 to 16 MWU block-limit transition and contextual PQ witness discount | | **Script** | Witness version 2 validation: 2-element witness stack [mldsa_sig, mldsa_pk], SHA256(pk) == program | -| **Policy** | `TX_WITNESS_V2_PQ_KEYHASH` standard type, PQ witness discount (8x), PQ-aware dust threshold | +| **Policy** | `TX_WITNESS_V2_PQ_KEYHASH` standard type and PQ-aware dust threshold | | **Addresses** | Bech32m encoding for witness v2 (HRP: `rvn` mainnet, `trvn` testnet, `rcrt` regtest) | | **Network** | `NODE_PQ_HYBRID` service flag (bit 5), 16 MB protocol message limit | | **Crypto** | `src/crypto/mldsa.h/cpp` — ML-DSA-44 via [liboqs](https://github.com/open-quantum-safe/liboqs) (FIPS 204 compliant) | diff --git a/doc/RIP-0025-PQ-Signatures.md b/doc/RIP-0025-PQ-Signatures.md index c8f79de978..c9fd3cdb29 100644 --- a/doc/RIP-0025-PQ-Signatures.md +++ b/doc/RIP-0025-PQ-Signatures.md @@ -267,7 +267,10 @@ When a node encounters a witness version 2 program of length 32 bytes: 7. Verify: `ML_DSA_44_Verify(mldsa_pk, sighash, network_context, mldsa_sig)` (ML-DSA check) 8. If all checks pass, the input is valid -For unupgraded nodes, witness version 2 outputs are treated as "anyone-can-spend" per BIP141 rules, which is safe as long as a supermajority of miners enforce the new rules. +Unupgraded nodes treat witness version 2 outputs as "anyone-can-spend" +under BIP141. They do not enforce the ML-DSA signature rule, and miner +signaling does not make them compatible with the higher block limits or +PQ witness discount. See the public activation plan below. #### 3.4 Script Size Limits From 6a2acc716a9fc59d66c3620e8a3700ff87f97450 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:20:29 +0200 Subject: [PATCH 066/195] [FINDING-160] Record approved incompatible deployment plan --- ...0025-v4.8-security-remediation-register.md | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 98d6bcce68..4bc847d348 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4441,10 +4441,13 @@ own Build Raven and Final Gate qualification. FINDING-067 remains HIGH and OPEN. Its remaining release conditions are: -1. Resolve FINDING-160 with an explicit public network upgrade plan for - the original RIP-25 8 to 12 to 16 MWU transition and contextual PQ +1. Carry the owner-approved FINDING-160 incompatible network upgrade plan + into the versioned release notes and operator instructions. The plan + preserves the original 8 to 12 to 16 MWU transition and contextual PQ witness discount. Core 4.8.0 rejects allowed post-bit-12 blocks; - signaling alone does not upgrade validating nodes. + miners, pools, validating nodes, and services must upgrade before + ACTIVE. Repeat the boundary checks on the final SHA and establish + network readiness before requesting signaling. 2. Finalize the mainnet bit-12 start date with the owner, then apply the approved 18-month timeout relative to that date and requalify its consensus parameters. No bit-12 public parameter change is included @@ -8347,6 +8350,12 @@ Cross-platform CI and public deployment remain separate release gates. activation and 16 MWU one year later. This requires an explicit incompatible-rule deployment plan and release documentation before public activation; miner signaling alone does not upgrade old nodes. + On 2026-10-13 the owner approved the documented plan: distribute 4.8.1 + before STARTED, identify Core 4.8.0 incompatibility, require operators + to upgrade before ACTIVE, apply 12 MWU and PQ discount at ACTIVE and + 16 MWU after one nominal year, and retain testnet-history and 8 MWU + boundary proofs as pre-activation gates. This is not approval to tag or + publish the release. - **Expected behavior:** Prove old-node acceptance of every post-activation block allowed by the candidate, or explicitly choose and approve a deployment that accounts for the incompatible block rules. Correct the @@ -8355,7 +8364,10 @@ Cross-platform CI and public deployment remain separate release gates. blocks on both sides of the 8 MWU boundary, including PQ witness data whose discounted and old-accounted weights differ. Verify block and chain outcomes, not only isolated weight functions. -- **Remediation commit:** None. +- **Remediation commits:** `73cbc93fd` documents the public upgrade plan; + `3c3bf8385` removes residual claims that BIP9 signaling alone makes + the combined deployment compatible with Core 4.8.0. No consensus + limit was changed by these documentation commits. - **Verification:** Static comparison of the baseline 8 MWU constant and candidate contextual 12/16 MWU checks establishes the incompatible acceptance ranges. In a local two-binary regtest at height 540 after @@ -8372,8 +8384,10 @@ Cross-platform CI and public deployment remain separate release gates. advanced one block. This proves the discount is independently incompatible at the 8 MWU boundary, even without using the 12 MWU phase-1 allowance. -- **Final status:** OPEN; blocks any claim of a compatible public bit-12 - activation and the 4.8.1 release. +- **Final status:** ACCEPTED as an incompatible consensus upgrade with an + owner-approved plan. The 4.8.1 release and public bit-12 activation + remain gated by FINDING-067: testnet history, finalized bit-12 timing, + final-SHA qualification, release instructions, and operator readiness. ### FINDING-161: Pre-activation PQ-only lookalike output has no PQ custody From 1abbcfec2cde40969c7c95c969421ee94cc9ad33 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:21:08 +0200 Subject: [PATCH 067/195] [FINDING-166] Record accepted historical CI disposition --- ...0025-v4.8-security-remediation-register.md | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 4bc847d348..9f35aefc38 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4454,14 +4454,17 @@ FINDING-067 remains HIGH and OPEN. Its remaining release conditions are: in `1dfae5b21`. 3. Revalidate the shared testnet history for the earlier empty-context PQ signature rule and document a compatible deployment boundary if needed. -4. Give the original unexplained `make check` failure in FINDING-166 an - evidence-based disposition. The diagnostic workflow repair and later - green runs do not reveal the missing test log from run `38047871989`. -5. Bump the version to 4.8.1, rerun mandatory `make check`, all five +4. Bump the version to 4.8.1, rerun mandatory `make check`, all five Build Raven artifacts, and Final Gate on one exact final SHA, then complete release notes, checksums, and migration guidance. The final private audit report and explicit tag and release approval follow. +The owner accepted FINDING-166's evidence-based historical disposition on +2026-10-13: the original test failure cannot be classified more narrowly +without the missing log, while the diagnostic workflow defect is fixed. +This does not waive mandatory `make check` on the final release SHA or +permit a future failing gate to be ignored. + The legacy asset and `ASSET!` migration path is voluntary and has no sunset. Newly created post-bit-13 asset outputs need an ML-DSA anchor and no ECDSA signature; the synthetic asset identifier has no classical @@ -8616,20 +8619,20 @@ Cross-platform CI and public deployment remain separate release gates. by a diagnosable, successful `make check` on their exact source SHA. - **Affected Core 4.8.0 fix:** None. The gate is specific to this release workflow. -- **Root cause:** Build Raven runs `make check` without printing +- **Root cause:** Build Raven previously ran `make check` without printing `src/test-suite.log` or `src/test/test_raven.log` on failure. Automake - prints only the aggregate `FAIL: test/test_raven` result. + printed only the aggregate `FAIL: test/test_raven` result. The original + unit assertion cannot be reconstructed from the retained CI log. - **Affected file/function/lines:** .github/workflows/build-raven.yml:65-68, mandatory unit-test step. - **Concrete scenario:** Build Raven run `38047871989` on `3ed32302d37fb343231c20905d178016ab1a07c2` builds successfully but fails `make check` with one failing test binary. It skips all release artifact jobs. Final Gate on the same SHA passes its unit tests, so the - particular Build Raven test failure remains unclassified until its test - log is available. + particular Build Raven test failure has no retained diagnostic log. - **Expected behavior:** Preserve `make check` as a hard gate and print its - bounded failure logs before exiting nonzero. Use the rerun to identify - and remediate any underlying deterministic or intermittent test defect. + bounded failure logs before exiting nonzero. If a failure recurs, use + its log to identify and remediate the underlying test defect. - **Regression required:** Simulate a failing `make check` and prove the workflow reports the specific test failure and still fails; then rerun both workflows on one exact SHA. @@ -8639,10 +8642,15 @@ Cross-platform CI and public deployment remain separate release gates. showed the original step hid a test assertion, while the revised step prints it and still fails. Local `make check` passes. Build Raven `38296258242` and Final Gate `38296262444` passed their mandatory - `make check` at `9151946ad`. The original `38047871989` unit failure - remains unclassified because its missing log cannot be reconstructed. -- **Final status:** OPEN for the original unexplained unit failure. The - diagnostic workflow repair is CI-qualified at `9151946ad`. + `make check` at `9151946ad`. Build Raven `38335505917` and Final Gate + `38335508527` also passed at `6bdbae2b4`. The original run has no + preserved test log or artifact, so its particular assertion remains + unknown. On 2026-10-13 the owner accepted this documented uncertainty + after reviewing the diagnostic repair and repeated green paired runs. +- **Final status:** DISPOSED as an unclassifiable historical unit failure; + the diagnostic defect is FIXED and qualified on candidate CI. Mandatory + `make check` on the final 4.8.1 SHA remains a release gate. Any new + failure requires its own diagnosis and remedy. ### FINDING-167: Pinned FreeType source host blocks the ARM64 artifact From 2827cca77decb4f9cacb2c0eec3eea8fa7c1a43c Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:21:49 +0200 Subject: [PATCH 068/195] [FINDING-067] Describe staged asset protection in README --- README.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 65e4fa261b..3ee481b139 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,16 @@ https://ravencoin.org ## RIP-25: Post-Quantum Signatures (This Fork) -This fork implements [RIP-25](doc/RIP-0025-PQ-Signatures.md) ([GitHub Issue #1280](https://github.com/RavenProject/Ravencoin/issues/1280)), a proposal to add **quantum-resistant native RVN transaction signing** to Ravencoin using ML-DSA-44 (FIPS 204). +This fork implements [RIP-25](doc/RIP-0025-PQ-Signatures.md) ([GitHub Issue #1280](https://github.com/RavenProject/Ravencoin/issues/1280)), which adds **quantum-resistant native RVN transaction signing** using ML-DSA-44 (FIPS 204). The 4.8.1 candidate adds a separate bit-13 rule for new asset and owner-token outputs. ### What it does -New **witness v2** addresses use ML-DSA-44 (a NIST-standardized post-quantum signature algorithm) exclusively. Existing ECDSA addresses (witness v0) continue working unchanged. Users can migrate native RVN from ECDSA to ML-DSA-44 addresses before quantum computers can break ECDSA. Ravencoin assets remain on their legacy ownership conditions and are outside the current RIP-25 scope. +New **witness v2** addresses use ML-DSA-44 (a NIST-standardized post-quantum signature algorithm) exclusively. Existing ECDSA addresses continue working unchanged. Users can migrate native RVN to ML-DSA-44 addresses voluntarily. The original bit-12 rule does not change legacy asset ownership. Once bit 13 activates, newly created asset and owner-token outputs require a matching native RVN witness-v2 ML-DSA anchor when spent, with no ECDSA asset signature. Historical outputs retain their classical spending rule until their owners migrate them. -- **Old addresses (witness v0):** ECDSA/secp256k1, unchanged -- **New addresses (witness v2):** ML-DSA-44 only, quantum-resistant +- **Classical addresses:** ECDSA/secp256k1, unchanged +- **PQ addresses (witness v2):** ML-DSA-44 only, quantum-resistant - **Migration:** Users send native RVN from old to new addresses at their own pace -- **Assets:** Normal, owner, reissuable, unique, restricted, and qualifier assets are not protected by RIP-25 +- **Assets:** New post-bit-13 asset and owner-token outputs require an ML-DSA anchor; historical holdings remain classical until voluntary migration ### Key changes @@ -30,15 +30,11 @@ New **witness v2** addresses use ML-DSA-44 (a NIST-standardized post-quantum sig | **Crypto** | `src/crypto/mldsa.h/cpp` — ML-DSA-44 via [liboqs](https://github.com/open-quantum-safe/liboqs) (FIPS 204 compliant) | | **Keys** | `src/pqkey.h/cpp` — `CPQKey` / `CPQPubKey` for ML-DSA-44 key management | | **Wallet** | `getnewpqaddress` RPC, PQ keystore integration, `IsMine` for witness v2 | -| **Assets** | Existing asset scripts remain legacy P2PKH-only; witness-v2 PQ destinations protect native RVN only | +| **Assets** | Bit 13 adds PQ-only asset carriers paired with native RVN witness-v2 inputs; historical asset outputs remain classically spendable | | **Signing** | ML-DSA-44 signing in `sign.cpp` via `TransactionSignatureCreator` | | **Build** | liboqs added as dependency (`depends/packages/liboqs.mk`, `configure.ac --with-liboqs`) | | **Tests** | `src/test/pqkey_tests.cpp` — unit tests for ML-DSA-44 keygen, sign/verify, witness programs | -### Branch - -Audit and remediation work is on [`fix/rip25-v48-glm-remediation`](https://github.com/ALENOC/Ravencoin/tree/fix/rip25-v48-glm-remediation). - ### Building with liboqs ```bash From a809f6757bd44e97dfee9f2584058a8c3d978d5b Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:24:08 +0200 Subject: [PATCH 069/195] [FINDING-067] Use plain punctuation in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3ee481b139..7bd0740f26 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ New **witness v2** addresses use ML-DSA-44 (a NIST-standardized post-quantum sig | **Policy** | `TX_WITNESS_V2_PQ_KEYHASH` standard type and PQ-aware dust threshold | | **Addresses** | Bech32m encoding for witness v2 (HRP: `rvn` mainnet, `trvn` testnet, `rcrt` regtest) | | **Network** | `NODE_PQ_HYBRID` service flag (bit 5), 16 MB protocol message limit | -| **Crypto** | `src/crypto/mldsa.h/cpp` — ML-DSA-44 via [liboqs](https://github.com/open-quantum-safe/liboqs) (FIPS 204 compliant) | -| **Keys** | `src/pqkey.h/cpp` — `CPQKey` / `CPQPubKey` for ML-DSA-44 key management | +| **Crypto** | `src/crypto/mldsa.h/cpp`: ML-DSA-44 via [liboqs](https://github.com/open-quantum-safe/liboqs) (FIPS 204 compliant) | +| **Keys** | `src/pqkey.h/cpp`: `CPQKey` / `CPQPubKey` for ML-DSA-44 key management | | **Wallet** | `getnewpqaddress` RPC, PQ keystore integration, `IsMine` for witness v2 | | **Assets** | Bit 13 adds PQ-only asset carriers paired with native RVN witness-v2 inputs; historical asset outputs remain classically spendable | | **Signing** | ML-DSA-44 signing in `sign.cpp` via `TransactionSignatureCreator` | | **Build** | liboqs added as dependency (`depends/packages/liboqs.mk`, `configure.ac --with-liboqs`) | -| **Tests** | `src/test/pqkey_tests.cpp` — unit tests for ML-DSA-44 keygen, sign/verify, witness programs | +| **Tests** | `src/test/pqkey_tests.cpp`: unit tests for ML-DSA-44 keygen, sign/verify, witness programs | ### Building with liboqs From 2df4d2d25cb28dec3c953f2afb3a09741510f6d2 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:52:09 +0200 Subject: [PATCH 070/195] [FINDING-067] Schedule bit 12 mainnet activation --- src/chainparams.cpp | 4 +-- src/test/versionbits_tests.cpp | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index e52a091410..c5941611ed 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -184,8 +184,8 @@ class CMainParams : public CChainParams { // RIP-25: ML-DSA-44 witness-v2 deployment (historical DEPLOYMENT_PQ_HYBRID enum name retained) consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].bit = 12; - consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nStartTime = 1798761600; // placeholder - consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nTimeout = 1830297600; // placeholder + consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nStartTime = 1814400000; // 2027-07-01 00:00:00 UTC + consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nTimeout = 1861920000; // 2029-01-01 00:00:00 UTC consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nOverrideRuleChangeActivationThreshold = 1714; // Approx 85% of 2016 consensus.vDeployments[Consensus::DEPLOYMENT_PQ_HYBRID].nOverrideMinerConfirmationWindow = 2016; // RIP-25: PQ-only asset deployment diff --git a/src/test/versionbits_tests.cpp b/src/test/versionbits_tests.cpp index 8b99ffea54..6baa6bb64d 100644 --- a/src/test/versionbits_tests.cpp +++ b/src/test/versionbits_tests.cpp @@ -387,5 +387,60 @@ BOOST_FIXTURE_TEST_SUITE(versionbits_tests, TestingSetup) //BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, mainnetParams) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS); } + BOOST_AUTO_TEST_CASE(pq_hybrid_mainnet_activation_test) + { + const auto chainParams = CreateChainParams(CBaseChainParams::MAIN); + const Consensus::Params ¶ms = chainParams->GetConsensus(); + const Consensus::DeploymentPos deployment = Consensus::DEPLOYMENT_PQ_HYBRID; + const auto &rules = params.vDeployments[deployment]; + + BOOST_CHECK_EQUAL(rules.bit, 12); + BOOST_CHECK_EQUAL(rules.nStartTime, 1814400000LL); // July 1, 2027 00:00 UTC + BOOST_CHECK_EQUAL(rules.nTimeout, 1861920000LL); // January 1, 2029 00:00 UTC + BOOST_CHECK_EQUAL(rules.nOverrideRuleChangeActivationThreshold, 1714); + BOOST_CHECK_EQUAL(rules.nOverrideMinerConfirmationWindow, 2016); + + const int32_t oldVersion = VERSIONBITS_TOP_BITS; + const int32_t signalVersion = oldVersion | VersionBitsMask(params, deployment); + + // A calendar start time alone cannot start the deployment. The median + // time of the preceding period must reach the start time. + VersionBitsTester chain; + VersionBitsCache cache; + chain.Mine(2016, rules.nStartTime - 1, oldVersion); + BOOST_CHECK_EQUAL(VersionBitsState(chain.Tip(), params, deployment, cache), THRESHOLD_DEFINED); + chain.Mine(4032, rules.nStartTime, oldVersion); + BOOST_CHECK_EQUAL(VersionBitsState(chain.Tip(), params, deployment, cache), THRESHOLD_STARTED); + + // The 1714th signal locks in the rule at the next period boundary. + chain.Mine(4334, rules.nStartTime + 60, oldVersion); + chain.Mine(6047, rules.nStartTime + 60, signalVersion); + BOOST_CHECK_EQUAL(VersionBitsState(chain.Tip(), params, deployment, cache), THRESHOLD_STARTED); + chain.Mine(6048, rules.nStartTime + 60, signalVersion); + BOOST_CHECK_EQUAL(VersionBitsState(chain.Tip(), params, deployment, cache), THRESHOLD_LOCKED_IN); + chain.Mine(8064, rules.nStartTime + 120, oldVersion); + BOOST_CHECK_EQUAL(VersionBitsState(chain.Tip(), params, deployment, cache), THRESHOLD_ACTIVE); + + VersionBitsTester belowThreshold; + VersionBitsCache belowThresholdCache; + belowThreshold.Mine(2016, rules.nStartTime - 1, oldVersion); + belowThreshold.Mine(4032, rules.nStartTime, oldVersion); + belowThreshold.Mine(4335, rules.nStartTime + 60, oldVersion); + belowThreshold.Mine(6048, rules.nStartTime + 60, signalVersion); + BOOST_CHECK_EQUAL(VersionBitsState(belowThreshold.Tip(), params, deployment, belowThresholdCache), THRESHOLD_STARTED); + + // An unsuccessful signaling period reaches FAILED only after its MTP + // reaches the timeout at a period boundary. + VersionBitsTester timeoutChain; + VersionBitsCache timeoutCache; + timeoutChain.Mine(2016, rules.nStartTime - 1, oldVersion); + timeoutChain.Mine(4032, rules.nStartTime, oldVersion); + BOOST_CHECK_EQUAL(VersionBitsState(timeoutChain.Tip(), params, deployment, timeoutCache), THRESHOLD_STARTED); + timeoutChain.Mine(6048, rules.nTimeout - 1, oldVersion); + BOOST_CHECK_EQUAL(VersionBitsState(timeoutChain.Tip(), params, deployment, timeoutCache), THRESHOLD_STARTED); + timeoutChain.Mine(8064, rules.nTimeout, oldVersion); + BOOST_CHECK_EQUAL(VersionBitsState(timeoutChain.Tip(), params, deployment, timeoutCache), THRESHOLD_FAILED); + } + BOOST_AUTO_TEST_SUITE_END() From 948b1ea2031837407aae8d4b3f156a2949c0332d Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:57:30 +0200 Subject: [PATCH 071/195] [FINDING-160] Document bit 12 operator rollout --- doc/RIP-0025-PQ-Assets-Followup.md | 18 ++++-- doc/RIP-0025-PQ-Signatures.md | 25 ++++++-- doc/release-notes/release-notes-4.8.1.md | 81 ++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 doc/release-notes/release-notes-4.8.1.md diff --git a/doc/RIP-0025-PQ-Assets-Followup.md b/doc/RIP-0025-PQ-Assets-Followup.md index a05a8ba39f..e72c65c1b6 100644 --- a/doc/RIP-0025-PQ-Assets-Followup.md +++ b/doc/RIP-0025-PQ-Assets-Followup.md @@ -387,9 +387,12 @@ outputs and 35 later spends from the candidate, including an asset spend with empty scriptSig. This checks the asset format at ordinary block sizes; Core 4.8.0 does not enforce ML-DSA. FINDING-160 separately proves a split at the native 8 MWU boundary and requires a coordinated incompatible-rule -upgrade plan. Testnet history, the bit-12 mainnet date, the old unexplained -CI failure, final-SHA artifacts, and operator migration guidance remain -release gates. Reusing bit 12 for assets would change the meaning of its +upgrade plan. The bit-12 mainnet dates are now fixed at 2027-07-01 and +2029-01-01 UTC. Testnet history, final-SHA artifacts and `make check`, +the 8 MWU boundary proof on that SHA, and operator readiness remain +release gates. The historical unexplained CI failure has an accepted +disposition, but any new failure still requires investigation. Reusing +bit 12 for assets would change the meaning of its earlier deployment and is excluded. ## 16. Proposed Operator Activation Sequence @@ -398,8 +401,11 @@ This is the operator sequence for the approved bit-13 windows, not an authorization to publish the release or activate a public chain. Testnet starts on 2027-04-01 and times out on 2028-10-01 at 1310/2016; mainnet starts on 2029-01-01 and times out on 2030-07-01 at 1714/2016. -FINDING-160, testnet-history review, and final release qualification -remain gates before public deployment. +The mainnet bit-12 window starts on 2027-07-01 and times out on +2029-01-01 at 1714/2016. The approved FINDING-160 operator plan, +testnet-history review, and final release qualification remain gates +before public deployment. If bit 12 fails or activates too late for a +safe asset rollout, review the bit-13 window before it starts. 1. Deploy and review the native RIP-25 bit-12 implementation. Its BIP9 transition enables witness-v2 ML-DSA-44 script validation, the 12 MWU @@ -413,7 +419,7 @@ remain gates before public deployment. cover the `H-2` through `H+2` boundary, restart, invalidate/reconsider, and reorgs across BIP9 `LOCKED_IN` and `ACTIVE`. 3. Before public deployment, verify the full shared testnet history and - bit-12 prerequisite, finish the incompatible block-rule upgrade plan, + bit-12 prerequisite, execute the incompatible block-rule upgrade plan, obtain same-SHA final release artifacts, and communicate migration. Updating software without a network deployment does not protect assets. 4. At or after `H`, newly created spendable asset outputs of every class, diff --git a/doc/RIP-0025-PQ-Signatures.md b/doc/RIP-0025-PQ-Signatures.md index c9fd3cdb29..b3b4650f9e 100644 --- a/doc/RIP-0025-PQ-Signatures.md +++ b/doc/RIP-0025-PQ-Signatures.md @@ -364,13 +364,18 @@ or about 1866 weight units for these bytes ``` Deployment parameters: bit: 12 - nStartTime: <6 months after release> - nTimeout: <18 months after start> + mainnet nStartTime: 1814400000 (2027-07-01 00:00 UTC) + mainnet nTimeout: 1861920000 (2029-01-01 00:00 UTC) nOverrideRuleChangeActivationThreshold: 1714 (85% of 2016 blocks) - nOverrideMinerConfirmationWindow: 2016 (~33.6 hours) + nOverrideMinerConfirmationWindow: 2016 (~33.6 hours at the target spacing) ``` -The 85% threshold provides additional safety margin for this cryptographically significant upgrade. +The owner approved this 18-month mainnet window on 2026-10-13. BIP9 uses +median time past at window boundaries; the start timestamp is not an +activation date. At least 1714 signals in one complete STARTED window +lead to one LOCKED_IN window, then ACTIVE. If the timeout is reached +before lock-in, the deployment fails. The 85% threshold provides an +additional safety margin for this cryptographically significant upgrade. ### 6. Implementation @@ -529,8 +534,10 @@ activation still depend on the listed qualification gates. ### Public network activation plan for bit 12 -1. Distribute the qualified 4.8.1 release before the bit-12 STARTED window. - Announce the exact start, timeout, threshold, and 2016-block window with +1. Distribute the qualified 4.8.1 release before the mainnet bit-12 + STARTED window beginning no earlier than 2027-07-01 00:00 UTC by + median time past. Its timeout is 2029-01-01 00:00 UTC. Announce the + exact start, timeout, 1714/2016 threshold, and 2016-block window with binaries, checksums, upgrade instructions, and the native RVN migration path. Tell miners, pools, validating nodes, exchanges, custodians, and explorers that Core 4.8.0 can reject blocks accepted by upgraded nodes. @@ -554,6 +561,12 @@ activation still depend on the listed qualification gates. becomes 16 MWU. This second phase is height-based, not a fixed calendar date. Operators must treat both phases as part of the same incompatible network upgrade. +5. The bit-13 mainnet STARTED timestamp is also 2029-01-01 00:00 UTC. + Bit 13 cannot enforce PQ-only asset spending until bit 12 and the + transfer-script deployment on bit 8 are ACTIVE. + If bit 12 fails or activates too late for a safe asset rollout, review + the bit-13 window in a separate release before it starts. Do not treat + the shared timestamp as evidence that bit 12 has activated. The old and new binaries agreed below 8 MWU in the recorded regtest comparison. Core 4.8.0 rejected a larger candidate-accepted block and a diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md new file mode 100644 index 0000000000..0ab6ba8a0d --- /dev/null +++ b/doc/release-notes/release-notes-4.8.1.md @@ -0,0 +1,81 @@ +# Raven Core 4.8.1 release notes (draft) + +Status: release qualification in progress. These notes are for review and +must be updated with the final source SHA, artifact checksums, and release +date before publication. + +## Upgrade before bit 12 activates + +RIP-25 adds ML-DSA-44 signatures for native RVN witness-v2 outputs. The +same activation changes the block limit from 8 to 12 million weight units +(MWU) and applies a contextual discount to PQ witness data. Core 4.8.0 +can reject blocks accepted by upgraded nodes after activation. This is an +incompatible consensus upgrade. Miners, pools, full nodes, exchanges, +custodians, explorers, and other validating services must install a +qualified 4.8.1 release before bit 12 becomes ACTIVE. A miner's signal +does not upgrade any other validator. + +Back up wallets, stop the old node cleanly, install the release artifact +for the platform, verify its published checksum, and allow the upgraded +node to synchronize. Before relying on it for validation, confirm the +running version and inspect `getblockchaininfo.bip9_softforks.pq_hybrid`. +Do not downgrade a validator to Core 4.8.0 after bit 12 is ACTIVE: it may +follow a different chain once a newly allowed block appears. + +## Mainnet signaling and block limits + +| Rule | Value | +|------|-------| +| Bit 12 STARTED threshold | Median time past reaches 2027-07-01 00:00 UTC (1814400000) at a window boundary | +| Timeout | 2029-01-01 00:00 UTC (1861920000), 18 months after start | +| Signaling threshold | At least 1714 of 2016 blocks in one STARTED window | +| Lock-in | One complete LOCKED_IN window follows a successful signaling window | +| Activation | ACTIVE begins at the following window boundary, at height `H12` | +| Block limit at `H12` | 12 MWU, with the contextual PQ witness discount | +| Later block limit | 16 MWU at `H12 + 525600` blocks, one nominal year at the 60-second target spacing | + +The start timestamp is not the date when the new rules take effect. BIP9 +uses chain median time past and full 2016-block windows. If the timeout +is reached before lock-in, bit 12 fails and these rules do not activate +through this deployment. The 16 MWU phase is height-based, not tied to +a calendar date. There is no automatic return to Core 4.8.0 validation +rules after activation. + +Before miners begin signaling, the complete shared testnet history must +be checked against the network-bound ML-DSA rule, the old and new binaries +must be retested on both sides of the 8 MWU boundary, and the final 4.8.1 +SHA must pass mandatory `make check`, Final Gate, and all five Build Raven +targets. Operators must have time to upgrade before ACTIVE. If these +conditions are not met before STARTED, a reviewed release must move the +start and timeout before the original window begins. + +## Migration to PQ custody + +Native RVN in historical classical outputs remains spendable with the +existing signature. Owners migrate voluntarily by generating a witness-v2 +PQ address with `getnewpqaddress` and sending RVN to it. Its later spend +uses ML-DSA-44 without an ECDSA signature. Existing classical outputs +are not converted by activation. + +The separate bit-13 mainnet deployment is scheduled to start no earlier +than 2029-01-01 00:00 UTC by median time past, with a 1714/2016 threshold +and 2030-07-01 timeout. It cannot enforce its PQ-only asset rule until +bit 12 and the transfer-script deployment on bit 8 are ACTIVE. Once all +three deployments are ACTIVE, newly created asset and owner-token outputs +use the PQ asset format: spending requires a matching native RVN +witness-v2 input signed with ML-DSA-44 and no classical asset signature. +Historical asset and owner-token outputs retain their classical spending +condition until their owners move them to new protected outputs. Activation +does not retroactively protect them. If bit 12 fails or activates too late, +the bit-13 window must be reviewed before it starts. + +For the asset format, wallet recovery routes, and current limitations in +automatic anchor refresh and raw transaction funding, see +[RIP-25 PQ Asset Extension Design Note](../RIP-0025-PQ-Assets-Followup.md). + +## Release qualification still required + +This draft does not assert that 4.8.1 artifacts, the complete testnet +history check, operator readiness, or the final source SHA have passed. +The release will be tagged and published only after those checks and +the owner's separate release approval. From 7c0d43c85550afc310a7a3a2e458fda3a274fc4f Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:57:44 +0200 Subject: [PATCH 072/195] [FINDING-067] Record bit 12 qualification status --- ...0025-v4.8-security-remediation-register.md | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 9f35aefc38..2aa3d58144 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4439,7 +4439,8 @@ passed with all nine focused functional tests. This commit follows the green CI pair at `9151946ad`, but the new SHA has not yet received its own Build Raven and Final Gate qualification. -FINDING-067 remains HIGH and OPEN. Its remaining release conditions are: +At the `1dfae5b21` checkpoint, FINDING-067 remained HIGH and OPEN. Its +then-remaining release conditions were: 1. Carry the owner-approved FINDING-160 incompatible network upgrade plan into the versioned release notes and operator instructions. The plan @@ -4513,6 +4514,35 @@ fully synchronized unpruned node or verified snapshot is available. The partial chain does not establish whether historical empty-context PQ signatures exist. Release condition 3 above remains OPEN. +**2026-10-13 bit-12 mainnet parameter decision and implementation:** The +owner selected 2027-07-01 00:00 UTC (`1814400000`) for STARTED and +2029-01-01 00:00 UTC (`1861920000`) for timeout, an 18-month window at +1714/2016. This supersedes the earlier approval of an 18-month timeout +measured from the provisional 2027-01-01 start. Commit `2df4d2d25` +sets the two mainnet parameters and adds +`versionbits_tests/pq_hybrid_mainnet_activation_test`. Before the code +change, the new test failed on both old timestamps, `1798761600` and +`1830297600`. After the change it passed, including median-time-past +STARTED, the 1713/1714 signal boundary, LOCKED_IN, ACTIVE, and timeout +FAILED transitions. The complete `versionbits_tests` and +`rip25_versionbits_tests` suites passed, as did local `make -j8 check`. +The bit-12 dates are now implemented locally, but this SHA has not yet +passed Build Raven and Final Gate. The bit-13 mainnet STARTED timestamp +equals the bit-12 timeout; this does not imply bit 12 has activated. +If bit 12 fails or activates too late for the asset rollout, the bit-13 +window must be reviewed in a separate release before it starts. Commit +`948b1ea20` records the approved schedule and operator plan in the +protocol and asset notes and adds draft versioned 4.8.1 release notes. + +FINDING-067 remains HIGH and OPEN. The outstanding gates are complete +shared-testnet history validation, final 4.8.1 source and artifact +qualification at one SHA with mandatory `make check`, a repeat of the +8 MWU old/new block boundary evidence on that SHA, final release notes +and checksums, operator readiness before bit-12 ACTIVE, and the separate +owner approval for the tag and release. The bit-12 parameter decision +itself is no longer an open gate. Versioned 4.8.1 release notes are a +review draft until the final SHA and artifacts are known. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical @@ -8369,8 +8399,9 @@ Cross-platform CI and public deployment remain separate release gates. chain outcomes, not only isolated weight functions. - **Remediation commits:** `73cbc93fd` documents the public upgrade plan; `3c3bf8385` removes residual claims that BIP9 signaling alone makes - the combined deployment compatible with Core 4.8.0. No consensus - limit was changed by these documentation commits. + the combined deployment compatible with Core 4.8.0; `948b1ea20` + records the chosen mainnet window in protocol and operator release + notes. No consensus limit was changed by these documentation commits. - **Verification:** Static comparison of the baseline 8 MWU constant and candidate contextual 12/16 MWU checks establishes the incompatible acceptance ranges. In a local two-binary regtest at height 540 after @@ -8389,8 +8420,10 @@ Cross-platform CI and public deployment remain separate release gates. phase-1 allowance. - **Final status:** ACCEPTED as an incompatible consensus upgrade with an owner-approved plan. The 4.8.1 release and public bit-12 activation - remain gated by FINDING-067: testnet history, finalized bit-12 timing, - final-SHA qualification, release instructions, and operator readiness. + remain gated by FINDING-067: testnet history, final-SHA qualification, + release instructions and checksums, and operator readiness. The + 2027-07-01 to 2029-01-01 mainnet bit-12 timing was implemented in + `2df4d2d25` after a red-then-green BIP9 parameter regression. ### FINDING-161: Pre-activation PQ-only lookalike output has no PQ custody From 7c7ef25b98070ecda21ee5c13ec6b0a57cc40bba Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:07:40 +0200 Subject: [PATCH 073/195] [FINDING-160] Clarify miner signaling scope --- doc/RIP-0025-PQ-Signatures.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-PQ-Signatures.md b/doc/RIP-0025-PQ-Signatures.md index b3b4650f9e..8b74ffdd2c 100644 --- a/doc/RIP-0025-PQ-Signatures.md +++ b/doc/RIP-0025-PQ-Signatures.md @@ -374,8 +374,8 @@ The owner approved this 18-month mainnet window on 2026-10-13. BIP9 uses median time past at window boundaries; the start timestamp is not an activation date. At least 1714 signals in one complete STARTED window lead to one LOCKED_IN window, then ACTIVE. If the timeout is reached -before lock-in, the deployment fails. The 85% threshold provides an -additional safety margin for this cryptographically significant upgrade. +before lock-in, the deployment fails. The 85% threshold measures miner +signaling only; it does not establish readiness of validating services. ### 6. Implementation From 01bb5161254abcccd86835a1bcd82259ecf1d169 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:48:39 +0200 Subject: [PATCH 074/195] [FINDING-169] Freeze functional cache CI failure --- ...0025-v4.8-security-remediation-register.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 2aa3d58144..7f8af8ee37 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4543,6 +4543,16 @@ owner approval for the tag and release. The bit-12 parameter decision itself is no longer an open gate. Versioned 4.8.1 release notes are a review draft until the final SHA and artifacts are known. +**2026-10-13 bit-12 CI checkpoint:** RIP-25 v4.8 Final Gate run +`38366157600` passed on exact SHA `7c0d43c85550afc310a7a3a2e458fda3a274fc4f`, +including its security tests and all eight build jobs. Paired Build Raven +run `38366146893` failed while creating the functional test cache in its +required behavioral security step. Its five artifact jobs were skipped. +The cause and remediation are tracked in FINDING-169. This is not a green +cross-platform qualification pair. Commit `7c7ef25b9` subsequently +clarified that miner signaling measures signaling only; it is documentation +only and is not included in those two runs. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical @@ -8781,3 +8791,39 @@ Cross-platform CI and public deployment remain separate release gates. and SHA. - **Final status:** FIXED on the qualified candidate SHA; final release-SHA qualification remains due, and FINDING-067 remains OPEN. + +### FINDING-169: Functional cache peer can remain at genesis during CI qualification + +- **Severity:** MEDIUM release qualification failure; the artifact gate fails closed. +- **Frozen initial status:** OPEN on 2026-10-13 at `7c0d43c85`, before a + cache connectivity remedy or diagnostic regression. +- **Affected RIP-25 invariant:** Required functional security tests must run + before release artifacts are accepted from a Build Raven run. +- **Affected Core 4.8.0 fix:** None. The failure occurs in the functional + test framework before an asset or PQ functional assertion runs. +- **Root cause:** Not established by the failed run. During cache creation, + node 7 remained at genesis while nodes 0 through 6 reached height 25. + The runner retained no node debug logs as CI artifacts, so the failed + peer connection or block acceptance cannot be distinguished from the + available evidence. +- **Affected file/function/lines:** `test/functional/test_framework/test_framework.py:385-417`, + cache node startup and synchronization; + `test/functional/test_framework/util.py:490-512`, block sync timeout; + `test/functional/test_runner.py:442-455`, cache failure reporting; + `.github/workflows/build-raven.yml:85-86`, required behavioral gate. +- **Concrete scenario:** Build Raven run `38366146893` on + `7c0d43c85550afc310a7a3a2e458fda3a274fc4f` failed in + `create_cache.py`: seven nodes had the same height-25 block hash, while + node 7 reported height 0 for the full 60-second wait. The five artifact + jobs were skipped. Final Gate run `38366157600` passed the behavioral + and required functional suites on the identical SHA. +- **Expected behavior:** Cache creation establishes usable peer connections + before mining, fails with enough node evidence to diagnose any persistent + disconnect, and never reports a green artifact gate if its tests did not run. +- **Regression required:** Reproduce a missing cache peer deterministically, + show the failure before remediation, then show cache creation recovering + or diagnosing it after remediation. Rerun mandatory `make check`, Build + Raven and Final Gate at one exact SHA. +- **Remediation commit:** Pending. +- **Final status:** OPEN; the passing Final Gate does not override the + failed Build Raven run. From e6d03388345dd83035c639f141ac7d14c3d91301 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:59:05 +0200 Subject: [PATCH 075/195] [FINDING-169] Reconnect cache peers before mining --- .../devtools/check-rip25-v48-invariants.sh | 1 + .../test_framework/test_cache_peers.py | 65 +++++++++++++++++++ .../test_framework/test_framework.py | 27 +++++++- 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 test/functional/test_framework/test_cache_peers.py diff --git a/contrib/devtools/check-rip25-v48-invariants.sh b/contrib/devtools/check-rip25-v48-invariants.sh index 79d4265574..3bd5ed63a9 100755 --- a/contrib/devtools/check-rip25-v48-invariants.sh +++ b/contrib/devtools/check-rip25-v48-invariants.sh @@ -804,6 +804,7 @@ echo "$fuzz_smoke_output" "$fuzz_binary" --pq-seed-tx | "$fuzz_binary" python3 contrib/devtools/test-required-functional-gate.py +PYTHONPATH=test/functional python3 -m unittest test_framework.test_cache_peers python3 test/functional/test_runner.py --require-tests --jobs=2 \ wallet_encryption_rewrite.py rpc_assettransfer.py feature_chainstate_ahead.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py diff --git a/test/functional/test_framework/test_cache_peers.py b/test/functional/test_framework/test_cache_peers.py new file mode 100644 index 0000000000..9650048253 --- /dev/null +++ b/test/functional/test_framework/test_cache_peers.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The Raven Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Check that cache generation connects every node before block sync.""" + +import logging +import os +import tempfile +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +from . import test_framework as framework_module +from .test_framework import RavenTestFramework +from .test_node import TestNode +from .util import MAX_NODES, PortSeed + + +class CacheFramework(RavenTestFramework): + def set_test_params(self): + self.num_nodes = 0 + + +class FirstSyncComplete(Exception): + pass + + +class CachePeerTest(unittest.TestCase): + def test_node_without_startup_peer_reaches_first_cache_tip(self): + with tempfile.TemporaryDirectory(prefix="cache_peer_test_") as tmpdir: + cache_dir = os.path.join(tmpdir, "cache") + os.mkdir(cache_dir) + framework = CacheFramework() + framework.options = SimpleNamespace(cachedir=cache_dir, tmpdir=tmpdir, coveragedir=None) + framework.log = logging.getLogger("CachePeerTest") + previous_port_seed = PortSeed.n + PortSeed.n = os.getpid() + + start_node = TestNode.start + sync_blocks = framework_module.sync_blocks + + def start_without_node7_peer(node, extra_args=None, stderr=None): + if node.index == MAX_NODES - 1: + node.args = [arg for arg in node.args if not arg.startswith("-connect=")] + return start_node(node, extra_args, stderr) + + def stop_after_first_sync(nodes): + sync_blocks(nodes, timeout=15) + raise FirstSyncComplete() + + try: + with patch.object(TestNode, "start", start_without_node7_peer), \ + patch.object(framework_module, "sync_blocks", stop_after_first_sync): + with self.assertRaises(FirstSyncComplete): + framework._initialize_chain() + self.assertEqual([node.getblockcount() for node in framework.nodes], [25] * MAX_NODES) + finally: + if framework.nodes: + framework.stop_nodes() + PortSeed.n = previous_port_seed + + +if __name__ == "__main__": + unittest.main() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 00718081c2..fc9ee5718d 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -398,6 +398,18 @@ def _initialize_chain(self): for node in self.nodes: node.wait_for_rpc_connection() + # RPC readiness does not ensure that the cache nodes have connected + # to node 0. Retry a missing peer before mining any blocks. + cache_peer = "127.0.0.1:" + str(p2p_port(0)) + for node in self.nodes[1:]: + deadline = time.monotonic() + 60 + while not any(peer['addr'] == cache_peer and peer['version'] > 0 + for peer in node.getpeerinfo()): + if time.monotonic() >= deadline: + raise AssertionError("Cache node {} did not connect to {}".format(node.index, cache_peer)) + node.addnode(cache_peer, "onetry") + time.sleep(0.5) + # Create a 200-block-long chain; each of the 4 first nodes # gets 25 mature blocks and 25 immature. # Note: To preserve compatibility with older versions of @@ -414,7 +426,20 @@ def _initialize_chain(self): self.nodes[peer].generate(1) block_time += 1 * 60 # Must sync before next peer starts generating blocks - sync_blocks(self.nodes) + try: + sync_blocks(self.nodes) + except AssertionError as e: + peer_states = [] + for node in self.nodes: + try: + peers = [{key: peer.get(key) for key in + ('addr', 'version', 'inbound', 'synced_headers', 'synced_blocks')} + for peer in node.getpeerinfo()] + except Exception as peer_error: + peers = "unavailable: {}".format(peer_error) + peer_states.append("node {}: {}".format(node.index, peers)) + raise AssertionError("{}\nCache peer state:\n {}".format( + e, "\n ".join(peer_states))) from e # Shut them down, and clean up cache directories: self.stop_nodes() From 4078257ee2862586eb7a0c07ed79b7ee4635f5f3 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:59:31 +0200 Subject: [PATCH 076/195] [FINDING-169] Record cache peer regression evidence --- ...0025-v4.8-security-remediation-register.md | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 7f8af8ee37..d20aa67ed9 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8801,11 +8801,12 @@ Cross-platform CI and public deployment remain separate release gates. before release artifacts are accepted from a Build Raven run. - **Affected Core 4.8.0 fix:** None. The failure occurs in the functional test framework before an asset or PQ functional assertion runs. -- **Root cause:** Not established by the failed run. During cache creation, - node 7 remained at genesis while nodes 0 through 6 reached height 25. - The runner retained no node debug logs as CI artifacts, so the failed - peer connection or block acceptance cannot be distinguished from the - available evidence. +- **Root cause:** The precise cause of the original CI event is not + established. Cache creation waited for RPC readiness but did not require + P2P connectivity before mining. During the failed run, node 7 remained + at genesis while nodes 0 through 6 reached height 25. The runner + retained no node debug logs as CI artifacts, so the original missing + connection or block acceptance cannot be distinguished from that log. - **Affected file/function/lines:** `test/functional/test_framework/test_framework.py:385-417`, cache node startup and synchronization; `test/functional/test_framework/util.py:490-512`, block sync timeout; @@ -8824,6 +8825,20 @@ Cross-platform CI and public deployment remain separate release gates. show the failure before remediation, then show cache creation recovering or diagnosing it after remediation. Rerun mandatory `make check`, Build Raven and Final Gate at one exact SHA. -- **Remediation commit:** Pending. -- **Final status:** OPEN; the passing Final Gate does not override the - failed Build Raven run. +- **Remediation commit:** `e6d033883` requires each cache follower to + establish a versioned peer connection to node 0 before mining, retries + a missing connection, and includes peer state when later block sync + fails. The real eight-node regression is part of both CI behavioral + gates through `check-rip25-v48-invariants.sh`. +- **Verification:** Before the repair, the regression withheld node 7's + startup connection and reproduced the observed height pattern: nodes + 0 through 6 reached 25, node 7 remained at genesis, and sync timed + out. After the repair the same eight-node test passed in 3.1 seconds; + a fresh complete `create_cache.py` run passed in 4.4 seconds. Local + `make -j8 check` passed. The full behavioral gate, including the new + regression and nine functional cases, passed under Python 3.10. The + original CI cause remains uncertain because its node logs were lost. +- **Final status:** FIXED locally for the reproduced missing-peer failure + mode. Build Raven and Final Gate must both pass on a single post-repair + SHA before CI qualification; the earlier passing Final Gate does not + override the failed Build Raven run. From 26726e6a85249a763c41dc157b25363a032479ed Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:08:15 +0200 Subject: [PATCH 077/195] [FINDING-067] Refresh release translation source --- src/qt/forms/mnemonicdialog2.ui | 2 +- src/qt/locale/raven_en.ts | 802 +++++++++++++++++++++----------- src/qt/ravenstrings.cpp | 54 ++- 3 files changed, 568 insertions(+), 290 deletions(-) diff --git a/src/qt/forms/mnemonicdialog2.ui b/src/qt/forms/mnemonicdialog2.ui index 211b56a437..b776c9fd56 100644 --- a/src/qt/forms/mnemonicdialog2.ui +++ b/src/qt/forms/mnemonicdialog2.ui @@ -209,7 +209,7 @@ A 12 word seed phrase is a useful way of backing up a wallet, because it is a system for generating the keys to all Ravencoin and Assets. However, anyone who finds/learns your seed phrase will likewise have control of your wallet. For this reason, some users choose to add another level of security to these 12 words. Unlike your 12 word phrase, which is generated randomly, this 13th word (known as the Seed Extension Word) is something the user comes up with themselves and memorizes, or stores separately from their seed phrase. - + The Seed Extension Word is like adding a password to your randomly generated seed phrase. If someone finds your written 12 word seed phrase, but you’ve chosen to use a Seed Extension Word, they will not be able to access your wallet. diff --git a/src/qt/locale/raven_en.ts b/src/qt/locale/raven_en.ts index 0a0eb91336..b7f1f1c1d3 100644 --- a/src/qt/locale/raven_en.ts +++ b/src/qt/locale/raven_en.ts @@ -127,7 +127,7 @@ AddressTableModel - + Label @@ -221,12 +221,12 @@ - + Wallet encrypted - + %1 will close now to finish the encryption process. Remember that encrypting your wallet cannot fully protect your ravens from being stolen by malware infecting your computer. @@ -236,20 +236,26 @@ - - + + + Wallet encryption failed - + + Wallet encryption failed after the live key state changed. The application will close; restart before using the wallet. + + + + Wallet encryption failed due to an internal error. Your wallet was not encrypted. - + The supplied passphrases do not match. @@ -472,13 +478,14 @@ - + + (no label) - + change from %1 (%2) @@ -575,7 +582,7 @@ - + Transaction Fee: @@ -905,7 +912,7 @@ - + Address already has the qualifier assigned to it @@ -1147,12 +1154,12 @@ - + Quantity: - + Bytes: @@ -1192,7 +1199,7 @@ - + Name: @@ -1286,6 +1293,11 @@ The ipfs/txid hash that is associated with the asset being created (e.g. QmU4h365LYMHx...) + + + Browse IPFS + + ERROR TEXT @@ -1393,7 +1405,7 @@ - + Copy quantity @@ -1433,7 +1445,7 @@ - + Main Asset @@ -1473,7 +1485,7 @@ - + IPFS/Txid Hash must start with 'Qm' and be 46 characters or Txid Hash must have 64 hex characters @@ -1483,24 +1495,19 @@ - + IPFS/Txid hash is not valid. Please use a valid IPFS/Txid hash - - - + + + Warning: Invalid Raven address - - Warning: Restricted Assets Reissuance requires an address - - - - + Valid Asset @@ -1515,7 +1522,23 @@ - + + Open IPFS content? + + + + + Open the following IPFS content in your default browser? + + + + + + Unable to create a protected asset destination + + + + %1 to %2 @@ -1778,7 +1801,7 @@ - + Address is already frozen @@ -1806,7 +1829,7 @@ GUIUtil::SyncWarningMessage - + Warning: transaction while syncing wallet! @@ -1985,7 +2008,7 @@ Recommended action: Fully sync your wallet before sending a transaction. MnemonicDialog - + HD Wallet Setup @@ -2047,7 +2070,7 @@ Recommended action: Fully sync your wallet before sending a transaction. - + BIP39 Compliant Seed Words: @@ -2063,32 +2086,34 @@ Recommended action: Fully sync your wallet before sending a transaction. - Passphrase: + Optional Seed Extension Word: - - Enter a Passphrase to protect your seed words (optional). + + You may enter an optional extension word to protect your seed. - - You may enter an optional Passphrase to protect your seed words. + + Please write down your 12 seed words and the optional seed extension word before accepting. +They are not recoverable!! - + Warning: - - Please write down your 12 seed words and Passphrase before Accepting. -They are not recoverable !! + + A 12 word seed phrase is a useful way of backing up a wallet, because it is a system for generating the keys to all Ravencoin and Assets. However, anyone who finds/learns your seed phrase will likewise have control of your wallet. For this reason, some users choose to add another level of security to these 12 words. Unlike your 12 word phrase, which is generated randomly, this 13th word (known as the Seed Extension Word) is something the user comes up with themselves and memorizes, or stores separately from their seed phrase. + + The Seed Extension Word is like adding a password to your randomly generated seed phrase. If someone finds your written 12 word seed phrase, but you’ve chosen to use a Seed Extension Word, they will not be able to access your wallet. - + Accept @@ -2098,7 +2123,7 @@ They are not recoverable !! - + Words are not valid, please generate new words and try again @@ -2111,7 +2136,7 @@ They are not recoverable !! - + BIP39 Compliant Seed Words: @@ -2122,32 +2147,32 @@ They are not recoverable !! - Passphrase: + Seed Extension Word: - You will need the Passphrase if your previous wallet used one. + The Seed Extension Word is like adding a password to your randomly generated seed phrase. If someone finds your written 12 word seed phrase, but you’ve chosen to use a Seed Extension Word, they will not be able to access your wallet. - Enter the Passphrase from your previous wallet if it used one. + Enter the Seed Extension Word from your previous wallet (if it used one). - - Warning: + + Please continue to remember your 12 seed words and extension word before accepting. +They are not recoverable!! - - Please continue to remember your 12 seed words and Passphrase before Accepting. -They are not recoverable !! + + Warning: - + Accept @@ -2157,8 +2182,13 @@ They are not recoverable !! - - Words are not valid, please check the words and try again + + Words are not valid, please check the words and the language, and try again. + + + + + In Japanese, please use standard space, ideographic japanese space is not supported. @@ -2231,7 +2261,7 @@ They are not recoverable !! MyRestrictedAssetsTableModel - + Date Date @@ -2715,19 +2745,19 @@ They are not recoverable !! OverviewPage - + Form Form - - - + + + The displayed information may be out of date. Your wallet automatically synchronizes with the Raven network after a connection is established, but this process has not completed yet. The displayed information may be out of date. Your wallet automatically synchronizes with the Raven network after a connection is established, but this process has not completed yet. - + Watch-only: @@ -2772,12 +2802,12 @@ They are not recoverable !! Your current total balance - + RVN Balances - + Your current balance in watch-only addresses @@ -2787,7 +2817,7 @@ They are not recoverable !! - + Asset Balances @@ -2797,12 +2827,12 @@ They are not recoverable !! - + Recent transactions - + Unconfirmed transactions to watch-only addresses @@ -3035,12 +3065,17 @@ They are not recoverable !! Amount - + Enter a Raven address (e.g. %1) - + + Enter an asset destination (PQ-only after activation) + + + + %1 d @@ -3150,7 +3185,7 @@ They are not recoverable !! - + %1 didn't yet exit safely... @@ -3159,6 +3194,11 @@ They are not recoverable !! unknown + + + Authority return requires a canonical PQ-only asset destination + + QObject::QObject @@ -3710,13 +3750,7 @@ They are not recoverable !! - - - Create new main/sub/unique assets - - - - + &Transfer Assets @@ -4065,7 +4099,13 @@ They are not recoverable !! Up to date - + + + Create new assets + + + + Show the %1 help message to get a list with possible Raven command-line options @@ -4213,7 +4253,19 @@ They are not recoverable !! - + + Create or display a protected destination for receiving assets. The PQ part must hold RVN before the received assets can be spent. + + + + + + + Receive PQ asset + + + + Clear all fields of the form. @@ -4228,12 +4280,12 @@ They are not recoverable !! - + &Request payment - + Show the selected request (does the same as double clicking an entry) @@ -4253,7 +4305,7 @@ They are not recoverable !! - + Copy URI @@ -4272,6 +4324,47 @@ They are not recoverable !! Copy amount + + + + PQ asset destination + + + + + Protected asset destinations are unavailable before PQ asset activation. + + + + + Create a new protected asset destination + + + + + Protected asset destination: + + + + + The protected asset destination could not be created or validated. + + + + + Share this complete protected asset destination: + + + + + To spend assets received here, first fund the PQ address with RVN: %1 + + + + + Copy asset destination + + ReceiveRequestDialog @@ -4382,7 +4475,7 @@ They are not recoverable !! ReissueAssetDialog - + Coin Control Features @@ -4403,12 +4496,12 @@ They are not recoverable !! - + Quantity: - + Bytes: @@ -4448,18 +4541,18 @@ They are not recoverable !! - - + + Reissue Asset - + Select an asset to reissue: - + Address: @@ -4469,7 +4562,7 @@ They are not recoverable !! - + Verifier String: @@ -4484,7 +4577,7 @@ They are not recoverable !! - + The number of assets that will be created @@ -4499,7 +4592,7 @@ They are not recoverable !! - + If the owner of this asset will be able to issue more assets in the future @@ -4524,17 +4617,22 @@ They are not recoverable !! - + + Browse IPFS + + + + ERROR TEXT - + Current Asset Settings - + Updated Asset Settings @@ -4620,7 +4718,7 @@ They are not recoverable !! - + Clear @@ -4635,7 +4733,7 @@ They are not recoverable !! - + Copy quantity @@ -4690,7 +4788,7 @@ They are not recoverable !! - + Asset data couldn't be found @@ -4705,18 +4803,13 @@ They are not recoverable !! - - Warning: Restricted Assets Issuance requires an address - - - - - + + Warning: Invalid Raven address - + Yes @@ -4826,7 +4919,23 @@ They are not recoverable !! - + + Open IPFS content? + + + + + Open the following IPFS content in your default browser? + + + + + + Unable to create a protected asset destination + + + + %1 to %2 @@ -4934,13 +5043,13 @@ They are not recoverable !! Balance: - - - Failed to create a change address + + + Invalid authority return - + Failed to generate the correct transaction. Please try again @@ -4966,24 +5075,24 @@ They are not recoverable !! - + added as transaction fee - - + + Total Amount %1 - - + + or - + Confirm adding restriction @@ -4993,7 +5102,7 @@ They are not recoverable !! - + Adding qualifier <b>%1</b> to address <b>%2</b><br> @@ -5127,7 +5236,7 @@ They are not recoverable !! - + Enter a label for this address to add it to your address book @@ -5138,12 +5247,32 @@ They are not recoverable !! - + Select an asset to transfer - + + PQ asset destination + + + + + No saved PQ-only asset destination is available. Paste the recipient's complete PQ-only destination. + + + + + Choose a protected asset destination + + + + + Owned destinations (paste an external recipient in the address field): + + + + Memos can only be added once RIP5 is voted in @@ -5153,7 +5282,7 @@ They are not recoverable !! - + Failed to get asset metadata for: @@ -5257,7 +5386,7 @@ They are not recoverable !! - + Transaction Fee: @@ -5348,12 +5477,12 @@ They are not recoverable !! - + Dust: - + Confirmation time target: @@ -5883,11 +6012,6 @@ They are not recoverable !! conflicted with a transaction with %1 confirmations - - - %1/offline - - 0/unconfirmed, %1 @@ -5919,34 +6043,19 @@ They are not recoverable !! - - + + Status - - - , has not been successfully broadcast yet - - - - - - , broadcast through %n node(s) - - , broadcast through %n node - , broadcast through %n nodes - - - - - + + Date Date - + Source @@ -5959,58 +6068,58 @@ They are not recoverable !! - - + + From - - + + unknown - + - - + + To - - + + own address - + - + watch-only - - + + label - + - - + + Credit - + matures in %n more block(s) matures in %n more block @@ -6025,14 +6134,14 @@ They are not recoverable !! - - + + Debit - + Total debit @@ -6054,44 +6163,44 @@ They are not recoverable !! - + Message - - + + Comment - - + + Transaction ID - - + + Transaction total size - - + + Output index - - + + Merchant - - + + Generated coins must mature %1 blocks before they can be spent. When you generated this block, it was broadcast to the network to be added to the block chain. If it fails to get into the chain, its state will change to "not accepted" and it won't be spendable. This may occasionally happen if another node generates a block within a few seconds of yours. @@ -6101,7 +6210,7 @@ They are not recoverable !! - + Debug information @@ -6186,11 +6295,6 @@ They are not recoverable !! Open until %1 - - - Offline - - Unconfirmed @@ -6221,11 +6325,6 @@ They are not recoverable !! Immature (%1 confirmations, will be available after %2) - - - This block was not received by any other nodes and will probably not be accepted! - - Generated but not accepted @@ -6277,17 +6376,27 @@ They are not recoverable !! - + + Atomic Swap + + + + + Executed Atomic Swap + + + + watch-only - + (n/a) - + (no label) @@ -6560,6 +6669,21 @@ They are not recoverable !! to + + + Traded Away + + + + + Sold + + + + + Bought + + UnitDisplayStatusBarControl @@ -6580,17 +6704,17 @@ They are not recoverable !! WalletModel - + Send Coins Send Coins - + Error: Wallet doesn't have 12 words. Only new wallets generated by the mnemonic phrase will have 12 words - + Error: Wallet locked @@ -6649,7 +6773,7 @@ They are not recoverable !! - Recovery information + Recovery information. (Will close after 5 min) @@ -6658,7 +6782,7 @@ They are not recoverable !! - + This wallet is not a HD wallet, words not supported. @@ -6666,42 +6790,42 @@ They are not recoverable !! raven-core - + Options: Options: - + Specify data directory Specify data directory - + Connect to a node to retrieve peer addresses, and disconnect Connect to a node to retrieve peer addresses, and disconnect - + Specify your own public address Specify your own public address - + Accept command line and JSON-RPC commands Accept command line and JSON-RPC commands - + Distributed under the MIT software license, see the accompanying file %s or %s - + If <category> is not supplied or if <category> = 1, output all debugging information. - + Prune configured below the minimum of %d MiB. Please use a higher number. @@ -6716,7 +6840,7 @@ They are not recoverable !! - + Error: A fatal internal error occurred, see debug.log for details @@ -6731,17 +6855,17 @@ They are not recoverable !! - + Run in the background as a daemon and accept commands Run in the background as a daemon and accept commands - + Unable to start HTTP server. See debug log for details. - + Raven Core Raven Core @@ -6755,11 +6879,21 @@ They are not recoverable !! A fee rate (in %s/kB) that will be used when fee estimation has insufficient data (default: %s) + + + A space separated list of 12 words used to import a BIP44 wallet. Command-line arguments can be visible to other local users and process tools. + + Accept relayed transactions received from whitelisted peers even when not relaying transactions (default: %d) + + + Active PQ asset change requires a protected source or explicit PQ change destination + + Add a node to connect to and attempt to keep the connection open (see the `addnode` RPC command help for more info) @@ -6767,6 +6901,26 @@ They are not recoverable !! + Asset change requires a legacy P2PKH address; RIP-25 witness-v2 protects native RVN only + + + + + Asset database state is not certified and required block data is unavailable. Retain the data directory and redownload the blockchain. + + + + + Asset database state is not certified and the chainstate tip is unknown. Retain the data directory and rebuild the block index before retrying. + + + + + Asset database state is not certified at the coins tip. This pruned chainstate cannot be rebuilt safely; retain the data directory and redownload the blockchain. + + + + Bind to given address and always listen on it. Use [host]:port notation for IPv6 Bind to given address and always listen on it. Use [host]:port notation for IPv6 @@ -6800,6 +6954,11 @@ They are not recoverable !! Error reading %s! All keys read correctly, but transaction data or address book entries might be missing or incorrect. + + + Error rescanning wallet: block %d could not be read. Restore the missing block data or restart with -reindex. + + Exclude debugging information for a category. Can be used in conjunction with -debug=1 to output debug logs for all categories except one or more specified categories. @@ -6832,6 +6991,11 @@ They are not recoverable !! + Passphrase securing your 12-word mnemonic word list. Command-line arguments and configuration files can retain plaintext secrets. + + + + Please check that your computer's date and time are correct! If your clock is wrong, %s will not work properly. @@ -6897,6 +7061,16 @@ They are not recoverable !! + Wallet encryption recovery could not complete: restart %s after resolving the database error + + + + + Wallet salvage retained the original %s as %s. The retained file may contain recoverable unencrypted private-key material even if the recovered wallet is encrypted. Protect it and remove it securely only after verifying recovery. + + + + Wallet will not create transactions that violate mempool chain limits (default: %u) @@ -6936,17 +7110,42 @@ They are not recoverable !! - + Accept connections from outside (default: 1 if no -proxy or -connect) - + + Active PQ asset change requires a canonical PQ-only destination + + + + + Active PQ asset input has no canonical program + + + + + Active PQ asset output requires a canonical PQ-only asset destination + + + + + Active PQ asset recipient is missing a canonical program + + + + Append comment to the user agent string - + + Asset database requires a full chainstate rebuild + + + + Attempt to recover private keys from a corrupt wallet on startup @@ -6985,6 +7184,11 @@ They are not recoverable !! Corrupted block database detected Corrupted block database detected + + + Could not construct canonical PQ asset change + + Debugging/Testing options: @@ -7061,7 +7265,22 @@ They are not recoverable !! Error opening block database - + + Error storing bip 39 passphrase + + + + + Error storing bip 39 vchseed + + + + + Error storing bip 39 words + + + + Error: Disk space is low! Error: Disk space is low! @@ -7131,7 +7350,7 @@ They are not recoverable !! - + Print this help message and exit @@ -7140,6 +7359,11 @@ They are not recoverable !! Print version and exit + + + Protected asset input requires a funded matching PQ anchor + + Prune cannot be configured with a negative value. @@ -7151,7 +7375,12 @@ They are not recoverable !! - + + RIP-25 chainstate requires context-aware revalidation + + + + Rebuild chain state and block index from the blk*.dat files on disk @@ -7171,7 +7400,12 @@ They are not recoverable !! - + + Selected asset input is missing from the wallet + + + + Set database cache size in megabytes (%d to %d, default: %d) @@ -7191,12 +7425,27 @@ They are not recoverable !! - + + Unable to begin the initial wallet transaction + + + + Unable to bind to %s on this computer. %s is probably already running. - + + Unable to commit the initial wallet transaction + + + + + Unable to persist the wallet feature version + + + + Unsupported argument -benchmark ignored, use -debug=bench. @@ -7236,7 +7485,12 @@ They are not recoverable !! - + + Verifier string for restricted asset change not found + + + + Verifying blocks... Verifying blocks... @@ -7261,12 +7515,12 @@ They are not recoverable !! - + Allow JSON-RPC connections from specified source. Valid for <ip> are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0) or a network/CIDR (e.g. 1.2.3.4/24). This option can be specified multiple times - + Bind to given address and whitelist peers connecting to it. Use [host]:port notation for IPv6 @@ -7281,7 +7535,7 @@ They are not recoverable !! - + Error: Listening for incoming connections failed (listen returned error %s) @@ -7311,7 +7565,7 @@ They are not recoverable !! - + Randomize credentials for every proxy connection. This enables Tor stream isolation (default: %u) @@ -7321,7 +7575,7 @@ They are not recoverable !! - + Whitelisted peers cannot be DoS banned and their transactions are always relayed, even if they are already in the mempool, useful e.g. for a gateway @@ -7336,12 +7590,12 @@ They are not recoverable !! - + Accept public REST requests (default: %u) - + Automatically create Tor hidden service (default: %d) @@ -7351,7 +7605,7 @@ They are not recoverable !! - + Error loading %s: You can't disable HD on an already existing HD wallet @@ -7361,12 +7615,12 @@ They are not recoverable !! - + Error upgrading chainstate database - + Imports blocks from external blk000??.dat file on startup @@ -7411,7 +7665,7 @@ They are not recoverable !! - + RPC server options: @@ -7426,7 +7680,7 @@ They are not recoverable !! - + Send trace/debug info to console instead of debug.log file Send trace/debug info to console instead of debug.log file @@ -7481,12 +7735,12 @@ They are not recoverable !! Transaction too large - + Unable to bind to %s on this computer (bind returned error %s) - + Upgrade wallet to latest format on startup @@ -7526,7 +7780,7 @@ They are not recoverable !! - + Verifier string is empty @@ -7542,6 +7796,11 @@ They are not recoverable !! + Wallet salvage for %s did not report the retained original file. + + + + Warning Warning @@ -7571,22 +7830,22 @@ They are not recoverable !! - + Password for JSON-RPC connections Password for JSON-RPC connections - + Execute command when the best block changes (%s in cmd is replaced by block hash) Execute command when the best block changes (%s in cmd is replaced by block hash) - + Allow DNS lookups for -addnode, -seednode and -connect Allow DNS lookups for -addnode, -seednode and -connect - + (1 = keep tx meta data e.g. account owner and payment request information, 2 = drop tx meta data) @@ -7596,7 +7855,7 @@ They are not recoverable !! - + Bind to given address to listen for JSON-RPC connections. This option is ignored unless -rpcallowip is also passed. Port is optional and overrides -rpcport. Use [host]:port notation for IPv6. This option can be specified multiple times (default: 127.0.0.1 and ::1 i.e., localhost, or if -rpcallowip has been specified, 0.0.0.0 and :: i.e., all addresses) @@ -7621,7 +7880,7 @@ They are not recoverable !! - + Fees (in %s/kB) smaller than this are considered zero fee for transaction creation (default: %s) @@ -7696,7 +7955,7 @@ They are not recoverable !! - + Set maximum BIP141 block weight to this * 4. Deprecated, use blockmaxweight @@ -7801,7 +8060,7 @@ They are not recoverable !! - + Warning: Unknown block versions being mined! It's possible unknown rules are in effect @@ -7881,12 +8140,7 @@ They are not recoverable !! - - A space separated list of 12-words used to import a bip44 wallet - - - - + Always query for peer addresses via DNS lookup (default: %u) @@ -7896,7 +8150,7 @@ They are not recoverable !! - + Asset doesn't exist: @@ -7916,7 +8170,7 @@ They are not recoverable !! - + Done Loading @@ -7951,22 +8205,7 @@ They are not recoverable !! - - Error writing bip 39 passphrase to database - - - - - Error writing bip 39 vchseed to database - - - - - Error writing bip 39 words to database - - - - + Every '(' must have a corresponding ')' in the expression: @@ -8141,17 +8380,12 @@ They are not recoverable !! - - Passphrase securing your 12-word mnemonic word-list - - - - + Prepend debug output with timestamp (default: %u) - + Relay and mine data carrier transactions (default: %u) @@ -8166,7 +8400,7 @@ They are not recoverable !! - + Send transactions with full-RBF opt-in enabled (default: %u) @@ -8271,7 +8505,7 @@ They are not recoverable !! - + Unable to generate initial keys @@ -8281,7 +8515,7 @@ They are not recoverable !! - + Unable to reissue asset: amount must be 0 or larger @@ -8311,7 +8545,7 @@ They are not recoverable !! Unknown network specified in -onlynet: '%s' - + Insufficient funds Insufficient funds @@ -8326,17 +8560,17 @@ They are not recoverable !! Loading wallet... - + Cannot downgrade wallet Cannot downgrade wallet - + Rescanning... Rescanning... - + Error Error diff --git a/src/qt/ravenstrings.cpp b/src/qt/ravenstrings.cpp index bfc2850bac..3eada1ffba 100644 --- a/src/qt/ravenstrings.cpp +++ b/src/qt/ravenstrings.cpp @@ -21,9 +21,15 @@ QT_TRANSLATE_NOOP("raven-core", "" "A fee rate (in %s/kB) that will be used when fee estimation has insufficient " "data (default: %s)"), QT_TRANSLATE_NOOP("raven-core", "" +"A space separated list of 12 words used to import a BIP44 wallet. Command-" +"line arguments can be visible to other local users and process tools."), +QT_TRANSLATE_NOOP("raven-core", "" "Accept relayed transactions received from whitelisted peers even when not " "relaying transactions (default: %d)"), QT_TRANSLATE_NOOP("raven-core", "" +"Active PQ asset change requires a protected source or explicit PQ change " +"destination"), +QT_TRANSLATE_NOOP("raven-core", "" "Add a node to connect to and attempt to keep the connection open (see the " "`addnode` RPC command help for more info)"), QT_TRANSLATE_NOOP("raven-core", "" @@ -31,6 +37,19 @@ QT_TRANSLATE_NOOP("raven-core", "" "single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0) or " "a network/CIDR (e.g. 1.2.3.4/24). This option can be specified multiple times"), QT_TRANSLATE_NOOP("raven-core", "" +"Asset change requires a legacy P2PKH address; RIP-25 witness-v2 protects " +"native RVN only"), +QT_TRANSLATE_NOOP("raven-core", "" +"Asset database state is not certified and required block data is " +"unavailable. Retain the data directory and redownload the blockchain."), +QT_TRANSLATE_NOOP("raven-core", "" +"Asset database state is not certified and the chainstate tip is unknown. " +"Retain the data directory and rebuild the block index before retrying."), +QT_TRANSLATE_NOOP("raven-core", "" +"Asset database state is not certified at the coins tip. This pruned " +"chainstate cannot be rebuilt safely; retain the data directory and " +"redownload the blockchain."), +QT_TRANSLATE_NOOP("raven-core", "" "Bind to given address and always listen on it. Use [host]:port notation for " "IPv6"), QT_TRANSLATE_NOOP("raven-core", "" @@ -78,6 +97,9 @@ QT_TRANSLATE_NOOP("raven-core", "" "Error reading %s! All keys read correctly, but transaction data or address " "book entries might be missing or incorrect."), QT_TRANSLATE_NOOP("raven-core", "" +"Error rescanning wallet: block %d could not be read. Restore the missing " +"block data or restart with -reindex."), +QT_TRANSLATE_NOOP("raven-core", "" "Error: Listening for incoming connections failed (listen returned error %s)"), QT_TRANSLATE_NOOP("raven-core", "" "Exclude debugging information for a category. Can be used in conjunction " @@ -164,6 +186,9 @@ QT_TRANSLATE_NOOP("raven-core", "" QT_TRANSLATE_NOOP("raven-core", "" "Output debugging information (default: %u, supplying is optional)"), QT_TRANSLATE_NOOP("raven-core", "" +"Passphrase securing your 12-word mnemonic word list. Command-line arguments " +"and configuration files can retain plaintext secrets."), +QT_TRANSLATE_NOOP("raven-core", "" "Please check that your computer's date and time are correct! If your clock " "is wrong, %s will not work properly."), QT_TRANSLATE_NOOP("raven-core", "" @@ -292,6 +317,13 @@ QT_TRANSLATE_NOOP("raven-core", "" "Verifier string has length greater than 80 after whitespaces and '#' are " "removed"), QT_TRANSLATE_NOOP("raven-core", "" +"Wallet encryption recovery could not complete: restart %s after resolving " +"the database error"), +QT_TRANSLATE_NOOP("raven-core", "" +"Wallet salvage retained the original %s as %s. The retained file may contain " +"recoverable unencrypted private-key material even if the recovered wallet is " +"encrypted. Protect it and remove it securely only after verifying recovery."), +QT_TRANSLATE_NOOP("raven-core", "" "Wallet will not create transactions that violate mempool chain limits " "(default: %u)"), QT_TRANSLATE_NOOP("raven-core", "" @@ -350,14 +382,18 @@ QT_TRANSLATE_NOOP("raven-core", "(default: %s)"), QT_TRANSLATE_NOOP("raven-core", "(default: %u)"), QT_TRANSLATE_NOOP("raven-core", "-maxmempool must be at least %d MB"), QT_TRANSLATE_NOOP("raven-core", " can be:"), -QT_TRANSLATE_NOOP("raven-core", "A space separated list of 12-words used to import a bip44 wallet"), QT_TRANSLATE_NOOP("raven-core", "Accept command line and JSON-RPC commands"), QT_TRANSLATE_NOOP("raven-core", "Accept connections from outside (default: 1 if no -proxy or -connect)"), QT_TRANSLATE_NOOP("raven-core", "Accept public REST requests (default: %u)"), +QT_TRANSLATE_NOOP("raven-core", "Active PQ asset change requires a canonical PQ-only destination"), +QT_TRANSLATE_NOOP("raven-core", "Active PQ asset input has no canonical program"), +QT_TRANSLATE_NOOP("raven-core", "Active PQ asset output requires a canonical PQ-only asset destination"), +QT_TRANSLATE_NOOP("raven-core", "Active PQ asset recipient is missing a canonical program"), QT_TRANSLATE_NOOP("raven-core", "Allow DNS lookups for -addnode, -seednode and -connect"), QT_TRANSLATE_NOOP("raven-core", "Always query for peer addresses via DNS lookup (default: %u)"), QT_TRANSLATE_NOOP("raven-core", "Append comment to the user agent string"), QT_TRANSLATE_NOOP("raven-core", "Asset Transfer amounts must be greater than 0"), +QT_TRANSLATE_NOOP("raven-core", "Asset database requires a full chainstate rebuild"), QT_TRANSLATE_NOOP("raven-core", "Asset doesn't exist: "), QT_TRANSLATE_NOOP("raven-core", "Asset must be a qualifier, sub qualifier, or a restricted asset"), QT_TRANSLATE_NOOP("raven-core", "Asset name is not valid"), @@ -374,6 +410,7 @@ QT_TRANSLATE_NOOP("raven-core", "Connect to a node to retrieve peer addresses, a QT_TRANSLATE_NOOP("raven-core", "Connection options:"), QT_TRANSLATE_NOOP("raven-core", "Copyright (C) %i-%i"), QT_TRANSLATE_NOOP("raven-core", "Corrupted block database detected"), +QT_TRANSLATE_NOOP("raven-core", "Could not construct canonical PQ asset change"), QT_TRANSLATE_NOOP("raven-core", "Debugging/Testing options:"), QT_TRANSLATE_NOOP("raven-core", "Do not load the wallet and disable wallet RPC calls"), QT_TRANSLATE_NOOP("raven-core", "Do you want to rebuild the block database now?"), @@ -398,10 +435,10 @@ QT_TRANSLATE_NOOP("raven-core", "Error loading wallet %s. Invalid characters in QT_TRANSLATE_NOOP("raven-core", "Error not set"), QT_TRANSLATE_NOOP("raven-core", "Error opening block database"), QT_TRANSLATE_NOOP("raven-core", "Error reading from database, shutting down."), +QT_TRANSLATE_NOOP("raven-core", "Error storing bip 39 passphrase"), +QT_TRANSLATE_NOOP("raven-core", "Error storing bip 39 vchseed"), +QT_TRANSLATE_NOOP("raven-core", "Error storing bip 39 words"), QT_TRANSLATE_NOOP("raven-core", "Error upgrading chainstate database"), -QT_TRANSLATE_NOOP("raven-core", "Error writing bip 39 passphrase to database"), -QT_TRANSLATE_NOOP("raven-core", "Error writing bip 39 vchseed to database"), -QT_TRANSLATE_NOOP("raven-core", "Error writing bip 39 words to database"), QT_TRANSLATE_NOOP("raven-core", "Error"), QT_TRANSLATE_NOOP("raven-core", "Error: A fatal internal error occurred, see debug.log for details"), QT_TRANSLATE_NOOP("raven-core", "Error: Disk space is low!"), @@ -467,14 +504,15 @@ QT_TRANSLATE_NOOP("raven-core", "Node relay options:"), QT_TRANSLATE_NOOP("raven-core", "Not enough file descriptors available."), QT_TRANSLATE_NOOP("raven-core", "Only connect to nodes in network (ipv4, ipv6 or onion)"), QT_TRANSLATE_NOOP("raven-core", "Options:"), -QT_TRANSLATE_NOOP("raven-core", "Passphrase securing your 12-word mnemonic word-list"), QT_TRANSLATE_NOOP("raven-core", "Password for JSON-RPC connections"), QT_TRANSLATE_NOOP("raven-core", "Prepend debug output with timestamp (default: %u)"), QT_TRANSLATE_NOOP("raven-core", "Print this help message and exit"), QT_TRANSLATE_NOOP("raven-core", "Print version and exit"), +QT_TRANSLATE_NOOP("raven-core", "Protected asset input requires a funded matching PQ anchor"), QT_TRANSLATE_NOOP("raven-core", "Prune cannot be configured with a negative value."), QT_TRANSLATE_NOOP("raven-core", "Prune mode is incompatible with -txindex."), QT_TRANSLATE_NOOP("raven-core", "Pruning blockstore..."), +QT_TRANSLATE_NOOP("raven-core", "RIP-25 chainstate requires context-aware revalidation"), QT_TRANSLATE_NOOP("raven-core", "RPC server options:"), QT_TRANSLATE_NOOP("raven-core", "Rebuild chain state and block index from the blk*.dat files on disk"), QT_TRANSLATE_NOOP("raven-core", "Rebuild chain state from the currently indexed blocks"), @@ -487,6 +525,7 @@ QT_TRANSLATE_NOOP("raven-core", "Rescanning..."), QT_TRANSLATE_NOOP("raven-core", "Restricted asset transfer from address that has been frozen"), QT_TRANSLATE_NOOP("raven-core", "Rewinding blocks..."), QT_TRANSLATE_NOOP("raven-core", "Run in the background as a daemon and accept commands"), +QT_TRANSLATE_NOOP("raven-core", "Selected asset input is missing from the wallet"), QT_TRANSLATE_NOOP("raven-core", "Send trace/debug info to console instead of debug.log file"), QT_TRANSLATE_NOOP("raven-core", "Send transactions with full-RBF opt-in enabled (default: %u)"), QT_TRANSLATE_NOOP("raven-core", "Set database cache size in megabytes (%d to %d, default: %d)"), @@ -525,10 +564,13 @@ QT_TRANSLATE_NOOP("raven-core", "Transaction must have at least one recipient"), QT_TRANSLATE_NOOP("raven-core", "Transaction too large for fee policy"), QT_TRANSLATE_NOOP("raven-core", "Transaction too large"), QT_TRANSLATE_NOOP("raven-core", "Turn off the databasing the messages sent with assets (default: %u)"), +QT_TRANSLATE_NOOP("raven-core", "Unable to begin the initial wallet transaction"), QT_TRANSLATE_NOOP("raven-core", "Unable to bind to %s on this computer (bind returned error %s)"), QT_TRANSLATE_NOOP("raven-core", "Unable to bind to %s on this computer. %s is probably already running."), +QT_TRANSLATE_NOOP("raven-core", "Unable to commit the initial wallet transaction"), QT_TRANSLATE_NOOP("raven-core", "Unable to generate initial keys"), QT_TRANSLATE_NOOP("raven-core", "Unable to get coin to verify restricted asset transfer from address"), +QT_TRANSLATE_NOOP("raven-core", "Unable to persist the wallet feature version"), QT_TRANSLATE_NOOP("raven-core", "Unable to reissue asset: amount must be 0 or larger"), QT_TRANSLATE_NOOP("raven-core", "Unable to reissue asset: asset_name '"), QT_TRANSLATE_NOOP("raven-core", "Unable to reissue asset: reissuable is set to false"), @@ -552,6 +594,7 @@ QT_TRANSLATE_NOOP("raven-core", "Verifier String doesn't exist for asset: "), QT_TRANSLATE_NOOP("raven-core", "Verifier String for asset trasnfer, not found"), QT_TRANSLATE_NOOP("raven-core", "Verifier not found for asset: "), QT_TRANSLATE_NOOP("raven-core", "Verifier string can not be empty. To default to true, use \"true\""), +QT_TRANSLATE_NOOP("raven-core", "Verifier string for restricted asset change not found"), QT_TRANSLATE_NOOP("raven-core", "Verifier string is empty"), QT_TRANSLATE_NOOP("raven-core", "Verifier string not found"), QT_TRANSLATE_NOOP("raven-core", "Verifying blocks..."), @@ -560,6 +603,7 @@ QT_TRANSLATE_NOOP("raven-core", "Wallet %s resides outside data directory %s"), QT_TRANSLATE_NOOP("raven-core", "Wallet debugging/testing options:"), QT_TRANSLATE_NOOP("raven-core", "Wallet needed to be rewritten: restart %s to complete"), QT_TRANSLATE_NOOP("raven-core", "Wallet options:"), +QT_TRANSLATE_NOOP("raven-core", "Wallet salvage for %s did not report the retained original file."), QT_TRANSLATE_NOOP("raven-core", "Warning"), QT_TRANSLATE_NOOP("raven-core", "Warning: unknown new rules activated (versionbit %i)"), QT_TRANSLATE_NOOP("raven-core", "Whether to operate in a blocks only mode (default: %u)"), From 21005a72eea5da6fc78b1b63904a83ee7ebd7a79 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:08:43 +0200 Subject: [PATCH 078/195] [FINDING-067] Record translation source verification --- doc/RIP-0025-v4.8-security-remediation-register.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index d20aa67ed9..4ae2d5da2c 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4553,6 +4553,14 @@ cross-platform qualification pair. Commit `7c7ef25b9` subsequently clarified that miner signaling measures signaling only; it is documentation only and is not included in those two runs. +**2026-10-13 release translation preparation:** Commit `26726e6a8` +regenerates the English Qt source catalog and extracted core strings +from the current 4.8.1 candidate, using GNU gettext 0.21 and the pinned +Qt 5.12.11 `lupdate`. The pinned `lrelease` accepted the updated catalog; +local `make -j8 check` passed. This is an English source refresh, not a +pull of translated language catalogs from Transifex. The latter remains +a release-process step because the `tx` client is not installed here. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical From d45aa5b4451a90b844126a659f47255c1a351365 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:14:28 +0200 Subject: [PATCH 079/195] [FINDING-170] Resolve cache test binary from source tree --- test/functional/test_framework/test_cache_peers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/functional/test_framework/test_cache_peers.py b/test/functional/test_framework/test_cache_peers.py index 9650048253..1a3e940be8 100644 --- a/test/functional/test_framework/test_cache_peers.py +++ b/test/functional/test_framework/test_cache_peers.py @@ -8,6 +8,7 @@ import os import tempfile import unittest +from pathlib import Path from types import SimpleNamespace from unittest.mock import patch @@ -50,7 +51,9 @@ def stop_after_first_sync(nodes): raise FirstSyncComplete() try: - with patch.object(TestNode, "start", start_without_node7_peer), \ + ravend = Path(__file__).resolve().parents[3] / "src" / "ravend" + with patch.dict(os.environ, {"RAVEND": str(ravend)}), \ + patch.object(TestNode, "start", start_without_node7_peer), \ patch.object(framework_module, "sync_blocks", stop_after_first_sync): with self.assertRaises(FirstSyncComplete): framework._initialize_chain() From 06f9d90fd0d213ad33b078c87fd1e9f4fd05edde Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:14:55 +0200 Subject: [PATCH 080/195] [FINDING-170] Record standalone cache gate failure --- ...0025-v4.8-security-remediation-register.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 4ae2d5da2c..04e55e9d5c 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8850,3 +8850,45 @@ Cross-platform CI and public deployment remain separate release gates. mode. Build Raven and Final Gate must both pass on a single post-repair SHA before CI qualification; the earlier passing Final Gate does not override the failed Build Raven run. + +### FINDING-170: Standalone cache peer regression cannot locate ravend in CI + +- **Severity:** MEDIUM release qualification failure; the artifact gate fails closed. +- **Frozen initial status:** OPEN on 2026-10-13 at `4078257ee`, before + correcting the new standalone regression's binary path. +- **Affected RIP-25 invariant:** A required behavioral regression must run + against the just-built node binary before artifacts are accepted. +- **Affected Core 4.8.0 fix:** None. This is a test harness integration + defect introduced during FINDING-169 remediation. +- **Root cause:** The standalone unittest called the functional cache + initializer without setting `RAVEND`. The initializer then attempted + to execute the bare name `ravend`; the CI runner does not place the + just-built binary on `PATH`. +- **Affected file/function/lines:** + `test/functional/test_framework/test_cache_peers.py:29-60`, standalone + regression setup; `test/functional/test_framework/test_framework.py:385-395`, + cache node command; `contrib/devtools/check-rip25-v48-invariants.sh:807`, + required invocation. +- **Concrete scenario:** Build Raven run `38394096626` on exact SHA + `4078257ee2862586eb7a0c07ed79b7ee4635f5f3` passed mandatory + `make check`, wallet behavioral cases, fuzz smoke, and gate helper + checks. Its new cache peer unittest then raised `FileNotFoundError` + for `ravend` before starting any node; all five artifact jobs were skipped. +- **Expected behavior:** The standalone test resolves the built `src/ravend` + binary independently of the runner's ambient `PATH`, while still + withholding node 7's startup peer to test the recovery path. +- **Regression required:** Run the test with `RAVEND` unset and a `PATH` + lacking the build tree; show the failure before correction and success + afterward. Rerun mandatory `make check`, Build Raven, and Final Gate + on one exact post-correction SHA. +- **Remediation commit:** `d45aa5b44` resolves the built node binary + from the regression test's source-tree location and supplies it as + `RAVEND` only for that test. +- **Verification:** The isolated local command reproduced the same + `FileNotFoundError` before correction. With the test deriving the + binary path from its source location, it passed in 3.1 seconds under + the same restricted environment. Local `make -j8 check` and the full + behavioral invariant gate with nine focused functional cases passed + under Python 3.10 after the correction. CI qualification remains pending. +- **Final status:** FIXED locally; Build Raven and Final Gate must both + pass on a post-correction SHA. From f21e964a6c6146bfda08a046b6ea6308e2514315 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:42:39 +0200 Subject: [PATCH 081/195] [FINDING-067] Record complete testnet history scan --- ...0025-v4.8-security-remediation-register.md | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 04e55e9d5c..e67934219d 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4502,17 +4502,22 @@ The Windows unsigned installer and macOS unsigned DMG also matched their published manifest entries. This verifies artifact integrity and source identity; it does not replace execution on each target platform. -**2026-10-13 testnet-history qualification attempt:** The previously -available local testnet chain reached only height 49. An isolated, -unpruned, no-wallet testnet node received more than 2.5 million headers, -but full block validation reached only height 39628, hash -`000000015c51be2026a84eb9d97d4ddf4fd1d224b472c7f61594ed6e0a207524`, -at shutdown. The reported verification progress was 0.008097. A private -scanner has been prepared to examine every block for native witness-v2 -outputs and their spends, plus P2SH-wrapped witness-v2 spends, once a -fully synchronized unpruned node or verified snapshot is available. -The partial chain does not establish whether historical empty-context -PQ signatures exist. Release condition 3 above remains OPEN. +**2026-10-13 testnet-history scan:** The isolated, unpruned, no-wallet +testnet node synchronized through height 2551767, hash +`00000b4e32c9846b4cdbd341a80cd8a5294412bec7199621843b6dc9b8cf1105`. +The private block scanner examined all 2551768 blocks from genesis through +that hash, containing 2772158 transactions. It checked native witness-v2 +program lengths 2 through 40, their spends, P2SH-wrapped witness-v2 +redeems, non-32-byte programs, and malformed redeem candidates. All five +candidate counts were zero. Its 1267 saved checkpoints and the final block +hash agreed with the node, and the source and completion tips had the same +chainwork `000000000000000000000000000000000000000000000000000f229e8df3b0e5`. +The private evidence is `testnet-history-scan.json`, completed at +2026-10-13 12:32:13 UTC. This excludes historical empty-context PQ spends +on the examined active testnet chain. The synchronizing node identified +itself as `v4.8.0.0-a809f6757-dirty`; its exact clean source provenance +is not established. A full chainstate revalidation with a clean final +4.8.1 binary and `-assumevalid=0` remains required before release. **2026-10-13 bit-12 mainnet parameter decision and implementation:** The owner selected 2027-07-01 00:00 UTC (`1814400000`) for STARTED and @@ -4534,8 +4539,8 @@ window must be reviewed in a separate release before it starts. Commit `948b1ea20` records the approved schedule and operator plan in the protocol and asset notes and adds draft versioned 4.8.1 release notes. -FINDING-067 remains HIGH and OPEN. The outstanding gates are complete -shared-testnet history validation, final 4.8.1 source and artifact +FINDING-067 remains HIGH and OPEN. The outstanding gates are clean +final-SHA revalidation of the scanned testnet history, final 4.8.1 source and artifact qualification at one SHA with mandatory `make check`, a repeat of the 8 MWU old/new block boundary evidence on that SHA, final release notes and checksums, operator readiness before bit-12 ACTIVE, and the separate From 6faabc2d7f67ca4f9ae47398f5e9877c1499fbce Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:43:52 +0200 Subject: [PATCH 082/195] [FINDING-171] Freeze repeat ARM64 source failure --- ...0025-v4.8-security-remediation-register.md | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index e67934219d..1b61911409 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8743,8 +8743,9 @@ Cross-platform CI and public deployment remain separate release gates. `38234048442`. Build Raven `38296258242` built the aarch64 artifact `11771315730` and the other four release targets at `9151946ad`; paired Final Gate `38296262444` succeeded at that SHA. -- **Final status:** FIXED on the qualified candidate SHA; final release-SHA - qualification remains due. +- **Final status:** REOPENED for the 4.8.1 release after the same ARM64 + source-fetch class recurred in Build Raven `38395943288`, as recorded in + FINDING-171. The earlier green candidate remains valid for its SHA. ### FINDING-168: Wallet rewrite recovery can leave PQ plaintext in the database file @@ -8897,3 +8898,40 @@ Cross-platform CI and public deployment remain separate release gates. under Python 3.10 after the correction. CI qualification remains pending. - **Final status:** FIXED locally; Build Raven and Final Gate must both pass on a post-correction SHA. + +### FINDING-171: FreeType mirror timeout again blocks the ARM64 release artifact + +- **Severity:** MEDIUM release availability; artifact qualification fails closed. +- **Frozen initial status:** OPEN on 2026-10-13 at + `06f9d90fd0d213ad33b078c87fd1e9f4fd05edde`, before another source + mirror or fallback change. +- **Affected RIP-25 invariant:** All five Build Raven release targets must + succeed at the same exact SHA as a green Final Gate. +- **Affected Core 4.8.0 fix:** The FreeType 2.7.1 archive and SHA-256 pin + are inherited from Core 4.8.0. FINDING-167 changed its primary host but + retained a single reachable-host dependency. +- **Root cause:** The ARM64 runner could not connect to + `download-mirror.savannah.gnu.org` during six curl attempts. The generic + fallback path was empty, so the final request used an invalid URL. +- **Affected file/function/lines:** `depends/packages/freetype.mk:1-5`, + primary source and pinned digest; `depends/funcs.mk:22-35`, fetch and + generic fallback; `.github/workflows/build-raven.yml`, ARM64 artifact job. +- **Concrete scenario:** Build Raven run `38395943288`, job + `115296217551`, passed checkout and security invariant checks but failed + in Build Dependencies before configuring Ravencoin. The log shows six + connection timeouts followed by `curl: (3)` for the empty fallback. + The required security job had passed; this is an artifact-source + availability failure, not evidence of an asset-consensus failure. +- **Expected behavior:** Fetch the exact pinned FreeType archive from a + second official source if the primary host is unreachable, validate the + unchanged SHA-256 before extraction, and reject any mismatch. +- **Regression required:** Preserve this red CI log; force primary-source + failure in a clean local depends fetch and prove the secondary source + downloads the exact pinned bytes. Then rerun Build Raven and Final Gate + at a common post-remediation SHA with all release targets green. +- **Remediation commit:** Pending. +- **Verification:** The official FreeType SourceForge 2.7.1 archive was + fetched locally and matched the existing pinned digest + `3a3bb2c4e15ffb433f2032f50a5b5a92558206822e22bfe8cbe339af4aa82f88`. + This does not yet prove the fallback works in the depends recipe or CI. +- **Final status:** OPEN; FINDING-067 remains OPEN. From 38b84a9c71b7690826d2fdce3c42422f7decaec4 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:44:58 +0200 Subject: [PATCH 083/195] [FINDING-171] Add verified FreeType source fallback --- depends/packages/freetype.mk | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/depends/packages/freetype.mk b/depends/packages/freetype.mk index 0bd0936e03..025bbb2383 100644 --- a/depends/packages/freetype.mk +++ b/depends/packages/freetype.mk @@ -1,9 +1,16 @@ package=freetype $(package)_version=2.7.1 $(package)_download_path=https://download-mirror.savannah.gnu.org/releases/$(package) +$(package)_fallback_download_path=https://downloads.sourceforge.net/project/freetype/freetype2/$($(package)_version) $(package)_file_name=$(package)-$($(package)_version).tar.bz2 $(package)_sha256_hash=3a3bb2c4e15ffb433f2032f50a5b5a92558206822e22bfe8cbe339af4aa82f88 +define $(package)_fetch_cmds +( test -f $(freetype_source) || \ + $(call fetch_file_inner,freetype,$(freetype_download_path),$(freetype_download_file),$(freetype_file_name),$(freetype_sha256_hash)) || \ + $(call fetch_file_inner,freetype,$(freetype_fallback_download_path),$(freetype_download_file),$(freetype_file_name),$(freetype_sha256_hash)) ) +endef + define $(package)_set_vars $(package)_config_opts=--without-zlib --without-png --disable-static $(package)_config_opts_linux=--with-pic From 25cf51a803fda24e02e56cf6ab11319558cbd7c5 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:45:33 +0200 Subject: [PATCH 084/195] [FINDING-067] Set release version 4.8.1 --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 9e51c20fec..61029467d9 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N) AC_PREREQ([2.60]) define(_CLIENT_VERSION_MAJOR, 4) define(_CLIENT_VERSION_MINOR, 8) -define(_CLIENT_VERSION_REVISION, 0) +define(_CLIENT_VERSION_REVISION, 1) define(_CLIENT_VERSION_BUILD, 0) define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_IS_RELEASE, true) From fd1c277d7749d79667c13dc83eb3339e76fe99cc Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:46:08 +0200 Subject: [PATCH 085/195] [FINDING-171] Record verified source fallback regression --- ...P-0025-v4.8-security-remediation-register.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 1b61911409..4ac79c8672 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8929,9 +8929,16 @@ Cross-platform CI and public deployment remain separate release gates. failure in a clean local depends fetch and prove the secondary source downloads the exact pinned bytes. Then rerun Build Raven and Final Gate at a common post-remediation SHA with all release targets green. -- **Remediation commit:** Pending. -- **Verification:** The official FreeType SourceForge 2.7.1 archive was - fetched locally and matched the existing pinned digest +- **Remediation commit:** `38b84a9c7` adds a FreeType-specific SourceForge + fallback to the depends recipe. Each fetched archive is checked against + the unchanged pinned digest before it reaches the source directory. +- **Verification:** With the primary URL forced to the closed local port + `127.0.0.1:9`, a clean ARM64 depends fetch used the SourceForge fallback + and accepted the archive with SHA-256 `3a3bb2c4e15ffb433f2032f50a5b5a92558206822e22bfe8cbe339af4aa82f88`. - This does not yet prove the fallback works in the depends recipe or CI. -- **Final status:** OPEN; FINDING-067 remains OPEN. + The same forced primary failure followed by altered fallback bytes + failed its hash check and left no source archive in place. The red CI + evidence remains Build Raven `38395943288`, job `115296217551`. +- **Final status:** FIXED locally; all five Build Raven artifacts and + Final Gate must pass at one exact post-remediation SHA. FINDING-067 + remains OPEN. From cee06da4aa0f5bdd7911f01ff86eaa2af9a75074 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:46:25 +0200 Subject: [PATCH 086/195] [FINDING-169] Record successful cache security gates --- doc/RIP-0025-v4.8-security-remediation-register.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 4ac79c8672..3660f67741 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8852,10 +8852,15 @@ Cross-platform CI and public deployment remain separate release gates. `make -j8 check` passed. The full behavioral gate, including the new regression and nine functional cases, passed under Python 3.10. The original CI cause remains uncertain because its node logs were lost. + On exact SHA `06f9d90fd0d213ad33b078c87fd1e9f4fd05edde`, the + required security job in Build Raven `38395943288` and the security + job plus all eight builds in Final Gate `38395942637` passed. Both + security jobs ran the cache regression successfully. Build Raven as a + whole failed only in its ARM64 FreeType fetch, recorded in FINDING-171. - **Final status:** FIXED locally for the reproduced missing-peer failure - mode. Build Raven and Final Gate must both pass on a single post-repair - SHA before CI qualification; the earlier passing Final Gate does not - override the failed Build Raven run. + mode and verified in the two required security jobs. Release + qualification still requires a wholly green Build Raven and Final Gate + pair at one final SHA. ### FINDING-170: Standalone cache peer regression cannot locate ravend in CI From bfa2efbb3f47dca3e32df3c73b74625d5c64b5ee Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:46:35 +0200 Subject: [PATCH 087/195] [FINDING-170] Record standalone cache CI pass --- doc/RIP-0025-v4.8-security-remediation-register.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 3660f67741..d442b42289 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8900,9 +8900,15 @@ Cross-platform CI and public deployment remain separate release gates. binary path from its source location, it passed in 3.1 seconds under the same restricted environment. Local `make -j8 check` and the full behavioral invariant gate with nine focused functional cases passed - under Python 3.10 after the correction. CI qualification remains pending. -- **Final status:** FIXED locally; Build Raven and Final Gate must both - pass on a post-correction SHA. + under Python 3.10 after the correction. On exact SHA + `06f9d90fd0d213ad33b078c87fd1e9f4fd05edde`, the required security + jobs in Build Raven `38395943288` and Final Gate `38395942637` both + passed the standalone cache regression. Final Gate passed all eight + builds; Build Raven's ARM64 artifact failed for the separate FreeType + source timeout in FINDING-171. +- **Final status:** FIXED locally and verified in both required CI + security jobs. A wholly green same-SHA Build Raven and Final Gate pair + is still required for release qualification. ### FINDING-171: FreeType mirror timeout again blocks the ARM64 release artifact From 0d2552fad27da9c75da127b99246568e3a62c7d0 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:50:27 +0200 Subject: [PATCH 088/195] [FINDING-172] Freeze optional transaction tool link failure --- ...0025-v4.8-security-remediation-register.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index d442b42289..fd03cb54e6 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8953,3 +8953,39 @@ Cross-platform CI and public deployment remain separate release gates. - **Final status:** FIXED locally; all five Build Raven artifacts and Final Gate must pass at one exact post-remediation SHA. FINDING-067 remains OPEN. + +### FINDING-172: Optional raven-tx cannot link the asset and PQ helpers + +- **Severity:** LOW release tooling failure; the default release artifact + workflow does not enable `raven-tx`. +- **Frozen initial status:** OPEN on 2026-10-13 at + `bfa2efbb3f47dca3e32df3c73b74625d5c64b5ee`, before changing its + link libraries. +- **Affected RIP-25 invariant:** The 4.8.1 release process regenerates + manpages from current utilities. An optional `--with-tx` build must + link before its `raven-tx` manpage can be regenerated faithfully. +- **Affected Core 4.8.0 fix:** Core 4.8.0 already omitted the asset/server + library from `raven_tx_LDADD`. The current PQ-only asset helper adds + another missing symbol to that existing link gap. +- **Root cause:** The target links common transaction objects, including + `coins.cpp` and `script/sign.cpp`, but omits the server archive that + defines the asset helpers they call. Other server dependencies may + also be needed once the archive is included. +- **Affected file/function/lines:** `src/Makefile.am:531-549`, optional + target link list; `src/Makefile.am:247-270`, server asset sources; + `src/assets/assets.cpp:3719`, PQ-only helper. +- **Concrete scenario:** After configuring this checkout with + `--with-tx --with-gui=no` and the pinned local depends prefix, + `make -C src -j8 raven-tx` exited 2. Its linker reported unresolved + `CAssetsCache`, `AreAssetsDeployed`, and `IsPQOnlyAssetScript` symbols. + The default `--without-tx` local `make -j8 check` passed. +- **Expected behavior:** The optional utility links against the full + dependency closure and its current `--help` can regenerate the manpage. +- **Regression required:** Preserve the red link result, then build + `raven-tx` successfully with `--with-tx`, run its version and help smoke + checks, regenerate its manpage, and rerun mandatory `make check`. +- **Remediation commit:** Pending. +- **Verification:** Red local link log is private + `evidence/f172-raven-tx-red.log`; CI release artifacts are unaffected + because the Build Raven configure script does not pass `--with-tx`. +- **Final status:** OPEN; the manpage update remains incomplete. From a468e2ab22785f5bcad91abc42851044a99d78ee Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:01:43 +0200 Subject: [PATCH 089/195] [FINDING-067] Refresh 4.8.1 release manpages --- doc/man/raven-cli.1 | 27 +++++--- doc/man/raven-qt.1 | 154 ++++++++++++++++++++++++++++++++++---------- doc/man/raven-tx.1 | 15 ++--- doc/man/ravend.1 | 154 ++++++++++++++++++++++++++++++++++---------- 4 files changed, 264 insertions(+), 86 deletions(-) diff --git a/doc/man/raven-cli.1 b/doc/man/raven-cli.1 index 14975837c2..b365e98fc4 100644 --- a/doc/man/raven-cli.1 +++ b/doc/man/raven-cli.1 @@ -1,9 +1,9 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH RAVEN-CLI "1" "September 2017" "raven-cli v0.15.99.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH RAVEN-CLI "1" "October 2026" "raven-cli v4.8.1.0" "User Commands" .SH NAME -raven-cli \- manual page for raven-cli v0.15.99.0 +raven-cli \- manual page for raven-cli v4.8.1.0 .SH DESCRIPTION -Raven Core RPC client version v0.15.99.0 +Raven Core RPC client version v4.8.1.0 .SS "Usage:" .TP raven\-cli [options] [params] @@ -25,6 +25,14 @@ Specify configuration file (default: raven.conf) \fB\-datadir=\fR .IP Specify data directory +.HP +\fB\-getinfo\fR +.IP +Get general information from the remote server. Note that unlike +server\-side RPC calls, the results of \fB\-getinfo\fR is the result of +multiple non\-atomic requests. Some entries in the result may +represent results from different states (e.g. wallet balance may +be as of a different block from the chain state reported) .PP Chain selection options: .HP @@ -85,14 +93,13 @@ input is used for the RPC password. \fB\-rpcwallet=\fR .IP Send RPC for non\-default wallet on RPC server (argument is wallet -filename in ravend directory, required if ravend/\-Qt runs -with multiple wallets) +filename in ravend directory, required if ravend/\-Qt runs with +multiple wallets) .SH COPYRIGHT -Copyright (C) 2009-2017 The Bitcoin Core developers -Copyright (c) 2017-2019 The Raven Core developers +Copyright (C) 2009-2022 The Raven Core developers -Please contribute if you find Raven Core useful. Visit - for further information about the software. +Please contribute if you find Raven Core useful. Visit +for further information about the software. The source code is available from . This is experimental software. diff --git a/doc/man/raven-qt.1 b/doc/man/raven-qt.1 index 035d12efec..0ac69f95e1 100644 --- a/doc/man/raven-qt.1 +++ b/doc/man/raven-qt.1 @@ -1,9 +1,9 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH RAVEN-QT "1" "September 2017" "raven-qt v0.15.99.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH RAVEN-QT "1" "October 2026" "raven-qt v4.8.1.0" "User Commands" .SH NAME -raven-qt \- manual page for raven-qt v0.15.99.0 +raven-qt \- manual page for raven-qt v4.8.1.0 .SH DESCRIPTION -Raven Core version v0.15.99.0 (64\-bit) +Raven Core version v4.8.1.0 (64\-bit) Usage: .IP raven\-qt [command\-line options] @@ -32,9 +32,9 @@ block hash) If this block is in the chain assume that it and its ancestors are valid and potentially skip their script verification (0 to verify all, default: -0000000000000000003b9ce759c2a087d52abc4266f8f4ebd6d768b89defa50a, +0000000000018d2fdcf4ac8eaac8db059584bd2840be5629562bb8599d39998c, testnet: -0000000002e9e7b00e1f6dc5123a04aad68dd0f0968d8c7aa45f6640795c37b1) +000000006272208605c4df3b54d4d5515759105e7ffcb258e8cd8077924ffef1) .HP \fB\-conf=\fR .IP @@ -48,10 +48,29 @@ Specify data directory .IP Set database cache size in megabytes (4 to 16384, default: 450) .HP +\fB\-disablemessaging\fR +.IP +Turn off the databasing the messages sent with assets (default: 0) +.HP \fB\-loadblock=\fR .IP Imports blocks from external blk000??.dat file on startup .HP +\fB\-maxreorg=\fR +.IP +Set the Maximum reorg depth (default: 60) +.HP +\fB\-minreorgpeers=\fR +.IP +Set the Minimum amount of peers required to disallow reorg of chains of +depth >= maxreorg. Peers must be greater than. (default: 4) +.HP +\fB\-minreorgage=\fR +.IP +Set the Minimum tip age (in seconds) required to allow reorg of a chain +of depth >= maxreorg on a node with more than minreorgpeers +peers. (default: 43200) +.HP \fB\-maxorphantx=\fR .IP Keep at most unconnectable transactions in memory (default: 100) @@ -76,9 +95,19 @@ Extra transactions to keep in memory for compact block reconstructions .HP \fB\-par=\fR .IP -Set the number of script verification threads (\fB\-2\fR to 16, 0 = auto, <0 = +Set the number of script verification threads (\fB\-8\fR to 16, 0 = auto, <0 = leave that many cores free, default: 0) .HP +\fB\-autofixmempool\fR +.IP +When set, if the CreateNewBlock fails because of a transaction. The +mempool will be cleared. (default: 0) +.HP +\fB\-bypassdownload\fR +.IP +When set, if the chain is in initialblockdownload the getblocktemplate +rpc call will still return block data (default: 0) +.HP \fB\-pid=\fR .IP Specify pid file (default: ravend.pid) @@ -112,12 +141,33 @@ Create new files with system default permissions, instead of umask 077 .IP Maintain a full transaction index, used by the getrawtransaction rpc call (default: 0) +.HP +\fB\-assetindex\fR +.IP +Keep an index of assets, used by the requestsnapshot rpc call. Requires +a \fB\-reindex\fR. +.HP +\fB\-addressindex\fR +.IP +Maintain a full address index, used to query for the balance, txids and +unspent outputs for addresses (default: 0) +.HP +\fB\-timestampindex\fR +.IP +Maintain a timestamp index for block hashes, used to query blocks hashes +by a range of timestamps (default: 0) +.HP +\fB\-spentindex\fR +.IP +Maintain a full spent index, used to query the spending txid and input +index for an outpoint (default: 0) .PP Connection options: .HP \fB\-addnode=\fR .IP -Add a node to connect to and attempt to keep the connection open +Add a node to connect to and attempt to keep the connection open (see +the `addnode` RPC command help for more info) .HP \fB\-banscore=\fR .IP @@ -136,7 +186,8 @@ for IPv6 \fB\-connect=\fR .IP Connect only to the specified node(s); \fB\-connect\fR=\fI\,0\/\fR disables automatic -connections +connections (the rules for this peer are the same as for +\fB\-addnode\fR) .HP \fB\-discover\fR .IP @@ -206,7 +257,7 @@ Support filtering of blocks and transaction with bloom filters (default: .HP \fB\-port=\fR .IP -Listen for connections on (default: 8767 or testnet: 18767) +Listen for connections on (default: 8767 or testnet: 18770) .HP \fB\-proxy=\fR .IP @@ -258,31 +309,53 @@ Tries to keep outbound traffic under the given target (in MiB per 24h), .PP Wallet options: .HP -\fB\-disablewallet\fR +\fB\-bip44=\fR .IP -Do not load the wallet and disable wallet RPC calls +Sets the wallet to use/not use bip44 12\-words, non\-bip44=0 or bip44=1 +(default: 1). Note: By default 12\-words will automatically be +generated for you (random word selection). See \fB\-mnemonic\fR and +\fB\-mnemonicpassphrase\fR below to create a wallet using a specific +word list (use an existing bip\-44 wallet word\-list), or use the +RPC/CLI getmywords or dumpwallet to retrieve the auto\-generated +word\-list. This flag is ignored if there is already an existing +non\-bip44 wallet. .HP -\fB\-keypool=\fR +\fB\-discardfee=\fR .IP -Set key pool size to (default: 1000) +The fee rate (in RVN/kB) that indicates your tolerance for discarding +change by adding it to the fee (default: 0.00025). Note: An +output is discarded if it is dust at this rate, but we will +always discard up to the dust relay fee and a discard fee above +that is limited by the fee estimate for the longest target +.HP +\fB\-disablewallet\fR +.IP +Do not load the wallet and disable wallet RPC calls .HP \fB\-fallbackfee=\fR .IP A fee rate (in RVN/kB) that will be used when fee estimation has -insufficient data (default: 0.0002) +insufficient data (default: 0.01025) .HP -\fB\-discardfee=\fR +\fB\-keypool=\fR .IP -The fee rate (in RVN/kB) that indicates your tolerance for discarding -change by adding it to the fee (default: 0.0001). Note: An output -is discarded if it is dust at this rate, but we will always -discard up to the dust relay fee and a discard fee above that is -limited by the fee estimate for the longest target +Set key pool size to (default: 1000) .HP \fB\-mintxfee=\fR .IP Fees (in RVN/kB) smaller than this are considered zero fee for -transaction creation (default: 0.00001) +transaction creation (default: 0.01) +.HP +\fB\-mnemonic=\fR +.IP +A space separated list of 12 words used to import a BIP44 wallet. +Command\-line arguments can be visible to other local users and +process tools. +.HP +\fB\-mnemonicpassphrase=\fR +.IP +Passphrase securing your 12\-word mnemonic word list. Command\-line +arguments and configuration files can retain plaintext secrets. .HP \fB\-paytxfee=\fR .IP @@ -305,14 +378,14 @@ Spend unconfirmed change when sending transactions (default: 1) If paytxfee is not set, include enough fee so transactions begin confirmation on average within n blocks (default: 6) .HP -\fB\-walletrbf\fR -.IP -Send transactions with full\-RBF opt\-in enabled (default: 0) -.HP \fB\-upgradewallet\fR .IP Upgrade wallet to latest format on startup .HP +\fB\-walletrbf\fR +.IP +Send transactions with full\-RBF opt\-in enabled (default: 0) +.HP \fB\-wallet=\fR .IP Specify wallet file (within data directory) (default: wallet.dat) @@ -332,6 +405,16 @@ Delete all wallet transactions and only recover those parts of the blockchain through \fB\-rescan\fR on startup (1 = keep tx meta data e.g. account owner and payment request information, 2 = drop tx meta data) +.HP +\fB\-miningaddress=\fR
+.IP +When getblocktemplate is called. It will create the coinbase transaction +using this address(default: empty string) +.HP +\fB\-minrewardheight=\fR +.IP +The default height that is required before rewards are allowed to be +sent out (default: 60) .PP ZeroMQ notification options: .HP @@ -350,6 +433,10 @@ Enable publish raw block in
\fB\-zmqpubrawtx=\fR
.IP Enable publish raw transaction in
+.HP +\fB\-zmqpubrawmessage=\fR
+.IP +Enable publish raw asset messages in
.PP Debugging/Testing options: .HP @@ -364,7 +451,7 @@ optional). If is not supplied or if = 1, output all debugging information. can be: net, tor, mempool, http, bench, zmq, db, rpc, estimatefee, addrman, selectcoins, reindex, cmpctblock, rand, prune, proxy, mempoolrej, -libevent, coindb, qt, leveldb. +libevent, coindb, qt, leveldb, rewards. .HP \fB\-debugexclude=\fR .IP @@ -388,7 +475,7 @@ Prepend debug output with timestamp (default: 1) .IP Maximum total fees (in RVN) to use in a single wallet transaction or raw transaction; setting this too low may abort large transactions -(default: 0.10) +(default: 1000.00) .HP \fB\-printtoconsole\fR .IP @@ -422,12 +509,12 @@ Maximum size of data in data carrier transactions we relay and mine .HP \fB\-mempoolreplacement\fR .IP -Enable transaction replacement in the memory pool (default: 1) +Enable transaction replacement in the memory pool (default: 0) .HP \fB\-minrelaytxfee=\fR .IP Fees (in RVN/kB) smaller than this are considered zero fee for relaying, -mining and transaction creation (default: 0.00001) +mining and transaction creation (default: 0.01) .HP \fB\-whitelistrelay\fR .IP @@ -542,11 +629,10 @@ Show splash screen on startup (default: 1) .IP Reset all settings changed in the GUI .SH COPYRIGHT -Copyright (C) 2009-2017 The Bitcoin Core developers -Copyright (c) 2017-2019 The Raven Core developers +Copyright (C) 2009-2022 The Raven Core developers -Please contribute if you find Raven Core useful. Visit - for further information about the software. +Please contribute if you find Raven Core useful. Visit +for further information about the software. The source code is available from . This is experimental software. diff --git a/doc/man/raven-tx.1 b/doc/man/raven-tx.1 index fedd023110..22196be205 100644 --- a/doc/man/raven-tx.1 +++ b/doc/man/raven-tx.1 @@ -1,9 +1,9 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH RAVEN-TX "1" "September 2017" "raven-tx v0.15.99.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH RAVEN-TX "1" "October 2026" "raven-tx v4.8.1.0" "User Commands" .SH NAME -raven-tx \- manual page for raven-tx v0.15.99.0 +raven-tx \- manual page for raven-tx v4.8.1.0 .SH DESCRIPTION -Raven Core raven\-tx utility version v0.15.99.0 +Raven Core raven\-tx utility version v4.8.1.0 .SS "Usage:" .TP raven\-tx [options] [commands] @@ -112,11 +112,10 @@ set=NAME:JSON\-STRING .IP Set register NAME to given JSON\-STRING .SH COPYRIGHT -Copyright (C) 2009-2017 The Bitcoin Core developers -Copyright (c) 2017-2019 The Raven Core developers +Copyright (C) 2009-2022 The Raven Core developers -Please contribute if you find Raven Core useful. Visit - for further information about the software. +Please contribute if you find Raven Core useful. Visit +for further information about the software. The source code is available from . This is experimental software. diff --git a/doc/man/ravend.1 b/doc/man/ravend.1 index a0507a9147..773ee77a5e 100644 --- a/doc/man/ravend.1 +++ b/doc/man/ravend.1 @@ -1,9 +1,9 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH RAVEND "1" "September 2017" "ravend v0.15.99.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH RAVEND "1" "October 2026" "ravend v4.8.1.0" "User Commands" .SH NAME -ravend \- manual page for ravend v0.15.99.0 +ravend \- manual page for ravend v4.8.1.0 .SH DESCRIPTION -Raven Core Daemon version v0.15.99.0 +Raven Core Daemon version v4.8.1.0 .SS "Usage:" .TP ravend [options] @@ -33,9 +33,9 @@ block hash) If this block is in the chain assume that it and its ancestors are valid and potentially skip their script verification (0 to verify all, default: -0000000000000000003b9ce759c2a087d52abc4266f8f4ebd6d768b89defa50a, +0000000000018d2fdcf4ac8eaac8db059584bd2840be5629562bb8599d39998c, testnet: -0000000002e9e7b00e1f6dc5123a04aad68dd0f0968d8c7aa45f6640795c37b1) +000000006272208605c4df3b54d4d5515759105e7ffcb258e8cd8077924ffef1) .HP \fB\-conf=\fR .IP @@ -53,10 +53,29 @@ Specify data directory .IP Set database cache size in megabytes (4 to 16384, default: 450) .HP +\fB\-disablemessaging\fR +.IP +Turn off the databasing the messages sent with assets (default: 0) +.HP \fB\-loadblock=\fR .IP Imports blocks from external blk000??.dat file on startup .HP +\fB\-maxreorg=\fR +.IP +Set the Maximum reorg depth (default: 60) +.HP +\fB\-minreorgpeers=\fR +.IP +Set the Minimum amount of peers required to disallow reorg of chains of +depth >= maxreorg. Peers must be greater than. (default: 4) +.HP +\fB\-minreorgage=\fR +.IP +Set the Minimum tip age (in seconds) required to allow reorg of a chain +of depth >= maxreorg on a node with more than minreorgpeers +peers. (default: 43200) +.HP \fB\-maxorphantx=\fR .IP Keep at most unconnectable transactions in memory (default: 100) @@ -81,9 +100,19 @@ Extra transactions to keep in memory for compact block reconstructions .HP \fB\-par=\fR .IP -Set the number of script verification threads (\fB\-2\fR to 16, 0 = auto, <0 = +Set the number of script verification threads (\fB\-8\fR to 16, 0 = auto, <0 = leave that many cores free, default: 0) .HP +\fB\-autofixmempool\fR +.IP +When set, if the CreateNewBlock fails because of a transaction. The +mempool will be cleared. (default: 0) +.HP +\fB\-bypassdownload\fR +.IP +When set, if the chain is in initialblockdownload the getblocktemplate +rpc call will still return block data (default: 0) +.HP \fB\-pid=\fR .IP Specify pid file (default: ravend.pid) @@ -117,12 +146,33 @@ Create new files with system default permissions, instead of umask 077 .IP Maintain a full transaction index, used by the getrawtransaction rpc call (default: 0) +.HP +\fB\-assetindex\fR +.IP +Keep an index of assets, used by the requestsnapshot rpc call. Requires +a \fB\-reindex\fR. +.HP +\fB\-addressindex\fR +.IP +Maintain a full address index, used to query for the balance, txids and +unspent outputs for addresses (default: 0) +.HP +\fB\-timestampindex\fR +.IP +Maintain a timestamp index for block hashes, used to query blocks hashes +by a range of timestamps (default: 0) +.HP +\fB\-spentindex\fR +.IP +Maintain a full spent index, used to query the spending txid and input +index for an outpoint (default: 0) .PP Connection options: .HP \fB\-addnode=\fR .IP -Add a node to connect to and attempt to keep the connection open +Add a node to connect to and attempt to keep the connection open (see +the `addnode` RPC command help for more info) .HP \fB\-banscore=\fR .IP @@ -141,7 +191,8 @@ for IPv6 \fB\-connect=\fR .IP Connect only to the specified node(s); \fB\-connect\fR=\fI\,0\/\fR disables automatic -connections +connections (the rules for this peer are the same as for +\fB\-addnode\fR) .HP \fB\-discover\fR .IP @@ -211,7 +262,7 @@ Support filtering of blocks and transaction with bloom filters (default: .HP \fB\-port=\fR .IP -Listen for connections on (default: 8767 or testnet: 18767) +Listen for connections on (default: 8767 or testnet: 18770) .HP \fB\-proxy=\fR .IP @@ -263,31 +314,53 @@ Tries to keep outbound traffic under the given target (in MiB per 24h), .PP Wallet options: .HP -\fB\-disablewallet\fR +\fB\-bip44=\fR .IP -Do not load the wallet and disable wallet RPC calls +Sets the wallet to use/not use bip44 12\-words, non\-bip44=0 or bip44=1 +(default: 1). Note: By default 12\-words will automatically be +generated for you (random word selection). See \fB\-mnemonic\fR and +\fB\-mnemonicpassphrase\fR below to create a wallet using a specific +word list (use an existing bip\-44 wallet word\-list), or use the +RPC/CLI getmywords or dumpwallet to retrieve the auto\-generated +word\-list. This flag is ignored if there is already an existing +non\-bip44 wallet. .HP -\fB\-keypool=\fR +\fB\-discardfee=\fR .IP -Set key pool size to (default: 1000) +The fee rate (in RVN/kB) that indicates your tolerance for discarding +change by adding it to the fee (default: 0.00025). Note: An +output is discarded if it is dust at this rate, but we will +always discard up to the dust relay fee and a discard fee above +that is limited by the fee estimate for the longest target +.HP +\fB\-disablewallet\fR +.IP +Do not load the wallet and disable wallet RPC calls .HP \fB\-fallbackfee=\fR .IP A fee rate (in RVN/kB) that will be used when fee estimation has -insufficient data (default: 0.0002) +insufficient data (default: 0.01025) .HP -\fB\-discardfee=\fR +\fB\-keypool=\fR .IP -The fee rate (in RVN/kB) that indicates your tolerance for discarding -change by adding it to the fee (default: 0.0001). Note: An output -is discarded if it is dust at this rate, but we will always -discard up to the dust relay fee and a discard fee above that is -limited by the fee estimate for the longest target +Set key pool size to (default: 1000) .HP \fB\-mintxfee=\fR .IP Fees (in RVN/kB) smaller than this are considered zero fee for -transaction creation (default: 0.00001) +transaction creation (default: 0.01) +.HP +\fB\-mnemonic=\fR +.IP +A space separated list of 12 words used to import a BIP44 wallet. +Command\-line arguments can be visible to other local users and +process tools. +.HP +\fB\-mnemonicpassphrase=\fR +.IP +Passphrase securing your 12\-word mnemonic word list. Command\-line +arguments and configuration files can retain plaintext secrets. .HP \fB\-paytxfee=\fR .IP @@ -310,14 +383,14 @@ Spend unconfirmed change when sending transactions (default: 1) If paytxfee is not set, include enough fee so transactions begin confirmation on average within n blocks (default: 6) .HP -\fB\-walletrbf\fR -.IP -Send transactions with full\-RBF opt\-in enabled (default: 0) -.HP \fB\-upgradewallet\fR .IP Upgrade wallet to latest format on startup .HP +\fB\-walletrbf\fR +.IP +Send transactions with full\-RBF opt\-in enabled (default: 0) +.HP \fB\-wallet=\fR .IP Specify wallet file (within data directory) (default: wallet.dat) @@ -337,6 +410,16 @@ Delete all wallet transactions and only recover those parts of the blockchain through \fB\-rescan\fR on startup (1 = keep tx meta data e.g. account owner and payment request information, 2 = drop tx meta data) +.HP +\fB\-miningaddress=\fR
+.IP +When getblocktemplate is called. It will create the coinbase transaction +using this address(default: empty string) +.HP +\fB\-minrewardheight=\fR +.IP +The default height that is required before rewards are allowed to be +sent out (default: 60) .PP ZeroMQ notification options: .HP @@ -355,6 +438,10 @@ Enable publish raw block in
\fB\-zmqpubrawtx=\fR
.IP Enable publish raw transaction in
+.HP +\fB\-zmqpubrawmessage=\fR
+.IP +Enable publish raw asset messages in
.PP Debugging/Testing options: .HP @@ -369,7 +456,7 @@ optional). If is not supplied or if = 1, output all debugging information. can be: net, tor, mempool, http, bench, zmq, db, rpc, estimatefee, addrman, selectcoins, reindex, cmpctblock, rand, prune, proxy, mempoolrej, -libevent, coindb, qt, leveldb. +libevent, coindb, qt, leveldb, rewards. .HP \fB\-debugexclude=\fR .IP @@ -393,7 +480,7 @@ Prepend debug output with timestamp (default: 1) .IP Maximum total fees (in RVN) to use in a single wallet transaction or raw transaction; setting this too low may abort large transactions -(default: 0.10) +(default: 1000.00) .HP \fB\-printtoconsole\fR .IP @@ -427,12 +514,12 @@ Maximum size of data in data carrier transactions we relay and mine .HP \fB\-mempoolreplacement\fR .IP -Enable transaction replacement in the memory pool (default: 1) +Enable transaction replacement in the memory pool (default: 0) .HP \fB\-minrelaytxfee=\fR .IP Fees (in RVN/kB) smaller than this are considered zero fee for relaying, -mining and transaction creation (default: 0.00001) +mining and transaction creation (default: 0.01) .HP \fB\-whitelistrelay\fR .IP @@ -521,11 +608,10 @@ non\-verbose mode, non\-segwit(0) or segwit(1) (default: 1) .IP Set the number of threads to service RPC calls (default: 4) .SH COPYRIGHT -Copyright (C) 2009-2017 The Bitcoin Core developers -Copyright (c) 2017-2019 The Raven Core developers +Copyright (C) 2009-2022 The Raven Core developers -Please contribute if you find Raven Core useful. Visit - for further information about the software. +Please contribute if you find Raven Core useful. Visit +for further information about the software. The source code is available from . This is experimental software. From 17e2d5c97555498b62f20286edac3e78bb79a9ae Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:02:25 +0200 Subject: [PATCH 090/195] [FINDING-067][FINDING-172] Record manual and optional tool status --- ...0025-v4.8-security-remediation-register.md | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index fd03cb54e6..7a123e18b5 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4566,6 +4566,17 @@ local `make -j8 check` passed. This is an English source refresh, not a pull of translated language catalogs from Transifex. The latter remains a release-process step because the `tx` client is not installed here. +**2026-10-13 pinned local Qt and manual preparation:** The local 4.8.1 +candidate at `0d2552fad` configured with the depends `config.site`, +Qt 5.12.11, static Qt libraries, and the QMinimal plugin. Its full +build and mandatory `make -j4 check` passed. The daemon, CLI, and Qt +reported `v4.8.1.0-0d2552fad`. Commit `a468e2ab2` regenerates all four +4.8.1 manpages from those current utilities with help2man 1.49.3; +the Qt help command ran under a virtual X display. The optional +`raven-tx` manpage came from a manually linked copy of the same current +sources and libraries because FINDING-172 still blocks its Automake +target. Manual end-user GUI interaction on the final SHA remains due. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical @@ -8962,8 +8973,8 @@ Cross-platform CI and public deployment remain separate release gates. `bfa2efbb3f47dca3e32df3c73b74625d5c64b5ee`, before changing its link libraries. - **Affected RIP-25 invariant:** The 4.8.1 release process regenerates - manpages from current utilities. An optional `--with-tx` build must - link before its `raven-tx` manpage can be regenerated faithfully. + manpages from current utilities. The optional `--with-tx` utility must + link for the normal Automake build of that target. - **Affected Core 4.8.0 fix:** Core 4.8.0 already omitted the asset/server library from `raven_tx_LDADD`. The current PQ-only asset helper adds another missing symbol to that existing link gap. @@ -8981,11 +8992,21 @@ Cross-platform CI and public deployment remain separate release gates. The default `--without-tx` local `make -j8 check` passed. - **Expected behavior:** The optional utility links against the full dependency closure and its current `--help` can regenerate the manpage. -- **Regression required:** Preserve the red link result, then build - `raven-tx` successfully with `--with-tx`, run its version and help smoke - checks, regenerate its manpage, and rerun mandatory `make check`. +- **Regression required for a future fix:** Preserve the red link result, + then build `raven-tx` successfully with `--with-tx` on supported + platforms, run its version and help smoke checks, and rerun mandatory + `make check`. - **Remediation commit:** Pending. - **Verification:** Red local link log is private `evidence/f172-raven-tx-red.log`; CI release artifacts are unaffected because the Build Raven configure script does not pass `--with-tx`. -- **Final status:** OPEN; the manpage update remains incomplete. + A direct linker invocation with all current source archives in one + GNU linker group produced a working `raven-tx` at `0d2552fad`; its + `--help` and `--version` supplied the current 4.8.1 manpage committed + in `a468e2ab2`. The normal Automake target remains broken because + libtool removes repeated static archives and moves linker group flags + ahead of the archives. This workaround is only for documentation and + is not a supported 4.8.1 binary build. +- **Final status:** OPEN for the optional `--with-tx` target. The + release manpage is current, and none of the five Build Raven release + artifacts enables or contains this target. From 19ade999c13a000f385af98bdcf2fb8d65ffbf74 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:10:11 +0200 Subject: [PATCH 091/195] [FINDING-067] Document 4.8.1 asset and build limitations --- doc/release-notes/release-notes-4.8.1.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md index 0ab6ba8a0d..b0a7f9b4b8 100644 --- a/doc/release-notes/release-notes-4.8.1.md +++ b/doc/release-notes/release-notes-4.8.1.md @@ -73,6 +73,22 @@ For the asset format, wallet recovery routes, and current limitations in automatic anchor refresh and raw transaction funding, see [RIP-25 PQ Asset Extension Design Note](../RIP-0025-PQ-Assets-Followup.md). +## Known asset and build limitations + +An asset PQ anchor is native RVN at the protected output's matching PQ +address. Some asset spends and ordinary RVN payments consume the last +anchor without creating a replacement. Exact-fee, dust, multiple-program, +and explicit native-change cases need the owner to fund the matching PQ +address again before the next protected asset spend. The asset output +remains unspent; consensus rejects a spend without its anchor. + +`fundrawtransaction` does not discover or select anchors for protected +asset inputs. Raw transaction callers must supply matching funded PQ +inputs and check the final transaction before broadcast. The optional +`raven-tx` target does not currently link with `--with-tx` and is absent +from the five release artifacts. Its current manual is included for +reference; it is not a qualified 4.8.1 binary. + ## Release qualification still required This draft does not assert that 4.8.1 artifacts, the complete testnet From 09277a96bb0de59768467bec59600bef597cbabd Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:15:10 +0200 Subject: [PATCH 092/195] [FINDING-172] Link optional transaction tool with core archive --- src/Makefile.am | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Makefile.am b/src/Makefile.am index bd9bdd9c7a..0957c63197 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -529,6 +529,17 @@ raven_cli_LDADD += $(BOOST_LIBS) $(SSL_LIBS) $(CRYPTO_LIBS) $(EVENT_LIBS) $(LIBO # # raven-tx binary # +# Keep the interdependent transaction, asset, and wallet objects in one archive. +EXTRA_LIBRARIES += libraven_tx_core.a +libraven_tx_core_a_SOURCES = +libraven_tx_core_a_LIBADD = \ + $(libraven_common_a_OBJECTS) \ + $(libraven_server_a_OBJECTS) + +if ENABLE_WALLET +libraven_tx_core_a_LIBADD += $(libraven_wallet_a_OBJECTS) +endif + raven_tx_SOURCES = raven-tx.cpp raven_tx_CPPFLAGS = $(AM_CPPFLAGS) $(RAVEN_INCLUDES) raven_tx_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) @@ -539,14 +550,18 @@ raven_tx_SOURCES += raven-tx-res.rc endif raven_tx_LDADD = \ + libraven_tx_core.a \ $(LIBUNIVALUE) \ - $(LIBRAVEN_COMMON) \ $(LIBRAVEN_UTIL) \ + $(LIBRAVEN_ZMQ) \ $(LIBRAVEN_CONSENSUS) \ $(LIBRAVEN_CRYPTO) \ + $(LIBLEVELDB) \ + $(LIBLEVELDB_SSE42) \ + $(LIBMEMENV) \ $(LIBSECP256K1) -raven_tx_LDADD += $(BOOST_LIBS) $(CRYPTO_LIBS) $(LIBOQS_LIBS) +raven_tx_LDADD += $(BOOST_LIBS) $(BDB_LIBS) $(SSL_LIBS) $(CRYPTO_LIBS) $(MINIUPNPC_LIBS) $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(ZMQ_LIBS) $(LIBOQS_LIBS) # # ravenconsensus library # From af0cc47d40d7a7c797b9958b985179bbcc34f0f3 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:15:18 +0200 Subject: [PATCH 093/195] [FINDING-172] Exercise optional transaction tool in security gates --- .github/workflows/build-raven.yml | 2 +- .github/workflows/rip25-v48-final-gate.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index bf31870a3b..3c731e642b 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -60,7 +60,7 @@ jobs: ./autogen.sh mkdir -p "$HOME/.ccache" CONFIG_SITE="$PWD/depends/x86_64-pc-linux-gnu/share/config.site" \ - ./configure --without-gui --disable-bench + ./configure --without-gui --with-tx --disable-bench - name: Build and run unit tests run: | diff --git a/.github/workflows/rip25-v48-final-gate.yml b/.github/workflows/rip25-v48-final-gate.yml index 35bc9d677d..9a2e3a1095 100644 --- a/.github/workflows/rip25-v48-final-gate.yml +++ b/.github/workflows/rip25-v48-final-gate.yml @@ -73,7 +73,7 @@ jobs: ./autogen.sh mkdir -p "$HOME/.ccache" CONFIG_SITE="$PWD/depends/x86_64-pc-linux-gnu/share/config.site" \ - ./configure --without-gui --disable-bench + ./configure --without-gui --with-tx --disable-bench - id: build_tests name: Build tests From 55be9a0f2fd7a41d3e38a6e0b814de7d1d0bf5be Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:16:02 +0200 Subject: [PATCH 094/195] [FINDING-172] Record optional tool repair and regression --- ...0025-v4.8-security-remediation-register.md | 40 ++++++++++--------- doc/release-notes/release-notes-4.8.1.md | 7 +--- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 7a123e18b5..773e86cd33 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4574,8 +4574,8 @@ reported `v4.8.1.0-0d2552fad`. Commit `a468e2ab2` regenerates all four 4.8.1 manpages from those current utilities with help2man 1.49.3; the Qt help command ran under a virtual X display. The optional `raven-tx` manpage came from a manually linked copy of the same current -sources and libraries because FINDING-172 still blocks its Automake -target. Manual end-user GUI interaction on the final SHA remains due. +sources and libraries before the Automake repair in FINDING-172. +Manual end-user GUI interaction on the final SHA remains due. ## Finding frozen during the second adversarial review @@ -8982,7 +8982,7 @@ Cross-platform CI and public deployment remain separate release gates. `coins.cpp` and `script/sign.cpp`, but omits the server archive that defines the asset helpers they call. Other server dependencies may also be needed once the archive is included. -- **Affected file/function/lines:** `src/Makefile.am:531-549`, optional +- **Affected file/function/lines:** `src/Makefile.am:531-565`, optional target link list; `src/Makefile.am:247-270`, server asset sources; `src/assets/assets.cpp:3719`, PQ-only helper. - **Concrete scenario:** After configuring this checkout with @@ -8992,21 +8992,25 @@ Cross-platform CI and public deployment remain separate release gates. The default `--without-tx` local `make -j8 check` passed. - **Expected behavior:** The optional utility links against the full dependency closure and its current `--help` can regenerate the manpage. -- **Regression required for a future fix:** Preserve the red link result, - then build `raven-tx` successfully with `--with-tx` on supported - platforms, run its version and help smoke checks, and rerun mandatory - `make check`. -- **Remediation commit:** Pending. +- **Regression required:** Preserve the red link result, then build + `raven-tx` successfully with `--with-tx`, run help and transaction + creation smoke checks, rerun mandatory `make check`, and exercise the + optional target in the required CI security builds. +- **Remediation commits:** `09277a96b` places the mutually dependent + common, server, and wallet objects in one aggregate static archive and + adds the missing external libraries; `af0cc47d4` enables `--with-tx` + in both required Linux security builds. - **Verification:** Red local link log is private `evidence/f172-raven-tx-red.log`; CI release artifacts are unaffected because the Build Raven configure script does not pass `--with-tx`. - A direct linker invocation with all current source archives in one - GNU linker group produced a working `raven-tx` at `0d2552fad`; its - `--help` and `--version` supplied the current 4.8.1 manpage committed - in `a468e2ab2`. The normal Automake target remains broken because - libtool removes repeated static archives and moves linker group flags - ahead of the archives. This workaround is only for documentation and - is not a supported 4.8.1 binary build. -- **Final status:** OPEN for the optional `--with-tx` target. The - release manpage is current, and none of the five Build Raven release - artifacts enables or contains this target. + Before the fix, libtool removed repeated core archives and moved GNU + linker group flags ahead of them; a direct linker invocation supplied + the current manpage from the same source. After the fix, local + configuration with the pinned depends prefix, Qt 5.12.11, and + `--with-tx` built `libraven_tx_core.a` and linked `raven-tx` through + Automake. Its `--help` and `-create -json` smoke checks passed, as did + `make -j4 check`. The aggregate archive contains 152 core objects. + Linux CI and other platform builds of this optional target remain due. +- **Final status:** FIXED locally; the required CI security builds must + confirm the Automake target on an exact post-remediation SHA. The five + release artifacts do not contain this optional utility. diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md index b0a7f9b4b8..e2b7f5e5ea 100644 --- a/doc/release-notes/release-notes-4.8.1.md +++ b/doc/release-notes/release-notes-4.8.1.md @@ -73,7 +73,7 @@ For the asset format, wallet recovery routes, and current limitations in automatic anchor refresh and raw transaction funding, see [RIP-25 PQ Asset Extension Design Note](../RIP-0025-PQ-Assets-Followup.md). -## Known asset and build limitations +## Known asset limitations An asset PQ anchor is native RVN at the protected output's matching PQ address. Some asset spends and ordinary RVN payments consume the last @@ -84,10 +84,7 @@ remains unspent; consensus rejects a spend without its anchor. `fundrawtransaction` does not discover or select anchors for protected asset inputs. Raw transaction callers must supply matching funded PQ -inputs and check the final transaction before broadcast. The optional -`raven-tx` target does not currently link with `--with-tx` and is absent -from the five release artifacts. Its current manual is included for -reference; it is not a qualified 4.8.1 binary. +inputs and check the final transaction before broadcast. ## Release qualification still required From 7d9844c7a0146a525683017e726725c79267c8db Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:33:09 +0200 Subject: [PATCH 095/195] [FINDING-160] Record 4.8.1 block boundary checks --- doc/RIP-0025-v4.8-security-remediation-register.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 773e86cd33..bbb93747a3 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8451,7 +8451,17 @@ Cross-platform CI and public deployment remain separate release gates. 4.8.0 rejected it with `bad-blk-weight`; the candidate accepted it and advanced one block. This proves the discount is independently incompatible at the 8 MWU boundary, even without using the 12 MWU - phase-1 allowance. + phase-1 allowance. On 2026-10-13 the two tests were repeated with the + versioned 4.8.1 binary from `55be9a0f2`, SHA-256 + `19de029e001adddfa4eb6143cc18c012a6e88d13d5b5dbf234a33be184edd378`. + Both binaries accepted the 7,600,636-WU control block. Core 4.8.0 + rejected the 8,801,320-WU block with `bad-blk-length` and the + 8,000,697-WU old-accounted PQ witness block with `bad-blk-weight`; + 4.8.1 accepted both. The private logs are + `evidence/f160-v481-55be-plain.log` and + `evidence/f160-v481-55be-pq-witness.log`. These are candidate checks; + the final release SHA must repeat them after the remaining evidence + updates. - **Final status:** ACCEPTED as an incompatible consensus upgrade with an owner-approved plan. The 4.8.1 release and public bit-12 activation remain gated by FINDING-067: testnet history, final-SHA qualification, From f837f1f8c9aa58aa0b3fe915fe4a5f0b8e7a30ac Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:37:40 +0200 Subject: [PATCH 096/195] [FINDING-067] Clarify voluntary PQ migration limit --- doc/RIP-0025-PQ-Assets-Followup.md | 5 +++++ doc/RIP-0025-v4.8-security-remediation-register.md | 5 ++++- doc/release-notes/release-notes-4.8.1.md | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-PQ-Assets-Followup.md b/doc/RIP-0025-PQ-Assets-Followup.md index e72c65c1b6..a8406288ea 100644 --- a/doc/RIP-0025-PQ-Assets-Followup.md +++ b/doc/RIP-0025-PQ-Assets-Followup.md @@ -532,6 +532,11 @@ classical spending rule and migrate voluntarily, with no sunset. The PQ-only format and wallet path passed paired CI on `9151946ad` under FINDING-158; public activation and release qualification remain pending. +This means a holder can reach PQ-only custody after migrating all historical +UTXOs, but consensus continues to accept classical spends of unmigrated +historical outputs. Bits 12 and 13 do not by themselves impose a global +PQ-only spending rule on the earlier UTXO set. + An input carrying a native witness-v2 RVN output with the matching PQ program provides the ML-DSA authorization for an asset input. The asset input has an empty scriptSig and needs no ECDSA signature. The separate native input still diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index bbb93747a3..e65f2cde1f 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4470,7 +4470,10 @@ The legacy asset and `ASSET!` migration path is voluntary and has no sunset. Newly created post-bit-13 asset outputs need an ML-DSA anchor and no ECDSA signature; the synthetic asset identifier has no classical private key. FINDING-148, 150, 151, and 155 remain documented fail-safe -operational limits rather than closed automatic funding features. +operational limits rather than closed automatic funding features. A holder +reaches PQ-only custody after migrating every historical RVN, asset, and +owner-token UTXO they hold. Bits 12 and 13 do not globally disable +classical spends of unmigrated historical outputs. **2026-10-13 bit-13 parameter CI checkpoint:** Build Raven run `38335505917` and RIP-25 v4.8 Final Gate run `38335508527` both passed diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md index e2b7f5e5ea..e20b87fffe 100644 --- a/doc/release-notes/release-notes-4.8.1.md +++ b/doc/release-notes/release-notes-4.8.1.md @@ -69,6 +69,11 @@ condition until their owners move them to new protected outputs. Activation does not retroactively protect them. If bit 12 fails or activates too late, the bit-13 window must be reviewed before it starts. +There is no automatic network-wide point at which only PQ keys can spend +all existing holdings. Each owner reaches PQ-only custody after moving every +historical RVN, asset, and owner-token UTXO they hold. Unmigrated historical +outputs remain spendable with their original classical keys. + For the asset format, wallet recovery routes, and current limitations in automatic anchor refresh and raw transaction funding, see [RIP-25 PQ Asset Extension Design Note](../RIP-0025-PQ-Assets-Followup.md). From 7c1a721a0a4bf932142c8feb758cf035c71dd8d3 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:59:37 +0200 Subject: [PATCH 097/195] [FINDING-067] Record clean testnet chainstate replay --- doc/RIP-0025-v4.8-security-remediation-register.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index e65f2cde1f..3a7e6f9edd 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4522,6 +4522,20 @@ itself as `v4.8.0.0-a809f6757-dirty`; its exact clean source provenance is not established. A full chainstate revalidation with a clean final 4.8.1 binary and `-assumevalid=0` remains required before release. +**2026-10-13 clean 4.8.1 testnet replay checkpoint:** A disconnected, +unpruned, no-wallet testnet node rebuilt the chainstate with +`-reindex-chainstate -assumevalid=0` from genesis through height 2551779. +The running binary was the clean 4.8.1 build from `17e2d5c97`, SHA-256 +`6abe722ad054020baebd48e9e223e966dbebb4aeaf6397bbee01e17036cc46fe`. +It reached the known headers with no RPC warnings, at hash +`000006bfde295576c7ed2dcb3f832f3ca9331c16644b35099a8591250917070f` +and chainwork +`000000000000000000000000000000000000000000000000000f229e8eb57277`. +At height 2551767 the block hash matched the earlier full-block scan. +The private provenance is `evidence/testnet-reindex-17e-final.json`. +This validates the scanned history with a clean 4.8.1 candidate; +the final source SHA must repeat the replay before release. + **2026-10-13 bit-12 mainnet parameter decision and implementation:** The owner selected 2027-07-01 00:00 UTC (`1814400000`) for STARTED and 2029-01-01 00:00 UTC (`1861920000`) for timeout, an 18-month window at From cfc439c6f8e80a612e2ccdfd602fcb8a88f9002b Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:00:45 +0200 Subject: [PATCH 098/195] [FINDING-169][FINDING-170] Record paired cache CI success --- ...0025-v4.8-security-remediation-register.md | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 3a7e6f9edd..81b850c3ed 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8895,10 +8895,15 @@ Cross-platform CI and public deployment remain separate release gates. job plus all eight builds in Final Gate `38395942637` passed. Both security jobs ran the cache regression successfully. Build Raven as a whole failed only in its ARM64 FreeType fetch, recorded in FINDING-171. -- **Final status:** FIXED locally for the reproduced missing-peer failure - mode and verified in the two required security jobs. Release - qualification still requires a wholly green Build Raven and Final Gate - pair at one final SHA. + On exact versioned SHA + `bfa2efbb3f47dca3e32df3c73b74625d5c64b5ee`, Build Raven + `38433449366` and Final Gate `38433455013` both completed successfully. + The required security jobs passed the cache regression and mandatory + `make check`; Build Raven completed all five release targets and Final + Gate all eight builds. +- **Final status:** FIXED on the paired versioned candidate SHA. The + final release SHA still requires the same two wholly green workflows + after the later optional-tool repair in FINDING-172. ### FINDING-170: Standalone cache peer regression cannot locate ravend in CI @@ -8944,9 +8949,14 @@ Cross-platform CI and public deployment remain separate release gates. passed the standalone cache regression. Final Gate passed all eight builds; Build Raven's ARM64 artifact failed for the separate FreeType source timeout in FINDING-171. -- **Final status:** FIXED locally and verified in both required CI - security jobs. A wholly green same-SHA Build Raven and Final Gate pair - is still required for release qualification. + On exact versioned SHA + `bfa2efbb3f47dca3e32df3c73b74625d5c64b5ee`, Build Raven + `38433449366` and Final Gate `38433455013` both completed successfully + with mandatory `make check`, all five release targets, and all eight + Final Gate builds. +- **Final status:** FIXED on the paired versioned candidate SHA. The + final release SHA still requires the same two wholly green workflows + after the later optional-tool repair in FINDING-172. ### FINDING-171: FreeType mirror timeout again blocks the ARM64 release artifact From 1a496a0a5d74ad31f0a88faad908468151545ecf Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:01:26 +0200 Subject: [PATCH 099/195] [FINDING-167][FINDING-171] Record paired FreeType CI success --- ...0025-v4.8-security-remediation-register.md | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 81b850c3ed..ac47e19bf7 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8780,10 +8780,15 @@ Cross-platform CI and public deployment remain separate release gates. revised recipe passed and checked the same digest. The red CI run is `38234048442`. Build Raven `38296258242` built the aarch64 artifact `11771315730` and the other four release targets at `9151946ad`; - paired Final Gate `38296262444` succeeded at that SHA. -- **Final status:** REOPENED for the 4.8.1 release after the same ARM64 - source-fetch class recurred in Build Raven `38395943288`, as recorded in - FINDING-171. The earlier green candidate remains valid for its SHA. + paired Final Gate `38296262444` succeeded at that SHA. After the + recurrence and fallback repair in FINDING-171, Build Raven + `38433449366` completed the ARM64 artifact `11837205453` and the + other four release targets on exact versioned SHA + `bfa2efbb3f47dca3e32df3c73b74625d5c64b5ee`; Final Gate + `38433455013` passed on the same SHA. +- **Final status:** FIXED on the paired versioned candidate SHA after + the fallback repair in FINDING-171. Final release-SHA artifact + qualification remains due. ### FINDING-168: Wallet rewrite recovery can leave PQ plaintext in the database file @@ -8998,8 +9003,14 @@ Cross-platform CI and public deployment remain separate release gates. The same forced primary failure followed by altered fallback bytes failed its hash check and left no source archive in place. The red CI evidence remains Build Raven `38395943288`, job `115296217551`. -- **Final status:** FIXED locally; all five Build Raven artifacts and - Final Gate must pass at one exact post-remediation SHA. FINDING-067 + On exact versioned SHA + `bfa2efbb3f47dca3e32df3c73b74625d5c64b5ee`, Build Raven + `38433449366` passed all five release targets, including ARM64 + artifact `11837205453`, and Final Gate `38433455013` passed all + eight builds and its required security job. Both security jobs passed + mandatory `make check`. +- **Final status:** FIXED on the paired versioned candidate SHA; + final release-SHA artifact qualification remains due. FINDING-067 remains OPEN. ### FINDING-172: Optional raven-tx cannot link the asset and PQ helpers From a16a05bdb925985bc33694455e582cd7d2261958 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:04:24 +0200 Subject: [PATCH 100/195] [FINDING-067] Record versioned paired CI and artifacts --- ...0025-v4.8-security-remediation-register.md | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index ac47e19bf7..00824d6531 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4548,14 +4548,33 @@ change, the new test failed on both old timestamps, `1798761600` and STARTED, the 1713/1714 signal boundary, LOCKED_IN, ACTIVE, and timeout FAILED transitions. The complete `versionbits_tests` and `rip25_versionbits_tests` suites passed, as did local `make -j8 check`. -The bit-12 dates are now implemented locally, but this SHA has not yet -passed Build Raven and Final Gate. The bit-13 mainnet STARTED timestamp +The bit-12 dates passed the versioned paired CI checkpoint below, +but final release-SHA qualification remains due. The bit-13 mainnet +STARTED timestamp equals the bit-12 timeout; this does not imply bit 12 has activated. If bit 12 fails or activates too late for the asset rollout, the bit-13 window must be reviewed in a separate release before it starts. Commit `948b1ea20` records the approved schedule and operator plan in the protocol and asset notes and adds draft versioned 4.8.1 release notes. +**2026-10-13 versioned 4.8.1 paired CI checkpoint:** Build Raven +`38433449366` and Final Gate `38433455013` both passed on exact SHA +`bfa2efbb3f47dca3e32df3c73b74625d5c64b5ee`. Both required +security jobs completed mandatory `make check` and behavioral tests; +Final Gate completed all eight builds. Build Raven produced all five +release targets: Linux x86_64 artifact `11835601861`, arm32v7 +`11836926163`, aarch64 `11837205453`, Windows MinGW `11835885092`, +and macOS `11836521468`. All files named by their downloaded +`SHA256SUMS` matched their bytes, each standalone SHA-256 file agreed, +each `PROVENANCE.txt` named this exact commit and target, and all five +targets contained the same source archive digest +`94ed2505ad5f64482faf979133531514ba5b2a2bfb6fa575c410e77effaa0d98`. +The Windows unsigned installer and macOS unsigned DMG also matched +their manifests. The private detailed evidence is +`evidence/ci-bfa2efbb3/artifact-verification.json`. This candidate +precedes the optional `raven-tx` repair in FINDING-172 and the latest +documentation; it is not the final release SHA. + FINDING-067 remains HIGH and OPEN. The outstanding gates are clean final-SHA revalidation of the scanned testnet history, final 4.8.1 source and artifact qualification at one SHA with mandatory `make check`, a repeat of the From 867e3f6337616fb5dc7b9bb26b0a87544517e384 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:52:41 +0200 Subject: [PATCH 101/195] [FINDING-067] Record 4.8.1 CI and testnet qualification --- ...0025-v4.8-security-remediation-register.md | 85 ++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 00824d6531..0b692bc06f 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4613,6 +4613,80 @@ the Qt help command ran under a virtual X display. The optional sources and libraries before the Automake repair in FINDING-172. Manual end-user GUI interaction on the final SHA remains due. +**2026-10-14 exact-candidate qualification checkpoint:** Build Raven +38475261912 and RIP-25 v4.8 Final Gate 38475261551 both completed +successfully on the same versioned 4.8.1 source commit +a16a05bdb925985bc33694455e582cd7d2261958. Both required security +jobs passed mandatory make check and the optional raven-tx build. +Final Gate passed all eight build jobs. Build Raven produced Linux x86_64 +artifact 11858960806, aarch64 11858772223, arm32v7 11858057542, +Windows MinGW 11857481025, and macOS 11856949763. Every downloaded +file matched its target SHA256SUMS manifest and standalone checksum; +each PROVENANCE.txt named the exact source commit and target. All five +targets contained the same source archive with SHA-256 +e4d5ec80c1808c2c6ddfc659e9d036c0624d5e3594c1bba62a671a2a07434a32. +The unsigned Windows installer and macOS DMG also matched their +manifests. The detailed private result is +evidence/ci-a16a05bdb/artifact-verification.json. These checks prove +package integrity and provenance, not GUI behavior on every platform. + +The clean a16a05bdb daemon, binary SHA-256 +48610aafe1d145586419fda9920517442930743f8b1b937ca4c5b56df289267e, +replayed the available non-pruned testnet chainstate from genesis with +-reindex-chainstate and -assumevalid=0. It reached all 2,551,779 locally +known headers, tip +000006bfde295576c7ed2dcb3f832f3ca9331c16644b35099a8591250917070f, +chainwork 000000000000000000000000000000000000000f229e8eb57277, +and no RPC warnings. The first history scan had read heights 0 through +2,551,767; the clean replay validated those blocks and matched its last +checkpoint. + +The same binary then connected to four public testnet peers and +validated 897 more blocks. At the frozen 2026-10-14 04:49:44 UTC +snapshot, blocks and headers both equaled 2,552,676, tip +0000066f4e0492e34a25315b1500952a3f6e5d3818ffae785f7e364a702c5456. +The scanner had covered all 2,552,677 active-chain blocks and +2,773,068 transactions. It found zero native witness-v2 outputs, +native spends, wrapped spends, and malformed candidates, with 1,271 +verified checkpoints. Exactly 2,552,677 block records were stored; +the maximum serialized length was 1,998,152 bytes. The upper bound +of four times that size is 7,992,608 WU, below Core 4.8.0's 8-MWU +limit. This size bound supplements, but does not replace, consensus +replay. A deep verifychain level 4 over the last 2016 blocks +completed with no coin database inconsistencies in 2017 blocks. +The UTXO-set digest was +f751e2ca4150daeb14bb0ec964089dc939273ad1b23bcfc4856f8aa678cce281. +The attempted all-history verifychain level 2 was interrupted after +about 11 minutes to allow live synchronization. Its true RPC response +on daemon shutdown is not counted as a passing check. Private evidence +is in testnet-live-sync-a16-final.json, +testnet-history-scan-a16-live.json, testnet-block-record-size-a16-live.json, +and testnet-verifychain-a16.json. + +The live RPC warning said unknown new rules activated, versionbit 11. +Bit 11 is the known transfer-overflow deployment, ACTIVE since height +2,493,792. None of the latest 2016 block headers signaled bits 11, +12, or 13. The warning-checking code exists unchanged in official +Core 4.8.0. This historical warning is recorded in +testnet-versionbit11-a16-final.json; it is not evidence that bit 12 +or bit 13 activated. Public testnet bit 13 remains DEFINED until its +2027 window. The scan covers the locally available active chain, +not unknown side branches or the future public activation. + +The exact-candidate two-binary FINDING-160 tests also passed: both +Core 4.8.0 and a16a05bdb accept a 7,600,636-WU control block; +Core 4.8.0 rejects an 8,801,324-WU block and an 8,000,697-WU +old-accounting PQ witness block that the candidate accepts after +bit-12 activation. The approved incompatible-upgrade plan remains +necessary. + +FINDING-067 remains HIGH and OPEN. Remaining gates are operator +readiness and published upgrade instructions for bit 12, manual +GUI exercise on release operating systems, final release notes and +signed checksums, CI and package provenance on the actual release +commit if it differs from this checkpoint, and the owner's separate +approval before a tag or GitHub release. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical @@ -9077,7 +9151,12 @@ Cross-platform CI and public deployment remain separate release gates. `--with-tx` built `libraven_tx_core.a` and linked `raven-tx` through Automake. Its `--help` and `-create -json` smoke checks passed, as did `make -j4 check`. The aggregate archive contains 152 core objects. - Linux CI and other platform builds of this optional target remain due. -- **Final status:** FIXED locally; the required CI security builds must - confirm the Automake target on an exact post-remediation SHA. The five + Both required Linux CI security jobs now exercise this optional target. + Cross-platform execution of the optional tool is outside the release + artifact matrix. +- **Final status:** FIXED. The required CI security jobs in Build Raven + 38475261912 and Final Gate 38475261551 both built the optional + Automake raven-tx target and passed mandatory make check on exact + SHA a16a05bdb925985bc33694455e582cd7d2261958. All five Build + Raven artifacts and eight Final Gate builds also passed. The five release artifacts do not contain this optional utility. From fe3e5e5185aa8b19713bbc55978bc9fb5e2a219f Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:03:22 +0200 Subject: [PATCH 102/195] [FINDING-067] Reconcile release blockers and testnet limits --- ...0025-v4.8-security-remediation-register.md | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 0b692bc06f..35cffe21b4 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4406,7 +4406,7 @@ FINDING-067 remains OPEN. at `9151946ad27857d4d4c0016c74819476d1296557`. The mandatory `make check`, wallet rewrite and full-page-clear regressions, nine focused security functional tests, and the 32-case functional matrix passed in the -required security jobs. Final Gate also completed its eight build jobs. +required security jobs. Final Gate also completed its seven build jobs. Build Raven produced five unsigned artifacts on that SHA: Linux x86_64 `11770728793`, aarch64 `11771315730`, arm32v7 `11770548092`, Windows MinGW `11771318535`, and macOS `11771367693`. The Linux artifact's @@ -4561,7 +4561,7 @@ protocol and asset notes and adds draft versioned 4.8.1 release notes. `38433449366` and Final Gate `38433455013` both passed on exact SHA `bfa2efbb3f47dca3e32df3c73b74625d5c64b5ee`. Both required security jobs completed mandatory `make check` and behavioral tests; -Final Gate completed all eight builds. Build Raven produced all five +Final Gate completed all seven builds. Build Raven produced all five release targets: Linux x86_64 artifact `11835601861`, arm32v7 `11836926163`, aarch64 `11837205453`, Windows MinGW `11835885092`, and macOS `11836521468`. All files named by their downloaded @@ -4586,7 +4586,7 @@ review draft until the final SHA and artifacts are known. **2026-10-13 bit-12 CI checkpoint:** RIP-25 v4.8 Final Gate run `38366157600` passed on exact SHA `7c0d43c85550afc310a7a3a2e458fda3a274fc4f`, -including its security tests and all eight build jobs. Paired Build Raven +including its security job and all seven build jobs. Paired Build Raven run `38366146893` failed while creating the functional test cache in its required behavioral security step. Its five artifact jobs were skipped. The cause and remediation are tracked in FINDING-169. This is not a green @@ -4618,7 +4618,7 @@ Manual end-user GUI interaction on the final SHA remains due. successfully on the same versioned 4.8.1 source commit a16a05bdb925985bc33694455e582cd7d2261958. Both required security jobs passed mandatory make check and the optional raven-tx build. -Final Gate passed all eight build jobs. Build Raven produced Linux x86_64 +Final Gate passed its security job and all seven build jobs. Build Raven produced Linux x86_64 artifact 11858960806, aarch64 11858772223, arm32v7 11858057542, Windows MinGW 11857481025, and macOS 11856949763. Every downloaded file matched its target SHA256SUMS manifest and standalone checksum; @@ -4633,8 +4633,8 @@ package integrity and provenance, not GUI behavior on every platform. The clean a16a05bdb daemon, binary SHA-256 48610aafe1d145586419fda9920517442930743f8b1b937ca4c5b56df289267e, replayed the available non-pruned testnet chainstate from genesis with --reindex-chainstate and -assumevalid=0. It reached all 2,551,779 locally -known headers, tip +-reindex-chainstate and -assumevalid=0. It reached height 2,551,779, +or 2,551,780 locally known headers including genesis, tip 000006bfde295576c7ed2dcb3f832f3ca9331c16644b35099a8591250917070f, chainwork 000000000000000000000000000000000000000f229e8eb57277, and no RPC warnings. The first history scan had read heights 0 through @@ -4673,6 +4673,18 @@ or bit 13 activated. Public testnet bit 13 remains DEFINED until its 2027 window. The scan covers the locally available active chain, not unknown side branches or the future public activation. +The live testnet `getblocktemplate` at height 2,552,686 advertised +16,000,000 for both size and weight, with bit 12 available for +signaling while the raw `pq_hybrid` BIP9 state was STARTED. This is the +intentional testnet `nPQHybridEnabled=true` override: native RIP-25 is +effective from height zero and its nominal-year phase 2 has already +elapsed. The assembler and validator use this contextual forced-active +limit. Current public testnet therefore cannot demonstrate the native +8-to-12-MWU BIP9 transition; the controlled regtest boundary vectors +provide that proof. The operator plan must identify this testnet/mainnet +difference. The private live chain, peer, and template record is +`evidence/testnet-live-template-a16.json`. + The exact-candidate two-binary FINDING-160 tests also passed: both Core 4.8.0 and a16a05bdb accept a 7,600,636-WU control block; Core 4.8.0 rejects an 8,801,324-WU block and an 8,000,697-WU @@ -4680,12 +4692,18 @@ old-accounting PQ witness block that the candidate accepts after bit-12 activation. The approved incompatible-upgrade plan remains necessary. -FINDING-067 remains HIGH and OPEN. Remaining gates are operator -readiness and published upgrade instructions for bit 12, manual -GUI exercise on release operating systems, final release notes and -signed checksums, CI and package provenance on the actual release -commit if it differs from this checkpoint, and the owner's separate -approval before a tag or GitHub release. +FINDING-067 remains HIGH and OPEN. CRITICAL FINDING-104 through +FINDING-107 also remain OPEN for release qualification: the current +contextual repairs and tests do not yet cover every required mined +activation and side-branch boundary. The active-chain testnet replay +does not close those reorg gaps. FINDING-112 remains OPEN for wallet +test-gate reliability. Other unresolved inherited wallet and asset +limitations retain their individual statuses below. Remaining rollout +gates are operator readiness and published upgrade instructions for bit +12, manual GUI exercise on release operating systems, final release +notes and signed checksums, CI and package provenance on the actual +release commit if it differs from this checkpoint, and the owner's +separate approval before a tag or GitHub release. ## Finding frozen during the second adversarial review @@ -8990,7 +9008,7 @@ Cross-platform CI and public deployment remain separate release gates. original CI cause remains uncertain because its node logs were lost. On exact SHA `06f9d90fd0d213ad33b078c87fd1e9f4fd05edde`, the required security job in Build Raven `38395943288` and the security - job plus all eight builds in Final Gate `38395942637` passed. Both + job plus all seven builds in Final Gate `38395942637` passed. Both security jobs ran the cache regression successfully. Build Raven as a whole failed only in its ARM64 FreeType fetch, recorded in FINDING-171. On exact versioned SHA @@ -8998,7 +9016,7 @@ Cross-platform CI and public deployment remain separate release gates. `38433449366` and Final Gate `38433455013` both completed successfully. The required security jobs passed the cache regression and mandatory `make check`; Build Raven completed all five release targets and Final - Gate all eight builds. + Gate all seven builds. - **Final status:** FIXED on the paired versioned candidate SHA. The final release SHA still requires the same two wholly green workflows after the later optional-tool repair in FINDING-172. @@ -9158,5 +9176,5 @@ Cross-platform CI and public deployment remain separate release gates. 38475261912 and Final Gate 38475261551 both built the optional Automake raven-tx target and passed mandatory make check on exact SHA a16a05bdb925985bc33694455e582cd7d2261958. All five Build - Raven artifacts and eight Final Gate builds also passed. The five + Raven artifacts and seven Final Gate builds also passed. The five release artifacts do not contain this optional utility. From edcd4ddbb29f71d6d7ce9d8a9302cb76a250ef32 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:45:32 +0200 Subject: [PATCH 103/195] [FINDING-104] Extend transfer activation boundary test --- test/functional/feature_transfer_script_reorg.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/functional/feature_transfer_script_reorg.py b/test/functional/feature_transfer_script_reorg.py index c750b7b2de..2d812f7425 100755 --- a/test/functional/feature_transfer_script_reorg.py +++ b/test/functional/feature_transfer_script_reorg.py @@ -27,8 +27,20 @@ def run_test(self): script = node.decoderawtransaction(raw)['vout'][0]['scriptPubKey']['hex'] assert_equal(script[52:54], '4c') # OP_PUSHDATA1 after P2PKH and OP_RVN_ASSET. - node.generate(287) + # The tip first reports the ACTIVE next-block state at height 863. + # Check the two preceding tips with the identical OP_PUSHDATA1 + # transaction before crossing that boundary. + node.generate(286) + assert_equal(node.getblockcount(), 861) assert_equal(node.getblockchaininfo()['bip9_softforks']['transfer_script']['status'], 'locked_in') + before_two = node.testmempoolaccept([raw])[0] + assert_equal(before_two['reject-reason'], '16: bad-txns-transfer-asset-bad-deserialize') + + node.generate(1) + assert_equal(node.getblockcount(), 862) + assert_equal(node.getblockchaininfo()['bip9_softforks']['transfer_script']['status'], 'locked_in') + before_one = node.testmempoolaccept([raw])[0] + assert_equal(before_one['reject-reason'], before_two['reject-reason']) locked_in_tip = node.getbestblockhash() issue_txid = node.issue(asset_name, 2)[0] active_block = None @@ -58,6 +70,8 @@ def run_test(self): assert_equal(message_outputs[0]['asset']['amount'], 1) assert_equal(message_outputs[0]['asset']['expire_time'], 123456789) assert_equal(message_outputs[0]['hex'][52:54], '4c') + after_one = node.testmempoolaccept([raw])[0] + assert_equal(after_one['reject-reason'], active_result['reject-reason']) # Recheck blocks on both sides of the activation boundary. assert_equal(node.verifychain(4, 4), True) From 241d1517fbb147b122e7c17098c661440ef5d32a Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:45:38 +0200 Subject: [PATCH 104/195] [FINDING-105] Test mined asset activation boundary --- .../feature_asset_activation_reorg.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/functional/feature_asset_activation_reorg.py b/test/functional/feature_asset_activation_reorg.py index 31497dd2b6..10fc70cb84 100755 --- a/test/functional/feature_asset_activation_reorg.py +++ b/test/functional/feature_asset_activation_reorg.py @@ -4,7 +4,13 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Asset activation policy must not depend on whether the node saw ACTIVE.""" +import configparser +import os + +from test_framework.blocktools import create_block, create_coinbase +from test_framework.mininode import CTransaction, from_hex from test_framework.test_framework import RavenTestFramework +from test_framework import util as test_util from test_framework.util import Decimal, assert_equal @@ -38,9 +44,49 @@ def run_test(self): 'units': 0, 'reissuable': 1, 'has_ipfs': 0}}}) raw = node.signrawtransaction(raw)['hex'] + active_height = node.getblockcount() + active_accept = node.testmempoolaccept([raw])[0] + assert_equal(active_accept['allowed'], True) + issue_txid = node.sendrawtransaction(raw) + issue_block = node.generate(1)[0] + assert_equal(node.getblockcount(), active_height + 1) + assert issue_txid in node.getblock(issue_block)['tx'] + after_block = node.generate(1)[0] + assert_equal(node.getblockcount(), active_height + 2) + assert_equal(node.verifychain(4, 3), True) + node.invalidateblock(active_block) assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'locked_in') live_result = node.testmempoolaccept([raw])[0] + assert_equal(live_result['allowed'], False) + + # Check one more pre-activation tip with the same signed issue. + locked_tip = node.getbestblockhash() + config = configparser.ConfigParser() + config.read(self.options.configfile) + test_util.x16r_hash_cmd = os.path.join( + config['environment']['BUILDDIR'], 'src', 'test', + 'test_raven_hash' + config['environment']['EXEEXT']) + block_time = node.getblockheader(locked_tip)['time'] + 1 + invalid_issue = create_block(int(locked_tip, 16), create_coinbase(active_height), block_time) + invalid_issue.nVersion = node.getblockheader(active_block)['version'] + invalid_issue.vtx.append(from_hex(CTransaction(), raw)) + invalid_issue.hashMerkleRoot = invalid_issue.calc_merkle_root() + invalid_issue.solve() + rejected = node.submitblock(invalid_issue.serialize().hex()) + self.log.info('Pre-activation issue block rejected: %s', rejected) + assert_equal(rejected, 'bad-txns-assets-not-active') + assert_equal(node.getbestblockhash(), locked_tip) + + node.invalidateblock(locked_tip) + before_two = node.testmempoolaccept([raw])[0] + assert_equal(before_two['allowed'], False) + node.reconsiderblock(locked_tip) + # Reconsidering an ancestor also revives its descendants. Keep the + # ACTIVE branch invalid while comparing the LOCKED_IN tip to restart. + node.invalidateblock(active_block) + assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'locked_in') + assert_equal(node.testmempoolaccept([raw])[0]['allowed'], False) self.stop_node(0) self.start_node(0, ['-assetindex', '-persistmempool=0']) @@ -49,6 +95,11 @@ def run_test(self): restarted_result = node.testmempoolaccept([raw])[0] assert_equal(live_result['allowed'], restarted_result['allowed']) + node.reconsiderblock(active_block) + assert_equal(node.getbestblockhash(), after_block) + assert issue_txid in node.getblock(issue_block)['tx'] + assert_equal(node.verifychain(4, 3), True) + if __name__ == '__main__': AssetActivationReorgTest().main() From 209cd937a7ae4bb7caf13c6cfb7497adf46ce03f Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:56:32 +0200 Subject: [PATCH 105/195] [FINDING-106] Test candidate deployment boundaries --- src/test/rip25_versionbits_tests.cpp | 30 +++- .../feature_rip5_activation_reorg.py | 165 ++++++++++++++---- 2 files changed, 155 insertions(+), 40 deletions(-) diff --git a/src/test/rip25_versionbits_tests.cpp b/src/test/rip25_versionbits_tests.cpp index 45d6cc64ae..bbe4cba8d9 100644 --- a/src/test/rip25_versionbits_tests.cpp +++ b/src/test/rip25_versionbits_tests.cpp @@ -298,14 +298,23 @@ BOOST_FIXTURE_TEST_CASE(asset_deployment_queries_rewind_after_active_tip, Transf lockedBranch.Mine(period - threshold, VERSIONBITS_TOP_BITS); SyntheticVersionBitsChain activeBranch(lockedBranch.Tip()); - activeBranch.Mine(period, VERSIONBITS_TOP_BITS); + activeBranch.Mine(period - 1, VERSIONBITS_TOP_BITS); + CBlockIndex* beforeActivation = activeBranch.Tip(); + activeBranch.Mine(1, VERSIONBITS_TOP_BITS); + CBlockIndex* activationParent = activeBranch.Tip(); + activeBranch.Mine(2, VERSIONBITS_TOP_BITS); + CBlockIndex* afterActivation = activeBranch.Tip(); SyntheticVersionBitsChain startedBranch(common.Tip()); startedBranch.Mine(2 * period, VERSIONBITS_TOP_BITS); VersionBitsCache expectedCache; BOOST_REQUIRE_EQUAL(VersionBitsState(lockedBranch.Tip(), params, check.deployment, expectedCache), THRESHOLD_LOCKED_IN); - BOOST_REQUIRE_EQUAL(VersionBitsState(activeBranch.Tip(), params, check.deployment, expectedCache), THRESHOLD_ACTIVE); + BOOST_REQUIRE_EQUAL(VersionBitsState(beforeActivation, params, check.deployment, expectedCache), THRESHOLD_LOCKED_IN); + BOOST_REQUIRE_EQUAL(VersionBitsState(activationParent, params, check.deployment, expectedCache), THRESHOLD_ACTIVE); + BOOST_REQUIRE_EQUAL(VersionBitsState(afterActivation, params, check.deployment, expectedCache), THRESHOLD_ACTIVE); + BOOST_CHECK_EQUAL(VersionBitsStateSinceHeight(activationParent, params, check.deployment, expectedCache), + 3 * period); BOOST_REQUIRE_EQUAL(VersionBitsState(startedBranch.Tip(), params, check.deployment, expectedCache), THRESHOLD_STARTED); SetAssetsDeployed(false); @@ -316,16 +325,22 @@ BOOST_FIXTURE_TEST_CASE(asset_deployment_queries_rewind_after_active_tip, Transf LOCK(cs_main); originalTip = chainActive.Tip(); versionbitscache.Clear(); - chainActive.SetTip(lockedBranch.Tip()); + chainActive.SetTip(beforeActivation); } - const bool lockedResult = check.query(); + const bool beforeResult = check.query(); { LOCK(cs_main); - chainActive.SetTip(activeBranch.Tip()); + chainActive.SetTip(activationParent); } const bool activeResult = check.query(); + { + LOCK(cs_main); + chainActive.SetTip(afterActivation); + } + const bool laterResult = check.query(); + { LOCK(cs_main); chainActive.SetTip(startedBranch.Tip()); @@ -338,8 +353,9 @@ BOOST_FIXTURE_TEST_CASE(asset_deployment_queries_rewind_after_active_tip, Transf versionbitscache.Clear(); } const bool expectedLocked = check.deployment == Consensus::DEPLOYMENT_ENFORCE_VALUE; - BOOST_CHECK_MESSAGE(lockedResult == expectedLocked, check.name << " has the wrong LOCKED_IN rule"); - BOOST_CHECK_MESSAGE(activeResult, check.name << " did not activate"); + BOOST_CHECK_MESSAGE(beforeResult == expectedLocked, check.name << " has the wrong pre-activation rule"); + BOOST_CHECK_MESSAGE(activeResult, check.name << " did not activate for the first active candidate"); + BOOST_CHECK_MESSAGE(laterResult, check.name << " did not stay active for later candidates"); BOOST_CHECK_MESSAGE(!rewoundResult, check.name << " remained active after rewind"); } } diff --git a/test/functional/feature_rip5_activation_reorg.py b/test/functional/feature_rip5_activation_reorg.py index 80ef323c09..0a0b1a3a93 100755 --- a/test/functional/feature_rip5_activation_reorg.py +++ b/test/functional/feature_rip5_activation_reorg.py @@ -2,52 +2,151 @@ # Copyright (c) 2026 The Raven Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Restricted-asset activation must agree after reorg and restart.""" +"""Restricted-asset blocks must use the candidate parent's bit-7 state.""" +import configparser +import os + +from test_framework import util as test_util +from test_framework.blocktools import create_block, create_coinbase +from test_framework.mininode import CTransaction, from_hex from test_framework.test_framework import RavenTestFramework -from test_framework.authproxy import JSONRPCException -from test_framework.util import assert_equal +from test_framework.util import Decimal, assert_equal, assert_raises_rpc_error + + +ASSETS_ONLY_VERSION = 0x30000040 +ASSETS_AND_RIP5_VERSION = 0x300000c0 class Rip5ActivationReorgTest(RavenTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - self.extra_args = [['-assetindex', '-persistmempool=0']] + self.extra_args = [['-assetindex', '-persistmempool=0', + '-blockversion={}'.format(ASSETS_ONLY_VERSION)]] + + def rip5_status(self, node): + return node.getblockchaininfo()['bip9_softforks']['messaging_restricted']['status'] + + def issue_raw(self, node, asset_name): + native_coin = next(coin for coin in node.listunspent() + if coin['amount'] > 1501 and coin['confirmations'] > 101) + owner_name = asset_name[1:] + '!' + owner_coin = node.listmyassets(owner_name, True)[owner_name]['outpoints'][0] + raw = node.createrawtransaction( + [{'txid': native_coin['txid'], 'vout': native_coin['vout']}, + {'txid': owner_coin['txid'], 'vout': owner_coin['vout']}], + {'n1issueRestrictedXXXXXXXXXXXXZVT9V': 1500, + node.getnewaddress(): native_coin['amount'] - Decimal('1500.1'), + node.getnewaddress(): {'issue_restricted': { + 'asset_name': asset_name, 'asset_quantity': 1, + 'verifier_string': 'true', 'units': 0, + 'reissuable': 1, 'has_ipfs': 0}}}) + signed = node.signrawtransaction(raw) + assert_equal(signed['complete'], True) + return signed['hex'] def run_test(self): node = self.nodes[0] + activation_height = 576 node.generate(287) - assert_equal(node.getblockchaininfo()['bip9_softforks']['messaging_restricted']['status'], 'locked_in') - - active_block = None - for _ in range(150): - candidate = node.generate(1)[0] - if node.getblockchaininfo()['bip9_softforks']['messaging_restricted']['status'] == 'active': - active_block = candidate - break - assert active_block is not None - - node.listglobalrestrictions() # Exercise the cached RIP5 deployment query. - - node.invalidateblock(active_block) - assert_equal(node.getblockchaininfo()['bip9_softforks']['messaging_restricted']['status'], 'locked_in') - try: - node.listglobalrestrictions() - live_available = True - except JSONRPCException: - live_available = False - - self.stop_node(0) - self.start_node(0, ['-assetindex', '-persistmempool=0']) + assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'locked_in') + assert_equal(self.rip5_status(node), 'started') + # Signal bit 6 alone in the first STARTED period. Signal bit 7 in + # the next one so assets are active before restricted assets. + self.restart_node(0, ['-assetindex', '-persistmempool=0', + '-blockversion={}'.format(ASSETS_AND_RIP5_VERSION)]) + node = self.nodes[0] + node.generate(activation_height - 3 - node.getblockcount()) + assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'active') + assert_equal(self.rip5_status(node), 'locked_in') + base_name = 'RIP5BOUNDARY' + restricted_name = '$' + base_name + base_txid = node.issue(base_name, 1)[0] + base_block = node.generate(1)[0] + assert_equal(node.getblockcount(), activation_height - 2) + assert base_txid in node.getblock(base_block)['tx'] + assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'active') + assert_equal(self.rip5_status(node), 'locked_in') + + original_pre_activation = node.generate(1)[0] + node.generate(1) + assert_equal(node.getblockcount(), activation_height) + assert_equal(self.rip5_status(node), 'active') + assert_equal(node.listglobalrestrictions(), []) + raw = self.issue_raw(node, restricted_name) + txid = node.decoderawtransaction(raw)['txid'] + assert_equal(node.testmempoolaccept([raw])[0]['allowed'], True) + + # Rewind to H-2. A competing H-1 block must reject the same valid + # transaction even though this process has already reached ACTIVE. + node.invalidateblock(original_pre_activation) + assert_equal(node.getbestblockhash(), base_block) + assert_equal(self.rip5_status(node), 'locked_in') + assert_raises_rpc_error(-1, 'Restricted assets must be active', + node.listglobalrestrictions) + early_result = node.testmempoolaccept([raw])[0] + assert_equal(early_result['allowed'], False) + assert_equal(early_result['reject-reason'], + '16: bad-tx-null-asset-data-before-restricted-assets-activated') + + config = configparser.ConfigParser() + config.read(self.options.configfile) + test_util.x16r_hash_cmd = os.path.join( + config['environment']['BUILDDIR'], 'src', 'test', + 'test_raven_hash' + config['environment']['EXEEXT']) + block = create_block(int(base_block, 16), create_coinbase(activation_height - 1), + node.getblockheader(base_block)['time'] + 1) + block.nVersion = node.getblockheader(original_pre_activation)['version'] + block.vtx.append(from_hex(CTransaction(), raw)) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + assert_equal(node.submitblock(block.serialize().hex()), + 'bad-tx-null-asset-data-before-restricted-assets-activated') + assert_equal(node.getbestblockhash(), base_block) + + # Mempool admission at H-1 targets H, the first active block. + pre_activation = node.generate(1)[0] + assert_equal(node.getblockcount(), activation_height - 1) + assert_equal(self.rip5_status(node), 'active') + assert_equal(node.testmempoolaccept([raw])[0]['allowed'], True) + assert_equal(node.sendrawtransaction(raw), txid) + activation_block = node.generate(1)[0] + assert_equal(node.getblockcount(), activation_height) + assert txid in node.getblock(activation_block)['tx'] + assert_equal(node.getassetdata(restricted_name)['amount'], 1) + post_activation = node.generate(1)[0] + assert_equal(node.getblockcount(), activation_height + 1) + assert_equal(self.rip5_status(node), 'active') + assert_equal(node.verifychain(4, 4), True) + + # Disconnect both sides of the boundary and restart while the old + # rules apply. Reconsidering the saved branch must restore the asset. + node.invalidateblock(pre_activation) + assert_equal(node.getbestblockhash(), base_block) + assert_equal(node.getassetdata(restricted_name), None) + assert_equal(self.rip5_status(node), 'locked_in') + assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'active') + assert_raises_rpc_error(-1, 'Restricted assets must be active', + node.listglobalrestrictions) + assert_equal(node.testmempoolaccept([raw])[0]['reject-reason'], + early_result['reject-reason']) + self.restart_node(0, ['-assetindex', '-persistmempool=0', + '-blockversion={}'.format(ASSETS_AND_RIP5_VERSION)]) node = self.nodes[0] - assert_equal(node.getblockchaininfo()['bip9_softforks']['messaging_restricted']['status'], 'locked_in') - try: - node.listglobalrestrictions() - restarted_available = True - except JSONRPCException: - restarted_available = False - assert_equal(live_available, restarted_available) + assert_equal(node.getbestblockhash(), base_block) + assert_equal(self.rip5_status(node), 'locked_in') + assert_raises_rpc_error(-1, 'Restricted assets must be active', + node.listglobalrestrictions) + assert_equal(node.testmempoolaccept([raw])[0]['reject-reason'], + early_result['reject-reason']) + node.reconsiderblock(pre_activation) + assert_equal(node.getbestblockhash(), post_activation) + assert_equal(node.getblockcount(), activation_height + 1) + assert txid in node.getblock(activation_block)['tx'] + assert_equal(node.getassetdata(restricted_name)['amount'], 1) + assert_equal(node.listglobalrestrictions(), []) + assert_equal(node.verifychain(4, 4), True) if __name__ == '__main__': From 3e2d65d89597cd423751f16486e28ac9a017b033 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:56:41 +0200 Subject: [PATCH 106/195] [FINDING-107] Test side-branch activation boundary --- src/test/rip25_versionbits_tests.cpp | 4 +-- .../functional/feature_transfer_sidebranch.py | 35 +++++++++++++------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/test/rip25_versionbits_tests.cpp b/src/test/rip25_versionbits_tests.cpp index bbe4cba8d9..40a165b5ae 100644 --- a/src/test/rip25_versionbits_tests.cpp +++ b/src/test/rip25_versionbits_tests.cpp @@ -414,8 +414,8 @@ BOOST_FIXTURE_TEST_CASE(checkblock_asset_result_does_not_depend_on_unrelated_tip chainActive.SetTip(originalTip); versionbitscache.Clear(); } - BOOST_CHECK_MESSAGE(activeResult == startedResult, - "CheckBlock judged the same candidate differently after the active tip changed"); + BOOST_CHECK_MESSAGE(activeResult, activeState.GetRejectReason()); + BOOST_CHECK_MESSAGE(startedResult, startedState.GetRejectReason()); } BOOST_FIXTURE_TEST_CASE(transfer_payload_uses_explicit_candidate_context, TransferOverflowRegtestSetup) diff --git a/test/functional/feature_transfer_sidebranch.py b/test/functional/feature_transfer_sidebranch.py index 2b09f0b097..2859a6b575 100755 --- a/test/functional/feature_transfer_sidebranch.py +++ b/test/functional/feature_transfer_sidebranch.py @@ -38,12 +38,12 @@ def run_test(self): self.sync_all() assert_equal(verifier.getblockcount(), 861) assert_equal(verifier.getblockchaininfo()['bip9_softforks']['transfer_script']['status'], 'locked_in') - parent_hash = verifier.getbestblockhash() disconnect_nodes(producer, 1) disconnect_nodes(verifier, 0) producer.generate(2) verifier.generate(3) + assert_equal(producer.getblockcount(), 863) assert_equal(producer.getblockchaininfo()['bip9_softforks']['transfer_script']['status'], 'active') assert_equal(verifier.getblockchaininfo()['bip9_softforks']['transfer_script']['status'], 'active') active_tip = verifier.getbestblockhash() @@ -65,20 +65,26 @@ def run_test(self): config['environment']['BUILDDIR'], 'src', 'test', 'test_raven_hash' + config['environment']['EXEEXT']) - block_time = verifier.getblockheader(parent_hash)['time'] + 1 - block = create_block(int(parent_hash, 16), create_coinbase(862), block_time) - block.nVersion = verifier.getblockheader(verifier.getblockhash(862))['version'] - block.vtx.append(from_hex(CTransaction(), transfer_raw)) - block.hashMerkleRoot = block.calc_merkle_root() - block.solve() - assert_equal(verifier.submitblock(block.serialize().hex()), - 'bad-txns-transfer-asset-bad-deserialize') - assert_equal(verifier.getbestblockhash(), active_tip) + # Both candidate blocks immediately before activation use the old + # parser, even though the verifier's current tip has reached ACTIVE. + for candidate_height in (862, 863): + candidate_parent = verifier.getblockhash(candidate_height - 1) + block_time = verifier.getblockheader(candidate_parent)['time'] + 1 + block = create_block(int(candidate_parent, 16), + create_coinbase(candidate_height), block_time) + block.nVersion = verifier.getblockheader(verifier.getblockhash(candidate_height))['version'] + block.vtx.append(from_hex(CTransaction(), transfer_raw)) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + assert_equal(verifier.submitblock(block.serialize().hex()), + 'bad-txns-transfer-asset-bad-deserialize') + assert_equal(verifier.getbestblockhash(), active_tip) # The producer's branch has a different height-862 parent history. Its # transfer is mined only after bit 8 is active on that branch. branch_start = producer.getblockhash(862) transfer_block = producer.generate(1)[0] + assert_equal(producer.getblockcount(), 864) assert txid in producer.getblock(transfer_block)['tx'] producer.generate(2) branch_tip = producer.getbestblockhash() @@ -94,6 +100,15 @@ def run_test(self): assert_equal(verifier.getbestblockhash(), branch_tip) assert_equal(verifier.getblockchaininfo()['bip9_softforks']['transfer_script']['status'], 'active') assert txid in verifier.getblock(transfer_block)['tx'] + transfer_tx = next(tx for tx in verifier.getblock(transfer_block, 2)['tx'] + if tx['txid'] == txid) + message_outputs = [out['scriptPubKey'] for out in transfer_tx['vout'] + if out['scriptPubKey'].get('asset', {}).get('message') == 'ab' * 32] + assert_equal(len(message_outputs), 1) + assert_equal(message_outputs[0]['asset']['name'], asset_name) + assert_equal(message_outputs[0]['asset']['amount'], 1) + assert_equal(message_outputs[0]['asset']['expire_time'], 123456789) + assert_equal(message_outputs[0]['hex'][52:54], '4c') assert_equal(verifier.verifychain(4, 6), True) # Walk backward over both forks and the activation boundary, restart From 975ab4f4a49d6217fa213da87384950b4e21290d Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:57:43 +0200 Subject: [PATCH 107/195] [FINDING-067] Record testnet and boundary qualification evidence --- ...0025-v4.8-security-remediation-register.md | 60 +++++++++++++++++++ doc/release-notes/release-notes-4.8.1.md | 24 +++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 35cffe21b4..e89d45ffa0 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4663,6 +4663,22 @@ is in testnet-live-sync-a16-final.json, testnet-history-scan-a16-live.json, testnet-block-record-size-a16-live.json, and testnet-verifychain-a16.json. +A later 2026-10-14 09:46:57 UTC snapshot, with the same clean a16a05bdb +binary still running, reached height 2,552,938 and tip +00000f98a1138d092840a074b6896b1e50fc3b492f43e9f3bcfc9235d2a45f7d. +The complete active-chain scan covered 2,552,939 blocks and 2,773,330 +transactions, again with zero native witness-v2 outputs or spends, +wrapped spends, and malformed candidates; it checked 1,272 checkpoints. +The corresponding 2,552,939 block records still had a maximum size of +1,998,152 bytes, yielding the same 7,992,608-WU old-rule upper bound. +A fresh verifychain level 4 over the last 2016 blocks returned true in +24.3 seconds while the node remained running. The private records are +testnet-history-scan-a16-later.json, +testnet-block-record-size-a16-later.json, and +testnet-verifychain-a16-later.json. This later scan strengthens the +active-chain evidence but does not demonstrate public PQ spending or +unknown side branches. + The live RPC warning said unknown new rules activated, versionbit 11. Bit 11 is the known transfer-overflow deployment, ACTIVE since height 2,493,792. None of the latest 2016 block headers signaled bits 11, @@ -6765,6 +6781,17 @@ Cross-platform CI and public deployment remain separate release gates. also passes. The parser is still tip-contextual in candidate-block and disconnect paths, so this is not a complete consensus correction. - **Mitigation commit:** `cb86a3ca566bfc884313eb747cbae2cd6d224b65`. +- **Additional boundary regression:** `edcd4ddbb` checks the identical + raw `OP_PUSHDATA1` transfer at LOCKED_IN tips 861 and 862, at the + first ACTIVE next-block state at tip 863, and after a real transfer is + mined at height 864. Both pre-ACTIVE tips reject the transfer payload; + the ACTIVE tips reach the expected missing-input result. Invalidation, + restart, reconsideration, and the real transfer block remain in the + same passing functional test. The private run log is + evidence/f104-expanded-boundary-final.log. This extends the previously + recorded vulnerable-binary red and corrected-binary green regression. It does + not yet supply a mined `OP_PUSHDATA1` accept/reject vector at every + requested height around the boundary. - **Final status:** MITIGATED locally, OPEN for release qualification. ### FINDING-105: Asset deployment activation survives a reorg incorrectly @@ -6811,6 +6838,17 @@ Cross-platform CI and public deployment remain separate release gates. `make check` also passes. Candidate-block and disconnect paths still need explicit parent context, so this is not a complete consensus correction. - **Mitigation commit:** `cb86a3ca566bfc884313eb747cbae2cd6d224b65`. +- **Additional mined boundary regression:** `241d1517f` mines a signed + issue in the first block after the tip reports ACTIVE and verifies the + next block. On rollback, a hand-built issue block at the pre-ACTIVE + boundary is rejected with `bad-txns-assets-not-active`. The same raw + issue is rejected in the mempool at two earlier tips, then checked + after restart and accepted again when the ACTIVE branch is + reconsidered. The direct Python 3.10 functional run passed. The + private run log is evidence/f105-expanded-boundary-final.log. The + original vulnerable-binary red and corrected-binary green reorg + comparison remains recorded above. A complete mined H-2 through H+2 + and alternate-side-branch matrix is still due. - **Final status:** MITIGATED locally, OPEN for release qualification. ### FINDING-106: Additional Core 4.8.0 deployment latches survive reorgs @@ -6871,6 +6909,18 @@ Cross-platform CI and public deployment remain separate release gates. - **Mitigation commits:** `1a7a2030a8f6fde4d3e04e6140df75463cd16b0e` (RIP5), `8a4cd56cac2af9d124033118c69406d29e692ec4` (boundary test), `b912239dfd494bcb594cb0190161140b45fcb15d` (value and coinbase). +- **Additional candidate-block boundary regression:** `209cd937a` extends + the synthetic VersionBits test to the last LOCKED_IN parent, first ACTIVE + parent, and two later ACTIVE parents for all three predicates. It also + isolates bit 7 from already-active bit 6 in the RIP5 functional test: + the same valid signed restricted issue is rejected by a mined candidate + block immediately before bit 7 activation, accepted and mined at its + first active height, then verified after rollback, restart, and + reconsideration. Nine VersionBits unit cases and the direct Python 3.10 + functional run passed. Private logs are + evidence/f106-107-versionbits-241d-final.log and + evidence/f106-rip5-boundary-241d-final.log. Enforced-value and + coinbase mined-block vectors are still due; this finding remains open. - **Final status:** OPEN for release qualification. ### FINDING-107: Context-free block checks consume active-tip asset state @@ -6965,6 +7015,16 @@ Cross-platform CI and public deployment remain separate release gates. `ravend` selected explicitly. One initial direct invocation accidentally selected an older executable on `PATH` and returned `inconclusive`; that was a test-command error, not a result from the audited binary. +- **Additional side-branch boundary regression:** `3e2d65d89` submits the + same signed long-name transfer in candidate blocks at heights 862 and + 863 while the verifier's active tip is past activation; both candidates + are rejected with the old-parser reason. The competing branch mines + the transfer at height 864 and the verifier checks its asset name, + amount, message, expiry, and `OP_PUSHDATA1` encoding after reorganization. + The direct Python 3.10 functional run and nine VersionBits unit cases + passed; private log: evidence/f107-sidebranch-boundary-241d-final.log. + Side-branch block vectors around the other deployment + boundaries remain due; this finding remains open. - **Final status:** MITIGATED, still OPEN for release qualification. Nested candidate-parent propagation and side-branch connection after overtaking are tested. Broader historical replay and second-audit qualification remain diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md index e20b87fffe..f2e07f6f71 100644 --- a/doc/release-notes/release-notes-4.8.1.md +++ b/doc/release-notes/release-notes-4.8.1.md @@ -41,6 +41,16 @@ through this deployment. The 16 MWU phase is height-based, not tied to a calendar date. There is no automatic return to Core 4.8.0 validation rules after activation. +Testnet differs from this mainnet schedule. Its native RIP-25 rule is +force-enabled from genesis by `nPQHybridEnabled=true`. At the reviewed +2026-10-14 testnet tip, `getblocktemplate` therefore advertises the later +16 MWU limit, even while the raw bit-12 BIP9 state reads STARTED. This +does not imply that mainnet has activated bit 12. The testnet override +cannot demonstrate the public 8-to-12-MWU BIP9 transition; controlled +regtest boundary tests cover that transition. Testnet miners and node +operators should account for the already active PQ and block-weight +rules when comparing with Core 4.8.0. + Before miners begin signaling, the complete shared testnet history must be checked against the network-bound ML-DSA rule, the old and new binaries must be retested on both sides of the 8 MWU boundary, and the final 4.8.1 @@ -93,7 +103,17 @@ inputs and check the final transaction before broadcast. ## Release qualification still required -This draft does not assert that 4.8.1 artifacts, the complete testnet -history check, operator readiness, or the final source SHA have passed. +The available testnet active chain was replayed from genesis by a clean +4.8.1 candidate with `-assumevalid=0`, then synchronized with public +peers; the exact snapshot and limits are in the remediation register. +The scanned chain contained no native witness-v2 outputs or spends, so +the replay checks historical compatibility rather than an on-chain PQ +signature. It does not test unknown side branches or future bit-13 +activation. +CRITICAL FINDING-104 through FINDING-107 remain open for release +qualification because their mined activation and side-branch regression +matrix is incomplete. FINDING-112 remains open for intermittent wallet +test-gate diagnosis. Final-SHA artifacts, operator readiness, manual +GUI checks, checksums, and signatures still require qualification. The release will be tagged and published only after those checks and the owner's separate release approval. From 87965b3a4f7e7143dd111ab85fdadddf440b88b5 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:11:10 +0200 Subject: [PATCH 108/195] [FINDING-104] Test mined transfer after activation --- .../feature_transfer_script_reorg.py | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/test/functional/feature_transfer_script_reorg.py b/test/functional/feature_transfer_script_reorg.py index 2d812f7425..c504483ed7 100755 --- a/test/functional/feature_transfer_script_reorg.py +++ b/test/functional/feature_transfer_script_reorg.py @@ -70,10 +70,27 @@ def run_test(self): assert_equal(message_outputs[0]['asset']['amount'], 1) assert_equal(message_outputs[0]['asset']['expire_time'], 123456789) assert_equal(message_outputs[0]['hex'][52:54], '4c') - after_one = node.testmempoolaccept([raw])[0] - assert_equal(after_one['reject-reason'], active_result['reject-reason']) - # Recheck blocks on both sides of the activation boundary. - assert_equal(node.verifychain(4, 4), True) + + # Mine the same transfer format in both following blocks. Together + # with the side-branch candidate test, this checks H-2 through H+2. + later_tip = transfer_block + for height in (865, 866): + later_txid = node.transfer(asset_name, 1, node.getnewaddress(), + 'ab' * 32, 123456789)[0] + later_tip = node.generate(1)[0] + assert_equal(node.getblockcount(), height) + assert later_txid in node.getblock(later_tip)['tx'] + later_tx = next(tx for tx in node.getblock(later_tip, 2)['tx'] + if tx['txid'] == later_txid) + later_outputs = [vout['scriptPubKey'] for vout in later_tx['vout'] + if vout['scriptPubKey'].get('asset', {}).get('message') == 'ab' * 32] + assert_equal(len(later_outputs), 1) + assert_equal(later_outputs[0]['asset']['name'], asset_name) + assert_equal(later_outputs[0]['asset']['amount'], 1) + assert_equal(later_outputs[0]['hex'][52:54], '4c') + after_active = node.testmempoolaccept([raw])[0] + assert_equal(after_active['reject-reason'], active_result['reject-reason']) + assert_equal(node.verifychain(4, 6), True) node.invalidateblock(active_block) assert_equal(node.getbestblockhash(), locked_in_tip) @@ -88,9 +105,9 @@ def run_test(self): assert_equal(live_result['reject-reason'], restarted_result['reject-reason']) node.reconsiderblock(active_block) - assert_equal(node.getbestblockhash(), transfer_block) + assert_equal(node.getbestblockhash(), later_tip) assert_equal(node.getblockchaininfo()['bip9_softforks']['transfer_script']['status'], 'active') - assert_equal(node.verifychain(4, 4), True) + assert_equal(node.verifychain(4, 6), True) if __name__ == '__main__': From 9ded737c7ec5f41519f38352c24594d3eb30856b Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:11:13 +0200 Subject: [PATCH 109/195] [FINDING-105] Test mined asset boundary on side branches --- .../feature_asset_activation_reorg.py | 76 ++++++++++++++----- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/test/functional/feature_asset_activation_reorg.py b/test/functional/feature_asset_activation_reorg.py index 10fc70cb84..521cb5a006 100755 --- a/test/functional/feature_asset_activation_reorg.py +++ b/test/functional/feature_asset_activation_reorg.py @@ -51,32 +51,68 @@ def run_test(self): issue_block = node.generate(1)[0] assert_equal(node.getblockcount(), active_height + 1) assert issue_txid in node.getblock(issue_block)['tx'] + after_one_txid = node.issue('LATCHPLUSONE', 1)[0] after_block = node.generate(1)[0] assert_equal(node.getblockcount(), active_height + 2) - assert_equal(node.verifychain(4, 3), True) + assert after_one_txid in node.getblock(after_block)['tx'] + after_two_txid = node.issue('LATCHPLUSTWO', 1)[0] + after_two_block = node.generate(1)[0] + assert_equal(node.getblockcount(), active_height + 3) + assert after_two_txid in node.getblock(after_two_block)['tx'] + assert_equal(node.verifychain(4, 5), True) - node.invalidateblock(active_block) - assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'locked_in') - live_result = node.testmempoolaccept([raw])[0] - assert_equal(live_result['allowed'], False) - - # Check one more pre-activation tip with the same signed issue. - locked_tip = node.getbestblockhash() + # Reject the same signed issue in mined candidates at H-2 and H-1 + # while this node's unrelated active tip is already past activation. + active_tip = node.getbestblockhash() + locked_tip = node.getblockhash(active_height - 1) config = configparser.ConfigParser() config.read(self.options.configfile) test_util.x16r_hash_cmd = os.path.join( config['environment']['BUILDDIR'], 'src', 'test', 'test_raven_hash' + config['environment']['EXEEXT']) - block_time = node.getblockheader(locked_tip)['time'] + 1 - invalid_issue = create_block(int(locked_tip, 16), create_coinbase(active_height), block_time) - invalid_issue.nVersion = node.getblockheader(active_block)['version'] - invalid_issue.vtx.append(from_hex(CTransaction(), raw)) - invalid_issue.hashMerkleRoot = invalid_issue.calc_merkle_root() - invalid_issue.solve() - rejected = node.submitblock(invalid_issue.serialize().hex()) - self.log.info('Pre-activation issue block rejected: %s', rejected) - assert_equal(rejected, 'bad-txns-assets-not-active') + for candidate_height in (active_height - 1, active_height): + parent = node.getblockhash(candidate_height - 1) + original_block = node.getblockhash(candidate_height) + block_time = node.getblockheader(parent)['time'] + 1 + invalid_issue = create_block(int(parent, 16), + create_coinbase(candidate_height), block_time) + invalid_issue.nVersion = node.getblockheader(original_block)['version'] + invalid_issue.vtx.append(from_hex(CTransaction(), raw)) + invalid_issue.hashMerkleRoot = invalid_issue.calc_merkle_root() + invalid_issue.solve() + rejected = node.submitblock(invalid_issue.serialize().hex()) + self.log.info('Pre-activation side-branch block %d: %s', + candidate_height, rejected) + assert_equal(rejected, 'inconclusive') + assert_equal(node.getbestblockhash(), active_tip) + # Make this candidate the next possible block. It must not + # replace the parent even after its branch is considered. + node.invalidateblock(original_block) + assert_equal(node.getbestblockhash(), parent) + side_tip = next(tip for tip in node.getchaintips() + if tip['hash'] == invalid_issue.hash) + assert_equal(side_tip['status'], 'invalid') + + # A distinct candidate at the same height now reaches the + # active-chain rejection path and reports the exact reason. + direct_issue = create_block(int(parent, 16), + create_coinbase(candidate_height), + block_time + 2) + direct_issue.nVersion = invalid_issue.nVersion + direct_issue.vtx.append(from_hex(CTransaction(), raw)) + direct_issue.hashMerkleRoot = direct_issue.calc_merkle_root() + direct_issue.solve() + assert_equal(node.submitblock(direct_issue.serialize().hex()), + 'bad-txns-assets-not-active') + assert_equal(node.getbestblockhash(), parent) + node.reconsiderblock(original_block) + assert_equal(node.getbestblockhash(), active_tip) + + node.invalidateblock(active_block) assert_equal(node.getbestblockhash(), locked_tip) + assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'locked_in') + live_result = node.testmempoolaccept([raw])[0] + assert_equal(live_result['allowed'], False) node.invalidateblock(locked_tip) before_two = node.testmempoolaccept([raw])[0] @@ -96,9 +132,11 @@ def run_test(self): assert_equal(live_result['allowed'], restarted_result['allowed']) node.reconsiderblock(active_block) - assert_equal(node.getbestblockhash(), after_block) + assert_equal(node.getbestblockhash(), after_two_block) assert issue_txid in node.getblock(issue_block)['tx'] - assert_equal(node.verifychain(4, 3), True) + assert after_one_txid in node.getblock(after_block)['tx'] + assert after_two_txid in node.getblock(after_two_block)['tx'] + assert_equal(node.verifychain(4, 5), True) if __name__ == '__main__': From fde4d21e8fd555d80ab5757dc32091c05e47d270 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:18 +0200 Subject: [PATCH 110/195] [FINDING-106] Require mined activation boundary regression --- .github/workflows/build-raven.yml | 2 +- .github/workflows/rip25-v48-final-gate.yml | 2 +- .../feature_enforced_coinbase_activation.py | 232 ++++++++++++++++++ test/functional/test_runner.py | 1 + 4 files changed, 235 insertions(+), 2 deletions(-) create mode 100755 test/functional/feature_enforced_coinbase_activation.py diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index 3c731e642b..728e73d1cb 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -90,7 +90,7 @@ jobs: python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ wallet_encryption_rewrite.py wallet_asset_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ - feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py \ + feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ feature_restricted_dbcrash.py feature_restricted_verifydb.py feature_restricted_verifydb_root.py \ diff --git a/.github/workflows/rip25-v48-final-gate.yml b/.github/workflows/rip25-v48-final-gate.yml index 9a2e3a1095..7cbdad9c92 100644 --- a/.github/workflows/rip25-v48-final-gate.yml +++ b/.github/workflows/rip25-v48-final-gate.yml @@ -104,7 +104,7 @@ jobs: python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ wallet_encryption_rewrite.py wallet_asset_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ - feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py \ + feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ feature_restricted_dbcrash.py feature_restricted_verifydb.py feature_restricted_verifydb_root.py \ diff --git a/test/functional/feature_enforced_coinbase_activation.py b/test/functional/feature_enforced_coinbase_activation.py new file mode 100755 index 0000000000..60dcf66843 --- /dev/null +++ b/test/functional/feature_enforced_coinbase_activation.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The Raven Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Mined asset-value and coinbase checks must use the candidate parent.""" + +import configparser +import os + +from test_framework import util as test_util +from test_framework.blocktools import create_block, create_coinbase +from test_framework.mininode import CTransaction, CTxOut, from_hex, to_hex +from test_framework.test_framework import RavenTestFramework +from test_framework.util import Decimal, assert_equal, hex_str_to_bytes + + +ASSETS_VERSION = 0x30000040 +ASSETS_VALUES_VERSION = 0x30000240 +ASSETS_VALUES_COINBASE_VERSION = 0x30000640 + + +class EnforcedCoinbaseActivationTest(RavenTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [[ + '-assetindex', '-persistmempool=0', + '-blockversion={}'.format(ASSETS_VERSION), + ]] + + def restart_with_version(self, version): + self.restart_node(0, [ + '-assetindex', '-persistmempool=0', + '-blockversion={}'.format(version), + ]) + return self.nodes[0] + + @staticmethod + def deployment_status(node, name): + return node.getblockchaininfo()['bip9_softforks'][name]['status'] + + @staticmethod + def candidate(node, parent, height, version, time_offset=1, + transaction=None, coinbase_asset_script=None): + coinbase = create_coinbase(height) + if coinbase_asset_script is not None: + coinbase.vout.append(CTxOut(0, hex_str_to_bytes(coinbase_asset_script))) + coinbase.rehash() + block_time = node.getblockheader(parent)['time'] + time_offset + block = create_block(int(parent, 16), coinbase, block_time) + block.nVersion = version + if transaction is not None: + block.vtx.append(from_hex(CTransaction(), transaction)) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + return block + + @staticmethod + def submit(node, block): + return node.submitblock(block.serialize().hex()) + + @staticmethod + def assert_invalid_tip(node, block): + tip = next(tip for tip in node.getchaintips() + if tip['hash'] == block.hash) + assert_equal(tip['status'], 'invalid') + + @staticmethod + def nonzero_reissue(node, asset_name): + native_coin = next(coin for coin in node.listunspent() + if coin['amount'] > Decimal('101') + and coin['confirmations'] > 101) + owner_name = asset_name + '!' + owner_coin = node.listmyassets(owner_name, True)[owner_name]['outpoints'][0] + raw = node.createrawtransaction( + [{'txid': native_coin['txid'], 'vout': native_coin['vout']}, + {'txid': owner_coin['txid'], 'vout': owner_coin['vout']}], + {'n1ReissueAssetXXXXXXXXXXXXXXWG9NLd': 100, + node.getnewaddress(): native_coin['amount'] - Decimal('100.1'), + node.getnewaddress(): {'reissue': { + 'asset_name': asset_name, 'asset_quantity': 1}}}) + tx = from_hex(CTransaction(), raw) + assert b'rvnr' in tx.vout[-1].scriptPubKey + tx.vout[-1].nValue = 1 + signed = node.signrawtransaction(to_hex(tx)) + assert_equal(signed['complete'], True) + return signed['hex'] + + def run_test(self): + config = configparser.ConfigParser() + config.read(self.options.configfile) + test_util.x16r_hash_cmd = os.path.join( + config['environment']['BUILDDIR'], 'src', 'test', + 'test_raven_hash' + config['environment']['EXEEXT']) + + node = self.nodes[0] + node.generate(431) + assert_equal(self.deployment_status(node, 'assets'), 'active') + assert_equal(self.deployment_status(node, 'enforce'), 'started') + + # Bit 9 must lag bit 6: its normal LOCKED_IN boundary at H288 + # precedes asset activation, so no reissue can exercise that rule. + node = self.restart_with_version(ASSETS_VALUES_VERSION) + node.generate(142) + assert_equal(node.getblockcount(), 573) + asset_name = 'F106VALUE' + issue_txid = node.issue(asset_name, 1)[0] + issue_block = node.generate(1)[0] + assert_equal(node.getblockcount(), 574) + assert issue_txid in node.getblock(issue_block)['tx'] + assert_equal(self.deployment_status(node, 'enforce'), 'started') + assert_equal(node.getassetdata(asset_name)['amount'], 1) + reissue_raw = self.nonzero_reissue(node, asset_name) + reissue_txid = node.decoderawtransaction(reissue_raw)['txid'] + + # H575 still uses STARTED, and H576 is the first LOCKED_IN + # candidate. The value rule intentionally starts at LOCKED_IN. + original_575 = node.generate(1)[0] + assert_equal(node.getblockcount(), 575) + assert_equal(self.deployment_status(node, 'enforce'), 'locked_in') + node = self.restart_with_version(ASSETS_VALUES_COINBASE_VERSION) + node.generate(145) + assert_equal(node.getblockcount(), 720) + assert_equal(self.deployment_status(node, 'enforce'), 'active') + active_value_tip = node.getbestblockhash() + + # Even while the active tip is far beyond H576, this candidate must + # use its own H575 parent and fail the contextual transaction check. + invalid_576 = self.candidate( + node, original_575, 576, ASSETS_VALUES_COINBASE_VERSION, + transaction=reissue_raw) + assert_equal(self.submit(node, invalid_576), + "bad-txns-asset-reissued-amount-isn't-zero") + original_576 = node.getblockhash(576) + node.invalidateblock(original_576) + assert_equal(node.getbestblockhash(), original_575) + self.assert_invalid_tip(node, invalid_576) + direct_invalid_576 = self.candidate( + node, original_575, 576, ASSETS_VALUES_COINBASE_VERSION, + time_offset=3, transaction=reissue_raw) + assert_equal(self.submit(node, direct_invalid_576), + "bad-txns-asset-reissued-amount-isn't-zero") + node.reconsiderblock(original_576) + assert_equal(node.getbestblockhash(), active_value_tip) + + valid_575 = self.candidate( + node, issue_block, 575, ASSETS_VALUES_VERSION, + time_offset=2, transaction=reissue_raw) + assert_equal(self.submit(node, valid_575), 'inconclusive') + node.invalidateblock(original_575) + assert_equal(node.getbestblockhash(), valid_575.hash) + assert reissue_txid in node.getblock(valid_575.hash)['tx'] + assert_equal(node.getassetdata(asset_name)['amount'], 2) + + # Rewind across LOCKED_IN after this process saw ACTIVE. Restart at + # the old-rule parent, then restore the long branch. + node.invalidateblock(valid_575.hash) + assert_equal(node.getbestblockhash(), issue_block) + assert_equal(self.deployment_status(node, 'enforce'), 'started') + assert_equal(node.getassetdata(asset_name)['amount'], 1) + node = self.restart_with_version(ASSETS_VALUES_COINBASE_VERSION) + assert_equal(node.getbestblockhash(), issue_block) + assert_equal(self.deployment_status(node, 'enforce'), 'started') + node.reconsiderblock(original_575) + assert_equal(node.getbestblockhash(), active_value_tip) + assert_equal(self.deployment_status(node, 'enforce'), 'active') + + # Signaling bit 10 from H576 through H999 exceeds its 400/500 + # threshold. Its first ACTIVE candidate is H1500. + node.generate(776) + assert_equal(node.getblockcount(), 1496) + coinbase_asset = 'F106COINBASE' + coinbase_issue = node.issue(coinbase_asset, 1)[0] + issue_block_1497 = node.generate(1)[0] + assert coinbase_issue in node.getblock(issue_block_1497)['tx'] + transfer_txid = node.transfer(coinbase_asset, 1, node.getnewaddress())[0] + transfer_block = node.generate(1)[0] + assert_equal(node.getblockcount(), 1498) + assert transfer_txid in node.getblock(transfer_block)['tx'] + assert_equal(self.deployment_status(node, 'coinbase'), 'locked_in') + transfer = node.decoderawtransaction(node.gettransaction(transfer_txid)['hex']) + transfer_outputs = [out for out in transfer['vout'] + if out['scriptPubKey'].get('asset', {}).get('name') == coinbase_asset] + assert_equal(len(transfer_outputs), 1) + asset_script = transfer_outputs[0]['scriptPubKey']['hex'] + + original_1499 = node.generate(1)[0] + assert_equal(node.getblockcount(), 1499) + assert_equal(self.deployment_status(node, 'coinbase'), 'active') + node.generate(2) + assert_equal(node.getblockcount(), 1501) + active_coinbase_tip = node.getbestblockhash() + + invalid_1500 = self.candidate( + node, original_1499, 1500, ASSETS_VALUES_COINBASE_VERSION, + coinbase_asset_script=asset_script) + assert_equal(self.submit(node, invalid_1500), + 'bad-txns-coinbase-contains-asset-txes') + original_1500 = node.getblockhash(1500) + node.invalidateblock(original_1500) + assert_equal(node.getbestblockhash(), original_1499) + self.assert_invalid_tip(node, invalid_1500) + direct_invalid_1500 = self.candidate( + node, original_1499, 1500, ASSETS_VALUES_COINBASE_VERSION, + time_offset=3, coinbase_asset_script=asset_script) + assert_equal(self.submit(node, direct_invalid_1500), + 'bad-txns-coinbase-contains-asset-txes') + node.reconsiderblock(original_1500) + assert_equal(node.getbestblockhash(), active_coinbase_tip) + + valid_1499 = self.candidate( + node, transfer_block, 1499, ASSETS_VALUES_COINBASE_VERSION, + time_offset=2, coinbase_asset_script=asset_script) + assert_equal(self.submit(node, valid_1499), 'inconclusive') + node.invalidateblock(original_1499) + assert_equal(node.getbestblockhash(), valid_1499.hash) + assert_equal(node.getblock(valid_1499.hash)['tx'][0], valid_1499.vtx[0].hash) + + node.invalidateblock(valid_1499.hash) + assert_equal(node.getbestblockhash(), transfer_block) + assert_equal(self.deployment_status(node, 'coinbase'), 'locked_in') + node = self.restart_with_version(ASSETS_VALUES_COINBASE_VERSION) + assert_equal(node.getbestblockhash(), transfer_block) + assert_equal(self.deployment_status(node, 'coinbase'), 'locked_in') + node.reconsiderblock(original_1499) + assert_equal(node.getbestblockhash(), active_coinbase_tip) + assert_equal(self.deployment_status(node, 'coinbase'), 'active') + assert_equal(node.verifychain(4, 4), True) + + +if __name__ == '__main__': + EnforcedCoinbaseActivationTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 8cfb4f9e0c..489d8a3d61 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -113,6 +113,7 @@ 'feature_asset_activation_reorg.py', 'feature_transfer_script_reorg.py', 'feature_rip5_activation_reorg.py', + 'feature_enforced_coinbase_activation.py', 'feature_assets_mempool.py', 'feature_restricted_assets.py', 'feature_raw_restricted_assets.py', From 8bbd40fb142541c4dfd44aa136f9db7673ea3129 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:25 +0200 Subject: [PATCH 111/195] [FINDING-173] Freeze XCB artifact source failure --- ...0025-v4.8-security-remediation-register.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index e89d45ffa0..3e3564ce94 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9238,3 +9238,37 @@ Cross-platform CI and public deployment remain separate release gates. SHA a16a05bdb925985bc33694455e582cd7d2261958. All five Build Raven artifacts and seven Final Gate builds also passed. The five release artifacts do not contain this optional utility. + +### FINDING-173: XCB source timeout blocks Linux release artifacts + +- **Severity:** MEDIUM release availability; exact-SHA artifact qualification fails closed. +- **Frozen initial status:** OPEN on 2026-10-14 at + `975ab4f4a49d6217fa213da87384950b4e21290d`, before changing either + XCB depends recipe. +- **Affected RIP-25 invariant:** Build Raven must produce all five release + targets on the same commit as a green Final Gate. +- **Affected Core 4.8.0 behavior:** The pinned libxcb 1.10 and xcb-proto 1.10 + source URLs are inherited depends configuration. Their expected SHA-256 + digests remain the release integrity checks. +- **Root cause:** `xcb.freedesktop.org` timed out during dependency download + in two release jobs. The generic fallback URL was empty, so the final + curl request failed before configuration or compilation. +- **Affected files:** `depends/packages/libxcb.mk`, + `depends/packages/xcb_proto.mk`, and `depends/funcs.mk` fetch fallback. +- **Concrete scenario:** Build Raven `38546424615` at the exact SHA above + passed its mandatory security job and produced Windows, ARM32, and macOS + artifacts. Linux x86_64 job `115800868704` timed out fetching + `libxcb-1.10.tar.bz2`; ARM64 job `115800868477` timed out fetching + `xcb-proto-1.10.tar.bz2`. Both ended in the `Build Dependencies` step. + Final Gate `38546434041` passed all eight jobs at the same SHA. The + earlier exact-SHA Build Raven `38475261912` fetched both XCB archives + successfully from the same host, so the failure is source availability, + not evidence of a consensus or compiler defect. +- **Expected behavior:** A second pinned source supplies the exact same + archive when the first source is unavailable. The fixed SHA-256 must be + checked before extraction; a digest mismatch must fail closed. +- **Regression required:** Preserve both red CI jobs. Force the first + source to fail in a clean local fetch, prove that the alternate source + supplies both expected bytes, and prove that altered bytes are rejected. + Then rerun Build Raven and Final Gate at one final SHA. +- **Final status:** OPEN. Release artifacts are not qualified at this SHA. From fecdcdaee865f9d95eb931a447ff9570562eafde Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:29 +0200 Subject: [PATCH 112/195] [FINDING-173] Add pinned XCB source fallback --- depends/packages/libxcb.mk | 7 +++++++ depends/packages/xcb_proto.mk | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/depends/packages/libxcb.mk b/depends/packages/libxcb.mk index 28f2bd6f25..e665a3655d 100644 --- a/depends/packages/libxcb.mk +++ b/depends/packages/libxcb.mk @@ -1,10 +1,17 @@ package=libxcb $(package)_version=1.10 $(package)_download_path=http://xcb.freedesktop.org/dist +$(package)_fallback_download_path=https://download.bitcoinabc.org/depends-sources $(package)_file_name=$(package)-$($(package)_version).tar.bz2 $(package)_sha256_hash=98d9ab05b636dd088603b64229dd1ab2d2cc02ab807892e107d674f9c3f2d5b5 $(package)_dependencies=xcb_proto libXau xproto +define $(package)_fetch_cmds +( test -f $(libxcb_source) || \ + $(call fetch_file_inner,libxcb,$(libxcb_download_path),$(libxcb_download_file),$(libxcb_file_name),$(libxcb_sha256_hash)) || \ + $(call fetch_file_inner,libxcb,$(libxcb_fallback_download_path),$(libxcb_download_file),$(libxcb_file_name),$(libxcb_sha256_hash)) ) +endef + define $(package)_set_vars $(package)_config_opts=--disable-static endef diff --git a/depends/packages/xcb_proto.mk b/depends/packages/xcb_proto.mk index 0c7c958d62..e6c9a34e7b 100644 --- a/depends/packages/xcb_proto.mk +++ b/depends/packages/xcb_proto.mk @@ -1,9 +1,16 @@ package=xcb_proto $(package)_version=1.10 $(package)_download_path=http://xcb.freedesktop.org/dist +$(package)_fallback_download_path=https://download.bitcoinabc.org/depends-sources $(package)_file_name=xcb-proto-$($(package)_version).tar.bz2 $(package)_sha256_hash=7ef40ddd855b750bc597d2a435da21e55e502a0fefa85b274f2c922800baaf05 +define $(package)_fetch_cmds +( test -f $(xcb_proto_source) || \ + $(call fetch_file_inner,xcb_proto,$(xcb_proto_download_path),$(xcb_proto_download_file),$(xcb_proto_file_name),$(xcb_proto_sha256_hash)) || \ + $(call fetch_file_inner,xcb_proto,$(xcb_proto_fallback_download_path),$(xcb_proto_download_file),$(xcb_proto_file_name),$(xcb_proto_sha256_hash)) ) +endef + define $(package)_set_vars $(package)_config_opts=--disable-shared $(package)_config_opts_linux=--with-pic From 12d4bf27df762cd635379784927e044d0980c31d Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:59:39 +0200 Subject: [PATCH 113/195] [FINDING-174] Freeze ccache first-use CI failure --- ...0025-v4.8-security-remediation-register.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 3e3564ce94..575d616571 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9272,3 +9272,40 @@ Cross-platform CI and public deployment remain separate release gates. supplies both expected bytes, and prove that altered bytes are rejected. Then rerun Build Raven and Final Gate at one final SHA. - **Final status:** OPEN. Release artifacts are not qualified at this SHA. + +### FINDING-174: Parallel first use of pinned ccache can abort the test gate + +- **Severity:** MEDIUM release availability; the mandatory security job + fails closed before tests or artifact builds run. +- **Frozen initial status:** OPEN on 2026-10-14 at + `fecdcdaee865f9d95eb931a447ff9570562eafde`, before changing the + CI cache initialization. +- **Affected RIP-25 invariant:** Mandatory `make check` and all release + builds must complete at one exact candidate SHA. +- **Affected Core 4.8.0 behavior:** The pinned native ccache 3.3.4 recipe + is inherited; the first-use workflow setup is release CI configuration. +- **Root cause:** Two compiler processes can start ccache concurrently + when its per-runner `ccache.conf` is absent. In ccache 3.3.4, + `initialize` reads that path and then checks it with `stat` after a + failed open. One process can create the file between the other + process's failed open and `stat`, causing the latter to report the + original missing-file error as fatal. Creating only the directory + does not eliminate this race. +- **Affected files:** `.github/workflows/rip25-v48-final-gate.yml` and + `.github/workflows/build-raven.yml`, the cache setup before parallel + compilation; pinned ccache 3.3.4 `ccache.c:2980-3073`. +- **Concrete scenario:** Final Gate `38592942408`, security job + `115952251224`, on the exact SHA above failed in `Build tests` at its + first parallel C++ objects with + `ccache: error: /home/runner/.ccache/ccache.conf: No such file or directory`. + No `make check` result exists for that run. Build Raven `38592937851` + passed its security job and all five artifact jobs at the same SHA; + the independent failure is timing-dependent. +- **Expected behavior:** Initialize the pinned ccache configuration once + serially before any parallel compile in each workflow job. Preserve + existing configuration when present. +- **Regression required:** Retain the red Final Gate log, start the pinned + ccache from an empty cache directory and prove serial initialization + creates its configuration, then run parallel compiler wrappers without + the missing-file error. Both exact-SHA workflows must pass afterward. +- **Final status:** OPEN. The candidate has no green paired Final Gate. From 8efa331e0422c1a38c6d342906ef708891623e3e Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:01:28 +0200 Subject: [PATCH 114/195] [FINDING-174] Initialize ccache before parallel builds --- .github/workflows/build-raven.yml | 2 ++ .github/workflows/rip25-v48-final-gate.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index 728e73d1cb..39036ef723 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -59,6 +59,7 @@ jobs: run: | ./autogen.sh mkdir -p "$HOME/.ccache" + "$PWD/depends/x86_64-pc-linux-gnu/native/bin/ccache" --print-config > /dev/null CONFIG_SITE="$PWD/depends/x86_64-pc-linux-gnu/share/config.site" \ ./configure --without-gui --with-tx --disable-bench @@ -235,6 +236,7 @@ jobs: run: | ./autogen.sh mkdir -p "$HOME/.ccache" + "$PWD/depends/${{ matrix.host }}/native/bin/ccache" --print-config > /dev/null - name: Configure Build run: bash -Eeuo pipefail "${SCRIPTS}/04-configure-build.sh" "${{ matrix.OS }}" "$GITHUB_WORKSPACE" diff --git a/.github/workflows/rip25-v48-final-gate.yml b/.github/workflows/rip25-v48-final-gate.yml index 7cbdad9c92..60e737e1f2 100644 --- a/.github/workflows/rip25-v48-final-gate.yml +++ b/.github/workflows/rip25-v48-final-gate.yml @@ -72,6 +72,7 @@ jobs: run: | ./autogen.sh mkdir -p "$HOME/.ccache" + "$PWD/depends/x86_64-pc-linux-gnu/native/bin/ccache" --print-config > /dev/null CONFIG_SITE="$PWD/depends/x86_64-pc-linux-gnu/share/config.site" \ ./configure --without-gui --with-tx --disable-bench @@ -218,6 +219,7 @@ jobs: run: | ./autogen.sh mkdir -p "$HOME/.ccache" + "$PWD/depends/${{ matrix.host }}/native/bin/ccache" --print-config > /dev/null CONFIG_SITE="$PWD/depends/${{ matrix.host }}/share/config.site" \ ./configure ${{ matrix.configure_flags }} From 0391e1edb9c85dccc32261bf8e74a36b2142fafa Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:01:53 +0200 Subject: [PATCH 115/195] [FINDING-173] Record XCB fallback qualification --- ...0025-v4.8-security-remediation-register.md | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 575d616571..b3583d8736 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9271,7 +9271,25 @@ Cross-platform CI and public deployment remain separate release gates. source to fail in a clean local fetch, prove that the alternate source supplies both expected bytes, and prove that altered bytes are rejected. Then rerun Build Raven and Final Gate at one final SHA. -- **Final status:** OPEN. Release artifacts are not qualified at this SHA. +- **Remediation commit:** `fecdcdaee865f9d95eb931a447ff9570562eafde` + adds a package-specific HTTPS fallback for both pinned XCB archives. + Their versions and SHA-256 digests are unchanged. +- **Verification:** The original `38546424615` attempt 1 remains the red + artifact evidence. Its same-SHA attempt 2 passed all five release jobs + once the original XCB host responded. In a clean local fetch with the + primary URL forced to `127.0.0.1:9`, both fallback downloads matched + their pinned digests. One-byte-altered fallback archives were rejected + for both packages, without installing source archives. Private logs: + `evidence/f173-xcb-fallback-green.log`, + `evidence/f173-xcb-fallback-reject.log`, and + `evidence/f173-xcb-proto-fallback-reject.log`. Build Raven + `38592937851` passed its security job and all five release targets on + exact `fecdcdaee`; the restored primary host served the XCB archives in + that run, so the forced local test supplies direct fallback evidence. + Final Gate `38592942408` failed separately at pinned ccache first use, + recorded in FINDING-174. +- **Final status:** FIXED for the XCB availability defect. A wholly green + Build Raven and Final Gate pair at the final release SHA is still due. ### FINDING-174: Parallel first use of pinned ccache can abort the test gate From ba37d512f21b7cd860bee144b38c9ea32ab31de1 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:02:07 +0200 Subject: [PATCH 116/195] [FINDING-174] Record serial cache regression --- ...RIP-0025-v4.8-security-remediation-register.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index b3583d8736..bfa897e8a6 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9326,4 +9326,17 @@ Cross-platform CI and public deployment remain separate release gates. ccache from an empty cache directory and prove serial initialization creates its configuration, then run parallel compiler wrappers without the missing-file error. Both exact-SHA workflows must pass afterward. -- **Final status:** OPEN. The candidate has no green paired Final Gate. +- **Remediation commit:** `8efa331e0` runs the pinned ccache + `--print-config` once after creating its directory and before any + parallel compile in both security jobs and both artifact matrices. + Existing cache configuration is preserved. +- **Verification:** The red Final Gate log is private + `evidence/f174-final-gate-38592942408-failed.log`. A clean local + ccache directory gained `ccache.conf` from the serial invocation; + 16 subsequent parallel compiler wrappers all exited successfully + and produced their object files. Its JSON record is + `evidence/f174-ccache-first-use-local.json`. The required functional + list gate and local `make -j4 check` passed after the workflow edit; + log: `evidence/local-make-check-f174-candidate.log`. +- **Final status:** FIXED locally, OPEN until both exact-SHA workflows + pass and the five release artifacts are checked. From 2480b0caadf0fe9ae1e504938bb199daaca8d317 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:02:24 +0200 Subject: [PATCH 117/195] [FINDING-104] Record mined transfer boundary evidence --- doc/RIP-0025-v4.8-security-remediation-register.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index bfa897e8a6..4063c384eb 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -6792,6 +6792,14 @@ Cross-platform CI and public deployment remain separate release gates. recorded vulnerable-binary red and corrected-binary green regression. It does not yet supply a mined `OP_PUSHDATA1` accept/reject vector at every requested height around the boundary. +- **Additional mined positive boundary regression:** `87965b3a4` extends + the same functional test with actual long-name transfer outputs mined + at the first three ACTIVE heights, 864 through 866, and checks those + outputs through rollback, restart, reconsideration, and `verifychain`. + Its direct Python 3.10 run passed; private log: + `evidence/f104-hminus2-hplus2-local.log`. The earlier pre-ACTIVE + candidate-block rejection and vulnerable-binary red test remain in + place. This does not claim a connected pre-ACTIVE transfer block. - **Final status:** MITIGATED locally, OPEN for release qualification. ### FINDING-105: Asset deployment activation survives a reorg incorrectly From f6afca5295386a2469d085701aef714c9a7b87b2 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:02:38 +0200 Subject: [PATCH 118/195] [FINDING-105] Record mined asset side-branch evidence --- doc/RIP-0025-v4.8-security-remediation-register.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 4063c384eb..4d4b12cf8a 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -6857,6 +6857,18 @@ Cross-platform CI and public deployment remain separate release gates. original vulnerable-binary red and corrected-binary green reorg comparison remains recorded above. A complete mined H-2 through H+2 and alternate-side-branch matrix is still due. +- **Additional side-branch boundary regression:** `9ded737c7` mines + real issue outputs at the first three ACTIVE heights and submits the + same signed issue in pre-ACTIVE sibling candidates at heights H-2 and + H-1 while the active tip is past activation. Those short branches + remain `inconclusive` until eligible for connection; after rewinding + the main chain, they are marked invalid. A separate candidate built + on the current pre-ACTIVE parent rejects with + `bad-txns-assets-not-active`. Restart, reconsideration, and + `verifychain` pass. The direct Python 3.10 run passed; private log: + `evidence/f105-hminus2-hplus2-local.log`. The deferred short-branch + result is recorded explicitly rather than treated as immediate + block-connection proof. - **Final status:** MITIGATED locally, OPEN for release qualification. ### FINDING-106: Additional Core 4.8.0 deployment latches survive reorgs From d6bc19be1467e4c010ea5240dc97514a45cc23f7 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:02:53 +0200 Subject: [PATCH 119/195] [FINDING-106] Record enforced and coinbase boundary evidence --- ...IP-0025-v4.8-security-remediation-register.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 4d4b12cf8a..86f59c44e3 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -6941,6 +6941,22 @@ Cross-platform CI and public deployment remain separate release gates. evidence/f106-107-versionbits-241d-final.log and evidence/f106-rip5-boundary-241d-final.log. Enforced-value and coinbase mined-block vectors are still due; this finding remains open. +- **Mined enforced-value and coinbase regression:** `fde4d21e8` adds + mandatory `feature_enforced_coinbase_activation.py`. A signed + nonzero-value reissue candidate is accepted at height 575 and + rejected at the first bit-9 LOCKED_IN height 576 with + `bad-txns-asset-reissued-amount-isn't-zero`. A coinbase asset + candidate is accepted at height 1499 and rejected at the first + bit-10 ACTIVE height 1500 with + `bad-txns-coinbase-contains-asset-txes`. The test includes sibling + branches, rewind, restart, reconsideration, and `verifychain`. + Its direct Python 3.10 run and all 33 required functional cases + passed locally, as did `make -j4 check`; private logs: + `evidence/f106-f107-enforced-coinbase-local.log`, + `evidence/required-functional-local-final-candidate.log`, and + `evidence/local-make-check-f173-test-candidate.log`. Build Raven + `38592937851` passed its security job, but the paired Final Gate + stopped before tests for the separate FINDING-174 ccache race. - **Final status:** OPEN for release qualification. ### FINDING-107: Context-free block checks consume active-tip asset state From 58c897c9e94eabc3d2e77265ca8a401ef823c635 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:03:06 +0200 Subject: [PATCH 120/195] [FINDING-107] Record cross-deployment side-branch tests --- doc/RIP-0025-v4.8-security-remediation-register.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 86f59c44e3..ff4d82a836 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7061,6 +7061,19 @@ Cross-platform CI and public deployment remain separate release gates. passed; private log: evidence/f107-sidebranch-boundary-241d-final.log. Side-branch block vectors around the other deployment boundaries remain due; this finding remains open. +- **Additional cross-deployment side-branch evidence:** `9ded737c7` + covers bit-6 issue candidates on pre-ACTIVE siblings and confirms + they become invalid when that branch is connected. `fde4d21e8` + covers bit-9 enforced-value and bit-10 coinbase candidates on both + sides of their respective boundary, with rewind, restart, and + reconsideration. These complement the earlier bit-7 RIP5 and bit-8 + transfer candidates. Direct functional runs passed, and all 33 + required functional cases passed locally. The complete active-chain + testnet replay is separate evidence for historical compatibility; + it cannot establish unknown side-branch behavior. Private logs: + `evidence/f105-hminus2-hplus2-local.log`, + `evidence/f106-f107-enforced-coinbase-local.log`, and + `evidence/required-functional-local-final-candidate.log`. - **Final status:** MITIGATED, still OPEN for release qualification. Nested candidate-parent propagation and side-branch connection after overtaking are tested. Broader historical replay and second-audit qualification remain From ee30148e794ab5fec6c07d621e6bb4a581ef942f Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:03:36 +0200 Subject: [PATCH 121/195] [FINDING-067] Record latest testnet and CI gate evidence --- ...0025-v4.8-security-remediation-register.md | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index ff4d82a836..212402a6e0 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4679,6 +4679,25 @@ testnet-verifychain-a16-later.json. This later scan strengthens the active-chain evidence but does not demonstrate public PQ spending or unknown side branches. +At 2026-10-14 15:52 UTC the same unpruned clean-binary node reached +height 2,553,271, tip +00000b023f5da9e8593dbf7a3a3552e47e07cb415760364e12d551a1ae80802c. +The resumed scanner covered 2,553,272 active-chain blocks and +2,773,663 transactions with zero witness-v2 outputs or spends, +wrapped spends, and malformed candidates in its defined classes. +It verified 1,273 checkpoints. All 2,553,272 stored block records +remained below 2,000,000 serialized bytes; the maximum remained +1,998,152 bytes, yielding the 7,992,608-WU old-rule upper bound. +A fresh verifychain level 4 over 2016 blocks returned true in +23.9 seconds at the same tip. Private records are +testnet-history-scan-a16-current.json, +testnet-block-record-size-a16-current.json, and +testnet-verifychain-a16-current.json. A path comparison from +the clean binary source a16a05bdb to fecdcdaee found no changed +production source under src/; workflow, depends, documentation, +and test paths changed. The private comparison is +evidence/a16-to-fecdc-source-diff.json. + The live RPC warning said unknown new rules activated, versionbit 11. Bit 11 is the known transfer-overflow deployment, ACTIVE since height 2,493,792. None of the latest 2016 block headers signaled bits 11, @@ -4709,17 +4728,20 @@ bit-12 activation. The approved incompatible-upgrade plan remains necessary. FINDING-067 remains HIGH and OPEN. CRITICAL FINDING-104 through -FINDING-107 also remain OPEN for release qualification: the current -contextual repairs and tests do not yet cover every required mined -activation and side-branch boundary. The active-chain testnet replay -does not close those reorg gaps. FINDING-112 remains OPEN for wallet -test-gate reliability. Other unresolved inherited wallet and asset -limitations retain their individual statuses below. Remaining rollout -gates are operator readiness and published upgrade instructions for bit -12, manual GUI exercise on release operating systems, final release -notes and signed checksums, CI and package provenance on the actual -release commit if it differs from this checkpoint, and the owner's -separate approval before a tag or GitHub release. +FINDING-107 remain release-open despite the expanded mined and +side-branch boundary tests recorded below; the complete coverage +disposition and a green final-SHA CI pair are still due. The +active-chain testnet replay cannot close unknown side-branch gaps. +FINDING-112 remains OPEN for wallet test-gate reliability. The latest +paired CI attempt at fecdcdaee had a green Build Raven and a Final +Gate stopped before `make check` by the separate ccache race in +FINDING-174. The repaired workflow must pass on one final SHA. +Other unresolved inherited wallet and asset limitations retain their +individual statuses below. Remaining rollout gates are operator +readiness and published upgrade instructions for bit 12, manual GUI +exercise on release operating systems, final release notes and signed +checksums, artifact provenance on the actual release commit, and the +owner's separate approval before a tag or GitHub release. ## Finding frozen during the second adversarial review From 76983ad18e5e19aa69c375837f017c11ec91324c Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:16:40 +0200 Subject: [PATCH 122/195] [FINDING-106] Expand enforced and coinbase boundary matrix --- .../feature_enforced_coinbase_activation.py | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/test/functional/feature_enforced_coinbase_activation.py b/test/functional/feature_enforced_coinbase_activation.py index 60dcf66843..fbd2244c7a 100755 --- a/test/functional/feature_enforced_coinbase_activation.py +++ b/test/functional/feature_enforced_coinbase_activation.py @@ -101,20 +101,22 @@ def run_test(self): # Bit 9 must lag bit 6: its normal LOCKED_IN boundary at H288 # precedes asset activation, so no reissue can exercise that rule. node = self.restart_with_version(ASSETS_VALUES_VERSION) - node.generate(142) - assert_equal(node.getblockcount(), 573) + node.generate(141) + assert_equal(node.getblockcount(), 572) asset_name = 'F106VALUE' issue_txid = node.issue(asset_name, 1)[0] issue_block = node.generate(1)[0] - assert_equal(node.getblockcount(), 574) + assert_equal(node.getblockcount(), 573) assert issue_txid in node.getblock(issue_block)['tx'] assert_equal(self.deployment_status(node, 'enforce'), 'started') assert_equal(node.getassetdata(asset_name)['amount'], 1) reissue_raw = self.nonzero_reissue(node, asset_name) reissue_txid = node.decoderawtransaction(reissue_raw)['txid'] - # H575 still uses STARTED, and H576 is the first LOCKED_IN + # H574 and H575 still use STARTED; H576 is the first LOCKED_IN # candidate. The value rule intentionally starts at LOCKED_IN. + original_574 = node.generate(1)[0] + assert_equal(node.getblockcount(), 574) original_575 = node.generate(1)[0] assert_equal(node.getblockcount(), 575) assert_equal(self.deployment_status(node, 'enforce'), 'locked_in') @@ -143,24 +145,42 @@ def run_test(self): node.reconsiderblock(original_576) assert_equal(node.getbestblockhash(), active_value_tip) + for height in (577, 578): + invalid = self.candidate( + node, node.getblockhash(height - 1), height, + ASSETS_VALUES_COINBASE_VERSION, time_offset=2, + transaction=reissue_raw) + assert_equal(self.submit(node, invalid), + "bad-txns-asset-reissued-amount-isn't-zero") + valid_575 = self.candidate( - node, issue_block, 575, ASSETS_VALUES_VERSION, + node, original_574, 575, ASSETS_VALUES_VERSION, time_offset=2, transaction=reissue_raw) assert_equal(self.submit(node, valid_575), 'inconclusive') + valid_574 = self.candidate( + node, issue_block, 574, ASSETS_VALUES_VERSION, + time_offset=2, transaction=reissue_raw) + assert_equal(self.submit(node, valid_574), 'inconclusive') node.invalidateblock(original_575) assert_equal(node.getbestblockhash(), valid_575.hash) assert reissue_txid in node.getblock(valid_575.hash)['tx'] assert_equal(node.getassetdata(asset_name)['amount'], 2) + node.invalidateblock(original_574) + assert_equal(node.getbestblockhash(), valid_574.hash) + assert reissue_txid in node.getblock(valid_574.hash)['tx'] + assert_equal(node.getassetdata(asset_name)['amount'], 2) + # Rewind across LOCKED_IN after this process saw ACTIVE. Restart at # the old-rule parent, then restore the long branch. - node.invalidateblock(valid_575.hash) + node.invalidateblock(valid_574.hash) assert_equal(node.getbestblockhash(), issue_block) assert_equal(self.deployment_status(node, 'enforce'), 'started') assert_equal(node.getassetdata(asset_name)['amount'], 1) node = self.restart_with_version(ASSETS_VALUES_COINBASE_VERSION) assert_equal(node.getbestblockhash(), issue_block) assert_equal(self.deployment_status(node, 'enforce'), 'started') + node.reconsiderblock(original_574) node.reconsiderblock(original_575) assert_equal(node.getbestblockhash(), active_value_tip) assert_equal(self.deployment_status(node, 'enforce'), 'active') @@ -187,8 +207,8 @@ def run_test(self): original_1499 = node.generate(1)[0] assert_equal(node.getblockcount(), 1499) assert_equal(self.deployment_status(node, 'coinbase'), 'active') - node.generate(2) - assert_equal(node.getblockcount(), 1501) + node.generate(3) + assert_equal(node.getblockcount(), 1502) active_coinbase_tip = node.getbestblockhash() invalid_1500 = self.candidate( @@ -208,20 +228,37 @@ def run_test(self): node.reconsiderblock(original_1500) assert_equal(node.getbestblockhash(), active_coinbase_tip) + for height in (1501, 1502): + invalid = self.candidate( + node, node.getblockhash(height - 1), height, + ASSETS_VALUES_COINBASE_VERSION, time_offset=2, + coinbase_asset_script=asset_script) + assert_equal(self.submit(node, invalid), + 'bad-txns-coinbase-contains-asset-txes') + valid_1499 = self.candidate( node, transfer_block, 1499, ASSETS_VALUES_COINBASE_VERSION, time_offset=2, coinbase_asset_script=asset_script) assert_equal(self.submit(node, valid_1499), 'inconclusive') + valid_1498 = self.candidate( + node, issue_block_1497, 1498, ASSETS_VALUES_COINBASE_VERSION, + time_offset=2, coinbase_asset_script=asset_script) + assert_equal(self.submit(node, valid_1498), 'inconclusive') node.invalidateblock(original_1499) assert_equal(node.getbestblockhash(), valid_1499.hash) assert_equal(node.getblock(valid_1499.hash)['tx'][0], valid_1499.vtx[0].hash) - node.invalidateblock(valid_1499.hash) - assert_equal(node.getbestblockhash(), transfer_block) + node.invalidateblock(transfer_block) + assert_equal(node.getbestblockhash(), valid_1498.hash) + assert_equal(node.getblock(valid_1498.hash)['tx'][0], valid_1498.vtx[0].hash) + + node.invalidateblock(valid_1498.hash) + assert_equal(node.getbestblockhash(), issue_block_1497) assert_equal(self.deployment_status(node, 'coinbase'), 'locked_in') node = self.restart_with_version(ASSETS_VALUES_COINBASE_VERSION) - assert_equal(node.getbestblockhash(), transfer_block) + assert_equal(node.getbestblockhash(), issue_block_1497) assert_equal(self.deployment_status(node, 'coinbase'), 'locked_in') + node.reconsiderblock(transfer_block) node.reconsiderblock(original_1499) assert_equal(node.getbestblockhash(), active_coinbase_tip) assert_equal(self.deployment_status(node, 'coinbase'), 'active') From 65461d0b559a798a3cab41a4ba8663b011781491 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:16:40 +0200 Subject: [PATCH 123/195] [FINDING-107] Expand restricted asset boundary matrix --- .../feature_rip5_activation_reorg.py | 68 +++++++++++++------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/test/functional/feature_rip5_activation_reorg.py b/test/functional/feature_rip5_activation_reorg.py index 0a0b1a3a93..d5c0931f2d 100755 --- a/test/functional/feature_rip5_activation_reorg.py +++ b/test/functional/feature_rip5_activation_reorg.py @@ -57,19 +57,21 @@ def run_test(self): self.restart_node(0, ['-assetindex', '-persistmempool=0', '-blockversion={}'.format(ASSETS_AND_RIP5_VERSION)]) node = self.nodes[0] - node.generate(activation_height - 3 - node.getblockcount()) + node.generate(activation_height - 4 - node.getblockcount()) assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'active') assert_equal(self.rip5_status(node), 'locked_in') - base_name = 'RIP5BOUNDARY' + base_names = ('RIP5BOUNDARY', 'RIP5PLUSONE', 'RIP5PLUSTWO') + base_name = base_names[0] restricted_name = '$' + base_name - base_txid = node.issue(base_name, 1)[0] + base_txids = [node.issue(name, 1)[0] for name in base_names] base_block = node.generate(1)[0] - assert_equal(node.getblockcount(), activation_height - 2) - assert base_txid in node.getblock(base_block)['tx'] + assert_equal(node.getblockcount(), activation_height - 3) + assert all(txid in node.getblock(base_block)['tx'] for txid in base_txids) assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'active') assert_equal(self.rip5_status(node), 'locked_in') - original_pre_activation = node.generate(1)[0] + original_h_minus_two = node.generate(1)[0] + original_h_minus_one = node.generate(1)[0] node.generate(1) assert_equal(node.getblockcount(), activation_height) assert_equal(self.rip5_status(node), 'active') @@ -78,9 +80,9 @@ def run_test(self): txid = node.decoderawtransaction(raw)['txid'] assert_equal(node.testmempoolaccept([raw])[0]['allowed'], True) - # Rewind to H-2. A competing H-1 block must reject the same valid + # Rewind to H-3. H-2 and H-1 candidates must reject the same # transaction even though this process has already reached ACTIVE. - node.invalidateblock(original_pre_activation) + node.invalidateblock(original_h_minus_two) assert_equal(node.getbestblockhash(), base_block) assert_equal(self.rip5_status(node), 'locked_in') assert_raises_rpc_error(-1, 'Restricted assets must be active', @@ -95,18 +97,25 @@ def run_test(self): test_util.x16r_hash_cmd = os.path.join( config['environment']['BUILDDIR'], 'src', 'test', 'test_raven_hash' + config['environment']['EXEEXT']) - block = create_block(int(base_block, 16), create_coinbase(activation_height - 1), - node.getblockheader(base_block)['time'] + 1) - block.nVersion = node.getblockheader(original_pre_activation)['version'] - block.vtx.append(from_hex(CTransaction(), raw)) - block.hashMerkleRoot = block.calc_merkle_root() - block.solve() - assert_equal(node.submitblock(block.serialize().hex()), - 'bad-tx-null-asset-data-before-restricted-assets-activated') - assert_equal(node.getbestblockhash(), base_block) + for candidate_height, original in ( + (activation_height - 2, original_h_minus_two), + (activation_height - 1, original_h_minus_one)): + parent = node.getbestblockhash() + block = create_block( + int(parent, 16), create_coinbase(candidate_height), + node.getblockheader(parent)['time'] + 1) + block.nVersion = node.getblockheader(original)['version'] + block.vtx.append(from_hex(CTransaction(), raw)) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + assert_equal(node.submitblock(block.serialize().hex()), + 'bad-tx-null-asset-data-before-restricted-assets-activated') + assert_equal(node.getbestblockhash(), parent) + node.generate(1) + assert_equal(node.getblockcount(), candidate_height) # Mempool admission at H-1 targets H, the first active block. - pre_activation = node.generate(1)[0] + pre_activation = node.getbestblockhash() assert_equal(node.getblockcount(), activation_height - 1) assert_equal(self.rip5_status(node), 'active') assert_equal(node.testmempoolaccept([raw])[0]['allowed'], True) @@ -115,16 +124,27 @@ def run_test(self): assert_equal(node.getblockcount(), activation_height) assert txid in node.getblock(activation_block)['tx'] assert_equal(node.getassetdata(restricted_name)['amount'], 1) + plus_one_name = '$' + base_names[1] + plus_one_raw = self.issue_raw(node, plus_one_name) + plus_one_txid = node.sendrawtransaction(plus_one_raw) + plus_one_block = node.generate(1)[0] + assert plus_one_txid in node.getblock(plus_one_block)['tx'] + plus_two_name = '$' + base_names[2] + plus_two_raw = self.issue_raw(node, plus_two_name) + plus_two_txid = node.sendrawtransaction(plus_two_raw) post_activation = node.generate(1)[0] - assert_equal(node.getblockcount(), activation_height + 1) + assert plus_two_txid in node.getblock(post_activation)['tx'] + assert_equal(node.getblockcount(), activation_height + 2) assert_equal(self.rip5_status(node), 'active') assert_equal(node.verifychain(4, 4), True) # Disconnect both sides of the boundary and restart while the old # rules apply. Reconsidering the saved branch must restore the asset. node.invalidateblock(pre_activation) - assert_equal(node.getbestblockhash(), base_block) + assert_equal(node.getblockcount(), activation_height - 2) assert_equal(node.getassetdata(restricted_name), None) + assert_equal(node.getassetdata(plus_one_name), None) + assert_equal(node.getassetdata(plus_two_name), None) assert_equal(self.rip5_status(node), 'locked_in') assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'active') assert_raises_rpc_error(-1, 'Restricted assets must be active', @@ -134,7 +154,7 @@ def run_test(self): self.restart_node(0, ['-assetindex', '-persistmempool=0', '-blockversion={}'.format(ASSETS_AND_RIP5_VERSION)]) node = self.nodes[0] - assert_equal(node.getbestblockhash(), base_block) + assert_equal(node.getblockcount(), activation_height - 2) assert_equal(self.rip5_status(node), 'locked_in') assert_raises_rpc_error(-1, 'Restricted assets must be active', node.listglobalrestrictions) @@ -142,9 +162,13 @@ def run_test(self): early_result['reject-reason']) node.reconsiderblock(pre_activation) assert_equal(node.getbestblockhash(), post_activation) - assert_equal(node.getblockcount(), activation_height + 1) + assert_equal(node.getblockcount(), activation_height + 2) assert txid in node.getblock(activation_block)['tx'] + assert plus_one_txid in node.getblock(plus_one_block)['tx'] + assert plus_two_txid in node.getblock(post_activation)['tx'] assert_equal(node.getassetdata(restricted_name)['amount'], 1) + assert_equal(node.getassetdata(plus_one_name)['amount'], 1) + assert_equal(node.getassetdata(plus_two_name)['amount'], 1) assert_equal(node.listglobalrestrictions(), []) assert_equal(node.verifychain(4, 4), True) From 2fd13fc9ec54283b8a8b25ecb307abb2f77ce138 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:27:11 +0200 Subject: [PATCH 124/195] [FINDING-105] Connect competing active asset issue branch --- .../feature_asset_activation_reorg.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/functional/feature_asset_activation_reorg.py b/test/functional/feature_asset_activation_reorg.py index 521cb5a006..7915b0560d 100755 --- a/test/functional/feature_asset_activation_reorg.py +++ b/test/functional/feature_asset_activation_reorg.py @@ -108,6 +108,42 @@ def run_test(self): node.reconsiderblock(original_block) assert_equal(node.getbestblockhash(), active_tip) + # Connect a longer competing branch with the signed issue at the + # first active height. The later main-chain issues are absent on this + # branch, then restored after it is invalidated. + side_parent = active_block + side_first = None + for candidate_height in range(active_height + 1, active_height + 5): + block = create_block( + int(side_parent, 16), create_coinbase(candidate_height), + node.getblockheader(side_parent)['time'] + 2) + block.nVersion = node.getblockheader(issue_block)['version'] + if candidate_height == active_height + 1: + block.vtx.append(from_hex(CTransaction(), raw)) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + result = node.submitblock(block.serialize().hex()) + assert_equal(result, None if candidate_height == active_height + 4 + else 'inconclusive') + if side_first is None: + side_first = block.hash + assert side_first != issue_block + side_parent = block.hash + assert_equal(node.getbestblockhash(), side_parent) + assert issue_txid in node.getblock(side_first)['tx'] + assert_equal(node.getassetdata('LATCHASSET')['amount'], 1) + assert_equal(node.getassetdata('LATCHPLUSONE'), None) + assert_equal(node.getassetdata('LATCHPLUSTWO'), None) + self.stop_node(0) + self.start_node(0, ['-assetindex', '-persistmempool=0']) + node = self.nodes[0] + assert_equal(node.getbestblockhash(), side_parent) + assert_equal(node.getassetdata('LATCHASSET')['amount'], 1) + node.invalidateblock(side_first) + assert_equal(node.getbestblockhash(), active_tip) + assert_equal(node.getassetdata('LATCHPLUSONE')['amount'], 1) + assert_equal(node.getassetdata('LATCHPLUSTWO')['amount'], 1) + node.invalidateblock(active_block) assert_equal(node.getbestblockhash(), locked_tip) assert_equal(node.getblockchaininfo()['bip9_softforks']['assets']['status'], 'locked_in') @@ -132,6 +168,9 @@ def run_test(self): assert_equal(live_result['allowed'], restarted_result['allowed']) node.reconsiderblock(active_block) + assert_equal(node.getbestblockhash(), side_parent) + assert issue_txid in node.getblock(side_first)['tx'] + node.invalidateblock(side_first) assert_equal(node.getbestblockhash(), after_two_block) assert issue_txid in node.getblock(issue_block)['tx'] assert after_one_txid in node.getblock(after_block)['tx'] From cb9bbbbe844341b585993863bb179084347d0c83 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:27:11 +0200 Subject: [PATCH 125/195] [FINDING-107] Connect restricted asset branches across activation --- .../feature_rip5_activation_reorg.py | 76 +++++++++++++++++-- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/test/functional/feature_rip5_activation_reorg.py b/test/functional/feature_rip5_activation_reorg.py index d5c0931f2d..16415595a4 100755 --- a/test/functional/feature_rip5_activation_reorg.py +++ b/test/functional/feature_rip5_activation_reorg.py @@ -80,6 +80,37 @@ def run_test(self): txid = node.decoderawtransaction(raw)['txid'] assert_equal(node.testmempoolaccept([raw])[0]['allowed'], True) + config = configparser.ConfigParser() + config.read(self.options.configfile) + test_util.x16r_hash_cmd = os.path.join( + config['environment']['BUILDDIR'], 'src', 'test', + 'test_raven_hash' + config['environment']['EXEEXT']) + + # Submit pre-active siblings while the unrelated active tip is at H. + # They are stored as side branches, then fail when the main child is + # invalidated and each sibling is considered for connection. + active_tip = node.getbestblockhash() + for candidate_height, parent, original in ( + (activation_height - 1, original_h_minus_two, + original_h_minus_one), + (activation_height - 2, base_block, original_h_minus_two)): + block = create_block( + int(parent, 16), create_coinbase(candidate_height), + node.getblockheader(parent)['time'] + 2) + block.nVersion = node.getblockheader(original)['version'] + block.vtx.append(from_hex(CTransaction(), raw)) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + assert_equal(node.submitblock(block.serialize().hex()), 'inconclusive') + assert_equal(node.getbestblockhash(), active_tip) + node.invalidateblock(original) + assert_equal(node.getbestblockhash(), parent) + side_tip = next(tip for tip in node.getchaintips() + if tip['hash'] == block.hash) + assert_equal(side_tip['status'], 'invalid') + node.reconsiderblock(original) + assert_equal(node.getbestblockhash(), active_tip) + # Rewind to H-3. H-2 and H-1 candidates must reject the same # transaction even though this process has already reached ACTIVE. node.invalidateblock(original_h_minus_two) @@ -92,11 +123,6 @@ def run_test(self): assert_equal(early_result['reject-reason'], '16: bad-tx-null-asset-data-before-restricted-assets-activated') - config = configparser.ConfigParser() - config.read(self.options.configfile) - test_util.x16r_hash_cmd = os.path.join( - config['environment']['BUILDDIR'], 'src', 'test', - 'test_raven_hash' + config['environment']['EXEEXT']) for candidate_height, original in ( (activation_height - 2, original_h_minus_two), (activation_height - 1, original_h_minus_one)): @@ -138,6 +164,43 @@ def run_test(self): assert_equal(self.rip5_status(node), 'active') assert_equal(node.verifychain(4, 4), True) + # A longer competing branch may issue the same asset at the first + # active height. Connect it, restart on it, and restore the main + # branch before the old-rule rollback below. + main_tip = post_activation + side_parent = pre_activation + side_first = None + for candidate_height in range(activation_height, activation_height + 4): + block = create_block( + int(side_parent, 16), create_coinbase(candidate_height), + node.getblockheader(side_parent)['time'] + 2) + block.nVersion = ASSETS_AND_RIP5_VERSION + if candidate_height == activation_height: + block.vtx.append(from_hex(CTransaction(), raw)) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + result = node.submitblock(block.serialize().hex()) + assert_equal(result, None if candidate_height == activation_height + 3 + else 'inconclusive') + if side_first is None: + side_first = block.hash + assert side_first != activation_block + side_parent = block.hash + assert_equal(node.getbestblockhash(), side_parent) + assert txid in node.getblock(side_first)['tx'] + assert_equal(node.getassetdata(restricted_name)['amount'], 1) + assert_equal(node.getassetdata(plus_one_name), None) + assert_equal(node.getassetdata(plus_two_name), None) + self.restart_node(0, ['-assetindex', '-persistmempool=0', + '-blockversion={}'.format(ASSETS_AND_RIP5_VERSION)]) + node = self.nodes[0] + assert_equal(node.getbestblockhash(), side_parent) + assert_equal(node.getassetdata(restricted_name)['amount'], 1) + node.invalidateblock(side_first) + assert_equal(node.getbestblockhash(), main_tip) + assert_equal(node.getassetdata(plus_one_name)['amount'], 1) + assert_equal(node.getassetdata(plus_two_name)['amount'], 1) + # Disconnect both sides of the boundary and restart while the old # rules apply. Reconsidering the saved branch must restore the asset. node.invalidateblock(pre_activation) @@ -161,6 +224,9 @@ def run_test(self): assert_equal(node.testmempoolaccept([raw])[0]['reject-reason'], early_result['reject-reason']) node.reconsiderblock(pre_activation) + assert_equal(node.getbestblockhash(), side_parent) + assert txid in node.getblock(side_first)['tx'] + node.invalidateblock(side_first) assert_equal(node.getbestblockhash(), post_activation) assert_equal(node.getblockcount(), activation_height + 2) assert txid in node.getblock(activation_block)['tx'] From 3c08913f3f4842c2272d3187fef1bfdd3c479f01 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:31:57 +0200 Subject: [PATCH 126/195] [FINDING-067] Record testnet and activation branch evidence --- ...0025-v4.8-security-remediation-register.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 212402a6e0..aa6aae7353 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4698,6 +4698,27 @@ production source under src/; workflow, depends, documentation, and test paths changed. The private comparison is evidence/a16-to-fecdc-source-diff.json. +At 2026-10-15 09:17 UTC, the same running clean a16a05bdb daemon was +at testnet height 2,554,210, tip +00000907a0a6ddbc93cff6b51cfe9805315fffc3ca4c36acf97e9a797877d9e8. +The incremental active-chain scanner covered 2,554,211 blocks and +2,774,605 transactions with zero witness-v2 outputs or spends, +wrapped spends, and malformed candidates in its defined classes. It +verified 1,274 checkpoints. All 2,554,211 stored block records remained +below 2,000,000 serialized bytes; the maximum was still 1,998,152 +bytes, giving the same 7,992,608-WU old-rule upper bound. A fresh +level-4 `verifychain` over the last 2016 blocks returned true in 24.8 +seconds at the same tip. The private records are +evidence/testnet-history-scan-a16-20261015.json, +evidence/testnet-block-record-size-a16-20261015.json, and +evidence/testnet-verifychain-a16-20261015.json. The live daemon's +executable digest matches the previously recorded clean a16a05bdb +binary, as recorded in +evidence/testnet-daemon-provenance-a16-20261015.json. The separate CLI +path used by the record-size scanner does not establish daemon +provenance. The scan still covers only the known +active chain and cannot test future bit-13 activation. + The live RPC warning said unknown new rules activated, versionbit 11. Bit 11 is the known transfer-overflow deployment, ACTIVE since height 2,493,792. None of the latest 2016 block headers signaled bits 11, @@ -4719,6 +4740,9 @@ limit. Current public testnet therefore cannot demonstrate the native provide that proof. The operator plan must identify this testnet/mainnet difference. The private live chain, peer, and template record is `evidence/testnet-live-template-a16.json`. +A fresh 2026-10-15 template for height 2,554,219 again advertised +16,000,000 for both limits and bit 12 as available for signaling. Its +private record is evidence/testnet-live-template-a16-20261015.json. The exact-candidate two-binary FINDING-160 tests also passed: both Core 4.8.0 and a16a05bdb accept a 7,600,636-WU control block; @@ -6891,6 +6915,17 @@ Cross-platform CI and public deployment remain separate release gates. `evidence/f105-hminus2-hplus2-local.log`. The deferred short-branch result is recorded explicitly rather than treated as immediate block-connection proof. +- **Connected active competing branch:** `2fd13fc9e` builds a distinct + issue block at the first bit-6 active height on a side branch, extends + that branch beyond the original three-issue tip, and verifies its connected + asset state before and after restart. Invalidating the side branch + restores the original two later asset issues. Reconsidering both + branches after a rollback to LOCKED_IN again selects the longer branch, + then restores the original branch on invalidation. The direct Python + 3.10 test, all 33 mandatory functional tests, and local `make -j4 check` + passed. Private logs are + `evidence/required-functional-f107-sidebranches-local.log` and + `evidence/local-make-check-f107-sidebranches.log`. - **Final status:** MITIGATED locally, OPEN for release qualification. ### FINDING-106: Additional Core 4.8.0 deployment latches survive reorgs @@ -6979,6 +7014,18 @@ Cross-platform CI and public deployment remain separate release gates. `evidence/local-make-check-f173-test-candidate.log`. Build Raven `38592937851` passed its security job, but the paired Final Gate stopped before tests for the separate FINDING-174 ccache race. +- **Expanded mined boundary matrix:** `76983ad18` submits the same signed + nonzero reissue at bit-9 heights 574 and 575, where the old rule accepts + it, and at 576, 577, and 578, where the enforced-value rule rejects it. + It also submits an asset-bearing coinbase at bit-10 heights 1498 and + 1499, where the old rule accepts it, and at 1500, 1501, and 1502, where + the active rule rejects it. The test connects competing pre-activation + branches, rewinds, restarts, restores the active branch, and verifies + chain state. The direct Python 3.10 run passed; all 33 mandatory + functional tests and local `make -j4 check` passed afterward. Private + logs are `evidence/f107-bit9-bit10-matrix-local.log`, + `evidence/required-functional-f107-expanded-local.log`, and + `evidence/local-make-check-f107-expanded.log`. - **Final status:** OPEN for release qualification. ### FINDING-107: Context-free block checks consume active-tip asset state @@ -7096,6 +7143,31 @@ Cross-platform CI and public deployment remain separate release gates. `evidence/f105-hminus2-hplus2-local.log`, `evidence/f106-f107-enforced-coinbase-local.log`, and `evidence/required-functional-local-final-candidate.log`. +- **Expanded bit-7 side-branch matrix:** `65461d0b5` rejects the same + signed restricted-issue transaction in candidate blocks at heights + 574 and 575 while the process has already observed bit 7 ACTIVE. + Distinct restricted issues then enter connected blocks at the first + three active heights 576, 577, and 578. A rewind across the boundary, + restart under the old rule, reconsideration, and `verifychain` preserve + all three issues and reject pre-activation admission. The direct Python + 3.10 run, all 33 mandatory functional tests, and local `make -j4 check` + passed. `76983ad18` separately extends the bit-9 and bit-10 competing + branch checks to H-2, H-1, H, H+1, and H+2. Private logs are + `evidence/f107-bit7-matrix-local.log`, + `evidence/required-functional-f107-expanded-local.log`, and + `evidence/local-make-check-f107-expanded.log`. +- **Connected bit-7 side branches:** `cb9bbbbe8` submits restricted-issue + candidates at H-2 and H-1 while an unrelated active tip is at H; + each candidate remains on a short side branch, then becomes invalid + when its competing main-chain child is removed. It also connects a + distinct valid issue branch at H, extends it beyond the original H+2 + tip, verifies its restricted asset state after restart, and restores + the original H+1 and H+2 issues after invalidation. Reconsideration + after a rollback to LOCKED_IN again selects the longer valid branch. + The direct Python 3.10 test, all 33 mandatory functional tests, and + local `make -j4 check` passed. Private logs are + `evidence/required-functional-f107-sidebranches-local.log` and + `evidence/local-make-check-f107-sidebranches.log`. - **Final status:** MITIGATED, still OPEN for release qualification. Nested candidate-parent propagation and side-branch connection after overtaking are tested. Broader historical replay and second-audit qualification remain From 0c33cc92c48dd475cc9955c8696fc86dfb6882d1 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:31:57 +0200 Subject: [PATCH 127/195] [FINDING-067] Update draft qualification notes --- doc/release-notes/release-notes-4.8.1.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md index f2e07f6f71..7bd4aa7c5f 100644 --- a/doc/release-notes/release-notes-4.8.1.md +++ b/doc/release-notes/release-notes-4.8.1.md @@ -43,7 +43,7 @@ rules after activation. Testnet differs from this mainnet schedule. Its native RIP-25 rule is force-enabled from genesis by `nPQHybridEnabled=true`. At the reviewed -2026-10-14 testnet tip, `getblocktemplate` therefore advertises the later +2026-10-15 testnet tip, `getblocktemplate` therefore advertises the later 16 MWU limit, even while the raw bit-12 BIP9 state reads STARTED. This does not imply that mainnet has activated bit 12. The testnet override cannot demonstrate the public 8-to-12-MWU BIP9 transition; controlled @@ -105,14 +105,16 @@ inputs and check the final transaction before broadcast. The available testnet active chain was replayed from genesis by a clean 4.8.1 candidate with `-assumevalid=0`, then synchronized with public -peers; the exact snapshot and limits are in the remediation register. +peers through height 2,554,210. The exact snapshot and limits are in the +remediation register. The scanned chain contained no native witness-v2 outputs or spends, so the replay checks historical compatibility rather than an on-chain PQ signature. It does not test unknown side branches or future bit-13 activation. CRITICAL FINDING-104 through FINDING-107 remain open for release -qualification because their mined activation and side-branch regression -matrix is incomplete. FINDING-112 remains open for intermittent wallet +qualification. Local regressions now cover the known boundary heights +and competing asset branches; their final coverage review and exact-SHA +CI remain due. FINDING-112 remains open for intermittent wallet test-gate diagnosis. Final-SHA artifacts, operator readiness, manual GUI checks, checksums, and signatures still require qualification. The release will be tagged and published only after those checks and From 2556cb434dea6e69da4725743997adae927b6d55 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:40:49 +0200 Subject: [PATCH 128/195] [FINDING-175] Freeze xproto source timeout failure --- ...0025-v4.8-security-remediation-register.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index aa6aae7353..9743863798 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9483,3 +9483,35 @@ Cross-platform CI and public deployment remain separate release gates. log: `evidence/local-make-check-f174-candidate.log`. - **Final status:** FIXED locally, OPEN until both exact-SHA workflows pass and the five release artifacts are checked. + +### FINDING-175: xproto source timeout blocks the Linux release artifact + +- **Severity:** MEDIUM release availability; the artifact job fails closed + before building the Linux x86_64 package. +- **Frozen initial status:** OPEN on 2026-10-15 at + `ee30148e794ab5fec6c07d621e6bb4a581ef942f`, before changing the + pinned xproto fetch recipe. +- **Affected RIP-25 invariant:** Build Raven must deliver all five release + targets on the same exact SHA as a green Final Gate. +- **Affected Core 4.8.0 behavior:** The xproto 7.0.26 package recipe and + its HTTP primary source are inherited from Core 4.8.0. This is a + dependency availability defect, not a consensus failure. +- **Root cause:** The primary xproto archive host timed out repeatedly. + The generic fetch then attempted an unset fallback download path, + producing a malformed URL and terminating the depends build. +- **Affected file/function:** `depends/packages/xproto.mk`, the pinned + source recipe; `depends/funcs.mk`, generic fetch fallback. +- **Concrete scenario:** Build Raven `38710185861`, Linux x86_64 job + `116347291798`, failed in `Build Dependencies` after repeated + `curl: (28) Connection timeout` for `xproto-7.0.26.tar.bz2` and + `curl: (3) URL using bad/illegal format or missing URL` on the empty + fallback. The mandatory security job at that SHA passed. +- **Expected behavior:** When the primary host is unavailable, an HTTPS + alternate source supplies exactly the pinned archive. A SHA-256 + mismatch must reject the archive before extraction. +- **Regression required:** Retain the red CI log; force the primary + source to fail in a clean local fetch, verify the alternate archive's + existing SHA-256, and prove that altered bytes fail closed. Then rerun + both workflows on one final SHA. +- **Final status:** OPEN pending a red/green fallback regression, fix, + and exact-SHA CI qualification. From 8552b2ca94255e1fd02f5411a7b2ae1ba7e6062a Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:42:45 +0200 Subject: [PATCH 129/195] [FINDING-175] Add verified xproto HTTPS fallback --- depends/packages/xproto.mk | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/depends/packages/xproto.mk b/depends/packages/xproto.mk index 50a90b2685..03b6a8b1f9 100644 --- a/depends/packages/xproto.mk +++ b/depends/packages/xproto.mk @@ -1,9 +1,16 @@ package=xproto $(package)_version=7.0.26 $(package)_download_path=http://xorg.freedesktop.org/releases/individual/proto +$(package)_fallback_download_path=https://download.bitcoinabc.org/depends-sources $(package)_file_name=$(package)-$($(package)_version).tar.bz2 $(package)_sha256_hash=636162c1759805a5a0114a369dffdeccb8af8c859ef6e1445f26a4e6e046514f +define $(package)_fetch_cmds +( test -f $(xproto_source) || \ + $(call fetch_file_inner,xproto,$(xproto_download_path),$(xproto_download_file),$(xproto_file_name),$(xproto_sha256_hash)) || \ + $(call fetch_file_inner,xproto,$(xproto_fallback_download_path),$(xproto_download_file),$(xproto_file_name),$(xproto_sha256_hash)) ) +endef + define $(package)_set_vars $(package)_config_opts=--disable-shared endef From 1961a4602617825b224fbbceac67c0f1d82cb5b1 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:43:11 +0200 Subject: [PATCH 130/195] [FINDING-175] Record verified xproto fallback --- ...-0025-v4.8-security-remediation-register.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 9743863798..5c5312fa7e 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9513,5 +9513,19 @@ Cross-platform CI and public deployment remain separate release gates. source to fail in a clean local fetch, verify the alternate archive's existing SHA-256, and prove that altered bytes fail closed. Then rerun both workflows on one final SHA. -- **Final status:** OPEN pending a red/green fallback regression, fix, - and exact-SHA CI qualification. +- **Remediation commit:** `8552b2ca94255e1fd02f5411a7b2ae1ba7e6062a` + adds a package-specific HTTPS fallback. The xproto version and pinned + SHA-256 are unchanged. +- **Verification:** The red CI job log is private + `evidence/build-raven-38710185861-linux-job.raw.log`. A clean local + fetch with the primary URL forced to `127.0.0.1:9` reproduced the + empty fallback failure before the fix. With the new recipe, the same + forced-primary test fetched the alternate archive and matched its + existing digest. A one-byte-altered alternate archive was rejected + before installation. Private logs: `evidence/f175-xproto-red.log`, + `evidence/f175-xproto-green.log`, and + `evidence/f175-xproto-reject.log`. Local `make -j4 check` passed after + the fix, including `test/test_raven`; log: + `evidence/local-make-check-f175-candidate.log`. +- **Final status:** FIXED locally, OPEN until both exact-SHA workflows + pass and all five release artifacts are checked. From 6e0a2213e13654c322ee0ad9cff3b8ee56b616ad Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:43:43 +0200 Subject: [PATCH 131/195] [FINDING-112] Correct recovery locations and repeat evidence --- doc/RIP-0025-v4.8-security-remediation-register.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 5c5312fa7e..323f0295e8 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7344,8 +7344,8 @@ Cross-platform CI and public deployment remain separate release gates. complete `test_raven` run and third `make check` passed without a source change between the failures and successes. - **Affected file/function/lines:** - `src/wallet/test/pq_wallet_tests.cpp:1485-1503,1540-1551`, recovery tests; - `src/wallet/db.cpp:285-465`, `CDB::RecoverInternal`. The failing return + `src/wallet/test/pq_wallet_tests.cpp:1640-1666,1699-1709`, recovery tests; + `src/wallet/db.cpp:285-523`, `CDB::RecoverInternal`. The failing return branch has not yet been identified. - **Expected behavior:** Identical source and fixtures produce consistent pass/fail results, with enough failure detail to diagnose a recovery @@ -7356,6 +7356,12 @@ Cross-platform CI and public deployment remain separate release gates. regression. Do not skip or relax either test. - **Regression required:** Repeated full wallet `make check` runs and the affected recovery cases with the original assertions intact. +- **Additional evidence:** Five later full `make check` runs passed all 511 + unit cases without a source change. Private logs are + `evidence/f112-repeat-1.make.log` through + `evidence/f112-repeat-5.make.log`, with matching `.unit.log` files. + Neither historical failure yielded a retained stage-specific Berkeley + DB diagnostic, so the passing repeats do not establish a root cause. - **Final status:** OPEN pending root-cause investigation. ### FINDING-113: Crash replay rolls UTXO state forward without asset state From ff4cb3be498cf3e5c98d05bc306f66bd788da499 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:45:03 +0200 Subject: [PATCH 132/195] [FINDING-176] Freeze libXau source fallback failure --- ...0025-v4.8-security-remediation-register.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 323f0295e8..281975d682 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9535,3 +9535,33 @@ Cross-platform CI and public deployment remain separate release gates. `evidence/local-make-check-f175-candidate.log`. - **Final status:** FIXED locally, OPEN until both exact-SHA workflows pass and all five release artifacts are checked. + +### FINDING-176: libXau source has the same empty fallback failure + +- **Severity:** MEDIUM release availability; the Linux depends build can + fail closed before packaging. +- **Frozen initial status:** OPEN on 2026-10-15 at `6e0a2213e`, before + changing the pinned libXau fetch recipe. +- **Affected RIP-25 invariant:** The release SHA must build all five + targets after its mandatory security gate. +- **Affected Core 4.8.0 behavior:** The libXau 1.0.8 package recipe and + HTTP Xorg primary source are inherited from Core 4.8.0. +- **Root cause:** libXau depends on xproto and uses the same Xorg host + that timed out in FINDING-175. It has no package-specific alternate + source. The generic fallback URL is empty, so a primary failure ends + with an invalid URL instead of trying an available mirror. +- **Affected file/function:** `depends/packages/libXau.mk`, pinned source + recipe; `depends/funcs.mk`, generic fetch fallback. +- **Concrete scenario:** In an isolated local depends fetch with the + primary path forced to `127.0.0.1:9`, the unmodified recipe failed + with `curl: (3) URL rejected: No host part in the URL`. The prior + Linux CI job stopped at xproto before reaching libXau. The same Xorg + archive host timed out during a separate direct availability probe. +- **Expected behavior:** A second HTTPS source supplies the exact pinned + libXau archive when the primary is unavailable. The unchanged SHA-256 + must reject altered bytes before extraction. +- **Regression required:** Preserve the red local fetch, force the + primary source to fail after adding the alternate, verify the pinned + digest, reject a one-byte-altered archive, and rerun `make check` and + exact-SHA CI. +- **Final status:** OPEN pending fix and verification. From f154953a8e554c7e32a2c878b41fb0795b014103 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:24:45 +0200 Subject: [PATCH 133/195] [FINDING-176] Add verified libXau HTTPS fallback --- depends/packages/libXau.mk | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/depends/packages/libXau.mk b/depends/packages/libXau.mk index e87df2e4de..ca5d1779eb 100644 --- a/depends/packages/libXau.mk +++ b/depends/packages/libXau.mk @@ -1,10 +1,17 @@ package=libXau $(package)_version=1.0.8 $(package)_download_path=http://xorg.freedesktop.org/releases/individual/lib/ +$(package)_fallback_download_path=https://download.bitcoinabc.org/depends-sources $(package)_file_name=$(package)-$($(package)_version).tar.bz2 $(package)_sha256_hash=fdd477320aeb5cdd67272838722d6b7d544887dfe7de46e1e7cc0c27c2bea4f2 $(package)_dependencies=xproto +define $(package)_fetch_cmds +( test -f $(libXau_source) || \ + $(call fetch_file_inner,libXau,$(libXau_download_path),$(libXau_download_file),$(libXau_file_name),$(libXau_sha256_hash)) || \ + $(call fetch_file_inner,libXau,$(libXau_fallback_download_path),$(libXau_download_file),$(libXau_file_name),$(libXau_sha256_hash)) ) +endef + define $(package)_set_vars $(package)_config_opts=--disable-shared $(package)_config_opts_linux=--with-pic From 0fca1ce1f8b2d7cc0e480bf68d4db0928ec13caa Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:25:54 +0200 Subject: [PATCH 134/195] [FINDING-176] Record verified libXau fallback --- doc/RIP-0025-v4.8-security-remediation-register.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 281975d682..9a3ff2c379 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9564,4 +9564,16 @@ Cross-platform CI and public deployment remain separate release gates. primary source to fail after adding the alternate, verify the pinned digest, reject a one-byte-altered archive, and rerun `make check` and exact-SHA CI. -- **Final status:** OPEN pending fix and verification. +- **Remediation commit:** `f154953a8e554c7e32a2c878b41fb0795b014103` + adds a package-specific HTTPS fallback. The libXau version and pinned + SHA-256 are unchanged. +- **Verification:** `evidence/f176-libxau-red.log` records the missing + fallback before the fix. With the primary source forced to fail, the + new recipe downloaded the alternate archive with the pinned digest; + `evidence/f176-libxau-green.log` records the passing fetch. A + one-byte-altered archive failed its checksum before installation in + `evidence/f176-libxau-reject.log`. Local `make -j4 check` passed after + the fix, including `test/test_raven`; log: + `evidence/local-make-check-f176-candidate.log`. +- **Final status:** FIXED locally, OPEN until the final exact-SHA CI + pair and artifact checks pass. From df41e8912e6c13fbee1e44aa826d8de1944993f6 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:27:41 +0200 Subject: [PATCH 135/195] [FINDING-104][FINDING-105][FINDING-106][FINDING-107] Close activation context defects --- ...0025-v4.8-security-remediation-register.md | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 9a3ff2c379..7b4112c3a0 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -6846,7 +6846,16 @@ Cross-platform CI and public deployment remain separate release gates. `evidence/f104-hminus2-hplus2-local.log`. The earlier pre-ACTIVE candidate-block rejection and vulnerable-binary red test remain in place. This does not claim a connected pre-ACTIVE transfer block. -- **Final status:** MITIGATED locally, OPEN for release qualification. +- **Final coverage disposition:** The bit-8 regression now checks the same + long transfer at the two pre-ACTIVE tips, rejects pre-ACTIVE mined + side-branch candidates, and connects the first three ACTIVE transfer + heights. Rollback, restart, reconsideration, mempool evaluation, and + `verifychain` pass. The original vulnerable-binary red result and the + corrected-binary green result remain preserved. All 33 mandatory + functional tests, local `make -j4 check`, Build Raven `38713686026`, + and Final Gate `38713685824` passed on `0c33cc92c`. +- **Final status:** FIXED for the bit-8 contextual activation defect. + Final release-SHA qualification remains tracked by FINDING-067. ### FINDING-105: Asset deployment activation survives a reorg incorrectly @@ -6926,7 +6935,16 @@ Cross-platform CI and public deployment remain separate release gates. passed. Private logs are `evidence/required-functional-f107-sidebranches-local.log` and `evidence/local-make-check-f107-sidebranches.log`. -- **Final status:** MITIGATED locally, OPEN for release qualification. +- **Final coverage disposition:** The bit-6 test rejects the same signed + issue on pre-ACTIVE candidate parents, mines real issues at H through + H+2, connects a distinct longer ACTIVE side branch, and compares asset + state after rollback, restart, and reconsideration. The original + vulnerable-binary red and corrected-binary green results remain + preserved. All 33 mandatory functional tests, local `make -j4 check`, + Build Raven `38713686026`, and Final Gate `38713685824` passed on + `0c33cc92c`. +- **Final status:** FIXED for the bit-6 contextual activation defect. + Final release-SHA qualification remains tracked by FINDING-067. ### FINDING-106: Additional Core 4.8.0 deployment latches survive reorgs @@ -7026,7 +7044,16 @@ Cross-platform CI and public deployment remain separate release gates. logs are `evidence/f107-bit9-bit10-matrix-local.log`, `evidence/required-functional-f107-expanded-local.log`, and `evidence/local-make-check-f107-expanded.log`. -- **Final status:** OPEN for release qualification. +- **Final coverage disposition:** The signed enforced-value reissue and + asset-bearing coinbase candidates now cover H-2, H-1, H, H+1, and H+2 + under their respective old and new rules. The bit-7 restricted-asset + test covers the same boundary, real ACTIVE issue outputs, competing + branches, rollback, restart, and reconsideration. The unit test + preserves the enforced-value LOCKED_IN rule. All 33 mandatory + functional tests, local `make -j4 check`, Build Raven `38713686026`, + and Final Gate `38713685824` passed on `0c33cc92c`. +- **Final status:** FIXED for the three inherited deployment latches. + Final release-SHA qualification remains tracked by FINDING-067. ### FINDING-107: Context-free block checks consume active-tip asset state @@ -7168,10 +7195,22 @@ Cross-platform CI and public deployment remain separate release gates. local `make -j4 check` passed. Private logs are `evidence/required-functional-f107-sidebranches-local.log` and `evidence/local-make-check-f107-sidebranches.log`. -- **Final status:** MITIGATED, still OPEN for release qualification. Nested - candidate-parent propagation and side-branch connection after overtaking - are tested. Broader historical replay and second-audit qualification remain - due. +- **Final coverage disposition:** Bits 6, 7, 8, 9, and 10 now have + candidate-parent boundary and competing-branch regressions. The bit-6 + and bit-7 valid alternate branches actually overtake the selected + chain and survive restart; invalid pre-ACTIVE candidates are judged + when their own branches become eligible for connection. The bit-8 + long-transfer branch also connects and reorganizes. All 33 mandatory + functional tests and local `make -j4 check` passed. The clean a16a05bdb + binary replayed public testnet history from genesis with + `-assumevalid=0`, then passed a full active-chain scan and a deep + 2016-block check. No production source file outside `src/test/` + changed between that build and `0c33cc92c`, whose Build Raven + `38713686026` and Final Gate + `38713685824` both passed. The previously open scoped boundary and + side-branch proof obligations are met. +- **Final status:** FIXED for the candidate-parent consensus-context + defect. Final release-SHA qualification remains tracked by FINDING-067. ### FINDING-108: Synthetic VersionBits test reuses stale cache entries From e0b928567f23e3c68786c2c075894c874f20cad2 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:28:57 +0200 Subject: [PATCH 136/195] [FINDING-067] Record exact remaining release gates --- ...0025-v4.8-security-remediation-register.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 7b4112c3a0..6bba4492b9 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4767,6 +4767,30 @@ exercise on release operating systems, final release notes and signed checksums, artifact provenance on the actual release commit, and the owner's separate approval before a tag or GitHub release. +At the 2026-10-15 `0c33cc92c` checkpoint, Build Raven `38713686026` +passed its mandatory security job and all five release targets; Final Gate +`38713685824` passed all eight jobs on the same SHA. The H-2 through H+2 +matrices, connected alternate branches, restart and rollback tests, and +clean testnet replay complete the scoped proof for FINDING-104 through +FINDING-107, now marked FIXED below. Artifact downloads and provenance +checks for this SHA are in progress. A prior Build Raven on `ee30148e7` +failed only its Linux x86_64 artifact job on the inherited xproto source +timeout, recorded as FINDING-175. The libXau dependency had the same +latent missing fallback, recorded as FINDING-176. Both have verified +local fail-closed fixes but require the final exact-SHA CI pair. + +FINDING-067 remains HIGH and OPEN. Its exact remaining gates are: +FINDING-112 wallet test-gate root-cause disposition; a wholly green +Build Raven and Final Gate pair on the final source SHA with five checked +artifact packages; deployment preparation and published upgrade +instructions for the incompatible bit-12 block-weight change; manual Qt +exercise on Windows and macOS release systems; completion of release +notes, checksums, and signatures on the final commit; and the owner's +separate approval before creating a tag or GitHub release. The clean +testnet historical replay and scans are complete at their recorded +heights, while future public bit-12 and bit-13 activation cannot yet be +observed on the public chain. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical From 3e9bb5096c745a8281ace8279f316a401beb5813 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:33:49 +0200 Subject: [PATCH 137/195] [FINDING-174] Close ccache race with paired CI evidence --- doc/RIP-0025-v4.8-security-remediation-register.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 6bba4492b9..6244919d04 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9549,9 +9549,15 @@ Cross-platform CI and public deployment remain separate release gates. and produced their object files. Its JSON record is `evidence/f174-ccache-first-use-local.json`. The required functional list gate and local `make -j4 check` passed after the workflow edit; - log: `evidence/local-make-check-f174-candidate.log`. -- **Final status:** FIXED locally, OPEN until both exact-SHA workflows - pass and the five release artifacts are checked. + log: `evidence/local-make-check-f174-candidate.log`. Build Raven + `38713686026` and Final Gate `38713685824` passed on exact + `0c33cc92c`; both mandatory security jobs ran `make check`, all five + release targets and seven Final Gate matrix jobs passed, and the five + downloaded release packages passed checksum and provenance review. + The private verification record is + `evidence/ci-0c33cc92c-verification.json`. +- **Final status:** FIXED for the ccache first-use race. Release + qualification on the later final source SHA remains FINDING-067. ### FINDING-175: xproto source timeout blocks the Linux release artifact From fec5ca26164aeb4b73d908cacb69597f1ffdf1a0 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:34:08 +0200 Subject: [PATCH 138/195] [FINDING-067] Update draft release qualification notes --- doc/release-notes/release-notes-4.8.1.md | 35 ++++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md index 7bd4aa7c5f..eec0955881 100644 --- a/doc/release-notes/release-notes-4.8.1.md +++ b/doc/release-notes/release-notes-4.8.1.md @@ -51,13 +51,14 @@ regtest boundary tests cover that transition. Testnet miners and node operators should account for the already active PQ and block-weight rules when comparing with Core 4.8.0. -Before miners begin signaling, the complete shared testnet history must -be checked against the network-bound ML-DSA rule, the old and new binaries -must be retested on both sides of the 8 MWU boundary, and the final 4.8.1 -SHA must pass mandatory `make check`, Final Gate, and all five Build Raven -targets. Operators must have time to upgrade before ACTIVE. If these -conditions are not met before STARTED, a reviewed release must move the -start and timeout before the original window begins. +Before miners begin signaling, the final 4.8.1 SHA must pass mandatory +`make check`, Final Gate, all five Build Raven targets, and artifact +checksum and provenance review. The clean candidate has already replayed +the available public testnet history from genesis, and both old and new +binaries have been tested on both sides of the 8 MWU boundary. Operators +must have time to upgrade before ACTIVE. If the remaining release gates +are not met before STARTED, a reviewed release must move the start and +timeout before the original window begins. ## Migration to PQ custody @@ -105,17 +106,21 @@ inputs and check the final transaction before broadcast. The available testnet active chain was replayed from genesis by a clean 4.8.1 candidate with `-assumevalid=0`, then synchronized with public -peers through height 2,554,210. The exact snapshot and limits are in the -remediation register. +peers through height 2,554,227. The exact snapshots and limits are in the +remediation register and private audit evidence. The scanned chain contained no native witness-v2 outputs or spends, so the replay checks historical compatibility rather than an on-chain PQ signature. It does not test unknown side branches or future bit-13 activation. -CRITICAL FINDING-104 through FINDING-107 remain open for release -qualification. Local regressions now cover the known boundary heights -and competing asset branches; their final coverage review and exact-SHA -CI remain due. FINDING-112 remains open for intermittent wallet -test-gate diagnosis. Final-SHA artifacts, operator readiness, manual -GUI checks, checksums, and signatures still require qualification. +The scoped activation and side-branch defects FINDING-104 through +FINDING-107 are fixed after H-2 through H+2 regressions, connected +alternate branches, rollback and restart checks, and a green exact-SHA +Build Raven and Final Gate pair on `0c33cc92c`. All five packages from +that run passed downloaded checksum and provenance review. Subsequent +source-availability fixes for pinned xproto and libXau require a new +exact-SHA pair and artifact review. FINDING-112 remains open for +intermittent wallet test-gate diagnosis. Operator readiness, manual +GUI checks on release operating systems, final checksums, and signatures +still require qualification. The release will be tagged and published only after those checks and the owner's separate release approval. From 86af1abb56bf88299b9417f16918678cadc24e9f Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:35:28 +0200 Subject: [PATCH 139/195] [FINDING-112] Record accepted historical disposition --- doc/RIP-0025-v4.8-security-remediation-register.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 6244919d04..42691c59ad 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -7425,7 +7425,15 @@ Cross-platform CI and public deployment remain separate release gates. `evidence/f112-repeat-5.make.log`, with matching `.unit.log` files. Neither historical failure yielded a retained stage-specific Berkeley DB diagnostic, so the passing repeats do not establish a root cause. -- **Final status:** OPEN pending root-cause investigation. +- **Owner disposition:** On 2026-10-15 the owner accepted classifying the + historical cause as not reconstructible from the retained evidence, + documenting the residual test-gate risk, and requiring a green + mandatory `make check` on the final 4.8.1 SHA. This disposition is + separate from FINDING-166 and does not prove a wallet production + defect or waive either recovery assertion. +- **Final status:** DISPOSED, historical cause unknown. Any renewed + recovery failure must be diagnosed and tracked before release; + FINDING-067 retains the final-SHA `make check` gate. ### FINDING-113: Crash replay rolls UTXO state forward without asset state From eb09997ca36356424cdcc00cdcc9ae024e0f9e5e Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:36:55 +0200 Subject: [PATCH 140/195] [FINDING-067] Update testnet and remaining gates --- ...0025-v4.8-security-remediation-register.md | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 42691c59ad..df5be07fe6 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4773,16 +4773,38 @@ passed its mandatory security job and all five release targets; Final Gate matrices, connected alternate branches, restart and rollback tests, and clean testnet replay complete the scoped proof for FINDING-104 through FINDING-107, now marked FIXED below. Artifact downloads and provenance -checks for this SHA are in progress. A prior Build Raven on `ee30148e7` -failed only its Linux x86_64 artifact job on the inherited xproto source +checks for this SHA subsequently passed on all five packages, including +individual SHA-256 manifests and one common source archive. The private +record is `evidence/ci-0c33cc92c-verification.json`. A prior Build +Raven on `ee30148e7` failed only its Linux x86_64 artifact job on the +inherited xproto source timeout, recorded as FINDING-175. The libXau dependency had the same latent missing fallback, recorded as FINDING-176. Both have verified local fail-closed fixes but require the final exact-SHA CI pair. +On 2026-10-15 the owner accepted the separate FINDING-112 disposition: +its two historical wallet recovery test failures could not be +reconstructed from retained logs, five later full 511-case suites +passed without source changes, and final-SHA `make check` remains +mandatory. This does not waive a renewed failure. + +The same unpruned clean testnet node later reached height 2,554,504, +hash `00000c918af9255f60483c82131231f8c5c1ef54cb74e63647e595504eaa1924`. +The full active-chain scan covered 2,554,505 blocks and 2,774,899 +transactions with zero witness-v2 outputs or spends, zero wrapped +spends, and zero malformed candidates in the scanner's defined classes. +It verified 1,278 checkpoints. Every stored block record remained below +2,000,000 bytes, with maximum 1,998,152 and old-rule weight upper +bound 7,992,608 WU. `verifychain 4 2016` passed in 23.4 seconds on +the same tip. Private records are +`evidence/testnet-history-scan-fec5-20261015.json`, +`evidence/testnet-block-record-size-fec5-20261015.json`, and +`evidence/testnet-verifychain-fec5-20261015.json`. + FINDING-067 remains HIGH and OPEN. Its exact remaining gates are: -FINDING-112 wallet test-gate root-cause disposition; a wholly green -Build Raven and Final Gate pair on the final source SHA with five checked -artifact packages; deployment preparation and published upgrade +a wholly green Build Raven and Final Gate pair on the final source SHA +with mandatory `make check` and five checked artifact packages; +deployment preparation and published upgrade instructions for the incompatible bit-12 block-weight change; manual Qt exercise on Windows and macOS release systems; completion of release notes, checksums, and signatures on the final commit; and the owner's From 35fa04a40d4f61cae45d80f51920d36c639e838f Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:37:09 +0200 Subject: [PATCH 141/195] [FINDING-067][FINDING-112] Update release risk notes --- doc/release-notes/release-notes-4.8.1.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md index eec0955881..41516d8d1d 100644 --- a/doc/release-notes/release-notes-4.8.1.md +++ b/doc/release-notes/release-notes-4.8.1.md @@ -106,7 +106,7 @@ inputs and check the final transaction before broadcast. The available testnet active chain was replayed from genesis by a clean 4.8.1 candidate with `-assumevalid=0`, then synchronized with public -peers through height 2,554,227. The exact snapshots and limits are in the +peers through height 2,554,504. The exact snapshots and limits are in the remediation register and private audit evidence. The scanned chain contained no native witness-v2 outputs or spends, so the replay checks historical compatibility rather than an on-chain PQ @@ -118,8 +118,11 @@ alternate branches, rollback and restart checks, and a green exact-SHA Build Raven and Final Gate pair on `0c33cc92c`. All five packages from that run passed downloaded checksum and provenance review. Subsequent source-availability fixes for pinned xproto and libXau require a new -exact-SHA pair and artifact review. FINDING-112 remains open for -intermittent wallet test-gate diagnosis. Operator readiness, manual +exact-SHA pair and artifact review. The owner accepted a documented +cause-not-reconstructible disposition for two historical intermittent +wallet recovery test failures (FINDING-112); a renewed failure blocks +release, and `make check` remains mandatory on the final SHA. +Operator readiness, manual GUI checks on release operating systems, final checksums, and signatures still require qualification. The release will be tagged and published only after those checks and From 4eb34450d0576c225fdc4bfccf117305f108c821 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:54:50 +0200 Subject: [PATCH 142/195] [FINDING-177] Freeze wallet-disabled Qt crash --- ...0025-v4.8-security-remediation-register.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index df5be07fe6..edfb05e870 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9676,3 +9676,38 @@ Cross-platform CI and public deployment remain separate release gates. `evidence/local-make-check-f176-candidate.log`. - **Final status:** FIXED locally, OPEN until the final exact-SHA CI pair and artifact checks pass. + +### FINDING-177: Qt GUI crashes when wallet support is disabled at runtime + +- **Severity:** MEDIUM release availability. The wallet-disabled GUI + exits with a segmentation fault before it can serve as a node UI. +- **Frozen initial status:** OPEN on 2026-10-15 at `35fa04a40`, before + changing the GUI or its release workflow. +- **Affected RIP-25 invariant:** The pinned Qt release package must + start in each supported runtime mode without dereferencing an + absent wallet control. +- **Affected Core 4.8.0 behavior:** Official Core 4.8.0 source + `b60f50e04` has the same unconditional currency-control call + and the same wallet-disabled construction path. +- **Observed failure:** Unmodified Linux release packages from + `9151946ad` and `0c33cc92c` both exited 139 when started + under Xvfb with a fresh testnet data directory and `-disablewallet`. + The `0c33cc92c` package opened its main regtest window with an + existing wallet, answered RPC, and shut down cleanly without that + option. Private red logs are + `evidence/gui-smoke-35fa/no-wallet-old-startup.log` and + `evidence/gui-smoke-35fa/no-wallet-startup.log`. +- **Root cause:** `RavenGUI` omits `walletFrame` and + `comboRvnUnit` when the wallet is disabled, but + `setClientModel` still calls `onCurrencyChange`. + That slot dereferences the null currency combo box after a valid + initial index. The affected sites are + `src/qt/ravengui.cpp:132-171,714-720,962-975,1864-1877`. +- **Expected behavior:** Wallet-disabled GUI startup reaches an RPC + ready state, retains its non-wallet controls, and stops cleanly. +- **Regression required:** Keep the red package launch, run a + focused Xvfb wallet-disabled startup and RPC smoke against the + pinned Linux GUI after the fix, add that smoke to the Linux + release build, and rerun `make check` and exact-SHA CI. +- **Final status:** OPEN pending a GUI guard, red-then-green runtime + regression, and final exact-SHA CI. From 4f6e6305c97e32bdcda340730d599925497ca598 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:59:39 +0200 Subject: [PATCH 143/195] [FINDING-177] Gate Linux Qt wallet-disabled startup --- .github/workflows/build-raven.yml | 8 ++ contrib/devtools/test-qt-no-wallet-startup.sh | 130 ++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100755 contrib/devtools/test-qt-no-wallet-startup.sh diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index 39036ef723..f4ec2977c2 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -250,6 +250,14 @@ jobs: - name: Build Raven run: make -j2 + - name: Install Qt runtime test prerequisites + if: matrix.OS == 'linux' + run: sudo apt-get install -y xvfb xauth + + - name: Test Qt startup without wallet + if: matrix.OS == 'linux' + run: ./contrib/devtools/test-qt-no-wallet-startup.sh + # Skip the binary checks for now. # name: Check Binary Security # run: ${SCRIPTS}/05-binary-checks.sh ${{ MATRIX.OS }} ${{ GITHUB.WORKSPACE }} diff --git a/contrib/devtools/test-qt-no-wallet-startup.sh b/contrib/devtools/test-qt-no-wallet-startup.sh new file mode 100755 index 0000000000..8880f4182a --- /dev/null +++ b/contrib/devtools/test-qt-no-wallet-startup.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +if (( $# > 2 )); then + echo "Usage: $0 [raven-qt path] [raven-cli path]" >&2 + exit 2 +fi + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +qt_bin="${1:-${RAVEN_QT_BIN:-$repo_root/src/qt/raven-qt}}" +cli_bin="${2:-${RAVEN_CLI_BIN:-$repo_root/src/raven-cli}}" + +for tool in xvfb-run python3; do + command -v "$tool" >/dev/null 2>&1 || { + echo "Missing required tool: $tool" >&2 + exit 2 + } +done +for binary in "$qt_bin" "$cli_bin"; do + if [[ ! -x "$binary" ]]; then + echo "Missing executable: $binary" >&2 + exit 2 + fi +done + +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/raven-qt-no-wallet.XXXXXXXX")" +data_dir="$work_dir/data" +mkdir -p "$data_dir" +qt_log="$work_dir/raven-qt.log" +rpc_log="$work_dir/raven-cli.log" +qt_pid="" +waited=0 + +rpc_port="${RAVEN_RPC_PORT:-$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')}" +cli=("$cli_bin" -datadir="$data_dir" -regtest -rpcport="$rpc_port" -rpcclienttimeout=3) + +show_logs() { + for log in "$qt_log" "$rpc_log" "$data_dir/regtest/debug.log"; do + if [[ -f "$log" ]]; then + echo "===== $log =====" >&2 + tail -n 100 "$log" >&2 + fi + done +} + +cleanup() { + status=$? + trap - EXIT + if [[ -n "$qt_pid" && "$waited" -eq 0 ]]; then + if kill -0 "$qt_pid" 2>/dev/null; then + "${cli[@]}" stop >/dev/null 2>&1 || true + for (( i = 0; i < 10; i++ )); do + kill -0 "$qt_pid" 2>/dev/null || break + sleep 1 + done + if kill -0 "$qt_pid" 2>/dev/null; then + pkill -TERM -P "$qt_pid" 2>/dev/null || true + kill "$qt_pid" 2>/dev/null || true + fi + fi + wait "$qt_pid" 2>/dev/null || true + fi + if (( status != 0 )); then + show_logs + echo "Qt no-wallet startup failed. Logs: $work_dir" >&2 + fi + exit "$status" +} +trap cleanup EXIT + +echo "Starting $qt_bin with -regtest -disablewallet under Xvfb" +QT_QPA_PLATFORM=xcb xvfb-run -a -s '-screen 0 1024x768x24' \ + "$qt_bin" -datadir="$data_dir" -regtest -disablewallet -server=1 \ + -listen=0 -connect=0 -dnsseed=0 -rpcport="$rpc_port" -splash=0 >"$qt_log" 2>&1 & +qt_pid=$! + +deadline=$((SECONDS + 120)) +rpc_ready=0 +while (( SECONDS < deadline )); do + if ! kill -0 "$qt_pid" 2>/dev/null; then + if wait "$qt_pid"; then + exit_code=0 + else + exit_code=$? + fi + waited=1 + echo "Qt exited before RPC was ready (status $exit_code)" >&2 + exit 1 + fi + if "${cli[@]}" getblockchaininfo >/dev/null 2>"$rpc_log"; then + rpc_ready=1 + break + fi + sleep 1 +done + +if (( ! rpc_ready )); then + echo "Qt RPC did not become ready within 120 seconds" >&2 + exit 1 +fi +sleep 2 +if ! kill -0 "$qt_pid" 2>/dev/null; then + echo "Qt exited after RPC readiness" >&2 + exit 1 +fi + +"${cli[@]}" stop >"$rpc_log" 2>&1 || { + echo "Qt RPC stop failed" >&2 + exit 1 +} + +deadline=$((SECONDS + 30)) +while kill -0 "$qt_pid" 2>/dev/null; do + if (( SECONDS >= deadline )); then + echo "Qt did not stop within 30 seconds" >&2 + exit 1 + fi + sleep 1 +done + +if wait "$qt_pid"; then + waited=1 + echo "Qt no-wallet startup and RPC shutdown passed. Logs: $work_dir" +else + exit_code=$? + waited=1 + echo "Qt exited with status $exit_code after RPC stop" >&2 + exit 1 +fi From 9861294c4fb80581b846cb5545fcd8be689eb65c Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:59:44 +0200 Subject: [PATCH 144/195] [FINDING-177] Guard absent Qt currency control --- src/qt/ravengui.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/qt/ravengui.cpp b/src/qt/ravengui.cpp index c4bf411b19..db3f1f8d59 100644 --- a/src/qt/ravengui.cpp +++ b/src/qt/ravengui.cpp @@ -1869,6 +1869,10 @@ void RavenGUI::onCurrencyChange(int newIndex) return; } + if (!comboRvnUnit) { + return; + } + this->unitChanged = true; this->currentPriceDisplay = &CurrencyUnits::CurrencyOptions[newIndex]; //Update the main GUI box in case this was changed from the settings screen @@ -1895,4 +1899,4 @@ void RavenGUI::getLatestVersion() { versionRequest->setUrl(QUrl("https://api.github.com/repos/RavenProject/Ravencoin/releases")); networkVersionManager->get(*versionRequest); -} \ No newline at end of file +} From 9061e119b455e13e9691ef9429a40b7e0331f93f Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:01:12 +0200 Subject: [PATCH 145/195] [FINDING-177] Record GUI qualification evidence --- ...0025-v4.8-security-remediation-register.md | 31 +++++++++++++++++-- doc/release-notes/release-notes-4.8.1.md | 13 ++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index edfb05e870..7957c19008 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4803,7 +4803,8 @@ the same tip. Private records are FINDING-067 remains HIGH and OPEN. Its exact remaining gates are: a wholly green Build Raven and Final Gate pair on the final source SHA -with mandatory `make check` and five checked artifact packages; +with mandatory `make check`, the Linux wallet-disabled Qt smoke, +and five checked artifact packages; deployment preparation and published upgrade instructions for the incompatible bit-12 block-weight change; manual Qt exercise on Windows and macOS release systems; completion of release @@ -9703,11 +9704,35 @@ Cross-platform CI and public deployment remain separate release gates. That slot dereferences the null currency combo box after a valid initial index. The affected sites are `src/qt/ravengui.cpp:132-171,714-720,962-975,1864-1877`. + A local GDB run bypassing the separately documented GCC 13 + pinned-Qt fortify issue for that diagnostic process stopped at + `QComboBox::setCurrentIndex` from `RavenGUI::onCurrencyChange`. + Its private stack is + `evidence/gui-smoke-35fa/local-no-wallet-gdb.log`. - **Expected behavior:** Wallet-disabled GUI startup reaches an RPC ready state, retains its non-wallet controls, and stops cleanly. - **Regression required:** Keep the red package launch, run a focused Xvfb wallet-disabled startup and RPC smoke against the pinned Linux GUI after the fix, add that smoke to the Linux release build, and rerun `make check` and exact-SHA CI. -- **Final status:** OPEN pending a GUI guard, red-then-green runtime - regression, and final exact-SHA CI. +- **Remediation commits:** `4f6e6305c` adds the focused Xvfb + startup, liveness, RPC, and clean-shutdown regression to the Linux + Build Raven release job. `9861294c4` skips the absent wallet + currency control in the node-only GUI. +- **Verification:** The final regression script failed against the + unmodified `0c33cc92c` package with status 139 before RPC + readiness; private log: + `evidence/f177-red-final.log`. The local pinned-Qt GUI rebuilt + after the guard; with the same diagnostic-only Qt fortify shim + already required on this GCC 13 host, the script reached RPC + readiness, remained alive for two seconds, and shut down cleanly + through RPC. The shim is outside the repository and is not part of + any release package. Private log: + `evidence/f177-green-local-final.log`. Local + `make -j4 check` passed all 511 unit cases after the fix; + `evidence/local-make-check-f177.log` and + `evidence/local-make-check-f177-unit.log` retain the results. +- **Final status:** FIXED locally, OPEN until the final exact-SHA + Linux release job passes the unmodified GUI smoke without the + diagnostic shim, both workflows pass, and all five packages are + checked. diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md index 41516d8d1d..11d9ef67f3 100644 --- a/doc/release-notes/release-notes-4.8.1.md +++ b/doc/release-notes/release-notes-4.8.1.md @@ -102,6 +102,13 @@ remains unspent; consensus rejects a spend without its anchor. asset inputs. Raw transaction callers must supply matching funded PQ inputs and check the final transaction before broadcast. +## Node-only GUI + +The Qt GUI now starts with `-disablewallet` without accessing the +wallet-only currency selector. The crash was inherited from Core 4.8.0. +The Linux release build runs a wallet-disabled GUI startup and RPC +shutdown check. + ## Release qualification still required The available testnet active chain was replayed from genesis by a clean @@ -122,8 +129,10 @@ exact-SHA pair and artifact review. The owner accepted a documented cause-not-reconstructible disposition for two historical intermittent wallet recovery test failures (FINDING-112); a renewed failure blocks release, and `make check` remains mandatory on the final SHA. -Operator readiness, manual -GUI checks on release operating systems, final checksums, and signatures +The wallet-disabled Qt correction (FINDING-177) also requires a +passing unmodified Linux package smoke on the final SHA. +Operator readiness, manual GUI checks on release operating systems, +final checksums, and signatures still require qualification. The release will be tagged and published only after those checks and the owner's separate release approval. From cc512da94b3ff277a8bdac7639a33d4ea2118391 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:40:27 +0200 Subject: [PATCH 146/195] [FINDING-178] Freeze node-only toolbar option crash --- ...0025-v4.8-security-remediation-register.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 7957c19008..e8ae0459f0 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9736,3 +9736,29 @@ Cross-platform CI and public deployment remain separate release gates. Linux release job passes the unmodified GUI smoke without the diagnostic shim, both workflows pass, and all five packages are checked. + +### FINDING-178: Wallet-disabled Qt GUI can crash when toolbar icons option changes + +- **Severity:** MEDIUM release availability. The node-only GUI can + dereference absent toolbar controls after startup. +- **Frozen initial status:** OPEN on 2026-10-15 at `9061e119b`, before + changing the affected GUI slot or its regression. +- **Affected RIP-25 invariant:** The pinned Qt release package must + remain usable in wallet-disabled mode when available options change. +- **Affected Core 4.8.0 behavior:** The wallet-only toolbar creation + path and unconditional options signal connection are inherited from + Core 4.8.0. +- **Observed failure path:** `RavenGUI::createToolBars` creates + `labelToolbar` and `m_toolbar` only when `walletFrame` exists. + `-disablewallet` omits that frame, while the Options dialog retains + its Window tab and Icons only option. Changing the option emits + `updateIconsOnlyToolbar`; the connected GUI slot dereferences both + absent controls in `src/qt/ravengui.cpp`. +- **Expected behavior:** Changing the option in node-only mode must + not crash the GUI. The wallet-enabled toolbar must still update. +- **Regression required:** Exercise the option change against an + unmodified wallet-disabled GUI, preserve the red failure, then + repeat after a narrow guard. Keep an exact-SHA Linux release smoke + and mandatory `make check`. +- **Final status:** OPEN pending a red-then-green regression, remedy, + and exact-SHA release CI evidence. From 068a87ba2067ab32ef2877ee39e3fce394926339 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:50:16 +0200 Subject: [PATCH 147/195] [FINDING-178] Gate node-only Qt Options interaction --- .github/workflows/build-raven.yml | 9 +- contrib/devtools/test-qt-no-wallet-options.py | 206 ++++++++++++++++++ 2 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 contrib/devtools/test-qt-no-wallet-options.py diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index f4ec2977c2..1c0257158b 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -252,12 +252,19 @@ jobs: - name: Install Qt runtime test prerequisites if: matrix.OS == 'linux' - run: sudo apt-get install -y xvfb xauth + run: sudo apt-get install -y xvfb xauth x11-utils libxtst6 - name: Test Qt startup without wallet if: matrix.OS == 'linux' run: ./contrib/devtools/test-qt-no-wallet-startup.sh + - name: Test Qt Options without wallet + if: matrix.OS == 'linux' + run: | + xvfb-run -a -s '-screen 0 1024x768x24' \ + python3 contrib/devtools/test-qt-no-wallet-options.py \ + src/qt/raven-qt src/raven-cli + # Skip the binary checks for now. # name: Check Binary Security # run: ${SCRIPTS}/05-binary-checks.sh ${{ MATRIX.OS }} ${{ GITHUB.WORKSPACE }} diff --git a/contrib/devtools/test-qt-no-wallet-options.py b/contrib/devtools/test-qt-no-wallet-options.py new file mode 100644 index 0000000000..1c7058b79d --- /dev/null +++ b/contrib/devtools/test-qt-no-wallet-options.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Exercise the wallet-disabled Qt Options dialog under Xvfb.""" + +import ctypes +import os +from pathlib import Path +import re +import shutil +import socket +import subprocess +import sys +import tempfile +import time + + +def check_executable(path): + if not Path(path).is_file() or not os.access(path, os.X_OK): + raise RuntimeError("Missing executable: " + path) + + +def find_windows(): + output = subprocess.check_output(["xwininfo", "-root", "-tree"], text=True) + return [ + (int(match.group(1), 16), match.group(2)) + for line in output.splitlines() + if (match := re.search(r'^\s*(0x[0-9a-f]+) "([^"]+)"', line)) + ] + + +def wait_window(title, seconds): + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + matches = [ident for ident, name in find_windows() if title in name] + if matches: + return matches[-1] + time.sleep(0.25) + raise RuntimeError("Window did not appear: " + title) + + +def window_origin(ident): + output = subprocess.check_output(["xwininfo", "-id", hex(ident)], text=True) + x = re.search(r"Absolute upper-left X:\s*(-?\d+)", output) + y = re.search(r"Absolute upper-left Y:\s*(-?\d+)", output) + if not x or not y: + raise RuntimeError("Cannot locate Qt window") + return int(x.group(1)), int(y.group(1)) + + +class XInput: + def __init__(self): + x11 = ctypes.CDLL("libX11.so.6") + xtst = ctypes.CDLL("libXtst.so.6") + display_type = ctypes.c_void_p + window_type = ctypes.c_ulong + x11.XOpenDisplay.argtypes = [ctypes.c_char_p] + x11.XOpenDisplay.restype = display_type + x11.XSetInputFocus.argtypes = [ + display_type, window_type, ctypes.c_int, window_type + ] + x11.XStringToKeysym.argtypes = [ctypes.c_char_p] + x11.XStringToKeysym.restype = window_type + x11.XKeysymToKeycode.argtypes = [display_type, window_type] + x11.XKeysymToKeycode.restype = ctypes.c_uint + x11.XSync.argtypes = [display_type, ctypes.c_int] + xtst.XTestFakeKeyEvent.argtypes = [ + display_type, ctypes.c_uint, ctypes.c_int, window_type + ] + xtst.XTestFakeMotionEvent.argtypes = [ + display_type, ctypes.c_int, ctypes.c_int, ctypes.c_int, window_type + ] + xtst.XTestFakeButtonEvent.argtypes = [ + display_type, ctypes.c_uint, ctypes.c_int, window_type + ] + self.display = x11.XOpenDisplay(None) + if not self.display: + raise RuntimeError("X11 display is unavailable") + self.x11 = x11 + self.xtst = xtst + + def focus(self, ident): + self.x11.XSetInputFocus(self.display, ident, 2, 0) + self.x11.XSync(self.display, 0) + + def key(self, name): + alt = name.startswith("Alt+") + name = name[4:] if alt else name + alt_code = self.x11.XKeysymToKeycode( + self.display, self.x11.XStringToKeysym(b"Alt_L") + ) + code = self.x11.XKeysymToKeycode( + self.display, self.x11.XStringToKeysym(name.encode()) + ) + if not code: + raise RuntimeError("Unknown X11 key: " + name) + if alt: + self.xtst.XTestFakeKeyEvent(self.display, alt_code, 1, 0) + self.xtst.XTestFakeKeyEvent(self.display, code, 1, 0) + self.xtst.XTestFakeKeyEvent(self.display, code, 0, 0) + if alt: + self.xtst.XTestFakeKeyEvent(self.display, alt_code, 0, 0) + self.x11.XSync(self.display, 0) + time.sleep(0.3) + + def click(self, x, y): + self.xtst.XTestFakeMotionEvent(self.display, -1, x, y, 0) + self.xtst.XTestFakeButtonEvent(self.display, 1, 1, 0) + self.xtst.XTestFakeButtonEvent(self.display, 1, 0, 0) + self.x11.XSync(self.display, 0) + time.sleep(0.3) + + +def main(qt_binary, cli_binary): + check_executable(qt_binary) + check_executable(cli_binary) + if not shutil.which("xwininfo"): + raise RuntimeError("xwininfo is required") + if not os.environ.get("DISPLAY"): + raise RuntimeError("Run this test under Xvfb") + + work = Path(tempfile.mkdtemp(prefix="raven-qt-no-wallet-options.")) + data = work / "data" + config = work / "config" + data.mkdir() + config.mkdir() + with socket.socket() as port_socket: + port_socket.bind(("127.0.0.1", 0)) + port = port_socket.getsockname()[1] + cli = [ + cli_binary, f"-datadir={data}", "-regtest", + f"-rpcport={port}", "-rpcclienttimeout=3" + ] + env = os.environ.copy() + env["QT_QPA_PLATFORM"] = "xcb" + env["XDG_CONFIG_HOME"] = str(config) + qt_log = work / "raven-qt.log" + print(f"Qt options smoke evidence: {work}", flush=True) + + with qt_log.open("w") as log: + qt = subprocess.Popen( + [ + qt_binary, f"-datadir={data}", "-regtest", "-disablewallet", + "-server=1", "-listen=0", "-connect=0", "-dnsseed=0", + f"-rpcport={port}", "-splash=0", "-lang=en", + ], + env=env, stdout=log, stderr=subprocess.STDOUT, + ) + try: + deadline = time.monotonic() + 120 + while time.monotonic() < deadline: + if qt.poll() is not None: + raise RuntimeError(f"Qt exited before RPC readiness: {qt.returncode}") + response = subprocess.run( + cli + ["getblockchaininfo"], capture_output=True, timeout=5 + ) + if response.returncode == 0: + break + time.sleep(1) + else: + raise RuntimeError("Qt RPC did not become ready") + + main_window = wait_window("Raven Core - Node [regtest]", 30) + xinput = XInput() + xinput.focus(main_window) + left, top = window_origin(main_window) + xinput.click(left + 63, top + 12) + xinput.key("Down") + xinput.key("Return") + + options_window = wait_window("Options", 10) + xinput.focus(options_window) + for key in ("Alt+w", "Alt+i", "Tab", "space", "Alt+o"): + xinput.key(key) + time.sleep(2) + if qt.poll() is not None: + raise RuntimeError(f"Qt crashed after Options submit: {qt.returncode}") + + settings = list(config.rglob("*.conf")) + if not any( + "fToolbarIconsOnly=true" in file.read_text() for file in settings + ): + raise RuntimeError("Icons only option was not saved") + stopped = subprocess.run(cli + ["stop"], capture_output=True, timeout=10) + if stopped.returncode != 0: + raise RuntimeError("Qt RPC stop failed") + if qt.wait(timeout=30) != 0: + raise RuntimeError(f"Qt shutdown failed: {qt.returncode}") + print("Qt no-wallet Options interaction passed", flush=True) + finally: + if qt.poll() is None: + subprocess.run(cli + ["stop"], capture_output=True, timeout=5) + try: + qt.wait(timeout=10) + except subprocess.TimeoutExpired: + qt.kill() + qt.wait() + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: test-qt-no-wallet-options.py RAVEN_QT RAVEN_CLI", file=sys.stderr) + sys.exit(2) + try: + main(sys.argv[1], sys.argv[2]) + except Exception as error: + print(f"Qt no-wallet Options smoke failed: {error}", file=sys.stderr) + sys.exit(1) From 0441b38d34ab8622c28bd59e216c3386a0fc0ca9 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:52:08 +0200 Subject: [PATCH 148/195] [FINDING-178] Guard absent node-only toolbar controls --- src/qt/ravengui.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/qt/ravengui.cpp b/src/qt/ravengui.cpp index db3f1f8d59..03be4d9cb8 100644 --- a/src/qt/ravengui.cpp +++ b/src/qt/ravengui.cpp @@ -914,6 +914,9 @@ void RavenGUI::createToolBars() void RavenGUI::updateIconsOnlyToolbar(bool IconsOnly) { + if (!labelToolbar || !m_toolbar) + return; + if(IconsOnly) { labelToolbar->setPixmap(QPixmap::fromImage(QImage(":/icons/rvntext"))); m_toolbar->setMaximumWidth(65); From 54bbf8a520842042472062988a9d5c3a02453446 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:52:18 +0200 Subject: [PATCH 149/195] [FINDING-175][FINDING-176] Record green artifact pair --- ...0025-v4.8-security-remediation-register.md | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index e8ae0459f0..1dbbffdcdb 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4814,6 +4814,19 @@ testnet historical replay and scans are complete at their recorded heights, while future public bit-12 and bit-13 activation cannot yet be observed on the public chain. +At the later `35fa04a40d4f61cae45d80f51920d36c639e838f` +checkpoint, Build Raven `38753089034` and Final Gate +`38753089260` passed every required job on that same SHA. +The five downloaded release packages passed their provenance and +individual SHA-256 checks, and shared source archive digest +`ff654a71e9e6d68d4480c9fe2e8f5bf00f9209468c32010ba87b79db3b6d4ec0`. +Private evidence is +`evidence/ci-35fa04a40/artifact-verification.json`. This closes +the xproto and libXau package-source defects FINDING-175 and +FINDING-176 on that tested SHA. The later Qt corrections and release +tests have not yet passed an exact-SHA CI pair, so FINDING-067 +stays OPEN. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical @@ -9633,8 +9646,12 @@ Cross-platform CI and public deployment remain separate release gates. `evidence/f175-xproto-reject.log`. Local `make -j4 check` passed after the fix, including `test/test_raven`; log: `evidence/local-make-check-f175-candidate.log`. -- **Final status:** FIXED locally, OPEN until both exact-SHA workflows - pass and all five release artifacts are checked. +- **Final status:** FIXED. Build Raven `38753089034` and Final Gate + `38753089260` passed on `35fa04a40`; all five downloaded + packages passed provenance, individual checksums, and common-source + verification in + `evidence/ci-35fa04a40/artifact-verification.json`. + Final release-SHA qualification remains FINDING-067. ### FINDING-176: libXau source has the same empty fallback failure @@ -9675,8 +9692,12 @@ Cross-platform CI and public deployment remain separate release gates. `evidence/f176-libxau-reject.log`. Local `make -j4 check` passed after the fix, including `test/test_raven`; log: `evidence/local-make-check-f176-candidate.log`. -- **Final status:** FIXED locally, OPEN until the final exact-SHA CI - pair and artifact checks pass. +- **Final status:** FIXED. Build Raven `38753089034` and Final Gate + `38753089260` passed on `35fa04a40`; all five downloaded + packages passed provenance, individual checksums, and common-source + verification in + `evidence/ci-35fa04a40/artifact-verification.json`. + Final release-SHA qualification remains FINDING-067. ### FINDING-177: Qt GUI crashes when wallet support is disabled at runtime From 22b65545ca811cd936f8508ef9e5619ea95e0026 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:53:05 +0200 Subject: [PATCH 150/195] [FINDING-067][FINDING-178] Record Qt Options release gate --- ...0025-v4.8-security-remediation-register.md | 41 +++++++++++++++---- doc/release-notes/release-notes-4.8.1.md | 19 +++++---- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 1dbbffdcdb..3704f1e05b 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4803,7 +4803,8 @@ the same tip. Private records are FINDING-067 remains HIGH and OPEN. Its exact remaining gates are: a wholly green Build Raven and Final Gate pair on the final source SHA -with mandatory `make check`, the Linux wallet-disabled Qt smoke, +with mandatory `make check`, the Linux wallet-disabled Qt startup +and Options smokes, and five checked artifact packages; deployment preparation and published upgrade instructions for the incompatible bit-12 block-weight change; manual Qt @@ -9775,11 +9776,37 @@ Cross-platform CI and public deployment remain separate release gates. its Window tab and Icons only option. Changing the option emits `updateIconsOnlyToolbar`; the connected GUI slot dereferences both absent controls in `src/qt/ravengui.cpp`. +- **Observed failure:** The pinned-Qt source GUI at `9061e119b` + reached RPC readiness with `-disablewallet` under Xvfb. The + focused regression opened Options, selected Window and Icons only, + then submitted the dialog. Qt exited with SIGSEGV, status 139. + GDB stopped in `QLabel::setPixmap` from + `RavenGUI::updateIconsOnlyToolbar` at + `src/qt/ravengui.cpp:918`. The Options mapper also emits the + toolbar signal on a normal OK submission even if the setting did + not change. - **Expected behavior:** Changing the option in node-only mode must not crash the GUI. The wallet-enabled toolbar must still update. -- **Regression required:** Exercise the option change against an - unmodified wallet-disabled GUI, preserve the red failure, then - repeat after a narrow guard. Keep an exact-SHA Linux release smoke - and mandatory `make check`. -- **Final status:** OPEN pending a red-then-green regression, remedy, - and exact-SHA release CI evidence. +- **Regression:** Commit + `068a87ba2067ab32ef2877ee39e3fce394926339` adds a + real Xvfb Options interaction to the Linux Build Raven job. + It verifies RPC readiness, opens the dialog, selects Icons only, + submits, checks the stored setting and process liveness, then + shuts down through RPC. It failed before the GUI fix; private + evidence is `evidence/f178-options-red.log` and the GDB + trace in `evidence/f178-options-gdb.log`. +- **Remediation commit:** + `0441b38d34ab8622c28bd59e216c3386a0fc0ca9` skips the + toolbar update when its wallet-only controls are absent. +- **Verification:** The same Xvfb regression passed after the guard, + including the persisted Icons only setting and clean RPC + shutdown; private log: + `evidence/f178-options-green-local.log`. The local pinned + Qt build required the previously documented GCC 13 diagnostic + shim, which is outside the repository and package. Local + `make -j4 check` passed all 511 unit cases after the fix; + `evidence/f178-local-make-check-unit.log` preserves the result. +- **Final status:** FIXED locally, OPEN until the exact-SHA Linux + release job passes the unmodified Options smoke without the + diagnostic shim, both workflows pass, and all five packages + are checked. diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md index 11d9ef67f3..6d22c0bddb 100644 --- a/doc/release-notes/release-notes-4.8.1.md +++ b/doc/release-notes/release-notes-4.8.1.md @@ -105,9 +105,11 @@ inputs and check the final transaction before broadcast. ## Node-only GUI The Qt GUI now starts with `-disablewallet` without accessing the -wallet-only currency selector. The crash was inherited from Core 4.8.0. -The Linux release build runs a wallet-disabled GUI startup and RPC -shutdown check. +wallet-only currency selector. Its Options dialog can also submit +the Icons only setting without accessing absent wallet toolbar controls. +Both crashes were inherited from Core 4.8.0. The Linux release build +runs wallet-disabled GUI startup, Options interaction, and RPC +shutdown checks. ## Release qualification still required @@ -123,14 +125,15 @@ The scoped activation and side-branch defects FINDING-104 through FINDING-107 are fixed after H-2 through H+2 regressions, connected alternate branches, rollback and restart checks, and a green exact-SHA Build Raven and Final Gate pair on `0c33cc92c`. All five packages from -that run passed downloaded checksum and provenance review. Subsequent -source-availability fixes for pinned xproto and libXau require a new -exact-SHA pair and artifact review. The owner accepted a documented +that run passed downloaded checksum and provenance review. The +pinned xproto and libXau source-availability fixes passed both +workflows on `35fa04a40`; all five downloaded packages passed +provenance and checksum review. The owner accepted a documented cause-not-reconstructible disposition for two historical intermittent wallet recovery test failures (FINDING-112); a renewed failure blocks release, and `make check` remains mandatory on the final SHA. -The wallet-disabled Qt correction (FINDING-177) also requires a -passing unmodified Linux package smoke on the final SHA. +The wallet-disabled Qt corrections (FINDING-177 and FINDING-178) +require passing unmodified Linux package smokes on the final SHA. Operator readiness, manual GUI checks on release operating systems, final checksums, and signatures still require qualification. From 32eb1014124f2bd103fa7532959168d1c88e81db Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:59:14 +0200 Subject: [PATCH 151/195] [FINDING-179] Freeze bit12 rollout instructions gap --- ...0025-v4.8-security-remediation-register.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 3704f1e05b..0153f3a76d 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9810,3 +9810,32 @@ Cross-platform CI and public deployment remain separate release gates. release job passes the unmodified Options smoke without the diagnostic shim, both workflows pass, and all five packages are checked. + +### FINDING-179: Bit-12 rollout notes omit automatic miner signaling and a go/no-go gate + +- **Severity:** HIGH release safety. A pool can count toward bit-12 lock-in + before its validating ecosystem is ready for the incompatible block rules. +- **Frozen initial status:** OPEN on 2026-10-15 at `22b65545c`, before + changing the release notes or adding a rollout documentation regression. +- **Affected RIP-25 invariant:** Operators must coordinate an upgrade of + miners, pools, validators, and services before signaling can lock in the + 12 MWU and PQ witness rules. +- **Observed gap:** The 4.8.1 release notes require readiness before + signaling but do not say that upgraded mining templates set bit 12 + automatically in STARTED. They do not define a recorded go/no-go decision. + The notes also omit the need to deploy revised parameters to all miners + and validators if a replacement release moves the start window. +- **Code behavior:** `CreateNewBlock` uses `ComputeBlockVersion`; + `pq_hybrid` is a forced getblocktemplate version bit; the + `-blockversion` override applies only on regtest. Mainnet operators + cannot rely on that override to disable the bit in 4.8.1 templates. +- **Expected behavior:** Release instructions must state the automatic + template signaling, identify the evidence and owner decision required + before the STARTED window, and describe a qualified replacement release + with coordinated deployment if readiness is absent. +- **Regression required:** Freeze a focused release-document contract + check that fails against the current instructions and passes after the + rollout instructions are corrected. Review the described behavior + against the actual mining and versionbits code. +- **Final status:** OPEN. No release go/no-go approval is implied by + documenting the protocol schedule. From 7adab1984200ee4fa79614038e36889bab8b2712 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:59:23 +0200 Subject: [PATCH 152/195] [FINDING-180] Freeze pretag qualification conflict --- ...0025-v4.8-security-remediation-register.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 0153f3a76d..2407342913 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9839,3 +9839,25 @@ Cross-platform CI and public deployment remain separate release gates. against the actual mining and versionbits code. - **Final status:** OPEN. No release go/no-go approval is implied by documenting the protocol schedule. + +### FINDING-180: Generic tag-first process conflicts with 4.8.1 release qualification + +- **Severity:** MEDIUM release process. A tag made before final artifact + qualification could point to a candidate that later fails a mandatory gate. +- **Frozen initial status:** OPEN on 2026-10-15 at `32eb10141`, before + changing the release process or adding a process-order regression. +- **Affected RIP-25 invariant:** The release tag must identify exactly + the source SHA whose mandatory tests and five packages passed review, + after separate owner approval to create the tag. +- **Observed gap:** `doc/release-process.md` instructs maintainers to tag + before building binaries. The 4.8.1 remediation register instead + requires final-SHA `make check`, paired CI, artifact checksum and + provenance review, and owner authorization before tagging. +- **Expected behavior:** The release process should state a 4.8.1 + pre-tag qualification and approval sequence, then bind the tag to + that exact SHA. Any publication remains a separate owner decision. +- **Regression required:** A focused release-document contract check + fails before the process clarification and passes after it. +- **Final status:** OPEN pending corrected instructions and their + regression. FINDING-067 retains the actual qualification and + approval gates. From fe274e28b84ad168245e633c9f5b2cdcc305bee0 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:02:53 +0200 Subject: [PATCH 153/195] [FINDING-179][FINDING-180] Document bit-12 rollout and pre-tag qualification --- .../devtools/test-rip25-release-contract.py | 115 ++++++++++++++++++ doc/release-notes/release-notes-4.8.1.md | 37 ++++-- doc/release-process.md | 28 ++++- 3 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 contrib/devtools/test-rip25-release-contract.py diff --git a/contrib/devtools/test-rip25-release-contract.py b/contrib/devtools/test-rip25-release-contract.py new file mode 100644 index 0000000000..8472259669 --- /dev/null +++ b/contrib/devtools/test-rip25-release-contract.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Check the RIP-25 mining rollout and 4.8.1 release documentation contract.""" + +from pathlib import Path +import re +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +NOTES = ROOT / "doc/release-notes/release-notes-4.8.1.md" +PROCESS = ROOT / "doc/release-process.md" + + +def section(text, heading): + match = re.search(r"(?im)^#{2,3} " + heading + r"[^\n]*\n", text) + if match is None: + raise AssertionError("Missing section: " + heading) + end = re.search(r"(?m)^#{2,3} ", text[match.end():]) + return text[match.end():match.end() + end.start() if end else None] + + +def require_paragraph(test, text, description, *patterns): + paragraphs = re.split(r"\n\s*\n", text) + for paragraph in paragraphs: + if all(re.search(pattern, paragraph, re.IGNORECASE | re.DOTALL) + for pattern in patterns): + return + test.fail("Missing coherent guidance for " + description) + + +class Rip25ReleaseContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.notes = NOTES.read_text(encoding="utf-8") + cls.process = PROCESS.read_text(encoding="utf-8") + + def test_mining_template_source_contract(self): + miner = (ROOT / "src/miner.cpp").read_text(encoding="utf-8") + validation = (ROOT / "src/validation.cpp").read_text(encoding="utf-8") + versionbits = (ROOT / "src/versionbits.cpp").read_text(encoding="utf-8") + mining_rpc = (ROOT / "src/rpc/mining.cpp").read_text(encoding="utf-8") + chainparams = (ROOT / "src/chainparams.cpp").read_text(encoding="utf-8") + + self.assertRegex(miner, r"pblock->nVersion\s*=\s*ComputeBlockVersion\(") + self.assertRegex(validation, r"state\s*==\s*THRESHOLD_LOCKED_IN\s*\|\|\s*state\s*==\s*THRESHOLD_STARTED") + self.assertRegex(versionbits, r'"pq_hybrid"\s*,\s*/\*\.gbt_force =\*/\s*true') + self.assertRegex(mining_rpc, r"if\s*\(!vbinfo\.gbt_force\)\s*\{[^}]*pblock->nVersion\s*&=\s*~VersionBitsMask") + self.assertRegex(miner, r'if\s*\(chainparams\.MineBlocksOnDemand\(\)\)\s*pblock->nVersion\s*=\s*gArgs\.GetArg\("-blockversion"') + + mainnet = chainparams.split("class CMainParams", 1)[1].split("class CTestNetParams", 1)[0] + testnet = chainparams.split("class CTestNetParams", 1)[1].split("class CRegTestParams", 1)[0] + regtest = chainparams.split("class CRegTestParams", 1)[1] + self.assertRegex(mainnet, r"DEPLOYMENT_PQ_HYBRID\]\.bit\s*=\s*12;") + self.assertRegex(mainnet, r"fMineBlocksOnDemand\s*=\s*false;") + self.assertRegex(testnet, r"fMineBlocksOnDemand\s*=\s*false;") + self.assertRegex(regtest, r"fMineBlocksOnDemand\s*=\s*true;") + + def test_notes_explain_automatic_signaling(self): + rollout = section(self.notes, "Mainnet signaling") + require_paragraph(self, rollout, "automatic bit-12 mining templates", + r"getblocktemplate|block templates?", r"bit[ -]?12", + r"STARTED", r"automatic|by default", r"signal") + require_paragraph(self, rollout, "mainnet override limits", + r"-blockversion", r"regtest", r"mainnet", r"override|disable") + + def test_notes_require_recorded_prestart_decision(self): + rollout = section(self.notes, "Mainnet signaling") + require_paragraph(self, rollout, "recorded owner go/no-go before original STARTED", + r"go/no-go", r"record", r"owner", r"evidence", + r"before", r"original", r"STARTED") + + def test_notes_require_qualified_replacement_rollout(self): + rollout = section(self.notes, "Mainnet signaling") + require_paragraph(self, rollout, "qualified replacement before original window", + r"replacement release", r"qualif", r"start", r"timeout", + r"install|deploy", r"miner", r"validator", r"service", + r"before", r"original") + + def test_process_puts_qualification_and_owner_approval_before_tag(self): + qualification = section(self.process, "Core 4\\.8\\.1 pre-tag qualification") + steps = re.findall(r"(?ms)^\d+\. (.*?)(?=^\d+\. |\Z)", qualification) + self.assertGreaterEqual(len(steps), 4, "Use an ordered pre-tag sequence") + + def step_for(description, *patterns): + matches = [index for index, step in enumerate(steps) + if all(re.search(pattern, step, re.IGNORECASE) + for pattern in patterns)] + self.assertEqual(len(matches), 1, "Missing or repeated step: " + description) + return matches[0] + + check = step_for("green make check on final SHA", r"final.*SHA", r"make check", r"green|pass") + ci = step_for("paired Build Raven and Final Gate on final SHA", + r"Build Raven", r"Final Gate", r"same.*SHA|final.*SHA", r"green|pass") + packages = step_for("five reviewed packages", r"five", r"artifact|package", + r"checksum", r"provenance") + workflow = (ROOT / ".github/workflows/build-raven.yml").read_text(encoding="utf-8") + targets = re.findall(r"(?m)^\s+- OS: ([a-z0-9]+)\s*$", workflow) + self.assertEqual(len(targets), 5, "Review every Build Raven matrix target") + for target in targets: + self.assertRegex(steps[packages], r"\b" + re.escape(target) + r"\b") + approval = step_for("separate owner tag approval", r"owner", r"separate|explicit", + r"approv", r"tag") + tag = step_for("tag exact qualified SHA", r"create.*tag|tag.*create", + r"exact.*SHA|same.*SHA|final.*SHA") + self.assertLess(check, ci) + self.assertLess(ci, packages) + self.assertLess(packages, approval) + self.assertLess(approval, tag) + self.assertLess(self.process.index("Core 4.8.1 pre-tag qualification"), + self.process.index("For releases other than 4.8.1"), + "The 4.8.1 sequence must precede generic tag instructions") + + +if __name__ == "__main__": + unittest.main() diff --git a/doc/release-notes/release-notes-4.8.1.md b/doc/release-notes/release-notes-4.8.1.md index 6d22c0bddb..2586cca2b6 100644 --- a/doc/release-notes/release-notes-4.8.1.md +++ b/doc/release-notes/release-notes-4.8.1.md @@ -41,6 +41,16 @@ through this deployment. The 16 MWU phase is height-based, not tied to a calendar date. There is no automatic return to Core 4.8.0 validation rules after activation. +On mainnet, upgraded 4.8.1 `getblocktemplate` mining templates +automatically set bit 12 and signal during STARTED and LOCKED_IN. +`CreateNewBlock` uses `ComputeBlockVersion`, and `pq_hybrid` is a forced +getblocktemplate version bit. A pool can therefore signal as soon as its +upgraded node reaches STARTED, even if other validators and services are +still running Core 4.8.0. The `-blockversion` override is regtest-only; +it cannot disable bit 12 in mainnet 4.8.1 templates. Pools should check +the returned template version and coordinate their upgrade with the +validators and services that depend on their blocks. + Testnet differs from this mainnet schedule. Its native RIP-25 rule is force-enabled from genesis by `nPQHybridEnabled=true`. At the reviewed 2026-10-15 testnet tip, `getblocktemplate` therefore advertises the later @@ -51,14 +61,25 @@ regtest boundary tests cover that transition. Testnet miners and node operators should account for the already active PQ and block-weight rules when comparing with Core 4.8.0. -Before miners begin signaling, the final 4.8.1 SHA must pass mandatory -`make check`, Final Gate, all five Build Raven targets, and artifact -checksum and provenance review. The clean candidate has already replayed -the available public testnet history from genesis, and both old and new -binaries have been tested on both sides of the 8 MWU boundary. Operators -must have time to upgrade before ACTIVE. If the remaining release gates -are not met before STARTED, a reviewed release must move the start and -timeout before the original window begins. +Before the original bit-12 STARTED window, the release owner must record +an evidence-based go/no-go decision. The evidence includes a green +`make check` on the final 4.8.1 SHA, paired green Build Raven and Final +Gate runs on that SHA, checksum and provenance review of all five +packages, and confirmed readiness of miners, pools, validators, and +dependent services. The clean candidate has already replayed the +available public testnet history from genesis, and both old and new +binaries have been tested on both sides of the 8 MWU boundary. That +history does not replace the final qualification or operator readiness +decision. Operators must have time to upgrade before ACTIVE. + +If the decision is no-go or readiness is absent, a replacement release +must revise both the start and timeout dates and pass the full release +qualification again. The replacement release with revised parameters +must be installed across miners, pools, validators, and dependent +services before the original STARTED window begins. Merely preparing +revised parameters, or relying on `-blockversion` on mainnet, does not +prevent an already deployed 4.8.1 miner from signaling on the original +schedule. ## Migration to PQ custody diff --git a/doc/release-process.md b/doc/release-process.md index ed99c67a4a..6f2708d3f8 100644 --- a/doc/release-process.md +++ b/doc/release-process.md @@ -25,6 +25,32 @@ Before every major release: * Update [`BLOCK_CHAIN_SIZE`](/src/qt/intro.cpp) to the current size plus some overhead. * Update `src/chainparams.cpp` chainTxData with statistics about the transaction count and rate. +### Core 4.8.1 pre-tag qualification + +For the RIP-25 4.8.1 release, the tag instruction is step 5 below. +Complete this sequence before signing or publishing. Keep the evidence +tied to one full final source SHA. Any source change creates a new +candidate and restarts these gates. + +1. Freeze the final source SHA after all code and documentation edits. + Run `python3 contrib/devtools/test-rip25-release-contract.py` and the + mandatory `make check` on that final SHA; record green results. +2. Run Build Raven and RIP-25 v4.8 Final Gate on the same final SHA. + Both workflows must be green, and their reported checkout SHAs must + match the source SHA from step 1. +3. Download and review artifacts from all five Build Raven targets: + windows, osx, linux, arm32v7, and aarch64. Check each package against + its SHA256 checksum and review its provenance, including the recorded + source SHA. Record the five target results and artifact names before + proceeding. +4. Present the recorded qualification and operator readiness evidence to + the owner. Obtain a separate, explicit owner approval to create the + 4.8.1 tag. The rollout go/no-go decision and permission to publish are + separate decisions. +5. Create the 4.8.1 tag on the exact qualified final SHA. Verify the tag + resolves to that SHA before signing or publishing release material. + Publication requires its own owner decision. + ### First time / New builders @@ -40,7 +66,7 @@ Generate list of authors: git log --format='%aN' "$*" | sort -ui | sed -e 's/^/- /' -Tag version (or release candidate) in git +For releases other than 4.8.1, tag the version (or release candidate) in git: git tag -s v(new version, e.g. 0.8.0) From a689054bcb253f1c87d59f1a2e73251beb4578fd Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:03:46 +0200 Subject: [PATCH 154/195] [FINDING-179][FINDING-180] Record rollout guidance regression --- ...0025-v4.8-security-remediation-register.md | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 2407342913..044bde2fa3 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9837,8 +9837,21 @@ Cross-platform CI and public deployment remain separate release gates. check that fails against the current instructions and passes after the rollout instructions are corrected. Review the described behavior against the actual mining and versionbits code. -- **Final status:** OPEN. No release go/no-go approval is implied by - documenting the protocol schedule. +- **Remediation commit:** `fe274e28b84ad168245e633c9f5b2cdcc305bee0` + explains automatic mainnet bit-12 signaling, the recorded + pre-STARTED go/no-go decision, and the need to qualify and deploy + a replacement release across affected operators if the schedule + must move. +- **Verification:** The focused source-and-document contract in + `contrib/devtools/test-rip25-release-contract.py` failed four + documentation assertions against the frozen release notes, while + its mining-source check passed. It passed all five checks after + the documentation change. Private logs are + `evidence/f179-f180-release-contract-red.log` and + `evidence/f179-f180-release-contract-green.log`. +- **Final status:** FIXED for the release instructions. The actual + network go/no-go decision and operator adoption remain OPEN + under FINDING-067. ### FINDING-180: Generic tag-first process conflicts with 4.8.1 release qualification @@ -9858,6 +9871,16 @@ Cross-platform CI and public deployment remain separate release gates. that exact SHA. Any publication remains a separate owner decision. - **Regression required:** A focused release-document contract check fails before the process clarification and passes after it. -- **Final status:** OPEN pending corrected instructions and their - regression. FINDING-067 retains the actual qualification and - approval gates. +- **Remediation commit:** `fe274e28b84ad168245e633c9f5b2cdcc305bee0` + adds a 4.8.1-specific pre-tag qualification sequence to the + inherited general release process. +- **Verification:** The same focused contract failed on the old + tag-first instructions and passed after the new ordered steps + covered mandatory `make check`, paired exact-SHA CI, all five + artifact packages, provenance, and separate owner approval. + Private red and green logs are + `evidence/f179-f180-release-contract-red.log` and + `evidence/f179-f180-release-contract-green.log`. +- **Final status:** FIXED for the written process. FINDING-067 + retains the actual qualification, owner approval, and + publication gates. From 36d8b89641f8f9d2cad793d4027260b6230ca113 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:51:12 +0200 Subject: [PATCH 155/195] [FINDING-181] Freeze wallet rewrite CI failure --- ...0025-v4.8-security-remediation-register.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 044bde2fa3..e8f881b377 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9884,3 +9884,35 @@ Cross-platform CI and public deployment remain separate release gates. - **Final status:** FIXED for the written process. FINDING-067 retains the actual qualification, owner approval, and publication gates. + +### FINDING-181: Build Raven loses the wallet RPC during rewrite-failure recovery + +- **Severity:** HIGH release qualification until the process exit is + explained. The mandatory Build Raven security step did not pass. +- **Frozen initial status:** OPEN on 2026-10-16 at `22b65545c` + after Build Raven run `38805825854` failed, before changing + the wallet, test, or workflow. +- **Affected RIP-25 invariant:** A failed wallet rewrite must not + silently lose wallet access or bypass recovery checks; the + release security test must report and verify the actual result. +- **Observed failure:** In the required functional test step, + `wallet_encryption_rewrite.py` raised + `ConnectionRefusedError` instead of the expected RPC error + while inducing a rewrite failure. Its node was unavailable + during cleanup. The other 32 required functional tests passed. + Build Raven skipped all artifact jobs. The paired Final Gate + run `38805825878` passed its security job and all seven builds + on the same `22b65545c` SHA, so the pair is not a release + qualification. +- **Initial classification:** The CI log proves a process/RPC + disappearance in this test, but does not yet establish whether + the wallet exited on the intended failure path, crashed, + or the test harness observed a timing race. +- **Evidence:** Private retained CI output is + `evidence/ci-22b65545-build-raven-failed.log`. +- **Regression required:** Identify the mechanism, demonstrate + the failure against the frozen behavior, apply a narrow remedy, + rerun the same test and mandatory `make check`, then obtain + a fully green exact-SHA Build Raven and Final Gate pair with + five checked release packages. +- **Final status:** OPEN. FINDING-067 remains OPEN. From 9688f7e03e17dc9789039a1d2b0ea159a350a489 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:57:05 +0200 Subject: [PATCH 156/195] [FINDING-181] Accept shutdown transport loss in wallet rewrite test --- test/functional/wallet_encryption_rewrite.py | 32 ++++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/test/functional/wallet_encryption_rewrite.py b/test/functional/wallet_encryption_rewrite.py index 55963b9a9a..710a73d3e6 100755 --- a/test/functional/wallet_encryption_rewrite.py +++ b/test/functional/wallet_encryption_rewrite.py @@ -6,9 +6,11 @@ """Test fail-closed wallet encryption rewrite recovery.""" import glob +import http.client import os import tempfile +from test_framework.authproxy import JSONRPCException from test_framework.test_framework import RavenTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error @@ -30,13 +32,31 @@ def induce_rewrite_failure(self, node_index): ) os.mkdir(rewrite_path) - assert_raises_rpc_error( - -16, - "Wallet encryption failed after the live key state changed", - node.encryptwallet, - passphrase, - ) + try: + node.encryptwallet(passphrase) + except JSONRPCException as error: + assert_equal(error.error["code"], -16) + assert_equal( + error.error["message"], + "Error: Wallet encryption failed after the live key state changed. " + "The Raven server is stopping; restart before using the wallet.", + ) + except ( + http.client.RemoteDisconnected, + ConnectionRefusedError, + ConnectionResetError, + BrokenPipeError, + ): + # Shutdown can close the RPC connection before the error is delivered. + pass + else: + raise AssertionError("encryptwallet unexpectedly succeeded") node.wait_until_stopped() + debug_log = os.path.join(os.path.dirname(rewrite_path), "debug.log") + with open(debug_log, encoding="utf-8") as log_file: + assert ( + "CDB::Rewrite: Refusing stale non-regular path " + rewrite_path + ) in log_file.read() return address, private_key, passphrase, rewrite_path def assert_encrypted_key_survives(self, node_index, address, private_key, passphrase): From e67b23c7bedccb674e305f8a3dd43c599c93c6d6 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:58:45 +0200 Subject: [PATCH 157/195] [FINDING-067][FINDING-181] Record wallet rewrite regression --- ...0025-v4.8-security-remediation-register.md | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index e8f881b377..64ec36c807 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4806,6 +4806,8 @@ a wholly green Build Raven and Final Gate pair on the final source SHA with mandatory `make check`, the Linux wallet-disabled Qt startup and Options smokes, and five checked artifact packages; +the wallet rewrite functional regression in FINDING-181 on that +same SHA; deployment preparation and published upgrade instructions for the incompatible bit-12 block-weight change; manual Qt exercise on Windows and macOS release systems; completion of release @@ -9908,6 +9910,15 @@ Cross-platform CI and public deployment remain separate release gates. disappearance in this test, but does not yet establish whether the wallet exited on the intended failure path, crashed, or the test harness observed a timing race. +- **Root cause assessment:** On this induced failure path, + `CDB::Rewrite` refuses the injected directory and + `encryptwallet` calls `StartShutdown` before throwing + RPC error -16. HTTP replies are dispatched asynchronously; + the CI trace shows a disconnected reply followed by a + refused retry. A clean shutdown can therefore outrun the + expected RPC error. The failed runner did not publish its + node debug log or exit status, so the exact historical + process exit remains unproven. - **Evidence:** Private retained CI output is `evidence/ci-22b65545-build-raven-failed.log`. - **Regression required:** Identify the mechanism, demonstrate @@ -9915,4 +9926,30 @@ Cross-platform CI and public deployment remain separate release gates. rerun the same test and mandatory `make check`, then obtain a fully green exact-SHA Build Raven and Final Gate pair with five checked release packages. -- **Final status:** OPEN. FINDING-067 remains OPEN. +- **Remediation commit:** + `9688f7e03e17dc9789039a1d2b0ea159a350a489` accepts only + the exact RPC -16 error or a narrow set of shutdown transport + errors. It then requires a clean node exit and the path-specific + `CDB::Rewrite: Refusing stale non-regular path` log entry + before the existing restart and key-recovery assertions. +- **Verification:** The pre-fix required Build Raven gate is + the red regression. The revised focused test passed 11 + consecutive runs, the required static gate passed six + checks, and the complete required functional set passed + 33 of 33 tests under Python 3.10, matching CI. Private + green records are `evidence/f181-wallet-encryption-rewrite-focused-run1.log`, + `evidence/f181-wallet-encryption-rewrite-focused-repeat10.log`, + `evidence/f181-required-functional-gate-static.log`, and + `evidence/f181-required-functional-gate-full-python310.log`. + Local `make -j4 check` then passed all 511 unit cases; + `evidence/local-make-check-f181-final.log` and + `evidence/local-make-check-f181-final-unit.log` retain + the result. + A separate host Python 3.12 attempt could not import + inherited `asyncore` in 13 unrelated functional tests; + it is excluded from the green result and retained as + `evidence/f181-required-functional-gate-full.log`. +- **Final status:** FIXED locally, OPEN until mandatory + `make check`, the full required functional gate, and both + release workflows pass on the final exact SHA with five + checked Build Raven packages. FINDING-067 remains OPEN. From 74e834fe8bf63bbbf69173097ae3c49b74768797 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:59:58 +0200 Subject: [PATCH 158/195] [FINDING-182][FINDING-183] Freeze release build failures --- ...0025-v4.8-security-remediation-register.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 64ec36c807..7cefd47948 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -9953,3 +9953,58 @@ Cross-platform CI and public deployment remain separate release gates. `make check`, the full required functional gate, and both release workflows pass on the final exact SHA with five checked Build Raven packages. FINDING-067 remains OPEN. + +### FINDING-182: Linux Qt smoke lacks the pinned GUI XKB runtime + +- **Severity:** MEDIUM release availability. The Linux package job + cannot verify GUI startup while a required shared library is absent. +- **Frozen initial status:** OPEN on 2026-10-16 at `e67b23c7b`, + before changing the Build Raven runner dependencies. +- **Affected RIP-25 invariant:** The pinned-Qt Linux release GUI + must start in wallet-disabled mode without local diagnostic shims. +- **Observed failure:** Build Raven run `38805823946`, job + `116672035877`, passed its security gate and built the Linux + GUI, then failed `Test Qt startup without wallet`. The + executable exited 127 because the runner lacked + `libxkbcommon-x11.so.0`; the subsequent missing RPC cookie + was a consequence of that startup failure. The package was + not published from this job. +- **Evidence:** Private job log + `evidence/ci-22b65545-build-raven-linux-qt-failed.log`. +- **Expected behavior:** The Linux runner must install the + pinned Qt GUI's host runtime requirements before both + wallet-disabled Xvfb smokes, then fail if the GUI itself + does not reach RPC readiness or clean shutdown. +- **Regression required:** Preserve the red missing-library + log, add the precise runner dependency, rerun both GUI + smokes on an unmodified CI build without a shim, and + qualify all five packages on the exact final SHA. +- **Final status:** OPEN. The inherited GUI defects in + FINDING-177 and FINDING-178 remain unverified by the + release package job. + +### FINDING-183: dbus source timeout leaves ARM64 depends without a fallback + +- **Severity:** MEDIUM release availability. The ARM64 package + cannot build when its primary dbus source host times out. +- **Frozen initial status:** OPEN on 2026-10-16 at `e67b23c7b`, + before changing the pinned dbus fetch recipe. +- **Affected RIP-25 invariant:** All five release packages must + be reproducible from pinned source archives on the exact SHA. +- **Observed failure:** Build Raven run `38805823946`, job + `116672036056`, timed out repeatedly while fetching + `dbus-1.10.18.tar.gz`. The generic fallback URL was empty, + so curl failed with an invalid URL and Build Dependencies + stopped. The run's mandatory security job passed, but + no complete five-target release set existed. +- **Evidence:** Private job log + `evidence/ci-22b65545-build-raven-arm64-dep-failed.log`. +- **Expected behavior:** A second HTTPS source supplies the + identical pinned archive when the primary is unavailable; + the existing SHA-256 check must still reject altered bytes. +- **Regression required:** Force the primary dbus source to + fail against the old recipe, demonstrate the missing + fallback, then verify alternate download, pinned digest, + rejection of altered bytes, `make check`, and exact-SHA + paired CI with five checked packages. +- **Final status:** OPEN pending a verified source fallback. From 1de647a88e2449afe445da77fdc7667052be505a Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:00:54 +0200 Subject: [PATCH 159/195] [FINDING-182] Install Qt X11 runtime for Linux smoke tests --- .github/workflows/build-raven.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index 1c0257158b..fcfd813c81 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -252,7 +252,20 @@ jobs: - name: Install Qt runtime test prerequisites if: matrix.OS == 'linux' - run: sudo apt-get install -y xvfb xauth x11-utils libxtst6 + run: sudo apt-get install -y xvfb xauth x11-utils libxtst6 libxkbcommon-x11-0 + + - name: Verify Qt smoke runtime libraries + if: matrix.OS == 'linux' + shell: bash + run: | + for binary in src/qt/raven-qt src/raven-cli; do + test -x "$binary" + libraries="$(ldd "$binary")" + if grep -Fq 'not found' <<< "$libraries"; then + printf 'Missing runtime libraries for %s:\n%s\n' "$binary" "$libraries" + exit 1 + fi + done - name: Test Qt startup without wallet if: matrix.OS == 'linux' From 21962b1804b1ab5960b6042756cd5d2a2b3c155e Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:02:32 +0200 Subject: [PATCH 160/195] [FINDING-183] Add dbus archive fallback mirror --- depends/packages/dbus.mk | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/depends/packages/dbus.mk b/depends/packages/dbus.mk index bbe0375409..034b14e2d4 100644 --- a/depends/packages/dbus.mk +++ b/depends/packages/dbus.mk @@ -1,10 +1,17 @@ package=dbus $(package)_version=1.10.18 $(package)_download_path=https://dbus.freedesktop.org/releases/dbus +$(package)_fallback_download_path=https://download.bitcoinabc.org/depends-sources $(package)_file_name=$(package)-$($(package)_version).tar.gz $(package)_sha256_hash=6049ddd5f3f3e2618f615f1faeda0a115104423a7996b7aa73e2f36e38cc514a $(package)_dependencies=expat +define $(package)_fetch_cmds +( test -f $(dbus_source) || \ + $(call fetch_file_inner,dbus,$(dbus_download_path),$(dbus_download_file),$(dbus_file_name),$(dbus_sha256_hash)) || \ + $(call fetch_file_inner,dbus,$(dbus_fallback_download_path),$(dbus_download_file),$(dbus_file_name),$(dbus_sha256_hash)) ) +endef + define $(package)_set_vars $(package)_config_opts=--disable-tests --disable-doxygen-docs --disable-xml-docs --disable-static --without-x endef From 77d8f4c65791fb53351f1b318e7f593cfe7559b0 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:04:26 +0200 Subject: [PATCH 161/195] [FINDING-182][FINDING-183] Record build fixes and evidence --- ...0025-v4.8-security-remediation-register.md | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 7cefd47948..c6e20d04cf 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4808,6 +4808,8 @@ and Options smokes, and five checked artifact packages; the wallet rewrite functional regression in FINDING-181 on that same SHA; +the Linux Qt runner dependency in FINDING-182 and the ARM64 dbus +source fallback in FINDING-183 on that same SHA; deployment preparation and published upgrade instructions for the incompatible bit-12 block-weight change; manual Qt exercise on Windows and macOS release systems; completion of release @@ -9965,7 +9967,9 @@ Cross-platform CI and public deployment remain separate release gates. - **Observed failure:** Build Raven run `38805823946`, job `116672035877`, passed its security gate and built the Linux GUI, then failed `Test Qt startup without wallet`. The - executable exited 127 because the runner lacked + pull-request run checked out synthetic merge `614dbd5ed` + of branch head `22b65545c` into base `7dc448453`. + The executable exited 127 because the runner lacked `libxkbcommon-x11.so.0`; the subsequent missing RPC cookie was a consequence of that startup failure. The package was not published from this job. @@ -9979,8 +9983,17 @@ Cross-platform CI and public deployment remain separate release gates. log, add the precise runner dependency, rerun both GUI smokes on an unmodified CI build without a shim, and qualify all five packages on the exact final SHA. -- **Final status:** OPEN. The inherited GUI defects in - FINDING-177 and FINDING-178 remain unverified by the +- **Remediation commit:** `1de647a88e2449afe445da77fdc7667052be505a` + installs the missing `libxkbcommon-x11-0` runner package and adds + an explicit dynamic-library preflight before the Xvfb smokes. +- **Verification:** Workflow YAML, job ordering, mandatory + `make check`, and the five release targets passed local structural + checks. The failed PR package job is the retained red run. + An unmodified release GUI smoke and complete artifact set on + the final source SHA are still required for green CI evidence. +- **Final status:** FIXED locally, OPEN until exact-SHA CI. + The inherited GUI defects in FINDING-177 and + FINDING-178 remain unverified by the release package job. ### FINDING-183: dbus source timeout leaves ARM64 depends without a fallback @@ -9995,7 +10008,8 @@ Cross-platform CI and public deployment remain separate release gates. `116672036056`, timed out repeatedly while fetching `dbus-1.10.18.tar.gz`. The generic fallback URL was empty, so curl failed with an invalid URL and Build Dependencies - stopped. The run's mandatory security job passed, but + stopped. This job also checked out synthetic merge + `614dbd5ed`. The run's mandatory security job passed, but no complete five-target release set existed. - **Evidence:** Private job log `evidence/ci-22b65545-build-raven-arm64-dep-failed.log`. @@ -10007,4 +10021,24 @@ Cross-platform CI and public deployment remain separate release gates. fallback, then verify alternate download, pinned digest, rejection of altered bytes, `make check`, and exact-SHA paired CI with five checked packages. -- **Final status:** OPEN pending a verified source fallback. +- **Remediation commit:** `21962b1804b1ab5960b6042756cd5d2a2b3c155e` + adds the package-specific HTTPS fallback at + `download.bitcoinabc.org/depends-sources` while preserving the + pinned dbus 1.10.18 SHA-256. +- **Verification:** An isolated ARM64 depends fetch forced the + primary source to fail. The frozen recipe failed with an empty + fallback URL; the remedy fetched a 1,986,589-byte archive from + the alternate host and accepted only SHA-256 + `6049ddd5f3f3e2618f615f1faeda0a115104423a7996b7aa73e2f36e38cc514a`. + An altered fallback archive failed the checksum and left no + accepted source file. Private logs are + `evidence/f183-red-frozen-forced-primary.log`, + `evidence/f183-green-forced-primary.log`, and + `evidence/f183-corrupt-fallback-rejected.log`. + With both FINDING-182 and FINDING-183 remedies present, local + `make -j4 check` passed all 511 unit cases at `21962b180`. + Private logs are `evidence/local-make-check-f182-f183.log` + and `evidence/local-make-check-f182-f183-unit.log`. + Mirror reachability was measured locally; final CI remains due. +- **Final status:** FIXED locally, OPEN until exact-SHA CI and + five checked release packages. From 1e58379df7c3ede470f8562711070d2b8152d10a Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:11:41 +0200 Subject: [PATCH 162/195] [FINDING-067] Record exact-source CI and artifact qualification --- ...0025-v4.8-security-remediation-register.md | 95 +++++++++++++------ 1 file changed, 66 insertions(+), 29 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index c6e20d04cf..a44f2bab30 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -4802,14 +4802,10 @@ the same tip. Private records are `evidence/testnet-verifychain-fec5-20261015.json`. FINDING-067 remains HIGH and OPEN. Its exact remaining gates are: -a wholly green Build Raven and Final Gate pair on the final source SHA -with mandatory `make check`, the Linux wallet-disabled Qt startup -and Options smokes, -and five checked artifact packages; -the wallet rewrite functional regression in FINDING-181 on that -same SHA; -the Linux Qt runner dependency in FINDING-182 and the ARM64 dbus -source fallback in FINDING-183 on that same SHA; +a wholly green Build Raven and Final Gate pair on the final release +SHA after this register update, with mandatory `make check`, the +Linux wallet-disabled Qt startup and Options smokes, and five +newly checked artifact packages; deployment preparation and published upgrade instructions for the incompatible bit-12 block-weight change; manual Qt exercise on Windows and macOS release systems; completion of release @@ -4832,6 +4828,30 @@ FINDING-176 on that tested SHA. The later Qt corrections and release tests have not yet passed an exact-SHA CI pair, so FINDING-067 stays OPEN. +At the later `77d8f4c65791fb53351f1b318e7f593cfe7559b0` +checkpoint, Build Raven `38853895481` and Final Gate +`38853895080` completed successfully on the same source SHA. +Build Raven passed its required security job, including the +wallet rewrite recovery test and all 33 required functional tests. +Its Linux release job passed the dynamic-library preflight and +both wallet-disabled pinned-Qt startup and Options smokes without +the local diagnostic shim. All five release targets built and +uploaded packages. Final Gate passed its security job and all +seven build configurations. The five downloaded Build Raven +packages passed SHA-256 manifests, provenance, embedded source +commit verification, and a common source archive digest of +`4913098eba47fc787745bedd503e1bbb3a84fc062553957bb5082b2296bea7d8`. +Private records are +`evidence/ci-77d8f4c65/build-raven-metadata.json`, +`evidence/ci-77d8f4c65/final-gate-metadata.json`, +`evidence/ci-77d8f4c65/build-linux-job.log`, and +`evidence/ci-77d8f4c65/artifact-verification.json`. +This closes the tested-source gates for FINDING-177, FINDING-178, +FINDING-181, FINDING-182, and FINDING-183. The source SHA will +change when this register update is committed, so FINDING-067 +still requires a new exact-SHA CI pair and package inspection +along with the operator and release gates listed above. + ## Finding frozen during the second adversarial review The network-context remediation was reviewed independently after its technical @@ -9758,10 +9778,13 @@ Cross-platform CI and public deployment remain separate release gates. `make -j4 check` passed all 511 unit cases after the fix; `evidence/local-make-check-f177.log` and `evidence/local-make-check-f177-unit.log` retain the results. -- **Final status:** FIXED locally, OPEN until the final exact-SHA - Linux release job passes the unmodified GUI smoke without the - diagnostic shim, both workflows pass, and all five packages are - checked. +- **Final status:** FIXED on tested source `77d8f4c65`. + Build Raven `38853895481`, Linux job `116826216457`, passed + the unmodified pinned-Qt startup smoke without the diagnostic + shim. Final Gate `38853895080` passed on the same SHA, and all + five Build Raven packages passed the checks recorded in + `evidence/ci-77d8f4c65/artifact-verification.json`. + Final release-SHA qualification remains FINDING-067. ### FINDING-178: Wallet-disabled Qt GUI can crash when toolbar icons option changes @@ -9810,10 +9833,13 @@ Cross-platform CI and public deployment remain separate release gates. shim, which is outside the repository and package. Local `make -j4 check` passed all 511 unit cases after the fix; `evidence/f178-local-make-check-unit.log` preserves the result. -- **Final status:** FIXED locally, OPEN until the exact-SHA Linux - release job passes the unmodified Options smoke without the - diagnostic shim, both workflows pass, and all five packages - are checked. +- **Final status:** FIXED on tested source `77d8f4c65`. + Build Raven `38853895481`, Linux job `116826216457`, passed + the unmodified pinned-Qt Options smoke without the diagnostic + shim. Final Gate `38853895080` passed on the same SHA, and all + five packages passed provenance and checksum checks in + `evidence/ci-77d8f4c65/artifact-verification.json`. + Final release-SHA qualification remains FINDING-067. ### FINDING-179: Bit-12 rollout notes omit automatic miner signaling and a go/no-go gate @@ -9951,10 +9977,13 @@ Cross-platform CI and public deployment remain separate release gates. inherited `asyncore` in 13 unrelated functional tests; it is excluded from the green result and retained as `evidence/f181-required-functional-gate-full.log`. -- **Final status:** FIXED locally, OPEN until mandatory - `make check`, the full required functional gate, and both - release workflows pass on the final exact SHA with five - checked Build Raven packages. FINDING-067 remains OPEN. +- **Final status:** FIXED on tested source `77d8f4c65`. + Build Raven `38853895481`, security job `116820958622`, + passed the wallet rewrite recovery test and all 33 required + functional tests. Final Gate `38853895080` passed on the + same SHA; the five Build Raven packages passed the checks in + `evidence/ci-77d8f4c65/artifact-verification.json`. + Final release-SHA qualification remains FINDING-067. ### FINDING-182: Linux Qt smoke lacks the pinned GUI XKB runtime @@ -9989,12 +10018,14 @@ Cross-platform CI and public deployment remain separate release gates. - **Verification:** Workflow YAML, job ordering, mandatory `make check`, and the five release targets passed local structural checks. The failed PR package job is the retained red run. - An unmodified release GUI smoke and complete artifact set on - the final source SHA are still required for green CI evidence. -- **Final status:** FIXED locally, OPEN until exact-SHA CI. - The inherited GUI defects in FINDING-177 and - FINDING-178 remain unverified by the - release package job. + Build Raven `38853895481`, Linux job `116826216457`, installed + `libxkbcommon-x11-0`, passed the dynamic-library preflight, + passed both unmodified GUI smokes, and uploaded the Linux + package. All five packages passed checks in + `evidence/ci-77d8f4c65/artifact-verification.json`. +- **Final status:** FIXED on tested source `77d8f4c65`. + Final Gate `38853895080` passed on the same SHA. Final + release-SHA qualification remains FINDING-067. ### FINDING-183: dbus source timeout leaves ARM64 depends without a fallback @@ -10039,6 +10070,12 @@ Cross-platform CI and public deployment remain separate release gates. `make -j4 check` passed all 511 unit cases at `21962b180`. Private logs are `evidence/local-make-check-f182-f183.log` and `evidence/local-make-check-f182-f183-unit.log`. - Mirror reachability was measured locally; final CI remains due. -- **Final status:** FIXED locally, OPEN until exact-SHA CI and - five checked release packages. + Build Raven `38853895481`, ARM64 job `116826216414`, built + and uploaded its package. The primary dbus source succeeded in + that CI job; the forced-primary-failure and altered-byte tests + above are the fallback-path evidence. All five downloaded + packages passed the provenance and checksum checks in + `evidence/ci-77d8f4c65/artifact-verification.json`. +- **Final status:** FIXED on tested source `77d8f4c65`. + Final Gate `38853895080` passed on the same SHA. Final + release-SHA qualification remains FINDING-067. From d4117d3bfdd6e59025c8242b39ef5c24f591f94a Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:40:39 +0200 Subject: [PATCH 163/195] [FINDING-049] Reconcile negative mock database open fix --- doc/RIP-0025-v4.8-security-remediation-register.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index a44f2bab30..6965054330 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -2942,8 +2942,16 @@ before these findings were recorded. - **Regression required:** Force `DB_RUNRECOVERY` (or another negative BDB result); `MakeMock` must throw and both initialization flags must remain false. -- **Remediation commit:** PENDING -- **Final status:** OPEN +- **Remediation commit:** `b2e5c8cafc295b71ab0d335a25416aa5a6960bec` + (shared with FINDING-050). +- **Verification:** `database_mock_negative_open_failure_renews_handle` + forces a negative `DB_RUNRECOVERY` result, checks that `MakeMock` throws, + and checks that neither initialized nor mock state is published. The test + failed before the correction and passed afterward under `make check` and + the mandatory invariant gate. The FINDING-050 checkpoint records the same + red-then-green evidence. +- **Final status:** FIXED. The negative-open path is covered by the + regression and the final exact-SHA `make check` jobs. FINDING-047 through FINDING-049 extend the unresolved LOW, MEDIUM, and INFO lists. The supplemental verdict remains **FAIL**. From 9bb987db6354477b42d45ccc6dc31e488daae983 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:40:52 +0200 Subject: [PATCH 164/195] [FINDING-136] Close wallet asset ownership classification --- ...RIP-0025-v4.8-security-remediation-register.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 6965054330..44da9d9f5d 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -6415,7 +6415,8 @@ and final release SHA require a new qualification pair. - **Regression required:** Wallet and functional vectors for normal, owner, reissuable, unique, restricted, and qualifier assets, missing PQ key, missing anchor, change, migration, and encrypted recovery. -- **Current status:** MITIGATED, still OPEN for release. The height-aware +- **Status at the first remediation checkpoint:** MITIGATED, still OPEN for + release. The height-aware filters in commit `8fb90c0aa` now exclude a post-activation asset unless the wallet has its tagged PQ key. The two-node `feature_pq_asset_wallet_ownership.py` regression reproduces @@ -6435,6 +6436,18 @@ and missing-PQ-key regressions pass, alongside the 32-case required functional matrix and `make check`. The wallet-code defect is FIXED locally. Cross-platform CI and public deployment remain separate release gates. +**2026-10-17 final disposition:** FIXED for the wallet ownership defect. +The owner-approved PQ-only asset rule supersedes the initial hybrid wording: +a new protected asset is spendable only through its matching ML-DSA key and +funded witness-v2 anchor, without an asset ECDSA key. The missing-PQ-key and +pre-activation ownership regressions failed before the ownership correction +and passed afterward. The exact-SHA Build Raven run `38885751826` and Final +Gate run `38885752445` passed the mandatory `make check` and required +functional suite at `1e58379df7c3ede470f8562711070d2b8152d10a`. +Anchor funding, migration availability, and public release readiness are +tracked separately in FINDING-067, FINDING-148, and FINDING-155. This status +does not close those release conditions. + ### FINDING-137: Invalid locked asset outpoint is dereferenced before its bound check - **Severity:** MEDIUM. Authenticated local RPC input can cause an From a72458390294bc544e8cfd13b0a187f68f9cb6c0 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:58:11 +0200 Subject: [PATCH 165/195] [FINDING-022] Close aarch64 cross-build with CI artifact proof --- doc/RIP-0025-v4.8-security-remediation-register.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 44da9d9f5d..6b75ade8e1 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -1175,9 +1175,13 @@ from the demonstrated coverage gaps. contains `-DCMAKE_SYSTEM_PROCESSOR=aarch64`; the native pinned depends build, exact configure probe, `make check`, and the invariant gate pass. A complete aarch64 node cross-build remains part of FINDING-065 CI qualification. -- **Final status:** MITIGATED. The configuration defect is corrected, but this - host has no aarch64 cross compiler and therefore cannot supply the final - artifact-level proof locally. +- **Final status:** FIXED. The configuration correction passed the aarch64 + cross-build in Build Raven run `38885751826` and Final Gate run + `38885752445` at `1e58379df7c3ede470f8562711070d2b8152d10a`. + The downloaded aarch64 package verified against its SHA-256 manifest, + embedded source commit, and the common five-target source digest + `ecc323108253549a6728521aedaf7bb2c9ffc9d62259c9423555392bed661b73`. + The final release SHA still requires the same full-matrix qualification. ### FINDING-023 : Release workflows omit documented supported artifacts From 502a6ea673f5fe08f68bd0e9784eaf9813d4a8d6 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:02:05 +0200 Subject: [PATCH 166/195] [FINDING-045] Validate plaintext BIP39 lineage before wallet load --- src/wallet/test/pq_wallet_tests.cpp | 65 +++++++++++++++++++++++++++ src/wallet/walletdb.cpp | 70 +++++++++++++++++++---------- 2 files changed, 112 insertions(+), 23 deletions(-) diff --git a/src/wallet/test/pq_wallet_tests.cpp b/src/wallet/test/pq_wallet_tests.cpp index 9c03ed1e5d..2ce8c48408 100644 --- a/src/wallet/test/pq_wallet_tests.cpp +++ b/src/wallet/test/pq_wallet_tests.cpp @@ -2227,6 +2227,71 @@ BOOST_AUTO_TEST_CASE(bip44_incomplete_or_malformed_seed_fails_load) expectCorrupt("long-seed", true, true, std::vector(65, 0x22)); } +BOOST_AUTO_TEST_CASE(bip44_plaintext_lineage_requires_semantic_integrity) +{ + const std::vector validWords = Bip39TestWords(); + const std::vector validPassphrase = Bip39TestPassphrase(); + const std::vector validSeed = Bip39TestSeed(); + const uint256 validHash = Hash(validWords.begin(), validWords.end()); + + for (int fault = 0; fault < 5; ++fault) { + const std::string filename = strprintf("bip44-lineage-integrity-%d.dat", fault); + std::vector words = validWords; + std::vector passphrase = validPassphrase; + std::vector seed = validSeed; + uint256 wordHash = validHash; + if (fault == 1) + wordHash.begin()[0] ^= 1; + else if (fault == 2) { + const std::string invalidMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"; + words.assign(invalidMnemonic.begin(), invalidMnemonic.end()); + wordHash = Hash(words.begin(), words.end()); + const SecureString invalidWords(words.begin(), words.end()); + const SecureString securePassphrase(passphrase.begin(), passphrase.end()); + SecureVector invalidSeed; + BOOST_REQUIRE(CMnemonic::ToSeed(invalidWords, securePassphrase, invalidSeed)); + seed.assign(invalidSeed.begin(), invalidSeed.end()); + } else if (fault == 3) + passphrase.push_back('!'); + else if (fault == 4) + seed[0] ^= 1; + + { + std::unique_ptr wallet = LoadPQWallet(filename); + BOOST_REQUIRE(wallet->SetHDChain(Bip44TestChain(wallet.get()), false)); + CWalletDB walletdb(wallet->GetDBHandle()); + BOOST_REQUIRE(walletdb.WriteBip39Words(wordHash, words, false)); + BOOST_REQUIRE(walletdb.WriteBip39Passphrase(passphrase, false)); + BOOST_REQUIRE(walletdb.WriteBip39VchSeed(seed, false)); + } + bitdb.Flush(false); + + std::unique_ptr dbw(new CWalletDBWrapper(&bitdb, filename)); + CWallet wallet(std::move(dbw)); + bool firstRun = false; + const DBErrors loadResult = wallet.LoadWallet(firstRun); + if (fault == 0) { + BOOST_REQUIRE_EQUAL(loadResult, DB_LOAD_OK); + const std::vector expectedBytes = ParseHex( + "023765b56ecb006a47d775beee38c45a9fe5dbe11d100b2e2ea3c99196dc915a2d"); + const CPubKey expectedFirstExternal(expectedBytes.begin(), expectedBytes.end()); + BOOST_REQUIRE(expectedFirstExternal.IsValid()); + BOOST_REQUIRE(wallet.TopUpKeyPool(1)); + CPubKey firstExternal; + BOOST_REQUIRE(wallet.GetKeyFromPool(firstExternal, false)); + BOOST_CHECK(firstExternal == expectedFirstExternal); + } else { + BOOST_CHECK_EQUAL(loadResult, DB_CORRUPT); + BOOST_CHECK(!wallet.HasValidBip39Seed()); + BOOST_CHECK_EQUAL(wallet.GetHDChain().nExternalChainCounter, 0U); + BOOST_CHECK_EQUAL(wallet.GetHDChain().nInternalChainCounter, 0U); + BOOST_CHECK(wallet.GetKeys().empty()); + } + bitdb.Flush(false); + } +} + BOOST_AUTO_TEST_CASE(bip44_derivation_refuses_missing_seed_without_advancing_counter) { const std::string filename = "bip44-missing-seed-derivation-wallet.dat"; diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index d549992dce..52beee10b2 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -423,6 +423,10 @@ class CWalletScanState { bool fHasCryptedBip39Words; bool fHasCryptedBip39Passphrase; bool fHasCryptedBip39Seed; + uint256 plaintextBip39WordHash; + SecureVector plaintextBip39Words; + SecureVector plaintextBip39Passphrase; + SecureVector plaintextBip39Seed; bool fHasHDChain; bool fHasPQHDChain; bool fEncryptionRewritePending; @@ -879,36 +883,27 @@ bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, else if (strType == "bip39words") { wss.fHasPlaintextBip39Words = true; - std::pair > valuePair; + std::pair valuePair; ssValue >> valuePair; - if (!ssValue.empty()) - { - strErr = "Error reading wallet database: BIP39 words trailing data"; - return false; - } - - if (!pwallet->LoadWords(valuePair.first, valuePair.second)) + if (!ssValue.empty() || valuePair.second.size() > 4096) { - strErr = "Error reading wallet database: LoadWords failed"; + strErr = "Error reading wallet database: BIP39 words corrupt"; return false; } + wss.plaintextBip39WordHash = valuePair.first; + wss.plaintextBip39Words.swap(valuePair.second); } else if (strType == "bip39passphrase") { wss.fHasPlaintextBip39Passphrase = true; - std::vector vchPassphrase; + SecureVector vchPassphrase; ssValue >> vchPassphrase; if (!ssValue.empty()) { strErr = "Error reading wallet database: BIP39 passphrase trailing data"; return false; } - - if (!pwallet->LoadPassphrase(vchPassphrase)) - { - strErr = "Error reading wallet database: LoadPassphrase failed"; - return false; - } + wss.plaintextBip39Passphrase.swap(vchPassphrase); } else if (strType == "bip39vchseed") { @@ -924,14 +919,9 @@ bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, strErr = "Error reading wallet database: BIP39 seed trailing data"; return false; } - std::vector vchSeed(seedSize); + SecureVector vchSeed(seedSize); ssValue.read(reinterpret_cast(vchSeed.data()), vchSeed.size()); - - if (!pwallet->LoadVchSeed(vchSeed)) - { - strErr = "Error reading wallet database: LoadVchSeed failed"; - return false; - } + wss.plaintextBip39Seed.swap(vchSeed); } else if (strType == "encryption_rewrite_pending") { @@ -1098,6 +1088,40 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet) result = DB_CORRUPT; } + if (result == DB_LOAD_OK && pwallet->IsBip44Enabled() && hasPlaintextBip39) { + SecureString words; + words.reserve(wss.plaintextBip39Words.size() > 64 ? + wss.plaintextBip39Words.size() : 64); + words.assign(wss.plaintextBip39Words.begin(), + wss.plaintextBip39Words.end()); + SecureString passphrase; + passphrase.reserve(wss.plaintextBip39Passphrase.size() > 64 ? + wss.plaintextBip39Passphrase.size() : 64); + passphrase.assign(wss.plaintextBip39Passphrase.begin(), + wss.plaintextBip39Passphrase.end()); + SecureVector derivedSeed; + const bool validLineage = + Hash(wss.plaintextBip39Words.begin(), wss.plaintextBip39Words.end()) == + wss.plaintextBip39WordHash && + CMnemonic::Check(words) && + CMnemonic::ToSeed(words, passphrase, derivedSeed) && + derivedSeed.size() == BIP39_SEED_SIZE && + TimingResistantEqual(derivedSeed, wss.plaintextBip39Seed); + ClearSecureString(words); + ClearSecureString(passphrase); + if (!validLineage) { + LogPrintf("Error reading wallet database: BIP39 plaintext lineage corrupt\n"); + result = DB_CORRUPT; + } else if (!pwallet->LoadWords(wss.plaintextBip39WordHash, + std::move(wss.plaintextBip39Words)) || + !pwallet->LoadPassphrase( + std::move(wss.plaintextBip39Passphrase)) || + !pwallet->LoadVchSeed(std::move(wss.plaintextBip39Seed))) { + LogPrintf("Error reading wallet database: BIP39 plaintext install failed\n"); + result = DB_CORRUPT; + } + } + if (fNoncriticalErrors && result == DB_LOAD_OK) result = DB_NONCRITICAL_ERROR; From a3ce50bbb477220c9b410fcb650e4de9d3349ec2 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:02:17 +0200 Subject: [PATCH 167/195] [FINDING-045] Record BIP39 lineage regression and verification --- ...0025-v4.8-security-remediation-register.md | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 6b75ade8e1..505317b9f3 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -2720,8 +2720,24 @@ audited commit and both authoritative baselines. passphrase, and seed while keeping a complete record set. Every case must return `DB_CORRUPT`, leave no usable seed, and advance no HD/keypool counter; an official Trezor tuple must reload and derive the fixed expected pubkey. -- **Remediation commit:** PENDING -- **Final status:** OPEN +- **Remediation commit:** `502a6ea673f5fe08f68bd0e9784eaf9813d4a8d6`. +- **Modified files:** `src/wallet/walletdb.cpp` and + `src/wallet/test/pq_wallet_tests.cpp`. +- **Red regression:** The new complete-tuple test failed eight assertions + before the loader correction across altered word hash, invalid mnemonic, + passphrase, and seed records. The valid Trezor tuple passed. An additional + negative control removed the mnemonic checksum check while retaining the + other checks; the invalid-mnemonic case then loaded and exposed a usable + seed, causing two test failures. +- **Green verification:** The loader stages plaintext records in secure + buffers, checks the word hash and mnemonic checksum, rederives the BIP39 + seed, compares it in constant time, and installs the tuple only after the + complete database scan validates. Corrupt tuples return `DB_CORRUPT` with + no usable seed or advanced HD/keypool counter. The focused test, all 60 + `pq_wallet_tests` cases, and `make check -j4` passed after restoration of + the checksum check. +- **Final status:** FIXED locally. Exact-SHA cross-platform CI qualification + remains required for the 4.8.1 release. FINDING-045 extends the unresolved MEDIUM list. The supplemental verdict remains **FAIL**. From 7e36bb890d26ba92be883d78b04a95afe26744f5 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:03:56 +0200 Subject: [PATCH 168/195] [FINDING-023] Record five-target artifact qualification checkpoint --- doc/RIP-0025-v4.8-security-remediation-register.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 505317b9f3..cf28276d4b 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -1237,6 +1237,18 @@ from the demonstrated coverage gaps. artifact-level release qualification remains open until the remote matrix and detached signing/assembly are verified at the final audited SHA. +**2026-10-17 artifact checkpoint:** Build Raven run `38885751826` and Final +Gate run `38885752445` passed at exact commit +`1e58379df7c3ede470f8562711070d2b8152d10a`. The five Build Raven +packages for Linux x86_64, ARM32, AArch64, Windows, and macOS were +downloaded. Every package manifest, contained file digest, target/host +provenance, and embedded commit matched; the common source archive digest +was `ecc323108253549a6728521aedaf7bb2c9ffc9d62259c9423555392bed661b73`. +This satisfies the previously missing five-target unsigned artifact proof +for that SHA. FINDING-023 remains MITIGATED until the final release SHA is +qualified and detached signing, installer/DMG assembly, and signed checksums +are complete. + ### Regression-of-fixes conclusions | Earlier remediation | Second-audit result | From 1e33543fd20daebc654d4a7d3835f7969ad56943 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:57:15 +0200 Subject: [PATCH 169/195] [FINDING-062] Test bit 12 resource limits across activation and reorg --- src/test/rip25_versionbits_tests.cpp | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/test/rip25_versionbits_tests.cpp b/src/test/rip25_versionbits_tests.cpp index 40a165b5ae..00c038fd44 100644 --- a/src/test/rip25_versionbits_tests.cpp +++ b/src/test/rip25_versionbits_tests.cpp @@ -6,6 +6,7 @@ #include "chainparams.h" #include "assets/assets.h" #include "base58.h" +#include "consensus/consensus.h" #include "consensus/params.h" #include "consensus/tx_verify.h" #include "consensus/validation.h" @@ -148,6 +149,66 @@ BOOST_AUTO_TEST_CASE(pq_bit12_activates_independently_from_v48_overflow_bit11) BOOST_CHECK_EQUAL(VersionBitsState(chain.Tip(), params, Consensus::DEPLOYMENT_TRANSFER_OVERFLOW, cache), THRESHOLD_STARTED); } +BOOST_AUTO_TEST_CASE(pq_bit12_resources_follow_exact_boundary_and_reorg) +{ + Consensus::Params params = MakeRIP25VersionBitsParams(); + params.nPQHybridEnabled = false; + params.nPowTargetSpacing = 60; + const uint32_t pqMask = VersionBitsMask(params, Consensus::DEPLOYMENT_PQ_HYBRID); + + SyntheticVersionBitsChain common; + common.Mine(4, VERSIONBITS_TOP_BITS); + SyntheticVersionBitsChain signaled(common.Tip()); + signaled.Mine(3, VERSIONBITS_TOP_BITS | pqMask); + signaled.Mine(1, VERSIONBITS_TOP_BITS); + signaled.Mine(2, VERSIONBITS_TOP_BITS); + CBlockIndex* twoBefore = signaled.Tip(); + signaled.Mine(1, VERSIONBITS_TOP_BITS); + CBlockIndex* oneBefore = signaled.Tip(); + signaled.Mine(1, VERSIONBITS_TOP_BITS); + CBlockIndex* activationParent = signaled.Tip(); + signaled.Mine(1, VERSIONBITS_TOP_BITS); + CBlockIndex* oneAfter = signaled.Tip(); + signaled.Mine(1, VERSIONBITS_TOP_BITS); + CBlockIndex* twoAfter = signaled.Tip(); + + BOOST_REQUIRE_EQUAL(twoBefore->nHeight, 9); + BOOST_REQUIRE_EQUAL(oneBefore->nHeight, 10); + BOOST_REQUIRE_EQUAL(activationParent->nHeight, 11); + BOOST_REQUIRE_EQUAL(oneAfter->nHeight, 12); + BOOST_REQUIRE_EQUAL(twoAfter->nHeight, 13); + + const CBlockIndex* parents[] = { + twoBefore, oneBefore, activationParent, oneAfter, twoAfter, + }; + const bool expectedActive[] = {false, false, true, true, true}; + { + LOCK(cs_main); + versionbitscache.Clear(); + for (unsigned int i = 0; i < 5; ++i) + BOOST_CHECK_EQUAL(IsPQWitnessDiscountActive(parents[i], params), expectedActive[i]); + } + for (unsigned int i = 0; i < 5; ++i) { + const unsigned int expectedWeight = expectedActive[i] ? + MAX_BLOCK_WEIGHT_RIP25_PHASE1 : MAX_BLOCK_WEIGHT_RIP2; + BOOST_CHECK_EQUAL(GetMaxBlockWeightForPrev(parents[i], params), expectedWeight); + BOOST_CHECK_EQUAL(GetMaxBlockSerializedSizeForPrev(parents[i], params), expectedWeight); + } + + SyntheticVersionBitsChain unsignaled(common.Tip()); + unsignaled.Mine(10, VERSIONBITS_TOP_BITS); + { + LOCK(cs_main); + BOOST_CHECK(!IsPQWitnessDiscountActive(unsignaled.Tip(), params)); + } + BOOST_CHECK_EQUAL(GetMaxBlockWeightForPrev(unsignaled.Tip(), params), + MAX_BLOCK_WEIGHT_RIP2); + { + LOCK(cs_main); + versionbitscache.Clear(); + } +} + BOOST_AUTO_TEST_CASE(overflow_bit11_does_not_signal_pq_bit12) { Consensus::Params params = MakeRIP25VersionBitsParams(); From d07b00b92c857a201b72ad098078619899702114 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:57:27 +0200 Subject: [PATCH 170/195] [FINDING-062] Record activation boundary mutation proof --- doc/RIP-0025-v4.8-security-remediation-register.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index cf28276d4b..e93e15cf50 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -3827,6 +3827,15 @@ before closing that finding. invalidate/reconsider, and PQ sigop-over-limit transaction vectors remain outstanding; FINDING-061 covers the unsettled sigop cost. +Post-checkpoint extension: `1e33543fd` adds a synthetic BIP9 branch test for +the two candidate blocks before and after bit 12 activation. It checks the +8 MWU to 12 MWU weight and serialized-size transition, witness discount, +and a competing unsignaled branch. A deliberate mutation retaining the +8 MWU limit after activation failed six assertions; the restored source +passed the focused test and `make check -j4`. A real invalidate/reconsider +full-chain vector and PQ sigop-over-limit transaction vector remain due, so +FINDING-062 remains MITIGATED. + ### FINDING-063: Repository KAT coverage cannot detect backend drift - **Severity:** MEDIUM From 64fb051f48a54c02b62ffff1f25760f40a24c8e6 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:57:47 +0200 Subject: [PATCH 171/195] [FINDING-036] Give BIP39 seeds stable unique IDs with atomic migration --- src/wallet/rpcwallet.cpp | 2 +- src/wallet/test/wallet_tests.cpp | 207 +++++++++++++++++++++++++++++++ src/wallet/wallet.cpp | 123 +++++++++++++++++- src/wallet/walletdb.cpp | 5 + src/wallet/walletdb.h | 3 +- 5 files changed, 335 insertions(+), 5 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 79363908b4..cfa094faf8 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2915,7 +2915,7 @@ UniValue getwalletinfo(const JSONRPCRequest& request) " \"keypoolsize_hd_internal\": xxxx, (numeric) how many new keys are pre-generated for internal use (used for change outputs, only appears if the wallet is using this feature, otherwise external keys are used)\n" " \"unlocked_until\": ttt, (numeric) the timestamp in seconds since epoch (midnight Jan 1 1970 GMT) that the wallet is unlocked for transfers, or 0 if the wallet is locked\n" " \"paytxfee\": x.xxxx, (numeric) the transaction fee configuration, set in " + CURRENCY_UNIT + "/kB\n" - " \"hdseedid\": \"\" (string, optional) the Hash160 of the HD seed (only present when HD is enabled)\n" + " \"hdseedid\": \"\" (string, optional) HD seed identifier. BIP44 uses a domain-separated digest of the raw seed; legacy HD uses the seed public-key Hash160.\n" " \"hdmasterkeyid\": \"\" (string, optional) alias for hdseedid retained for backwards-compatibility. Will be removed in V0.18.\n" "}\n" "\nExamples:\n" diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 3b48d056a7..4287f1b1df 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -6,6 +6,7 @@ #include "wallet/wallet.h" #include "assets/assets.h" #include "chainparams.h" +#include "hash.h" #include #include @@ -62,6 +63,36 @@ class FailingOpenDbEnv : public DbEnv bool& destroyed_; }; +class FailingTxnBeginDbEnv : public DbEnv +{ +public: + FailingTxnBeginDbEnv() : DbEnv(DB_CXX_NO_EXCEPTIONS) {} + + int txn_begin(DbTxn*, DbTxn** transaction, u_int32_t) override + { + *transaction = nullptr; + return DB_RUNRECOVERY; + } +}; + +class ScopedFailingTxnBegin +{ +private: + DbEnv* const previous; + FailingTxnBeginDbEnv failing; + +public: + ScopedFailingTxnBegin() : previous(bitdb.dbenv) + { + bitdb.dbenv = &failing; + } + + ~ScopedFailingTxnBegin() + { + bitdb.dbenv = previous; + } +}; + class ScopedWalletFactoryTestState { private: @@ -113,6 +144,99 @@ using RegisteredWalletPtr = std::unique_ptr; BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) + BOOST_AUTO_TEST_CASE(bip44_seed_ids_are_unique_and_stable) + { + ScopedWalletFactoryTestState testState(false); + const std::string mnemonics[] = { + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + }; + CKeyID seedIds[3]; + CPubKey firstKeys[3]; + for (unsigned int i = 0; i < 3; ++i) { + const std::string filename = strprintf("bip44-seed-id-%u.dat", i); + gArgs.ForceSetArg("-mnemonic", mnemonics[i]); + gArgs.ForceSetArg("-mnemonicpassphrase", "TREZOR"); + { + RegisteredWalletPtr wallet(CWallet::CreateWalletFromFile(filename)); + BOOST_REQUIRE(wallet != nullptr); + BOOST_REQUIRE(wallet->IsBip44Enabled()); + seedIds[i] = wallet->GetHDChain().seed_id; + BOOST_REQUIRE(wallet->GetKeyFromPool(firstKeys[i], false)); + } + bitdb.Flush(false); + std::unique_ptr dbw(new CWalletDBWrapper(&bitdb, filename)); + CWallet reloaded(std::move(dbw)); + bool firstRun = true; + BOOST_REQUIRE_EQUAL(reloaded.LoadWallet(firstRun, false), DB_LOAD_OK); + BOOST_CHECK(!firstRun); + BOOST_CHECK(reloaded.GetHDChain().seed_id == seedIds[i]); + } + BOOST_CHECK(seedIds[0] != seedIds[1]); + BOOST_CHECK(seedIds[0] == seedIds[2]); + BOOST_CHECK(firstKeys[0] != firstKeys[1]); + BOOST_CHECK(firstKeys[0] == firstKeys[2]); + const unsigned char blank = 0; + BOOST_CHECK(seedIds[0] != CKeyID(Hash160(&blank, &blank))); + } + + BOOST_AUTO_TEST_CASE(bip44_legacy_seed_id_migrates_without_changing_keys) + { + ScopedWalletFactoryTestState testState(false); + const std::string filename = "bip44-legacy-seed-id.dat"; + const std::string mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + gArgs.ForceSetArg("-mnemonic", mnemonic); + gArgs.ForceSetArg("-mnemonicpassphrase", "TREZOR"); + CPubKey firstKey; + CKeyID expectedSeedId; + uint32_t externalCounter = 0; + { + RegisteredWalletPtr wallet(CWallet::CreateWalletFromFile(filename)); + BOOST_REQUIRE(wallet != nullptr); + expectedSeedId = wallet->GetHDChain().seed_id; + BOOST_REQUIRE(wallet->GetKeyFromPool(firstKey, false)); + BOOST_REQUIRE(firstKey.IsValid()); + externalCounter = wallet->GetHDChain().nExternalChainCounter; + } + bitdb.Flush(false); + + const unsigned char blank = 0; + const CKeyID legacySeedId(Hash160(&blank, &blank)); + { + CWalletDBWrapper dbw(&bitdb, filename); + CWalletDB walletdb(dbw); + CHDChain legacy(nullptr); + CDB raw(dbw); + BOOST_REQUIRE(raw.Read(std::string("hdchain"), legacy)); + legacy.seed_id = legacySeedId; + BOOST_REQUIRE(walletdb.WriteHDChain(legacy)); + + CKeyMetadata metadata; + const auto metadataKey = std::make_pair(std::string("keymeta"), firstKey); + BOOST_REQUIRE(raw.Read(metadataKey, metadata)); + metadata.hd_seed_id = legacySeedId; + BOOST_REQUIRE(raw.Write(metadataKey, metadata)); + } + bitdb.Flush(false); + + std::unique_ptr dbw(new CWalletDBWrapper(&bitdb, filename)); + CWallet reloaded(std::move(dbw)); + bool firstRun = true; + BOOST_REQUIRE_EQUAL(reloaded.LoadWallet(firstRun, false), DB_LOAD_OK); + BOOST_CHECK(!firstRun); + BOOST_CHECK(reloaded.GetHDChain().seed_id == expectedSeedId); + BOOST_CHECK(reloaded.GetHDChain().seed_id != legacySeedId); + BOOST_CHECK_EQUAL(reloaded.GetHDChain().nExternalChainCounter, externalCounter); + const auto entry = reloaded.mapKeyMetadata.find(firstKey.GetID()); + BOOST_REQUIRE(entry != reloaded.mapKeyMetadata.end()); + BOOST_CHECK(entry->second.hd_seed_id == expectedSeedId); + CKey existingKey; + BOOST_CHECK(reloaded.GetKey(firstKey.GetID(), existingKey)); + BOOST_CHECK(existingKey.GetPubKey() == firstKey); + } + BOOST_AUTO_TEST_CASE(database_environment_open_failure_renews_handle) { CDBEnv env; @@ -943,3 +1067,86 @@ BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) } BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_CASE(encrypted_bip44_legacy_seed_id_migrates_on_unlock, TestingSetup) + { + // Encryption rewrites the Berkeley DB file. Use a file-backed test environment. + bitdb.Close(); + bitdb.Reset(); + ScopedWalletFactoryTestState testState(false); + const std::string filename = "encrypted-bip44-legacy-seed-id.dat"; + const SecureString walletPassphrase("bip44-seed-id-migration-passphrase"); + gArgs.ForceSetArg("-mnemonic", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"); + gArgs.ForceSetArg("-mnemonicpassphrase", "TREZOR"); + CPubKey firstKey; + CKeyID expectedSeedId; + { + RegisteredWalletPtr wallet(CWallet::CreateWalletFromFile(filename)); + BOOST_REQUIRE(wallet != nullptr); + expectedSeedId = wallet->GetHDChain().seed_id; + BOOST_REQUIRE(wallet->GetKeyFromPool(firstKey, false)); + BOOST_REQUIRE(wallet->EncryptWallet(walletPassphrase)); + } + bitdb.Flush(false); + + const unsigned char blank = 0; + const CKeyID legacySeedId(Hash160(&blank, &blank)); + { + CWalletDBWrapper dbw(&bitdb, filename); + CWalletDB walletdb(dbw); + CDB raw(dbw); + CHDChain legacy(nullptr); + BOOST_REQUIRE(raw.Read(std::string("hdchain"), legacy)); + legacy.seed_id = legacySeedId; + BOOST_REQUIRE(walletdb.WriteHDChain(legacy)); + const auto metadataKey = std::make_pair(std::string("keymeta"), firstKey); + CKeyMetadata metadata; + BOOST_REQUIRE(raw.Read(metadataKey, metadata)); + metadata.hd_seed_id = legacySeedId; + BOOST_REQUIRE(raw.Write(metadataKey, metadata)); + } + bitdb.Flush(false); + + { + std::unique_ptr dbw(new CWalletDBWrapper(&bitdb, filename)); + CWallet reloaded(std::move(dbw)); + bool firstRun = true; + BOOST_REQUIRE_EQUAL(reloaded.LoadWallet(firstRun, false), DB_LOAD_OK); + BOOST_CHECK(!firstRun); + BOOST_CHECK(reloaded.IsLocked()); + BOOST_CHECK(reloaded.GetHDChain().seed_id == legacySeedId); + { + ScopedFailingTxnBegin failingTransaction; + BOOST_CHECK(!reloaded.Unlock(walletPassphrase)); + } + BOOST_CHECK(reloaded.IsLocked()); + BOOST_CHECK(reloaded.GetHDChain().seed_id == legacySeedId); + const auto legacyMetadata = reloaded.mapKeyMetadata.find(firstKey.GetID()); + BOOST_REQUIRE(legacyMetadata != reloaded.mapKeyMetadata.end()); + BOOST_CHECK(legacyMetadata->second.hd_seed_id == legacySeedId); + { + CDB raw(reloaded.GetDBHandle(), "r"); + CHDChain unchanged(nullptr); + BOOST_REQUIRE(raw.Read(std::string("hdchain"), unchanged)); + BOOST_CHECK(unchanged.seed_id == legacySeedId); + CKeyMetadata metadata; + BOOST_REQUIRE(raw.Read( + std::make_pair(std::string("keymeta"), firstKey), metadata)); + BOOST_CHECK(metadata.hd_seed_id == legacySeedId); + } + BOOST_REQUIRE(reloaded.Unlock(walletPassphrase)); + BOOST_CHECK(reloaded.GetHDChain().seed_id == expectedSeedId); + CKey existingKey; + BOOST_REQUIRE(reloaded.GetKey(firstKey.GetID(), existingKey)); + BOOST_CHECK(existingKey.GetPubKey() == firstKey); + BOOST_CHECK(reloaded.Lock()); + } + bitdb.Flush(false); + + std::unique_ptr dbw(new CWalletDBWrapper(&bitdb, filename)); + CWallet reloadedAgain(std::move(dbw)); + bool firstRun = true; + BOOST_REQUIRE_EQUAL(reloadedAgain.LoadWallet(firstRun, false), DB_LOAD_OK); + BOOST_CHECK(reloadedAgain.GetHDChain().seed_id == expectedSeedId); + } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 167d3ff69c..49a91408c4 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -38,6 +38,7 @@ #include "wallet/pqderivation.h" #include +#include #include #include @@ -79,6 +80,114 @@ class ScopedSecureStringCleanser ~ScopedSecureStringCleanser() { ClearSecureString(value); } }; +static CKeyID Bip39SeedId(const SecureVector& seed) +{ + // Include the trailing zero to make this domain unambiguous. The raw + // BIP39 seed remains the input to BIP32 derivation; this is metadata only. + static const char domain[] = "Ravencoin BIP39 HD seed ID v1"; + uint160 hash; + CHash160() + .Write(reinterpret_cast(domain), sizeof(domain)) + .Write(seed.data(), seed.size()) + .Finalize(hash.begin()); + return CKeyID(hash); +} + +static CKeyID LegacyBip39SeedId() +{ + uint160 hash; + CHash160().Finalize(hash.begin()); + return CKeyID(hash); +} + +static bool MatchesBip44Keypath(const SecureVector& seed, + const std::string& path, const CPubKey& pubkey) +{ + unsigned int coin = 0; + unsigned int account = 0; + unsigned int change = 0; + unsigned int index = 0; + int consumed = 0; + if (std::sscanf(path.c_str(), "m/44'/%u'/%u'/%u/%u%n", + &coin, &account, &change, &index, &consumed) != 4 || + consumed != static_cast(path.size()) || + coin >= BIP32_HARDENED_KEY_LIMIT || + account >= BIP32_HARDENED_KEY_LIMIT || + change > 1 || index >= BIP32_HARDENED_KEY_LIMIT || + path != strprintf("m/44'/%u'/%u'/%u/%u", + coin, account, change, index)) + return false; + + CExtKey root, purposeKey, coinKey, accountKey, chainKey, childKey; + root.SetSeed(seed.data(), seed.size()); + return root.Derive(purposeKey, 44 | BIP32_HARDENED_KEY_LIMIT) && + purposeKey.Derive(coinKey, coin | BIP32_HARDENED_KEY_LIMIT) && + coinKey.Derive(accountKey, account | BIP32_HARDENED_KEY_LIMIT) && + accountKey.Derive(chainKey, change) && + chainKey.Derive(childKey, index) && + childKey.key.GetPubKey() == pubkey; +} + +static bool MigrateLegacyBip39SeedId(CWallet& wallet) +{ + AssertLockHeld(wallet.cs_wallet); + if (!wallet.IsBip44Enabled() || wallet.IsLocked()) + return true; + + SecureVector seed; + if (!wallet.GetBip39Seed(seed)) + return false; + const CKeyID oldId = LegacyBip39SeedId(); + const CKeyID newId = Bip39SeedId(seed); + if (wallet.GetHDChain().seed_id != oldId) + return true; + if (newId.IsNull() || newId == oldId) + return false; + + // The old constant ID could also label keys from a prior BIP44 seed. + // Update only keys provably derived from the currently stored seed. + // Replace those references and the current chain together so a failed + // write leaves the legacy state intact. + std::vector> metadataUpdates; + for (const auto& entry : wallet.mapKeyMetadata) { + if (entry.second.hd_seed_id != oldId) + continue; + const CKeyID* keyId = boost::get(&entry.first); + CPubKey pubkey; + if (keyId && wallet.GetPubKey(*keyId, pubkey) && + pubkey.IsValid() && pubkey.GetID() == *keyId && + MatchesBip44Keypath(seed, entry.second.hdKeypath, pubkey)) + metadataUpdates.emplace_back(entry.first, pubkey); + } + + CHDChain migrated = wallet.GetHDChain(); + migrated.seed_id = newId; + // The synchronous transaction commit supplies durability; skip the + // separate checkpoint so transaction-begin failure leaves no side effect. + CWalletDB walletdb(wallet.GetDBHandle(), "r+", false); + if (!walletdb.TxnBegin(DB_TXN_SYNC)) + return false; + for (const auto& update : metadataUpdates) { + CKeyMetadata metadata = wallet.mapKeyMetadata.at(update.first); + metadata.hd_seed_id = newId; + if (!walletdb.WriteKeyMetadata(update.second, metadata)) { + walletdb.TxnAbort(); + return false; + } + } + if (!walletdb.WriteHDChain(migrated)) { + walletdb.TxnAbort(); + return false; + } + if (!walletdb.TxnCommit(DB_TXN_SYNC)) + return false; + + wallet.SetHDChain(migrated, true); + for (const auto& update : metadataUpdates) + wallet.mapKeyMetadata.at(update.first).hd_seed_id = newId; + return true; +} + void SetPendingMnemonicInput(SecureString words, SecureString passphrase) { ScopedSecureStringCleanser cleanseWords(words); @@ -839,6 +948,10 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase) if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, _vMasterKey)) continue; // try another master key if (CCryptoKeyStore::Unlock(_vMasterKey)) { + if (!MigrateLegacyBip39SeedId(*this)) { + Lock(); + return false; + } return true; } } @@ -2217,12 +2330,13 @@ CPubKey CWallet::GenerateNewSeed(CWalletDB* pwalletdb) if (!AddVchSeed(vchSeed)) throw std::runtime_error(std::string(__func__) + ": storing BIP39 seed failed"); - CPubKey seed(vchSeed.begin(), vchSeed.end()); - newHdChain.seed_id = seed.GetID(); + newHdChain.seed_id = Bip39SeedId(vchSeed); SetHDChain(newHdChain, false, pwalletdb); - return seed; + // BIP39 uses a 64-byte seed, which is not a public key. The BIP44 caller + // uses the persisted chain ID and ignores this legacy return value. + return CPubKey(); } @@ -4908,6 +5022,9 @@ DBErrors CWallet::LoadWallet(bool& fFirstRunRet, bool notifyLoad) if (nLoadWalletRet != DB_LOAD_OK) return nLoadWalletRet; + if (!MigrateLegacyBip39SeedId(*this)) + return DB_LOAD_FAIL; + if (notifyLoad) uiInterface.LoadWallet(this); diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 52beee10b2..3987f4c947 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -74,6 +74,11 @@ bool CWalletDB::WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, c return WriteIC(std::make_pair(std::string("key"), vchPubKey), std::make_pair(vchPrivKey, Hash(vchKey.begin(), vchKey.end())), false); } +bool CWalletDB::WriteKeyMetadata(const CPubKey& vchPubKey, const CKeyMetadata& keyMeta) +{ + return WriteIC(std::make_pair(std::string("keymeta"), vchPubKey), keyMeta); +} + bool CWalletDB::WriteCryptedKey(const CPubKey& vchPubKey, const std::vector& vchCryptedSecret, const CKeyMetadata &keyMeta) diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index d08844e398..65e1199cb2 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -70,7 +70,7 @@ class CHDChain public: uint32_t nExternalChainCounter; uint32_t nInternalChainCounter; - CKeyID seed_id; //!< seed hash160 + CKeyID seed_id; //!< legacy key ID or domain-separated BIP39 seed ID bool bUse_bip44; SecureVector vchMnemonic; @@ -290,6 +290,7 @@ class CWalletDB bool WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata &keyMeta); bool WriteCryptedKey(const CPubKey& vchPubKey, const std::vector& vchCryptedSecret, const CKeyMetadata &keyMeta); + bool WriteKeyMetadata(const CPubKey& vchPubKey, const CKeyMetadata& keyMeta); bool WritePQKey(const uint256& witnessProgram, const CPQPubKey& pqPubKey, const CPQKey::KeyData& pqKeyData); bool WriteCryptedPQKey(const uint256& witnessProgram, const CPQPubKey& pqPubKey, const std::vector& vchCryptedSecret); From 3d9294b674b5daf5a9f0bcc4aea1cc1099d79678 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:59:20 +0200 Subject: [PATCH 172/195] [FINDING-036] Record BIP39 seed ID migration proof --- doc/RIP-0025-v4.8-security-remediation-register.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index e93e15cf50..c8e7daeff6 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -2151,8 +2151,18 @@ was changed before these findings were frozen. - **Regression required:** Two independent mnemonic vectors must produce distinct, stable identifiers across reload; a legacy constant-ID wallet must migrate deterministically without changing any derived key. -- **Remediation commit:** PENDING -- **Final status:** OPEN +- **Remediation commit:** `64fb051f4`. +- **Verification:** Two different mnemonic vectors produce different stable + seed IDs; repeating one mnemonic reproduces the ID and first derived key. + Plaintext and encrypted legacy constant-ID wallets migrate the current + chain and only metadata whose stored public key matches rederivation from + the current seed. The encrypted test injects a Berkeley DB transaction-begin + failure and confirms the wallet remains locked with its original database + and in-memory metadata; a later unlock completes migration. The existing + derived key and chain counter remain unchanged. The red regression found + the former constant ID and lack of migration. All focused cases, the wallet + suite, and `make check -j4` passed after the fix. +- **Final status:** FIXED locally; final exact-SHA CI qualification remains due. FINDING-033 and FINDING-034 extend the unresolved HIGH list; FINDING-035 is MEDIUM and FINDING-036 is LOW. The supplemental verdict remains **FAIL**. From 12200477be3dea3fbd29622761357851a3f2f82a Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:59:40 +0200 Subject: [PATCH 173/195] [FINDING-155] Expose protected asset change in transfer dialog --- src/qt/assetsdialog.cpp | 85 ++++++++++++++++++++++++++++++++++++ src/qt/assetsdialog.h | 3 ++ src/qt/forms/assetsdialog.ui | 33 ++++++++++++++ src/qt/test/wallettests.cpp | 29 ++++++++++++ src/qt/test/wallettests.h | 1 + 5 files changed, 151 insertions(+) diff --git a/src/qt/assetsdialog.cpp b/src/qt/assetsdialog.cpp index 30cdc81bc1..73e56e4a54 100644 --- a/src/qt/assetsdialog.cpp +++ b/src/qt/assetsdialog.cpp @@ -32,6 +32,7 @@ #include #include +#include #include #include #include @@ -67,6 +68,8 @@ AssetsDialog::AssetsDialog(const PlatformStyle *_platformStyle, QWidget *parent) } GUIUtil::setupAddressWidget(ui->lineEditAssetControlChange, this); + GUIUtil::setupAssetAddressWidget(ui->lineEditProtectedAssetChange, this); + updateProtectedAssetChangeAvailability(); addEntry(); @@ -77,6 +80,7 @@ AssetsDialog::AssetsDialog(const PlatformStyle *_platformStyle, QWidget *parent) connect(ui->pushButtonAssetControl, SIGNAL(clicked()), this, SLOT(assetControlButtonClicked())); connect(ui->checkBoxAssetControlChange, SIGNAL(stateChanged(int)), this, SLOT(assetControlChangeChecked(int))); connect(ui->lineEditAssetControlChange, SIGNAL(textEdited(const QString &)), this, SLOT(assetControlChangeEdited(const QString &))); + connect(ui->buttonChooseProtectedAssetChange, SIGNAL(clicked()), this, SLOT(chooseProtectedAssetChange())); // Coin Control: clipboard actions QAction *clipboardQuantityAction = new QAction(tr("Copy quantity"), this); @@ -135,12 +139,14 @@ void AssetsDialog::setClientModel(ClientModel *_clientModel) if (_clientModel) { connect(_clientModel, SIGNAL(numBlocksChanged(int,QDateTime,double,bool)), this, SLOT(updateSmartFeeLabel())); + connect(_clientModel, SIGNAL(numBlocksChanged(int,QDateTime,double,bool)), this, SLOT(updateProtectedAssetChangeAvailability())); } } void AssetsDialog::setModel(WalletModel *_model) { this->model = _model; + updateProtectedAssetChangeAvailability(); if(_model && _model->getOptionsModel()) { @@ -377,6 +383,13 @@ void AssetsDialog::on_sendButton_clicked() if (model->getOptionsModel()->getCoinControlFeatures()) ctrl = *AssetControlDialog::assetControl; + QString assetChangeError; + if (!applyProtectedAssetChange(ctrl, &assetChangeError)) { + QMessageBox::warning(this, tr("Protected asset change"), assetChangeError); + fNewRecipientAllowed = true; + return; + } + updateAssetControlState(ctrl); CWalletTx tx; @@ -958,6 +971,78 @@ void AssetsDialog::assetControlChangeEdited(const QString& text) } } +void AssetsDialog::updateProtectedAssetChangeAvailability() +{ + const bool active = GUIUtil::pqAssetDestinationRequired(); + if (!active) + ui->lineEditProtectedAssetChange->clear(); + ui->lineEditProtectedAssetChange->setEnabled(active); + ui->buttonChooseProtectedAssetChange->setEnabled(active && model); + ui->labelProtectedAssetChange->setEnabled(active); + ui->widgetProtectedAssetChange->setToolTip(active ? + tr("Choose a wallet-owned PQ-only destination when a partial transfer of historical assets creates change.") : + tr("Protected asset change is available after PQ asset activation.")); +} + +void AssetsDialog::chooseProtectedAssetChange() +{ + if (!model || !GUIUtil::pqAssetDestinationRequired()) + return; + + QStringList choices; + choices << tr("Create a new protected change destination"); + choices.append(model->getAssetDestinations()); + bool selected = false; + const QString choice = QInputDialog::getItem(this, tr("Protected asset change"), + tr("Wallet-owned destination:"), choices, 0, false, &selected); + if (!selected) + return; + + QString descriptor = choice; + if (choice == choices.first()) { + WalletModel::UnlockContext ctx(model->requestUnlock()); + if (!ctx.isValid()) + return; + descriptor = model->newAssetDestination(); + } + if (!GUIUtil::isValidAssetDestination(descriptor) || + !model->getAssetDestinations().contains(descriptor)) { + QMessageBox::warning(this, tr("Protected asset change"), + tr("Could not create or select a wallet-owned PQ-only asset destination.")); + return; + } + ui->lineEditProtectedAssetChange->setText(descriptor); +} + +bool AssetsDialog::applyProtectedAssetChange(CCoinControl& ctrl, QString* error) const +{ + ctrl.assetDestChange = CNoDestination(); + ctrl.pqAssetChangeProgram.reset(); + + const QString descriptor = ui->lineEditProtectedAssetChange->text().trimmed(); + if (descriptor.isEmpty()) + return true; + + CKeyID assetID; + uint256 pqProgram; + if (!model || !GUIUtil::pqAssetDestinationRequired() || + !DecodePQOnlyAssetDestination(descriptor.toStdString(), assetID, pqProgram)) { + if (error) + *error = tr("Enter a canonical PQ-only asset destination after PQ asset activation."); + return false; + } + if (!model->getWallet()->IsPQOnlyAssetID(assetID) || + !model->getAssetDestinations().contains(descriptor)) { + if (error) + *error = tr("Asset change must use a protected destination owned by this wallet."); + return false; + } + + ctrl.assetDestChange = assetID; + ctrl.pqAssetChangeProgram = pqProgram; + return true; +} + // Coin Control: update labels void AssetsDialog::assetControlUpdateLabels() { diff --git a/src/qt/assetsdialog.h b/src/qt/assetsdialog.h index d646d2df0c..1157f4dcbd 100644 --- a/src/qt/assetsdialog.h +++ b/src/qt/assetsdialog.h @@ -40,6 +40,7 @@ class AssetsDialog : public QDialog void setupAssetControlFrame(const PlatformStyle *platformStyle); void setupScrollView(const PlatformStyle *platformStyle); void setupFeeControl(const PlatformStyle *platformStyle); + bool applyProtectedAssetChange(CCoinControl& ctrl, QString* error = nullptr) const; /** Set up the tab chain manually, as Qt messes up the tab chain by default in some cases (issue https://bugreports.qt-project.org/browse/QTBUG-10907). */ @@ -93,6 +94,8 @@ private Q_SLOTS: void assetControlButtonClicked(); void assetControlChangeChecked(int); void assetControlChangeEdited(const QString &); + void chooseProtectedAssetChange(); + void updateProtectedAssetChangeAvailability(); void assetControlUpdateLabels(); void assetControlClipboardQuantity(); void assetControlClipboardAmount(); diff --git a/src/qt/forms/assetsdialog.ui b/src/qt/forms/assetsdialog.ui index 291e1cc2b3..38e4a97b15 100644 --- a/src/qt/forms/assetsdialog.ui +++ b/src/qt/forms/assetsdialog.ui @@ -594,6 +594,39 @@ + + + + + + + For a partial transfer of historical assets after PQ asset activation, choose a wallet-owned protected destination for the remaining assets. + + + Protected asset change: + + + + + + + + 0 + 0 + + + + + + + + Choose... + + + + + + diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 849bea2550..e7005a64a2 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -13,8 +13,10 @@ #include "test/test_raven.h" #include "validation.h" #include "wallet/wallet.h" +#include "wallet/coincontrol.h" #include "qt/overviewpage.h" #include "qt/receivecoinsdialog.h" +#include "qt/assetsdialog.h" #include "qt/recentrequeststablemodel.h" #include "qt/receiverequestdialog.h" @@ -263,3 +265,30 @@ void WalletTests::walletTests() { TestGUI(); } + +void WalletTests::protectedAssetChangeField() +{ + TestingSetup test; + std::unique_ptr platformStyle(PlatformStyle::instantiate("other")); + AssetsDialog dialog(platformStyle.get()); + QValidatedLineEdit* field = dialog.findChild("lineEditProtectedAssetChange"); + QLabel* label = dialog.findChild("labelProtectedAssetChange"); + QVERIFY2(field != nullptr, "The asset transfer dialog must expose protected asset change"); + QVERIFY(label != nullptr); + QVERIFY(!field->isHidden()); + QVERIFY(!field->isEnabled()); + + CCoinControl ctrl; + ctrl.assetDestChange = CKeyID(); + ctrl.pqAssetChangeProgram = uint256S("01"); + QVERIFY(dialog.applyProtectedAssetChange(ctrl)); + QVERIFY(!IsValidDestination(ctrl.assetDestChange)); + QVERIFY(!ctrl.pqAssetChangeProgram); + + field->setText("not-a-protected-asset-destination"); + QString error; + QVERIFY(!dialog.applyProtectedAssetChange(ctrl, &error)); + QVERIFY(error.contains("canonical PQ-only")); + QVERIFY(!IsValidDestination(ctrl.assetDestChange)); + QVERIFY(!ctrl.pqAssetChangeProgram); +} diff --git a/src/qt/test/wallettests.h b/src/qt/test/wallettests.h index 673cf5075d..10b08da52e 100644 --- a/src/qt/test/wallettests.h +++ b/src/qt/test/wallettests.h @@ -10,6 +10,7 @@ class WalletTests : public QObject private Q_SLOTS: void walletTests(); + void protectedAssetChangeField(); }; #endif // RAVEN_QT_TEST_WALLETTESTS_H From 82da3be7aea0c8a9d212786cf6005530f08e8a83 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:59:54 +0200 Subject: [PATCH 174/195] [FINDING-155] Record GUI migration field qualification --- ...P-0025-v4.8-security-remediation-register.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index c8e7daeff6..2ce580f200 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -8728,14 +8728,25 @@ does not close those release conditions. - **Regression required:** Show the partial GUI-equivalent path fails without change, then show whole-UTXO migration and RPC partial migration succeed with canonical tags. Confirm later spends require a funded anchor. -- **Remediation commit:** No GUI change; `193d26483` documents whole-UTXO - migration and the explicit asset-change RPC path. +- **Remediation commits:** `193d26483` documents whole-UTXO migration and the + explicit asset-change RPC path; `12200477b` adds a protected change field + and wallet-owned PQ-only descriptor chooser to the transfer dialog. - **Verification:** `588ef7ec8` proves that a partial legacy spend without protected change fails, while whole-UTXO asset and owner-token migration and explicit PQ change succeed. A later protected spend needs and accepts a funded matching anchor. The focused source RPC test passed locally and in both required security jobs at `9151946ad`. -- **Final status:** OPEN; documented fail-safe GUI migration limitation. +- **Further verification:** The focused pinned Qt 5.12.11 regression failed + before the GUI field existed and passed after it was added. It checks that + empty or invalid entries leave no classical asset-change destination in + coin control. The dialog accepts a canonical descriptor only if it belongs + to the selected wallet. The pinned Qt GUI binary builds and `make check -j4` + passes. The upstream Qt test runner still exits before its test cases; + the focused test used a temporary local runner and host Qt compatibility + shim. A full active-bit-13 GUI transfer and native Windows/macOS manual GUI + checks remain release qualification work under FINDING-067. +- **Final status:** FIXED locally for the GUI input path; final exact-SHA CI + and manual GUI qualification remain due under FINDING-067. ### FINDING-156: macOS release runner disconnects during the artifact build From 84a5e350b0d721e3883084f6f38814804909a115 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:03:07 +0200 Subject: [PATCH 175/195] [FINDING-063] Pin deterministic ML-DSA signature vector --- src/test/pqkey_hardening_tests.cpp | 141 +++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/src/test/pqkey_hardening_tests.cpp b/src/test/pqkey_hardening_tests.cpp index 1d1ea81a8d..92f52b3f7c 100644 --- a/src/test/pqkey_hardening_tests.cpp +++ b/src/test/pqkey_hardening_tests.cpp @@ -35,6 +35,15 @@ #include #include +// The pinned liboqs 0.16.0 backend exposes the FIPS 204 internal signing API +// for deterministic known-answer testing. Production signing stays randomized. +extern "C" int PQCP_MLDSA_NATIVE_MLDSA44_C_signature_internal( + unsigned char* signature, size_t* signatureLength, + const unsigned char* message, size_t messageLength, + const unsigned char* prefix, size_t prefixLength, + const unsigned char* randomness, const unsigned char* secretKey, + int externalMu); + BOOST_FIXTURE_TEST_SUITE(pqkey_hardening_tests, BasicTestingSetup) namespace { @@ -349,6 +358,138 @@ BOOST_AUTO_TEST_CASE(mldsa_acvp_keygen_kat) memory_cleanse(secretKey.data(), secretKey.size()); } +BOOST_AUTO_TEST_CASE(mldsa_acvp_deterministic_signature_bytes_kat) +{ + // FIPS 204 ML-DSA-sigGen, liboqs 0.12.0 ACVP internalProjection.json, + // test group 1, case 4. This vector fixes every byte of the output. + // The internal API takes an empty prefix and zero randomness for this case. + std::vector secretKey = ParseHex( + "abcec4a46e695fc6ebe64a191389f0d0ae180f911d5b824f4ed9111728ff4f9493ef3a7512dacf766d576898d33c4c8f" + "4001b777ee5ec2e2dc1a8e3e181b43418af45100b92a3835d02b9892e609b2aa8c6af7661ce0bc8362ae0da172a79e84" + "ff4cdad8607e4924ff41db6ec28dccd09b8d1f5657ba17c848babc71bb242a50c44069a0200101914d130822c1142884" + "36508b0422d83004e23848032532ca902509a0009a848440004cc3304d04c0601cc84889340950b68918c81103410d63" + "1870db444ae4224d8b96491b0224c2802904365290222d424626e0060613b12de1187203b18442228144027159186c22" + "092c012751083160dc48061045089ac444601609240224da006623945149c200182232ca462a0ba16d22b2215028700a" + "c108d1828c00b24014083221b5810c4501240225e44291984622c90826532650dba8495b226294b049603050242382dc" + "0429d2260254262cc3a868001546089168cc067184b86c518285da221110402494149044b8102220299ab40190824d52" + "3050229668101072913411ccc625d2880d1b1951c3888c222509da20322003209106028c188000b02d92421111b92da1" + "0890a228700b4464a1b01153966800b925423860884688630800c3c2910ba9511a1902c2b229532052d9906dd9c08019" + "2400249589e004914180600022440908012435624c48864bb46123c900588869c1001251248960b64844322513c5318b" + "909118a8691a850449b800a3b02598a2052293455342701b3722590860911248939221d9024e20426014394109373183" + "46851a836d192751c93686111749a0b4649bb285ccc049c0c4611a066203c304a032248cb861a34408cb485248486624" + "4610a1068909280c01810c028531232102e4442c0a988c19c54192187223b168d844862295405824408c322222c27110" + "170a910668d438529b025212086458185208078dd388240c1092c2c60d549640031989093530988425d104524c146e62" + "a06c5aa004cc126ac3284490268a43468021454460b249a390681a4745d2965188220e6032249aa6280a034a9cb6888b" + "180cc1021012b70d0340480cb3444c0010011549e2b23094064504378441b2401aa3884aa885112229d2402550b0048c" + "2621180872912851481842499020c9126d1a1289a4404c4a82710301228920449c48624006920813819ca670d0c808d9" + "38721a118a1bb589111150c3c6695386851a0204d2426d19116419a38152c861403a93cb8575520d2a3a7317cae1963e" + "2705b7596c8e5dbf0daeaf8755df38a5df16297cfc84097b480d9729e4cc62170739a1a8a2057ea7fefb06275344adb6" + "934e1c2da7e7f3e831fa35e6a4b8d8def435235ce957df5fa1d842962711443beade91070833c84264b45e2380b09420" + "2e079a0a7c6058a54e6f552f202760230f6d95f5ea873709be4d7603ac010cfbadafe229cada1f2bc717f877856d8b93" + "0d0e215c4ba2212d66e21a2d1f09b1f1a9bc8c298cfd65b318fe91847279f204201203e0922e82bd298d9bf18b8fbcf7" + "2070f7c7c51d5480e60674341cf263fd179862f37d5665fe35ed0b2a86b7115c90093f5785309cd56c48bbc50570a0c2" + "d066bd0eccd3c86e2a6c8b098afd9c0e235ceb920d58f0b913bfb633bfe21bb1668d9c45638f5ce9650caaa83db2d9b4" + "b24b1f518b19226abda06239698a90f30a50af69ae1d20fa00e3d88ff6f2c2466e45a39a1c946fb695888383cb6a59a7" + "c8395082134a82dea3da7fe6d9e6f76c7e86a50ca04990c70dd5f9af062ed14cc661f453bf309da08056e19f2f7b34a1" + "5235230c15ec6859d7dcf0ed892ddff4e5096b36b406a10cb35aa81f72827c5982e3c5bfbb989e062cb4a7f0f76b008a" + "b8ca5ec1cebdabcaa1e97809b44c5f49281415337978184811adb8131d2dfa2477d27532e92409493d46c597a6886250" + "593fd58d305d0760ce8f772337d42b0f7dbfc483a941e8cdf32ce3e97309c3c404b6e4101678f123438853fc8a71c835" + "d1ad0c7712460dbe83c1abc6bb0834c0271a6627e7dcb93efc25f78417bbc801488e5a051455343757f6bfaf923867c4" + "5ed5bf37304b11e012ee63a3b8d84dce7a15d5ab940d87fe1181eaba3c97bca702f5de4df74848a99d2b1f34fe2b0363" + "3d6aac900a09c278556172df5d9cde361a8acd779465cbaf50de5c8f4ca0d15df3f74347c6ddf7d9b3e3e5e197bfe0aa" + "170949fb42b78364a72b1b106156ec09a6e4ec72f3f814781d3ce7b7aa2b02e49caa25ac36daade0fd570a61589553a0" + "cae582ba2894c82c0380a713b0b74924e006a6b341f21ae2aaec2016d2687f1ab696337f5a268b3b6f3730f507d6122d" + "c92cab36107e864bb3edeea6fc1c5309a9b51582cdfcc1a899929ad7cdbecbcbf9d38121d58c3b3e6d9be001b117e4a7" + "f762816174b761efcf291a1cc5da354029962eb8b0f6166a9c9eaf26921d1777e621c50c41614300605b1ee2a0cc41bc" + "666ce90a15733c69a82451fd41f23efaa73a2482c4e3d476ccbbfb59b25140ffa0c1ceceabd3b036f2611c83aa834e6c" + "fe03963941bdad4aefb11d01ec43293bcd22ecb8784ef5ce2a6042f200f1b9d6c595ef920c3cacd1c1c3cb61b6b46454" + "a3b28a472ae0203038a5602cb9b001620c98bc09bc2db5621c8db085d88561058aea691afd199c6d4bb1511137ed2800" + "722a81670b44fac51dfa6683675bf34c52f6eb7eba35d22c907a207ae5ce6c3c40ab0a26b88dae777e10b4fc33ab38c3" + "08ca2532032a7f306e9eca723b58119c3be662817a1eab6069fa05c3b0ed31060d5794121a83fbb152c7fc05bb753c9d" + "29bc329e745d7c7d493372c26c0336aab37b884fb41741b344ee4d247d6b5d049e5e322cc97ec6647ec7551824c6aa9c" + "d249f49fe1652ecfd01c3e7eb026fdce7320d21ec9e4d460e50d6440c7364a15ae3c107ce8ee1e8a33ebc9d2b5585b8f" + "69771f687eb6940c21f45750079d68d3dd6ce1cd7ca8d91a64d093a25a96628169b675cda9f14fada4af3d11b6524465" + "b89dd4ef93a9a159f8df2a134fea301110eb77e1eca51166d26cb036bf92f1655167bd32d12be04a91ff0b3c52c69de3" + "76856fab9b4e14524e5858717ecbd0865719bf1ddeddf8cc141396f9f0b4ed38b0caada08b64451ad8bd38557660cfef" + "46ec0059b4aea6a7534a3db767c537e60210a1af84ec939413fd7ebbf14fe96d6ee82a0c632edc63715c0c6654aa4fe2" + "98f43ed5b47af7350c32c8d7f696f9a96b81e9832f486a66d9b304a6531139561fe5a967061bdffa4793ea986c3a2693" + "c21dad4428fe98f168eb928fcbeb8fe0a611049c1f430ccd80f9181d276afedaea40261fe1b038f5677ad507eb48b976" + "8964baec928197ac26acb1a89cddaf51c4336b6f49985c13926e76aa7d69f5f0844f23e7b2b977565587718903b39173" + "f7f18af84264370bd61020f2a76ed281687419e334443159bf6a21533f41e030654f6876aabb21025b6d2312304ff8be" + "ffb7ad2e225bf79f1b6f8c33aa90d9dc18b369846fa06548e72efb2ec4fd6bd833f1872df9659f62af040345cb5b8399" + "a4836f7f5a9f920f0484009c1f6871d2"); + const std::vector message = ParseHex( + "22aa98c685e1552b525b4302c943037f668279c224b6270dcaf2b06c4f4ab1254c48de253829fe6dffa9cb6bb294f054" + "711bae3fbacfb900cfd1f0844e55d51ec6f697b998759b14c13392ddb6f7deba77ffc22468781ce402"); + const std::vector expectedSignature = ParseHex( + "0313b5dd1e344bf95eb5d825837f570922337633494f8f2ee3618aeb906df766f5addccb8ec1c5cc51aae0b9fad876fc" + "ff5d6814357e8c63fe59fd72db3ce4d1650660a0cf64339d8b7c6ddb7a7a3c8ec6c88f2f0f70b377d9d5e21f0d40e175" + "73f07c94cdf844cb6794ce2cb8e4a11254ff0012d562dcf4d9fbbd18348d13e1b69395c69dccbf618d7a38ea6a51b9f1" + "f8cc70cd36d087682a558a1ea9bf9de6ace5b350b6eb977f435cc46fd43f155b29822d68d1ca9603ba43f1db28bf4d3c" + "e284523bb440d1e2c124cf92dc74f19b74cbc177de9b08377b0e363401d23e97ae967e13ab77e9b7c7488e49b45e8351" + "36ec2874a24cc79059de5b0a67c842f2e09b37b3039dc186427f760ce60790a9fb58aa388880ec720a783efeb4e11abd" + "817a7ea884e5efc279ad9e8ac2c58a41020c7468a749933348ebf78a0f0597b29b00a126050fa40f532b0f4411b733ba" + "a5c80ca1b52828cb236268989ea2641431a359e6595f49b4feabfa85ed4ba85443e2d7b82dc7523ff627d336da155189" + "7438100c20f0184a963d0b9af7f7cc9fd64a2878d8fc5ade2d0d928445eebed286f2b280af573ee62e70f698ba0c1438" + "1a5b4e001c53c49a77e9f181ec65b49e6cd8bbe30b5f309686f7dc80176f654771df17fdf136d6c05498cc00c476e716" + "08a39cb29a19a986297cb9c1e3ffb4ba08ed42ad12908287f018d49a0e1ae89a9bb3f932df98ca91e691ab5f3bfadea9" + "a233350a9ede63cfc45427cad109468fce4abf7fdb83c6ec83865134380d3ffca4d94c5604dd9daffac204c14009fe06" + "311b81310940c43fbfcf049f99f5deea649a333f6a3ae6232ec7cde62c95338c23085d776b9b8f4454e03bd0fb280466" + "18c33a7ccb409be7bcbc0906b5a9424b41998a3a1e65e5ec667a14339e3bb44354047a2868d64b78fe5faedddffde497" + "368c3cc130d0821c1deb2c43119195c6c4cff1d5a778965ff97685dd93775837ea0852284c670dd418dc9fdc9c44db7e" + "da60e58030d8fbdf86d48b98230e6ad19e97d21d7358510e20ee374b8486341302efa590b96c1dc253226a12cafdb3b4" + "a31ee2270f764b74f17bf9c5681b6578777739ff767d4e094ce80ad9804261767097bdb163430332c428cb3ccd9aa881" + "316b51556c718a95816797cd5c8a2a640b85dfda520d1a0129f0e9de772724ee8b74b4df6793ebc0a801b6f77a2647ca" + "2a5f564d336bc23ecf591327a8dc4214953d99ea3a9ed9943f07809b4c36a8bff093721949a2aef2cdd9687934849ca3" + "5ba1f38743ac663e48b39a2a32122b6951cf6f1be17fa96628d8dee8760cf7f2699045df30484722601e7a45deb277cb" + "eb749f5cb4383c8643241158ef13d8723b9b00c98f8bc68c5f42dfc4a1b8d95f4652aef67798a8b3c0c2ad40cb7a2b88" + "a23650147e32cb605324a99370bdb640edac9ce57ec22689dd45d1922811584b01685615520dbb0ffbb028e43b77b54a" + "f4a3ebc6b9f857e9100fd9c64028867999bb9c7ff05d37704d41c94cd63dfd1a7673f817931a4f2c42ce572b24fb5e77" + "9897fa4e6e2191524783c710a099d1a6bb9eae47e849187432a2039bbe1d37d0e7c797907f99bdb8e7fb33c3b2fc9e06" + "708312beeef2935d443f29b646fb5ad52c99eb71559571781a4c86250d262329ff3daaca18a5e7b77c3ce593cf3a92b1" + "2b9ae86ca3af05ebbbd75b6befc368982d96f2e5fbc128b6a8d65fdb0b0f7859689f0b6f0a3d1efcbf1915dc61c7feeb" + "d077f71122e77775b4279f4f57aba521be4682daa5b836b5b4d44cdb726dff3ac9cdda9f0d1cf4e79bc9e90dff7562cf" + "206e756c38e708bd30820a14c595288730f42c8ed99a7c1f2fa6ca9759cd31b8f28fd35b9e0980acc3432f462cd9cb4c" + "594ab63fd250ddd4877269056677ea7e3b0aacd4757aeba398688423015d952d0cf67e5027ba4b6b376f6380b5823717" + "d64513efb35015fbd20fb2284e1e340c9b94f87cfba24310325d7597ab34baca6753215ab994390c2e5a82eeb06f3382" + "93e72c5d0ff786666ff2ee2d429461f1112551ad0928844d7698d3ab0c4054881a623525d728e514e12550aab227389e" + "5f0c2341a6aa34bab5472bc9e465dc24e9910175b2b968f2e7883cd22cc07b3d203cfdcdd877cbb28368dbe668f561e3" + "fa1c5d3391d4a408a71e0bf0c32c1fd6494def7678e58b829ab428509c33f2491725959485928b2e7fa96ec7aff12ba3" + "196087d3c83dc242e5ec95bf95c8581693fc0b744e758e1e85959ab63e9b4b0a4547be5abd7d29bf9da2d192b4bca491" + "d1dc856ec80ad6c3c738fb775f95a217d76093852aee0cc203db1316c6253bb75ce2d92906b47a5b733e4a8b0c28b128" + "3419249b5a05f6302ce110f85790eb46ded7109b0dac58a4d25255b6950f3b2c3421140e4825b6ac968091ea7ae9790b" + "c8d03f3f8f4c1bf1006e821e5da2c65c542183f4230cec934acb81fea94d959fb6a21c7ab20652c9b247edef72674fe9" + "15858795a9a00564f602136c364a6d9638cb1bbd8a705b90429d13a19de93520229282d5122ee112a32a68e23577e358" + "fb28ec45478d05f64513a9e814a5f1fc7cde4d03ec51ed41d29abbc0f60292e2f7d0aa4490c38b583329e2d5a1e61c73" + "d6033cb73a7a4f75abbb29a31e52cd1eedc3dc4f0b9152fae6b29dd1f9739456ac5b1890b3bb00415027c634f1e6374f" + "310c95e680086862b8bdea0833997746814fc602f97999a2309d4cc05362303f72bbe2eead04d566579a00a8265e238f" + "c578fdb2a63c57250b5714f2c6485bc6247752853f75abf066637645a5a6ac0e85459ec2e24d704ed7da4ba0fd23748d" + "1d853bcf84d4d00eb4b6da6d036b8f29797882ef5400f2f7d6efcda0418cbc5752ae5431b59d2d518fd752467b7ee13f" + "699cee1581aa1a305b2adaca12647fa8ff00b427f28aa33fc094a374521437e3b4a109113e1ad8ad23e2aebd69edeced" + "dfef0b5aa0a484ae32bfb2b05f8f7f65077f5b35c99b61ec28060437d9cb982afa28ffe8ad4c02bfbfb071ca8e9eea15" + "cceed97d47be2313e3c344793d40b45f709c8a2c639c09662a81f117839ab62135b6715fd57f7b0368c6ee1e3234655e" + "ae94213d24d92989a46cbd5c7ef116243a085462c5454949d41d2d570ac810b54c727fa5e187d0d9f7fa7db3682d9946" + "0f5077aed0a90d513c0d833e5f5a91a5a26cf47ad134c074d658ac0164acbaae8fae161e00c7f8b043fff46fc5c900c9" + "ed14c44c98addedcd5f3bb7038868f8460ee12472de2a24bcd0a611d9e039d60000d131d242e3c436e808fcdd3e2f556" + "67698d91929da3a4f0f72d4b58636c6d9cc9d9eff0141c233b3f4a53595f696b6f848ac2f7fe00000000000000000000" + "000000000000000000000000000000000f1a2536"); + BOOST_REQUIRE_EQUAL(secretKey.size(), mldsa::SECRETKEY_BYTES); + BOOST_REQUIRE_EQUAL(message.size(), 89U); + BOOST_REQUIRE_EQUAL(expectedSignature.size(), mldsa::SIGNATURE_BYTES); + + const std::array zeroRandomness{}; + std::array signature{}; + size_t signatureLength = 0; + BOOST_REQUIRE_EQUAL(PQCP_MLDSA_NATIVE_MLDSA44_C_signature_internal( + signature.data(), &signatureLength, message.data(), message.size(), + nullptr, 0, zeroRandomness.data(), secretKey.data(), 0), 0); + BOOST_REQUIRE_EQUAL(signatureLength, mldsa::SIGNATURE_BYTES); + BOOST_CHECK(std::equal(signature.begin(), signature.end(), + expectedSignature.begin())); + memory_cleanse(secretKey.data(), secretKey.size()); + memory_cleanse(signature.data(), signature.size()); +} + BOOST_AUTO_TEST_CASE(secret_public_key_binding) { CPQKey key1; From b3803bbc09cb8e6ad53049c9091bb49576cf1c00 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:58:04 +0200 Subject: [PATCH 176/195] [FINDING-100] Persist reward wallet and payout intent across restart --- .github/workflows/build-raven.yml | 2 +- .github/workflows/rip25-v48-final-gate.yml | 2 +- src/assets/rewards.cpp | 144 +++++++++++++++++---- src/assets/rewards.h | 36 +++++- src/assets/snapshotrequestdb.cpp | 52 +++++++- src/assets/snapshotrequestdb.h | 8 +- src/init.cpp | 4 + test/functional/feature_rewards_restart.py | 89 +++++++++++++ test/functional/test_runner.py | 1 + 9 files changed, 299 insertions(+), 39 deletions(-) create mode 100644 test/functional/feature_rewards_restart.py diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index fcfd813c81..451f1319d2 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -90,7 +90,7 @@ jobs: run: | python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py wallet_asset_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ + wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py rpc_assettransfer.py feature_chainstate_ahead.py \ feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ diff --git a/.github/workflows/rip25-v48-final-gate.yml b/.github/workflows/rip25-v48-final-gate.yml index 60e737e1f2..05b6034b8d 100644 --- a/.github/workflows/rip25-v48-final-gate.yml +++ b/.github/workflows/rip25-v48-final-gate.yml @@ -104,7 +104,7 @@ jobs: run: | python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py wallet_asset_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ + wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py rpc_assettransfer.py feature_chainstate_ahead.py \ feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ diff --git a/src/assets/rewards.cpp b/src/assets/rewards.cpp index afcf897c9c..2a5678140d 100644 --- a/src/assets/rewards.cpp +++ b/src/assets/rewards.cpp @@ -21,7 +21,7 @@ std::map mapRewardSnapshots; #ifdef ENABLE_WALLET -static std::map mapRewardWalletNames; +std::map mapRewardWalletBindings; #endif uint256 CRewardSnapshot::GetHash() const @@ -33,18 +33,35 @@ uint256 CRewardSnapshot::GetHash() const bool AddDistributeRewardSnapshot(CWallet* p_wallet, CRewardSnapshot& p_rewardSnapshot) { AssertLockHeld(cs_main); - if (!p_wallet || !pDistributeSnapshotDb) + if (!p_wallet || !pDistributeSnapshotDb || !pAssetSnapshotDb || + p_rewardSnapshot.nHeight > static_cast(chainActive.Height()) || + !chainActive[p_rewardSnapshot.nHeight]) return false; auto hash = p_rewardSnapshot.GetHash(); - CRewardSnapshot temp; - if (pDistributeSnapshotDb->RetrieveDistributeSnapshotRequest(hash, temp)) { + if (pDistributeSnapshotDb->ContainsDistributeSnapshotRequest(hash)) { return false; } - if (!pDistributeSnapshotDb->AddDistributeSnapshot(hash, p_rewardSnapshot)) + CAssetSnapshotDBEntry snapshotEntry; + if (!pAssetSnapshotDb->RetrieveOwnershipSnapshot(p_rewardSnapshot.strOwnershipAsset, + p_rewardSnapshot.nHeight, snapshotEntry)) + return false; + + CPubKey walletKey; + if (!p_wallet->GetKeyFromPool(walletKey) || !walletKey.IsValid()) + return false; + + CRewardWalletBinding binding; + binding.walletName = p_wallet->GetName(); + binding.walletKeyId = walletKey.GetID(); + binding.genesisHash = GetParams().GenesisBlock().GetHash(); + binding.snapshotBlockHash = chainActive[p_rewardSnapshot.nHeight]->GetBlockHash(); + binding.snapshotContentHash = SerializeHash(snapshotEntry, SER_GETHASH); + if (binding.walletName.empty() || !p_wallet->HaveKey(binding.walletKeyId) || + !pDistributeSnapshotDb->AddDistributeSnapshot(hash, p_rewardSnapshot, binding)) return false; - mapRewardWalletNames[hash] = p_wallet->GetName(); + mapRewardWalletBindings[hash] = binding; mapRewardSnapshots[hash] = p_rewardSnapshot; return true; } @@ -228,26 +245,43 @@ void DistributeRewardSnapshot(CWallet * p_wallet, const CRewardSnapshot& p_rewar LogPrint(BCLog::REWARDS, "Failed distribution: Tx conflict with another tx: %s: number of block back %d!\n", txid.GetHex(), depth); return; } else if (depth == 0) { - LogPrint(BCLog::REWARDS, "Tx is in the mempool! %s\n", txid.GetHex()); + if (!walletTx->InMempool()) { + LogPrint(BCLog::REWARDS, "Reward transaction %s is unconfirmed and absent from the mempool; refusing automatic retry\n", txid.GetHex()); + mapRewardSnapshots[p_rewardSnapshot.GetHash()].nStatus = CRewardSnapshot::STUCK_TX; + pDistributeSnapshotDb->OverrideDistributeSnapshot( + p_rewardSnapshot.GetHash(), mapRewardSnapshots.at(p_rewardSnapshot.GetHash())); + return; + } + LogPrint(BCLog::REWARDS, "Reward transaction remains in the mempool: %s\n", txid.GetHex()); return; } else if (depth > 0) { LogPrint(BCLog::REWARDS, "Tx is in a block %s!\n", txid.GetHex()); continue; } } else { - LogPrint(BCLog::REWARDS, "Failed to get wallet Tx: %s\n", txid.GetHex()); + LogPrint(BCLog::REWARDS, "Reward batch %d has durable intent %s but no wallet transaction; refusing automatic retry\n", + i, txid.GetHex()); + mapRewardSnapshots[p_rewardSnapshot.GetHash()].nStatus = CRewardSnapshot::STUCK_TX; + pDistributeSnapshotDb->OverrideDistributeSnapshot( + p_rewardSnapshot.GetHash(), mapRewardSnapshots.at(p_rewardSnapshot.GetHash())); + return; } + } else if (pDistributeSnapshotDb->ContainsDistributeTransaction(p_rewardSnapshot.GetHash(), i)) { + LogPrint(BCLog::REWARDS, "Reward batch %d has an unreadable durable intent; refusing automatic retry\n", i); + mapRewardSnapshots[p_rewardSnapshot.GetHash()].nStatus = CRewardSnapshot::STUCK_TX; + pDistributeSnapshotDb->OverrideDistributeSnapshot( + p_rewardSnapshot.GetHash(), mapRewardSnapshots.at(p_rewardSnapshot.GetHash())); + return; } else { LogPrint(BCLog::REWARDS, "Didn't find transaction in database creating new transaction: %s %s %d %d\n", p_rewardSnapshot.strOwnershipAsset, p_rewardSnapshot.strDistributionAsset, p_rewardSnapshot.nDistributionAmount, i); // Create a new transaction and database it int start = i * MAX_PAYMENTS_PER_TRANSACTION; uint256 retTxid; std::string change = ""; - if (!BuildTransaction(p_wallet, p_rewardSnapshot, paymentDetails, start, change, retTxid)) { + if (!BuildTransaction(p_wallet, p_rewardSnapshot, paymentDetails, start, i, change, retTxid)) { LogPrint(BCLog::REWARDS, "Failed to build Tx: distribute: %s, amount: %d\n", p_rewardSnapshot.strDistributionAsset, p_rewardSnapshot.nDistributionAmount); return; } - pDistributeSnapshotDb->AddDistributeTransaction(p_rewardSnapshot.GetHash(), i, retTxid); } } } @@ -255,6 +289,7 @@ void DistributeRewardSnapshot(CWallet * p_wallet, const CRewardSnapshot& p_rewar bool BuildTransaction( CWallet * const p_walletPtr, const CRewardSnapshot& p_rewardSnapshot, const std::vector & p_pendingPayments,const int& start, + const int& batchNumber, std::string& change_address, uint256& retTxid) { int expectedCount = 0; @@ -277,6 +312,29 @@ bool BuildTransaction( CAmount nFeeRequired = 0; CAmount totalPaymentAmt = 0; + // Store the batch identity before any wallet side effect. A crash after + // this write can stop a retry, but cannot silently pay the batch twice. + const auto commitRewardTransaction = [&]() { + const uint256 txid = txnPtr->GetHash(); + if (!pDistributeSnapshotDb->AddDistributeTransaction(rewardSnapshotHash, batchNumber, txid)) { + LogPrint(BCLog::REWARDS, "Failed to persist reward batch intent before transaction commit\n"); + return false; + } + + if (p_walletPtr->CommitTransaction(*txnPtr.get(), *reserveKeyPtr.get(), g_connman.get(), state)) + return true; + + // Wallet abandonment and the rewards database are separate stores. + // A failed wallet database write can revive this transaction after a + // crash, so never remove its durable intent for an automatic retry. + mapRewardSnapshots[rewardSnapshotHash].nStatus = CRewardSnapshot::STUCK_TX; + pDistributeSnapshotDb->OverrideDistributeSnapshot(rewardSnapshotHash, + mapRewardSnapshots.at(rewardSnapshotHash)); + LogPrint(BCLog::REWARDS, "Reward commit failed for %s: %s; automatic retry blocked\n", + txid.GetHex(), state.GetRejectReason()); + return false; + }; + // Handle payouts using RVN differently from those using an asset if (p_rewardSnapshot.strDistributionAsset == "RVN") { @@ -334,10 +392,7 @@ bool BuildTransaction( return false; } - if (!p_walletPtr->CommitTransaction(*txnPtr.get(), *reserveKeyPtr.get(), g_connman.get(), state)) { - mapRewardSnapshots[rewardSnapshotHash].nStatus = CRewardSnapshot::FAILED_COMMIT_TRANSACTION; - pDistributeSnapshotDb->OverrideDistributeSnapshot(rewardSnapshotHash, mapRewardSnapshots.at(rewardSnapshotHash)); - LogPrint(BCLog::REWARDS, "%s\n", state.GetRejectReason()); + if (!commitRewardTransaction()) { return false; } } @@ -377,10 +432,7 @@ bool BuildTransaction( return false; } - if (!p_walletPtr->CommitTransaction(*txnPtr.get(), *reserveKeyPtr.get(), g_connman.get(), state)) { - mapRewardSnapshots[rewardSnapshotHash].nStatus = CRewardSnapshot::FAILED_COMMIT_TRANSACTION; - pDistributeSnapshotDb->OverrideDistributeSnapshot(rewardSnapshotHash, mapRewardSnapshots.at(rewardSnapshotHash)); - LogPrint(BCLog::REWARDS, "%s\n", state.GetRejectReason()); + if (!commitRewardTransaction()) { return false; } } @@ -395,25 +447,61 @@ void CheckRewardDistributions() { AssertLockHeld(cs_main); for (const auto& item : mapRewardSnapshots) { - const auto owner = mapRewardWalletNames.find(item.first); - if (owner == mapRewardWalletNames.end()) { + if (item.second.nStatus == CRewardSnapshot::STUCK_TX || + item.second.nStatus == CRewardSnapshot::INVALIDATED_SNAPSHOT) + continue; + const auto owner = mapRewardWalletBindings.find(item.first); + if (owner == mapRewardWalletBindings.end()) { LogPrint(BCLog::REWARDS, "Skipping reward %s without a wallet binding\n", item.first.GetHex()); continue; } + const CRewardWalletBinding& binding = owner->second; + if (binding.nVersion != CRewardWalletBinding::CURRENT_VERSION || + binding.walletName.empty() || binding.walletKeyId.IsNull() || + binding.genesisHash != GetParams().GenesisBlock().GetHash() || + item.second.nHeight > static_cast(chainActive.Height()) || + !chainActive[item.second.nHeight]) { + LogPrint(BCLog::REWARDS, "Skipping reward %s with invalid wallet or chain binding\n", + item.first.GetHex()); + continue; + } + + if (binding.snapshotBlockHash != chainActive[item.second.nHeight]->GetBlockHash()) { + LogPrint(BCLog::REWARDS, "Reward %s snapshot block was replaced; automatic retry blocked\n", + item.first.GetHex()); + mapRewardSnapshots[item.first].nStatus = CRewardSnapshot::INVALIDATED_SNAPSHOT; + pDistributeSnapshotDb->OverrideDistributeSnapshot(item.first, mapRewardSnapshots.at(item.first)); + continue; + } + + CAssetSnapshotDBEntry snapshotEntry; + if (!pAssetSnapshotDb || + !pAssetSnapshotDb->RetrieveOwnershipSnapshot(item.second.strOwnershipAsset, + item.second.nHeight, snapshotEntry)) { + LogPrint(BCLog::REWARDS, "Skipping reward %s with changed ownership snapshot\n", + item.first.GetHex()); + continue; + } + if (binding.snapshotContentHash != SerializeHash(snapshotEntry, SER_GETHASH)) { + LogPrint(BCLog::REWARDS, "Reward %s snapshot contents changed; automatic retry blocked\n", + item.first.GetHex()); + mapRewardSnapshots[item.first].nStatus = CRewardSnapshot::INVALIDATED_SNAPSHOT; + pDistributeSnapshotDb->OverrideDistributeSnapshot(item.first, mapRewardSnapshots.at(item.first)); + continue; + } + CWallet* wallet = nullptr; - bool ambiguous = false; + int identityMatches = 0; for (const CWalletRef& loaded : vpwallets) { - if (loaded && loaded->GetName() == owner->second) { - if (wallet) { - ambiguous = true; - break; - } - wallet = loaded; + if (loaded && loaded->HaveKey(binding.walletKeyId)) { + ++identityMatches; + if (loaded->GetName() == binding.walletName) + wallet = loaded; } } - if (!wallet || ambiguous) { + if (!wallet || identityMatches != 1) { LogPrint(BCLog::REWARDS, "Skipping reward %s without a unique loaded wallet\n", item.first.GetHex()); continue; diff --git a/src/assets/rewards.h b/src/assets/rewards.h index 07076cddd7..72849fbe4b 100644 --- a/src/assets/rewards.h +++ b/src/assets/rewards.h @@ -6,6 +6,7 @@ #define RAVENCOIN_REWARDS_H #include "amount.h" +#include "pubkey.h" #include "tinyformat.h" #include "assettypes.h" @@ -90,7 +91,8 @@ class CRewardSnapshot { STUCK_TX = 6, NETWORK_ERROR = 7, FAILED_CREATE_TRANSACTION = 8, - FAILED_COMMIT_TRANSACTION = 9 + FAILED_COMMIT_TRANSACTION = 9, + INVALIDATED_SNAPSHOT = 10 }; std::string strOwnershipAsset; @@ -141,6 +143,37 @@ class CRewardSnapshot { } }; +// Persist separately from CRewardSnapshot so legacy requests remain readable +// but cannot be retried without an explicit wallet binding. +class CRewardWalletBinding { +public: + static const uint32_t CURRENT_VERSION = 1; + + uint32_t nVersion{CURRENT_VERSION}; + std::string walletName; + CKeyID walletKeyId; + uint256 genesisHash; + uint256 snapshotBlockHash; + uint256 snapshotContentHash; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(nVersion); + READWRITE(walletName); + READWRITE(walletKeyId); + READWRITE(genesisHash); + READWRITE(snapshotBlockHash); + READWRITE(snapshotContentHash); + } +}; + +#ifdef ENABLE_WALLET +extern std::map mapRewardWalletBindings; +#endif + enum { FAILED_GETTING_DISTRIBUTION_LIST = 1, FAILED_ @@ -157,6 +190,7 @@ void DistributeRewardSnapshot(CWallet * p_wallet, const CRewardSnapshot& p_rewar bool BuildTransaction( CWallet * const p_walletPtr, const CRewardSnapshot& p_rewardSnapshot, const std::vector & p_pendingPayments, const int& start, + const int& batchNumber, std::string& change_address, uint256& retTxid); void CheckRewardDistributions(); diff --git a/src/assets/snapshotrequestdb.cpp b/src/assets/snapshotrequestdb.cpp index 1546ec412b..b0ca975fac 100644 --- a/src/assets/snapshotrequestdb.cpp +++ b/src/assets/snapshotrequestdb.cpp @@ -11,6 +11,7 @@ static const char SNAPSHOTREQUEST_FLAG = 'S'; static const char DISTRIBUTEREQUEST_FLAG = 'D'; static const char DISTRIBUTETRANSACTION_FLAG = 'T'; +static const char DISTRIBUTEBINDING_FLAG = 'B'; CSnapshotRequestDBEntry::CSnapshotRequestDBEntry() { @@ -162,9 +163,14 @@ CDistributeSnapshotRequestDB::CDistributeSnapshotRequestDB( } // Schedule a distribution to occur -bool CDistributeSnapshotRequestDB::AddDistributeSnapshot(const uint256& hash, const CRewardSnapshot& p_rewardSnapshot) +bool CDistributeSnapshotRequestDB::AddDistributeSnapshot(const uint256& hash, + const CRewardSnapshot& p_rewardSnapshot, + const CRewardWalletBinding& binding) { - return Write(std::make_pair(DISTRIBUTEREQUEST_FLAG, hash), p_rewardSnapshot); + CDBBatch batch(*this); + batch.Write(std::make_pair(DISTRIBUTEBINDING_FLAG, hash), binding); + batch.Write(std::make_pair(DISTRIBUTEREQUEST_FLAG, hash), p_rewardSnapshot); + return WriteBatch(batch, true); } bool CDistributeSnapshotRequestDB::OverrideDistributeSnapshot(const uint256& hash, const CRewardSnapshot& p_rewardSnapshot) @@ -177,7 +183,7 @@ bool OverrideDistributeSnapshot(const uint256& hash, const CRewardSnapshot& p_re // Add a new distribution transaction bool CDistributeSnapshotRequestDB::AddDistributeTransaction(const uint256& hash, const int& nBatchNumber, const uint256& txid) { - return Write(std::make_pair(DISTRIBUTETRANSACTION_FLAG, std::make_pair(hash, nBatchNumber)), txid); + return Write(std::make_pair(DISTRIBUTETRANSACTION_FLAG, std::make_pair(hash, nBatchNumber)), txid, true); } // Find a distribution transaction from the database @@ -186,12 +192,22 @@ bool CDistributeSnapshotRequestDB::GetDistributeTransaction(const uint256& hash, return Read(std::make_pair(DISTRIBUTETRANSACTION_FLAG, std::make_pair(hash, nBatchNumber)), txid); } +bool CDistributeSnapshotRequestDB::ContainsDistributeTransaction(const uint256& hash, const int& nBatchNumber) +{ + return Exists(std::make_pair(DISTRIBUTETRANSACTION_FLAG, std::make_pair(hash, nBatchNumber))); +} + // Find a distribute snapshot request bool CDistributeSnapshotRequestDB::RetrieveDistributeSnapshotRequest(const uint256& hash, CRewardSnapshot& p_rewardSnapshot) { return Read(std::make_pair(DISTRIBUTEREQUEST_FLAG, hash), p_rewardSnapshot); } +bool CDistributeSnapshotRequestDB::ContainsDistributeSnapshotRequest(const uint256& hash) +{ + return Exists(std::make_pair(DISTRIBUTEREQUEST_FLAG, hash)); +} + // Remove a distribute snapshot request bool CDistributeSnapshotRequestDB::RemoveDistributeSnapshotRequest(const uint256& hash) { @@ -199,10 +215,32 @@ bool CDistributeSnapshotRequestDB::RemoveDistributeSnapshotRequest(const uint256 } // Load all distribute snapshot from database -void CDistributeSnapshotRequestDB::LoadAllDistributeSnapshot(std::map& mapRewardSnapshots) +void CDistributeSnapshotRequestDB::LoadAllDistributeSnapshot( + std::map& mapRewardSnapshots, + std::map& mapWalletBindings) { std::unique_ptr pcursor(NewIterator()); + mapRewardSnapshots.clear(); + mapWalletBindings.clear(); + + pcursor->Seek(std::make_pair(DISTRIBUTEBINDING_FLAG, uint256())); + while (pcursor->Valid()) { + boost::this_thread::interruption_point(); + std::pair key; + if (!pcursor->GetKey(key) || key.first != DISTRIBUTEBINDING_FLAG) + break; + + CRewardWalletBinding binding; + if (pcursor->GetValue(binding)) { + mapWalletBindings[key.second] = binding; + } else { + LogPrint(BCLog::REWARDS, "%s: Failed to read wallet binding for reward %s\n", + __func__, key.second.GetHex()); + } + pcursor->Next(); + } + pcursor->Seek(std::make_pair(DISTRIBUTEREQUEST_FLAG, uint256())); // Load all pending rewards @@ -211,10 +249,12 @@ void CDistributeSnapshotRequestDB::LoadAllDistributeSnapshot(std::map key; // Only retrieve entries at the provided block height - if (pcursor->GetKey(key) && key.first == DISTRIBUTEREQUEST_FLAG) { + if (!pcursor->GetKey(key) || key.first != DISTRIBUTEREQUEST_FLAG) + break; + { CRewardSnapshot distributeDbEntry; - if (pcursor->GetValue(distributeDbEntry)) { + if (pcursor->GetValue(distributeDbEntry) && distributeDbEntry.GetHash() == key.second) { mapRewardSnapshots[key.second] = distributeDbEntry; } else { LogPrint(BCLog::REWARDS, "%s: Failed to read snapshot distribution for key: %s\n", __func__, key.second.GetHex()); diff --git a/src/assets/snapshotrequestdb.h b/src/assets/snapshotrequestdb.h index 4240d9003a..d7a36efb7e 100644 --- a/src/assets/snapshotrequestdb.h +++ b/src/assets/snapshotrequestdb.h @@ -95,19 +95,23 @@ class CDistributeSnapshotRequestDB : public CDBWrapper CDistributeSnapshotRequestDB& operator=(const CDistributeSnapshotRequestDB&) = delete; // Schedule a asset snapshot - bool AddDistributeSnapshot(const uint256& hash, const CRewardSnapshot& p_rewardSnapshot); + bool AddDistributeSnapshot(const uint256& hash, const CRewardSnapshot& p_rewardSnapshot, + const CRewardWalletBinding& binding); bool OverrideDistributeSnapshot(const uint256& hash, const CRewardSnapshot& p_rewardSnapshot); // Find a snapshot request using its ID bool RetrieveDistributeSnapshotRequest(const uint256& hash, CRewardSnapshot& p_rewardSnapshot); + bool ContainsDistributeSnapshotRequest(const uint256& hash); // Remove a snapshot request bool RemoveDistributeSnapshotRequest(const uint256& hash); bool AddDistributeTransaction(const uint256& hash, const int& nBatchNumber, const uint256& txid); bool GetDistributeTransaction(const uint256& hash, const int& nBatchNumber, uint256& txid); + bool ContainsDistributeTransaction(const uint256& hash, const int& nBatchNumber); - void LoadAllDistributeSnapshot(std::map& mapRewardSnapshots); + void LoadAllDistributeSnapshot(std::map& mapRewardSnapshots, + std::map& mapWalletBindings); }; diff --git a/src/init.cpp b/src/init.cpp index 1393f1e9c0..94966838a2 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1605,6 +1605,10 @@ bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler) pSnapshotRequestDb = new CSnapshotRequestDB(nBlockTreeDBCache, false, false); pAssetSnapshotDb = new CAssetSnapshotDB(nBlockTreeDBCache, false, false); pDistributeSnapshotDb = new CDistributeSnapshotRequestDB(nBlockTreeDBCache, false, false); +#ifdef ENABLE_WALLET + pDistributeSnapshotDb->LoadAllDistributeSnapshot(mapRewardSnapshots, + mapRewardWalletBindings); +#endif // Read for fAssetIndex to make sure that we only load asset address balances if it if true pblocktree->ReadFlag("assetindex", fAssetIndex); diff --git a/test/functional/feature_rewards_restart.py b/test/functional/feature_rewards_restart.py new file mode 100644 index 0000000000..795c117a1a --- /dev/null +++ b/test/functional/feature_rewards_restart.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Verify that a wallet-bound reward resumes once after a node restart.""" + +from test_framework.test_framework import RavenTestFramework +from test_framework.util import assert_equal, connect_nodes_bi, Decimal + + +class RewardRestartTest(RavenTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 3 + self.extra_args = [["-assetindex"], + ["-assetindex", "-minrewardheight=15", "-debug=rewards"], + ["-assetindex"]] + + def run_test(self): + miner, payer, owner = self.nodes + miner.generate(432) + self.sync_all() + assert_equal("active", miner.getblockchaininfo()["bip9_softforks"]["assets"]["status"]) + + recipient = owner.getnewaddress() + miner.issue("RESTARTREWARD", 1, recipient) + miner.generate(1) + self.sync_all() + + snapshot_height = miner.getblockcount() + 1 + payer.requestsnapshot("RESTARTREWARD", snapshot_height) + miner.generate(31) + self.sync_all() + + payout = Decimal("2000") + before = owner.getreceivedbyaddress(recipient, 1) + payer.distributereward("RESTARTREWARD", snapshot_height, "RVN", payout) + assert_equal(payer.getdistributestatus( + "RESTARTREWARD", snapshot_height, "RVN", payout)["Status"], 3) + + self.restart_node(1, self.extra_args[1]) + connect_nodes_bi(self.nodes, 0, 1) + connect_nodes_bi(self.nodes, 1, 2) + payer = self.nodes[1] + assert_equal(payer.getdistributestatus( + "RESTARTREWARD", snapshot_height, "RVN", payout)["Status"], 3) + + miner.sendtoaddress(payer.getnewaddress(), 3000) + miner.generate(3) + self.sync_all() + # The retry can run before the wallet sees the funding block. + # Confirm a payout broadcast during the final block separately. + miner.generate(1) + self.sync_all() + assert_equal(owner.getreceivedbyaddress(recipient, 1), before + payout) + + self.restart_node(1, self.extra_args[1]) + connect_nodes_bi(self.nodes, 0, 1) + connect_nodes_bi(self.nodes, 1, 2) + miner.generate(3) + self.sync_all() + assert_equal(owner.getreceivedbyaddress(recipient, 1), before + payout) + + # A replacement snapshot must stop the old payout permanently. Its + # recorded status tells the operator to reconcile any prior batches. + miner.issue("REORGREWARD", 1, recipient) + miner.generate(1) + self.sync_all() + replacement_height = miner.getblockcount() + 1 + payer = self.nodes[1] + payer.requestsnapshot("REORGREWARD", replacement_height) + miner.generate(31) + self.sync_all() + snapshot_block = miner.getblockhash(replacement_height) + payer.distributereward("REORGREWARD", replacement_height, "RVN", payout) + assert_equal(payer.getdistributestatus( + "REORGREWARD", replacement_height, "RVN", payout)["Status"], 3) + + for node in self.nodes: + node.invalidateblock(snapshot_block) + miner.generate(32) + self.sync_all() + assert_equal(payer.getdistributestatus( + "REORGREWARD", replacement_height, "RVN", payout)["Status"], 10) + miner.sendtoaddress(payer.getnewaddress(), 3000) + miner.generate(3) + self.sync_all() + assert_equal(owner.getreceivedbyaddress(recipient, 1), before + payout) + + +if __name__ == "__main__": + RewardRestartTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 489d8a3d61..7b932a0710 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -104,6 +104,7 @@ 'wallet_create_tx.py', # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv Tests less than 30s vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv 'feature_rewards.py', + 'feature_rewards_restart.py', 'wallet_basic.py', 'mempool_limit.py', 'feature_assets.py', From 39602092632fb4145add76666b776ffc912423b4 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:58:55 +0200 Subject: [PATCH 177/195] [FINDING-063][FINDING-100] Record local verification and residual gates --- ...0025-v4.8-security-remediation-register.md | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 2ce580f200..40af8bc3b3 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -3891,6 +3891,13 @@ production signature through the real script path. These tests pass under signature byte vector, nor does it execute full `ConnectBlock`, so the finding remains MITIGATED rather than fully closed. +Post-checkpoint extension: `84a5e350b` pins every byte of the ML-DSA-44 +deterministic signature for the liboqs 0.12.0 FIPS 204 ACVP sigGen group 1, +case 4 vector against the pinned 0.16.0 backend. The expected signature was +deliberately changed for a red test, then restored for a green focused test +and `make check -j4`. A full `ConnectBlock` vector remains outstanding, so +FINDING-063 remains MITIGATED. + ### FINDING-064: Fuzzing never reaches the real ML-DSA verifier - **Severity:** MEDIUM @@ -6849,7 +6856,26 @@ does not close those release conditions. - **Regression required:** Fail a reward for low funds, restart, fund the initiating wallet, and verify a single payout from that wallet. Verify old unbound records do not charge another wallet. -- **Final status:** OPEN. +- **Remediation commit:** `b3803bbc0` binds new reward requests to a unique + wallet key, genesis, snapshot block and snapshot contents. Startup reloads + those requests. Each payout batch persists its transaction ID before wallet + commit. A missing or unconfirmed transaction outside the mempool, duplicate + wallet identity, changed snapshot, or old unbound request cannot trigger an + automatic replacement payout. The changed snapshot receives status 10 for + operator reconciliation; an uncertain transaction receives status 6. +- **Verification:** Before the fix, the low-funds restart scenario failed + because funding after restart produced no payout. The dedicated + `feature_rewards_restart.py` now passes the single-payout restart and + snapshot replacement cases. The original `feature_rewards.py` and + `make check -j4` pass. The dedicated test is mandatory in both CI + workflows. Exact-SHA CI qualification is pending. +- **Residual limitation:** Requests created before the new binding format and + uncertain committed transactions require manual reconciliation. A retry + occurs when the next block is connected after startup, not immediately at + wallet load. Automatic reassignment of legacy requests is intentionally + disallowed because the original wallet identity was never persisted. +- **Final status:** MITIGATED locally; exact-SHA CI and operator recovery + instructions remain due. ### FINDING-101: Restricted and qualifier wallet state uses wallet zero From b8dffce5adfc1b6642010ce7d1a0663385743ca6 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:01:12 +0200 Subject: [PATCH 178/195] [FINDING-070] Recover bounded deterministic PQ keys and wallet history --- .github/workflows/build-raven.yml | 2 +- .github/workflows/rip25-v48-final-gate.yml | 2 +- src/wallet/rpcwallet.cpp | 90 +++++++++++++++++ src/wallet/test/pq_wallet_tests.cpp | 64 +++++++++++++ src/wallet/wallet.cpp | 106 +++++++++++++++++++++ src/wallet/wallet.h | 2 + test/functional/test_runner.py | 1 + test/functional/wallet_pq_recovery.py | 77 +++++++++++++++ 8 files changed, 342 insertions(+), 2 deletions(-) create mode 100755 test/functional/wallet_pq_recovery.py diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index 451f1319d2..062332c64e 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -90,7 +90,7 @@ jobs: run: | python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py rpc_assettransfer.py feature_chainstate_ahead.py \ + wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py rpc_assettransfer.py feature_chainstate_ahead.py \ feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ diff --git a/.github/workflows/rip25-v48-final-gate.yml b/.github/workflows/rip25-v48-final-gate.yml index 05b6034b8d..84889d8f07 100644 --- a/.github/workflows/rip25-v48-final-gate.yml +++ b/.github/workflows/rip25-v48-final-gate.yml @@ -104,7 +104,7 @@ jobs: run: | python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py rpc_assettransfer.py feature_chainstate_ahead.py \ + wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py rpc_assettransfer.py feature_chainstate_ahead.py \ feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index cfa094faf8..549a02e26e 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -261,6 +261,95 @@ UniValue getnewpqaddress(const JSONRPCRequest& request) return EncodeDestination(dest); } +UniValue recoverpqkeys(const JSONRPCRequest& request) +{ + CWallet* const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) return NullUniValue; + if (request.fHelp || request.params.size() != 4) { + throw std::runtime_error( + "recoverpqkeys start_index end_index start_height stop_height\n" + "Restore deterministic PQ keys in an inclusive index range and rescan an inclusive block range.\n" + "At most 100 keys and 100000 blocks may be processed per call. Split larger recovery into multiple calls.\n" + "All keys and the next allocation index are persisted before the rescan. If the rescan fails or is aborted, retry the same call.\n" + "The next allocation index advances past end_index, including unused indices.\n" + "An encrypted wallet must be unlocked. This cannot recover older random PQ keys; those require a wallet-file backup.\n" + "Arguments:\n" + "1. start_index (numeric, required) first hardened PQ receive index\n" + "2. end_index (numeric, required) last hardened PQ receive index\n" + "3. start_height (numeric, required) first block to rescan\n" + "4. stop_height (numeric, required) last block to rescan\n" + "Result: {\"start_index\":n,\"end_index\":n,\"new_keys\":n,\"next_index\":n,\"start_height\":n,\"stop_height\":n}\n"); + } + + const int64_t startIndex = request.params[0].get_int64(); + const int64_t endIndex = request.params[1].get_int64(); + const int64_t startHeight = request.params[2].get_int64(); + const int64_t stopHeight = request.params[3].get_int64(); + if (startIndex < 0 || endIndex < startIndex || + endIndex >= CPQHDChain::MAX_COUNTER || endIndex - startIndex >= 100) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Invalid PQ index range: use 1 to 100 indices below 2147483648"); + } + + LOCK2(cs_main, pwallet->cs_wallet); + if (startHeight < 0 || stopHeight < startHeight || + stopHeight > chainActive.Height() || stopHeight - startHeight >= 100000) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Invalid block height range: use 1 to 100000 available blocks"); + } + if (!pwallet->IsHDEnabled() || !pwallet->GetPQHDChain().IsValid()) { + throw JSONRPCError(RPC_WALLET_ERROR, + "PQ recovery requires a valid HD wallet"); + } + EnsureWalletIsUnlocked(pwallet); + + CBlockIndex* const pindexStart = chainActive[static_cast(startHeight)]; + CBlockIndex* const pindexStop = chainActive[static_cast(stopHeight)]; + if (fPruneMode) { + for (CBlockIndex* block = pindexStop; + block && block->nHeight >= pindexStart->nHeight; + block = block->pprev) { + if (!(block->nStatus & BLOCK_HAVE_DATA)) { + throw JSONRPCError(RPC_MISC_ERROR, + "Requested block range contains pruned data"); + } + } + } + + int newKeys = 0; + for (int64_t index = startIndex; index <= endIndex; ++index) { + bool added = false; + if (!pwallet->RecoverPQKey(static_cast(index), added)) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("PQ recovery stopped at index %u. Persisted keys remain available; retry the same range.", + static_cast(index))); + } + if (added) + ++newKeys; + } + + CBlockIndex* const failedBlock = pwallet->ScanForWalletTransactions( + pindexStart, pindexStop, true); + if (pwallet->IsAbortingRescan()) { + throw JSONRPCError(RPC_MISC_ERROR, + "PQ recovery rescan aborted. Keys remain persisted; retry the same range."); + } + if (failedBlock) { + throw JSONRPCError(RPC_MISC_ERROR, + "PQ recovery rescan failed. Keys remain persisted; retry the same range."); + } + + UniValue result(UniValue::VOBJ); + result.pushKV("start_index", startIndex); + result.pushKV("end_index", endIndex); + result.pushKV("new_keys", newKeys); + result.pushKV("next_index", static_cast( + pwallet->GetPQHDChain().nExternalChainCounter)); + result.pushKV("start_height", startHeight); + result.pushKV("stop_height", stopHeight); + return result; +} + UniValue getnewpqassetaddress(const JSONRPCRequest& request) { CWallet* const pwallet = GetWalletForJSONRPCRequest(request); @@ -3658,6 +3747,7 @@ static const CRPCCommand commands[] = { "wallet", "getmywords", &getmywords, {} }, { "wallet", "getnewaddress", &getnewaddress, {"account"} }, { "wallet", "getnewpqaddress", &getnewpqaddress, {"account"} }, + { "wallet", "recoverpqkeys", &recoverpqkeys, {"start_index", "end_index", "start_height", "stop_height"} }, { "wallet", "getnewpqassetaddress", &getnewpqassetaddress, {"account"} }, { "wallet", "listpqassetaddresses", &listpqassetaddresses, {} }, { "wallet", "getrawchangeaddress", &getrawchangeaddress, {} }, diff --git a/src/wallet/test/pq_wallet_tests.cpp b/src/wallet/test/pq_wallet_tests.cpp index 2ce8c48408..6b64ad7752 100644 --- a/src/wallet/test/pq_wallet_tests.cpp +++ b/src/wallet/test/pq_wallet_tests.cpp @@ -1213,6 +1213,70 @@ BOOST_AUTO_TEST_CASE(encrypted_deterministic_pq_generation_is_ciphertext_only) } } +BOOST_AUTO_TEST_CASE(encrypted_pq_index_recovery_is_idempotent_and_ciphertext_only) +{ + const std::string filename = "pq-hd-encrypted-index-recovery-wallet.dat"; + const SecureString passphrase("pq-hd-index-recovery-passphrase"); + SecureVector seed; + const std::vector bip39Seed = Bip39TestSeed(); + BOOST_REQUIRE(pqderivation::DeriveSeed( + bip39Seed.data(), bip39Seed.size(), 1, 2, seed)); + CPQKey expected; + BOOST_REQUIRE(expected.SetSeed(seed.data())); + const uint256 recoveredProgram = expected.GetPubKey().GetWitnessProgram(); + + { + std::unique_ptr wallet = LoadPQWallet(filename); + BOOST_REQUIRE(InitializeBip39Wallet(*wallet)); + CKey classicalKey; + classicalKey.MakeNewKey(true); + { + LOCK(wallet->cs_wallet); + BOOST_REQUIRE(wallet->AddKeyPubKey( + classicalKey, classicalKey.GetPubKey())); + } + BOOST_REQUIRE(wallet->EncryptWallet(passphrase)); + + bool added = true; + BOOST_CHECK(!wallet->RecoverPQKey(2, added)); + BOOST_CHECK(!added); + BOOST_CHECK_EQUAL(wallet->GetPQHDChain().nExternalChainCounter, 0U); + + BOOST_REQUIRE(wallet->Unlock(passphrase)); + BOOST_REQUIRE(wallet->RecoverPQKey(2, added)); + BOOST_CHECK(added); + BOOST_CHECK_EQUAL(wallet->GetPQHDChain().nExternalChainCounter, 3U); + BOOST_CHECK(wallet->HavePQKey(recoveredProgram)); + + BOOST_REQUIRE(wallet->RecoverPQKey(2, added)); + BOOST_CHECK(!added); + BOOST_REQUIRE(wallet->RecoverPQKey(0, added)); + BOOST_CHECK(added); + BOOST_CHECK_EQUAL(wallet->GetPQHDChain().nExternalChainCounter, 3U); + + CWalletDBWrapper rawDbw(&bitdb, filename); + CDB rawDb(rawDbw, "r"); + BOOST_CHECK(rawDb.Exists( + std::make_pair(std::string("cpqkey"), recoveredProgram))); + BOOST_CHECK(!rawDb.Exists( + std::make_pair(std::string("pqkey"), recoveredProgram))); + } + bitdb.Flush(false); + + { + std::unique_ptr wallet = LoadPQWallet(filename); + BOOST_CHECK_EQUAL(wallet->GetPQHDChain().nExternalChainCounter, 3U); + BOOST_REQUIRE(wallet->Unlock(passphrase)); + CPQKey loaded; + BOOST_REQUIRE(wallet->GetPQKey(recoveredProgram, loaded)); + BOOST_CHECK(loaded.MatchesPubKey(expected.GetPubKey())); + CPQPubKey next; + uint32_t index = 99; + BOOST_REQUIRE(wallet->GenerateNewPQKey(next, &index)); + BOOST_CHECK_EQUAL(index, 3U); + } +} + BOOST_AUTO_TEST_CASE(encrypted_legacy_hd_pq_derivation_recovers_and_advances) { const std::string filename = "pq-hd-encrypted-legacy-wallet.dat"; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 49a91408c4..51370f58f4 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -703,6 +703,112 @@ bool CWallet::GenerateNewPQKey(CPQPubKey& pubkeyOut, uint32_t* indexOut) return true; } +bool CWallet::RecoverPQKey(uint32_t index, bool& addedOut) +{ + LOCK(cs_wallet); + addedOut = false; + if (index >= pqderivation::HARDENED_LIMIT || !IsHDEnabled() || + IsLocked() || !pqHDChain.IsValid()) { + return false; + } + + SecureVector walletSeed; + const uint8_t seedSource = hdChain.IsBip44() + ? pqderivation::SEED_SOURCE_BIP39 + : pqderivation::SEED_SOURCE_LEGACY_HD; + if (hdChain.IsBip44()) { + if (!GetBip39Seed(walletSeed)) + return false; + } else { + CKey seed; + if (!GetKey(hdChain.seed_id, seed) || + seed.size() != pqderivation::LEGACY_SEED_BYTES) { + return false; + } + walletSeed.assign(seed.begin(), seed.end()); + } + + const uint32_t coinType = GetParams().ExtCoinType(); + uint256 lineageId; + if (!pqderivation::GetLineageId(walletSeed.data(), walletSeed.size(), + seedSource, coinType, lineageId)) { + return false; + } + + CPQHDChain allocationChain = pqHDChain; + if (!allocationChain.IsInitialized() || + allocationChain.nSeedSource != seedSource || + allocationChain.nCoinType != coinType || + allocationChain.lineage_id != lineageId) { + allocationChain.SetLineage(seedSource, coinType, lineageId); + } + if (!allocationChain.IsInitialized()) + return false; + + SecureVector pqSeed; + if (!pqderivation::DeriveSeed(walletSeed.data(), walletSeed.size(), + coinType, index, pqSeed)) { + return false; + } + SecureVector().swap(walletSeed); + CPQKey candidateKey; + const bool generated = candidateKey.SetSeed(pqSeed.data()); + SecureVector().swap(pqSeed); + if (!generated) + return false; + const CPQPubKey candidatePubKey = candidateKey.GetPubKey(); + const bool keyExists = HavePQKey(candidatePubKey.GetWitnessProgram()); + + CPQHDChain updatedChain = allocationChain; + updatedChain.nExternalChainCounter = std::max( + allocationChain.nExternalChainCounter, index + 1); + const bool chainChanged = !pqHDChain.IsInitialized() || + pqHDChain.nSeedSource != updatedChain.nSeedSource || + pqHDChain.nCoinType != updatedChain.nCoinType || + pqHDChain.lineage_id != updatedChain.lineage_id || + pqHDChain.nExternalChainCounter != updatedChain.nExternalChainCounter; + if (keyExists && !chainChanged) + return true; + + CWalletDB walletdb(*dbw); + if (!walletdb.TxnBegin(DB_TXN_SYNC)) + return false; + + bool addedToMemory = false; + bool transactionActive = true; + auto abortRecovery = [&]() { + if (transactionActive && !walletdb.TxnAbort()) + LogPrintf("RecoverPQKey: failed to abort wallet transaction\n"); + transactionActive = false; + if (addedToMemory) + ErasePQKeyFromMemory(candidatePubKey.GetWitnessProgram()); + return false; + }; + + try { + if (!keyExists) { + if (!AddPQKeyPubKeyWithDB(walletdb, candidateKey, candidatePubKey)) + return abortRecovery(); + addedToMemory = true; + } + if (chainChanged && !walletdb.WritePQHDChain(updatedChain)) + return abortRecovery(); + if (!walletdb.TxnCommit(DB_TXN_SYNC)) { + transactionActive = false; + if (addedToMemory) + ErasePQKeyFromMemory(candidatePubKey.GetWitnessProgram()); + return false; + } + transactionActive = false; + } catch (...) { + return abortRecovery(); + } + + pqHDChain = updatedChain; + addedOut = !keyExists; + return true; +} + bool CWallet::AddCryptedKey(const CPubKey &vchPubKey, const std::vector &vchCryptedSecret) { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 4e1d998ea2..7c1ae5255e 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -952,6 +952,8 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface bool AddPQKeyPubKey(const CPQKey &key, const CPQPubKey &pubkey) override; //! Derives and atomically persists the next deterministic PQ key. bool GenerateNewPQKey(CPQPubKey& pubkeyOut, uint32_t* indexOut = nullptr); + //! Restores one deterministic PQ key and advances its allocation counter atomically. + bool RecoverPQKey(uint32_t index, bool& addedOut); //! Adds a PQ key to the store, without saving it to disk (used by LoadWallet) bool LoadPQKey(const CPQKey& key, const CPQPubKey &pubkey) { return CCryptoKeyStore::AddPQKeyPubKey(key, pubkey); } //! Load metadata (used by LoadWallet) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 7b932a0710..06861c3291 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -105,6 +105,7 @@ # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv Tests less than 30s vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv 'feature_rewards.py', 'feature_rewards_restart.py', + 'wallet_pq_recovery.py', 'wallet_basic.py', 'mempool_limit.py', 'feature_assets.py', diff --git a/test/functional/wallet_pq_recovery.py b/test/functional/wallet_pq_recovery.py new file mode 100755 index 0000000000..d93768085e --- /dev/null +++ b/test/functional/wallet_pq_recovery.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The Raven Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Recover a funded deterministic PQ key from mnemonic words alone.""" + +from decimal import Decimal +import os + +from test_framework.test_framework import RavenTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error + + +MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' + + +class PQRecoveryTest(RavenTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [['-bip44=1', '-mnemonic=' + MNEMONIC]] + + def run_test(self): + node = self.nodes[0] + node.generate(432) + source_addresses = [node.getnewpqaddress() for _ in range(4)] + funded_address = source_addresses[2] + funding_txid = node.sendtoaddress(funded_address, Decimal('1')) + node.generate(1) + tip = node.getblockcount() + + self.stop_node(0) + os.remove(os.path.join(node.datadir, 'regtest', 'wallet.dat')) + self.start_node(0, extra_args=['-bip44=1', '-mnemonic=' + MNEMONIC]) + + assert_equal(node.validateaddress(funded_address)['ismine'], False) + assert_equal([coin for coin in node.listunspent() + if coin['txid'] == funding_txid and + coin['address'] == funded_address], []) + assert_raises_rpc_error(-8, 'index', node.recoverpqkeys, 0, 100, 0, tip) + assert_raises_rpc_error(-8, 'height', node.recoverpqkeys, 0, 2, tip, 0) + + result = node.recoverpqkeys(0, 2, 0, tip) + assert_equal(result['new_keys'], 3) + assert_equal(result['next_index'], 3) + assert_equal(result['start_height'], 0) + assert_equal(result['stop_height'], tip) + assert_equal(node.validateaddress(funded_address)['ismine'], True) + + recovered = [coin for coin in node.listunspent() + if coin['txid'] == funding_txid and + coin['address'] == funded_address] + assert_equal(len(recovered), 1) + assert_equal(recovered[0]['amount'], Decimal('1')) + assert_equal(recovered[0]['spendable'], True) + + repeated = node.recoverpqkeys(0, 2, 0, tip) + assert_equal(repeated['new_keys'], 0) + assert_equal(repeated['next_index'], 3) + + self.stop_node(0) + self.start_node(0, extra_args=['-bip44=1']) + assert_equal(node.validateaddress(funded_address)['ismine'], True) + assert_equal(node.getnewpqaddress(), source_addresses[3]) + + destination = node.getnewaddress() + raw = node.createrawtransaction( + [{'txid': recovered[0]['txid'], 'vout': recovered[0]['vout']}], + {destination: Decimal('0.9')}) + signed = node.signrawtransaction(raw) + assert_equal(signed['complete'], True) + node.sendrawtransaction(signed['hex']) + + +if __name__ == '__main__': + PQRecoveryTest().main() From 09faa9b52f2291e0e0932c6f074a302381b5c549 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:01:48 +0200 Subject: [PATCH 179/195] [FINDING-070] Record recovery proof and operator procedure --- ...0025-v4.8-security-remediation-register.md | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 40af8bc3b3..7d64c79ca9 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -5089,8 +5089,27 @@ rebuild logic was added. than zero, discard the wallet database, restore only the mnemonic, invoke the recovery mechanism, rescan, and prove the UTXO is found and spendable. Keep the pre-FINDING-057 random-key limitation explicitly documented. -- **Remediation commit:** PENDING -- **Final status:** OPEN +- **Remediation commit:** `b8dffce5a` adds `recoverpqkeys` with inclusive + index and block-height bounds, transactional per-key persistence, an + advanced allocation counter, and a rescan. A failed or interrupted rescan + leaves the keys and counter persisted so the same request can be retried. +- **Verification:** Before the fix, the mnemonic-only functional case failed + at the missing RPC. After the fix, `wallet_pq_recovery.py` restores an + index-two funded key from mnemonic words, finds and spends its UTXO, proves + repeat calls are idempotent, and preserves index-three allocation over a + restart. A focused encrypted-wallet unit case proves ciphertext-only + persistence and a locked-wallet rejection. `make check -j4` and the local + required-functional and structural gates pass. Both CI workflows now + require the functional case; exact-SHA CI remains due. +- **Operator procedure:** Restore the mnemonic into a fresh wallet, unlock it + if encrypted, then call `recoverpqkeys start_index end_index start_height + stop_height` in chunks of at most 100 indices and 100000 blocks. Include + the funding block and repeat the same range after an interruption. Inspect + `listunspent` before spending. Indices beyond `end_index` remain available + for new addresses. Keys generated randomly before deterministic PQ + derivation still require the original wallet-file backup. +- **Final status:** FIXED locally for deterministic key recovery; exact-SHA + CI qualification remains due. ## Berkeley DB retry remediation checkpoint From 405b4d55d1fb1f7401febadc98b269ad289343ac Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:02:29 +0200 Subject: [PATCH 180/195] [FINDING-100] Execute mandatory reward restart regression --- test/functional/feature_rewards_restart.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 test/functional/feature_rewards_restart.py diff --git a/test/functional/feature_rewards_restart.py b/test/functional/feature_rewards_restart.py old mode 100644 new mode 100755 From e4f88d188bee4f125a907f3397fcf799d89d5d5c Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:04:28 +0200 Subject: [PATCH 181/195] [FINDING-100] Record required test launch regression --- doc/RIP-0025-v4.8-security-remediation-register.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 7d64c79ca9..6610b298f4 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -6887,7 +6887,11 @@ does not close those release conditions. `feature_rewards_restart.py` now passes the single-payout restart and snapshot replacement cases. The original `feature_rewards.py` and `make check -j4` pass. The dedicated test is mandatory in both CI - workflows. Exact-SHA CI qualification is pending. + workflows. The first local behavioral gate could not launch that new test + because its executable bit was missing; `405b4d55d` sets the bit. A + subsequent gate passed the new test, then stopped on an unrelated local + Python 3.12 `asyncore` import limitation. Exact-SHA CI qualification is + pending. - **Residual limitation:** Requests created before the new binding format and uncertain committed transactions require manual reconciliation. A retry occurs when the next block is connected after startup, not immediately at From 0e1399a840c6a5014d4e2e8208b235afbdaec6a0 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:07:06 +0200 Subject: [PATCH 182/195] [FINDING-020][FINDING-070][FINDING-100] Harden required gate execution checks --- .../devtools/check-rip25-v48-invariants.sh | 24 ++++- ...-rip25-invariant-gate-negative-controls.py | 96 +++++++++++++++++++ 2 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 contrib/devtools/test-rip25-invariant-gate-negative-controls.py diff --git a/contrib/devtools/check-rip25-v48-invariants.sh b/contrib/devtools/check-rip25-v48-invariants.sh index 3bd5ed63a9..fb2cbf0d99 100755 --- a/contrib/devtools/check-rip25-v48-invariants.sh +++ b/contrib/devtools/check-rip25-v48-invariants.sh @@ -91,6 +91,12 @@ require_fixed 'feature_pq_asset_source_rpc.py' .github/workflows/build-raven.yml require_fixed 'feature_pq_asset_wallet_ownership.py' test/functional/test_runner.py 'PQ asset wallet ownership regression is not registered' require_fixed 'feature_mempool_clear_reissue.py' test/functional/test_runner.py 'mempool clear reissue regression is not registered' require_fixed 'feature_mempool_reissue_restart.py' test/functional/test_runner.py 'mempool reissue restart regression is not registered' +require_fixed "'feature_rewards_restart.py'" test/functional/test_runner.py 'reward restart regression is not registered' +require_fixed 'feature_rewards_restart.py' .github/workflows/rip25-v48-final-gate.yml 'final gate omits reward restart regression' +require_fixed 'feature_rewards_restart.py' .github/workflows/build-raven.yml 'release workflow omits reward restart regression' +require_fixed "'wallet_pq_recovery.py'" test/functional/test_runner.py 'PQ wallet recovery regression is not registered' +require_fixed 'wallet_pq_recovery.py' .github/workflows/rip25-v48-final-gate.yml 'final gate omits PQ wallet recovery regression' +require_fixed 'wallet_pq_recovery.py' .github/workflows/build-raven.yml 'release workflow omits PQ wallet recovery regression' # GLM-003: contextual 8 -> 12 -> 16 MWU and UTXO-bound 8x discount. require_fixed 'VersionBitsStateSinceHeight' src/validation.cpp 'deterministic RIP-25 phase boundary missing' @@ -619,12 +625,17 @@ require_fixed 'python3 contrib/devtools/test-required-functional-gate.py' "$fina require_fixed 'python3 test/functional/test_runner.py --require-tests' "$final_gate" 'functional gate permits absent or skipped security tests' require_fixed 'feature_mempool_clear_reissue.py' "$final_gate" 'final gate omits inherited mempool clear regression' require_fixed 'feature_mempool_reissue_restart.py' "$final_gate" 'final gate omits inherited reissue restart regression' -require_fixed 'wallet_encryption_rewrite.py wallet_asset_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py' "$final_gate" 'required wallet, multiwallet asset, asset-scope, and chainstate-ahead functional tests are missing' +require_fixed 'wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py rpc_assettransfer.py feature_chainstate_ahead.py' "$final_gate" 'required wallet, rewards, PQ recovery, multiwallet asset, asset-scope, and chainstate-ahead functional tests are missing' require_fixed 'wallet_asset_multiwallet.py' .github/workflows/build-raven.yml 'release security job omits the multiwallet asset regression' require_fixed "'wallet_asset_multiwallet.py'" test/functional/test_runner.py 'multiwallet asset regression is not in the functional suite' require_fixed 'id: posttest_integrity' "$final_gate" 'final gate does not recheck source after security tests' final_security_job="$(sed -n '/^ security-tests:/,/^ build:/p' "$final_gate")" require_text "$final_security_job" 'test/test_raven_fuzzy --pq-smoke' 'final security job does not run the PQ verifier fuzz smoke test' +mapfile -t final_security_sha_lines < <(grep -nFx ' test "$(git rev-parse HEAD)" = "$GITHUB_SHA"' <<<"$final_security_job" | cut -d: -f1) +(( ${#final_security_sha_lines[@]} == 2 )) || fail 'final security job must execute exact-SHA checks before and after tests' +final_make_check_line="$(grep -nFx ' run: make check' <<<"$final_security_job" | cut -d: -f1 || true)" +[[ "$final_make_check_line" =~ ^[0-9]+$ ]] || fail 'final security job does not execute make check' +(( final_security_sha_lines[0] < final_make_check_line && final_make_check_line < final_security_sha_lines[1] )) || fail 'final security job does not bracket make check with exact-SHA checks' require_fixed 'id: postbuild_integrity' "$final_gate" 'final gate does not recheck source after cross-builds' final_build_matrix="$(sed -n '/^ matrix:/,/^ steps:/p' "$final_gate")" require_text "$final_build_matrix" $' - name: aarch64-disable-wallet\n host: aarch64-linux-gnu\n packages: g++-aarch64-linux-gnu\n configure_flags: --without-gui --disable-wallet --disable-bench\n run_tests: false' 'final gate lacks the aarch64 no-wallet cross-build' @@ -667,7 +678,12 @@ require_text "$release_security_job" 'feature_mempool_clear_reissue.py' 'release require_text "$release_security_job" 'feature_mempool_reissue_restart.py' 'release artifact workflow omits inherited reissue restart regression' require_text "$release_security_job" 'feature_chainstate_ahead.py' 'release artifact workflow does not test chainstate-ahead recovery' require_text "$release_security_job" 'test "$(git rev-parse HEAD)" = "$GITHUB_SHA"' 'release security job does not bind tests to the artifact SHA' -require_text "$release_build_job" ' needs: security-tests' 'release artifact matrix can package without passing security tests' +grep -Fxq ' needs: security-tests' <<<"$release_build_job" || fail 'release artifact matrix can package without passing security tests' +mapfile -t release_security_sha_lines < <(grep -nFx ' test "$(git rev-parse HEAD)" = "$GITHUB_SHA"' <<<"$release_security_job" | cut -d: -f1) +(( ${#release_security_sha_lines[@]} == 2 )) || fail 'release security job must execute exact-SHA checks before and after tests' +release_make_check_line="$(grep -nFx ' if ! make check; then' <<<"$release_security_job" | cut -d: -f1 || true)" +[[ "$release_make_check_line" =~ ^[0-9]+$ ]] || fail 'release security job does not execute make check' +(( release_security_sha_lines[0] < release_make_check_line && release_make_check_line < release_security_sha_lines[1] )) || fail 'release security job does not bracket make check with exact-SHA checks' release_matrix="$(sed -n '/^ matrix:/,/^ steps:/p' "$release_workflow")" release_axes="$(grep -E '^ [A-Za-z_][A-Za-z_0-9]*:' <<<"$release_matrix" || true)" [[ "$release_axes" == ' include:' ]] || fail 'release workflow has unexpected matrix axes' @@ -723,6 +739,8 @@ if [[ "$mode" == '--structural-only' ]]; then exit 0 fi +python3 contrib/devtools/test-rip25-invariant-gate-negative-controls.py + test_binary=src/test/test_raven [[ -x "$test_binary" ]] || fail "behavioral test binary is missing or not executable: $test_binary" # Qt and benchmark translation units are not linked into the headless @@ -806,6 +824,6 @@ echo "$fuzz_smoke_output" python3 contrib/devtools/test-required-functional-gate.py PYTHONPATH=test/functional python3 -m unittest test_framework.test_cache_peers python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py rpc_assettransfer.py feature_chainstate_ahead.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py + wallet_encryption_rewrite.py feature_rewards_restart.py wallet_pq_recovery.py rpc_assettransfer.py feature_chainstate_ahead.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py echo 'RIP-25/v4.8 structural + behavioral invariants: OK' diff --git a/contrib/devtools/test-rip25-invariant-gate-negative-controls.py b/contrib/devtools/test-rip25-invariant-gate-negative-controls.py new file mode 100644 index 0000000000..ccb4d1da9c --- /dev/null +++ b/contrib/devtools/test-rip25-invariant-gate-negative-controls.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Check that inert workflow bait cannot satisfy the invariant gate.""" + +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +GATE = ROOT / "contrib/devtools/check-rip25-v48-invariants.sh" +WORKFLOWS = ("build-raven.yml", "rip25-v48-final-gate.yml") + + +class InvariantGateNegativeControls(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.tempdir = tempfile.TemporaryDirectory(prefix="rip25-gate-") + cls.fixture = Path(cls.tempdir.name) + for entry in ROOT.iterdir(): + if entry.name in (".git", ".github"): + continue + (cls.fixture / entry.name).symlink_to(entry, target_is_directory=entry.is_dir()) + github = cls.fixture / ".github" + github.mkdir() + for entry in (ROOT / ".github").iterdir(): + if entry.name == "workflows": + shutil.copytree(entry, github / entry.name) + else: + (github / entry.name).symlink_to(entry, target_is_directory=entry.is_dir()) + + @classmethod + def tearDownClass(cls): + cls.tempdir.cleanup() + + def run_gate(self): + return subprocess.run( + ["bash", str(GATE), "--structural-only"], cwd=self.fixture, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + ) + + def check_mutation(self, workflow, old, new, occurrence=1): + path = self.fixture / ".github/workflows" / workflow + original = path.read_text(encoding="utf-8") + self.assertIn(old, original, "The workflow fixture no longer matches the test") + self.assertGreaterEqual(original.count(old), occurrence) + at = -1 + for _ in range(occurrence): + at = original.index(old, at + 1) + mutated = original[:at] + new + original[at + len(old):] + path.write_text(mutated, encoding="utf-8") + try: + result = self.run_gate() + self.assertNotEqual( + result.returncode, 0, + workflow + " accepted inert bait: " + old + "\n" + result.stdout, + ) + finally: + path.write_text(original, encoding="utf-8") + + def test_unmodified_workflows_pass(self): + result = self.run_gate() + self.assertEqual(result.returncode, 0, result.stderr) + + def test_build_raven_dependency_is_executable_yaml(self): + self.check_mutation( + WORKFLOWS[0], " needs: security-tests", + " # needs: security-tests", + ) + + def test_build_raven_make_check_is_executed(self): + self.check_mutation( + WORKFLOWS[0], " if ! make check; then", + " if ! echo 'make check'; then", + ) + + def test_build_raven_sha_check_is_executed_before_and_after(self): + old = ' test "$(git rev-parse HEAD)" = "$GITHUB_SHA"' + self.check_mutation(WORKFLOWS[0], old, " # " + old.strip()) + self.check_mutation(WORKFLOWS[0], old, " # " + old.strip(), occurrence=2) + + def test_final_gate_make_check_is_executed(self): + self.check_mutation( + WORKFLOWS[1], " run: make check", + " run: echo 'make check'", + ) + + def test_final_gate_sha_check_is_executed_before_and_after(self): + old = ' test "$(git rev-parse HEAD)" = "$GITHUB_SHA"' + self.check_mutation(WORKFLOWS[1], old, " # " + old.strip()) + self.check_mutation(WORKFLOWS[1], old, " # " + old.strip(), occurrence=2) + + +if __name__ == "__main__": + unittest.main() From 4869df7baba89dd1d6bd7c64698043ca3ebe6564 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:07:19 +0200 Subject: [PATCH 183/195] [FINDING-020] Record gate negative controls and remaining scope --- doc/RIP-0025-v4.8-security-remediation-register.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 6610b298f4..6ffe512e6f 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -1089,6 +1089,14 @@ from the demonstrated coverage gaps. corruption regressions. The broader checker still relies on structural fragments for properties without mutation controls. The revised workflow has not run on GitHub, so this finding is not closed. + `0e1399a84` adds six workflow negative controls: a dependency hidden in a + YAML comment, a non-executing `make check`, and pre/post exact-SHA checks + hidden in comments for both Build Raven and Final Gate. All six fail when + mutated and pass on the restored workflows. The same change requires the + new reward-restart and PQ wallet-recovery regressions in both workflows and + the local behavioral gate. These controls cover only the named workflow + properties; the broader mutation matrix and final exact-SHA CI are still + due. - **Final status:** OPEN, partially mitigated. The false two-platform release assertion is removed, but the checker cannot yet certify every security property it describes, and remote artifact-level evidence is absent. From 4f026d02bdc82f5da814c48010e2cd56471b75cb Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:04:39 +0200 Subject: [PATCH 184/195] [FINDING-101] Isolate restricted views by wallet and restore index state --- .github/workflows/build-raven.yml | 2 +- .github/workflows/rip25-v48-final-gate.yml | 2 +- .../devtools/check-rip25-v48-invariants.sh | 7 +- src/assets/assets.cpp | 9 +- src/assets/assets.h | 3 + src/assets/myassetsdb.cpp | 142 +++++++++++++++ src/assets/myassetsdb.h | 13 ++ src/init.cpp | 9 + src/qt/myrestrictedassettablemodel.cpp | 44 ++++- src/rpc/messages.cpp | 27 +++ src/validation.cpp | 164 ++++++++++++++++-- src/validation.h | 4 + test/functional/test_runner.py | 1 + .../wallet_restricted_multiwallet.py | 101 +++++++++++ 14 files changed, 502 insertions(+), 26 deletions(-) create mode 100755 test/functional/wallet_restricted_multiwallet.py diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index 062332c64e..b9c4d654cf 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -90,7 +90,7 @@ jobs: run: | python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py rpc_assettransfer.py feature_chainstate_ahead.py \ + wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ diff --git a/.github/workflows/rip25-v48-final-gate.yml b/.github/workflows/rip25-v48-final-gate.yml index 84889d8f07..f9e8dc0b01 100644 --- a/.github/workflows/rip25-v48-final-gate.yml +++ b/.github/workflows/rip25-v48-final-gate.yml @@ -104,7 +104,7 @@ jobs: run: | python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py rpc_assettransfer.py feature_chainstate_ahead.py \ + wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ diff --git a/contrib/devtools/check-rip25-v48-invariants.sh b/contrib/devtools/check-rip25-v48-invariants.sh index fb2cbf0d99..b0b9a0bc7f 100755 --- a/contrib/devtools/check-rip25-v48-invariants.sh +++ b/contrib/devtools/check-rip25-v48-invariants.sh @@ -97,6 +97,9 @@ require_fixed 'feature_rewards_restart.py' .github/workflows/build-raven.yml 're require_fixed "'wallet_pq_recovery.py'" test/functional/test_runner.py 'PQ wallet recovery regression is not registered' require_fixed 'wallet_pq_recovery.py' .github/workflows/rip25-v48-final-gate.yml 'final gate omits PQ wallet recovery regression' require_fixed 'wallet_pq_recovery.py' .github/workflows/build-raven.yml 'release workflow omits PQ wallet recovery regression' +require_fixed "'wallet_restricted_multiwallet.py'" test/functional/test_runner.py 'restricted multiwallet regression is not registered' +require_fixed 'wallet_restricted_multiwallet.py' .github/workflows/rip25-v48-final-gate.yml 'final gate omits restricted multiwallet regression' +require_fixed 'wallet_restricted_multiwallet.py' .github/workflows/build-raven.yml 'release workflow omits restricted multiwallet regression' # GLM-003: contextual 8 -> 12 -> 16 MWU and UTXO-bound 8x discount. require_fixed 'VersionBitsStateSinceHeight' src/validation.cpp 'deterministic RIP-25 phase boundary missing' @@ -625,7 +628,7 @@ require_fixed 'python3 contrib/devtools/test-required-functional-gate.py' "$fina require_fixed 'python3 test/functional/test_runner.py --require-tests' "$final_gate" 'functional gate permits absent or skipped security tests' require_fixed 'feature_mempool_clear_reissue.py' "$final_gate" 'final gate omits inherited mempool clear regression' require_fixed 'feature_mempool_reissue_restart.py' "$final_gate" 'final gate omits inherited reissue restart regression' -require_fixed 'wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py rpc_assettransfer.py feature_chainstate_ahead.py' "$final_gate" 'required wallet, rewards, PQ recovery, multiwallet asset, asset-scope, and chainstate-ahead functional tests are missing' +require_fixed 'wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py' "$final_gate" 'required wallet, rewards, PQ recovery, restricted multiwallet, asset-scope, and chainstate-ahead functional tests are missing' require_fixed 'wallet_asset_multiwallet.py' .github/workflows/build-raven.yml 'release security job omits the multiwallet asset regression' require_fixed "'wallet_asset_multiwallet.py'" test/functional/test_runner.py 'multiwallet asset regression is not in the functional suite' require_fixed 'id: posttest_integrity' "$final_gate" 'final gate does not recheck source after security tests' @@ -824,6 +827,6 @@ echo "$fuzz_smoke_output" python3 contrib/devtools/test-required-functional-gate.py PYTHONPATH=test/functional python3 -m unittest test_framework.test_cache_peers python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py feature_rewards_restart.py wallet_pq_recovery.py rpc_assettransfer.py feature_chainstate_ahead.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py + wallet_encryption_rewrite.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py echo 'RIP-25/v4.8 structural + behavioral invariants: OK' diff --git a/src/assets/assets.cpp b/src/assets/assets.cpp index 6c38dd3eb5..5feadf3426 100644 --- a/src/assets/assets.cpp +++ b/src/assets/assets.cpp @@ -5722,13 +5722,8 @@ bool ContextualCheckNullAssetTxOut(const CTxOut& txout, CAssetsCache* assetCache } } -#ifdef ENABLE_WALLET - if (myNullAssetData && vpwallets.size()) { - if (GetRestrictedAddressOwnership(*vpwallets[0], address) & ISMINE_ALL) { - myNullAssetData->emplace_back(std::make_pair(address, data)); - } - } -#endif + if (myNullAssetData) + myNullAssetData->emplace_back(address, data); return true; } diff --git a/src/assets/assets.h b/src/assets/assets.h index 1a2c18f750..40658d55a2 100644 --- a/src/assets/assets.h +++ b/src/assets/assets.h @@ -561,6 +561,9 @@ std::string EncodeIPFS(std::string decoded); #ifdef ENABLE_WALLET +/** Ask wallet restricted asset views to reload after index recovery. */ +static constexpr int RESTRICTED_ASSET_VIEW_RELOAD = -2; + /** Include wallet-owned PQ-only asset identifiers in tag and freeze history. */ isminetype GetRestrictedAddressOwnership(const CWallet& wallet, const std::string& address); diff --git a/src/assets/myassetsdb.cpp b/src/assets/myassetsdb.cpp index a0c519964b..a07f9baf19 100644 --- a/src/assets/myassetsdb.cpp +++ b/src/assets/myassetsdb.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "validation.h" +#include "assets.h" #include "myassetsdb.h" #include "messages.h" #include @@ -15,6 +16,12 @@ static const char DB_FLAG = 'D'; // Database Flags static const char MY_TAGGED_ADDRESSES = 'T'; // Addresses that have been tagged static const char MY_RESTRICTED_ADDRESSES = 'R'; // Addresses that have been restricted +static const char MY_RESTRICTED_UNDO = 'U'; + +using RestrictedEventKey = std::pair>; +using RestrictedEventValue = std::pair; +using RestrictedBlockUndo = std::vector>>; CMessageDB::CMessageDB(size_t nCacheSize, bool fMemory, bool fWipe) : CDBWrapper(GetDataDir() / "messages" / "messages", nCacheSize, fMemory, fWipe) { } @@ -328,6 +335,141 @@ bool CMyRestrictedDB::LoadMyRestrictedAddresses(std::vector cursor(NewIterator()); + cursor->Seek(std::make_pair(prefix, std::make_pair(std::string(), std::string()))); + while (cursor->Valid()) { + CDBBatch batch(*this); + size_t count = 0; + while (cursor->Valid() && count < 1024) { + std::pair> key; + if (!cursor->GetKey(key) || key.first != prefix) + break; + batch.Erase(key); + cursor->Next(); + ++count; + } + if (!count) + break; + if (!WriteBatch(batch)) + return false; + boost::this_thread::interruption_point(); + } + } + std::unique_ptr cursor(NewIterator()); + cursor->Seek(std::make_pair(MY_RESTRICTED_UNDO, uint256())); + while (cursor->Valid()) { + CDBBatch batch(*this); + size_t count = 0; + while (cursor->Valid() && count < 1024) { + std::pair key; + if (!cursor->GetKey(key) || key.first != MY_RESTRICTED_UNDO) + break; + batch.Erase(key); + cursor->Next(); + ++count; + } + if (!count) + break; + if (!WriteBatch(batch)) + return false; + boost::this_thread::interruption_point(); + } + return true; +} + +bool CMyRestrictedDB::WriteRestrictedBlock( + const std::vector>& events, + uint32_t blockTime, const uint256& hash) +{ + CDBBatch batch(*this); + std::set seen; + RestrictedBlockUndo undo; + for (const auto& event : events) { + char prefix = 0; + if (IsAssetNameAQualifier(event.second.asset_name)) { + prefix = MY_TAGGED_ADDRESSES; + } else if (IsAssetNameAnRestricted(event.second.asset_name)) { + prefix = MY_RESTRICTED_ADDRESSES; + } + if (!prefix) + continue; + const RestrictedEventKey key(prefix, std::make_pair(event.first, event.second.asset_name)); + if (seen.insert(key).second) { + RestrictedEventValue prior(0, 0); + const bool existed = Read(key, prior); + undo.emplace_back(key, std::make_pair(existed, prior)); + } + batch.Write(key, RestrictedEventValue(event.second.flag ? 1 : 0, blockTime)); + } + if (!undo.empty()) + batch.Write(std::make_pair(MY_RESTRICTED_UNDO, hash), undo); + batch.Write(std::make_pair(DB_FLAG, std::string("restricted_global_index_tip_v1")), hash); + return WriteBatch(batch); +} + +bool CMyRestrictedDB::UndoRestrictedBlock( + const std::vector>& events, + const uint256& hash, const uint256& previousHash, + std::vector>& restored) +{ + restored.clear(); + std::set expected; + for (const auto& event : events) { + char prefix = 0; + if (IsAssetNameAQualifier(event.second.asset_name)) + prefix = MY_TAGGED_ADDRESSES; + else if (IsAssetNameAnRestricted(event.second.asset_name)) + prefix = MY_RESTRICTED_ADDRESSES; + if (prefix) + expected.emplace(prefix, std::make_pair(event.first, event.second.asset_name)); + } + + CDBBatch batch(*this); + if (!expected.empty()) { + RestrictedBlockUndo undo; + const auto undoKey = std::make_pair(MY_RESTRICTED_UNDO, hash); + if (!Read(undoKey, undo) || undo.size() != expected.size()) + return false; + for (const auto& item : undo) { + if (!expected.erase(item.first)) + return false; + if (item.second.first) + batch.Write(item.first, item.second.second); + else + batch.Erase(item.first); + restored.emplace_back(item.first.second.first, item.first.second.second, + item.second.first ? item.second.second.first : -1, + item.second.first ? item.second.second.second : 0); + } + batch.Erase(undoKey); + } + batch.Write(std::make_pair(DB_FLAG, std::string("restricted_global_index_tip_v1")), previousHash); + return WriteBatch(batch); +} + +bool CMyRestrictedDB::WriteRestrictedIndexTip(const uint256& hash) +{ + return Write(std::make_pair(DB_FLAG, std::string("restricted_global_index_tip_v1")), hash); +} + +bool CMyRestrictedDB::ReadRestrictedIndexTip(uint256& hash) +{ + return Read(std::make_pair(DB_FLAG, std::string("restricted_global_index_tip_v1")), hash); +} + +bool CMyRestrictedDB::WriteRestrictedIndexState(char state) +{ + return Write(std::make_pair(DB_FLAG, std::string("restricted_global_index_state_v1")), state); +} + +bool CMyRestrictedDB::ReadRestrictedIndexState(char& state) +{ + return Read(std::make_pair(DB_FLAG, std::string("restricted_global_index_state_v1")), state); +} + bool CMyRestrictedDB::WriteFlag(const std::string &name, bool fValue) { diff --git a/src/assets/myassetsdb.h b/src/assets/myassetsdb.h index 0bf5fd5422..c0fe09c541 100644 --- a/src/assets/myassetsdb.h +++ b/src/assets/myassetsdb.h @@ -9,6 +9,8 @@ class CMessage; class COutPoint; +class CNullAssetTxData; +class uint256; class CMessageDB : public CDBWrapper { @@ -73,6 +75,17 @@ class CMyRestrictedDB : public CDBWrapper { bool EraseRestrictedAddress(const std::string& address, const std::string& tag_name); bool LoadMyRestrictedAddresses(std::vector >& vecRestrictedAddresses); + bool EraseAllRestrictedEvents(); + bool WriteRestrictedBlock(const std::vector>& events, + uint32_t blockTime, const uint256& hash); + bool UndoRestrictedBlock(const std::vector>& events, + const uint256& hash, const uint256& previousHash, + std::vector>& restored); + bool WriteRestrictedIndexTip(const uint256& hash); + bool ReadRestrictedIndexTip(uint256& hash); + bool WriteRestrictedIndexState(char state); + bool ReadRestrictedIndexState(char& state); + // Write / Read Database flags bool WriteFlag(const std::string &name, bool fValue); bool ReadFlag(const std::string &name, bool &fValue); diff --git a/src/init.cpp b/src/init.cpp index 94966838a2..98512d7ad0 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1655,6 +1655,13 @@ bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler) strLoadError = _("Error loading block database"); break; } +#ifdef ENABLE_WALLET + if ((fReset || fReindexChainState || fReindex) && + !pmyrestricteddb->WriteRestrictedIndexState('N')) { + strLoadError = _("Unable to reset restricted address history for reindex"); + break; + } +#endif // If the loaded chain has a wrong genesis, bail out immediately // (we're likely using a testnet datadir, or the other way around). @@ -1928,6 +1935,8 @@ bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler) // ********************************************************* Step 8: load wallet #ifdef ENABLE_WALLET + if (!BackfillMyRestrictedIndex(chainparams)) + return InitError(_("Unable to rebuild restricted address history")); if (!OpenWallets()) return false; #else diff --git a/src/qt/myrestrictedassettablemodel.cpp b/src/qt/myrestrictedassettablemodel.cpp index f16734c6f0..569c4e3e11 100644 --- a/src/qt/myrestrictedassettablemodel.cpp +++ b/src/qt/myrestrictedassettablemodel.cpp @@ -7,6 +7,7 @@ #include "addresstablemodel.h" #include "assets/assets.h" +#include "chainparams.h" #include "guiconstants.h" #include "guiutil.h" #include "optionsmodel.h" @@ -84,7 +85,18 @@ class MyRestrictedAssetsTablePriv qDebug() << "MyRestrictedAssetsTablePriv::refreshWallet"; cacheMyAssetData.clear(); vectAssetData.clear(); + LOCK(cs_main); + if (!pmyrestricteddb || !BackfillMyRestrictedIndex(GetParams())) { + qWarning() << "Restricted address history could not be loaded"; + return; + } + char indexState = 0; + if (!pmyrestricteddb->ReadRestrictedIndexState(indexState) || indexState != 'C') { + qWarning() << "Restricted address history is incomplete"; + return; + } { + LOCK(wallet->cs_wallet); std::vector > myTaggedAddresses; std::vector > myRestrictedAddresses; pmyrestricteddb->LoadMyTaggedAddresses(myTaggedAddresses); @@ -94,13 +106,16 @@ class MyRestrictedAssetsTablePriv for (auto item : myTaggedAddresses) { MyRestrictedAssetRecord sub; sub.address = std::get<0>(item); + const isminetype ownership = GetRestrictedAddressOwnership(*this->wallet, sub.address); + if (!(ownership & ISMINE_ALL)) + continue; sub.assetName = std::get<1>(item); sub.time = std::get<3>(item); if (IsAssetNameAQualifier(sub.assetName)) sub.type = std::get<2>(item) ? MyRestrictedAssetRecord::Type::Tagged : MyRestrictedAssetRecord::Type::UnTagged; else if (IsAssetNameAnRestricted(sub.assetName)) sub.type = std::get<2>(item) ? MyRestrictedAssetRecord::Type::Frozen : MyRestrictedAssetRecord::Type::UnFrozen; - sub.involvesWatchAddress = GetRestrictedAddressOwnership(*this->wallet, sub.address) & ISMINE_WATCH_ONLY; + sub.involvesWatchAddress = ownership & ISMINE_WATCH_ONLY; vectAssetData.push_back(qMakePair(QString::fromStdString(std::get<0>(item)), QString::fromStdString(std::get<1>(item)))); cacheMyAssetData[qMakePair(QString::fromStdString(std::get<0>(item)), QString::fromStdString(std::get<1>(item)))] = sub; } @@ -109,6 +124,26 @@ class MyRestrictedAssetsTablePriv } void updateMyRestrictedAssets(const QString &address, const QString& asset_name, const int type, const qint64& date) { + if (type == RESTRICTED_ASSET_VIEW_RELOAD) { + parent->beginResetModel(); + refreshWallet(); + parent->endResetModel(); + return; + } + const isminetype ownership = GetRestrictedAddressOwnership(*this->wallet, address.toStdString()); + if (!(ownership & ISMINE_ALL)) + return; + QPair pair(address, asset_name); + if (type < 0) { + const int row = vectAssetData.indexOf(pair); + if (row >= 0) { + parent->beginRemoveRows(QModelIndex(), row, row); + vectAssetData.removeAt(row); + cacheMyAssetData.remove(pair); + parent->endRemoveRows(); + } + return; + } MyRestrictedAssetRecord rec; if (IsAssetNameAQualifier(asset_name.toStdString())) { @@ -121,13 +156,16 @@ class MyRestrictedAssetsTablePriv rec.assetName = asset_name.toStdString(); rec.address = address.toStdString(); - QPair pair(address, asset_name); if (cacheMyAssetData.contains(pair)) { rec.involvesWatchAddress = cacheMyAssetData[pair].involvesWatchAddress; cacheMyAssetData[pair] = rec; + const int row = vectAssetData.indexOf(pair); + if (row >= 0) + Q_EMIT parent->dataChanged(parent->index(row, 0), + parent->index(row, parent->columnCount(QModelIndex()) - 1)); } else { rec.involvesWatchAddress = - GetRestrictedAddressOwnership(*this->wallet, address.toStdString()) & ISMINE_WATCH_ONLY; + ownership & ISMINE_WATCH_ONLY; parent->beginInsertRows(QModelIndex(), 0, 0); cacheMyAssetData[pair] = rec; vectAssetData.push_front(pair); diff --git a/src/rpc/messages.cpp b/src/rpc/messages.cpp index 99bdb846e6..87d52ba8b0 100644 --- a/src/rpc/messages.cpp +++ b/src/rpc/messages.cpp @@ -6,6 +6,7 @@ #include "assets/assetdb.h" #include "assets/messages.h" #include "assets/myassetsdb.h" +#include "chainparams.h" #include #include "tinyformat.h" @@ -417,13 +418,26 @@ UniValue viewmytaggedaddresses(const JSONRPCRequest& request) { std::vector > myTaggedAddresses; + CWallet* const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) + return NullUniValue; + + LOCK(cs_main); if (!pmyrestricteddb) throw JSONRPCError(RPC_DATABASE_ERROR, "My restricted database is not available"); + if (!BackfillMyRestrictedIndex(GetParams())) + throw JSONRPCError(RPC_DATABASE_ERROR, "Unable to rebuild restricted address history"); + char indexState = 0; + if (!pmyrestricteddb->ReadRestrictedIndexState(indexState) || indexState != 'C') + throw JSONRPCError(RPC_DATABASE_ERROR, "Restricted address history is incomplete; restore pruned block data and reindex"); + LOCK(pwallet->cs_wallet); pmyrestricteddb->LoadMyTaggedAddresses(myTaggedAddresses); UniValue myTags(UniValue::VARR); for (auto item : myTaggedAddresses) { + if (!(GetRestrictedAddressOwnership(*pwallet, std::get<0>(item)) & ISMINE_ALL)) + continue; UniValue obj(UniValue::VOBJ); obj.push_back(Pair("Address", std::get<0>(item))); @@ -461,13 +475,26 @@ UniValue viewmyrestrictedaddresses(const JSONRPCRequest& request) { std::vector > myRestrictedAddresses; + CWallet* const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) + return NullUniValue; + + LOCK(cs_main); if (!pmyrestricteddb) throw JSONRPCError(RPC_DATABASE_ERROR, "My restricted database is not available"); + if (!BackfillMyRestrictedIndex(GetParams())) + throw JSONRPCError(RPC_DATABASE_ERROR, "Unable to rebuild restricted address history"); + char indexState = 0; + if (!pmyrestricteddb->ReadRestrictedIndexState(indexState) || indexState != 'C') + throw JSONRPCError(RPC_DATABASE_ERROR, "Restricted address history is incomplete; restore pruned block data and reindex"); + LOCK(pwallet->cs_wallet); pmyrestricteddb->LoadMyRestrictedAddresses(myRestrictedAddresses); UniValue myRestricted(UniValue::VARR); for (auto item : myRestrictedAddresses) { + if (!(GetRestrictedAddressOwnership(*pwallet, std::get<0>(item)) & ISMINE_ALL)) + continue; UniValue obj(UniValue::VOBJ); obj.push_back(Pair("Address", std::get<0>(item))); diff --git a/src/validation.cpp b/src/validation.cpp index 52bfff91e4..0da3425507 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2603,6 +2603,98 @@ static bool ContextualCheckBlock(const CBlock& block, CValidationState& state, const Consensus::Params& consensusParams, const CBlockIndex* pindexPrev, CAssetsCache* assetCache); +#ifdef ENABLE_WALLET +static void CollectRestrictedEvents(const CBlock& block, + std::vector>& events) +{ + for (const auto& tx : block.vtx) { + for (const auto& output : tx->vout) { + CNullAssetTxData data; + std::string address; + if (AssetNullDataFromScript(output.scriptPubKey, data, address)) + events.emplace_back(address, data); + } + } +} + +bool BackfillMyRestrictedIndex(const CChainParams& chainparams) +{ + static bool incompleteChecked = false; + LOCK(cs_main); + if (!pmyrestricteddb) + return true; + + char state = 0; + uint256 indexedTip; + const bool hasState = pmyrestricteddb->ReadRestrictedIndexState(state); + const bool hasTip = pmyrestricteddb->ReadRestrictedIndexTip(indexedTip); + const uint256 activeTip = chainActive.Tip() ? chainActive.Tip()->GetBlockHash() : uint256(); + if (hasState && state == 'C' && hasTip && indexedTip == activeTip) + return true; + if (hasState && state == 'P' && incompleteChecked) + return true; + + int startHeight = 0; + bool reset = !hasState || (state != 'C' && state != 'S') || !hasTip; + if (!reset && !indexedTip.IsNull()) { + const auto found = mapBlockIndex.find(indexedTip); + if (found == mapBlockIndex.end() || !chainActive.Contains(found->second)) { + reset = true; + } else { + startHeight = found->second->nHeight + 1; + } + } + + for (int height = reset ? 0 : startHeight; height <= chainActive.Height(); ++height) { + const CBlockIndex* pindex = chainActive[height]; + if (GetTxAssetDeploymentContextLocked(pindex->pprev, chainparams.GetConsensus()).fRestrictedAssetsDeployed && + !(pindex->nStatus & BLOCK_HAVE_DATA)) { + LogPrintf("Restricted address history is incomplete: block %d is pruned or missing\n", height); + incompleteChecked = true; + return pmyrestricteddb->WriteRestrictedIndexState('P'); + } + } + + if (reset) { + LogPrintf("Rebuilding restricted address index from active chain\n"); + if (!pmyrestricteddb->WriteRestrictedIndexState('N') || + !pmyrestricteddb->EraseAllRestrictedEvents() || + !pmyrestricteddb->WriteRestrictedIndexTip(uint256()) || + !pmyrestricteddb->WriteRestrictedIndexState('S')) + return error("BackfillMyRestrictedIndex(): could not reset restricted address index"); + } else if (!pmyrestricteddb->WriteRestrictedIndexState('S')) { + return error("BackfillMyRestrictedIndex(): could not mark scan in progress"); + } + + for (int height = startHeight; height <= chainActive.Height(); ++height) { + boost::this_thread::interruption_point(); + if (ShutdownRequested()) + return false; + const CBlockIndex* pindex = chainActive[height]; + if (GetTxAssetDeploymentContextLocked(pindex->pprev, chainparams.GetConsensus()).fRestrictedAssetsDeployed) { + CBlock block; + if (!ReadBlockFromDisk(block, pindex, chainparams.GetConsensus())) { + LogPrintf("Restricted address history is incomplete: block %d cannot be read\n", height); + incompleteChecked = true; + return pmyrestricteddb->WriteRestrictedIndexState('P'); + } + std::vector> events; + CollectRestrictedEvents(block, events); + if (!events.empty() && + !pmyrestricteddb->WriteRestrictedBlock(events, block.nTime, pindex->GetBlockHash())) + return error("BackfillMyRestrictedIndex(): failed writing block %d", height); + } + if ((height % 256) == 0 && !pmyrestricteddb->WriteRestrictedIndexTip(pindex->GetBlockHash())) + return error("BackfillMyRestrictedIndex(): failed checkpointing block %d", height); + } + + if (!pmyrestricteddb->WriteRestrictedIndexTip(activeTip) || + !pmyrestricteddb->WriteRestrictedIndexState('C')) + return error("BackfillMyRestrictedIndex(): could not finish restricted address index"); + return true; +} +#endif + /** Apply the effects of this block (with given index) on the UTXO set represented by coins. * Validity checks that depend on the UTXO set are also done; ConnectBlock() * can fail if those validity checks fail (among other reasons). */ @@ -3097,19 +3189,18 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd } } #ifdef ENABLE_WALLET - if (assetContext.fRestrictedAssetsDeployed && myNullAssetData.size() && pmyrestricteddb) { - for (auto item : myNullAssetData) { - if (IsAssetNameAQualifier(item.second.asset_name)) { - // TODO we can add block height to this data also, and use it to pull more info on when this was tagged/untagged - pmyrestricteddb->WriteTaggedAddress(item.first, item.second.asset_name, item.second.flag ? true : false, block.nTime); - } else if (IsAssetNameAnRestricted(item.second.asset_name)) { - pmyrestricteddb->WriteRestrictedAddress(item.first, item.second.asset_name, item.second.flag ? true : false, block.nTime); + if (!ignoreAddressIndex && pmyrestricteddb) { + if (!pmyrestricteddb->WriteRestrictedBlock(myNullAssetData, block.nTime, + pindex->GetBlockHash())) + return AbortNode(state, "Failed to write restricted address index"); + if (assetContext.fRestrictedAssetsDeployed) { + for (auto& item : myNullAssetData) { + for (CWalletRef wallet : vpwallets) { + if (GetRestrictedAddressOwnership(*wallet, item.first) & ISMINE_ALL) + wallet->UpdateMyRestrictedAssets(item.first, item.second.asset_name, + item.second.flag, block.nTime); + } } - - - if (vpwallets.size()) - vpwallets[0]->UpdateMyRestrictedAssets(item.first, item.second.asset_name, item.second.flag, block.nTime); - } } #endif @@ -3409,6 +3500,36 @@ bool static DisconnectTip(CValidationState& state, const CChainParams& chainpara CBlock& block = *pblock; if (!ReadBlockFromDisk(block, pindexDelete, chainparams.GetConsensus())) return error("DisconnectTip() : Failed to read block"); +#ifdef ENABLE_WALLET + std::vector> restoredRestrictedEvents; + bool reloadRestrictedViews = false; + if (pmyrestricteddb) { + char indexState = 0; + uint256 indexedTip; + if (pmyrestricteddb->ReadRestrictedIndexState(indexState) && indexState == 'C' && + pmyrestricteddb->ReadRestrictedIndexTip(indexedTip) && + indexedTip == pindexDelete->GetBlockHash()) { + std::vector> events; + if (GetTxAssetDeploymentContextLocked(pindexDelete->pprev, chainparams.GetConsensus()).fRestrictedAssetsDeployed) + CollectRestrictedEvents(block, events); + const uint256 previousHash = pindexDelete->pprev ? pindexDelete->pprev->GetBlockHash() : uint256(); + if (!pmyrestricteddb->UndoRestrictedBlock(events, indexedTip, previousHash, + restoredRestrictedEvents)) { + indexState = 'N'; + reloadRestrictedViews = true; + } + } else { + reloadRestrictedViews = true; + if (indexState != 'P') + indexState = 'N'; + } + if (indexState == 'N') { + restoredRestrictedEvents.clear(); + if (!pmyrestricteddb->WriteRestrictedIndexState('N')) + return AbortNode(state, "Failed to invalidate restricted address index"); + } + } +#endif // Apply the block atomically to the chain state. int64_t nStart = GetTimeMicros(); { @@ -3444,6 +3565,25 @@ bool static DisconnectTip(CValidationState& state, const CChainParams& chainpara // Update chainActive and related variables. UpdateTip(pindexDelete->pprev, chainparams); +#ifdef ENABLE_WALLET + if (reloadRestrictedViews) { + std::string address; + std::string assetName; + for (CWalletRef wallet : vpwallets) { + wallet->UpdateMyRestrictedAssets(address, assetName, RESTRICTED_ASSET_VIEW_RELOAD, 0); + } + } else { + for (const auto& event : restoredRestrictedEvents) { + std::string address = std::get<0>(event); + std::string assetName = std::get<1>(event); + for (CWalletRef wallet : vpwallets) { + if (GetRestrictedAddressOwnership(*wallet, address) & ISMINE_ALL) + wallet->UpdateMyRestrictedAssets(address, assetName, + std::get<2>(event), std::get<3>(event)); + } + } + } +#endif // Let wallets know transactions went from 1-confirmed to // 0-confirmed or conflicted: GetMainSignals().BlockDisconnected(pblock); diff --git a/src/validation.h b/src/validation.h index 9c1fc7e9a2..4f3e9d998e 100644 --- a/src/validation.h +++ b/src/validation.h @@ -538,6 +538,10 @@ extern CMessageChannelDB *pmessagechanneldb; /** Global variable that points to my wallets restricted database (protected by cs_main) */ extern CMyRestrictedDB *pmyrestricteddb; +#ifdef ENABLE_WALLET +bool BackfillMyRestrictedIndex(const CChainParams& chainparams); +#endif + /** Global variable that points to the active restricted asset database (protected by cs_main) */ extern CRestrictedDB *prestricteddb; diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 06861c3291..48bede622c 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -106,6 +106,7 @@ 'feature_rewards.py', 'feature_rewards_restart.py', 'wallet_pq_recovery.py', + 'wallet_restricted_multiwallet.py', 'wallet_basic.py', 'mempool_limit.py', 'feature_assets.py', diff --git a/test/functional/wallet_restricted_multiwallet.py b/test/functional/wallet_restricted_multiwallet.py new file mode 100755 index 0000000000..5fce52c941 --- /dev/null +++ b/test/functional/wallet_restricted_multiwallet.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The Raven Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Restricted address views follow wallet ownership across load orders and index rebuilds.""" + +import os +import shutil + +from test_framework.test_framework import RavenTestFramework +from test_framework.util import assert_equal, wait_until + + +TAG = '#WALLETVIEWTAG' +RESTRICTED = '$WALLETVIEWASSET' + + +class RestrictedMultiWalletTest(RavenTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [['-wallet=w1', '-wallet=w2', '-assetindex']] + + def assert_views(self, first_addresses, second_addresses): + node = self.nodes[0] + for name, expected in [('w1', first_addresses), ('w2', second_addresses)]: + wallet = node.get_wallet_rpc(name) + tagged = { + entry['Address']: entry for entry in wallet.viewmytaggedaddresses() + if entry['Tag Name'] == TAG + } + frozen = { + entry['Address']: entry for entry in wallet.viewmyrestrictedaddresses() + if entry['Asset Name'] == RESTRICTED + } + assert_equal(set(tagged), set(expected)) + assert_equal(set(frozen), set(expected)) + for address in expected: + assert 'Assigned' in tagged[address] + assert 'Restricted' in frozen[address] + + def add_events(self, addresses): + issuer = self.nodes[0].get_wallet_rpc('w1') + for address in addresses: + issuer.addtagtoaddress(TAG, address) + issuer.generate(1) + issuer.freezeaddress(RESTRICTED, address) + issuer.generate(1) + + def run_test(self): + node = self.nodes[0] + issuer = node.get_wallet_rpc('w1') + other = node.get_wallet_rpc('w2') + issuer.generate(432) + assert_equal(node.getblockchaininfo()['bip9_softforks']['messaging_restricted']['status'], 'active') + issuer.issuequalifierasset(TAG) + issuer.generate(1) + issuer.issue('WALLETVIEWASSET') + issuer.generate(1) + issuer.issuerestrictedasset(RESTRICTED, 100, 'true', issuer.getnewaddress()) + issuer.generate(1) + + first_addresses = [issuer.getnewaddress()] + second_addresses = [other.getnewaddress()] + self.add_events(first_addresses + second_addresses) + self.assert_views(first_addresses, second_addresses) + + self.restart_node(0, ['-wallet=w2', '-wallet=w1', '-assetindex']) + self.assert_views(first_addresses, second_addresses) + issuer = self.nodes[0].get_wallet_rpc('w1') + other = self.nodes[0].get_wallet_rpc('w2') + first_addresses.append(issuer.getnewaddress()) + second_addresses.append(other.getnewaddress()) + self.add_events(first_addresses[-1:] + second_addresses[-1:]) + self.assert_views(first_addresses, second_addresses) + + self.stop_node(0) + shutil.rmtree(os.path.join(self.nodes[0].datadir, 'regtest', 'myrestricted')) + self.start_node(0, ['-wallet=w1', '-wallet=w2', '-assetindex']) + self.assert_views(first_addresses, second_addresses) + + disconnected = self.nodes[0].getbestblockhash() + self.nodes[0].invalidateblock(disconnected) + other = self.nodes[0].get_wallet_rpc('w2') + frozen = { + entry['Address'] for entry in other.viewmyrestrictedaddresses() + if entry['Asset Name'] == RESTRICTED + } + assert_equal(frozen, {second_addresses[0]}) + self.nodes[0].reconsiderblock(disconnected) + self.assert_views(first_addresses, second_addresses) + + height = self.nodes[0].getblockcount() + self.restart_node(0, ['-wallet=w2', '-wallet=w1', '-assetindex', '-reindex']) + wait_until(lambda: self.nodes[0].getblockcount() == height, + err_msg='reindex did not restore the active chain', timeout=60) + self.assert_views(first_addresses, second_addresses) + + +if __name__ == '__main__': + RestrictedMultiWalletTest().main() From 54cd1befeb117b6b83987ef81a35ac1a8d11d839 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:04:58 +0200 Subject: [PATCH 185/195] [FINDING-101] Record wallet isolation and remaining GUI proof --- ...0025-v4.8-security-remediation-register.md | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 6ffe512e6f..72bfcbe479 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -6933,7 +6933,27 @@ does not close those release conditions. - **Regression required:** Two wallets in both load orders receive distinct tag/freeze events. Each RPC and GUI view shows only its own data before and after restart. -- **Final status:** OPEN. +- **Remediation commit:** `4f026d02b` records tag and freeze events in a + node-global index independent of wallet load order. The selected wallet + filters RPC and Qt rows by address ownership. Startup rebuilds historical + events before loading wallets; a missing pruned block marks the index + incomplete and the wallet RPC refuses to show a partial history. Per-block + undo restores rows on reorg; an index mismatch invalidates the index and + queues a Qt model reload. Reindex explicitly resets the index state. +- **Verification:** The new `wallet_restricted_multiwallet.py` failed on the + prior binary because wallet B inherited wallet A's tagged address. It + passes after the fix across both wallet load orders, restart, index deletion + and rebuild, reorg and reconsideration, and full reindex. Existing + `feature_restricted_assets.py` and `feature_restricted_verifydb.py` pass. + `make -j4` and mandatory `make check -j4` pass. The new regression is + registered in the test runner and both CI workflows. A separate review of + the fallback reorg path found and then cleared a stale Qt cache risk. +- **Residual limitation:** Qt's test entry point is disabled in this branch, + so the GUI display itself still needs manual qualification. Pruned nodes + without the required historical blocks cannot rebuild the wallet view; + they report incomplete history instead of presenting a partial result. +- **Final status:** FIXED locally; exact-SHA CI and manual GUI qualification + remain due under FINDING-067. ### FINDING-102: Message channel subscription uses wallet zero From 6cab6706eb343a452be2efbd7115ed1697572835 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:12:43 +0200 Subject: [PATCH 186/195] [FINDING-062] Exercise bit 12 reorg and PQ sigop boundary --- .github/workflows/build-raven.yml | 2 +- .github/workflows/rip25-v48-final-gate.yml | 2 +- .../devtools/check-rip25-v48-invariants.sh | 5 +- src/chainparams.cpp | 4 + .../feature_pq_activation_reorg_sigops.py | 162 ++++++++++++++++++ test/functional/test_runner.py | 1 + 6 files changed, 173 insertions(+), 3 deletions(-) create mode 100755 test/functional/feature_pq_activation_reorg_sigops.py diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index b9c4d654cf..be4349a4f6 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -91,7 +91,7 @@ jobs: python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ - feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ + feature_asset_activation_reorg.py feature_pq_activation_reorg_sigops.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ feature_restricted_dbcrash.py feature_restricted_verifydb.py feature_restricted_verifydb_root.py \ diff --git a/.github/workflows/rip25-v48-final-gate.yml b/.github/workflows/rip25-v48-final-gate.yml index f9e8dc0b01..7e712b173a 100644 --- a/.github/workflows/rip25-v48-final-gate.yml +++ b/.github/workflows/rip25-v48-final-gate.yml @@ -105,7 +105,7 @@ jobs: python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ - feature_asset_activation_reorg.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ + feature_asset_activation_reorg.py feature_pq_activation_reorg_sigops.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ feature_restricted_dbcrash.py feature_restricted_verifydb.py feature_restricted_verifydb_root.py \ diff --git a/contrib/devtools/check-rip25-v48-invariants.sh b/contrib/devtools/check-rip25-v48-invariants.sh index b0b9a0bc7f..4fe442d8cc 100755 --- a/contrib/devtools/check-rip25-v48-invariants.sh +++ b/contrib/devtools/check-rip25-v48-invariants.sh @@ -100,6 +100,9 @@ require_fixed 'wallet_pq_recovery.py' .github/workflows/build-raven.yml 'release require_fixed "'wallet_restricted_multiwallet.py'" test/functional/test_runner.py 'restricted multiwallet regression is not registered' require_fixed 'wallet_restricted_multiwallet.py' .github/workflows/rip25-v48-final-gate.yml 'final gate omits restricted multiwallet regression' require_fixed 'wallet_restricted_multiwallet.py' .github/workflows/build-raven.yml 'release workflow omits restricted multiwallet regression' +require_fixed "'feature_pq_activation_reorg_sigops.py'" test/functional/test_runner.py 'PQ activation reorg and sigop regression is not registered' +require_fixed 'feature_pq_activation_reorg_sigops.py' .github/workflows/rip25-v48-final-gate.yml 'final gate omits PQ activation reorg and sigop regression' +require_fixed 'feature_pq_activation_reorg_sigops.py' .github/workflows/build-raven.yml 'release workflow omits PQ activation reorg and sigop regression' # GLM-003: contextual 8 -> 12 -> 16 MWU and UTXO-bound 8x discount. require_fixed 'VersionBitsStateSinceHeight' src/validation.cpp 'deterministic RIP-25 phase boundary missing' @@ -827,6 +830,6 @@ echo "$fuzz_smoke_output" python3 contrib/devtools/test-required-functional-gate.py PYTHONPATH=test/functional python3 -m unittest test_framework.test_cache_peers python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py + wallet_encryption_rewrite.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py feature_pq_activation_reorg_sigops.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py echo 'RIP-25/v4.8 structural + behavioral invariants: OK' diff --git a/src/chainparams.cpp b/src/chainparams.cpp index c5941611ed..c986bb74f8 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -77,6 +77,10 @@ void CChainParams::UpdateVersionBitsParameters(Consensus::DeploymentPos d, int64 { consensus.vDeployments[d].nStartTime = nStartTime; consensus.vDeployments[d].nTimeout = nTimeout; + // A regtest bit-12 override exercises the actual versionbits boundary. + // The default regtest mode keeps PQ rules immediately available. + if (strNetworkID == "regtest" && d == Consensus::DEPLOYMENT_PQ_HYBRID) + consensus.nPQHybridEnabled = false; } void CChainParams::TurnOffSegwit() { diff --git a/test/functional/feature_pq_activation_reorg_sigops.py b/test/functional/feature_pq_activation_reorg_sigops.py new file mode 100755 index 0000000000..62d1a10fe4 --- /dev/null +++ b/test/functional/feature_pq_activation_reorg_sigops.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The Raven Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Bit 12 candidate-parent script rules and the 80,000 sigop boundary.""" + +import configparser +import os + +from test_framework import util as test_util +from test_framework.blocktools import add_witness_commitment, create_block, create_coinbase +from test_framework.mininode import COutPoint, CTransaction, CTxIn, CTxOut, from_hex, to_hex +from test_framework.script import CScript, OP_CHECKMULTISIG, OP_CHECKSIG, OP_TRUE +from test_framework.test_framework import RavenTestFramework +from test_framework.util import assert_equal + + +PQ_VERSION = 0x30001040 +PQ_BIP9_ARGS = [ + '-blockversion={}'.format(PQ_VERSION), + '-vbparams=pq_hybrid:0:999999999999', + '-persistmempool=0', +] + + +class PQActivationReorgSigopsTest(RavenTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + + @staticmethod + def status(node): + return node.getblockchaininfo()['bip9_softforks']['pq_hybrid']['status'] + + @staticmethod + def candidate(node, parent, height, tx=None, sigop_edge=False): + coinbase = create_coinbase(height) + if sigop_edge: + # 999 bare CHECKMULTISIGs plus 20 CHECKSIGs cost exactly + # 20,000 * 4 = 80,000. An active PQ input adds one. + script = CScript([OP_CHECKMULTISIG] * 999 + [OP_CHECKSIG] * 20) + assert_equal(script.get_sig_op_count(False) * 4, 80_000) + coinbase.vout.append(CTxOut(0, script)) + coinbase.rehash() + block = create_block(int(parent, 16), coinbase, + node.getblockheader(parent)['time'] + 1) + block.nVersion = PQ_VERSION + if tx is not None: + block.vtx.append(tx) + if not tx.wit.is_null(): + add_witness_commitment(block) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + return block + + @staticmethod + def submit(node, block): + return node.submitblock(block.serialize(True).hex()) + + @staticmethod + def spend(txid, vout): + tx = CTransaction() + tx.vin = [CTxIn(COutPoint(int(txid, 16), vout), b'', 0xffffffff)] + tx.vout = [CTxOut(90_000_000, CScript([OP_TRUE]))] + return tx + + def run_test(self): + config = configparser.ConfigParser() + config.read(self.options.configfile) + test_util.x16r_hash_cmd = os.path.join( + config['environment']['BUILDDIR'], 'src', 'test', + 'test_raven_hash' + config['environment']['EXEEXT']) + + node = self.nodes[0] + # Obtain the wallet key before switching regtest from its usual + # immediate PQ mode to real bit-12 versionbits activation. + pq_address = node.getnewpqaddress() + pq_script = bytes.fromhex(node.validateaddress(pq_address)['scriptPubKey']) + self.restart_node(0, PQ_BIP9_ARGS) + node = self.nodes[0] + assert_equal(self.status(node), 'defined') + + node.generate(99) + parent = node.getbestblockhash() + coinbase = create_coinbase(100) + coinbase.vout[0].nValue -= 200_000_000 + coinbase.vout.extend([CTxOut(100_000_000, pq_script), + CTxOut(100_000_000, pq_script)]) + coinbase.rehash() + funding = create_block(int(parent, 16), coinbase, + node.getblockheader(parent)['time'] + 1) + funding.nVersion = PQ_VERSION + funding.hashMerkleRoot = funding.calc_merkle_root() + funding.solve() + assert_equal(self.submit(node, funding), None) + funding_txid = funding.vtx[0].hash + + node.generate(187) + assert_equal(node.getblockcount(), 287) + assert_equal(self.status(node), 'locked_in') + node.generate(144) + assert_equal(node.getblockcount(), 431) + assert_equal(self.status(node), 'active') + self.log.info('Bit 12 reaches ACTIVE for candidate H432') + + malformed = self.spend(funding_txid, 1) + active_accept = node.testmempoolaccept([to_hex(malformed)])[0] + assert_equal(active_accept['allowed'], False) + assert 'Witness program hash mismatch' in active_accept['reject-reason'] + self.log.info('Malformed active PQ spend: %s', active_accept['reject-reason']) + active_parent = node.getbestblockhash() + invalid_432 = self.candidate(node, active_parent, 432, malformed) + assert_equal(self.submit(node, invalid_432), + 'block-validation-failed') + assert_equal(node.getbestblockhash(), active_parent) + + signed = node.signrawtransaction(to_hex(self.spend(funding_txid, 1))) + assert_equal(signed['complete'], True) + valid_432 = self.candidate(node, active_parent, 432, + from_hex(CTransaction(), signed['hex'])) + assert_equal(self.submit(node, valid_432), None) + node.generate(1) + active_tip = node.getbestblockhash() + assert_equal(node.getblockcount(), 433) + + # The same malformed spend is valid under the old rule on a branch + # whose own H430 parent is still LOCKED_IN. This node has already + # validated an active-chain PQ signature at H432. + old_parent = node.getblockhash(430) + old_431 = self.candidate(node, old_parent, 431, malformed) + assert_equal(self.submit(node, old_431), 'inconclusive') + node.invalidateblock(active_parent) + assert_equal(node.getbestblockhash(), old_431.hash) + assert_equal(self.status(node), 'active') + assert_equal(node.verifychain(4, 2), True) + self.log.info('Malformed PQ spend accepted at H431 on old-rule branch') + node.invalidateblock(old_431.hash) + assert_equal(node.getbestblockhash(), old_parent) + assert_equal(self.status(node), 'locked_in') + node.reconsiderblock(active_parent) + assert_equal(node.getbestblockhash(), active_tip) + + # CheckBlock sees precisely 80,000 legacy sigops. ConnectBlock + # must add one for the native PQ input and reject 80,001. + sigop_parent = node.getbestblockhash() + signed_second = node.signrawtransaction( + to_hex(self.spend(funding_txid, 2))) + assert_equal(signed_second['complete'], True) + excess = self.candidate(node, sigop_parent, 434, + from_hex(CTransaction(), signed_second['hex']), + sigop_edge=True) + assert_equal(self.submit(node, excess), 'bad-blk-sigops') + assert_equal(node.getbestblockhash(), sigop_parent) + self.log.info('80,001 sigops rejected with PQ input') + at_limit = self.candidate(node, sigop_parent, 434, sigop_edge=True) + assert_equal(self.submit(node, at_limit), None) + assert_equal(node.getbestblockhash(), at_limit.hash) + self.log.info('80,000 legacy sigops accepted') + + +if __name__ == '__main__': + PQActivationReorgSigopsTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 48bede622c..c81f25e049 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -114,6 +114,7 @@ 'feature_messaging.py', 'feature_assets_reorg.py', 'feature_asset_activation_reorg.py', + 'feature_pq_activation_reorg_sigops.py', 'feature_transfer_script_reorg.py', 'feature_rip5_activation_reorg.py', 'feature_enforced_coinbase_activation.py', From a8fa47e3948551b2074d300cda05af49c5643176 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:15:51 +0200 Subject: [PATCH 187/195] [FINDING-184] Freeze exact-multiple reward batch defect --- ...0025-v4.8-security-remediation-register.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 72bfcbe479..4c4e9c0ec4 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -10239,3 +10239,30 @@ does not close those release conditions. - **Final status:** FIXED on tested source `77d8f4c65`. Final Gate `38853895080` passed on the same SHA. Final release-SHA qualification remains FINDING-067. + +### FINDING-184: Exact-multiple reward recipients create an empty payout batch + +- **Severity:** MEDIUM. A completed payout can be reported as a failed + distribution, complicating safe operator reconciliation. +- **Frozen initial status:** OPEN before remediation. +- **Affected RIP-25 invariant:** Asset reward retries must not manufacture a + transaction with no recipients after committing all intended batches. +- **Affected Core 4.8.0 fix:** Inherited from official Core 4.8.0 tag + `22549129888d02e0e08fcdb9f96f3c699167e774`. +- **Root cause:** `src/assets/rewards.cpp` computes the transaction count as + `paymentDetails.size() / MAX_PAYMENTS_PER_TRANSACTION + 1`. When the + recipient count is an exact multiple of 1,000, the final iteration starts + at the end of the vector and calls `BuildTransaction` with no recipients. +- **Concrete scenario:** A reward has exactly 1,000 eligible addresses. The + first batch can be committed, then an empty second batch fails creation and + leaves a failure status for a distribution whose intended payout already + occurred. A repeated request is rejected as a duplicate. +- **Expected behavior:** Use a checked ceiling count, skip empty batches, + preserve each durable batch intent, and report completion only after all + real batches are resolved. +- **Regression required:** Prove the old exact-multiple failure, then test + recipient counts around the 1,000 boundary, including zero, 999, 1,000, + 1,001, and 2,000. Assert no empty transaction attempt and no duplicate + payout on restart. +- **Remediation commit:** PENDING. +- **Final status:** OPEN. From c90814b1a64e0008a746cf994ca30b1094282c99 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:27:18 +0200 Subject: [PATCH 188/195] [FINDING-184] Avoid empty reward payout batches --- src/assets/rewards.cpp | 4 ++-- src/assets/rewards.h | 7 +++++++ src/test/assets/asset_tests.cpp | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/assets/rewards.cpp b/src/assets/rewards.cpp index 2a5678140d..a81c451c19 100644 --- a/src/assets/rewards.cpp +++ b/src/assets/rewards.cpp @@ -231,9 +231,9 @@ void DistributeRewardSnapshot(CWallet * p_wallet, const CRewardSnapshot& p_rewar return; } - int nNumberOfTransactions = ((int)paymentDetails.size() / MAX_PAYMENTS_PER_TRANSACTION) + 1; + size_t nNumberOfTransactions = CountRewardBatches(paymentDetails.size()); CRewardSnapshot copyRewardSnapshot = p_rewardSnapshot; - for (int i = 0; i < nNumberOfTransactions; i++) { + for (size_t i = 0; i < nNumberOfTransactions; i++) { uint256 txid; if (pDistributeSnapshotDb->GetDistributeTransaction(p_rewardSnapshot.GetHash(), i, txid)) { CTransactionRef txRef; diff --git a/src/assets/rewards.h b/src/assets/rewards.h index 72849fbe4b..149d9311e0 100644 --- a/src/assets/rewards.h +++ b/src/assets/rewards.h @@ -11,6 +11,7 @@ #include "assettypes.h" #include +#include #include #include #include @@ -27,6 +28,12 @@ extern std::map mapRewardSnapshots; static const std::string ADDRESS_COMMA_DELIMITER = ","; const int MAX_PAYMENTS_PER_TRANSACTION = 1000; +inline size_t CountRewardBatches(size_t recipientCount) +{ + return recipientCount / MAX_PAYMENTS_PER_TRANSACTION + + (recipientCount % MAX_PAYMENTS_PER_TRANSACTION != 0); +} + // Individual payment record struct OwnerAndAmount { diff --git a/src/test/assets/asset_tests.cpp b/src/test/assets/asset_tests.cpp index efc7351c10..37ff995b8d 100644 --- a/src/test/assets/asset_tests.cpp +++ b/src/test/assets/asset_tests.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include @@ -731,4 +732,19 @@ BOOST_FIXTURE_TEST_SUITE(asset_tests, BasicTestingSetup) } + BOOST_AUTO_TEST_CASE(reward_batch_boundaries) + { + const std::pair cases[] = { + {0, 0}, {1, 1}, {999, 1}, {1000, 1}, + {1001, 2}, {1999, 2}, {2000, 2}, {2001, 3} + }; + for (const auto& item : cases) { + const size_t batches = CountRewardBatches(item.first); + BOOST_CHECK_EQUAL(batches, item.second); + for (size_t batch = 0; batch < batches; ++batch) { + BOOST_CHECK(batch * MAX_PAYMENTS_PER_TRANSACTION < item.first); + } + } + } + BOOST_AUTO_TEST_SUITE_END() From c70679b80d68c92148f1d24f8a568aefd0272186 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:27:58 +0200 Subject: [PATCH 189/195] [FINDING-185] Add pinned fontconfig source fallback --- depends/packages/fontconfig.mk | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/depends/packages/fontconfig.mk b/depends/packages/fontconfig.mk index 7b0eb1a8f1..a0b291cedb 100644 --- a/depends/packages/fontconfig.mk +++ b/depends/packages/fontconfig.mk @@ -1,10 +1,17 @@ package=fontconfig $(package)_version=2.12.1 $(package)_download_path=https://mirrors.mit.edu/macports/distfiles/fontconfig/ +$(package)_fallback_download_path=https://distfiles.macports.org/fontconfig $(package)_file_name=$(package)-$($(package)_version).tar.bz2 $(package)_sha256_hash=b449a3e10c47e1d1c7a6ec6e2016cca73d3bd68fbbd4f0ae5cc6b573f7d6c7f3 $(package)_dependencies=freetype expat +define $(package)_fetch_cmds +( test -f $(fontconfig_source) || \ + $(call fetch_file_inner,fontconfig,$(fontconfig_download_path),$(fontconfig_download_file),$(fontconfig_file_name),$(fontconfig_sha256_hash)) || \ + $(call fetch_file_inner,fontconfig,$(fontconfig_fallback_download_path),$(fontconfig_download_file),$(fontconfig_file_name),$(fontconfig_sha256_hash)) ) +endef + define $(package)_set_vars $(package)_config_opts=--disable-docs --disable-static endef From 24d8cae5bb0f3d871497c2887f21d030ca35dc04 Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:30:44 +0200 Subject: [PATCH 190/195] [FINDING-062][FINDING-184][FINDING-185] Record local verification --- ...0025-v4.8-security-remediation-register.md | 73 ++++++++++++++++--- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index 4c4e9c0ec4..fed1902b91 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -3833,26 +3833,35 @@ before closing that finding. one-long, bad key length, bad stack count, bad program/hash, wrong version, appended sighash byte, alternate mode, bit flips, pre-activation, boundary, reorg, and sigop-over-limit cases. -- **Remediation commits:** `2dc33e518`, `4ab366663`, and CI gate - `5ebeb2b09`. +- **Remediation commits:** `2dc33e518`, `4ab366663`, CI gate + `5ebeb2b09`, synthetic branch test `1e33543fd`, and full-chain + regression `6cab6706e`. - **Verification:** Static production-signed valid and one-bit-invalid PQ transactions now run through the existing `tx_valid` and `tx_invalid` JSON vector infrastructure with explicit mainnet context. A separate 13-case C++ mutation matrix checks malformed stack shape, signature, public key, program, and suffix behavior. Both polarities and the mutation matrix pass under `make check` and the mandatory behavioral gate. -- **Final status:** MITIGATED. Exact activation-height plus/minus two, - invalidate/reconsider, and PQ sigop-over-limit transaction vectors remain - outstanding; FINDING-061 covers the unsettled sigop cost. +- **Final status:** FIXED locally for the current one-unit PQ sigop rule. + Exact-SHA CI remains due. FINDING-061 retains the policy decision on + whether to change that cost. Post-checkpoint extension: `1e33543fd` adds a synthetic BIP9 branch test for the two candidate blocks before and after bit 12 activation. It checks the 8 MWU to 12 MWU weight and serialized-size transition, witness discount, and a competing unsignaled branch. A deliberate mutation retaining the 8 MWU limit after activation failed six assertions; the restored source -passed the focused test and `make check -j4`. A real invalidate/reconsider -full-chain vector and PQ sigop-over-limit transaction vector remain due, so -FINDING-062 remains MITIGATED. +passed the focused test and `make check -j4`. +The full-chain extension `6cab6706e` then verified a valid ML-DSA spend +in candidate block H432, rejection of a malformed spend at H432, acceptance +of that same spend on a pre-activation H431 side branch after active-chain +validation, invalidate/reconsider with `verifychain(4, 2)`, rejection at +80,001 sigops when the PQ input adds one, and acceptance of the 80,000 +legacy control. The malformed side-branch case failed on the old code and +passed after the regtest versionbits override was added. The focused test +passed twice; `make -j4 check` passed after the final code was built. +FINDING-062 is FIXED locally for the current consensus cost, with exact-SHA +CI qualification still pending. ### FINDING-063: Repository KAT coverage cannot detect backend drift @@ -10264,5 +10273,49 @@ does not close those release conditions. recipient counts around the 1,000 boundary, including zero, 999, 1,000, 1,001, and 2,000. Assert no empty transaction attempt and no duplicate payout on restart. -- **Remediation commit:** PENDING. -- **Final status:** OPEN. +- **Remediation commit:** `c90814b1a` uses a ceiling batch count without + manufacturing an empty batch at exact multiples of 1,000. +- **Verification:** `asset_tests/reward_batch_boundaries` failed six checks + on the old calculation at zero, 1,000, and 2,000 recipients. It passed + after the fix for zero, one, 999, 1,000, 1,001, 1,999, 2,000, and 2,001. + `make -j4 check`, `feature_rewards.py`, and the separate + `feature_rewards_restart.py` single-payout retry test passed. The boundary + unit test proves that no empty batch is requested; the restart test checks + that persisted batch intent does not duplicate a payout. +- **Final status:** FIXED locally; exact-SHA CI remains due. + +### FINDING-185: ARM release builds lack a fontconfig source fallback + +- **Severity:** MEDIUM release availability. Both ARM packages are absent from + a Build Raven run that otherwise passed its security gate and other targets. +- **Frozen initial status:** OPEN on 2026-10-19, before changing the pinned + fontconfig depends recipe. +- **Affected RIP-25 invariant:** Every release target must package the same + tested source SHA, with every downloaded dependency checked against its + pinned digest. +- **Observed failure:** Build Raven `39143408033` at `54cd1befe` passed its + required security tests, Windows, macOS, and Linux. ARM32 job + `117725944066` and ARM64 job `117725944097` timed out six times fetching + `fontconfig-2.12.1.tar.bz2` from `mirrors.mit.edu`, then called the generic + fallback with an empty URL (`curl: (3)`). Final Gate `39143408017` passed + at the same SHA, including its ARM builds, but does not replace the missing + Build Raven packages. +- **Root cause:** `depends/packages/fontconfig.mk` has one download host and + no package-specific fallback. The generic fallback in `depends/funcs.mk` + has no configured `FALLBACK_DOWNLOAD_PATH` in these jobs. +- **Expected behavior:** Use an available pinned-source mirror if the primary + times out, reject bytes that do not match the existing SHA-256, and produce + both ARM packages in Build Raven on the final candidate SHA. +- **Regression required:** Force primary failure and prove the fallback fetch + passes the pinned digest; prove altered bytes are rejected; re-run both ARM + release builds and all other mandatory jobs at one exact SHA. +- **Remediation commit:** `c70679b80` adds a package-specific MacPorts + archive fallback while retaining the pinned version and SHA-256. +- **Verification:** A forced primary failure reached the empty fallback URL + on the original recipe. With the fix, the same failure fetched the + 1,608,901-byte MacPorts archive and verified digest + `b449a3e10c47e1d1c7a6ec6e2016cca73d3bd68fbbd4f0ae5cc6b573f7d6c7f3`. + A deliberately wrong expected digest rejected the archive without + installing a source file. Direct primary and fallback fetches passed. +- **Final status:** FIXED locally; Build Raven ARM32 and ARM64 packages on + the final candidate SHA remain due under FINDING-067. From 1f06ce5b83a7386a7fe36cdf9740b8f4514225ac Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:36:43 +0200 Subject: [PATCH 191/195] [FINDING-186] Freeze legacy message export gap --- ...0025-v4.8-security-remediation-register.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/doc/RIP-0025-v4.8-security-remediation-register.md b/doc/RIP-0025-v4.8-security-remediation-register.md index fed1902b91..e5400ca548 100644 --- a/doc/RIP-0025-v4.8-security-remediation-register.md +++ b/doc/RIP-0025-v4.8-security-remediation-register.md @@ -10319,3 +10319,27 @@ does not close those release conditions. installing a source file. Direct primary and fallback fetches passed. - **Final status:** FIXED locally; Build Raven ARM32 and ARM64 packages on the final candidate SHA remain due under FINDING-067. + +### FINDING-186: Legacy global message records become unreachable after wallet scoping + +- **Severity:** MEDIUM. An operator upgrading a node can lose access through + RPC to historical messages and subscriptions even though their LevelDB rows + remain on disk. +- **Frozen initial status:** OPEN, found during independent review of the + uncommitted FINDING-102 wallet-scoping remedy. +- **Affected RIP-25 invariant:** Wallet isolation must preserve a safe way to + retrieve pre-upgrade notification history without guessing which wallet + owned each global record. +- **Affected Core 4.8.0 fix:** Core 4.8.0 stored messages under global `Z` + keys and subscribed channels under global `C` keys in separate LevelDBs. +- **Root cause:** The wallet-scoped message RPCs read only new `W` and `c` + records. Existing global rows have no wallet identity or reliable manual + subscription provenance, so automatically assigning them to a wallet would + leak data or fabricate user preferences. +- **Expected behavior:** Offer an explicitly labeled, read-only export of + persisted unscoped rows with each message outpoint and lossless payload. + Fail rather than silently omit malformed rows. Never import, delete, or + assign the exported rows to a wallet automatically. +- **Regression required:** Show the old RPC has no safe export, then test + valid legacy rows, an empty legacy database, and malformed legacy values. +- **Final status:** OPEN. From 212610f62d35fdf9d324d59d04c9618b5470dace Mon Sep 17 00:00:00 2001 From: ALENOC <185200505+ALENOC@users.noreply.github.com> Date: Mon, 19 Oct 2026 06:29:39 +0200 Subject: [PATCH 192/195] [FINDING-102][FINDING-186] Scope message state by wallet and export legacy records --- .github/workflows/build-raven.yml | 2 +- .github/workflows/rip25-v48-final-gate.yml | 2 +- .../devtools/check-rip25-v48-invariants.sh | 7 +- src/assets/messages.cpp | 640 ++++++++++++++++-- src/assets/messages.h | 35 +- src/assets/myassetsdb.cpp | 568 +++++++++++++++- src/assets/myassetsdb.h | 68 +- src/coins.cpp | 70 +- src/init.cpp | 61 +- src/rpc/client.cpp | 1 + src/rpc/messages.cpp | 141 ++-- src/test/assets/asset_tests.cpp | 38 ++ src/test/rpc_tests.cpp | 64 ++ src/validation.cpp | 96 ++- src/validation.h | 4 + test/functional/test_runner.py | 1 + .../wallet_messaging_multiwallet.py | 256 +++++++ 17 files changed, 1895 insertions(+), 159 deletions(-) create mode 100755 test/functional/wallet_messaging_multiwallet.py diff --git a/.github/workflows/build-raven.yml b/.github/workflows/build-raven.yml index be4349a4f6..d9af7ee028 100644 --- a/.github/workflows/build-raven.yml +++ b/.github/workflows/build-raven.yml @@ -90,7 +90,7 @@ jobs: run: | python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ + wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py wallet_messaging_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ feature_asset_activation_reorg.py feature_pq_activation_reorg_sigops.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ diff --git a/.github/workflows/rip25-v48-final-gate.yml b/.github/workflows/rip25-v48-final-gate.yml index 7e712b173a..740620882a 100644 --- a/.github/workflows/rip25-v48-final-gate.yml +++ b/.github/workflows/rip25-v48-final-gate.yml @@ -104,7 +104,7 @@ jobs: run: | python3 contrib/devtools/test-required-functional-gate.py python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ + wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py wallet_messaging_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py \ feature_asset_activation_reorg.py feature_pq_activation_reorg_sigops.py feature_transfer_script_reorg.py feature_rip5_activation_reorg.py feature_enforced_coinbase_activation.py \ feature_asset_dbcrash.py feature_asset_dbcrash_transfer.py feature_asset_db_missing_block.py \ feature_asset_verifydb_sameblock.py feature_transfer_mempool_reorg.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py feature_transfer_sidebranch.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_loadblock.py \ diff --git a/contrib/devtools/check-rip25-v48-invariants.sh b/contrib/devtools/check-rip25-v48-invariants.sh index 4fe442d8cc..82062b54c3 100755 --- a/contrib/devtools/check-rip25-v48-invariants.sh +++ b/contrib/devtools/check-rip25-v48-invariants.sh @@ -100,6 +100,9 @@ require_fixed 'wallet_pq_recovery.py' .github/workflows/build-raven.yml 'release require_fixed "'wallet_restricted_multiwallet.py'" test/functional/test_runner.py 'restricted multiwallet regression is not registered' require_fixed 'wallet_restricted_multiwallet.py' .github/workflows/rip25-v48-final-gate.yml 'final gate omits restricted multiwallet regression' require_fixed 'wallet_restricted_multiwallet.py' .github/workflows/build-raven.yml 'release workflow omits restricted multiwallet regression' +require_fixed "'wallet_messaging_multiwallet.py'" test/functional/test_runner.py 'messaging multiwallet regression is not registered' +require_fixed 'wallet_messaging_multiwallet.py' .github/workflows/rip25-v48-final-gate.yml 'final gate omits messaging multiwallet regression' +require_fixed 'wallet_messaging_multiwallet.py' .github/workflows/build-raven.yml 'release workflow omits messaging multiwallet regression' require_fixed "'feature_pq_activation_reorg_sigops.py'" test/functional/test_runner.py 'PQ activation reorg and sigop regression is not registered' require_fixed 'feature_pq_activation_reorg_sigops.py' .github/workflows/rip25-v48-final-gate.yml 'final gate omits PQ activation reorg and sigop regression' require_fixed 'feature_pq_activation_reorg_sigops.py' .github/workflows/build-raven.yml 'release workflow omits PQ activation reorg and sigop regression' @@ -631,7 +634,7 @@ require_fixed 'python3 contrib/devtools/test-required-functional-gate.py' "$fina require_fixed 'python3 test/functional/test_runner.py --require-tests' "$final_gate" 'functional gate permits absent or skipped security tests' require_fixed 'feature_mempool_clear_reissue.py' "$final_gate" 'final gate omits inherited mempool clear regression' require_fixed 'feature_mempool_reissue_restart.py' "$final_gate" 'final gate omits inherited reissue restart regression' -require_fixed 'wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py' "$final_gate" 'required wallet, rewards, PQ recovery, restricted multiwallet, asset-scope, and chainstate-ahead functional tests are missing' +require_fixed 'wallet_encryption_rewrite.py wallet_asset_multiwallet.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py wallet_messaging_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py' "$final_gate" 'required wallet, rewards, PQ recovery, restricted multiwallet, messaging multiwallet, asset-scope, and chainstate-ahead functional tests are missing' require_fixed 'wallet_asset_multiwallet.py' .github/workflows/build-raven.yml 'release security job omits the multiwallet asset regression' require_fixed "'wallet_asset_multiwallet.py'" test/functional/test_runner.py 'multiwallet asset regression is not in the functional suite' require_fixed 'id: posttest_integrity' "$final_gate" 'final gate does not recheck source after security tests' @@ -830,6 +833,6 @@ echo "$fuzz_smoke_output" python3 contrib/devtools/test-required-functional-gate.py PYTHONPATH=test/functional python3 -m unittest test_framework.test_cache_peers python3 test/functional/test_runner.py --require-tests --jobs=2 \ - wallet_encryption_rewrite.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py feature_pq_activation_reorg_sigops.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py + wallet_encryption_rewrite.py feature_rewards_restart.py wallet_pq_recovery.py wallet_restricted_multiwallet.py wallet_messaging_multiwallet.py rpc_assettransfer.py feature_chainstate_ahead.py feature_pq_activation_reorg_sigops.py feature_pq_asset_anchor.py feature_pq_asset_authority_selection.py feature_pq_asset_source_rpc.py feature_pq_asset_wallet_ownership.py feature_mempool_clear_reissue.py feature_mempool_reissue_restart.py echo 'RIP-25/v4.8 structural + behavioral invariants: OK' diff --git a/src/assets/messages.cpp b/src/assets/messages.cpp index 180e2ed401..3f93bd9f2d 100644 --- a/src/assets/messages.cpp +++ b/src/assets/messages.cpp @@ -5,26 +5,118 @@ #include #include #include +#include #include