reth EIP-7928 report: see how a Hackerbane report is structured. Open the report → (opens in a new tab)

· Barney Chambers

The Bitcoin Core BIP460 CISA review, explained

The published Bitcoin Core review covers BIP460 cross-input signature aggregation in pull request 36122 at commit 224e15f. It records one High, four Medium, two Low, and five informational findings, all open, and it does not include a remediation review.

BIP460 is a draft rule for sharing one signature across several inputs of a Bitcoin transaction. The inputs are witness version 2 programs, 32 bytes, the same shape as a Taproot output. Half-aggregation, BIP458, compresses signatures that were produced separately. Full aggregation, BIP459, also called DahLIAS, builds one signature from a shared nonce. BIP460 is the consensus encoding that would put those aggregates into a transaction. At review time all three documents were still drafts.

The code under review is Bitcoin Core pull request 36122, on branch bip460, at 224e15fd891334f0a2fb924d2ac60bc08ed9e4f3. The published report is the Bitcoin Core BIP460 security audit. The review ran from 8 September 2026 through 15 September 2026. In that tree the feature is always active on regtest. On mainnet, testnet, testnet4, and signet it is inactive. This note explains that report. It does not re-read the pull request, and the report's own scope stops at this commit.

The cover counts are 0 Critical, 1 High, 4 Medium, 2 Low, and 5 Informational. The remediation record says all 12 findings are open, pull request 36122 is unmerged, and no patch commit was reviewed. Version 0.6 of the document, dated 5 October 2026, changed the document id and nothing else.

A PSBT is a partially signed Bitcoin transaction: the file wallets pass around while signatures are still being collected. "Complete" means the wallet believes every input is signed and the transaction can be finalized.

What was run

The functional test feature_cisa.py passed on a Release build of this commit. The passing cases were regtest activation, v2 standard, opt-out, half-aggregation, full aggregation, mixed groups, an annex, the script path, cross-mode, invalid structures, and invalid blocks on both the queued path and -par=1. wallet_cisa.py also passed: descriptor receive, a half-agg joinpsbts, full-agg nonce combine, reserved nonces, and MuSig2 half-aggregation. That wallet test does not place two different half-agg signatures on one input. Conflicting full-agg nonces already return "PSBTs not compatible."

Fuzzing was not run. The test_bitcoin unit tests were not re-run. No static analyzer was run. feature_cisa.py does not cover the false "complete" bit, a half-agg PSBT merge, or the finalizer skipping a signed member. Those three were checked with RPC probes on the same binary. The results are under HACK-01, HACK-03, and HACK-04.

HACK-01. A stand-in witness looks signed

High, score 8.2, open. This is the only High. The report says coins do not move.

For a witness version 2 keypath spend, VerifyScript looks at the witness, sees the marker that means "this input belongs to an aggregate," and returns success. It does not check the aggregate signature. The real check is CISACheck, which consensus still queues when a transaction spends these inputs. A block that runs that path still verifies the aggregate.

The wallet does not wait for that check. ProduceSignature and DataFromTransaction treat VerifyScript success as "this input is complete." A 32-byte stand-in, an empty full-agg member, or a 65-byte stand-in whose last byte is 0xbc or 0xbd is therefore a successful VerifyScript. Callers that stop there treat an unsigned member as signed.

The probe on this binary built a two-input transaction, put a 32-byte half-agg member on both inputs, and left off the group final. signrawtransactionwithwallet returned complete=true with no input errors. testmempoolaccept then rejected it with mempool-script-verify-flag-failed and "Invalid CISA aggregation group structure." The completeness bit was wrong. The mempool check still ran.

HACK-02. The wallet names a type the public chains have not activated

Medium, score 6.3, open. The script classifier treats OP_2 plus a 32-byte program as the new CISA type, and it will describe that output as cisa() or rawcisa(). The consensus flag that actually enforces witness version 2 is set only when CISA deployment is active. In this tree that is regtest, on bit 3, always active. Mainnet, testnet, testnet4, and signet leave the deployment at the default: bit 28, never active.

On current master, the same output is already an unknown witness program. Unknown witness outputs already relay. With the version-specific flag off, consensus already accepts a spend of an unknown witness version. The mempool still discourages that spend, so it does not relay, and a miner can include it.

This tree adds a defined CISA type on top of that. A wallet running this binary that pays to a cisa() descriptor creates an output whose consensus spend, on every public chain in this binary, takes the flag-off success path. An empty witness or a random 64-byte witness is consensus-valid there. The mempool still rejects the spend. The default receive path remains the ordinary Taproot descriptor tr, so this requires someone to opt into cisa().

HACK-03. Merging two partial transactions keeps one signature and drops the other

Medium, score 6.3, open. When two PSBTs are merged, a conflict on the aggregation mode, the full-agg public nonce, or the full-agg partial signature is rejected. A conflict on the half-agg signature is not. If the destination already has a half-agg signature, the other one is ignored.

The probe built two PSBTs for the same unsigned two-input half-agg transaction and put a different 64-byte signature on input 0 of each. combinepsbt succeeded. The signature that was already present is the one that remained, so the order of the two PSBTs decides which signer survives. The person combining them can believe both parties signed. FinalizeCISAInputs then builds the group from that mixed material.

HACK-04. Finalizing can emit a second group, which consensus rejects

Medium, score 5.7, open. The finalizer walks the inputs of one mode and skips any input it already considers signed. Consensus collects every marked keypath in order and allows one finished half-agg group and one finished full-agg group. A later member of the same mode, after that group is closed, is invalid.

The probe signed both inputs of a two-input half-agg PSBT without finalizing, then cleared input 1 and finalized. Input 0 received a one-input final witness. Restoring input 1's mode and signature and finalizing again skipped input 0, because it was already signed, and wrote a second final on input 1. finalizepsbt reported complete=true. testmempoolaccept rejected the transaction with the same invalid-group error. If the later input is missing a signature, the walk instead aborts and writes nothing.

HACK-05. The hiding provider still reveals the full-aggregation nonce

Medium, score 5.0, open. A hiding signing provider is supposed to refuse private keys when secrets are hidden. GetKey does that. The CISA calls that set, read, and delete a full-aggregation secret nonce forward to the inner provider anyway.

The PSBT updater path constructs that hiding provider with secrets hidden, because it is not being asked to sign. It can still read, overwrite, or delete a DahLIAS secret nonce. The header on that nonce type says reuse leaks the private key. The sign path itself does not finish through a hiding provider, because creating the partial signature also needs the key, and hiding blocks the key. The report's condition for a key leak is a caller that reads the nonce from the hiding provider and signs a second time with the copy. Deleting the session while another signer is mid-sign burns the session.

HACK-06. The aggregate check never hits the signature cache

Low, score 2.5, open. Ordinary script checks go through the signature cache, so a block that is reconnected can reuse work it already did. CISACheck calls the aggregate verifier with no cache. Reconnecting a block that missed the script-execution cache repeats the full elliptic-curve verification. The cost stays on the order of one Schnorr check per aggregated input. The report files this as a missed cache, and the transaction is still verified.

HACK-07. A sighash failure is reported as a bad aggregate signature

Low, score 2.5, open. Building the Schnorr message can fail. The case the report isolates is SIGHASH_SINGLE when the input has no matching output, and a bad hash type. Taproot reports that as a sighash error. This code reports "CISA aggregate signature verification failed." The transaction is still invalid. A test or an RPC reject reason that matches on the aggregate-verify string hides a sighash-single bug. The parser already rejects a bad explicit sighash byte, so the remaining case is SIGHASH_SINGLE with no matching output.

HACK-08. Opt-out inputs store the default sighash

Informational, score 0.0, open. The comment on the parsed witness says an opt-out input does not extract its sighash byte, and that the type is the default unless an explicit byte is present. The parser then stores the default for every 64-byte element and every 65-byte element that is not a marker. That includes a 65-byte Taproot signature whose last byte is a real hash type. Consensus skips opt-out inputs, so it does not read the stored field today. A later change that trusts the stored type for an opt-out input would hash the wrong message.

HACK-09. The parallel check queue is reserved one slot short

Informational, score 0.0, open. When checks are queued, the code reserves one slot per input, then adds one aggregate check plus one script check per input. The vector grows once, and that extra copy is a single validation check. The report's fix is to reserve one more slot when the aggregate check will be queued.

HACK-10. The cryptography modules are not merged upstream

Informational, score 0.0, open. The pull request vendors two unmerged secp256k1 changes into Bitcoin Core: half-aggregation, secp256k1 pull request 1566, and full aggregation, secp256k1 pull request 1754. Consensus verification calls into them. The report's concern is a later secp256k1 rebase changing the bytes consensus checks, and a MuSig session refactor regressing existing MuSig2 signing. A merge of those modules on the secp256k1 repository itself was out of scope. The recommendation is to wait for that merge, then re-review the MuSig extract against the BIP327 tests before bumping the subtree in Core.

HACK-11. CISA shares version bit 28 with the test dummy

Informational, score 0.0, open. A version-bits deployment defaults to bit 28 and to never-active. The test dummy uses bit 28 as well. Only regtest assigns CISA its own bit. The option that sets a deployment's start time can be pointed at CISA without giving CISA a unique bit. Two deployments that share bit 28 are dormant today. Turning CISA on that way collides with the test dummy's version-bits state. The report files a first-class witness version 2, before those chains have an explicit assignment, as HACK-02. This row is the shared bit underneath that.

HACK-12. One half-agg group and one full-agg group per transaction

Informational, score 0.0, open. Verification keeps a single half-agg group and a single full-agg group. After a group is closed, another member of that mode is invalid. The finalizer collects every input of a given marker into that one group, in index order. Two coinjoin batches of the same mode in one transaction are consensus-invalid. A wallet that assumes several groups of one mode will build transactions that fail. The report leaves the rule to the draft: if BIP460 wants one group per mode, the BIP should say so. If it wants several, the boundaries have to come from the finals instead of one closed-group flag.

The counts, the probe results, and the open status of HACK-01 through HACK-12 are in the BIP460 CISA report. Consensus-code reviews of this kind are the work described on blockchain infrastructure audit.