Skip to content

Commit 35d0a33

Browse files
committed
Proper mocks
Add the `--mock` CLI parameter to appropriate commands. Add the `mock` field to `NormalizedSpell`. When verifying with `--mock`, both mock and non-mock spells are considered "correct". Without `--mock`, only non-mock spells are correct. Mock proofs are real Groth16 proofs over BLS12-381 generated with a dummy circuit.
1 parent 5352f64 commit 35d0a33

File tree

30 files changed

+760
-311
lines changed

30 files changed

+760
-311
lines changed

Cargo.lock

Lines changed: 281 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ license = "MIT"
1616

1717
[dependencies]
1818
anyhow = { workspace = true }
19+
ark-bls12-381 = { workspace = true }
20+
ark-ec = { workspace = true }
21+
ark-ff = { workspace = true }
22+
ark-groth16 = { workspace = true }
23+
ark-relations = { workspace = true }
24+
ark-serialize = { workspace = true }
25+
ark-snark = { workspace = true }
26+
ark-std = { workspace = true }
1927
axum = { version = "0.8.4", features = ["http2"] }
2028
axum-macros = { version = "0.5.0" }
2129
bincode = { version = "1.3.3" }
@@ -70,6 +78,14 @@ resolver = "3"
7078

7179
[workspace.dependencies]
7280
anyhow = { version = "1.0.98" }
81+
ark-bls12-381 = { version = "0.5.0" }
82+
ark-ec = { version = "0.5.0" }
83+
ark-ff = { version = "0.5.0" }
84+
ark-groth16 = { version = "0.5.0" }
85+
ark-relations = { version = "0.5.0" }
86+
ark-serialize = { version = "0.5.0" }
87+
ark-snark = { version = "0.5.0" }
88+
ark-std = { version = "0.5.0" }
7389
bitcoin = { version = "0.32.6" }
7490
ciborium = { version = "0.2.2" }
7591
ciborium-io = { version = "0.2.2" }

charms-client/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ license.workspace = true
99

1010
[dependencies]
1111
anyhow = { workspace = true }
12+
ark-bls12-381 = { workspace = true }
13+
ark-ff = { workspace = true }
14+
ark-groth16 = { workspace = true }
15+
ark-serialize = { workspace = true }
16+
ark-snark = { workspace = true }
1217
bitcoin = { workspace = true, features = ["serde"] }
1318
charms-app-runner = { path = "../charms-app-runner", version = "0.8.0" }
1419
charms-data = { path = "../charms-data", version = "0.8.0" }

charms-client/src/ark.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use anyhow::anyhow;
2+
use ark_bls12_381::Bls12_381;
3+
use ark_ff::ToConstraintField;
4+
use ark_groth16::{Groth16, Proof, VerifyingKey, prepare_verifying_key};
5+
use ark_serialize::CanonicalDeserialize;
6+
use ark_snark::SNARK;
7+
use sha2::{Digest, Sha256};
8+
9+
pub fn verify_groth16_proof(
10+
proof: &[u8],
11+
public_inputs: &[u8],
12+
vk_bytes: &[u8],
13+
) -> anyhow::Result<()> {
14+
let vk = VerifyingKey::deserialize_compressed(vk_bytes)
15+
.map_err(|e| anyhow!("Failed to deserialize verifying key: {}", e))?;
16+
let pvk = prepare_verifying_key::<Bls12_381>(&vk);
17+
let proof = Proof::deserialize_compressed(proof)?;
18+
let field_elements = Sha256::digest(public_inputs)
19+
.to_field_elements()
20+
.expect("non-empty vector");
21+
Groth16::<Bls12_381>::verify_with_processed_vk(&pvk, &[field_elements[0]], &proof)?;
22+
Ok(())
23+
}

charms-client/src/bitcoin_tx.rs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
use crate::{tx::EnchantedTx, NormalizedSpell, Proof};
1+
use crate::{NormalizedSpell, Proof, tx, tx::EnchantedTx};
22
use anyhow::{anyhow, bail, ensure};
33
use bitcoin::{
4+
TxIn,
45
consensus::encode::{deserialize_hex, serialize_hex},
56
hashes::Hash,
67
opcodes::all::{OP_ENDIF, OP_IF},
78
script::{Instruction, PushBytes},
8-
TxIn,
99
};
10-
use charms_data::{util, TxId, UtxoId};
10+
use charms_data::{TxId, UtxoId, util};
1111
use serde::{Deserialize, Serialize};
12-
use sp1_verifier::Groth16Verifier;
1312

1413
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1514
pub struct BitcoinTx(pub bitcoin::Transaction);
@@ -22,7 +21,11 @@ impl BitcoinTx {
2221
}
2322

2423
impl EnchantedTx for BitcoinTx {
25-
fn extract_and_verify_spell(&self, spell_vk: &str) -> anyhow::Result<NormalizedSpell> {
24+
fn extract_and_verify_spell(
25+
&self,
26+
spell_vk: &str,
27+
mock: bool,
28+
) -> anyhow::Result<NormalizedSpell> {
2629
let tx = &self.0;
2730

2831
let Some((spell_tx_in, tx_ins)) = tx.input.split_last() else {
@@ -31,26 +34,25 @@ impl EnchantedTx for BitcoinTx {
3134

3235
let (spell, proof) = parse_spell_and_proof(spell_tx_in)?;
3336

37+
if !mock {
38+
ensure!(!spell.mock, "spell is a mock, but we are not in mock mode");
39+
}
3440
ensure!(
35-
&spell.tx.ins.is_none(),
41+
spell.tx.ins.is_none(),
3642
"spell must inherit inputs from the enchanted tx"
3743
);
3844
ensure!(
39-
&spell.tx.outs.len() <= &tx.output.len(),
45+
spell.tx.outs.len() <= tx.output.len(),
4046
"spell tx outs mismatch"
4147
);
4248

4349
let spell = spell_with_ins(spell, tx_ins);
4450

45-
let (spell_vk, groth16_vk) = crate::tx::vks(spell.version, spell_vk)?;
51+
let spell_vk = tx::spell_vk(spell.version, spell_vk, spell.mock)?;
52+
53+
let public_values = tx::to_serialized_pv(spell.version, &(spell_vk, &spell));
4654

47-
Groth16Verifier::verify(
48-
&proof,
49-
crate::tx::to_sp1_pv(spell.version, &(spell_vk, &spell)).as_slice(),
50-
spell_vk,
51-
groth16_vk,
52-
)
53-
.map_err(|e| anyhow!("could not verify spell proof: {}", e))?;
55+
tx::verify_snark_proof(&proof, &public_values, spell_vk, spell.version, spell.mock)?;
5456

5557
Ok(spell)
5658
}

charms-client/src/cardano_tx.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
use crate::{tx::EnchantedTx, NormalizedSpell, Proof};
1+
use crate::{NormalizedSpell, Proof, tx, tx::EnchantedTx};
22
use anyhow::{anyhow, ensure};
3-
use charms_data::{util, TxId, UtxoId};
3+
use charms_data::{TxId, UtxoId, util};
44
use cml_chain::{
5+
Deserialize, Serialize, SetTransactionInput,
56
crypto::TransactionHash,
67
plutus::PlutusData,
78
transaction::{ConwayFormatTxOut, DatumOption, Transaction, TransactionOutput},
8-
Deserialize, Serialize, SetTransactionInput,
99
};
10-
use sp1_verifier::Groth16Verifier;
11-
1210
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1311
pub struct CardanoTx(pub Transaction);
1412

@@ -31,7 +29,11 @@ impl CardanoTx {
3129
}
3230

3331
impl EnchantedTx for CardanoTx {
34-
fn extract_and_verify_spell(&self, spell_vk: &str) -> anyhow::Result<NormalizedSpell> {
32+
fn extract_and_verify_spell(
33+
&self,
34+
spell_vk: &str,
35+
mock: bool,
36+
) -> anyhow::Result<NormalizedSpell> {
3537
let tx = &self.0;
3638

3739
let inputs = &tx.body.inputs;
@@ -60,6 +62,9 @@ impl EnchantedTx for CardanoTx {
6062
let (spell, proof): (NormalizedSpell, Proof) = util::read(spell_data.as_slice())
6163
.map_err(|e| anyhow!("could not parse spell and proof: {}", e))?;
6264

65+
if !mock {
66+
ensure!(!spell.mock, "spell is a mock, but we are not in mock mode");
67+
}
6368
ensure!(
6469
&spell.tx.ins.is_none(),
6570
"spell must inherit inputs from the enchanted tx"
@@ -71,15 +76,11 @@ impl EnchantedTx for CardanoTx {
7176

7277
let spell = spell_with_ins(spell, inputs);
7378

74-
let (spell_vk, groth16_vk) = crate::tx::vks(spell.version, spell_vk)?;
79+
let spell_vk = tx::spell_vk(spell.version, spell_vk, spell.mock)?;
80+
81+
let public_values = tx::to_serialized_pv(spell.version, &(spell_vk, &spell));
7582

76-
Groth16Verifier::verify(
77-
&proof,
78-
crate::tx::to_sp1_pv(spell.version, &(spell_vk, &spell)).as_slice(),
79-
spell_vk,
80-
groth16_vk,
81-
)
82-
.map_err(|e| anyhow!("could not verify spell proof: {}", e))?;
83+
tx::verify_snark_proof(&proof, &public_values, spell_vk, spell.version, spell.mock)?;
8384

8485
Ok(spell)
8586
}

charms-client/src/lib.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::collections::{BTreeMap, BTreeSet};
66

77
pub use charms_app_runner::{AppProverInput, AppProverOutput};
88

9+
pub mod ark;
910
pub mod bitcoin_tx;
1011
pub mod cardano_tx;
1112
pub mod tx;
@@ -14,6 +15,8 @@ pub const APP_VK: [u32; 8] = [
1415
773139792, 1871461666, 1172442063, 346922495, 1779450904, 263758648, 121652725, 113479979,
1516
];
1617

18+
pub const MOCK_SPELL_VK: &str = "7c38e8639a2eac0074cee920982b92376513e8940f4a7ca6859f17a728af5b0e";
19+
1720
/// Verification key for version `0` of the protocol implemented by `charms-spell-checker` binary.
1821
pub const V0_SPELL_VK: &str = "0x00e9398ac819e6dd281f81db3ada3fe5159c3cc40222b5ddb0e7584ed2327c5d";
1922
/// Verification key for version `1` of the protocol implemented by `charms-spell-checker` binary.
@@ -84,7 +87,7 @@ impl NormalizedTransaction {
8487
}
8588

8689
/// Proof of spell correctness.
87-
pub type Proof = Box<[u8]>;
90+
pub type Proof = Vec<u8>;
8891

8992
/// Normalized representation of a spell.
9093
/// Can be committed as public input.
@@ -96,6 +99,9 @@ pub struct NormalizedSpell {
9699
pub tx: NormalizedTransaction,
97100
/// Maps all `App`s in the transaction to (potentially empty) public input data.
98101
pub app_public_inputs: BTreeMap<App, Data>,
102+
/// Is this a mock spell?
103+
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
104+
pub mock: bool,
99105
}
100106

101107
pub fn utxo_id_hash(utxo_id: &UtxoId) -> B32 {
@@ -108,6 +114,7 @@ pub fn utxo_id_hash(utxo_id: &UtxoId) -> B32 {
108114
pub fn prev_spells(
109115
prev_txs: &Vec<Tx>,
110116
spell_vk: &str,
117+
mock: bool,
111118
) -> BTreeMap<TxId, (Option<NormalizedSpell>, usize)> {
112119
prev_txs
113120
.iter()
@@ -116,7 +123,7 @@ pub fn prev_spells(
116123
(
117124
tx_id,
118125
(
119-
extract_and_verify_spell(spell_vk, tx)
126+
extract_and_verify_spell(spell_vk, tx, mock)
120127
.map_err(|e| {
121128
tracing::info!("no correct spell in tx {}: {}", tx_id, e);
122129
})

charms-client/src/tx.rs

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
use crate::{
2-
CURRENT_VERSION, NormalizedSpell, V0, V0_SPELL_VK, V1, V1_SPELL_VK, V2, V2_SPELL_VK, V3,
3-
V3_SPELL_VK, V4, V4_SPELL_VK, V5, V5_SPELL_VK, bitcoin_tx::BitcoinTx, cardano_tx::CardanoTx,
2+
CURRENT_VERSION, MOCK_SPELL_VK, NormalizedSpell, V0, V0_SPELL_VK, V1, V1_SPELL_VK, V2,
3+
V2_SPELL_VK, V3, V3_SPELL_VK, V4, V4_SPELL_VK, V5, V5_SPELL_VK, ark, bitcoin_tx::BitcoinTx,
4+
cardano_tx::CardanoTx,
45
};
5-
use anyhow::bail;
6+
use anyhow::{anyhow, bail};
67
use charms_data::{TxId, util};
78
use enum_dispatch::enum_dispatch;
89
use serde::{Deserialize, Serialize};
910
use serde_with::{IfIsHumanReadable, serde_as};
1011
use sp1_primitives::io::SP1PublicValues;
12+
use sp1_verifier::Groth16Verifier;
1113

1214
#[enum_dispatch]
1315
pub trait EnchantedTx {
14-
fn extract_and_verify_spell(&self, spell_vk: &str) -> anyhow::Result<NormalizedSpell>;
16+
fn extract_and_verify_spell(
17+
&self,
18+
spell_vk: &str,
19+
mock: bool,
20+
) -> anyhow::Result<NormalizedSpell>;
1521
fn tx_outs_len(&self) -> usize;
1622
fn tx_id(&self) -> TxId;
1723
fn hex(&self) -> String;
@@ -65,23 +71,48 @@ impl Tx {
6571
/// Extract a [`NormalizedSpell`] from a transaction and verify it.
6672
/// Incorrect spells are rejected.
6773
#[tracing::instrument(level = "debug", skip_all)]
68-
pub fn extract_and_verify_spell(spell_vk: &str, tx: &Tx) -> anyhow::Result<NormalizedSpell> {
69-
tx.extract_and_verify_spell(spell_vk)
74+
pub fn extract_and_verify_spell(
75+
spell_vk: &str,
76+
tx: &Tx,
77+
mock: bool,
78+
) -> anyhow::Result<NormalizedSpell> {
79+
tx.extract_and_verify_spell(spell_vk, mock)
7080
}
7181

72-
pub fn vks(spell_version: u32, spell_vk: &str) -> anyhow::Result<(&str, &[u8])> {
82+
pub fn spell_vk(spell_version: u32, spell_vk: &str, mock: bool) -> anyhow::Result<&str> {
83+
if mock {
84+
return Ok(MOCK_SPELL_VK);
85+
}
7386
match spell_version {
74-
CURRENT_VERSION => Ok((spell_vk, CURRENT_GROTH16_VK_BYTES)),
75-
V5 => Ok((V5_SPELL_VK, V5_GROTH16_VK_BYTES)),
76-
V4 => Ok((V4_SPELL_VK, V4_GROTH16_VK_BYTES)),
77-
V3 => Ok((V3_SPELL_VK, V3_GROTH16_VK_BYTES)),
78-
V2 => Ok((V2_SPELL_VK, V2_GROTH16_VK_BYTES)),
79-
V1 => Ok((V1_SPELL_VK, V1_GROTH16_VK_BYTES)),
80-
V0 => Ok((V0_SPELL_VK, V0_GROTH16_VK_BYTES)),
87+
CURRENT_VERSION => Ok(spell_vk),
88+
V5 => Ok(V5_SPELL_VK),
89+
V4 => Ok(V4_SPELL_VK),
90+
V3 => Ok(V3_SPELL_VK),
91+
V2 => Ok(V2_SPELL_VK),
92+
V1 => Ok(V1_SPELL_VK),
93+
V0 => Ok(V0_SPELL_VK),
8194
_ => bail!("unsupported spell version: {}", spell_version),
8295
}
8396
}
8497

98+
pub fn groth16_vk(spell_version: u32, mock: bool) -> anyhow::Result<&'static [u8]> {
99+
if mock {
100+
return Ok(MOCK_GROTH16_VK_BYTES);
101+
}
102+
match spell_version {
103+
CURRENT_VERSION => Ok(CURRENT_GROTH16_VK_BYTES),
104+
V5 => Ok(V5_GROTH16_VK_BYTES),
105+
V4 => Ok(V4_GROTH16_VK_BYTES),
106+
V3 => Ok(V3_GROTH16_VK_BYTES),
107+
V2 => Ok(V2_GROTH16_VK_BYTES),
108+
V1 => Ok(V1_GROTH16_VK_BYTES),
109+
V0 => Ok(V0_GROTH16_VK_BYTES),
110+
_ => bail!("unsupported spell version: {}", spell_version),
111+
}
112+
}
113+
114+
pub const MOCK_GROTH16_VK_BYTES: &'static [u8] = include_bytes!("../vk/mock/mock-groth16-vk.bin");
115+
85116
pub const V0_GROTH16_VK_BYTES: &'static [u8] = include_bytes!("../vk/v0/groth16_vk.bin");
86117
pub const V1_GROTH16_VK_BYTES: &'static [u8] = include_bytes!("../vk/v1/groth16_vk.bin");
87118
pub const V2_GROTH16_VK_BYTES: &'static [u8] = V1_GROTH16_VK_BYTES;
@@ -91,21 +122,36 @@ pub const V5_GROTH16_VK_BYTES: &'static [u8] = V4_GROTH16_VK_BYTES;
91122
pub const V6_GROTH16_VK_BYTES: &'static [u8] = V5_GROTH16_VK_BYTES;
92123
pub const CURRENT_GROTH16_VK_BYTES: &'static [u8] = V6_GROTH16_VK_BYTES;
93124

94-
pub fn to_sp1_pv<T: Serialize>(spell_version: u32, t: &T) -> SP1PublicValues {
95-
let mut pv = SP1PublicValues::new();
125+
pub fn to_serialized_pv<T: Serialize>(spell_version: u32, t: &T) -> Vec<u8> {
96126
match spell_version {
97127
CURRENT_VERSION | V5 | V4 | V3 | V2 | V1 => {
98128
// we commit to CBOR-encoded tuple `(spell_vk, n_spell)`
99-
pv.write_slice(util::write(t).unwrap().as_slice());
129+
util::write(t).unwrap()
100130
}
101131
V0 => {
102132
// we used to commit to the tuple `(spell_vk, n_spell)`, which was serialized internally
103133
// by SP1
134+
let mut pv = SP1PublicValues::new();
104135
pv.write(t);
136+
pv.to_vec()
105137
}
106138
_ => unreachable!(),
107139
}
108-
pv
140+
}
141+
142+
pub fn verify_snark_proof(
143+
proof: &[u8],
144+
public_inputs: &[u8],
145+
vk_hash: &str,
146+
spell_version: u32,
147+
mock: bool,
148+
) -> anyhow::Result<()> {
149+
let groth16_vk = groth16_vk(spell_version, mock)?;
150+
match mock {
151+
false => Groth16Verifier::verify(proof, public_inputs, vk_hash, groth16_vk)
152+
.map_err(|e| anyhow!("could not verify spell proof: {}", e)),
153+
true => ark::verify_groth16_proof(proof, public_inputs, groth16_vk),
154+
}
109155
}
110156

111157
#[cfg(test)]
440 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)