Add new CLI commands and update dependencies in Cargo files

- Introduced new CLI commands: Verify, BuildPsbt, and AttachSigs for enhanced functionality.
- Updated dependencies in Cargo.lock and Cargo.toml, including new packages like autocfg, bytemuck, and image.
- Improved the Proof struct with additional metadata for better proof handling.
This commit is contained in:
thePR0M3TH3AN committed 2025-05-28 23:27:09 -04:00
1 parent cb6f97ddd1
commit d4971ddba9
5 files changed
+226 -133

No files matched your search

Generated
+86 -4
View File
@@ -58,6 +58,12 @@ version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "base58" name = "base58"
version = "0.2.0" version = "0.2.0"
@@ -193,6 +199,12 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "bytemuck"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@@ -214,6 +226,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "checked_int_cast"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.39" version = "4.5.39"
@@ -263,6 +281,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.3" version = "1.0.3"
@@ -385,6 +409,20 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
[[package]]
name = "image"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"num-iter",
"num-rational",
"num-traits",
]
[[package]] [[package]]
name = "indicatif" name = "indicatif"
version = "0.17.11" version = "0.17.11"
@@ -470,6 +508,46 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "number_prefix" name = "number_prefix"
version = "0.4.0" version = "0.4.0"
@@ -524,7 +602,7 @@ dependencies = [
"hex", "hex",
"indicatif", "indicatif",
"merlin", "merlin",
"qrcodegen", "qrcode",
"rand", "rand",
"serde", "serde",
"serde_json", "serde_json",
@@ -541,10 +619,14 @@ dependencies = [
] ]
[[package]] [[package]]
name = "qrcodegen" name = "qrcode"
version = "1.8.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f"
dependencies = [
"checked_int_cast",
"image",
]
[[package]] [[package]]
name = "quote" name = "quote"
+30 -15
View File
@@ -1,36 +1,51 @@
#######################################
# Private Proof-of-Reserve – CLI crate #
#######################################
[package] [package]
name = "private_proof_of_reserve_cli" name = "private_proof_of_reserve_cli"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0"
description = "CLI that builds and verifies private proof-of-reserve ZK proofs for Bitcoin."
repository = "https://github.com/<your-org>/Private-Proof-Of-Reserve-Cli"
# ────────────────────────── Dependencies ────────────────────────── # ────────────────────────── Dependencies ──────────────────────────
[dependencies] [dependencies]
# CLI / UX
clap = { version = "4.5", features = ["derive"] } ## CLI / UX
clap = { version = "4.5", features = ["derive"] }
indicatif = "0.17" indicatif = "0.17"
# Bitcoin & RPC ## Bitcoin primitives & RPC
bitcoin = { version = "0.31", features = ["base64"] } bitcoin = { version = "0.31", features = ["base64"] }
bitcoincore-rpc = "0.18" bitcoincore-rpc = "0.18"
# Crypto primitives ## Cryptography & ZK
bulletproofs = { version = "4.0", optional = true } bulletproofs = { version = "4.0", optional = true }
curve25519-dalek-ng = "4.1" curve25519-dalek-ng = "4.1"
rand = "0.8"
merlin = "3.0" merlin = "3.0"
rand = "0.8"
# Hash / misc ## Hashing / encoding
sha2 = "0.10" sha2 = "0.10"
hex = "0.4" hex = "0.4"
base64 = "0.21" base64 = "0.21"
qrcodegen = "1.8" base58 = "0.2"
base58 = "0.2" qrcode = "0.12"
# Serde / error handling ## JSON / error handling
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
anyhow = "1.0" anyhow = "1.0"
# ───────────────────────── Features ───────────────────────────────
[features] [features]
default = ["bulletproofs-backend"] default = ["bulletproofs-backend"]
bulletproofs-backend = ["bulletproofs"] bulletproofs-backend = ["bulletproofs"]
# ───────────────────────── Release profile ────────────────────────
[profile.release]
codegen-units = 1
lto = true
panic = "abort"
+110 -114
View File
@@ -1,47 +1,55 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
//! Prototype CLI for a lower-bound Bitcoin proof-of-reserve. //! Prototype CLI for a lower-bound Bitcoin proof-of-reserve.
//! SECURITY: demo code only. //! SECURITY: demo code only. Do **not** use with real funds until
//! every TODO is implemented and the code is audited.
use std::{fs::File, path::PathBuf, str::FromStr, time::Duration}; use std::{fs::File, path::PathBuf, str::FromStr, time::Duration};
use anyhow::Result;
use anyhow::{Result, Context};
use base58::ToBase58;
use base64::{engine::general_purpose as b64, Engine as _};
use bitcoin::{Address, Txid}; use bitcoin::{Address, Txid};
use bitcoin::address::NetworkChecked; use bitcoin::address::NetworkChecked;
use bitcoincore_rpc::{Auth, Client, RpcApi}; use bitcoincore_rpc::{Auth, Client, RpcApi};
use bulletproofs::{BulletproofGens, PedersenGens, RangeProof};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use curve25519_dalek_ng::scalar::Scalar; use curve25519_dalek_ng::{ristretto::CompressedRistretto, scalar::Scalar};
use curve25519_dalek_ng::ristretto::CompressedRistretto; use merlin::Transcript;
use qrcode::{QrCode, EcLevel, render::unicode};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use bulletproofs::{BulletproofGens, PedersenGens, RangeProof};
use merlin::Transcript;
use sha2::{Digest, Sha256};
use base64::{engine::general_purpose, Engine as _};
use qrcodegen::{QrCode, QrCodeEcc};
use base58::ToBase58;
use serde_json::json; use serde_json::json;
use sha2::{Digest, Sha256};
/// ─────────────────────────── CLI ────────────────────────────
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about)] #[command(author, version, about)]
struct Cli { #[command(subcommand)] cmd: Cmd } struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum Cmd { enum Cmd {
/// One-shot: generate proof JSON (still asks for private key)
Generate { Generate {
#[arg(long)] rpc_url: String, #[arg(long)] rpc_url: String,
#[arg(long)] rpc_user: String, #[arg(long)] rpc_user: String,
#[arg(long)] rpc_pass: String, #[arg(long)] rpc_pass: String,
#[arg(long)] address: String, #[arg(long)] address: String,
#[arg(long)] min: u64, // sats #[arg(long)] min: u64,
#[arg(long)] height: u64, #[arg(long)] height: u64,
#[arg(long, default_value = "proof.json")] #[arg(long, default_value = "proof.json")]
out: PathBuf, out: PathBuf,
}, },
/// Verify proof JSON against a full node
Verify { Verify {
#[arg(long)] proof: PathBuf, #[arg(long)] proof: PathBuf,
#[arg(long)] rpc_url: String, #[arg(long)] rpc_url: String,
#[arg(long)] rpc_user: String, #[arg(long)] rpc_user: String,
#[arg(long)] rpc_pass: String, #[arg(long)] rpc_pass: String,
}, },
/// Step 1 of PSBT flow: build unsigned PSBT (+ draft Proof)
BuildPsbt { BuildPsbt {
#[arg(long)] rpc_url: String, #[arg(long)] rpc_url: String,
#[arg(long)] rpc_user: String, #[arg(long)] rpc_user: String,
@@ -56,6 +64,7 @@ enum Cmd {
#[arg(long, default_value = "draft.json")] #[arg(long, default_value = "draft.json")]
draft_out: PathBuf, draft_out: PathBuf,
}, },
/// Step 2 of PSBT flow: merge signed PSBT, finish proof
AttachSigs { AttachSigs {
#[arg(long)] draft: PathBuf, #[arg(long)] draft: PathBuf,
#[arg(long)] signed_psbt: PathBuf, #[arg(long)] signed_psbt: PathBuf,
@@ -64,6 +73,7 @@ enum Cmd {
}, },
} }
/// ──────────────────── Proof JSON schema ─────────────────────
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct Proof { struct Proof {
block_height: u64, block_height: u64,
@@ -73,17 +83,16 @@ struct Proof {
diff_commitment: String, diff_commitment: String,
ownership_proofs: Vec<String>, ownership_proofs: Vec<String>,
min_amount: u64, min_amount: u64,
#[serde(default)] // new metadata
psbt_hash: Option<String>, psbt_hash: Option<String>,
#[serde(default)]
signing_type: Option<String>, signing_type: Option<String>,
} }
/// ────────────────────────── main ────────────────────────────
fn main() -> Result<()> { fn main() -> Result<()> {
match Cli::parse().cmd { match Cli::parse().cmd {
Cmd::Generate { rpc_url, rpc_user, rpc_pass, address, min, height, out } => { Cmd::Generate { rpc_url, rpc_user, rpc_pass, address, min, height, out } => {
let proof = generate_proof(&rpc_url, &rpc_user, &rpc_pass, let proof = generate_proof(&rpc_url, &rpc_user, &rpc_pass, &address, min, height)?;
&address, min, height)?;
serde_json::to_writer_pretty(File::create(out)?, &proof)?; serde_json::to_writer_pretty(File::create(out)?, &proof)?;
println!("✅ proof generated ({} commitments)", proof.commitments.len()); println!("✅ proof generated ({} commitments)", proof.commitments.len());
} }
@@ -92,7 +101,8 @@ fn main() -> Result<()> {
verify_proof(&pf, &rpc_url, &rpc_user, &rpc_pass)?; verify_proof(&pf, &rpc_url, &rpc_user, &rpc_pass)?;
println!("✅ proof verified"); println!("✅ proof verified");
} }
Cmd::BuildPsbt { rpc_url, rpc_user, rpc_pass, address, min, height, qr, psbt_out, draft_out } => { Cmd::BuildPsbt { rpc_url, rpc_user, rpc_pass, address, min, height,
qr, psbt_out, draft_out } => {
let (psbt, draft) = build_psbt(&rpc_url, &rpc_user, &rpc_pass, &address, min, height)?; let (psbt, draft) = build_psbt(&rpc_url, &rpc_user, &rpc_pass, &address, min, height)?;
std::fs::write(&psbt_out, &psbt)?; std::fs::write(&psbt_out, &psbt)?;
serde_json::to_writer_pretty(File::create(&draft_out)?, &draft)?; serde_json::to_writer_pretty(File::create(&draft_out)?, &draft)?;
@@ -110,8 +120,7 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
// ── helpers ───────────────────────────────────────────────── /// ── UTXO helpers ────────────────────────────────────────────
#[derive(Debug)] #[derive(Debug)]
struct Utxo { txid: Txid, vout: u32, value: u64 } struct Utxo { txid: Txid, vout: u32, value: u64 }
@@ -125,11 +134,11 @@ fn fetch_utxos(rpc: &Client, addr: &Address<NetworkChecked>) -> Result<Vec<Utxo>
fn commit_utxos(utxos: &[Utxo]) -> (Vec<[u8; 32]>, u64) { fn commit_utxos(utxos: &[Utxo]) -> (Vec<[u8; 32]>, u64) {
let pg = PedersenGens::default(); let pg = PedersenGens::default();
let mut rng = OsRng; let mut rng = OsRng;
let mut total = 0; let mut total = 0u64;
let commits: Vec<[u8; 32]> = utxos.iter().map(|u| { let commits = utxos.iter().map(|u| {
total += u.value; total += u.value;
let r = Scalar::random(&mut rng); let blinder = Scalar::random(&mut rng);
pg.commit(Scalar::from(u.value), r).compress().to_bytes() pg.commit(Scalar::from(u.value), blinder).compress().to_bytes()
}).collect(); }).collect();
(commits, total) (commits, total)
} }
@@ -147,8 +156,7 @@ fn merkle_root(mut leaves: Vec<[u8; 32]>) -> [u8; 32] {
leaves[0] leaves[0]
} }
// ── generate ──────────────────────────────────────────────── /// ── generate (single-shot) ──────────────────────────────────
fn generate_proof( fn generate_proof(
rpc_url: &str, rpc_user: &str, rpc_pass: &str, rpc_url: &str, rpc_user: &str, rpc_pass: &str,
address: &str, min: u64, height: u64 address: &str, min: u64, height: u64
@@ -157,8 +165,7 @@ fn generate_proof(
let rpc = Client::new(rpc_url, Auth::UserPass(rpc_user.into(), rpc_pass.into()))?; let rpc = Client::new(rpc_url, Auth::UserPass(rpc_user.into(), rpc_pass.into()))?;
let addr_raw = Address::from_str(address)?; let addr_raw = Address::from_str(address)?;
let net = *addr_raw.network(); let addr_chk = addr_raw.clone().require_network(*addr_raw.network())?;
let addr_chk = addr_raw.require_network(net)?;
let utxos = fetch_utxos(&rpc, &addr_chk)?; let utxos = fetch_utxos(&rpc, &addr_chk)?;
anyhow::ensure!(!utxos.is_empty(), "address has no UTXOs"); anyhow::ensure!(!utxos.is_empty(), "address has no UTXOs");
@@ -167,11 +174,10 @@ fn generate_proof(
anyhow::ensure!(total >= min, "balance below threshold"); anyhow::ensure!(total >= min, "balance below threshold");
let gens = BulletproofGens::new(64, 1); let gens = BulletproofGens::new(64, 1);
let blind = Scalar::random(&mut OsRng); let blinder = Scalar::random(&mut OsRng);
let mut t = Transcript::new(b"por"); let mut t = Transcript::new(b"por");
let (bp, diff_commit) = RangeProof::prove_single( let (bp, diff_commit) =
&gens, &PedersenGens::default(), &mut t, RangeProof::prove_single(&gens, &PedersenGens::default(), &mut t, total - min, &blinder, 64)?;
total - min, &blind, 64)?;
let root = merkle_root(commitments.clone()); let root = merkle_root(commitments.clone());
@@ -179,45 +185,53 @@ fn generate_proof(
block_height: height, block_height: height,
utxo_root: hex::encode(root), utxo_root: hex::encode(root),
commitments: commitments.iter().map(hex::encode).collect(), commitments: commitments.iter().map(hex::encode).collect(),
range_proof: general_purpose::STANDARD.encode(bp.to_bytes()), range_proof: b64::STANDARD.encode(bp.to_bytes()),
diff_commitment: hex::encode(diff_commit.to_bytes()), diff_commitment: hex::encode(diff_commit.to_bytes()),
ownership_proofs: Vec::new(), ownership_proofs: Vec::new(),
min_amount: min, min_amount: min,
psbt_hash: None,
signing_type: None,
}) })
} }
// ── verify ────────────────────────────────────────────────── /// ── verify ──────────────────────────────────────────────────
fn verify_proof(pf: &Proof, rpc_url: &str, rpc_user: &str, rpc_pass: &str) -> Result<()> { fn verify_proof(pf: &Proof, rpc_url: &str, rpc_user: &str, rpc_pass: &str) -> Result<()> {
let rpc = Client::new(rpc_url, Auth::UserPass(rpc_user.into(), rpc_pass.into()))?; let rpc = Client::new(rpc_url, Auth::UserPass(rpc_user.into(), rpc_pass.into()))?;
anyhow::ensure!(rpc.get_block_count()? as u64 >= pf.block_height, "node behind"); anyhow::ensure!(rpc.get_block_count()? as u64 >= pf.block_height, "node behind");
let leaves: Vec<[u8; 32]> = pf.commitments.iter().map(|h| { // Merkle root
let mut leaves: Vec<[u8; 32]> = pf.commitments.iter().map(|h| {
let mut b = [0u8; 32]; let mut b = [0u8; 32];
b.copy_from_slice(&hex::decode(h).unwrap()); b.copy_from_slice(&hex::decode(h).unwrap());
b b
}).collect(); }).collect();
anyhow::ensure!(hex::encode(merkle_root(leaves)) == pf.utxo_root, "root mismatch"); anyhow::ensure!(hex::encode(merkle_root(leaves)) == pf.utxo_root, "root mismatch");
let bp_bytes = general_purpose::STANDARD.decode(&pf.range_proof)?; // Range-proof
let bp_bytes = b64::STANDARD.decode(&pf.range_proof)?;
let bp = RangeProof::from_bytes(&bp_bytes)?; let bp = RangeProof::from_bytes(&bp_bytes)?;
let v_bytes = hex::decode(&pf.diff_commitment)?; let commitment = CompressedRistretto::from_slice(
let commitment = CompressedRistretto::from_slice(&v_bytes); &hex::decode(&pf.diff_commitment)?);
let mut t = Transcript::new(b"por"); let mut t = Transcript::new(b"por");
bp.verify_single(&BulletproofGens::new(64, 1), &PedersenGens::default(), &mut t, &commitment, 64)?; bp.verify_single(&BulletproofGens::new(64, 1), &PedersenGens::default(),
&mut t, &commitment, 64)
.context("range-proof failed")?;
// ownership_proofs verification TODO
Ok(()) Ok(())
} }
// ── psbt workflow ──────────────────────────────────────────── /// ── PSBT flow – build unsigned PSBT ─────────────────────────
fn build_psbt(
rpc_url: &str, rpc_user: &str, rpc_pass: &str,
address: &str, min: u64, height: u64
) -> Result<(String, Proof)> {
fn build_psbt(rpc_url: &str, rpc_user: &str, rpc_pass: &str,
address: &str, min: u64, height: u64) -> Result<(String, Proof)> {
let rpc = Client::new(rpc_url, Auth::UserPass(rpc_user.into(), rpc_pass.into()))?; let rpc = Client::new(rpc_url, Auth::UserPass(rpc_user.into(), rpc_pass.into()))?;
let addr_raw = Address::from_str(address)?; let addr_raw = Address::from_str(address)?;
let net = *addr_raw.network(); let addr_chk = addr_raw.clone().require_network(*addr_raw.network())?;
let addr_chk = addr_raw.require_network(net)?;
let utxos = fetch_utxos(&rpc, &addr_chk)?; let utxos = fetch_utxos(&rpc, &addr_chk)?;
anyhow::ensure!(!utxos.is_empty(), "address has no UTXOs"); anyhow::ensure!(!utxos.is_empty(), "address has no UTXOs");
@@ -226,134 +240,116 @@ fn build_psbt(rpc_url: &str, rpc_user: &str, rpc_pass: &str,
anyhow::ensure!(total >= min, "balance below threshold"); anyhow::ensure!(total >= min, "balance below threshold");
let gens = BulletproofGens::new(64, 1); let gens = BulletproofGens::new(64, 1);
let blind = Scalar::random(&mut OsRng); let blinder = Scalar::random(&mut OsRng);
let mut t = Transcript::new(b"por"); let mut t = Transcript::new(b"por");
let (bp, diff_commit) = RangeProof::prove_single( let (bp, diff_commit) =
&gens, &PedersenGens::default(), &mut t, RangeProof::prove_single(&gens, &PedersenGens::default(), &mut t, total - min, &blinder, 64)?;
total - min, &blind, 64)?;
let root = merkle_root(commitments.clone()); // Build dummy PSBT
let merkle_hex = hex::encode(root); let root_hex = hex::encode(merkle_root(commitments.clone()));
let inputs: Vec<_> = utxos.iter()
let inputs: Vec<serde_json::Value> = utxos.iter()
.map(|u| json!({"txid": u.txid, "vout": u.vout})) .map(|u| json!({"txid": u.txid, "vout": u.vout}))
.collect(); .collect();
let options = json!({"feeRate": 0, "changePosition": -1, "lockUnspents": true}); let options = json!({"feeRate": 0, "changePosition": -1, "lockUnspents": true});
let res: serde_json::Value = rpc.call( let res: serde_json::Value = rpc.call(
"walletcreatefundedpsbt", "walletcreatefundedpsbt",
&[inputs.into(), json!({"data": merkle_hex.clone()}).into(), serde_json::Value::Null, &[inputs.into(), // inputs
options.into(), true.into()])?; json!({"data": root_hex.clone()}).into(), // outputs
let psbt_str = res["psbt"].as_str().unwrap().to_string(); serde_json::Value::Null, // locktime
options.into(), true.into()])?; // includeWatching
let psbt_bytes = base64::decode(&psbt_str)?; let psbt_str = res["psbt"].as_str().context("rpc malformed")?.to_string();
let mut hasher = Sha256::new(); let psbt_hash = hex::encode(Sha256::digest(&b64::STANDARD.decode(&psbt_str)?));
hasher.update(&psbt_bytes);
let psbt_hash = hex::encode(hasher.finalize());
let proof = Proof { let proof = Proof {
block_height: height, block_height: height,
utxo_root: merkle_hex, utxo_root: root_hex,
commitments: commitments.iter().map(hex::encode).collect(), commitments: commitments.iter().map(hex::encode).collect(),
range_proof: general_purpose::STANDARD.encode(bp.to_bytes()), range_proof: b64::STANDARD.encode(bp.to_bytes()),
diff_commitment: hex::encode(diff_commit.to_bytes()), diff_commitment: hex::encode(diff_commit.to_bytes()),
ownership_proofs: Vec::new(), ownership_proofs: Vec::new(),
min_amount: min, min_amount: min,
psbt_hash: Some(psbt_hash), psbt_hash: Some(psbt_hash),
signing_type: Some("psbt-opreturn-v1".to_string()), signing_type: Some("psbt-opreturn-v1".into()),
}; };
Ok((psbt_str, proof)) Ok((psbt_str, proof))
} }
/// Show PSBT as animated QR in the terminal
fn display_qr_frames(data: &str, fps: u64) -> Result<()> { fn display_qr_frames(data: &str, fps: u64) -> Result<()> {
let raw = data.as_bytes(); let raw = data.as_bytes();
let chunks: Vec<&[u8]> = raw.chunks(400).collect(); let chunks: Vec<&[u8]> = raw.chunks(400).collect();
println!("Building PSBT QR ({} frames, press Ctrl-C to abort) …", chunks.len()); println!("Building PSBT QR ({} frames, Ctrl-C to abort)…", chunks.len());
for chunk in chunks.iter() { for chunk in chunks {
let mut frame = Vec::new(); let frame_txt = chunk.to_base58();
frame.extend_from_slice(chunk); let qr = QrCode::with_error_correction_level(frame_txt, EcLevel::L)?;
let text = frame.to_base58(); let art = qr.render::<unicode::Dense1x2>()
let qr = QrCode::encode_text(&text, QrCodeEcc::Low)?; .quiet_zone(false)
println!("{}", qr.to_string(true, 2)); .module_dimensions(2, 1)
.build();
println!("{art}");
std::thread::sleep(Duration::from_millis(1000 / fps)); std::thread::sleep(Duration::from_millis(1000 / fps));
} }
Ok(()) Ok(())
} }
/// Merge signed PSBT → fill ownership_proofs
fn attach_sigs(proof: &mut Proof, psbt_str: &str) -> Result<()> { fn attach_sigs(proof: &mut Proof, psbt_str: &str) -> Result<()> {
use std::str::FromStr; use std::str::FromStr;
let psbt: bitcoin::psbt::Psbt = bitcoin::psbt::Psbt::from_str(psbt_str)?; let psbt: bitcoin::psbt::Psbt = bitcoin::psbt::Psbt::from_str(psbt_str)?;
// verify hash // hash-tamper check
if let Some(ref expect) = proof.psbt_hash { if let Some(expect) = &proof.psbt_hash {
let bytes = base64::decode(psbt_str)?; let digest = Sha256::digest(&b64::STANDARD.decode(psbt_str)?);
let mut hasher = Sha256::new(); anyhow::ensure!(hex::encode(digest) == *expect, "PSBT hash mismatch");
hasher.update(&bytes);
let digest = hex::encode(hasher.finalize());
anyhow::ensure!(&digest == expect, "Signed PSBT does not match draft (hash mismatch)");
} }
anyhow::ensure!(psbt.inputs.len() == proof.commitments.len(), "Signed PSBT structure altered; aborting."); anyhow::ensure!(
psbt.inputs.len() == proof.commitments.len(),
"Signed PSBT structure altered"
);
proof.ownership_proofs.clear(); proof.ownership_proofs.clear();
for (idx, input) in psbt.inputs.iter().enumerate() { for (idx, input) in psbt.inputs.iter().enumerate() {
if let Some((_, sig)) = input.partial_sigs.iter().next() { let sig_hex = if let Some((_, sig)) = input.partial_sigs.iter().next() {
proof.ownership_proofs.push(hex::encode(sig.to_vec())); hex::encode(sig.to_vec())
} else if let Some(sig) = input.tap_key_sig { } else if let Some(sig) = input.tap_key_sig {
proof.ownership_proofs.push(hex::encode(sig.to_vec())); hex::encode(sig.to_vec())
} else { } else {
anyhow::bail!("Signed PSBT lacks sig for input {}", idx); anyhow::bail!("Input {idx} lacks signature");
} };
proof.ownership_proofs.push(sig_hex);
} }
Ok(()) Ok(())
} }
// ── tests ─────────────────────────────────────────────────── /// ── mini-tests ──────────────────────────────────────────────
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn merkle_root_single_leaf() { fn merkle_root_single() {
let leaf = [42u8; 32]; let leaf = [42u8; 32];
assert_eq!(merkle_root(vec![leaf]), leaf); assert_eq!(merkle_root(vec![leaf]), leaf);
} }
#[test] #[test]
fn merkle_root_two_leaves() { fn merkle_root_two() {
let a = [1u8; 32]; let a = [1u8; 32]; let b = [2u8; 32];
let b = [2u8; 32]; let mut h = Sha256::new(); h.update(a); h.update(b);
let mut h = Sha256::new(); let exp: [u8; 32] = h.finalize().into();
h.update(a); assert_eq!(merkle_root(vec![a, b]), exp);
h.update(b);
let expected: [u8; 32] = h.finalize().into();
assert_eq!(merkle_root(vec![a, b]), expected);
} }
#[test] #[test]
fn range_proof_verify_success() { fn range_ok() {
let gens = BulletproofGens::new(64, 1); let gens = BulletproofGens::new(64, 1);
let pg = PedersenGens::default(); let pg = PedersenGens::default();
let v = 42u64;
let blind = Scalar::from(7u64);
let mut t = Transcript::new(b"por"); let mut t = Transcript::new(b"por");
let (proof, commitment) = RangeProof::prove_single(&gens, &pg, &mut t, v, &blind, 64).unwrap(); let (p, c) = RangeProof::prove_single(&gens, &pg, &mut t, 42u64, &Scalar::from(7u64), 64).unwrap();
let mut vt = Transcript::new(b"por"); let mut vt = Transcript::new(b"por");
assert!(proof.verify_single(&gens, &pg, &mut vt, &commitment, 64).is_ok()); assert!(p.verify_single(&gens, &pg, &mut vt, &c, 64).is_ok());
}
#[test]
fn range_proof_verify_fail() {
let gens = BulletproofGens::new(64, 1);
let pg = PedersenGens::default();
let v = 42u64;
let blind = Scalar::from(7u64);
let mut t = Transcript::new(b"por");
let (proof, _commitment) = RangeProof::prove_single(&gens, &pg, &mut t, v, &blind, 64).unwrap();
// commitment for wrong value
let wrong_commitment = pg.commit(Scalar::from(43u64), Scalar::from(1u64)).compress();
let mut vt = Transcript::new(b"por");
assert!(proof.verify_single(&gens, &pg, &mut vt, &wrong_commitment, 64).is_err());
} }
} }
View File
Whitespace-only changes.
View File
Whitespace-only changes.