diff --git a/merkle-tree-example/README.md b/merkle-tree-example/README.md index f282138..bee66d8 100644 --- a/merkle-tree-example/README.md +++ b/merkle-tree-example/README.md @@ -42,4 +42,4 @@ Now, we must fill in the blanks by adding constraints to check the membership p Once we've written our path-checking constraints, we have to check that the resulting constraint system satisfies two properties: that it accepts a valid membership path, and that it rejects an invalid path. We perform these checks via two tests: `merkle_tree_constraints_correctness` and `merkle_tree_constraints_soundness`. Go ahead and look at those for an example of how to test constraint systems in practice. -This wraps up this part of the tutorial. Go to the `simple_payments` folder for the next step! \ No newline at end of file +This wraps up this part of the tutorial. Go to the `simple_payments` folder for the next step! diff --git a/merkle-tree-example/src/constraints.rs b/merkle-tree-example/src/constraints.rs index 35c77b9..d81e2b1 100644 --- a/merkle-tree-example/src/constraints.rs +++ b/merkle-tree-example/src/constraints.rs @@ -187,4 +187,4 @@ fn merkle_tree_constraints_soundness() { let is_satisfied = cs.is_satisfied().unwrap(); // We expect this to fail! assert!(!is_satisfied); -} \ No newline at end of file +} diff --git a/rollup/README.md b/rollup/README.md index ca68caf..e21beed 100644 --- a/rollup/README.md +++ b/rollup/README.md @@ -11,21 +11,21 @@ At a high level, the constraint system for batch verification works as follows: * Checks: For each transaction in the batch, check the validity of applying that transaction: - (1) Check a Merkle Tree path wrt initial root that demonstrates the existence of the sender's account. - (2) Check a Merkle Tree path wrt initial root that demonstrates the existence of the receiver's account. - (3) Verify the signature in the transaction with respect to the sender's public key. - (4) Verify that sender.balance >= tx.amount (i.e., sender has sufficient funds). - (5) Compute new balances for both the sender and the receiver. - (6) Check a Merkle Tree path wrt final root for the new sender balance. - (7) Check a Merkle Tree path wrt final root for the new receiver balance. + 1) Check a Merkle Tree path wrt initial root that demonstrates the existence of the sender's account. + 2) Check a Merkle Tree path wrt initial root that demonstrates the existence of the receiver's account. + 3) Verify the signature in the transaction with respect to the sender's public key. + 4) Verify that sender.balance >= tx.amount (i.e., sender has sufficient funds). + 5) Compute new balances for both the sender and the receiver. + 6) Check a Merkle Tree path wrt final root for the new sender balance. + 7) Check a Merkle Tree path wrt final root for the new receiver balance. To make it easier to write out this constraint system, we've provided gadget equivalents of the key data structures from `simple-payments`. Find these via `cargo doc --open --no-deps`. ## Verifying a single transaction -Our first task will be to verify the state transitions involved when applying a single transaction. Go to [`transaction.rs`](./src/transaction.rs) and fill in the blanks in the `validate` method, following the hints there. Use the pseudocode [above](#batch-verification) and the logic in `simple_payments::transaction::Transaction::validate` as guides. To check if your code works, run `cargo test single_tx_validity_test`. +Our first task will be to verify the state transitions involved when applying a single transaction. Go to [`transaction.rs`](./src/transaction.rs) and fill in the blanks in the `validate` method, following the hints there. Use the pseudocode [above](#batch-verification) and the logic in `simple_payments::transaction::Transaction::validate` as guides. To check if your code works, run `cargo test unary_rollup_validity_test`. ## Verifying a batch of transactions -Use the foregoing validation logic to verify a batch of transactions in the `generate_constraints` method in [`rollup.rs#148], and verify that your circuit works via `cargo test end_to_end`, and then test that you can generate a valid proof via `cargo test snark_verification`. \ No newline at end of file +Use the foregoing validation logic to verify a batch of transactions in the `generate_constraints` method in [`rollup.rs#148`], and verify that your circuit works via `cargo test single_tx_validity_test` and `cargo test end_to_end`, and then test that you can generate a valid proof via `cargo test snark_verification`. diff --git a/rollup/src/account.rs b/rollup/src/account.rs index 06a82e2..3c4d996 100644 --- a/rollup/src/account.rs +++ b/rollup/src/account.rs @@ -68,7 +68,7 @@ impl AllocVar for AccountInformationVar { let cs = cs.into(); let public_key = AccountPublicKeyVar::new_variable(cs.clone(), || Ok(&info.public_key), mode)?; - let balance = AmountVar::new_variable(cs.clone(), || Ok(&info.balance), mode)?; + let balance = AmountVar::new_variable(cs, || Ok(&info.balance), mode)?; Ok(Self { public_key, balance, diff --git a/rollup/src/rollup.rs b/rollup/src/rollup.rs index 8b19beb..3cd5c82 100644 --- a/rollup/src/rollup.rs +++ b/rollup/src/rollup.rs @@ -97,12 +97,12 @@ impl Rollup { let sender_id = tx.sender; let recipient_id = tx.recipient; let pre_tx_root = state.root(); - let sender_pre_acc_info = state.id_to_account_info.get(&sender_id)?.clone(); + let sender_pre_acc_info = *state.id_to_account_info.get(&sender_id)?; let sender_pre_path = state .account_merkle_tree .generate_proof(sender_id.0 as usize) .unwrap(); - let recipient_pre_acc_info = state.id_to_account_info.get(&recipient_id)?.clone(); + let recipient_pre_acc_info = *state.id_to_account_info.get(&recipient_id)?; let recipient_pre_path = state .account_merkle_tree .generate_proof(recipient_id.0 as usize) @@ -308,7 +308,7 @@ mod test { ) .unwrap(); assert!(test_cs(rollup)); - + let mut temp_state = state.clone(); let bad_tx = Transaction::create(&pp, alice_id, bob_id, Amount(5), &bob_sk, &mut rng); assert!(!bad_tx.validate(&pp, &temp_state)); diff --git a/rollup/src/transaction.rs b/rollup/src/transaction.rs index 7d79d00..6e124b3 100644 --- a/rollup/src/transaction.rs +++ b/rollup/src/transaction.rs @@ -1,9 +1,11 @@ use crate::account::{AccountIdVar, AccountInformationVar, AccountPublicKeyVar}; -use crate::ledger::{self, AccPathVar, AccRootVar, AmountVar}; +use crate::ledger::{self, AccPathVar, AccRootVar, AmountVar, ParametersVar}; use crate::ConstraintF; use ark_ed_on_bls12_381::{constraints::EdwardsVar, EdwardsProjective}; use ark_r1cs_std::prelude::*; -use ark_relations::r1cs::{Namespace, SynthesisError}; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; +use ark_simple_payments::account::AccountInformation; +use ark_simple_payments::ledger::{AccPath, AccRoot, Parameters, State}; use ark_simple_payments::signature::schnorr::constraints::{ ParametersVar as SchnorrParamsVar, SchnorrSignatureVerifyGadget, SignatureVar, }; @@ -37,7 +39,7 @@ impl TransactionVar { let mut message = self.sender.to_bytes_le(); message.extend(self.recipient.to_bytes_le()); message.extend(self.amount.to_bytes_le()); - SchnorrSignatureVerifyGadget::verify(&pp, &pub_key, &message, &self.signature) + SchnorrSignatureVerifyGadget::verify(pp, pub_key, &message, &self.signature) } /// Check that the transaction is valid for the given ledger state. This checks @@ -47,6 +49,7 @@ impl TransactionVar { /// 2. Verify that the sender's account has sufficient balance to finance /// the transaction. /// 3. Verify that the recipient's account exists. + #[allow(clippy::too_many_arguments)] #[tracing::instrument( target = "r1cs", skip( @@ -137,3 +140,216 @@ impl AllocVar for TransactionVar { }) } } + +pub struct UnaryRollup { + /// The ledger parameters. + pub ledger_params: Parameters, + /// The Merkle tree root before applying this batch of transactions. + pub initial_root: AccRoot, + /// The Merkle tree root after applying this batch of transactions. + pub final_root: AccRoot, + /// The current batch of transactions. + pub transaction: Transaction, + /// The sender's account information *before* applying the transaction. + pub sender_acc_info: AccountInformation, + /// The sender's authentication path, *before* applying the transaction. + pub sender_pre_path: AccPath, + /// The authentication path corresponding to the sender's account information *after* applying + /// the transactions. + pub sender_post_path: AccPath, + /// The recipient's account information *before* applying the transaction. + pub recv_acc_info: AccountInformation, + /// The recipient's authentication path, *before* applying the transaction. + pub recv_pre_path: AccPath, + /// The authentication path corresponding to the recipient's account information *after* + /// applying the transactions. + pub recv_post_path: AccPath, +} + +impl UnaryRollup { + pub fn with_state_and_transaction( + ledger_params: Parameters, + transaction: Transaction, + state: &mut State, + validate: bool, + ) -> Option { + if validate && !transaction.validate(&ledger_params, &*state) { + return None; + } + + let initial_root = state.root(); + let sender_id = transaction.sender; + let recipient_id = transaction.recipient; + + let sender_acc_info = *state.id_to_account_info.get(&sender_id)?; + let sender_pre_path = state + .account_merkle_tree + .generate_proof(sender_id.0 as usize) + .unwrap(); + + let recv_acc_info = *state.id_to_account_info.get(&recipient_id)?; + let recv_pre_path = state + .account_merkle_tree + .generate_proof(recipient_id.0 as usize) + .unwrap(); + + if validate { + state.apply_transaction(&ledger_params, &transaction)?; + } else { + let _ = state.apply_transaction(&ledger_params, &transaction); + } + + let final_root = state.root(); + let sender_post_path = state + .account_merkle_tree + .generate_proof(sender_id.0 as usize) + .unwrap(); + let recv_post_path = state + .account_merkle_tree + .generate_proof(recipient_id.0 as usize) + .unwrap(); + + Some(UnaryRollup { + ledger_params, + initial_root, + final_root, + transaction, + sender_acc_info, + sender_pre_path, + sender_post_path, + recv_acc_info, + recv_pre_path, + recv_post_path, + }) + } +} + +impl ConstraintSynthesizer for UnaryRollup { + fn generate_constraints( + self, + cs: ConstraintSystemRef, + ) -> Result<(), SynthesisError> { + // Declare the parameters as constants. + let ledger_params = ParametersVar::new_constant( + ark_relations::ns!(cs, "Ledger parameters"), + &self.ledger_params, + )?; + // Declare the initial root as a public input. + let initial_root = AccRootVar::new_input(ark_relations::ns!(cs, "Initial root"), || { + Ok(self.initial_root) + })?; + // Declare the final root as a public input. + let final_root = + AccRootVar::new_input(ark_relations::ns!(cs, "Final root"), || Ok(self.final_root))?; + + // Declare transaction as a witness. + let tx = TransactionVar::new_witness(ark_relations::ns!(cs, "Transaction"), || { + Ok(self.transaction.clone()) + })?; + + // Declare the sender's initial account balance... + let sender_acc_info = AccountInformationVar::new_witness( + ark_relations::ns!(cs, "Sender Account Info"), + || Ok(self.sender_acc_info), + )?; + // ..., corresponding authentication path, ... + let sender_pre_path = + AccPathVar::new_witness(ark_relations::ns!(cs, "Sender Pre-Path"), || { + Ok(self.sender_pre_path.clone()) + })?; + // ... and authentication path after the update. + let sender_post_path = + AccPathVar::new_witness(ark_relations::ns!(cs, "Sender Post-Path"), || { + Ok(self.sender_post_path.clone()) + })?; + + // Declare the recipient's initial account balance... + let recipient_acc_info = AccountInformationVar::new_witness( + ark_relations::ns!(cs, "Recipient Account Info"), + || Ok(self.recv_acc_info), + )?; + // ..., corresponding authentication path, ... + let recipient_pre_path = + AccPathVar::new_witness(ark_relations::ns!(cs, "Recipient Pre-Path"), || { + Ok(self.recv_pre_path.clone()) + })?; + // ... and authentication path after the update. + let recipient_post_path = + AccPathVar::new_witness(ark_relations::ns!(cs, "Recipient Post-Path"), || { + Ok(self.recv_post_path.clone()) + })?; + + // Validate that the transaction signature and amount is correct. + tx.validate( + &ledger_params, + &sender_acc_info, + &sender_pre_path, + &sender_post_path, + &recipient_acc_info, + &recipient_pre_path, + &recipient_post_path, + &initial_root, + &final_root, + )? + .enforce_equal(&Boolean::TRUE) + } +} + +#[cfg(test)] +mod test { + use super::*; + use ark_relations::r1cs::{ + ConstraintLayer, ConstraintSynthesizer, ConstraintSystem, TracingMode::OnlyConstraints, + }; + use ark_simple_payments::ledger::{Amount, Parameters, State}; + use ark_simple_payments::transaction::Transaction; + use tracing_subscriber::layer::SubscriberExt; + + fn test_cs(rollup: UnaryRollup) -> bool { + let mut layer = ConstraintLayer::default(); + layer.mode = OnlyConstraints; + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + let cs = ConstraintSystem::new_ref(); + rollup.generate_constraints(cs.clone()).unwrap(); + let result = cs.is_satisfied().unwrap(); + if !result { + println!("{:?}", cs.which_is_unsatisfied()); + } + result + } + + #[test] + fn unary_rollup_validity_test() { + let mut rng = ark_std::test_rng(); + let pp = Parameters::sample(&mut rng); + let mut state = State::new(32, &pp); + // Let's make an account for Alice. + let (alice_id, _alice_pk, alice_sk) = + state.sample_keys_and_register(&pp, &mut rng).unwrap(); + // Let's give her some initial balance to start with. + state + .update_balance(alice_id, Amount(20)) + .expect("Alice's account should exist"); + // Let's make an account for Bob. + let (bob_id, _bob_pk, bob_sk) = state.sample_keys_and_register(&pp, &mut rng).unwrap(); + + // Alice wants to transfer 5 units to Bob. + let mut temp_state = state.clone(); + let tx1 = Transaction::create(&pp, alice_id, bob_id, Amount(5), &alice_sk, &mut rng); + assert!(tx1.validate(&pp, &temp_state)); + let rollup = + UnaryRollup::with_state_and_transaction(pp.clone(), tx1, &mut temp_state, true) + .unwrap(); + assert!(test_cs(rollup)); + + let mut temp_state = state.clone(); + let bad_tx = Transaction::create(&pp, alice_id, bob_id, Amount(5), &bob_sk, &mut rng); + assert!(!bad_tx.validate(&pp, &temp_state)); + assert!(matches!(temp_state.apply_transaction(&pp, &bad_tx), None)); + let rollup = + UnaryRollup::with_state_and_transaction(pp.clone(), bad_tx, &mut temp_state, false) + .unwrap(); + assert!(!test_cs(rollup)); + } +} diff --git a/simple-payments/README.md b/simple-payments/README.md index 8c8304c..f159627 100644 --- a/simple-payments/README.md +++ b/simple-payments/README.md @@ -14,12 +14,15 @@ To transfer value from their account to another account, the user first creates * Recipient's account identifier * Transaction amount * Signature on the previous three parts, using the signature public key associated with the sender's account. + The user then publishes this to the ledger, which applies the transaction via `ledger::State::apply_transaction`. + The latter method updates the ledger's information if the following conditions are satisfied: * The sender's account exists * The recipient's account exists * The sender's account contains a balance greater than or equal to the transaction amount * The signature is valid with respect to the public key stored in the sender's account + To enforce this logic, `Transaction::verify` performs the following steps on input a transaction `tx` and existing ledger state `State`. * Look up the `(SigPubKey, Balance)` tuple corresponding to the sender's ID in the Merkle tree in `State`. * Verify the transaction signature with respect to `SigPubKey`. @@ -41,4 +44,4 @@ Our implementation uses the Merkle tree of [`ark-crypto-primitives`](https://doc ## Code walk-through -To get an overview of important data structures as well as their associated methods, run `cargo doc --open --no-deps`. \ No newline at end of file +To get an overview of important data structures as well as their associated methods, run `cargo doc --open --no-deps`. diff --git a/simple-payments/src/account.rs b/simple-payments/src/account.rs index 375a9d1..1d95dee 100644 --- a/simple-payments/src/account.rs +++ b/simple-payments/src/account.rs @@ -39,4 +39,4 @@ impl AccountInformation { pub fn to_bytes_le(&self) -> Vec { ark_ff::to_bytes![self.public_key, self.balance.to_bytes_le()].unwrap() } -} \ No newline at end of file +} diff --git a/simple-payments/src/signature/schnorr/constraints.rs b/simple-payments/src/signature/schnorr/constraints.rs index 6927a8f..b8ee4cc 100644 --- a/simple-payments/src/signature/schnorr/constraints.rs +++ b/simple-payments/src/signature/schnorr/constraints.rs @@ -130,7 +130,7 @@ where for i in 0..32 { constraint_salt.push(UInt8::>::new_variable( cs.clone(), - || Ok(native_salt.unwrap()[i].clone()), + || Ok(native_salt.unwrap()[i]), mode, )?); } @@ -186,17 +186,17 @@ where let challenge_bytes = val.borrow().verifier_challenge; let mut prover_response = Vec::>>::new(); let mut verifier_challenge = Vec::>>::new(); - for i in 0..response_bytes.len() { + for byte in &response_bytes { prover_response.push(UInt8::>::new_variable( cs.clone(), - || Ok(response_bytes[i].clone()), + || Ok(byte), mode, )?); } - for i in 0..32 { + for byte in &challenge_bytes { verifier_challenge.push(UInt8::>::new_variable( cs.clone(), - || Ok(challenge_bytes[i].clone()), + || Ok(byte), mode, )?); } diff --git a/simple-payments/src/signature/schnorr/mod.rs b/simple-payments/src/signature/schnorr/mod.rs index daaa4d9..c6ccbe1 100644 --- a/simple-payments/src/signature/schnorr/mod.rs +++ b/simple-payments/src/signature/schnorr/mod.rs @@ -162,7 +162,7 @@ where } hash_input.extend_from_slice(&to_bytes![pk]?); hash_input.extend_from_slice(&to_bytes![claimed_prover_commitment]?); - hash_input.extend_from_slice(&message); + hash_input.extend_from_slice(message); // cast the hash output to get e let obtained_verifier_challenge = &Blake2s::digest(&hash_input)[..]; diff --git a/simple-payments/src/transaction.rs b/simple-payments/src/transaction.rs index a34f34d..0a7afc8 100644 --- a/simple-payments/src/transaction.rs +++ b/simple-payments/src/transaction.rs @@ -33,7 +33,7 @@ impl Transaction { let mut message = self.sender.to_bytes_le(); message.extend(self.recipient.to_bytes_le()); message.extend(self.amount.to_bytes_le()); - Schnorr::verify(&pp, &pub_key, &message, &self.signature).unwrap() + Schnorr::verify(pp, pub_key, &message, &self.signature).unwrap() } /// Check that the transaction is valid for the given ledger state. This checks @@ -87,7 +87,7 @@ impl Transaction { let mut message = sender.to_bytes_le(); message.extend(recipient.to_bytes_le()); message.extend(amount.to_bytes_le()); - let signature = Schnorr::sign(¶meters.sig_params, &sender_sk, &message, rng).unwrap(); + let signature = Schnorr::sign(¶meters.sig_params, sender_sk, &message, rng).unwrap(); Self { sender, recipient,