Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion merkle-tree-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
This wraps up this part of the tutorial. Go to the `simple_payments` folder for the next step!
2 changes: 1 addition & 1 deletion merkle-tree-example/src/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,4 @@ fn merkle_tree_constraints_soundness() {
let is_satisfied = cs.is_satisfied().unwrap();
// We expect this to fail!
assert!(!is_satisfied);
}
}
18 changes: 9 additions & 9 deletions rollup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
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`.
2 changes: 1 addition & 1 deletion rollup/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ impl AllocVar<AccountInformation, ConstraintF> 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,
Expand Down
6 changes: 3 additions & 3 deletions rollup/src/rollup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ impl<const NUM_TX: usize> Rollup<NUM_TX> {
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)
Expand Down Expand Up @@ -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));
Expand Down
222 changes: 219 additions & 3 deletions rollup/src/transaction.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -137,3 +140,216 @@ impl AllocVar<Transaction, ConstraintF> 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<UnaryRollup> {
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<ConstraintF> for UnaryRollup {
fn generate_constraints(
self,
cs: ConstraintSystemRef<ConstraintF>,
) -> 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));
}
}
5 changes: 4 additions & 1 deletion simple-payments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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`.
To get an overview of important data structures as well as their associated methods, run `cargo doc --open --no-deps`.
2 changes: 1 addition & 1 deletion simple-payments/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ impl AccountInformation {
pub fn to_bytes_le(&self) -> Vec<u8> {
ark_ff::to_bytes![self.public_key, self.balance.to_bytes_le()].unwrap()
}
}
}
10 changes: 5 additions & 5 deletions simple-payments/src/signature/schnorr/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ where
for i in 0..32 {
constraint_salt.push(UInt8::<ConstraintF<C>>::new_variable(
cs.clone(),
|| Ok(native_salt.unwrap()[i].clone()),
|| Ok(native_salt.unwrap()[i]),
mode,
)?);
}
Expand Down Expand Up @@ -186,17 +186,17 @@ where
let challenge_bytes = val.borrow().verifier_challenge;
let mut prover_response = Vec::<UInt8<ConstraintF<C>>>::new();
let mut verifier_challenge = Vec::<UInt8<ConstraintF<C>>>::new();
for i in 0..response_bytes.len() {
for byte in &response_bytes {
prover_response.push(UInt8::<ConstraintF<C>>::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::<ConstraintF<C>>::new_variable(
cs.clone(),
|| Ok(challenge_bytes[i].clone()),
|| Ok(byte),
mode,
)?);
}
Expand Down
2 changes: 1 addition & 1 deletion simple-payments/src/signature/schnorr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)[..];
Expand Down
Loading