Skip to content

initial implementation of the groth16 verifier - #4

Open
zz-sol wants to merge 20 commits into
solana-program:mainfrom
zz-sol:main
Open

zz-sol wants to merge 20 commits into
solana-program:mainfrom
zz-sol:main

Conversation

@zz-sol

@zz-sol zz-sol commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

#2 got merged/closed because of a forced push. reopen this PR

@zz-sol
zz-sol marked this pull request as ready for review September 17, 2026 12:18

@samkim-crypto samkim-crypto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, some comments from my pass through the program.

Comment thread README.md
| `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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 21236dd, and some clarification in d895da5

Comment thread README.md
Comment on lines +218 to +226
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 21236dd

if !payload.is_empty() {
return Err(ProgramError::InvalidInstructionData);
}
let [authority, payer, staging, key, _system] = accounts else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5575269

Comment thread README.md Outdated
| --- | ------------------- | ------------------------------------------------- | ----------------- | ----- |
| `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) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 21236dd

Comment thread program/src/lib.rs Outdated
Comment on lines +34 to +51
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),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e35b573

Comment thread program/src/error.rs Outdated
Comment on lines +3 to +6
//! 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b58e289

Comment thread program/tests/registry.rs
}

#[test]
fn republishing_fails_and_leaves_the_first_intact() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bef975b

accounts.push(payer.clone());
}

let result = self

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bef975b

Comment thread program/src/processor/publish.rs Outdated
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)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@zz-sol zz-sol Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b430308

@samkim-crypto samkim-crypto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting to dig into the verify library, but some initial comments below.

Comment thread groth16-verify/src/lib.rs
//!
//! let vk = VerifyingKey::from_body(key_body)?;
//! let (proof, public_inputs) = Proof::split_from(instruction_data)?;
//! verify(&vk, &proof, public_inputs)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@zz-sol zz-sol Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d895da5

Comment thread groth16-verify/src/lib.rs
mod proof;
#[cfg(feature = "verify")]
pub mod scalar;
#[cfg(feature = "verify")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 21236dd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants