doc/spec/contract/vesting: formal definitions added

This commit is contained in:
skoupidi
2026-03-11 14:41:04 +02:00
parent 9c9cc9d340
commit eb0a98572b
5 changed files with 492 additions and 61 deletions

View File

@@ -102,6 +102,9 @@
- [Concepts](spec/contract/deploy/concepts.md)
- [Scheme](spec/contract/deploy/scheme.md)
- [Vesting](spec/contract/vesting/vesting.md)
- [Concepts](spec/contract/vesting/concepts.md)
- [Model](spec/contract/vesting/model.md)
- [Scheme](spec/contract/vesting/scheme.md)
# P2P API Tutorial

View File

@@ -0,0 +1,60 @@
# Concepts
The vesting process is divided in a few steps that are outlined below:
* **Vest:** a vesting configuration is submitted to the blockchain.
* **Withdraw:** vestee can withdraw some amount of their vested coin.
* **Forfeit:** vesting authority forfeits a specific vesting.
* **Exec:** overlay function over `Withdraw` and `Forfeit`, used as the
spend hook binding the coins to these contract calls.
## Vest
Vesting authority submits a vesting configuration on-chain, along with
a chained transfer call burning the to-be-vested input coins, which
must be for the same token, and minting a new coin with their total
amount for a shared secret address, using the contracts' `Exec` call
spend-hook, effectivelly becoming usable only withing the vesting
contract context.
> Note: There is a limitation though; the vested coin can only be
> controlled by a single address. To overcome this, when the vesting
> authority creates the vested coin, instead of using the recipients
> actual address, it generates a new random one, which is shared with
> the vestee(in a safe off-chain manner), so the coin can be controlled
> by both parties.
> Note: Since vesting requires a 1-1 vested coin to config matching, it
> means that those coins can be tracked by the vesting configuration
> bulla. It doesn't affect rest anonimity, since it's specific to the
> vesting contract flow and no other information can be derived by it.
## Withdraw
After some time has passed, the vestee can withdraw some amount from
their vested coin, which is done by a chained transfer call burning the
existing vested coin and creating two output coins; one which will be a
normal coin for a withdrawal address, and a second one representing the
remaining balance using the spend hook of the vesting contract. This
call is responsible to define the available-to-withdraw amount, along
with ensuring the chained transfer call second output correctly
represents the remaining balance vested coin. We don't care if that
becomes a zero value coin, since it won't be usable anymore, and we
want all our outputs to look the same, so the final withdrawl cannot be
tracked.
> Note: A medatada leak exists though; the final withdrawl cannot be
> conventionally tracked, since nobody but the parties knows when the
> remaining balance coin becomes a zero value one, but someone tracking
> all the withdrawls calls for a specific vesting configuration can
> assume that the vesting has ended, if no other withdrawl is observed
> after some time period. Still, they can't prove that the vesting has
> concluded without access to the vesting information and/or the shared
> secret address.
## Forfeit
With this call, a vesting authority is able to forfeit a specific
vesting, by removing the vesting configuration, burning the remaining
balance vested coin and mint a new one to a recipient address, using a
chained transfer call.

View File

@@ -0,0 +1,82 @@
# Model
Let $\t{Bulla}$ be defined as in the section [Bulla Commitments][1].
Let $ℙₚ, 𝔽ₚ, \mathcal{X}, \mathcal{Y}, \t{𝔹⁶⁴2𝔽ₚ}$ be defined as in the
section [Pallas and Vesta][2].
Let $\t{Coin}$ be defined as in the section [Coin][3].
## Vesting Configuration
The vesting configuration contains the main parameters that define the
vesting operation:
* The vesting authority public key $VAPK$ controls the vesting
configuration.
* The vestee public key $VPK$ for withdrawls.
* The shared secret public key $SPK$ controls who can use the vested
coin.
* Token $t$ is the token type of the to-be-vested input coins.
* Total $T$ is the total amount of the to-be-vested input coins.
* Cliff $C$ is the amount unlocked at the start blockwindow $S$.
* Start $S$ and end $E$ are the blockwindows defining when vesting
starts and ends.
* Blockwindow value $V$ is the amount unlocked on each blockwindow.
Define the vesting configuration $VC$ params:
$$ \begin{aligned}
\t{Params}_\t{VC}.\t{VAPK} &∈ ℙₚ \\
\t{Params}_\t{VC}.\t{VPK} &∈ ℙₚ \\
\t{Params}_\t{VC}.\t{SPK} &∈ ℙₚ \\
\t{Params}_\t{VC}.τ &∈ 𝔽ₚ \\
\t{Params}_\t{VC}.T &∈ ℕ₆₄ \\
\t{Params}_\t{VC}.C &∈ ℕ₆₄ \\
\t{Params}_\t{VC}.S &∈ ℕ₆₄ \\
\t{Params}_\t{VC}.E &∈ ℕ₆₄ \\
\t{Params}_\t{VC}.V &∈ ℕ₆₄
\end{aligned} $$
```rust
TODO: add model definition path
```
$$ \t{Bulla}_\t{VC} : \t{Params}_\t{VC} × 𝔽ₚ → 𝔽ₚ $$
$$ \begin{aligned}
\t{Bulla}_\t{VC}(p, b_\t{VC}) = \t{Bulla}( \\
\mathcal{X}(p.\t{VAPK}), \mathcal{Y}(p.\t{VAPK}), \\
\mathcal{X}(p.\t{VPK}), \mathcal{Y}(p.\t{VPK}), \\
\mathcal{X}(p.\t{SPK}), \mathcal{Y}(p.\t{SPK}), \\
p.τ, \\
₆₄2𝔽ₚ(p.T), \\
₆₄2𝔽ₚ(p.C), \\
₆₄2𝔽ₚ(p.S), \\
₆₄2𝔽ₚ(p.E), \\
₆₄2𝔽ₚ(p.V), \\
b_\t{VC} \\
)
\end{aligned} $$
> Note: Since a vesting configuration bulla is derived using a random
> blinding factor, it's safe to use the same parameters to generate
> different vesting configurations.
## Blockwindow
Time limits on vesting configurations are expressed in 1 day windows.
Since proofs cannot guarantee which block they get into, we therefore
must modulo the block height a certain number which we use in the
proofs.
```rust
TODO: add definition path
```
which can be used like this:
```rust
TODO: add usage example path
```
[1]: ../../crypto-schemes.md#bulla-commitments
[2]: ../../crypto-schemes.md#pallas-and-vesta
[3]: ../money/model.md#coin

View File

@@ -0,0 +1,305 @@
# Scheme
<!-- toc -->
Let $\t{Params}_\t{VC}, \t{Bulla}_\t{VC}$ be defined as in
[Vesting Configuration Model](model.md).
Let $\t{Coin}$ be defined as in the section [Coin][1].
Let $ℙₚ, 𝔽ₚ, \mathcal{X}, \mathcal{Y}, \t{𝔹⁶⁴2𝔽ₚ}$ be defined as in the
section [Pallas and Vesta][2].
Let $t₀ = \t{BlockWindow} ∈ 𝔽ₚ$ be the current blockwindow as defined
in [Blockwindow](model.md#blockwindow).
Let $\t{PoseidonHash}$ be defined as in the section
[PoseidonHash Function](../../crypto-schemes.md#poseidonhash-function).
Let $\t{ElGamal.Encrypt}, \t{ElGamalEncNote}ₖ$ be defined as in the
section [Verifiable In-Band Secret Distribution][3].
Denote the Vesting contract ID by $\t{CID}_\t{V} ∈ 𝔽ₚ$ and its `Exec`
function spend hook by $\t{SH}_\t{V} ∈ 𝔽ₚ$.
## Vest
This function creates a vesting configuration bulla $_\t{VC}$. We
commit to the vesting configuration params and then add the bulla to
the set, along with the vested coin $\t{Coin}$ minted by the child
`Money::transfer()` call. Each vesting configuration keeps track of its
minted coins, to ensure that only those can be burned in next actions,
creating a sequence of coins, enabling the contract to keep track of
remaining balances anonymously. Additionally, we verify the minted
vesting coin is encrypted for the configuration shared secret key,
ensuring both parties have access to it.
* Wallet builder: `TODO: add client path`
* WASM VM code: `TODO: add entrypoint path`
* ZK proof: `TODO: add proof path`
### Function Params
Define the vest function params
$$ \begin{aligned}
_\t{VC} &∈ \t{im}(\t{Bulla}_\t{VC}) \\
\t{SPK} &∈ ℙₚ
\end{aligned} $$
```rust
TODO: Add call params path
```
### Contract Statement
**Vesting configuration bulla uniqueness** &emsp; whether $_\t{VC}$
already exists. If yes then fail.
Let there be a prover auxiliary witness inputs:
$$ \begin{aligned}
VAx &∈ 𝔽ₚ \\
VPK &∈ 𝔽ₚ \\
Sx &∈ 𝔽ₚ \\
τ &∈ 𝔽ₚ \\
T &∈ ℕ₆₄ \\
C &∈ ℕ₆₄ \\
S &∈ ℕ₆₄ \\
E &∈ ℕ₆₄ \\
V &∈ ℕ₆₄ \\
b_\t{VC} &∈ 𝔽ₚ \\
b_\t{Coin} &∈ 𝔽ₚ
\end{aligned} $$
Attach a proof $π$ such that the following relations hold:
**Proof that start blockwindow is greater than current blockwindow**
&emsp; $S > t₀$.
**Proof that end blockwindow is greater than start blockwindow** &emsp;
$E > S$.
**Proof that total is greater than cliff** &emsp; $T >= C$.
**Proof that blockwindow value is valid** &emsp; $T == (E - S) * V +
C$.
**Proof of vesting authority public key ownership** &emsp; $\t{VAPK} =
\t{DerivePubKey}(VAx)$.
**Proof of shared secret public key ownership** &emsp; $\t{SPK} =
\t{DerivePubKey}(Sx)$.
**Vesting configuration bulla integrity** &emsp; $ =
\t{Bulla}_\t{VC}(\mathcal{X}(p.\t{VAPK}), \mathcal{Y}(p.\t{VAPK}),
\mathcal{X}(p.\t{VPK}), \mathcal{Y}(p.\t{VPK}),
\mathcal{X}(p.\t{SPK}), \mathcal{Y}(p.\t{SPK}), t, T, C, S, E, V,
b_\t{VC})$
**Minted vested coin integrity** &emsp; $Coin =
\t{PoseidonHash}(\mathcal{X}(p.\t{SPK}), \mathcal{Y}(p.\t{SPK}), T, t,
\t{CID}_\t{V}, \t{SH}_\t{V}, , b_\t{Coin})$
**Verifiable vested 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{SPK})$.
### Signatures
There should be a single signature attached, which uses
$\t{SPK}$ as the signature public key.
## Withdraw
This function enables the vestee to withdraw the corresponding unlocked
value up to that blockwindow. The child `Money::transfer()` call must
contain a single input, the vested coin we burn, and two outputs. The
first one being the withdrawed one while the second one is the
remaining vested balance coin. Both coins values are verified by the
vesting configuration rules, and we store the second one as the current
vested coin, to burn in next actions. Additionally, we verify the
second/vested coin is encrypted for the configuration shared secret
key, ensuring both parties have access to it.
* Wallet builder: `TODO: add client path`
* WASM VM code: `TODO: add entrypoint path`
* ZK proof: `TODO: add proof path`
### Function Params
Define the withdraw function params
$$ \begin{aligned}
_\t{VC} &∈ \t{im}(\t{Bulla}_\t{VC}) \\
\t{SPK} &∈ ℙₚ
\end{aligned} $$
```rust
TODO: Add call params path
```
### Contract Statement
**Vesting configuration bulla existance** &emsp; whether $_\t{VC}$
exists. If no then fail.
**Burned vested coin existance** &emsp; whether the burned coin
$\t{BCoin}$ matches the vesting configuration record one. If no then
fail.
Let there be a prover auxiliary witness inputs:
$$ \begin{aligned}
VAPK &∈ 𝔽ₚ \\
Vx &∈ 𝔽ₚ \\
Sx &∈ 𝔽ₚ \\
τ &∈ 𝔽ₚ \\
T &∈ ℕ₆₄ \\
C &∈ ℕ₆₄ \\
S &∈ ℕ₆₄ \\
E &∈ ℕ₆₄ \\
V &∈ ℕ₆₄ \\
b_\t{VC} &∈ 𝔽ₚ \\
Bv &∈ ℕ₆₄ \\
b_\t{BCoin} &∈ 𝔽ₚ \\
x_c &∈ 𝔽ₚ \\
Cv &∈ ℕ₆₄ \\
b_\t{Coin} &∈ 𝔽ₚ
\end{aligned} $$
Attach a proof $π$ such that the following relations hold:
**Proof of vestee public key ownership** &emsp; $\t{VPK} =
\t{DerivePubKey}(Vx)$.
**Proof of shared secret public key ownership** &emsp; $\t{SPK} =
\t{DerivePubKey}(Sx)$.
**Vesting configuration bulla integrity** &emsp; $ =
\t{Bulla}_\t{VC}(\mathcal{X}(p.\t{VAPK}), \mathcal{Y}(p.\t{VAPK}),
\mathcal{X}(p.\t{VPK}), \mathcal{Y}(p.\t{VPK}),
\mathcal{X}(p.\t{SPK}), \mathcal{Y}(p.\t{SPK}), t, T, C, Cb, S, E, V,
b_\t{VC})$
**Proof that current blockwindow is greater than start blockwindow**
&emsp; $t₀ >= S$.
TODO: cond_select statement to pick current or end blockwindow
**Proof of withdraw amount correctness** &emsp;
$$ \begin{aligned}
CurrentBlockwindow = CondSelect(BlockwindowCond, t₀, E); \\
BlockwindowsPassed = CurrentBlockwindow - S; \\
Available = (BlockwindowsPassed * V) + C; \\
Withdrawn = T - Bv; \\
WithdrawlCoinValue = Available - Withdrawn; \\
VestingChangeValue = T - (Withdrawn + WithdrawlCoinValue);
\end{aligned} $$
Verify the child `Money::transfer()` call correctnes:
**Burned vested coin integrity** &emsp; $BCoin =
\t{PoseidonHash}(\mathcal{X}(p.\t{SPK}), \mathcal{Y}(p.\t{SPK}),
Bv, t, \t{CID}_\t{V}, \t{SH}_\t{V}, , b_\t{Coin})$
**Burned vested coin nullifier integrity** &emsp; $\cN =
\t{PoseidonHash}(x_c, BCoin)$
**Minted vested coin integrity** &emsp; $Coin =
\t{PoseidonHash}(\mathcal{X}(p.\t{SPK}), \mathcal{Y}(p.\t{SPK}),
VestingChangeValue, t, \t{CID}_\t{V}, \t{SH}_\t{V}, , b_\t{Coin})$
**Verifiable vested 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{SPK})$.
### Signatures
There should be a single signature attached, which uses
$\t{SPK}$ as the signature public key.
## Forfeit
This function enables the vesting authority to forfeit a vesting
configuration, withdrawing the rest of vested value. The child
`Money::transfer()` call must containg a single input, the vested coin
we burn, and a single output, the newlly minted coin. Both coins values
are verified by the vesting configuration rules, and we remove the
vesting configuration bulla $_\t{VC}$ entry from the set.
* Wallet builder: `TODO: add client path`
* WASM VM code: `TODO: add entrypoint path`
* ZK proof: `TODO: add proof path`
### Function Params
Define the vest function params
$$ \begin{aligned}
_\t{VC} &∈ \t{im}(\t{Bulla}_\t{VC}) \\
\t{SPK} &∈ ℙₚ
\end{aligned} $$
```rust
TODO: Add call params path
```
### Contract Statement
**Vesting configuration bulla existance** &emsp; whether $_\t{VC}$
exists. If no then fail.
**Burned vested coin existance** &emsp; whether the burned coin
$\t{BCoin}$ matches the vesting configuration record one. If no then
fail.
Let there be a prover auxiliary witness inputs:
$$ \begin{aligned}
VAx &∈ 𝔽ₚ \\
VPK &∈ 𝔽ₚ \\
Sx &∈ 𝔽ₚ \\
τ &∈ 𝔽ₚ \\
T &∈ ℕ₆₄ \\
C &∈ ℕ₆₄ \\
S &∈ ℕ₆₄ \\
E &∈ ℕ₆₄ \\
V &∈ ℕ₆₄ \\
b_\t{VC} &∈ 𝔽ₚ
Bv &∈ ℕ₆₄ \\
b_\t{BCoin} &∈ 𝔽ₚ \\
x_c &∈ 𝔽ₚ
\end{aligned} $$
Attach a proof $π$ such that the following relations hold:
**Proof of vesting authority public key ownership** &emsp; $\t{VAPK} =
\t{DerivePubKey}(VAx)$.
**Proof of shared secret public key ownership** &emsp; $\t{SPK} =
\t{DerivePubKey}(Sx)$.
**Vesting configuration bulla integrity** &emsp; $ =
\t{Bulla}_\t{VC}(\mathcal{X}(p.\t{VAPK}), \mathcal{Y}(p.\t{VAPK}),
\mathcal{X}(p.\t{VPK}), \mathcal{Y}(p.\t{VPK}),
\mathcal{X}(p.\t{SPK}), \mathcal{Y}(p.\t{SPK}), t, T, C, S, E, V,
b_\t{VC})$
**Proof of forfeit amount correctness** &emsp; $ForfeitValue = T - Bv$
Verify the child `Money::transfer()` call correctnes:
**Burned vested coin integrity** &emsp; $BCoin =
\t{PoseidonHash}(\mathcal{X}(p.\t{SPK}), \mathcal{Y}(p.\t{SPK}),
ForfeitValue, t, \t{CID}_\t{V}, \t{SH}_\t{V}, , b_\t{Coin})$
**Burned vested coin nullifier integrity** &emsp; $\cN =
\t{PoseidonHash}(x_c, BCoin)$
**Minted coin integrity** &emsp; $Coin =
\t{PoseidonHash}(\mathcal{X}(p.\t{SPK}), \mathcal{Y}(p.\t{SPK}),
ForfeitValue, t, \t{CID}_\t{V}, \t{SH}_\t{V}, , b_\t{Coin})$
### Signatures
There should be a single signature attached, which uses
$\t{SPK}$ as the signature public key.
[1]: ../money/model.md#coin
[2]: ../../crypto-schemes.md#pallas-and-vesta
[3]: ../../crypto-schemes.md#verifiable-in-band-secret-distribution

View File

@@ -1,67 +1,48 @@
# Vesting
```
status: draft
```
## Abstract
We want to create a fully anonymous vesting contract, in which all the vesting information is private.
What we need, is a vesting authority, which initially submits coins to be vested, along with the vesting
configuration. When this happens, all input coins, which must be for the same token, get burned, and a new
coin is minted for the vested public key, which uses the spend hook of the vesting contract, effectivelly
becoming usable only withing the vesting contract context. After some time has passed, the vestee can
withdraw some coins, which is done by burning the existing vested coin, and creates two output coins;
one representing the remaining balance using the spend hook of the vesting contract and a second one
which will be a normal coin for a withdrawal address(DAOs can be recipients too).
This contract implements fully anonymous vesting, in which all the
vesting information is private. Anyone can become a vesting authority,
submitting coins to-be-vested for another user(or a DAO), the vestee.
After some time has passed, the vestee can withdraw a chunk of the
vested coin value. The vesting authority is also able to forfeit a
vesting at any time, retrieving the remaining vested coin balance.
The vesting authority must also be able to forfeit the configured vesting. There is a limitation though;
the vested coin can only be controlled by a single address. To overcome this, when the vesting authority
creates the vested coin, instead of using the recipients actual address, it generates a new random one,
which is shared with the vestee(in a safe off-chain manner), so the coin can be controlled by both parties.
When the vestee withdraws some funds, the new remaining balance token must use the same shared secret key.
We don't care if that becomes a zero value coin, since it won't be usable anymore, and we want all our
outputs to look the same, so the final withdrawl cannot be tracked.
## Vesting configuration
The vesting contract uses 1 day block windows as its time measurement, similar to the DAO contract.
The configuration structure contains the following:
1. auth_public_x: The vesting autority public key X coord
2. auth_public_y: The vesting autority public key Y coord
3. shared_public_x: The shared vestee public key X coord
4. shared_public_y: The shared vestee public key Y coord
5. cliff: Amount unlocked at the cliff timestamp.
6. cliff_window: Vesting contract cliff block window.
7. start_window: Block window when the tokens start vesting.
8. end_window: Block window when all the tokens are fully vested.
The above information get hashed by poseidon to produce the VestingBulla, which is used as the on-chain
identifier of this specific configuration.
## Contract calls
TODO: describe all the checks for each call
### Vest
Vesting authority submits a vesting configuration on-chain, burns the to-be-vested input coins and mints
a new coin with their total amount for the shared secret address, using the contracts' spend-hook.
### Withdraw
This call is responsible to define the available to withdraw amount, along with checking the first output
correctly represents the shared secret and uses contract spend-hook. In this call, input coin value
commitment must match the addition of the output coin value commitments. Also we check the next call is a
normal money transfer call, which executes the actual burn and mint functions.
### ExecWithdraw
This is an overlay function, acting as the parent of a Withdraw and money Transfer calls combination, to
bound them together into a single atomic action, and define the input spendhook that must be used in the
transfer call.
### Forfeit
With this call, the vest autority is able to forfeit a specific vesting, by removing the configuration,
burning the existing vest coin and mint a new one to a recipient address, using a normal money transfer.
Input and output coin values must be identical.
### ExecForfeit
This is an overlay function, acting as the parent of a Forfeit and money Transfer calls combination, to
bound them together into a single atomic action, and define the input spendhook that must be used in the
transfer call.
- [Concepts](concepts.md)
- [Model](model.md)
- [Scheme](scheme.md)
> Open questions:
> 1. Do we need a separate cliff time? If its set thats the start time
> so no real need to keep them both we can assume start == cliff.
> 2. Is using the shared key for signatures safe and needed?
> 3. Should vesting configurations be grouped by authority so is easier
> UX to manage them?
> 4. Is the vested coin encryption verification formula correct?
> 5. Do we need to check both coins in withdraw transfer in the proof or
> its fine since transfer itself enforces them?
> 6. Vesting requires 1-1 vested coin to config matching, which means
> vested coin is trackable as they are used during the vesting process.
> Does that break any anonymity properties? Withdrawed coins cannot be
> tracked, just the vested coin.
> 7. We need to figure out a way to handle withdrawls after end
> blockwindow has passed. We can use `cond_select` where both prover
> and verifier pass the condition checl `current >= end` and in the
> proof we pick current blockwindow or end blockwindow based on that.
> But this require the verifier to know the end blockwindow, unless we
> find a way the condition check can be done without revealing it.
> Another option is to have an explicit `WithdrawAfterEnd` to withdraw
> remaining balance after end blockwindow has passed. We already have
> the metadata leak of ending tracking assumption, so perhaps its
> worthy to sacrifice it.
> 8. Withdrawl calcs correctness? They can also be simplified further
> for proof optimization.
> 9. All calls use the same parameters. Unless we need something in any
> of them they will be the same structure in the final code.
> 10. Do we need to check both coins in forfeit transfer in the proof or
> its fine since transfer itself enforces them?