Przeglądaj źródła

doc: update the dao and money contract specs

darkfi 2 tygodni temu
rodzic
commit
804f296154

+ 2 - 2
doc/src/spec/contract/dao/concepts.md

@@ -4,12 +4,12 @@ The governance process is divided in a few steps that are outlined
 below:
 
 * **Propose:** a proposal is submitted to the blockchain.
-* **Vote:** governance token holdens can vote on the proposal.
+* **Vote:** governance token holders can vote on the proposal.
 * **Exec:** if the proposal passes within the time limit then the
   proposal is executed.
 
 > Note: There is a special case where a proposal can be executed before
-> voting period passes, if its strongly supported, based on the
+> the voting period passes, if it's strongly supported, based on the
 > configured early execution quorum.
 
 ## Propose

+ 33 - 18
doc/src/spec/contract/dao/model.md

@@ -12,10 +12,10 @@ The DAO contains the main parameters that define DAO operation:
   $τ$ required to create a valid proposal on chain. Note this minimum can
   come from multiple token holders.
 * Quorum $Q$ specifies the absolute minimum number of tokens required for
-  before a proposal can be accepted.
+  a proposal to be accepted.
 * Early exec quorum $EEQ$ specifies the absolute minimum number of tokens
-  required for before a proposal can be considered as strongly accepted.
-* The approval ratio $A^\%$ is a tuple that specifies the minimum theshold
+  required for a proposal to be considered as strongly accepted.
+* The approval ratio $A^\% = (A_q, A_b)$ specifies the minimum threshold
   of affirmative yes votes for a proposal to become accepted.
 * Notes public key $NPK$ controls who can view encrypted notes.
 * Proposer public key $pPK$ controls who can mint proposals.
@@ -30,7 +30,8 @@ $$ \begin{aligned}
   \t{Params}_\t{DAO}.L &∈ ℕ₆₄ \\
   \t{Params}_\t{DAO}.Q &∈ ℕ₆₄ \\
   \t{Params}_\t{DAO}.EEQ &∈ ℕ₆₄ \\
-  \t{Params}_\t{DAO}.A^\% &∈ ℕ₆₄ × ℕ₆₄ \\
+  \t{Params}_\t{DAO}.A_q &∈ ℕ₆₄ \\
+  \t{Params}_\t{DAO}.A_b &∈ ℕ₆₄ \\
   \t{Params}_\t{DAO}.τ &∈ 𝔽ₚ \\
   \t{Params}_\t{DAO}.\t{NPK} &∈ ℙₚ \\
   \t{Params}_\t{DAO}.\t{pPK} &∈ ℙₚ \\
@@ -52,7 +53,8 @@ $$ \begin{aligned}
 ℕ₆₄2𝔽ₚ(p.L), \\
 ℕ₆₄2𝔽ₚ(p.Q), \\
 ℕ₆₄2𝔽ₚ(p.EEQ), \\
-ℕ₆₄2𝔽ₚ(p.A^\%), \\
+ℕ₆₄2𝔽ₚ(p.A_q), \\
+ℕ₆₄2𝔽ₚ(p.A_b), \\
 p.τ, \\
 \mathcal{X}(p.\t{NPK}), \mathcal{Y}(p.\t{NPK}), \\
 \mathcal{X}(p.\t{pPK}), \mathcal{Y}(p.\t{pPK}), \\
@@ -72,7 +74,7 @@ Let $\t{FuncId}$ be defined as in [Function IDs](../../concepts.md#function-ids)
 
 Let $\t{BLAKE2b}$ be defined as in [BLAKE2b Hash Function](../../crypto-schemes.md#blake2b-hash-function).
 
-Define $\t{AuthCall} = (\t{FuncId}, 𝔹^*)$. Each *authorization call* represents
+Define $\t{AuthCall} = (\t{ContractId}, \t{FuncCode}, 𝔹^*)$. Each *authorization call* represents
 a child call made by the DAO. The *auth data* field is used by the child invoked
 contract to enforce additional invariants.
 ```rust
@@ -80,8 +82,10 @@ contract to enforce additional invariants.
 ```
 
 Define $\t{Commit}_\t{Auth} : \t{AuthCall}^* → 𝔽ₚ$ by
-$$ \t{Commit}_{\t{Auth}^*}(c) = 𝔹⁶⁴2𝔽ₚ(\t{BLAKE2b}₆₄(\t{Encode}(c))) $$
-which commits to a `Vec<DaoAuthCall>`.
+$$ \t{Commit}_{\t{Auth}^*}(c) = \t{𝔹⁶⁴2𝔽ₚ}(\t{BLAKE2b}_{64}(\t{Encode}(c))) $$
+which commits to a `Vec<DaoAuthCall>`. The BLAKE2b hash is personalized
+(`justDAOthings`) and the reduction to $𝔽ₚ$ is the uniform-bytes
+conversion described in [Pallas and Vesta](../../crypto-schemes.md#pallas-and-vesta).
 
 ### Proposal
 
@@ -91,30 +95,42 @@ $$ \begin{aligned}
   \t{Params}_\t{Proposal}.t₀ &∈ ℕ₆₄ \\
   \t{Params}_\t{Proposal}.D &∈ ℕ₆₄ \\
   \t{Params}_\t{Proposal}.φ &∈ 𝔽ₚ \\
-  \t{Params}_\t{Proposal}.\t{DAO} &∈ \t{Bulla}(\t{DAO2𝔽ₚ}(\t{Params}_\t{DAO})) \\
+  \t{Params}_\t{Proposal}.\t{DAO} &∈ 𝔽ₚ \\
 \end{aligned} $$
+where $\t{Params}_\t{Proposal}.\t{DAO}$ is a DAO bulla, $t₀$ is the
+creation blockwindow, $D$ is the duration in blockwindows, and $φ$ is
+arbitrary user data.
 
 ```rust
 {{#include ../../../../../src/contract/dao/src/model.rs:dao-proposal}}
 ```
 
-$$ \t{Bulla}_\t{Proposal} : \t{Params}_\t{Proposal} → 𝔽ₚ $$
-$$ \t{Bulla}_\t{Proposal}(p) = (\t{Commit}_{\t{Auth}^*}(p.C), ℕ₆₄2𝔽ₚ(p.t₀), ℕ₆₄2𝔽ₚ(p.D), p.φ, p.\t{DAO}) $$
+$$ \t{Bulla}_\t{Proposal} : \t{Params}_\t{Proposal} × 𝔽ₚ → 𝔽ₚ $$
+$$ \t{Bulla}_\t{Proposal}(p, b_p) = \t{Bulla}(\t{Commit}_{\t{Auth}^*}(p.C), ℕ₆₄2𝔽ₚ(p.t₀), ℕ₆₄2𝔽ₚ(p.D), p.φ, p.\t{DAO}, b_p) $$
 
-## Vote Nullifiers
+## Proposal Nullifiers
 
-Additionally for proposals, we keep track of nullifiers for each token weighted
-vote for or against a proposal.
+For proposals, we keep track of nullifiers for each token weighted
+vote cast on a proposal.
 
 Let $\t{PoseidonHash}$ be defined as in the section [PoseidonHash Function](../../crypto-schemes.md#poseidonhash-function).
 
 Let $\mathcal{C}$ be the coin params, and $C$ be the coin commitment
-as defined in [Money Contract](TODO).
+as defined in [Money Contract](../money/model.md#coin), with $x$ the
+secret key corresponding to $\mathcal{C}.\t{PK}$.
 
 Let $P$ be a proposal bulla as in the section [Proposal](#proposal).
 
-Define $\t{Nullifier}_\t{Vote} : 𝔽ₚ × 𝔽ₚ × 𝔽ₚ → 𝔽ₚ$ as follows:
-$$ \t{Nullifier}_\t{Vote}(\mathcal{C}.s, C, P) = \t{PoseidonHash}(\mathcal{C}.s, C, P) $$
+First the money nullifier is derived as in the Money contract:
+$$ N = \t{PoseidonHash}(x, C) $$
+
+For proposing, the nullifier additionally binds the proposal bulla,
+defeating input reuse attacks between calls:
+$$ \t{Nullifier}_\t{Propose}(N, P) = \t{PoseidonHash}(N, P) $$
+
+For voting, the nullifier additionally binds the proposal bulla and the
+coin secret key, defeating correlation attacks between calls:
+$$ \t{Nullifier}_\t{Vote}(N, x, P) = \t{PoseidonHash}(N, x, P) $$
 
 ## Blockwindow
 
@@ -130,4 +146,3 @@ which can be used like this
 ```rust
 {{#include ../../../../../src/contract/dao/src/entrypoint/propose.rs:dao-blockwindow-example-usage}}
 ```
-

+ 151 - 86
doc/src/spec/contract/dao/scheme.md

@@ -14,11 +14,11 @@ Let $\t{Params}_\t{DAO}, \t{Bulla}_\t{DAO}, \t{Params}_\t{Proposal}, \t{Bulla}_\
 
 Let $\t{AeadEncNote}$ be defined as in [In-band Secret Distribution](../../crypto-schemes.md#in-band-secret-distribution).
 
-Let $\t{ElGamal.Encrypt}, \t{ElGamalEncNote}ₖ$ be defined as in the section [Verifiable In-Band Secret Distribution](../../crypto-schemes.md#verifiable-in-band-secret-distribution).
+Let $\t{ElGamalEncNote}ₖ, \t{ElGamal}.\t{Encrypt}$ be defined as in the section [Verifiable In-Band Secret Distribution](../../crypto-schemes.md#verifiable-in-band-secret-distribution).
 
 ## Mint
 
-This function creates a DAO bulla $𝒟 $. It's comparatively simple- we commit to
+This function creates a DAO bulla $𝒟$. It's comparatively simple- we commit to
 the DAO params and then add the bulla to the set.
 
 * Wallet builder: `src/contract/dao/src/client/mint.rs`
@@ -39,14 +39,15 @@ $$ \begin{aligned}
 
 ### Contract Statement
 
-**DAO bulla uniqueness** &emsp; whether $$ already exists. If yes then fail.
+**DAO bulla uniqueness** &emsp; whether $𝒟$ already exists. If yes then fail.
 
 Let there be a prover auxiliary witness inputs:
 $$ \begin{aligned}
   L &∈ ℕ₆₄ \\
   Q &∈ ℕ₆₄ \\
   EEQ &∈ ℕ₆₄ \\
-  A^\% &∈ ℕ₆₄ × ℕ₆₄ \\
+  A_q &∈ ℕ₆₄ \\
+  A_b &∈ ℕ₆₄ \\
   τ &∈ 𝔽ₚ \\
   Nx &∈ 𝔽ₚ \\
   px &∈ 𝔽ₚ \\
@@ -56,7 +57,6 @@ $$ \begin{aligned}
   EEx &∈ 𝔽ₚ \\
   b_\t{DAO} &∈ 𝔽ₚ
 \end{aligned} $$
-
 Attach a proof $π$ such that the following relations hold:
 
 **Proof of notes public key ownership** &emsp; $\t{NPK} = \t{DerivePubKey}(Nx)$.
@@ -71,11 +71,14 @@ Attach a proof $π$ such that the following relations hold:
 
 **Proof of early executor public key ownership** &emsp; $\t{EEPK} = \t{DerivePubKey}(EEx)$.
 
-**Proof that early execution quorum is greater than normal quorum** &emsp; $Q <= EEQ1$.
+**Early execution quorum is not less than quorum** &emsp; $Q ≤ EEQ$.
 
-**DAO bulla integrity** &emsp; $ℬ  = \t{Bulla}_\t{DAO}((L, Q, EEQ, A^\%, τ,
+**DAO bulla integrity** &emsp; $𝒟 = \t{Bulla}_\t{DAO}((L, Q, EEQ, A_q, A_b, τ,
 \t{NPK}, \t{pPK}, \t{PPK}, \t{VPK}, \t{EPK}, \t{EEPK}), b_\t{DAO})$
 
+The state update additionally appends $𝒟$ to the DAO bulla Merkle tree,
+which is used later for existence proofs in the Propose phase.
+
 ### Signatures
 
 There should be a single signature attached, which uses
@@ -88,14 +91,16 @@ $R_\t{DAO}$ which contains the DAO bulla created in the Mint phase.
 
 Several inputs are attached containing proof of ownership for the governance
 token. This is to satisfy the proposer limit value set in the DAO.
-We construct the nullifier $\cN$ which can leak anonymity when those same
-coins are spent. To workaround this, wallet implementers can attach an
-additional `Money::transfer()` call to the transaction.
+We construct the proposal-specific nullifier $\cN$ which can leak anonymity
+when those same coins are spent. To workaround this, wallet implementers
+can attach an additional `Money::transfer()` call to the transaction.
 
-The nullifier $\cN$ proves the coin isn't already spent in the set determined
-by $R_\t{coin}$. Each value commit $V$ exported by the input is summed and
-used in the main proof to determine the total value attached in the inputs
-crosses the proposer limit threshold.
+For every input, an SMT non-membership proof shows the corresponding coin
+was unspent in the Money state snapshot determined by the coin merkle root
+$R_\t{coin}$ and the nullifier set SMT root $R_\t{null}$. Each value commit
+$V$ exported by the inputs is summed and used in the main proof to determine
+that the total value attached in the inputs crosses the proposer limit
+threshold.
 
 This is merely a proof of ownership of holding a certain amount of value.
 Coins are not locked and continue to be spendable.
@@ -116,6 +121,8 @@ A proposal contains a list of auth calls as specified in [Auth Calls](model.md#a
 Define the DAO propose function params
 $$ \begin{aligned}
   R_\t{DAO} &∈ 𝔽ₚ \\
+  R_\t{coin} &∈ 𝔽ₚ \\
+  R_\t{null} &∈ 𝔽ₚ \\
   T &∈ 𝔽ₚ \\
   𝒫 &∈ \t{im}(\t{Bulla}_\t{Proposal}) \\
   \t{note} &∈ \t{AeadEncNote} \\
@@ -126,10 +133,12 @@ Define the DAO propose-input function params
 $$ \begin{aligned}
   \t{ProposeInput}.\cN &∈ 𝔽ₚ \\
   \t{ProposeInput}.V &∈ ℙₚ \\
-  \t{ProposeInput}.R_\t{coin} &∈ 𝔽ₚ \\
   \t{ProposeInput}.\t{PK}_σ &∈ ℙₚ
 \end{aligned} $$
 
+The coin merkle root $R_\t{coin}$ and nullifier SMT root $R_\t{null}$ are
+attached once per call and bound to every input proof.
+
 ```rust
 {{#include ../../../../../src/contract/dao/src/model.rs:dao-propose-params}}
 ```
@@ -147,7 +156,15 @@ Let $\t{Attrs}_\t{Coin}$ be defined as in [Coin](../money/model.md#coin).
 **Valid DAO bulla merkle root** &emsp; check that $R_\t{DAO}$ is a previously
 seen merkle root in the DAO contract merkle roots DB.
 
-**Proposal bulla uniqueness** &emsp; whether $𝒫 $ already exists. If yes then fail.
+**Valid snapshot** &emsp; check that $R_\t{coin}$ is a previously seen
+merkle root in the money contract merkle roots DB, that $R_\t{null}$ is a
+previously seen SMT root in the money contract nullifier roots DB, and
+that the two snapshots correspond to the same state (the nullifier SMT
+root snapshot must contain the coin tree root snapshot). The snapshot
+must be recent enough: not older than `PROPOSAL_SNAPSHOT_CUTOFF_LIMIT`
+blocks (100).
+
+**Proposal bulla uniqueness** &emsp; whether $𝒫$ already exists. If yes then fail.
 
 Let there be prover auxiliary witness inputs:
 $$ \begin{aligned}
@@ -158,21 +175,23 @@ $$ \begin{aligned}
   b_p &∈ 𝔽ₚ \\
   d &∈ \t{Params}_\t{DAO} \\
   b_d &∈ 𝔽ₚ \\
+  px &∈ 𝔽ₚ \\
   (ψ, Π) &∈ \t{MerklePos} × \t{MerklePath} \\
 \end{aligned} $$
-Attach a proof $π_𝒫 $ such that the following relations hold:
+Attach a proof $π_𝒫$ such that the following relations hold:
 
-**Governance token commit** &emsp; export the DAO token ID as an encrypted pedersen
-commit $T = \t{PedersenCommit}(d.τ, b_τ)$ where $T = ∑_{i ∈ 𝐢} Tᵢ$.
+**Governance token commit** &emsp; export the DAO token ID as an encrypted
+commit $T = \t{PoseidonHash}(d.τ, b_τ)$, matching the token commit of
+every input.
 
 **Proof of proposer public key ownership** &emsp; $\t{pPK} = \t{DerivePubKey}(px)$.
 
-**DAO bulla integrity** &emsp; $𝒟  = \t{Bulla}_\t{DAO}(d, b_d)$
+**DAO bulla integrity** &emsp; $𝒟 = \t{Bulla}_\t{DAO}(d, b_d)$
 
-**DAO existence** &emsp; $R_\t{DAO} = \t{MerkleRoot}(ψ, Π, 𝒟 )$
+**DAO existence** &emsp; $R_\t{DAO} = \t{MerkleRoot}(ψ, Π, 𝒟)$
 
 **Proposal bulla integrity** &emsp; $𝒫 = \t{Bulla}_\t{Proposal}(p, b_p)$
-where $p.t₀ = t₀$.
+where $p.t₀ = t₀$ is enforced as a public input.
 
 **Proposer limit threshold met** &emsp; check the proposer has supplied enough
 inputs that the required funds for the proposer limit set in the DAO is met.
@@ -182,13 +201,9 @@ Let the total funds $v = ∑_{i ∈ 𝐢} i.v$, then check $d.L ≤ v$.
 $V = ∑_{i ∈ 𝐢} i.V$. We use this to check that $v = ∑_{i ∈ 𝐢} i.v$ as
 claimed in the *proposer limit threshold met* check.
 
-For each input $i ∈ 𝐢$, perform the following checks:
-
-&emsp; **Unused nullifier** &emsp; check that $\cN$ does not exist in the
-money contract nullifiers DB.
+**Input uniqueness** &emsp; the nullifiers $\cN$ must be unique within the call.
 
-&emsp; **Valid input coins merkle root** &emsp; check that $i.R_\t{coin}$ is a
-previously seen merkle root in the money contract merkle roots DB.
+For each input $i ∈ 𝐢$, perform the following checks:
 
 &emsp; Let there be a prover auxiliary witness inputs:
 $$ \begin{aligned}
@@ -197,20 +212,30 @@ $$ \begin{aligned}
   bᵥ &∈ 𝔽ᵥ \\
   b_τ &∈ 𝔽ₚ \\
   (ψᵢ, Πᵢ) &∈ \t{MerklePos} × \t{MerklePath} \\
+  (ψ^N, Π^N) &∈ \t{MerklePos} × \t{MerklePath} \\
   x_σ &∈ 𝔽ₚ \\
 \end{aligned} $$
 &emsp; Attach a proof $π_i$ such that the following relations hold:
 
-&emsp; **Nullifier integrity** &emsp; $\cN = \t{PoseidonHash}(x_c, C)$
+&emsp; **Nullifier integrity** &emsp; let $C = \t{Coin}(c)$ and
+$N = \t{PoseidonHash}(x_c, C)$, then $\cN = \t{PoseidonHash}(N, 𝒫)$
+
+&emsp; **Unspent at snapshot** &emsp;
+$R_\t{null} = \t{MerkleRoot}(ψ^N, Π^N, 0)$, i.e. an SMT non-membership
+proof of $N$ in the nullifier set snapshot.
 
 &emsp; **Coin value commit** &emsp; $i.V = \t{PedersenCommit}(c.v, bᵥ)$.
 
 &emsp; **Token commit** &emsp; $T = \t{PoseidonHash}(c.τ, b_τ)$.
 
-&emsp; **Valid coin** &emsp; Check $c.P = \t{DerivePubKey}(x_c)$. Let $C = \t{Coin}(c)$. Check $i.R_\t{coin} = \t{MerkleRoot}(ψᵢ, Πᵢ, C)$.
+&emsp; **Valid coin** &emsp; Check $c.P = \t{DerivePubKey}(x_c)$. Check $R_\t{coin} = \t{MerkleRoot}(ψᵢ, Πᵢ, C)$.
 
 &emsp; **Proof of signature public key ownership** &emsp; $i.\t{PK}_σ = \t{DerivePubKey}(x_σ)$.
 
+**Snapshot creation** &emsp; once the proposal is accepted, the latest
+Money coin merkle root and nullifier SMT root are snapshotted alongside
+the proposal. Only coins in this snapshot are votable with (see [Vote](#vote)).
+
 ### Signatures
 
 For each $i ∈ 𝐢$, attach a signature corresponding to the
@@ -224,13 +249,13 @@ of a certain value of governance tokens. This is how we achieve token weighted
 voting. The result of the vote is communicated to DAO members that can view votes
 through the encrypted note $\t{note}$.
 
-Each nullifier $𝒩 $ is stored uniquely per proposal. Additionally as before,
+Each nullifier $𝒩$ is stored uniquely per proposal. Additionally as before,
 there is a leakage here connecting the coins when spent. However prodigious
 usage of `Money::transfer()` to wash the coins after calling `DAO::vote()`
 should mitigate against this attack. In the future this can be fixed using
-set nonmembership primitives.
+set non-membership primitives.
 
-Another leakage is that the proposal bulla $𝒫 $ is public. To ensure every vote
+Another leakage is that the proposal bulla $𝒫$ is public. To ensure every vote
 is discoverable by verifiers (who cannot decrypt values) and protect against
 'nothing up my sleeve', we link them all together. This is so the final tally
 used for executing proposals is accurate.
@@ -248,10 +273,10 @@ and the yes votes by $V_\t{yes}$.
 
 Define the DAO vote function params
 $$ \begin{aligned}
-  τ &∈ 𝔽ₚ \\
+  T &∈ 𝔽ₚ \\
   𝒫 &∈ \t{im}(\t{Bulla}_\t{Proposal}) \\
   V_\t{yes} &∈ ℙₚ \\
-  \t{enc\_vote} &∈ \t{ElGamalEncNote}₄ \\
+  \t{note} &∈ \t{ElGamalEncNote}₄ \\
   𝐢 &∈ \t{VoteInput}^*
 \end{aligned} $$
 
@@ -259,7 +284,6 @@ Define the DAO vote-input function params
 $$ \begin{aligned}
   \t{VoteInput}.𝒩 &∈ 𝔽ₚ \\
   \t{VoteInput}.V &∈ ℙₚ \\
-  \t{VoteInput}.R_\t{coin} &∈ 𝔽ₚ \\
   \t{VoteInput}.\t{PK}_σ &∈ ℙₚ
 \end{aligned} $$
 
@@ -284,7 +308,10 @@ sum of DAO votes.
 
 Let $t₀ = \t{BlockWindow} ∈ 𝔽ₚ$ be the current blockwindow as defined in [Blockwindow](model.md#blockwindow).
 
-**Proposal bulla exists** &emsp; check $𝒫 $ exists in the DAO contract proposal
+Let $R_\t{coin}, R_\t{null}$ be the Money state snapshot attached to the
+proposal when it was created.
+
+**Proposal bulla exists** &emsp; check $𝒫$ exists in the DAO contract proposal
 bullas DB.
 
 Let there be prover auxiliary witness inputs:
@@ -303,59 +330,71 @@ $$ \begin{aligned}
 \end{aligned} $$
 Attach a proof $π_\mathcal{V}$ such that the following relations hold:
 
-**Governance token commit** &emsp; export the DAO token ID as an encrypted pedersen
-commit $T = \t{PedersenCommit}(d.τ, b_τ)$ where $T = ∑_{i ∈ 𝐢} Tᵢ$.
+**Governance token commit** &emsp; export the DAO token ID as an encrypted
+commit $T = \t{PoseidonHash}(d.τ, b_τ)$, matching the token commit of
+every input.
 
 **DAO bulla integrity** &emsp; $𝒟 = \t{Bulla}_\t{DAO}(d, b_d)$
 
 **Proposal bulla integrity** &emsp; $𝒫 = \t{Bulla}_\t{Proposal}(p, b_p)$
 
-**Yes vote commit** &emsp; $V_\t{yes} = \t{PedersenCommit}(ov, \t{Lift}_q(b_y))$
+**Yes vote commit** &emsp; $V_\t{yes} = (o \cdot v)G_V + b_y H_B$
 
-**Total vote value commit** &emsp; $V_\t{all} = \t{PedersenCommit}(v, \t{Lift}_q(bᵥ))$ where
-$V_\t{all} = ∑_{i ∈ 𝐢} i.V$ should also hold.
+**Total vote value commit** &emsp; $V_\t{all} = vG_V + bᵥH_B$ where
+$V_\t{all} = ∑_{i ∈ 𝐢} i.V$ should also hold. Here $G_V$ is the
+`VALUE_COMMIT_VALUE` generator, and the blinds $b_y, bᵥ$ are base field
+elements used with the `VALUE_COMMIT_RANDOM_BASE` generator $H_B$, so
+they can be verifiably encrypted.
 
 **Vote option boolean** &emsp; enforce $o ∈ \{ 0, 1 \}$.
 
 **Proposal not expired** &emsp; let $t_\t{end} = ℕ₆₄2𝔽ₚ(p.t₀) + ℕ₆₄2𝔽ₚ(p.D)$,
-and then check $t_\t{now} < t_\t{end}$.
+and then check $t_\t{now} < t_\t{end}$, where $t_\t{now}$ is enforced to be
+the current blockwindow via a public input.
 
 **Verifiable encryption of vote commit secrets** &emsp;
 let $𝐧 = (o, b_y, v, bᵥ)$, and verify
-$\t{enc\_vote} = \t{ElGamal}.\t{Encrypt}(𝐧, \t{esk}, d.\t{VPK})$.
+$\t{note} = \t{ElGamal}.\t{Encrypt}(𝐧, \t{esk}, d.\t{VPK})$.
 
 For each input $i ∈ 𝐢$, perform the following checks:
 
-&emsp; **Valid input merkle root** &emsp; check that $i.R_\t{coin}$ is the
-previously seen merkle root in the proposal snapshot merkle root.
-
-&emsp; **Unused nullifier (money)** &emsp; check that $\cN$ does not exist in the
-money contract nullifiers DB.
+&emsp; **Unused nullifier (proposal)** &emsp; check that $𝒩$ does not exist in the
+DAO contract nullifiers DB for this specific proposal (nullifiers are keyed
+by $(𝒫, 𝒩)$), and is unique within the call.
 
-&emsp; **Unused nullifier (proposal)** &emsp; check that $\cN$ does not exist in the
-DAO contract nullifiers DB for this specific proposal.
-
-Let there be prover auxiliary witness inputs:
+&emsp; Let there be a prover auxiliary witness inputs:
 $$ \begin{aligned}
   x_c &∈ 𝔽ₚ \\
   c &∈ \t{Attrs}_\t{Coin} \\
   bᵥ &∈ 𝔽ᵥ \\
   b_τ &∈ 𝔽ₚ \\
   (ψᵢ, Πᵢ) &∈ \t{MerklePos} × \t{MerklePath} \\
+  (ψ^N, Π^N) &∈ \t{MerklePos} × \t{MerklePath} \\
   x_σ &∈ 𝔽ₚ \\
 \end{aligned} $$
 Attach a proof $πᵢ$ such that the following relations hold:
 
-&emsp; **Nullifier integrity** &emsp; $\cN = \t{PoseidonHash}(x_c, C)$
+&emsp; **Nullifier integrity** &emsp; let $C = \t{Coin}(c)$ and
+$N = \t{PoseidonHash}(x_c, C)$, then
+$𝒩 = \t{PoseidonHash}(N, x_c, 𝒫)$
+
+&emsp; **Unspent at snapshot** &emsp;
+$R_\t{null} = \t{MerkleRoot}(ψ^N, Π^N, 0)$, i.e. an SMT non-membership
+proof of $N$ in the nullifier set snapshot, so only participants from
+before the proposal was posted can vote.
 
 &emsp; **Coin value commit** &emsp; $i.V = \t{PedersenCommit}(c.v, bᵥ)$.
 
 &emsp; **Token commit** &emsp; $T = \t{PoseidonHash}(c.τ, b_τ)$.
 
-&emsp; **Valid coin** &emsp; Check $c.P = \t{DerivePubKey}(x_c)$. Let $C = \t{Coin}(c)$. Check $i.R_\t{coin} = \t{MerkleRoot}(ψᵢ, Πᵢ, C)$.
+&emsp; **Valid coin** &emsp; Check $c.P = \t{DerivePubKey}(x_c)$. Check $R_\t{coin} = \t{MerkleRoot}(ψᵢ, Πᵢ, C)$, i.e. the coin existed in the proposal's snapshot of the coin tree.
 
 &emsp; **Proof of signature public key ownership** &emsp; $i.\t{PK}_σ = \t{DerivePubKey}(x_σ)$.
 
+**Vote aggregation** &emsp; the state update adds $V_\t{yes}$ and
+$∑ i.V$ to the proposal's aggregated vote commits
+($\t{DaoBlindAggregateVote}$), and records the used vote nullifiers.
+
 ### Signatures
 
 For each $i ∈ 𝐢$, attach a signature corresponding to the
@@ -374,6 +413,10 @@ calls set inside the proposal. One of these will usually be an auth module
 function. Currently the DAO provides a single preset for executing
 `Money::transfer()` calls so DAOs can manage anonymous treasuries.
 
+The `early_exec` flag selects which proof statement is verified:
+`Exec` for normal execution after expiry, or `EarlyExec` for strongly
+supported proposals (see [EarlyExec](#earlyexec)).
+
 * Wallet builder: `src/contract/dao/src/client/exec.rs`
 * WASM VM code: `src/contract/dao/src/entrypoint/exec.rs`
 * ZK proofs:
@@ -387,9 +430,11 @@ Let $\t{AuthCall}, \t{Commit}_{\t{Auth}^*}$ be defined as in the section [Auth C
 Define the DAO exec function params
 $$ \begin{aligned}
   𝒫 &∈ \t{im}(\t{Bulla}_\t{Proposal}) \\
-  𝒜  &∈ \t{AuthCall}^* \\
+  𝒜 &∈ \t{AuthCall}^* \\
   V_\t{yes} &∈ ℙₚ \\
   V_\t{all} &∈ ℙₚ \\
+  \t{early} &∈ ℤ₂ \\
+  \t{PK}_σ &∈ ℙₚ
 \end{aligned} $$
 
 ```rust
@@ -407,8 +452,9 @@ transaction matches what is specified in the proposal. Then in the second phase,
 we verify the correct voting rules.
 
 **Auth call spec match** &emsp; denote the child calls of Exec by $C$.
-If $\#C ≠ \#𝒜 $ then exit.
-Otherwise, for each $c ∈ C$ and $a ∈ 𝒜 $, check the function ID of $c$ is $a$.
+If $\#C ≠ \#𝒜$ then exit.
+Otherwise, for each $c ∈ C$ and $a ∈ 𝒜$, check the contract ID and
+function code of $c$ match $a$.
 
 **Aggregate votes lookup** &emsp; using the proposal bulla, fetch the
 aggregated votes from the DB and verify $V_\t{yes}$ and $V_\t{all}$ are set correctly.
@@ -423,6 +469,8 @@ $$ \begin{aligned}
   v_a &∈ 𝔽ₚ \\
   b_y &∈ 𝔽ᵥ \\
   b_a &∈ 𝔽ᵥ \\
+  t_\t{now} &∈ 𝔽ₚ \\
+  x_σ &∈ 𝔽ₚ
 \end{aligned} $$
 Attach a proof $π$ such that the following relations hold:
 
@@ -431,10 +479,12 @@ Attach a proof $π$ such that the following relations hold:
 **DAO bulla integrity** &emsp; $𝒟 = \t{Bulla}_\t{DAO}(d, b_d)$
 
 **Proposal bulla integrity** &emsp; $𝒫 = \t{Bulla}_\t{Proposal}(p, b_p)$
-where $p.𝒜  = 𝒜 $.
+where $\t{Commit}_{\t{Auth}^*}(p.C) = \t{Commit}_{\t{Auth}^*}(𝒜)$ is
+enforced as a public input.
 
 **Proposal has expired** &emsp; let $t_\t{end} = ℕ₆₄2𝔽ₚ(p.t₀) + ℕ₆₄2𝔽ₚ(p.D)$,
-and then check $t_\t{end} <= t_\t{now}$.
+and then check $t_\t{end} ≤ t_\t{now}$, where $t_\t{now}$ is enforced to
+be the current blockwindow via a public input.
 
 **Yes vote commit** &emsp; $V_\t{yes} = \t{PedersenCommit}(v_y, b_y)$
 
@@ -443,8 +493,11 @@ and then check $t_\t{end} <= t_\t{now}$.
 **All votes pass quorum** &emsp; $Q ≤ v_a$
 
 **Approval ratio satisfied** &emsp; we wish to check that
-$\frac{A^\%_q}{A^\%_b} ≤ \frac{v_y}{v_a}$. Instead we perform the
-equivalent check that $v_a A^\%_q ≤ v_y A^\%_b$.
+$\frac{A_q}{A_b} ≤ \frac{v_y}{v_a}$. Instead we perform the
+equivalent check that $v_a A_q ≤ v_y A_b$.
+
+**Proposal removal** &emsp; the state update removes the proposal from
+the DB so it cannot be executed twice.
 
 ### EarlyExec
 
@@ -461,6 +514,8 @@ $$ \begin{aligned}
   v_a &∈ 𝔽ₚ \\
   b_y &∈ 𝔽ᵥ \\
   b_a &∈ 𝔽ᵥ \\
+  t_\t{now} &∈ 𝔽ₚ \\
+  x_σ &∈ 𝔽ₚ
 \end{aligned} $$
 Attach a proof $π$ such that the following relations hold:
 
@@ -471,10 +526,12 @@ Attach a proof $π$ such that the following relations hold:
 **DAO bulla integrity** &emsp; $𝒟 = \t{Bulla}_\t{DAO}(d, b_d)$
 
 **Proposal bulla integrity** &emsp; $𝒫 = \t{Bulla}_\t{Proposal}(p, b_p)$
-where $p.𝒜  = 𝒜 $.
+where $\t{Commit}_{\t{Auth}^*}(p.C) = \t{Commit}_{\t{Auth}^*}(𝒜)$ is
+enforced as a public input.
 
 **Proposal has not expired** &emsp; let $t_\t{end} = ℕ₆₄2𝔽ₚ(p.t₀) + ℕ₆₄2𝔽ₚ(p.D)$,
-and then check $t_\t{now} < t_\t{end}$.
+and then check $t_\t{now} < t_\t{end}$, where $t_\t{now}$ is enforced to
+be the current blockwindow via a public input.
 
 **Yes vote commit** &emsp; $V_\t{yes} = \t{PedersenCommit}(v_y, b_y)$
 
@@ -483,12 +540,14 @@ and then check $t_\t{now} < t_\t{end}$.
 **All votes pass early execution quorum** &emsp; $EEQ ≤ v_a$
 
 **Approval ratio satisfied** &emsp; we wish to check that
-$\frac{A^\%_q}{A^\%_b} ≤ \frac{v_y}{v_a}$. Instead we perform the
-equivalent check that $v_a A^\%_q ≤ v_y A^\%_b$.
+$\frac{A_q}{A_b} ≤ \frac{v_y}{v_a}$. Instead we perform the
+equivalent check that $v_a A_q ≤ v_y A_b$.
 
 ### Signatures
 
-No signatures are attached.
+A single signature is attached, using $\t{PK}_σ$ as the signature public
+key. The signature binds the `DAO::exec()` call to the transaction so it
+cannot be combined with other calls.
 
 ## AuthMoneyTransfer
 
@@ -497,8 +556,8 @@ It checks the next sibling call is `Money::transfer()` and accordingly
 verifies the first $n - 1$ output coins match the data set in this
 call's [auth data](model.md#auth-calls).
 
-Additionally we provide a note with the coin params that are verifiably
-encrypted to mitigate the attack where Exec is called, but the supplied
+Additionally we provide verifiably encrypted notes for the coins, to
+mitigate the attack where Exec is called, but the supplied
 `Money::transfer()` call contains an invalid note which cannot be
 decrypted by the receiver. In this case, the money would still leave the
 DAO treasury but be unspendable.
@@ -525,18 +584,20 @@ This provides verifiable note encryption for all output coins in the sibling `Mo
 
 ### Contract Statement
 
-Denote the DAO contract ID by $\t{CID}_\t{DAO} ∈ 𝔽ₚ$.
+Denote the DAO function ID of `Dao::Exec` by $\t{FID}_\t{Exec} ∈ 𝔽ₚ$.
 
-**Sibling call is `Money::transfer()`** &emsp; load the sibling call and check
-the contract ID and function code match `Money::transfer()`.
+**Sibling call is `Money::transfer()`** &emsp; load the immediate next
+sibling call and check the contract ID and function code match
+`Money::transfer()`.
 
 **Money originates from the same DAO** &emsp; check all the input's `user_data`
 for the sibling `Money::transfer()` encode the same DAO. We do this by using the
 same blind for all `user_data`. Denote this value by $\t{UD}_\t{enc}$.
 
 **Output coins match proposal** &emsp; check there are $n + 1$ output coins,
-with the first $n$ coins exactly matching those set in the auth data in
-the parent `DAO::exec()` call. Denote these proposal auth calls by $𝒜 $.
+with the first $n$ coins exactly matching those set in this call's auth
+data in the parent `DAO::exec()` call. The auth data is decoded as a
+list of $n$ coins.
 
 Let there be a prover auxiliary witness inputs:
 $$ \begin{aligned}
@@ -557,37 +618,41 @@ following relations hold:
 **DAO bulla integrity** &emsp; $𝒟 = \t{Bulla}_\t{DAO}(d, b_d)$
 
 **Proposal bulla integrity** &emsp; $𝒫 = \t{Bulla}_\t{Proposal}(p, b_p)$
-where $𝒫 $ matches the value in `DAO::exec()`, and $p.𝒜  = 𝒜 $.
+where $𝒫$ matches the value in `DAO::exec()`, and
+$\t{Commit}_{\t{Auth}^*}(p.C)$ is enforced as a public input.
 
 **Input user data commits to DAO bulla** &emsp; $\t{UD}_\t{enc} =
-\t{PoseidonHash}(𝒟 , b_\t{UD})$
+\t{PoseidonHash}(𝒟, b_\t{UD})$
 
 **DAO change coin integrity** &emsp; denote the last coin in the
 `Money::transfer()` outputs by $C_\t{DAO}$. Then check
-$$ C_\t{DAO} = \t{Coin}(d.\t{PK}, v_\t{DAO}, τ_\t{DAO},
-                        \t{CID}_\t{DAO}, 𝒟 , b_\t{DAO}) $$
+$$ C_\t{DAO} = \t{Coin}(d.\t{NPK}, v_\t{DAO}, τ_\t{DAO},
+                        \t{FID}_\t{Exec}, 𝒟, b_\t{DAO}) $$
+i.e. the change is sent back to the DAO's notes public key, locked to
+`Dao::Exec` via the spend hook, with the DAO bulla as user data. The
+spend hook $\t{FID}_\t{Exec}$ is additionally enforced as a public input.
 
 **Verifiable DAO change coin note encryption** &emsp;
 let $𝐧 = (v_\t{DAO}, τ_\t{DAO}, b_\t{DAO})$, and verify
-$𝒟_\t{enc} = \t{ElGamal}.\t{Encrypt}(𝐧, \t{esk}, d.\t{PK})$.
+$𝒟_\t{enc} = \t{ElGamal}.\t{Encrypt}(𝐧, \t{esk}, d.\t{NPK})$.
 
 Then we do the same for each output coin of `Money::transfer()`.
-For $k ∈ [n]$, let $a = (𝒞_\t{enc})ₖ$ and $C$ be the $k$th output coin from
+For $k ∈ [n+1]$, let $a = (𝒞_\t{enc})ₖ$ and $C$ be the $k$th output coin from
 `Money::transfer()`.
-Let there be prover auxiliary witness inputs:
+Let there be a prover auxiliary witness inputs:
 $$ \begin{aligned}
   c &∈ \t{Attrs}_\t{Coin} \\
-  e &∈ 𝔽ₚ
 \end{aligned} $$
-Attach a proof $πₖ$ such that the following relations hold:
+Attach a proof $πₖ$ (reusing the same $\t{esk}$) such that the following
+relations hold:
 
 &emsp; **Coin integrity** &emsp; $C = \t{Coin}(c)$
 
 &emsp; **Verifiable output coin note encryption** &emsp;
-let $𝐧 = (c.v, c.τ, c.\t{SH}, c.\t{UD}, c.n)$, and verify
-$a = \t{ElGamal}.\t{Encrypt}(𝐧, \t{esk}, d.\t{PK})$.
+let $𝐧 = (c.v, c.τ, c.\t{SH}, c.\t{UD}, c.b)$, and verify
+$a = \t{ElGamal}.\t{Encrypt}(𝐧, \t{esk}, c.\t{PK})$, i.e. the coin
+secrets are encrypted to the coin's own recipient public key.
 
 ### Signatures
 
 No signatures are attached.
-

+ 37 - 3
doc/src/spec/contract/money/model.md

@@ -38,6 +38,36 @@ $$ \begin{aligned}
 $$ \t{Coin} : \t{Attrs}_\t{Coin} → 𝔽ₚ $$
 $$ \t{Coin}(p) = \t{Bulla}(\mathcal{X}(p.\t{PK}), \mathcal{Y}(p.\t{PK}), ℕ₆₄2𝔽ₚ(p.v), p.τ, p.\t{SH}, p.\t{UD}, p.b) $$
 
+Spending a coin $C = \t{Coin}(p)$ reveals its nullifier, derived from the
+secret key $x$ corresponding to $p.\t{PK}$:
+$$ \t{Nullifier} : 𝔽ₚ × 𝔽ₚ → 𝔽ₚ $$
+$$ \t{Nullifier}(x, C) = \t{PoseidonHash}(x, C) $$
+
+## Token
+
+Tokens are identified by a token ID $τ$, which is itself a commitment to
+the token's attributes. The parent authority $\t{SH}$ is the function ID
+allowed to mint the token (see `Money::AuthTokenMintV1`), the user data
+$\t{UD}$ can bind additional parameters, and the blind $b$ guarantees
+uniqueness of the token ID.
+
+Define the token attributes
+$$ \begin{aligned}
+  \t{Attrs}_\t{Token}.\t{SH} &∈ 𝔽ₚ \\
+  \t{Attrs}_\t{Token}.\t{UD} &∈ 𝔽ₚ \\
+  \t{Attrs}_\t{Token}.b &∈ 𝔽ₚ \\
+\end{aligned} $$
+
+```rust
+{{#include ../../../../../src/contract/money/src/model/mod.rs:token-attributes}}
+```
+
+$$ \t{TokenId} : \t{Attrs}_\t{Token} → 𝔽ₚ $$
+$$ \t{TokenId}(p) = \t{Bulla}(p.\t{SH}, p.\t{UD}, p.b) $$
+
+The native network token is the constant `DARK_TOKEN_ID` which does not
+correspond to any real commitment (see `src/contract/money/src/model/token_id.rs`).
+
 ## Inputs and Outputs
 
 ### Clear Input
@@ -45,7 +75,7 @@ $$ \t{Coin}(p) = \t{Bulla}(\mathcal{X}(p.\t{PK}), \mathcal{Y}(p.\t{PK}), ℕ₆
 Define the clear input attributes
 $$ \begin{aligned}
   \t{MoneyClearInput}.v &∈ ℕ₆₄ \\
-  \t{MoneyClearInput}.T &∈ ₚ \\
+  \t{MoneyClearInput}.T &∈ 𝔽ₚ \\
   \t{MoneyClearInput}.v_\t{blind} &∈ 𝔽_q \\
   \t{MoneyClearInput}.t_\t{blind} &∈ 𝔽ₚ \\
   \t{MoneyClearInput}.Z &∈ ℙₚ \\
@@ -63,11 +93,15 @@ $$ \begin{aligned}
   \t{MoneyInput}.T &∈ 𝔽ₚ \\
   \t{MoneyInput}.N &∈ 𝔽ₚ \\
   \t{MoneyInput}.R &∈ 𝔽ₚ \\
-  \t{MoneyInput}.h &∈ 𝔽ₚ \\
   \t{MoneyInput}.U &∈ 𝔽ₚ \\
   \t{MoneyInput}.Z &∈ ℙₚ \\
+  \t{MoneyInput}.\t{tx\_local} &∈ ℤ₂ \\
 \end{aligned} $$
 
+The spend hook $h$ verified by the `Burn_V1` proof is not part of the
+input itself: it is computed by the contract from the parent call's
+function reference during execution (see [Transfer](scheme.md#transfer)).
+
 ```rust
 {{#include ../../../../../src/contract/money/src/model/mod.rs:money-input}}
 ```
@@ -82,9 +116,9 @@ $$ \begin{aligned}
   \t{MoneyOutput}.T &∈ 𝔽ₚ \\
   \t{MoneyOutput}.C &∈ 𝔽ₚ \\
   \t{MoneyOutput}.\t{note} &∈ \t{AeadEncNote} \\
+  \t{MoneyOutput}.\t{tx\_local} &∈ ℤ₂ \\
 \end{aligned} $$
 
 ```rust
 {{#include ../../../../../src/contract/money/src/model/mod.rs:money-output}}
 ```
-

+ 2 - 1
doc/src/spec/contract/money/money.md

@@ -3,7 +3,8 @@
 ## Abstract
 
 The _Money_ contract implements network fees, token transfers,
-atomic swaps, and token minting.
+atomic swaps, token minting and freezing, value burning (destroying),
+the genesis allocation, and consensus rewards minting.
 
 The functions provided by this smart contract are:
 ```rust

+ 328 - 50
doc/src/spec/contract/money/scheme.md

@@ -2,10 +2,18 @@
 
 Let $\t{PoseidonHash}$ be defined as in the section [PoseidonHash Function](../../crypto-schemes.md#poseidonhash-function).
 
+Let $G, H$ be the Pedersen commitment generators `VALUE_COMMIT_VALUE`
+(`src/sdk/src/crypto/constants/fixed_bases/value_commit_v.rs`) and
+`VALUE_COMMIT_RANDOM` (`src/sdk/src/crypto/constants/fixed_bases/value_commit_r.rs`),
+and let $K$ be the `NULLIFIER_K` generator
+(`src/sdk/src/crypto/constants/fixed_bases/nullifier_k.rs`).
+
 ## Transfer
 
 This function transfers value by burning a set of coins $𝐂$, and minting a
-set of coins, such that the value spent and created are equal.
+set of coins, such that the value spent and created are equal. Value
+conservation is enforced per token: for every token commitment, the sum of
+the input Pedersen value commitments must equal the sum of the output ones.
 
 * Wallet:
   * Builder: `src/contract/money/src/client/transfer_v1/builder.rs`
@@ -18,12 +26,10 @@ set of coins, such that the value spent and created are equal.
 
 ### Function Params
 
-Let $\t{MoneyClearInput}, \t{MoneyInput}, \t{MoneyOutput}$
-be defined as in [Inputs and Outputs](model.md#inputs-and-outputs).
+Let $\t{MoneyInput}, \t{MoneyOutput}$ be defined as in [Inputs and Outputs](model.md#inputs-and-outputs).
 
 Define the Money transfer function params
 $$ \begin{aligned}
-  𝐣 &∈ \t{MoneyClearInput}^* \\
   𝐢 &∈ \t{MoneyInput}^* \\
   𝐨 &∈ \t{MoneyOutput}^*
 \end{aligned} $$
@@ -36,6 +42,24 @@ $$ \begin{aligned}
 
 Let $π_\t{mint}, π_\t{burn}$ be defined as in [ZK Proofs](#zk-proofs).
 
+Each input $i ∈ 𝐢$ carries a `Burn_V1` proof, and each output $o ∈ 𝐨$
+carries a `Mint_V1` proof. Additionally the contract verifies:
+
+* **Valid coin merkle root** &emsp; $i.R$ is a previously seen merkle root
+  in the money contract merkle roots DB (or in the transaction-local one
+  if $i.\t{tx\_local}$ is set).
+* **Unused nullifier** &emsp; $i.N$ does not exist in the money contract
+  nullifiers set, and is unique within the call.
+* **Unique coins** &emsp; each output coin $o.C$ has not been seen before,
+  neither on chain, transaction-locally, nor within the call.
+* **Per token value conservation** &emsp; grouping inputs and outputs by
+  their token commitment $T$, for every group:
+  $$ \sum_{i ∈ 𝐢, i.T = T} i.V = \sum_{o ∈ 𝐨, o.T = T} o.V $$
+* **Spend hook enforcement** &emsp; the spend hook $h$ verified in the
+  `Burn_V1` proofs is computed as the function ID of the parent call, or
+  `FuncId::none()` if the transfer is a root call. This way coins carrying
+  a spend hook can only be spent by the contract function they point to.
+
 ### ZK Proofs
 
 #### `Mint_V1`
@@ -49,14 +73,14 @@ Denote this proof by $π_\t{mint}$.
 
 **Circuit witnesses:**
 
-* $P$ - Public key of the recipient which goes into the coin commitment (pallas curve point)
-* $v$ - Value of the coin commitment (unsigned 64-bit integer)
-* $t$ - Token ID of the coin commitment (pallas base field element)
-* $s$ - Unique serial number of the coin commitment (pallas base field element)
+* $P_x, P_y$ - Coordinates of the recipient public key which go into the coin commitment (pallas base field elements)
+* $v$ - Value of the coin (unsigned 64-bit integer)
+* $τ$ - Token ID of the coin (pallas base field element)
 * $h$ - Spend hook, allows composing this ZK proof to invoke other contracts (pallas base field element)
 * $u$ - Data passed from this coin to the invoked contract (pallas base field element)
+* $b$ - Blinding factor of the coin, ensuring its uniqueness (pallas base field element)
 * $v_\t{blind}$ - Random blinding factor for a Pedersen commitment to $v$ (pallas scalar field element)
-* $t_\t{blind}$ - Random blinding factor for a commitment to $t$ (pallas base field element)
+* $τ_\t{blind}$ - Random blinding factor for a commitment to $τ$ (pallas base field element)
 
 **Circuit public inputs:**
 
@@ -66,19 +90,15 @@ Denote this proof by $π_\t{mint}$.
 
 **Circuit:**
 
-$$ C = \text{PoseidonHash}(P, v, t, s, h, u) $$
-$$ \text{RangeCheck}(64, v) $$
+$$ C = \text{PoseidonHash}(P_x, P_y, v, τ, h, u, b) $$
 $$ V = vG + v_{\text{blind}}H $$
-$$ T = \text{PoseidonHash}(t, t_{\text{blind}}) $$
-
-$G$ and $H$ are constant well-known generators that are in the codebase
-as `VALUE_COMMIT_VALUE` and `VALUE_COMMIT_RANDOM`:
-
+$$ T = \text{PoseidonHash}(τ, τ_{\text{blind}}) $$
 
-* `src/sdk/src/crypto/constants/fixed_bases/value_commit_v.rs`
-* `src/sdk/src/crypto/constants/fixed_bases/value_commit_r.rs`
+The 64-bit range of $v$ is enforced implicitly: $G$ is a *short* fixed
+base (`VALUE_COMMIT_VALUE`) and its scalar multiplication (`ec_mul_short`)
+only admits witnesses of at most $L_\t{value} = 64$ bits.
 
-### `Burn_V1`
+#### `Burn_V1`
 
 Using the `Burn_V1` circuit, we are able to create inputs in
 our UTXO set. It is used along with the `Mint_V1` circuit in
@@ -89,18 +109,18 @@ Denote this proof by $π_\t{burn}$.
 
 **Circuit witnesses:**
 
+* $x$ - Secret key used to derive the coin's public key $P$ and the nullifier (pallas base field element)
 * $v$ - Value of the coin being spent (unsigned 64-bit integer)
-* $t$ - Token ID of the coin being spent (pallas curve base field element)
-* $v_{\text{blind}}$ - Random blinding factor for a Pedersen commitment to $v$ (pallas scalar field element)
-* $t_{\text{blind}}$ - Random blinding factor for a commitment to $t$ (pallas base field element)
-* $s$ - Unique serial number of the coin commitment (pallas base field element)
+* $τ$ - Token ID of the coin being spent (pallas base field element)
 * $h$ - Spend hook, allows composing this ZK proof to invoke other contracts (pallas base field element)
 * $u$ - Data passed from this coin to the invoked contract (pallas base field element)
+* $b$ - Blinding factor of the coin being spent (pallas base field element)
+* $v_{\text{blind}}$ - Random blinding factor for a Pedersen commitment to $v$ (pallas scalar field element)
+* $τ_{\text{blind}}$ - Random blinding factor for a commitment to $τ$ (pallas base field element)
 * $u_{\text{blind}}$ - Blinding factor for encrypting $u$ (pallas base field element)
-* $x$ - Secret key used to derive $N$ (nullifier) and $P$ (public key) from the coin $C$ (pallas base field element)
 * $l$ - Leaf position of $C$ in the Merkle tree of all coin commitments (unsigned 32-bit integer)
 * $p$ - Merkle path to the coin $C$ in the Merkle tree (array of 32 pallas base field elements)
-* $z$ - Secret key used to derive public key for the tx signature $Z$
+* $z$ - Secret key used to derive the signature public key $Z$
 
 **Circuit public inputs:**
 
@@ -114,21 +134,16 @@ Denote this proof by $π_\t{burn}$.
 
 **Circuit:**
 
-$$ N = \text{PoseidonHash}(x, s) $$
-$$ V = vG + v_{\text{blind}}H $$
-$$ T = \text{PoseidonHash}(t, t_{\text{blind}}) $$
 $$ P = xK $$
-$$ C = \text{PoseidonHash}(P, v, t, s, h, u) $$
+$$ C = \text{PoseidonHash}(\mathcal{X}(P), \mathcal{Y}(P), v, τ, h, u, b) $$
+$$ N = \text{PoseidonHash}(x, C) $$
+$$ V = vG + v_{\text{blind}}H $$
+$$ T = \text{PoseidonHash}(τ, τ_{\text{blind}}) $$
 $$ C' = \text{ZeroCond}(v, C) $$
 $$ R = \text{MerkleRoot}(l, p, C') $$
 $$ U = \text{PoseidonHash}(u, u_{\text{blind}}) $$
 $$ Z = zK $$
 
-$G$ and $H$ are the same generators used in `Mint_V1`, $K$ is the
-generator in the codebase known as `NULLIFIER_K`:
-
-* `src/sdk/src/crypto/constants/fixed_bases/nullifier_k.rs`
-
 `ZeroCond` is a conditional selection: `f(a, b) = if a == 0 {a} else {b}`.
 We use this because the Merkle tree is instantiated with a fake coin of
 value 0 and so we're able to produce dummy inputs of value 0.
@@ -147,25 +162,30 @@ $C'$ that will be minted to Bob after $C$ is spent.
    in order to sign the transaction.
 2. Alice picks a random element $v_{\text{blind}}$ from $F_q$ to use
    as the blinding factor for $V$.
-3. Alice picks a random element $t_{\text{blind}}$ from $F_p$ to use
-   as the blinding factor for $T$.
+3. Alice picks a random element $u_{\text{blind}}$ from $F_p$ to use
+   as the blinding factor for $U$.
 4. Alice creates the `Burn_V1` ZK proof using the existing known values
    of her coin $C$ and the values picked above.
 
 **Values for `Mint_V1`:**
 
-1. Alice picks a random element $s$ from $F_p$ to use as a unique serial
-   number for the new coin $C'$.
-2. Alice optionally chooses a contract ID to use as $h$ or uses `ZERO`
-   if $h$ does not have to call another contract.
+1. Alice picks a random element $b$ from $F_p$ to use as the blinding
+   factor for the new coin $C'$, which guarantees its uniqueness.
+2. Alice optionally chooses a contract function ID to use as $h$ or uses `ZERO`
+   if the coin does not have to call another contract.
 3. Alice optionally chooses necessary data for $u$ or uses `ZERO`
    if no data has to be passed.
-4. Alice chooses the corresponding $v_{\text{blind}}$ to be able to
-   enforce the Pedersen commitment correctness ($\infty + V - V'$ has
-   to evaluate to $\infty$)
+4. Alice chooses $v_{\text{blind}}$ for the last output such that the
+   value blinds of all inputs and outputs cancel each other out. This
+   way the Pedersen commitment homomorphism enforces that the spent and
+   minted values are equal (see *per token value conservation* above).
 5. Alice creates the `Mint_V1` ZK proof using the existing known values
    and the values picked above.
 
+A single token blind $τ_\t{blind}$ is drawn for the whole call and reused
+for every token commitment, so that all inputs and outputs of the call
+commit to the same token.
+
 After creating the proofs, Alice builds a transaction containing a
 number of inputs that were created with `Burn_V1` and a number of
 outputs created with `Mint_V1`.
@@ -175,14 +195,272 @@ outputs created with `Mint_V1`.
 ```
 
 This gets encoded into the `Transaction` format and the transaction is
-signed with a Schnorr signature scheme using the $z$ secret key chosen
-in `Burn_V1`.
+signed with a Schnorr signature scheme using the $z$ secret keys chosen
+when building the `Burn_V1` proofs.
 
 ### Contract call execution
 
-For `MoneyFunction::TransferV1`, we have the following functions, in
-order:
+For `MoneyFunction::TransferV1`, the `get_metadata`, `process_instruction`
+and `process_update` entrypoint phases
+(`src/contract/money/src/entrypoint/transfer_v1.rs`) do the following:
+
+* `get_metadata` verifies one `Burn_V1` proof per input against the
+  public inputs $(N, V_x, V_y, T, R, U, h, Z_x, Z_y)$ and one `Mint_V1`
+  proof per output against $(C, V_x, V_y, T)$, and gathers all the $Z$
+  signature public keys.
+* `process_instruction` enforces the contract statement described above,
+  and produces a state update with the new nullifiers, the new on-chain
+  coins, and the new transaction-local coins.
+* `process_update` inserts the nullifiers into the nullifier sparse
+  Merkle tree, and appends the new coins to the on-chain or
+  transaction-local coin Merkle trees.
+
+## Fee
+
+This function attaches a fee to a transaction so it gets included in
+a block. Fees are paid in the native token, accumulate per block
+height, and are later claimed together with the block reward via
+[`Money::PoWRewardV1`](#powreward).
+
+The call data begins with the `u64` fee value, followed by the params.
+A single `Fee_V1` proof burns one input coin and mints one output coin
+(usually the change) back to the same public key.
+
+* Wallet builder: `src/contract/money/src/client/fee_v1.rs`
+* WASM VM code: `src/contract/money/src/entrypoint/fee_v1.rs`
+* ZK proof: `src/contract/money/proof/fee_v1.zk`
+
+### Function Params
+
+$$ \begin{aligned}
+  \t{input} &∈ \t{MoneyInput} \\
+  \t{output} &∈ \t{MoneyOutput} \\
+  f_\t{blind} &∈ 𝔽_q \\
+  t_\t{blind} &∈ 𝔽ₚ \\
+  \t{fee} &∈ ℕ₆₄ \\
+\end{aligned} $$
+
+### Contract Statement
+
+The `Fee_V1` circuit is a fusion of `Burn_V1` and `Mint_V1` for a
+single input and output sharing the same public key $P = xK$ and token
+$τ$. Besides the constraints of both circuits (with the input's spend
+hook additionally constrained to be `ZERO`, since fee coins cannot be
+hooked, and no `ZeroCond` applied to the input coin), the contract
+verifies:
+
+* **Nonzero fee** &emsp; $\t{fee} ≠ 0$.
+* **Native token** &emsp; the token commitments of both input and output
+  equal $\t{PoseidonHash}(\t{DARK}, t_\t{blind})$ where $\t{DARK}$ is the
+  native token ID.
+* **Value conservation** &emsp;
+  $V_\t{input} - V_\t{output} = \t{fee} G + f_\t{blind} H$.
+* **Fee accumulation** &emsp; the paid fee is added to the accumulator
+  for the verifying block height.
+
+The remaining checks (valid merkle root, unused nullifier, unique coin)
+are as in [Transfer](#transfer).
+
+### Signatures
+
+A single signature is attached, using $\t{input}.Z$ as the signature
+public key.
+
+## GenesisMint
+
+This function mints the initial supply of the native token. It is only
+valid when verified against the genesis block (height 0), and reuses
+the [`Mint_V1`](#mint_v1) proofs for its outputs.
+
+* Wallet builder: `src/contract/money/src/client/genesis_mint_v1.rs`
+* WASM VM code: `src/contract/money/src/entrypoint/genesis_mint_v1.rs`
+* ZK proof: `src/contract/money/proof/mint_v1.zk`
+
+### Function Params
+
+$$ \begin{aligned}
+  \t{input} &∈ \t{MoneyClearInput} \\
+  𝐨 &∈ \t{MoneyOutput}^* \\
+\end{aligned} $$
+
+### Contract Statement
+
+* **Genesis only** &emsp; the verifying block height must be 0.
+* **Native token only** &emsp; $\t{input}.T$ must be the native token ID,
+  and every output token commitment must equal
+  $\t{PoseidonHash}(\t{input}.T, \t{input}.t_\t{blind})$.
+* **Unique coins** &emsp; as in [Transfer](#transfer); transaction-local
+  outputs are not allowed.
+* **Value conservation** &emsp;
+  $\sum_{o ∈ 𝐨} o.V = vG + v_\t{blind}H$ for the clear input's value
+  and blind.
+
+### Signatures
+
+A single signature is attached, using $\t{input}.Z$ as the signature
+public key.
+
+## PoWReward
+
+This function mints the proof-of-work reward for a block, including the
+fees accumulated for that height via [`Money::FeeV1`](#fee). It reuses
+the [`Mint_V1`](#mint_v1) proof for its output.
+
+* Wallet builder: `src/contract/money/src/client/pow_reward_v1.rs`
+* WASM VM code: `src/contract/money/src/entrypoint/pow_reward_v1.rs`
+* ZK proof: `src/contract/money/proof/mint_v1.zk`
+
+### Function Params
+
+$$ \begin{aligned}
+  \t{input} &∈ \t{MoneyClearInput} \\
+  \t{output} &∈ \t{MoneyOutput} \\
+\end{aligned} $$
+
+### Contract Statement
+
+* **Next block only** &emsp; the call must be verified against exactly
+  the height following the current top block, and not against genesis.
+* **Native token only** &emsp; as in [GenesisMint](#genesismint).
+* **Correct reward value** &emsp; $\t{input}.v$ must equal the expected
+  reward for the block height plus the fees accumulated for that height
+  in the fees DB.
+* **Unique coin** &emsp; as in [Transfer](#transfer); transaction-local
+  outputs are not allowed.
+
+### Signatures
+
+A single signature is attached, using $\t{input}.Z$ as the signature
+public key.
+
+## TokenMint
+
+This function mints arbitrary tokens and coins for them, authorizing
+the mint via a *child* auth module call. The token ID is derived from
+the token attributes (see [Token](model.md#token)), where the parent
+authority is the function ID of the attached auth call.
+
+* Wallet builder: `src/contract/money/src/client/token_mint_v1.rs`
+* WASM VM code: `src/contract/money/src/entrypoint/token_mint_v1.rs`
+* ZK proof: `src/contract/money/proof/token_mint_v1.zk`
+
+### Function Params
+
+$$ \begin{aligned}
+  C &∈ 𝔽ₚ \\
+  \t{note} &∈ \t{AeadEncNote} \\
+\end{aligned} $$
+
+### Contract Statement
+
+The call must have exactly one child call, acting as the auth module
+(e.g. [`Money::AuthTokenMintV1`](#authtokenmint)). Let $h_\t{auth}$ be
+the function ID of this child call. The `TokenMint_V1` proof enforces:
+
+* **Token ID integrity** &emsp; $τ = \t{PoseidonHash}(h_\t{auth}, \t{UD}_τ, b_τ)$
+  for the token attributes.
+* **Coin integrity** &emsp; $C = \t{Coin}(\t{PK}, v, τ, \t{SH}, \t{UD}, b)$
+  using the derived token ID $τ$.
+
+Additionally the contract verifies the minted coin $C$ is unique, and
+the encrypted note $\t{note}$ carries the coin secrets to the receiver.
+
+### Signatures
+
+No signatures are attached. Authorization is delegated to the child
+auth module call.
+
+## AuthTokenMint
+
+This is an auth module for [`Money::TokenMintV1`](#tokenmint) which
+authorizes token mints with a Schnorr signature from the token's mint
+authority key. Token IDs are bound to their authority by deriving the
+token attributes' user data from the authority's public key:
+$\t{UD}_τ = \t{PoseidonHash}(\mathcal{X}(\t{PK}_\t{mint}), \mathcal{Y}(\t{PK}_\t{mint}))$.
+
+* Wallet builder: `src/contract/money/src/client/auth_token_mint_v1.rs`
+* WASM VM code: `src/contract/money/src/entrypoint/auth_token_mint_v1.rs`
+* ZK proof: `src/contract/money/proof/auth_token_mint_v1.zk`
+
+### Function Params
+
+$$ \begin{aligned}
+  τ &∈ 𝔽ₚ \\
+  \t{PK}_\t{mint} &∈ ℙₚ \\
+\end{aligned} $$
+
+### Contract Statement
+
+The parent call must be `Money::TokenMintV1`. Let $h$ be the function
+ID of this function itself. The `AuthTokenMint_V1` proof enforces:
+
+* **Mint authority key ownership** &emsp; $\t{PK}_\t{mint} = zK$.
+* **Token ID integrity** &emsp; $τ = \t{PoseidonHash}(h, \t{PoseidonHash}(\mathcal{X}(\t{PK}_\t{mint}), \mathcal{Y}(\t{PK}_\t{mint})), b_τ)$.
+* **Coin integrity** &emsp; the minted coin $C$ (taken from the parent
+  `Money::TokenMintV1` call) commits to the derived token ID $τ$.
+
+Additionally the contract verifies the token mint is not frozen in the
+token freezes DB.
+
+### Signatures
+
+A single signature is attached, using $\t{PK}_\t{mint}$ as the signature
+public key.
+
+## AuthTokenFreeze
+
+This function permanently freezes a token so it can no longer be minted
+via [`Money::AuthTokenMintV1`](#authtokenmint). It is authorized by the
+same mint authority key the token ID is bound to.
+
+* Wallet builder: `src/contract/money/src/client/auth_token_freeze_v1.rs`
+* WASM VM code: `src/contract/money/src/entrypoint/auth_token_freeze_v1.rs`
+* ZK proof: `src/contract/money/proof/auth_token_freeze_v1.zk`
+
+### Function Params
+
+$$ \begin{aligned}
+  \t{PK}_\t{mint} &∈ ℙₚ \\
+  τ &∈ 𝔽ₚ \\
+\end{aligned} $$
+
+### Contract Statement
+
+Let $h$ be the function ID of `Money::AuthTokenMintV1`. The
+`AuthTokenFreeze_V1` proof enforces the same **mint authority key
+ownership** and **token ID integrity** relations as
+[`AuthTokenMint`](#authtokenmint). Additionally the contract verifies
+the token is not already frozen, and the state update freezes it in the
+token freezes DB.
+
+### Signatures
+
+A single signature is attached, using $\t{PK}_\t{mint}$ as the signature
+public key.
+
+## Burn
+
+This function burns (destroys) coins, permanently removing value from
+circulation. The call only has inputs and no outputs; the value
+committed in the inputs is destroyed. It reuses the
+[`Burn_V1`](#burn_v1) proofs.
+
+* Wallet builder: `src/contract/money/src/client/burn_v1.rs`
+* WASM VM code: `src/contract/money/src/entrypoint/burn_v1.rs`
+* ZK proof: `src/contract/money/proof/burn_v1.zk`
+
+### Function Params
+
+$$ 𝐢 ∈ \t{MoneyInput}^* $$
+
+### Contract Statement
+
+Identical to the input part of [Transfer](#transfer): valid coin merkle
+roots, unused nullifiers, and spend hook enforcement via the parent
+call's function ID. Since there are no outputs, no value conservation
+check is performed — the burned value is simply gone.
+
+### Signatures
 
-* [`money_transfer_get_metadata_v1`](https://git.dark.fi/darkrenaissance/darkfi/src/master/src/contract/money/src/entrypoint/transfer_v1.rs#L42)
-* [`money_transfer_process_instruction_v1`](https://git.dark.fi/darkrenaissance/darkfi/src/master/src/contract/money/src/entrypoint/transfer_v1.rs#L106)
-* [`money_transfer_process_update_v1`](https://git.dark.fi/darkrenaissance/darkfi/src/master/src/contract/money/src/entrypoint/transfer_v1.rs#L258)
+For each $i ∈ 𝐢$, a signature corresponding to the public key $i.Z$
+is attached.

+ 2 - 0
src/contract/money/src/model/mod.rs

@@ -99,11 +99,13 @@ impl CoinAttributes {
 }
 
 #[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
+// ANCHOR: token-attributes
 pub struct TokenAttributes {
     pub auth_parent: FuncId,
     pub user_data: pallas::Base,
     pub blind: BaseBlind,
 }
+// ANCHOR_END: token-attributes
 
 impl TokenAttributes {
     pub fn to_token_id(&self) -> TokenId {