JoinSplit Circuit
Circuit Description
This circuit allows notes to be spent.
The circuit takes in two input notes, and two new output notes, and updates the Note Tree and Nullifier Tree accordingly.
Circuit Inputs: Summary
The inputs for the join-split circuit are all elements of the field from the BN254 specification.
Public Inputs: Detail
proof_id
output_note_commitment_1
output_note_commitment_2
nullifier_1
nullifier_2
public_value
public_owner
public_asset_id
old_data_tree_root
tx_fee
tx_fee_asset_id
bridge_call_data
defi_deposit_value
defi_root
// Note: this will not be used by the circuit, but is included so that the number of public inputs is uniform across base-level circuits.backward_link
allow_chain
Private Inputs: Detail
{
asset_id,
num_input_notes,
input_note_1_index,
input_note_2_index,
input_note_1_path,
input_note_2_path,
input_note_1: {
value,
secret,
owner,
asset_id,
account_required,
creator_pk,
input_nullifier,
},
input_note_2: {
value,
secret,
owner,
asset_id,
account_required,
creator_pk,
input_nullifier,
},
output_note_1: {
value,
secret,
owner,
asset_id,
account_required,
creator_pk, // (creator_pk = optional public key of note creator)
input_nullifier,
},
output_note_2: {
value,
secret,
owner,
asset_id,
account_required,
creator_pk, // (creator_pk = optional public key of note creator)
input_nullifier,
},
partial_claim_note_data: {
deposit_value,
bridge_call_data_local: {
bridge_address_id,
input_asset_id_a,
input_asset_id_b,
output_asset_id_a,
output_asset_id_b,
config: {
second_input_in_use,
second_output_in_use,
},
aux_data,
},
note_secret,
input_nullifier,
},
account_private_key,
alias_hash,
account_required,
account_note_index,
account_note_path,
signing_pk, // (a.k.a. spending public key)
signature,
}
Index of Functions
In the Pseudocode to follow, we use the following function names. See notes & nullifiers for more details.
public_key()
derives a public key from a given secret key.value_note_commit()
- Value note commitment function, which is assumed to be- Collision-resistant
- Field-friendly, which means the output value only depends on the inputs as field elements, and doesn’t change e.g. when input changes from a to a+r as bit string.
partial_value_note_commit()
- Partial value note commitment function. Has the same assumptions asvalue_note_commit
. Uses a different generator. Stresses that the data being committed to is partial - a subset of the data committed to byvalue_note_commit
.partial_claim_note_commit()
- Partial claim note commitment function. Has the same assumptions asvalue_note_commit
. Uses a different generator. Stresses that the data being committed to is partial - a subset of the data committed to byclaim_note_commit
(in the claim circuit).account_note_commit()
- Account note commitment function, which is assumed to be collision resistant.compute_nullifier()
- Nullifier Function, which we assume can be modeled as a random oracle, and only depends onaccount_private_key
.
Circuit Logic (Pseudocode)
// range checks:
for i = 1,2:
{
check:
input_note_i_index < 2 ** DATA_TREE_DEPTH
input_note_i.value < 2 ** NOTE_VALUE_BIT_LENGTH
output_note_i.value < 2 ** NOTE_VALUE_BIT_LENGTH
}
partial_claim_note.deposit_value < 2 ** DEFI_DEPOSIT_VALUE_BIT_LENGTH
asset_id < 2 ** MAX_NUM_ASSETS_BIT_LENGTH
public_value < 2 ** NOTE_VALUE_BIT_LENGTH
tx_fee < 2 ** TX_FEE_BIT_LENGTH
account_note_index < 2 ** DATA_TREE_DEPTH
alias_hash < 2 ** ALIAS_HASH_BIT_LENGTH
account_required < 2
num_input_notes in {0, 1, 2}
allow_chain in {0, 1, 2, 3}
// tx type initialisations:
const is_deposit = proof_id == DEPOSIT
const is_withdraw = proof_id == WITHDRAW
const is_send = proof_id == SEND
const is_defi_deposit = proof_id == DEFI_DEPOSIT
const is_public_tx = is_deposit || is_withdraw
// public value initialisations
const public_asset_id = is_public_tx ? asset_id : 0;
const public_input = is_deposit ? public_value : 0;
const public_output = is_withdraw ? public_value : 0;
// account initialisations
const account_pk = public_key(account_private_key);
const signer_pk = account_required ? signing_pk.x : account_pk.x;
const account_note = {
alias_hash,
account_pk,
signer_pk,
};
const account_note_commitment = account_note_commit(account_note);
// commitments
for i in 1,2
{
input_note_i.commitment = value_note_commit(input_note_i);
output_note_i.commitment = value_note_commit(output_note_i);
}
// Data validity checks:
require(num_input_notes = 0 || 1 || 2); // it's pseudocode!
require(is_deposit || is_send || is_withdraw || is_defi_deposit);
if(num_input_notes == 0) require(is_deposit);
if (is_public_tx) {
require(public_value > 0);
require(public_owner > 0);
} else {
require(public_value == 0);
require(public_owner == 0);
}
require(input_note_1.commitment != input_note_2.commitment);
require(
(asset_id == input_note_1.asset_id) &&
(asset_id == output_note_1.asset_id) &&
(asset_id == output_note_2.asset_id) &&
);
if (
(num_input_notes == 2) &&
!is_defi_deposit
) {
require(input_note_1.asset_id == input_note_2.asset_id);
}
require(account_private_key != 0);
const account_public_key = public_key(account_private_key);
require(
account_public_key == input_note_1.owner &&
account_public_key == input_note_2.owner
);
require(
account_required == input_note_1.account_required &&
account_required == input_note_2.account_required
);
if (output_note_1.creator_pubkey) {
require(account_public_key == output_note_1.creator_pubkey);
}
if (output_note_2.creator_pubkey) {
require(account_public_key == output_note_2.creator_pubkey);
}
// Defi deposit
let output_note_1_commitment = output_note_1.commitment; // supersedes output_note_1.commitment frin here on in.
let input_note_2_value = input_note_2.value; // supersedes input_note_2.value from here on in.
let output_note_1_value = output_note_1.value;
let defi_deposit_value = 0;
if (is_defi_deposit) {
const partial_value_note = {
secret: partial_claim_note_data.note_secret,
owner: input_note_1.owner,
account_required: input_note_1.account_required,
creator_pubkey = 0,
};
const partial_value_note_commitment = partial_value_note_commit(partial_value_note);
const partial_claim_note = {
deposit_value: partial_claim_note_data.deposit_value,
bridge_call_data: partial_claim_note_data.bridge_call_data_local.to_field(),
partial_value_note_commitment,
input_nullifier: partial_claim_note_data.input_nullifier,
}
const partial_claim_note_commitment = partial_claim_note_commit(partial_claim_note)
output_note_1_commitment = partial_claim_note_commitment;
defi_deposit_value = partial_claim_note.deposit_value;
require(defi_deposit_value > 0);
const { bridge_call_data_local } = partial_claim_note_data;
const bridge_call_data = bridge_call_data_local.to_field();
require(bridge_call_data_local.input_asset_id_a == input_note_1.asset_id);
if (input_note_2_in_use && (input_note_1.asset_id != input_note_2.asset_id)) {
require(defi_deposit_value == input_note_2.value);
require(bridge_call_data_local.config.second_input_in_use);
input_note_2_value = 0; // set to 0 for the 'conservation of value' equations below.
}
if (bridge_call_data_local.config.second_input_in_use) {
require(input_note_2_in_use);
require(input_note_2.asset_id == bridge_call_data_local.input_asset_id_b);
}
output_note_1_value = 0; // set to 0, since the partial claim note replaces it.
}
// Conservation of value: no value created or destroyed:
const total_in_value = public_input + input_note_1.value + input_note_2_value
const total_out_value = public_output + (is_defi_deposit ? defi_deposit_value : output_note_1_value) + output_note_2.valuue
// fee
const tx_fee = total_in_value - total_out_value // (no underflow allowed)
// Check input notes are valid:
let input_note_1_in_use = num_input_notes >= 1;
let input_note_2_in_use = num_input_notes == 2;
for i = 1,2:
{
if (input_note_i_in_use) {
const input_note_commitment_i = value_note_commit(input_note_i);
const exists = check_membership(
input_note_commitment_i, input_note_i_index, input_note_i_path, old_data_tree_root
);
require(exists);
} else {
require(input_note_i.value == 0);
}
}
// Compute nullifiers
for i = 1,2:
{
nullifier_i = compute_nullifier(
input_note_i.commitment,
account_private_key,
input_note_i_in_use,
);
}
require(
output_note_1.input_nullifier == nullifier_1 &&
output_note_2.input_nullifier == nullifier_2 &&
partial_claim_note.input_nullifier == is_defi_deposit ? nullifier_1 : 0;
)
// Verify account ownership
check_membership(account_note_commitment, account_note_index, account_note_path, old_data_tree_root);
message = (
public_value,
public_owner,
public_asset_id,
output_note_1_commitment, // notice this is NOT output_note_1.commitment
output_note_2.commitment,
nullifier_1,
nullifier_2,
backward_link,
allow_chain,
);
verify_signature(
message,
signature,
signer_public_key
);
// Check chained transaction inputs are valid:
const backward_link_in_use = inputs.backward_link != 0;
const note1_propagated = inputs.backward_link == input_note_1.commitment;
const note2_propagated = inputs.backward_link == input_note_2.commitment;
if (backward_link_in_use) require(note1_propagated || note2_propagated);
if (is_defi_deposit) require(allow_chain != 1);
if (inputs.allow_chain == 1) require(output_note_1.owner == input_note_1.owner);
if (inputs.allow_chain == 2) require(output_note_2.owner == input_note_1.owner);
// Constrain unused public inputs to zero:
require(defi_root == 0);
// Set public inputs (simply listed here without syntax):
proof_id,
output_note_1_commitment,
output_note_2.commitment,
nullifier_1,
nullifier_2,
public_value,
public_owner,
public_asset_id,
old_data_tree_root,
tx_fee,
asset_id,
bridge_call_data,
defi_deposit_value,
defi_root,
backward_link,
allow_chain