Skip to content

Commit 582ea61

Browse files
committed
Merge #298: blind: verify rangeproofs for confidential issuance amounts
173321e blind: verify rangeproofs for confidential issuance amounts (Byron Hambly) cb8e6f1 test: reproduce unverified confidential issuance amounts (Byron Hambly) Pull request description: `verify_tx_amt_proofs` pushed confidential issuance amount and inflation keys commitments into the balance sum without verifying any rangeproof. The commitment entered the aggregate sum as an unconstrained curve point, making the value-conservation check satisfiable for an arbitrary transaction carrying an issuance: a relying verifier could be induced to accept a transaction that mints value out of nothing. This PR verifies the accompanying rangeproof against the unblinded issuance generator, mirroring Elements Core's `VerifyIssuanceAmount`: the proof message is empty for issuances, and the generator is the unblinded generator of the issued asset (resp. reissuance token). A confidential issuance amount without a rangeproof is now rejected with `VerificationError::RangeProofMissing`. ACKs for top commit: apoelstra: ACK 173321e; successfully ran local tests Tree-SHA512: bde7960a4665032165999b9182a9a54b70579df3bcb15ed41ddc2b4a2380215fb03a72b0a7ac081f17b65df218f7fc75cc6ee1d639fb4e2ae21835e219cb0280
2 parents eef7f01 + 173321e commit 582ea61

2 files changed

Lines changed: 163 additions & 5 deletions

File tree

‎src/blind.rs‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,20 +1102,43 @@ impl Transaction {
11021102
if inp.has_issuance() {
11031103
let (asset_id, token_id) = inp.issuance_ids();
11041104
let arr = [
1105-
(inp.asset_issuance.amount, asset_id),
1106-
(inp.asset_issuance.inflation_keys, token_id),
1105+
(
1106+
inp.asset_issuance.amount,
1107+
asset_id,
1108+
&inp.witness.amount_rangeproof,
1109+
),
1110+
(
1111+
inp.asset_issuance.inflation_keys,
1112+
token_id,
1113+
&inp.witness.inflation_keys_rangeproof,
1114+
),
11071115
];
1108-
for (amt, asset) in &arr {
1116+
for (amt, asset, rangeproof) in &arr {
1117+
// Issuance pseudo-inputs are never asset-blinded: the
1118+
// generator is the unblinded generator of the issued
1119+
// (or reissuance token) asset.
1120+
let gen = Generator::new_unblinded(secp, asset.into_tag());
11091121
match amt {
11101122
Value::Null => {},
11111123
Value::Explicit(v) => {
1112-
let gen = Generator::new_unblinded(secp, asset.into_tag());
11131124
domain.push(gen);
11141125
let comm = PedersenCommitment::new_unblinded(secp, *v, gen);
11151126
in_commits.push(comm);
11161127
}
11171128
Value::Confidential(comm) => {
1118-
let gen = Generator::new_unblinded(secp, asset.into_tag());
1129+
// A confidential issuance amount must be accompanied
1130+
// by a rangeproof, verified against the unblinded
1131+
// issuance generator, mirroring Elements Core's
1132+
// `VerifyIssuanceAmount`. Without this check the
1133+
// commitment is an unconstrained term in the balance
1134+
// equation below. The rangeproof message is empty
1135+
// for issuances (Core passes an empty script).
1136+
let rangeproof = rangeproof
1137+
.as_ref()
1138+
.ok_or(VerificationError::RangeProofMissing(i))?;
1139+
rangeproof
1140+
.verify(secp, *comm, &[], gen)
1141+
.map_err(|e| VerificationError::RangeProofError(i, e))?;
11191142
domain.push(gen);
11201143
in_commits.push(*comm);
11211144
}

‎tests/poc_issuance_inflation.rs‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//! Regression tests for unverified confidential issuance amount rangeproofs:
2+
//! `Transaction::verify_tx_amt_proofs` used to push confidential *issuance*
3+
//! amount commitments into the balance sum without verifying any rangeproof,
4+
//! leaving an unconstrained term that made the value-conservation check
5+
//! vacuous for any transaction carrying an issuance.
6+
//!
7+
//! - `forged_issuance_amount_is_rejected` reproduces the attack: 1 L-BTC in,
8+
//! 100 L-BTC out, with the 99 L-BTC gap supplied by a bare curve point equal
9+
//! to 99 times the L-BTC generator in `asset_issuance.amount` and no
10+
//! rangeproof. Verification must now fail.
11+
//! - `blinded_issuance_amount_still_verifies` is the positive control: a
12+
//! properly blinded issuance amount (rangeproof present, produced by the
13+
//! crate's own blinding code) still verifies, so the fix does not break
14+
//! legitimate issuances.
15+
16+
use elements::confidential::{Asset, Nonce, Value};
17+
use elements::{
18+
AssetBlindingNonce, AssetEntropy, AssetId, AssetIssuance, LockTime, OutPoint, Script,
19+
Transaction, TxIn, TxOut, TxOutWitness, VerificationError,
20+
};
21+
use secp256k1_zkp::{Generator, PedersenCommitment, Secp256k1};
22+
23+
fn txout(asset: Asset, value: Value) -> TxOut {
24+
TxOut {
25+
asset,
26+
value,
27+
nonce: Nonce::Null,
28+
script_pubkey: Script::new(),
29+
witness: TxOutWitness::default(),
30+
}
31+
}
32+
33+
#[test]
34+
fn forged_issuance_amount_is_rejected() {
35+
let secp = Secp256k1::new();
36+
let asset = AssetId::LIQUID_BTC;
37+
38+
// Honest spent UTXO: 1 L-BTC, explicit (so its commitment is 1*G_LBTC).
39+
let spent_utxo = txout(Asset::Explicit(asset), Value::Explicit(1));
40+
41+
// Forged "confidential issuance amount": the curve point 99*G_LBTC.
42+
// It is not a valid opening of anything meaningful; no rangeproof is
43+
// produced for it (TxInWitness::default() has an EMPTY amount_rangeproof).
44+
let gen = Generator::new_unblinded(&secp, asset.into_tag());
45+
let forged_issuance_commitment = PedersenCommitment::new_unblinded(&secp, 99, gen);
46+
47+
let input = TxIn {
48+
previous_output: OutPoint::default(),
49+
asset_issuance: AssetIssuance {
50+
asset_blinding_nonce: AssetBlindingNonce::NEW_ISSUANCE,
51+
asset_entropy: AssetEntropy::NEW_ISSUANCE,
52+
amount: Value::Confidential(forged_issuance_commitment),
53+
inflation_keys: Value::Null, // never touched
54+
},
55+
..Default::default() // witness.amount_rangeproof is EMPTY
56+
};
57+
58+
// Attacker output: 100 L-BTC, explicit (no rangeproof / surjection proof required).
59+
let tx = Transaction {
60+
version: 2,
61+
lock_time: LockTime::ZERO,
62+
input: vec![input],
63+
output: vec![txout(Asset::Explicit(asset), Value::Explicit(100))],
64+
};
65+
66+
// Before the fix this returned Ok(()) and the tx minted 99 L-BTC.
67+
assert_eq!(
68+
tx.verify_tx_amt_proofs(&secp, std::slice::from_ref(&spent_utxo)),
69+
Err(VerificationError::RangeProofMissing(0)),
70+
"forged confidential issuance amount must not be accepted"
71+
);
72+
73+
// Non-vacuity control: the same tx with the forged issuance removed still
74+
// fails the aggregate balance check (1 in, 100 out).
75+
let mut control = tx.clone();
76+
control.input[0].asset_issuance.amount = Value::Null;
77+
assert_eq!(
78+
control.verify_tx_amt_proofs(&secp, &[spent_utxo]),
79+
Err(VerificationError::BalanceCheckFailed),
80+
"control tx without the forged issuance should fail the balance check"
81+
);
82+
}
83+
84+
#[test]
85+
fn blinded_issuance_amount_still_verifies() {
86+
use elements::confidential::ValueBlindingFactor;
87+
use rand::thread_rng;
88+
use secp256k1_zkp::SecretKey;
89+
90+
let secp = Secp256k1::new();
91+
let asset = AssetId::LIQUID_BTC;
92+
93+
let spent_utxo = txout(Asset::Explicit(asset), Value::Explicit(1));
94+
95+
let mut input = TxIn {
96+
previous_output: OutPoint::default(),
97+
asset_issuance: AssetIssuance {
98+
asset_blinding_nonce: AssetBlindingNonce::NEW_ISSUANCE,
99+
asset_entropy: AssetEntropy::NEW_ISSUANCE,
100+
amount: Value::Explicit(10),
101+
inflation_keys: Value::Null,
102+
},
103+
..Default::default()
104+
};
105+
106+
// Blind the issuance amount: this turns it into a confidential commitment
107+
// and generates the accompanying rangeproof. A zero blinding factor keeps
108+
// the balance simple (explicit outputs of 1 L-BTC + 10 of the new asset).
109+
let mut rng = thread_rng();
110+
input
111+
.blind_issuances_with_bfs(
112+
&secp,
113+
ValueBlindingFactor::zero(),
114+
ValueBlindingFactor::zero(),
115+
SecretKey::new(&mut rng),
116+
SecretKey::new(&mut rng),
117+
)
118+
.unwrap();
119+
assert!(input.asset_issuance.amount.is_confidential());
120+
121+
let (issued_asset, _token) = input.issuance_ids();
122+
123+
let tx = Transaction {
124+
version: 2,
125+
lock_time: LockTime::ZERO,
126+
input: vec![input],
127+
output: vec![
128+
txout(Asset::Explicit(asset), Value::Explicit(1)),
129+
txout(Asset::Explicit(issued_asset), Value::Explicit(10)),
130+
],
131+
};
132+
133+
tx.verify_tx_amt_proofs(&secp, &[spent_utxo])
134+
.expect("a legitimately blinded issuance amount must still verify");
135+
}

0 commit comments

Comments
 (0)