Skip to content

Commit ac8815d

Browse files
committed
CLI: charms spell check
Add `charms spell check` command to perform the same checks that `charms spell prove` does (when proving a spell), but much faster.
1 parent d16078e commit ac8815d

File tree

9 files changed

+172
-60
lines changed

9 files changed

+172
-60
lines changed

charms-spell-checker/src/app.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use charms_data::{
2-
nft_state_preserved, token_amounts_balanced, util, App, Data, Transaction, NFT, TOKEN,
3-
};
1+
use charms_data::{is_simple_transfer, util, App, Data, Transaction};
42
use serde::{Deserialize, Serialize};
53
use sp1_primitives::io::SP1PublicValues;
64
use sp1_zkvm::lib::verify::verify_sp1_proof;
@@ -28,11 +26,7 @@ impl AppContractVK {
2826
verify_sp1_proof(vk, &pv);
2927
true
3028
}
31-
None => match app.tag {
32-
TOKEN => token_amounts_balanced(app, &tx),
33-
NFT => nft_state_preserved(app, &tx),
34-
_ => false,
35-
},
29+
None => is_simple_transfer(app, tx),
3630
}
3731
}
3832
}

src/app.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::ensure;
2-
use charms_data::{util, App, Data, Transaction, B32};
2+
use charms_data::{is_simple_transfer, util, App, Data, Transaction, B32};
33
use sp1_prover::components::CpuProverComponents;
44
use sp1_sdk::{HashableKey, ProverClient, SP1Proof, SP1ProofMode, SP1Stdin, SP1VerifyingKey};
55
use std::{collections::BTreeMap, mem};
@@ -50,6 +50,7 @@ impl Prover {
5050
eprintln!("app binary not present: {:?}", app);
5151
continue;
5252
};
53+
dbg!(app);
5354
let mut app_stdin = SP1Stdin::new();
5455
let empty = Data::empty();
5556
let w = app_private_inputs.get(app).unwrap_or(&empty);
@@ -61,14 +62,48 @@ impl Prover {
6162
let SP1Proof::Compressed(compressed_proof) = app_proof.proof else {
6263
unreachable!()
6364
};
64-
dbg!(app);
65-
eprintln!("app proof generated!");
65+
eprintln!("app proof generated! for {:?}", app);
6666
spell_stdin.write_proof(*compressed_proof, vk.vk.clone());
6767
}
6868

6969
Ok(())
7070
}
7171

72+
pub(crate) fn run_all(
73+
&self,
74+
app_binaries: &BTreeMap<B32, Vec<u8>>,
75+
tx: &Transaction,
76+
app_public_inputs: &BTreeMap<App, Data>,
77+
app_private_inputs: BTreeMap<App, Data>,
78+
) -> anyhow::Result<()> {
79+
for (app, x) in app_public_inputs {
80+
let mut app_stdin = SP1Stdin::new();
81+
let empty = Data::empty();
82+
let w = app_private_inputs.get(app).unwrap_or(&empty);
83+
app_stdin.write_vec(util::write(&(app, tx, x, w))?);
84+
match app_binaries.get(&app.vk) {
85+
Some(app_binary) => {
86+
let (committed_values, _report) =
87+
self.client.execute(app_binary, &app_stdin)?;
88+
89+
let com: (App, Transaction, Data) =
90+
util::read(committed_values.to_vec().as_slice())?;
91+
ensure!(
92+
(&com.0, &com.1, &com.2) == (app, tx, x),
93+
"committed data mismatch"
94+
);
95+
}
96+
None => ensure!(is_simple_transfer(app, tx)),
97+
}
98+
99+
dbg!(app);
100+
101+
eprintln!("app checked!");
102+
}
103+
104+
Ok(())
105+
}
106+
72107
pub fn run(
73108
&self,
74109
app_binary: &[u8],

src/cli/app.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{app, spell::Spell};
1+
use crate::{app, app::Prover, spell::Spell};
22
use anyhow::{anyhow, ensure, Result};
33
use charms_data::{Data, B32};
44
use std::{
@@ -136,3 +136,18 @@ fn data_for_key(inputs: &BTreeMap<String, Data>, k: &String) -> Data {
136136
None => Data::empty(),
137137
}
138138
}
139+
140+
pub fn binaries_by_vk(
141+
app_prover: &Prover,
142+
app_bins: Vec<PathBuf>,
143+
) -> Result<BTreeMap<B32, Vec<u8>>> {
144+
let binaries: BTreeMap<B32, Vec<u8>> = app_bins
145+
.iter()
146+
.map(|path| {
147+
let binary = std::fs::read(path)?;
148+
let vk_hash = app_prover.vk(&binary);
149+
Ok((B32(vk_hash), binary))
150+
})
151+
.collect::<Result<_>>()?;
152+
Ok(binaries)
153+
}

src/cli/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,21 @@ pub struct SpellProveParams {
116116
fee_rate: f64,
117117
}
118118

119+
#[derive(Args)]
120+
pub struct SpellCheckParams {
121+
/// Path to spell source file (YAML/JSON).
122+
#[arg(long, default_value = "/dev/stdin")]
123+
spell: PathBuf,
124+
/// Path to the apps' RISC-V binaries.
125+
#[arg(long, value_delimiter = ',')]
126+
app_bins: Vec<PathBuf>,
127+
}
128+
119129
#[derive(Subcommand)]
120130
pub enum SpellCommands {
121-
/// Prove a spell.
131+
/// Check the spell is correct.
132+
Check(#[command(flatten)] SpellCheckParams),
133+
/// Prove the spell is correct.
122134
Prove(#[command(flatten)] SpellProveParams),
123135
}
124136

@@ -204,6 +216,7 @@ pub async fn run() -> anyhow::Result<()> {
204216
match cli.command {
205217
Commands::Server(server_config) => server::server(server_config).await,
206218
Commands::Spell { command } => match command {
219+
SpellCommands::Check(params) => spell::check(params),
207220
SpellCommands::Prove(params) => spell::prove(params),
208221
},
209222
Commands::Tx { command } => match command {

src/cli/spell.rs

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
use crate::{cli::SpellProveParams, spell, spell::Spell, tx, tx::txs_by_txid, utils};
1+
use crate::{
2+
app, cli,
3+
cli::{SpellCheckParams, SpellProveParams},
4+
spell,
5+
spell::Spell,
6+
tx,
7+
tx::txs_by_txid,
8+
utils, SPELL_VK,
9+
};
210
use anyhow::{ensure, Result};
311
use bitcoin::{
412
consensus::encode::{deserialize_hex, serialize_hex},
@@ -20,7 +28,7 @@ pub fn prove(
2028
utils::logger::setup_logger();
2129

2230
// Parse funding UTXO early: to fail fast
23-
let funding_utxo = crate::cli::tx::parse_outpoint(&funding_utxo_id)?;
31+
let funding_utxo = cli::tx::parse_outpoint(&funding_utxo_id)?;
2432

2533
ensure!(fee_rate >= 1.0, "fee rate must be >= 1.0");
2634

@@ -30,16 +38,23 @@ pub fn prove(
3038
Some(tx) => deserialize_hex::<Transaction>(&tx)?,
3139
None => tx::from_spell(&spell),
3240
};
41+
let prev_txs = prev_txs
42+
.into_iter()
43+
.map(|tx| Ok(deserialize_hex::<Transaction>(&tx)?))
44+
.collect::<Result<_>>()?;
3345
let prev_txs = txs_by_txid(prev_txs)?;
3446
ensure!(tx
3547
.input
3648
.iter()
3749
.all(|input| prev_txs.contains_key(&input.previous_output.txid)));
3850

51+
let app_prover = app::Prover::new();
52+
let binaries = cli::app::binaries_by_vk(&app_prover, app_bins)?;
53+
3954
let transactions = spell::prove_spell_tx(
4055
spell,
4156
tx,
42-
app_bins,
57+
binaries,
4358
prev_txs,
4459
funding_utxo,
4560
funding_utxo_value,
@@ -55,3 +70,52 @@ pub fn prove(
5570

5671
Ok(())
5772
}
73+
74+
pub fn check(SpellCheckParams { spell, app_bins }: SpellCheckParams) -> Result<()> {
75+
utils::logger::setup_logger();
76+
77+
let mut spell: Spell = serde_yaml::from_slice(&std::fs::read(spell)?)?;
78+
for u in spell.outs.iter_mut() {
79+
u.sats.get_or_insert(crate::cli::wallet::MIN_SATS);
80+
}
81+
82+
// make sure spell inputs all have utxo_id
83+
ensure!(
84+
spell.ins.iter().all(|u| u.utxo_id.is_some()),
85+
"all spell inputs must have utxo_id"
86+
);
87+
88+
let tx = tx::from_spell(&spell);
89+
90+
let prev_txs = cli::tx::get_prev_txs(&tx)?;
91+
92+
eprintln!("checking prev_txs");
93+
let prev_spells = charms_client::prev_spells(&prev_txs, &SPELL_VK);
94+
eprintln!("checking prev_txs... done!");
95+
96+
let (norm_spell, app_private_inputs) = spell.normalized()?;
97+
let norm_spell = spell::align_spell_to_tx(norm_spell, &tx)?;
98+
99+
eprintln!("checking spell is well-formed");
100+
ensure!(
101+
charms_client::well_formed(&norm_spell, &prev_spells),
102+
"spell is not well-formed"
103+
);
104+
eprintln!("checking spell is well-formed... done!");
105+
106+
eprintln!("checking spell is correct");
107+
let app_prover = app::Prover::new();
108+
109+
let binaries = cli::app::binaries_by_vk(&app_prover, app_bins)?;
110+
111+
let charms_tx = spell.to_tx()?;
112+
app_prover.run_all(
113+
&binaries,
114+
&charms_tx,
115+
&norm_spell.app_public_inputs,
116+
app_private_inputs,
117+
)?;
118+
eprintln!("checking spell is correct... done!");
119+
120+
Ok(())
121+
}

src/cli/tx.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use crate::{cli, tx};
22
use anyhow::{anyhow, Result};
3-
use bitcoin::{consensus::encode::deserialize_hex, OutPoint, Transaction};
3+
use bitcoin::{
4+
consensus::encode::{deserialize_hex, serialize_hex},
5+
OutPoint, Transaction,
6+
};
7+
use std::process::Command;
48

59
pub(crate) fn parse_outpoint(s: &str) -> Result<OutPoint> {
610
let parts: Vec<&str> = s.split(':').collect();
@@ -21,3 +25,15 @@ pub fn tx_show_spell(tx: String, json: bool) -> Result<()> {
2125

2226
Ok(())
2327
}
28+
29+
pub(crate) fn get_prev_txs(tx: &Transaction) -> Result<Vec<Transaction>> {
30+
let cmd_output = Command::new("bash")
31+
.args(&[
32+
"-c", format!("bitcoin-cli decoderawtransaction {} | jq -r '.vin[].txid' | sort | uniq | xargs -I {{}} bitcoin-cli getrawtransaction {{}} | paste -sd, -", serialize_hex(tx)).as_str()
33+
])
34+
.output()?;
35+
String::from_utf8(cmd_output.stdout)?
36+
.split(',')
37+
.map(|s| Ok(deserialize_hex::<Transaction>(s.trim())?))
38+
.collect()
39+
}

src/cli/wallet.rs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
cli,
2+
app, cli,
33
cli::{WalletCastParams, WalletListParams},
44
spell::{prove_spell_tx, KeyedCharms, Spell},
55
tx,
@@ -181,7 +181,7 @@ fn get_tx(txid: &str) -> Result<Transaction> {
181181
Ok(tx)
182182
}
183183

184-
const MIN_SATS: u64 = 1000;
184+
pub const MIN_SATS: u64 = 1000;
185185

186186
pub fn cast(
187187
WalletCastParams {
@@ -217,14 +217,17 @@ pub fn cast(
217217

218218
let tx = tx::from_spell(&spell);
219219

220-
let prev_txs = txs_by_txid(get_prev_txs(&tx)?)?;
220+
let prev_txs = txs_by_txid(cli::tx::get_prev_txs(&tx)?)?;
221221
let funding_utxo_value = funding_utxo_value(&funding_utxo)?;
222222
let change_address = new_change_address()?;
223223

224+
let app_prover = app::Prover::new();
225+
let binaries = cli::app::binaries_by_vk(&app_prover, app_bins)?;
226+
224227
let [commit_tx, spell_tx] = prove_spell_tx(
225228
spell,
226229
tx,
227-
app_bins,
230+
binaries,
228231
prev_txs,
229232
funding_utxo,
230233
funding_utxo_value,
@@ -287,15 +290,3 @@ fn funding_utxo_value(utxo: &OutPoint) -> Result<u64> {
287290
let cmd_out = Command::new("bash").args(&["-c", &cmd]).output()?;
288291
Ok(String::from_utf8(cmd_out.stdout)?.trim().parse()?)
289292
}
290-
291-
fn get_prev_txs(tx: &Transaction) -> Result<Vec<String>> {
292-
let cmd_output = Command::new("bash")
293-
.args(&[
294-
"-c", format!("bitcoin-cli decoderawtransaction {} | jq -r '.vin[].txid' | sort | uniq | xargs -I {{}} bitcoin-cli getrawtransaction {{}} | paste -sd, -", serialize_hex(tx)).as_str()
295-
])
296-
.output()?;
297-
Ok(String::from_utf8(cmd_output.stdout)?
298-
.split(',')
299-
.map(|s| s.trim().to_string())
300-
.collect())
301-
}

src/spell.rs

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize};
1010
use sp1_sdk::{HashableKey, ProverClient, SP1Stdin};
1111
use std::{
1212
collections::{BTreeMap, BTreeSet},
13-
path::PathBuf,
1413
str::FromStr,
1514
};
1615

@@ -366,26 +365,15 @@ $TOAD: 9
366365
pub fn prove_spell_tx(
367366
spell: Spell,
368367
tx: bitcoin::Transaction,
369-
app_bins: Vec<PathBuf>,
368+
binaries: BTreeMap<B32, Vec<u8>>,
370369
prev_txs: BTreeMap<Txid, bitcoin::Transaction>,
371370
funding_utxo: OutPoint,
372371
funding_utxo_value: u64,
373372
change_address: String,
374373
fee_rate: f64,
375374
) -> anyhow::Result<[bitcoin::Transaction; 2]> {
376-
let (mut norm_spell, app_private_inputs) = spell.normalized()?;
377-
align_spell_to_tx(&mut norm_spell, &tx)?;
378-
379-
let app_prover = app::Prover::new();
380-
381-
let binaries = app_bins
382-
.iter()
383-
.map(|path| {
384-
let binary = std::fs::read(path)?;
385-
let vk_hash = app_prover.vk(&binary);
386-
Ok((B32(vk_hash), binary))
387-
})
388-
.collect::<anyhow::Result<_>>()?;
375+
let (norm_spell, app_private_inputs) = spell.normalized()?;
376+
let norm_spell = align_spell_to_tx(norm_spell, &tx)?;
389377

390378
let (norm_spell, proof) = prove(
391379
norm_spell,
@@ -418,10 +406,11 @@ pub fn prove_spell_tx(
418406
Ok(transactions)
419407
}
420408

421-
fn align_spell_to_tx(
422-
norm_spell: &mut NormalizedSpell,
409+
pub(crate) fn align_spell_to_tx(
410+
norm_spell: NormalizedSpell,
423411
tx: &bitcoin::Transaction,
424-
) -> anyhow::Result<()> {
412+
) -> anyhow::Result<NormalizedSpell> {
413+
let mut norm_spell = norm_spell;
425414
let spell_ins = norm_spell.tx.ins.as_ref().ok_or(anyhow!("no inputs"))?;
426415

427416
ensure!(
@@ -458,5 +447,5 @@ fn align_spell_to_tx(
458447
norm_spell.tx.ins.get_or_insert_with(Vec::new).push(utxo_id);
459448
}
460449

461-
Ok(())
450+
Ok(norm_spell)
462451
}

0 commit comments

Comments
 (0)