mirror of
https://github.com/PR0M3TH3AN/Private-Proof-Of-Reserve-Cli.git
synced 2026-10-27 08:50:29 +00:00
Merge pull request #10 from PR0M3TH3AN/jeukpq-codex/add-psbt-round-trip-via-animated-qr-support
Fix PSBT feature flag
This commit is contained in:
2 files changed
+144
-2
No files matched your search
+3
-1
@@ -10,7 +10,7 @@ clap = { version = "4.5", features = ["derive"] }
|
||||
indicatif = "0.17"
|
||||
|
||||
# Bitcoin & RPC
|
||||
bitcoin = "0.31"
|
||||
bitcoin = { version = "0.31", features = ["base64"] }
|
||||
bitcoincore-rpc = "0.18"
|
||||
|
||||
# Crypto primitives
|
||||
@@ -23,6 +23,8 @@ merlin = "3.0"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
base64 = "0.21"
|
||||
qrcodegen = "1.8"
|
||||
base58 = "0.2"
|
||||
|
||||
# Serde / error handling
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
+141
-1
@@ -2,7 +2,7 @@
|
||||
//! Prototype CLI for a lower-bound Bitcoin proof-of-reserve.
|
||||
//! SECURITY: demo code only.
|
||||
|
||||
use std::{fs::File, path::PathBuf, str::FromStr};
|
||||
use std::{fs::File, path::PathBuf, str::FromStr, time::Duration};
|
||||
use anyhow::Result;
|
||||
use bitcoin::{Address, Txid};
|
||||
use bitcoin::address::NetworkChecked;
|
||||
@@ -16,6 +16,9 @@ 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;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about)]
|
||||
@@ -39,6 +42,26 @@ enum Cmd {
|
||||
#[arg(long)] rpc_user: String,
|
||||
#[arg(long)] rpc_pass: String,
|
||||
},
|
||||
BuildPsbt {
|
||||
#[arg(long)] rpc_url: String,
|
||||
#[arg(long)] rpc_user: String,
|
||||
#[arg(long)] rpc_pass: String,
|
||||
#[arg(long)] address: String,
|
||||
#[arg(long)] min: u64,
|
||||
#[arg(long)] height: u64,
|
||||
#[arg(long, default_value_t = false)]
|
||||
qr: bool,
|
||||
#[arg(long, default_value = "unsigned.psbt")]
|
||||
psbt_out: PathBuf,
|
||||
#[arg(long, default_value = "draft.json")]
|
||||
draft_out: PathBuf,
|
||||
},
|
||||
AttachSigs {
|
||||
#[arg(long)] draft: PathBuf,
|
||||
#[arg(long)] signed_psbt: PathBuf,
|
||||
#[arg(long, default_value = "proof.json")]
|
||||
out: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -50,6 +73,10 @@ struct Proof {
|
||||
diff_commitment: String,
|
||||
ownership_proofs: Vec<String>,
|
||||
min_amount: u64,
|
||||
#[serde(default)]
|
||||
psbt_hash: Option<String>,
|
||||
#[serde(default)]
|
||||
signing_type: Option<String>,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
@@ -65,6 +92,20 @@ fn main() -> Result<()> {
|
||||
verify_proof(&pf, &rpc_url, &rpc_user, &rpc_pass)?;
|
||||
println!("✅ proof verified");
|
||||
}
|
||||
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)?;
|
||||
std::fs::write(&psbt_out, &psbt)?;
|
||||
serde_json::to_writer_pretty(File::create(&draft_out)?, &draft)?;
|
||||
println!("✅ PSBT and draft saved");
|
||||
if qr { display_qr_frames(&psbt, 5)?; }
|
||||
}
|
||||
Cmd::AttachSigs { draft, signed_psbt, out } => {
|
||||
let mut pf: Proof = serde_json::from_reader(File::open(&draft)?)?;
|
||||
let psbt = std::fs::read_to_string(&signed_psbt)?;
|
||||
attach_sigs(&mut pf, &psbt)?;
|
||||
serde_json::to_writer_pretty(File::create(out)?, &pf)?;
|
||||
println!("✅ proof completed");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -168,6 +209,105 @@ fn verify_proof(pf: &Proof, rpc_url: &str, rpc_user: &str, rpc_pass: &str) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── psbt workflow ────────────────────────────────────────────
|
||||
|
||||
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 addr_raw = Address::from_str(address)?;
|
||||
let net = *addr_raw.network();
|
||||
let addr_chk = addr_raw.require_network(net)?;
|
||||
|
||||
let utxos = fetch_utxos(&rpc, &addr_chk)?;
|
||||
anyhow::ensure!(!utxos.is_empty(), "address has no UTXOs");
|
||||
|
||||
let (commitments, total) = commit_utxos(&utxos);
|
||||
anyhow::ensure!(total >= min, "balance below threshold");
|
||||
|
||||
let gens = BulletproofGens::new(64, 1);
|
||||
let blind = Scalar::random(&mut OsRng);
|
||||
let mut t = Transcript::new(b"por");
|
||||
let (bp, diff_commit) = RangeProof::prove_single(
|
||||
&gens, &PedersenGens::default(), &mut t,
|
||||
total - min, &blind, 64)?;
|
||||
|
||||
let root = merkle_root(commitments.clone());
|
||||
let merkle_hex = hex::encode(root);
|
||||
|
||||
let inputs: Vec<serde_json::Value> = utxos.iter()
|
||||
.map(|u| json!({"txid": u.txid, "vout": u.vout}))
|
||||
.collect();
|
||||
let options = json!({"feeRate": 0, "changePosition": -1, "lockUnspents": true});
|
||||
let res: serde_json::Value = rpc.call(
|
||||
"walletcreatefundedpsbt",
|
||||
&[inputs.into(), json!({"data": merkle_hex.clone()}).into(), serde_json::Value::Null,
|
||||
options.into(), true.into()])?;
|
||||
let psbt_str = res["psbt"].as_str().unwrap().to_string();
|
||||
|
||||
let psbt_bytes = base64::decode(&psbt_str)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&psbt_bytes);
|
||||
let psbt_hash = hex::encode(hasher.finalize());
|
||||
|
||||
let proof = Proof {
|
||||
block_height: height,
|
||||
utxo_root: merkle_hex,
|
||||
commitments: commitments.iter().map(hex::encode).collect(),
|
||||
range_proof: general_purpose::STANDARD.encode(bp.to_bytes()),
|
||||
diff_commitment: hex::encode(diff_commit.to_bytes()),
|
||||
ownership_proofs: Vec::new(),
|
||||
min_amount: min,
|
||||
psbt_hash: Some(psbt_hash),
|
||||
signing_type: Some("psbt-opreturn-v1".to_string()),
|
||||
};
|
||||
|
||||
Ok((psbt_str, proof))
|
||||
}
|
||||
|
||||
fn display_qr_frames(data: &str, fps: u64) -> Result<()> {
|
||||
let raw = data.as_bytes();
|
||||
let chunks: Vec<&[u8]> = raw.chunks(400).collect();
|
||||
println!("Building PSBT QR ({} frames, press Ctrl-C to abort) …", chunks.len());
|
||||
for chunk in chunks.iter() {
|
||||
let mut frame = Vec::new();
|
||||
frame.extend_from_slice(chunk);
|
||||
let text = frame.to_base58();
|
||||
let qr = QrCode::encode_text(&text, QrCodeEcc::Low)?;
|
||||
println!("{}", qr.to_string(true, 2));
|
||||
std::thread::sleep(Duration::from_millis(1000 / fps));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn attach_sigs(proof: &mut Proof, psbt_str: &str) -> Result<()> {
|
||||
use std::str::FromStr;
|
||||
let psbt: bitcoin::psbt::Psbt = bitcoin::psbt::Psbt::from_str(psbt_str)?;
|
||||
|
||||
// verify hash
|
||||
if let Some(ref expect) = proof.psbt_hash {
|
||||
let bytes = base64::decode(psbt_str)?;
|
||||
let mut hasher = Sha256::new();
|
||||
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.");
|
||||
|
||||
proof.ownership_proofs.clear();
|
||||
for (idx, input) in psbt.inputs.iter().enumerate() {
|
||||
if let Some((_, sig)) = input.partial_sigs.iter().next() {
|
||||
proof.ownership_proofs.push(hex::encode(sig.to_vec()));
|
||||
} else if let Some(sig) = input.tap_key_sig {
|
||||
proof.ownership_proofs.push(hex::encode(sig.to_vec()));
|
||||
} else {
|
||||
anyhow::bail!("Signed PSBT lacks sig for input {}", idx);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── tests ───────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in new issue
Block a user