From 204c757bdf1fcb51cd5f5d0e0a7a44e9b731aeb1 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 12 May 2026 21:08:52 -0300 Subject: [PATCH] update --- docs/specs.md | 58 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/docs/specs.md b/docs/specs.md index 759d85f9..d29f9818 100644 --- a/docs/specs.md +++ b/docs/specs.md @@ -342,7 +342,7 @@ The kind is serialized as a fixed 81-byte header prepended to the encrypted acco ``` Regular(ident): 0x00 || ident (16 bytes LE) || [0u8; 64] -Pda { program_id, seed, ident }: 0x01 || program_id (32 bytes) || seed (32 bytes) || ident (16 bytes LE) +Pda { program_id, seed, ident }: 0x01 || program_id (8 × u32 LE) || seed (32 bytes) || ident (16 bytes LE) ``` Both variants produce 81 header bytes, so ciphertext lengths are uniform across account types. @@ -436,11 +436,16 @@ All programs share the same function signature. They take as input: 3. A list of accounts, each annotated with metadata. 4. An instruction-specific data word list. -They output: +Programs are treated as blackboxes: given some inputs, they produce a `ProgramOutput` that represents their claimed state transition. All validation is performed on the output, never on the raw inputs. In privacy-preserving transactions the sequencer sees only a ZK proof of the circuit; the circuit cryptographically verifies each program output without having access to the program's inputs. In public transactions the program is executed directly, but the same output-based validation applies — this uniformity is intentional, so that the constraint rules do not differ between the two execution paths. -- A list of input account states echoed from the input (used for circuit verification). -- A list of accounts representing the post-execution state. -- Optionally, a list of chained calls representing other program execution calls. +`ProgramOutput` is the program's complete claimed state transition and contains: + +- `pre_states` — the accounts the program claims to have operated on, including their pre-execution state. +- `post_states` — the resulting account states after execution. +- `chained_calls` — queued executions of other programs. +- `self_program_id` and `caller_program_id` — used by the verifier to check that the output was produced by the expected program and invoked through the correct call chain. +- `instruction_data` — used to verify that each chained call was executed with the instruction the calling program requested. +- `block_validity_window` and `timestamp_validity_window` — range constraints on which blocks/timestamps this output is valid for. Formally: @@ -456,13 +461,26 @@ pub struct AccountPostState { claim: Option, } +pub struct ProgramOutput { + self_program_id: ProgramId, + caller_program_id: Option, + instruction_data: InstructionData, + pre_states: Vec, + post_states: Vec, + chained_calls: Vec, + block_validity_window: BlockValidityWindow, + timestamp_validity_window: TimestampValidityWindow, +} + /// A claim request indicating the executing program intends to take ownership of an account. pub enum Claim { - /// Ownership via user authorization (signature or nullifier key proof). + /// Standard claim path, used for all account kinds that are not self-owned PDAs. Succeeds + /// when the account's `is_authorized` flag is true (public accounts, public PDAs, private + /// PDAs), or unconditionally for standalone private accounts. Authorized, - /// Ownership via a PDA seed. - /// The AccountId is derived from (caller_program_id, seed) for public PDAs, - /// or from (caller_program_id, seed, npk, identifier) for private PDAs. + /// Ownership via a PDA seed. Only valid for PDAs owned by the executing program itself: + /// the AccountId must match the derivation from (self_program_id, seed) for public PDAs, + /// or from (self_program_id, seed, npk, identifier) for private PDAs. Pda(PdaSeed), } @@ -477,18 +495,13 @@ pub struct ChainedCall { type Program = fn( ProgramId, Option, List, InstructionData -) -> Result<(List, List, List)>; +) -> ProgramOutput; ``` -We will refer to Program parameters as: +The verifier validates that a `ProgramOutput` satisfies the following constraints: -- Input parameters: `self_program_id`, `caller_program_id`, `pre_states`, `instruction_data` -- Output values: `pre_states` (echoed), `post_states`, `chained_calls` - -All programs must satisfy the following constraints: - -1. Program receives a list of unique accounts as input. Each `AccountId` in the list is unique. -2. Program receives and outputs the same number of account states. `pre_states` and `post_states` must have the same length `N`. +1. The output's `pre_states` contain unique account IDs. Each `AccountId` in the list is unique. +2. The output's `pre_states` and `post_states` have the same length `N`. 3. Program cannot update an account's nonce. For all `i in 0..N`, `pre_states[i].account.nonce == post_states[i].account.nonce`. 4. Program cannot change the program owner of an account. For all `i in 0..N`, `pre_states[i].account.program_owner == post_states[i].account.program_owner`. 5. Program can only decrease the native token balance for accounts that the program owns. For all `i in 0..N`, if `post_states[i].account.balance < pre_states[i].account.balance`, then `pre_states[i].account.program_owner == executing_program_id`. @@ -595,10 +608,11 @@ impl AccountPostState { After execution, the runtime processes each post-state's optional `claim`: -- `Claim::Authorized` — sets `program_owner = executing_program_id`, but only if the account currently has `DEFAULT_PROGRAM_ID`. The authorization precondition depends on the account's kind: - - **Public account or private PDA:** the account must be authorized — i.e. its `is_authorized` flag must be `true`. Authorization comes from a signature (public) or from a caller's `pda_seeds` matching the PDA derivation under the appropriate npk (private PDA). - - **Standalone private account** (the `Regular` kinds — `PrivateAuthorizedInit`, `PrivateAuthorizedUpdate`, `PrivateUnauthorized`): the privacy circuit **does not enforce** the `is_authorized` precondition for `Claim::Authorized`. This is intentional: any party producing a valid update for a standalone private account already needs the corresponding `nsk` (to compute the update nullifier), so the authorization is implicit in the proof. The claim is therefore allowed unconditionally for these kinds, even when `pre_state.is_authorized == false`. -- `Claim::Pda(seed)` — sets `program_owner = executing_program_id` (when currently default) after verifying the account's ID matches the PDA derivation. For public accounts this is `AccountId::for_public_pda(executing_program_id, seed)`; for private PDAs it is `AccountId::for_private_pda(executing_program_id, seed, npk, identifier)` using the npk supplied for that pre-state. `Claim::Pda` is meaningless on a standalone private account and is not produced by well-behaved programs there. +- `Claim::Authorized` — the standard claim path: sets `program_owner = executing_program_id` (when currently default) for all account kinds except self-owned PDAs. Whether `is_authorized` is required depends on the account kind: + - **Plain public accounts:** `is_authorized` must be `true`, set when the transaction signer included the account in the authorized set. + - **Public and private PDAs:** `is_authorized` must be `true`, set when the caller included the matching seed in `ChainedCall.pda_seeds`. + - **Standalone private accounts** (`PrivateAuthorizedInit`, `PrivateAuthorizedUpdate`, `PrivateUnauthorized`): `is_authorized` is not enforced. For the authorized variants, possession of the `nsk` is already implicit proof of ownership; for `PrivateUnauthorized` the pre-state must be `Account::default()` (a fresh account), so there is no prior owner to protect. The claim is therefore allowed unconditionally for all Regular kinds. +- `Claim::Pda(seed)` — sets `program_owner = executing_program_id` (when currently default) by proving the account's ID is structurally derived from the executing program's own ID and the given seed, with no user authorization required. Unlike `Claim::Authorized`, the claim is not backed by a signature or nullifier key proof; instead, the program demonstrates ownership by construction: if the address was computed from `(self_program_id, seed)`, then no other program could have produced that same address. The derivation formula depends on the account kind: for public accounts it is `AccountId::for_public_pda(executing_program_id, seed)`; for private PDAs it is `AccountId::for_private_pda(executing_program_id, seed, npk, identifier)` using the npk supplied for that pre-state, making the claim user-specific. `Claim::Pda` is not applicable to standalone private accounts. ### Program-derived account IDs (PDAs)