Conversation
… arkworks, registry and malformed-input tests
| | `3` | `Verify` | vk PDA (r) | none | `proof ‖ public_inputs`; the hot path | | ||
| | `4` | `CloseStaging` | authority (s,w), staging (w) | stored authority | Refunds staging rent. Canonical accounts cannot be closed | | ||
|
|
||
| ### Registration |
There was a problem hiding this comment.
It would be good to document how an application should distribute the matching proving key and how users can check that it corresponds to the registered verifying key. I think keeping the proving key off-chain makes sense, but the example workflow should cover obtaining it so users can generate proofs independently.
| One client-side rule keeps the staging account private: **`create_account` and | ||
| `InitializeStaging` go in the same transaction.** `InitializeStaging` can only | ||
| check that the account is program-owned and blank; it has no way to know who | ||
| paid for it. An account created in one transaction and initialized in the next | ||
| can be initialized by a third party in between, who then owns its rent through | ||
| `CloseStaging`. The subsequent `Write`s may be spread over as many transactions | ||
| as the key needs. Small keys fit `create_account ‖ InitializeStaging ‖ Write ‖ | ||
| Publish` in one transaction; large ones take several, with no window in which a | ||
| third party can affect the outcome. |
There was a problem hiding this comment.
Could we add a runnable registration-and-verification example, with a helper that constructs staging account creation and initialization together? It would be useful to show chunked uploads, publication, and a consuming program checking the expected key address before verification.
| if !payload.is_empty() { | ||
| return Err(ProgramError::InvalidInstructionData); | ||
| } | ||
| let [authority, payer, staging, key, _system] = accounts else { |
There was a problem hiding this comment.
I might be misunderstanding how Pinocchio handles MAX_ACCOUNTS, but is this check actually needed? It looks like the entrypoint only deserializes five accounts, so any additional accounts would be discarded before reaching this check anyway.
| | --- | ------------------- | ------------------------------------------------- | ----------------- | ----- | | ||
| | `0` | `InitializeStaging` | authority (s), staging (w) | authority | Data `num_public_inputs: u16`, rejected if `n > 151`. Requires the account be owned by the program, uninitialized, and exactly `40 + 448 + 64·(n+1)` bytes. Writes the header. **Must be in the same transaction as the `create_account` that made the staging account** — see [Registration](#registration) | | ||
| | `1` | `Write` | authority (s), staging (w) | stored authority | Data `offset: u32 ‖ bytes`. `offset` is relative to the **body**; the write must satisfy `offset + len ≤ body_len` with overflow-checked arithmetic. The header is never writable | | ||
| | `2` | `Publish` | authority (s,w), payer (s,w), staging (w), vk PDA (w), system | stored authority | No instruction data. Validates the staging body, derives the canonical PDA from `sha256(body)`, brings it into existence at its exact final size, copies the body, writes the header — all in one instruction. Closes staging, refunding its rent to authority (which is why authority is writable) | |
There was a problem hiding this comment.
Publish requires both the authority and payer to sign, but the “Signer” column seems to only list the authority. Should we include the payer here too?
| pub fn process_instruction( | ||
| program_id: &Address, | ||
| accounts: &mut [AccountView], | ||
| data: &[u8], | ||
| ) -> ProgramResult { | ||
| let (&tag, payload) = data | ||
| .split_first() | ||
| .ok_or(ProgramError::InvalidInstructionData)?; | ||
| match Tag::from_u8(tag).ok_or(ProgramError::InvalidInstructionData)? { | ||
| Tag::Verify => processor::verify::process(program_id, accounts, payload), | ||
| Tag::InitializeStaging => { | ||
| processor::initialize_staging::process(program_id, accounts, payload) | ||
| } | ||
| Tag::Write => processor::write::process(program_id, accounts, payload), | ||
| Tag::Publish => processor::publish::process(program_id, accounts, payload), | ||
| Tag::CloseStaging => processor::close_staging::process(program_id, accounts, payload), | ||
| } | ||
| } |
There was a problem hiding this comment.
Nit: I think moving this process_instruction function to processor/mod.rs and just keeping lib.rs focused on declarations and entrypoint setup seem more natural to me. This is more of a stylistic preference, so I will leave it up to you though.
| //! Verification and layout errors from [`solana_groth16_verify`] are exposed as | ||
| //! `ProgramError::Custom(code)` with the codes below, so a CPI caller can tell | ||
| //! "proof did not verify" apart from "malformed input". Registry-level | ||
| //! failures use the standard `ProgramError` variants that describe them. |
There was a problem hiding this comment.
Nit: The module introduction says registry failures use standard ProgramError variants, but several use custom codes 100–104. Could we maybe distinguish standard account/signature failures from custom registry failures?
| } | ||
|
|
||
| #[test] | ||
| fn republishing_fails_and_leaves_the_first_intact() { |
There was a problem hiding this comment.
Maybe we can add tests that pass a published key account to Write, InitializeStaging, and CloseStaging, asserting rejection and unchanged account state? Published-key immutability is central to Verify’s security assumptions, so I think it would be useful to cover it explicitly.
| accounts.push(payer.clone()); | ||
| } | ||
|
|
||
| let result = self |
There was a problem hiding this comment.
Could we add a test using process_transaction_instructions for CreateAccount + InitializeStaging, including rollback when initialization fails? The current instruction-chain helper executes each instruction in a separate transaction context, whereas clients must submit these two instructions atomically.
| let vk = VerifyingKey::from_body_with_len(body, n).map_err(map_groth16)?; | ||
|
|
||
| // --- 2. validate every point ------------------------------------------------ | ||
| vk.validate_for_publish().map_err(map_groth16)?; |
There was a problem hiding this comment.
It might be better to perform the key-address and existing-account checks before validate_for_publish(). That would avoid the expensive point validation for duplicate publications or an incorrect target address.
| //! | ||
| //! let vk = VerifyingKey::from_body(key_body)?; | ||
| //! let (proof, public_inputs) = Proof::split_from(instruction_data)?; | ||
| //! verify(&vk, &proof, public_inputs)?; |
There was a problem hiding this comment.
Could we make the key-validation precondition explicit here and the crate-level example? VerifyingKey::from_body only checks the layout, so the example should either show validate_for_publish when accepting a key or explain where the key has already been validated.
| mod proof; | ||
| #[cfg(feature = "verify")] | ||
| pub mod scalar; | ||
| #[cfg(feature = "verify")] |
There was a problem hiding this comment.
I am wondering if we should expose the account-size helpers without requiring the verify feature? An instruction-only client still needs staging_account_len to create the staging account, and shouldn’t need the host verification dependencies just to calculate its size. But maybe we can address this in a follow-up.
#2 got merged/closed because of a forced push. reopen this PR