Overview
We outline SharedUintNote, a kind of partial note that allows a reimbursement to be performed in a given token TOKEN0, while the publicly known value of the reimbursement is in units of another token TOKEN1, without leaking the exchange rate between the tokens.
I.e., running the following in public
SharedUintNote{..., value}.mul(reimbursement)
would result in the completion of two private notes:
UintNote{owner: creator, value: price * reimbursement}UintNote{owner: completer, value: price * (value - reimbursement)}
with the following guarantees:
creator,completer, andpriceremain private- Total token supply is conserved
Recap: partial notes now and before
Now
Currently, note hashes have the following form
commitment = poseidon2_hash_with_separator(
[owner, randomness, storage_slot],
GENERATOR_INDEX__NOTE_HASH,
)
complete_hash = poseidon2_hash_with_separator(
[commitment, value],
GENERATOR_INDEX__NOTE_HASH,
)
This allows for the construction of
struct PartialUintNote {
commitment: Field
}
that can be constructed in private but completed in public, since the commitment suffices for any actor (the “completer”) to construct the note hash for a normal note with the committed data and a value chosen by the completer.
Before
Note hashes used MSM (multi-scalar multiplication). Paraphrasing these docs,
hash =
owner * g_owner
+ randomness * g_randomness
+ storage_slot * g_slot
+ value * g_value;
where the g variables are elliptic curve generators.
This allows the completion of a partial note after an additive operation without knowing the initial value, since hash + delta_value * g_value is the hash for a note with the same data but value value + delta_value.
Note that this hash computation can again be split as follows:
commitment = owner * g_owner + randomness * g_randomness + storage_slot * g_slot + value
hash = commitment + delta_value * g_value
Partial note flow
The usual flow of a partial note is as follows
sequenceDiagram
participant AppContract
participant TokenContract
participant UintNote
participant PartialNote
rect rgb(0, 64, 64, .5)
note over AppContract, PartialNote: Private
AppContract ->> TokenContract: call prepare_partial_note(..., owner)
TokenContract ->> UintNote: call partial(..., owner)
note over UintNote: log stuff
note over UintNote: compute commitment(owner, randomness)
note over UintNote: let partial_note = PartialNote{commitment}
UintNote -->> TokenContract: return partial_note
TokenContract -->> AppContract: return partial_note
note over AppContract: enqueue public_function(..., partial_note)
end
rect rgb(128, 90, 100, .5)
note over AppContract, PartialNote: Public
note over AppContract: compute amount
AppContract ->> TokenContract: call finalize(partial_note, amount)
note over TokenContract: balances[sender] -= amount
TokenContract ->> PartialNote: call partial_note.complete(amount)
note over PartialNote: log stuff
note over PartialNote: compute note_hash(commitment, amount)
note over PartialNote: emit note_hash
end
Logging is required to allow for note discovery by the note’s owner.
Concept: multiplying a partial note’s value
We could define the following hash for a note instead
data_commitment =
owner * g_owner
+ data_randomness * g_randomness
+ storage_slot * g_slot;
value_commitment =
value * g_value
+ value_randomness * g_randomness;
hash = data_commitment + value_commitment;
The partial note would look like this
struct PartialUintNote {
data_commitment: Field,
value_commitment: Field,
}
Now the value_commitment can be operated upon in two basic ways:
value_commitment + delta_value * g_valueas beforefactor * value_commitment
Given that
the assignment value_commitment = factor * value_commitment turns the commitment to {value: value, randomness: randomness} into a commitment to {value: factor * value, randomness: factor * randomness}, which produces a valid note.
This way, if TOKEN0 = price * TOKEN1, you could privately construct a partial note storing the value of price. When completing the note, you could assign the price in units of TOKEN1, but multiplying against the partial note’s value commitment would generate a note that’s in units of TOKEN0.
It’s worth noting that price can be a fraction, because the math can be done between Field elements, whose product is associative and commutative. Hence, if price = numerator / denominator, amount_token_1 * price == numerator * amount_token_1 / denominator.
Issue: the token can no longer enforce total supply invariance
Since the value of the note is no longer known in public, the token can no longer publicly decrement value from the completer’s balance in order to ensure conservation of total supply. This can be solved with the shared note, introduced in the next section.
Concept: shared note
We can build upon the previous concept to introduce the following
struct SharedPartialNote {
creator_partial_note: PartialUintNote,
completer_partial_note: PartialUintNote,
amount: Field,
}
When creating such a note, the token would consume notes from the issuer, worth consumed_amount = amount * price. The note is meant as a transaction to the completer, where a refund_amount is to be given back to the creator but can only be computed in public. In order to conceal the exchange rate between TOKEN0 and TOKEN1, all public computations are denominated in units of TOKEN1.
Completion would look like this:
Token::at(token_address)
.finalize_transfer_to_private(refund_amount, shared_partial_note)
.call(&mut context);
This public function would then complete the two partial notes as follows
creator_partial_note.mul_complete(refund_amount);
completer_partial_note.mul_complete(amount - refund_amount);
The resulting notes’ amount would satisfy
let creator_amount = refund_amount * price;
let completer_amount = (max_amount - refund_amount) * price;
assert!(completer_amount + creator_amount == consumed_amount);
This ensures the total token supply remains invariant, addressing the issue mentioned in the previous section.
Conceptually, the creator of the shared note burns their own notes in order to create a “shared” partial note that’s worth the same amount. The completer will define the amount to return to the creator, and the shared note will then emit two normal notes: one with the return amount for the creator, and another with the rest for the completer.
The exchange rate between the public amounts and the private amounts is kept private by emitting private notes to both the creator and the completer, instead of publicly modifying the completer’s balance.
Shared note flow
A shared note’s flow is almost identical to that of a normal partial note from the perspective of the app contract, except we must specify the private parameter price and the public parameter amount. Remember that to allow this, the partial note now must work as described in the previous section.
sequenceDiagram
participant AppContract
participant TokenContract
participant UintNote
participant SharedNote
participant PartialNote
rect rgb(0, 64, 64, .5)
note over AppContract, PartialNote: Private
note over AppContract: define price and max_amount
AppContract ->> TokenContract: call prepare_shared_note(..., owner, completer, price, max_amount)
note over TokenContract: burn notes from the creator worth (max_amount * price)
TokenContract ->> UintNote: call shared(..., owner, completer, price, max_amount)
note over UintNote: log stuff
note over UintNote: compute owner_commitment(owner, owner_randomness)
note over UintNote: compute completer_commitment(completer, completer_randomness)
note over UintNote: compute price_commitment(price, price_randomness)
note over UintNote: let owner_note = PartialNote{owner_commitment, price_commitment}
note over UintNote: let completer_note = PartialNote{completer_commitment, price_commitment}
note over UintNote: let shared_note = SharedNote{owner_note, completer_note, max_amount}
UintNote -->> TokenContract: return shared_note
TokenContract -->> AppContract: return shared_note
note over AppContract: enqueue public_function(..., shared_note)
end
rect rgb(128, 90, 100, .5)
note over AppContract, PartialNote: Public
note over AppContract: compute amount
AppContract ->> TokenContract: call finalize(shared_note, amount)
note over TokenContract: let amount = max(max_amount, amount)
TokenContract ->> SharedNote: call shared_note.mul_complete(amount)
SharedNote ->> PartialNote: call owner_note.mul_complete(amount)
note over PartialNote: log stuff and emit finalized note hash
SharedNote ->> PartialNote: call completer_note.mul_complete(max_amount - amount)
note over PartialNote: log stuff and emit finalized note hash
end
Use case: FPCs
With the current design of private notes, an FPC that takes an arbitrary token TOK as input would be forced to reveal the price between TOK and AZT (FeeJuice), which leaks some information. This scheme would allow the FPC to quantify the reimbursement in AZT but produce notes for TOK, only leaking the used token but not the exchange rate.