PSET outputs lose their nonce on round trip: `extract_tx` for unblinded outputs, `to_txout` for blinded ones
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 52/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- rust
- 領域
- blockchain
調査の方向性
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.
索引モデルが issue の本文から書いたものです。
説明
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_txleft the peg-in and issuance
flags inprevious_output.vout.
- 主要言語
- Rust
- スター
- 58
- フォーク
- 41
- 平均マージ
- 2日 2時間
- マージ済み PR(30日)
- 1
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
ElementsProject/rust-elements のほかの issue
-
難易度 3/5 1〜2日 初心者へのやさしさ 45/100
ElementsProject/rust-elements#290 · コメント 1 件 ·
-
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
ElementsProject/rust-elements#277 ·
-
難易度 5/5 1週間以上 初心者へのやさしさ 30/100
ElementsProject/rust-elements#273 · コメント 3 件 ·
-
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
ElementsProject/rust-elements#268 · コメント 1 件 ·
-
難易度 3/5 1〜2日 初心者へのやさしさ 52/100
ElementsProject/rust-elements#262 · コメント 2 件 · リアクション 1 件 ·
ElementsProject/rust-elements の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
メンテナーはふだん 4 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
メンテナーはふだん 1 日以内に返信
-
Update dusk-bls12_381 to 0.16対応中かも @HDauven が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
-
`TcpListenerService` shares one `Extensions` store across all accepted connections対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
googlefonts/fontquant#43 ·