Skip to content

blind/blindpsbt fixes - #1592

Merged
delta1 merged 10 commits into
ElementsProject:masterfrom
delta1:2026-09-various-fixes
Sep 1, 2026
Merged

blind/blindpsbt fixes#1592
delta1 merged 10 commits into
ElementsProject:masterfrom
delta1:2026-09-various-fixes

Conversation

@delta1

@delta1 delta1 commented Sep 1, 2026

Copy link
Copy Markdown
Member

Fixes a number of small issues picked up during LLM scans

The raw-blind RPC path can reach SurjectOutput with an empty
surjection_targets vector (a zero-input tx with multiple blindable
outputs), which indexed element [0] of the empty vector and passed it to
secp256k1_surjectionproof_initialize, triggering undefined behaviour.
Reject empty target sets up front, matching the existing over-limit guard.
An explicit 9-byte value (or a null field) passed the IsNull() check and
its buffer was then parsed as a 33-byte Pedersen commitment, reading past
the end. Require IsCommitment() on both the value and asset fields so the
parser's length precondition holds and the out-of-bounds read is avoided.
…ueProof

A range-membership proof whose lower bound equalled the displayed PSET
amount was accepted even when the committed value was larger, because only
min_value was compared. Require both verified bounds to equal the claimed
amount so a proof can no longer understate an output's value.
A dynafed header always serializes block_height as part of its identity
(CBlockHeader::Serialize/GetHash), independent of the legacy
-con_blockheightinheader option. Previously the height was only validated
in ContextualCheckBlockHeader and only reconstructed in
CBlockIndex/CDiskBlockIndex::GetBlockHeader when that option was on, so a
dynafed header with a mismatched height could be accepted, and a header
rebuilt from an accepted index no longer matched the accepted header's hash
when the option was off.

Validate and reconstruct block_height for all dynafed headers regardless of
the option. Non-dynafed headers keep the legacy option behaviour. This does
not change any consensus rule: the height was already part of every dynafed
header's hash.
NextBlockIsParameterTransition computed the approval threshold as
(epoch_length*4)/5, which floor-divides. For epoch lengths not divisible by
5 this is below the intended at-least-four-fifths rule, so a transition
could pass with fewer than 80% of the epoch's blocks voting for it.

Use the overflow-safe ceiling N - N/5 (== ceil(N*4/5)). This is a no-op for
epoch lengths divisible by 5 (the only currently deployed case) and only
corrects the under-approximation for non-divisible epoch lengths.
CompressedHeader kept only the Bitcoin PoW fields and GetFullHeader()
reconstructed only those. In the low-work headers presync/redownload path
Elements headers were therefore rebuilt without block_height, proof,
dynafed params, or the signblock witness, so the reconstructed header's hash
no longer matched what was received and the fields signed/dynafed validation
needs were dropped.

Retain block_height, proof, m_dynafed_params, and m_signblock_witness in
CompressedHeader and reconstruct them in GetFullHeader(). This enlarges the
per-header redownload buffer footprint (previously assumed to be 48 bytes);
the comment and static_assert are updated accordingly. No consensus rule
changes: the reconstructed header now matches the header that was received.
BlindPSBT dereferenced output.amount without a nullopt check. A crafted v0
PSET output (m_blinder_index set, amount absent) reached the blinding loop
and dereferenced a disengaged std::optional, which is undefined behaviour.
Refuse such outputs with BlindingStatus::INVALID_BLINDER.
BlindPSBT passed the blinding pubkey straight to CKey::ECDH, whose only
validation is an assert on the peer key, so a crafted off-curve pubkey
(reaching IsBlinded() but failing IsFullyValid()) aborted the process.
Mirror the non-PSET path and return BlindingStatus::INVALID_BLINDER when the
pubkey is not fully valid.
CreateAssetSurjectionProof asserted on secp256k1_surjectionproof_generate
and _verify failure. A crafted PSET can supply unrelated tags/generators
with no known discrete-log relationship, causing generation to fail and the
assert to abort the process. Make these recoverable errors by returning
false.
@tomt1664

tomt1664 commented Sep 1, 2026

Copy link
Copy Markdown
Member

ACK c26d719 tested locally.

(test each commit failing on master, unrelated to this PR)

@delta1
delta1 merged commit 31e8f27 into ElementsProject:master Sep 1, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants