mirror of
https://github.com/PR0M3TH3AN/Private-Proof-Of-Reserve-Cli.git
synced 2026-10-27 08:50:29 +00:00
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:
1 parent
cb6f97ddd1
commit
d4971ddba9
5 files changed
+226
-133
No files matched your search
Generated
+86
-4
@@ -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
@@ -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
@@ -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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Whitespace-only changes.
Whitespace-only changes.
Reference in new issue
Block a user