Hacktoberfest 2026: die Issues, die Maintainer für den Oktober markiert haben – offen und einsteigerfreundlich. Hacktoberfest-Issues durchsuchen

PSET outputs lose their nonce on round trip: `extract_tx` for unblinded outputs, `to_txout` for blinded ones

Offen
#299 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Bewertung

Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Anfängerfreundlichkeit
52/100
Issue-Typ
Bug
Klarheit
Größtenteils klar
Aktivitätsstatus
Aktiv
Tech-Stack
rust
Bereich
blockchain

Rechercherichtung

Start with the cited conversion code in src/pset/map/output.rs and src/pset/mod.rs, then run the two nonce round-trip tests from the reproduction. Trace how from_txout, Output::to_txout, and extract_tx choose nonce fields, and confirm the intended PSET semantics before changing them. Done means both unblinded and blinded round trips preserve the nonce while missing asset/value errors remain correct.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

While working on Sighashes in SimplicityHL, the LLM picked up this issue. I unfortunately don't know enough of the design to know whether this field was left out on purpose. I've created an issue so long to document the problem.

Summary

A transaction loaded with PartiallySignedTransaction::from_tx doesn't always come back with the
same output nonces. There are two converters from a PSET output back to a TxOut, and each one
drops the nonce in a different case:

Output loaded with from_tx extract_tx Output::to_txout
unblinded, with a nonce nonce lost (Null) ok
blinded, with a nonce ok nonce lost (Null)

Checked at 582ea613cb. The code is the same in 0.25.3 and 0.26.2.

Cause

Output::from_txout puts the nonce in ecdh_pubkey for a blinded output and in blinding_key
for an unblinded one
(src/pset/map/output.rs#L212-L222):

if txout.is_partially_blinded() {
    rv.ecdh_pubkey = txout.nonce.commitment().map(|pk| PublicKey { inner: pk, compressed: true });
} else {
    rv.blinding_key = txout.nonce.commitment().map(|pk| PublicKey { inner: pk, compressed: true });
}

PartiallySignedTransaction::extract_tx only reads ecdh_pubkey
(src/pset/mod.rs#L330-L333),
so it misses the unblinded case:

nonce: out
    .ecdh_pubkey
    .map(|x| confidential::Nonce::from(x.inner))
    .unwrap_or_default(),

Output::to_txout picks the field with Output::is_partially_blinded
(src/pset/map/output.rs#L250-L256).
That function requires is_marked_for_blinding(), which means blinding_key.is_some()
(L272-L283).
A blinded output from from_txout has ecdh_pubkey set but no blinding_key, so to_txout takes
the else branch, reads blinding_key, and gets nothing.

Because of this, changing extract_tx to call to_txout as it is now would move the bug to
blinded outputs rather than fix it.

Reproduction

use elements::confidential::{Asset, Nonce, Value};
use elements::pset::PartiallySignedTransaction;
use elements::secp256k1_zkp::{Generator, Keypair, Secp256k1, Tag, Tweak};
use elements::{AssetId, LockTime, Script, Transaction, TxOut, TxOutWitness};

fn tx_with_output(asset: Asset) -> Transaction {
    let secp = Secp256k1::new();
    let pk = Keypair::from_seckey_slice(&secp, &[1u8; 32]).unwrap().public_key();
    Transaction {
        version: 2,
        lock_time: LockTime::ZERO,
        input: vec![],
        output: vec![TxOut {
            asset,
            value: Value::Explicit(1_000),
            nonce: Nonce::Confidential(pk),
            script_pubkey: Script::new(),
            witness: TxOutWitness::default(),
        }],
    }
}

#[test]
fn unblinded_output_nonce_round_trip() {
    let tx = tx_with_output(Asset::Explicit(AssetId::default()));
    let pset = PartiallySignedTransaction::from_tx(tx.clone());

    assert_eq!(pset.outputs()[0].to_txout().nonce, tx.output[0].nonce); // passes
    assert_eq!(pset.extract_tx().unwrap().output[0].nonce, tx.output[0].nonce); // fails: Null
}

#[test]
fn blinded_output_nonce_round_trip() {
    let secp = Secp256k1::new();
    let asset = Generator::new_blinded(&secp, Tag::from([2u8; 32]), Tweak::from_slice(&[3u8; 32]).unwrap());
    let tx = tx_with_output(Asset::Confidential(asset));
    let pset = PartiallySignedTransaction::from_tx(tx.clone());

    assert_eq!(pset.extract_tx().unwrap().output[0].nonce, tx.output[0].nonce); // passes
    assert_eq!(pset.outputs()[0].to_txout().nonce, tx.output[0].nonce); // fails: Null
}

Both tests fail on their second assertion, with left: Null.

Impact

Values, assets and scripts are unaffected. The nonce is part of what signatures commit to (the
taproot sighash's sha_outputs, and Simplicity's outputs_hash). So the txid changes on a round
trip, and a signature made over the original transaction doesn't verify on the extracted one, and
vice versa. As far as I can tell this fails closed, with invalid signatures or a changed txid, and
doesn't put funds at risk.

The extract_tx case needs an unblinded output that carries a nonce, which is unusual. The
to_txout case affects ordinary blinded transactions loaded with from_tx.

Possible fix

Use one rule in both places, for example ecdh_pubkey.or(blinding_key), and have extract_tx build
its outputs with to_txout. extract_tx would still need to return its own errors when the asset
or value is missing. That makes both rows of the table round-trip, and ecdh_pubkey wins whenever
an output has been blinded.

I'm not sure whether this is the intended semantics. Putting an unblinded output's nonce in
blinding_key marks that output for blinding. Under strict PSET rules, extracting it before it has
been blinded might be meant to fail instead. I also haven't checked what Elements Core does here.
The history suggests the inconsistency is accidental: 724a14d changed extract_tx from
blinding_key to ecdh_pubkey, and dc350a0 replaced the ecdh_pubkey.or(blinding_key) rule in
to_txout that e73ee1f had added.

Unrelated, but nearby: extract_tx seems to return swapped errors. It returns
MissingOutputValue when the asset is missing and MissingOutputAsset when the value is missing
(src/pset/mod.rs#L319-L329).

I'm happy to open a PR with the tests and a fix if this direction sounds right.

Related

  • #292 fixed a similar round-trip problem for inputs: extract_tx left the peg-in and issuance
    flags in previous_output.vout.
Vorherrschende Sprache
Rust
Sterne
58
Forks
41
Ø Merge
2 T. 2 Std.
Gemergte PRs (30 T.)
1

Entwicklungsumgebung

Dieses Projekt bietet weder Dev-Container noch Dockerfile noch Beitragsleitfaden – die Einrichtung liegt bei Ihnen. Beginnen Sie mit der README; die allgemeinen Schritte stehen in unserem Leitfaden für den ersten Beitrag.

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus ElementsProject/rust-elements

Alle Issues in ElementsProject/rust-elements

Ähnliche Issues

Weitere Issues zu Rust

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.