Submitting, step by step
What one solver:submit run does under the hood, how to sign with an external wallet, and every way it can stop.
elgora-cli solver:submit --solver-address 0xYourWallet <bounty_id> ./artifactsThe command is one line. Underneath it are nine steps, each of which can refuse to continue. Every check exists so that neither Elgora's API nor a stale read can get your wallet to sign something you did not mean.
What happens
Resolve the pinned Guardian roster
The client asks the API for the bounty, then reads ElgoraHub directly for the
bounty's own pinned guardianRosterHash. It re-derives the hash from the
roster it was given — names, addresses, and encryption keys, in order — and
refuses to continue unless it matches the contract's pinned value exactly.
The API supplies the roster's contents; the contract decides whether they are the right contents. See The Guardian roster.
Check the bounty is submittable
Status must be open and the submission deadline must still be ahead. This
runs before any encryption work, so a doomed submission fails in a second
rather than after sealing 40 MB.
Seal the envelope locally
Your files are read, packaged, and encrypted with a fresh random AES-256 key. That key is wrapped once for each Guardian's public key, and the whole bag of wrapped keys is then time-locked to a public randomness beacon round that does not exist until your bounty's deadline has passed.
Nobody can open your Submission before then — not a Guardian, not Elgora, not the Poster. The mechanism is on Submission privacy.
Check the size
If the ciphertext exceeds 50 MiB the run stops here, before anything is uploaded. An oversized directory is caught earlier still, against the 37 MiB plaintext allowance, before any encryption work.
Get an upload target
A signed request exchanges the ciphertext's SHA-256 and byte length for a one-time upload URL. The client checks that the storage locator the API returned equals the one it derived itself; a mismatch means your configuration and the API disagree about the deployment, and it stops.
Upload ciphertext
Only ciphertext is uploaded. No plaintext artifact and no key ever leaves your machine.
Emits submit.ciphertext_uploaded.
Prepare the Submission
A second signed request sends the canonical envelope JSON. The API re-verifies
it: internal digests, the roster-recipients commitment against the bounty's
pinned roster, the reveal policy against the deadline, the storage locator,
the ciphertext's integrity against the hash inside the envelope, and that the
bounty is still open. Then it stores the record and returns the exact submit
call it believes you should send.
The client derives the submission_commitment itself from the envelope it
sealed, and stops if the API's differs.
Emits submit.submission_prepared.
Re-read the chain, then encode the transaction yourself
ElgoraHub is read again after preparation — status, deadline, pinned roster
hash — so a stale API response can never reach your wallet. The client then
encodes the submit calldata itself, from values it verified, against its own
copy of the contract interface, and compares field by field with what the API
returned. It signs its own encoding, never the server's bytes.
Record the commitment on-chain
submit(bounty_id, submission_commitment, guardian_roster_hash, 0x) from your
wallet. In external-wallet mode the client stops here and prints the exact
transaction instead.
Emits submit.submission_recorded with the transaction hash and block.
Signing with an external wallet
With --solver-address, no key enters the CLI process. You get two kinds of
prompt on stdout.
Request approvals, one per protected API call:
{"event":"submit.approval_required","solver":"0x…","typed_data":{"domain":{…},"types":{…},"primaryType":"ApiRequestApproval","message":{…}}}Sign that exact typed_data with the wallet at --solver-address and return
only the hex signature, on one line, on stdin. The scheme is documented in
Request authorization — read it before you
automate the signing.
The transaction:
{"event":"submit.transaction_prepared","chain_id":8453,"to":"0x…","value":"0","data":"0x…","function_name":"submit","args":{…}}Confirm your wallet is on the displayed chain and send that exact to,
value, and data. Do not substitute a different Solver, commitment, roster
hash, or extra_data.
Slow signers are handled
A hardware wallet or a multisig co-signer can take longer than the freshness window on a signed request. The client retries automatically with a freshly signed approval when the server reports the request has expired, so a slow approval is an inconvenience, not a failure.
Local signing — omit --solver-address, supply ELGORA_SOLVER_PRIVATE_KEY
through your host's secret manager — performs the identical checks and sends
the transaction for you. It is meant for controlled testing.
Where it can stop, and what it means
| Message | Meaning | What to do |
|---|---|---|
Bounty N is not open for submission (status: …) | The bounty already settled or timed out | Nothing to do |
Bounty N submission deadline has passed | Too late | Nothing to do |
Elgora API Guardian roster does not match its guardian_roster_hash | The roster the API returned is internally inconsistent | Stop and report it; do not encrypt to it |
…does not match this bounty's guardianRosterHash in ElgoraHub | The API's roster is not the one the contract pinned | Stop. Encrypting to it would produce a Submission no Guardian can open |
Artifact directory must be flat… / No files found… | Package rules — see Build the package | Flatten the directory; a challenge asking for a source/ folder does not change this |
Artifact directory holds N files and exceeds Elgora's maximum of 500 files | Too many files | Combine them into fewer files, if the challenge allows it |
Artifact directory exceeds Elgora's 37 MiB of files per Submission | Too big before sealing | Reduce the package |
Submission exceeds the maximum of 500 files. | A package built by a client that skipped the check | Rebuild within the limit; Guardians disqualify only this Submission |
Encrypted submission is N bytes and exceeds Elgora's 50 MiB limit | Too big after sealing | Reduce the package |
| Disqualified by a Guardian for extracted size | The Submission unpacks past 250 MB, or past a lower budget the challenge set | Nothing before decryption checks this — size the extracted content, not just the upload |
…returned a ciphertext locator that does not match the locally derived one | Your chain/Hub configuration does not match the API's deployment | Check --network and --api-base-url agree |
…returned a submission_commitment that does not match the locally sealed envelope | The API is describing a different Submission | Stop. Do not sign |
…returned a submit_transaction that does not match this submission | The prepared call disagrees with the verified values | Stop. Do not sign |
bounty_projection_unavailable | The read model has not caught up with a very recent on-chain change | The client retries with backoff automatically |
unauthorized | The signed request went stale, or the signature did not verify | The client re-signs and retries; if it persists, check your signer returns the signature for the exact typed data shown |
403 forbidden | The signing wallet is not the Solver named in the path | Sign with the wallet you passed to --solver-address |
Solver has no ETH on chain N | No gas | Fund the wallet |
Revert StaleGuardianRoster | The hash you submitted is not the one this bounty pinned — a bounty's pinned hash never changes, so this means the wrong bounty, API, or deployment | Check --network and --api-base-url against the bounty you meant, then re-run |
Revert DeadlinePassed / InvalidStatus | The window closed while you were signing | Nothing to do |
Revert SubmissionRejected | The deployment's submission guard declined the call. On Base mainnet each bounty admits up to 10 Solver accounts, so this bounty already has its full complement. Revising a submission you already made never hits this | Deployment policy; ask the operator |
Revert ContractPaused | The protocol owner has paused the Hub | Wait — see Limits and control |
An operational failure is never a protocol result
If upload, verification, or decryption fails somewhere, that is a blocker to resolve — never evidence about a Submission's quality. Guardians are held to the same rule from the other side: a Submission they cannot open does not become a Verdict against it.
Replacing a Submission
Run the command again before the deadline. The new commitment overwrites your old one, on-chain and in Elgora's storage. There is no second slot, and the replaced Submission stops being eligible immediately.
Re-read the challenge before replacing — and keep a record of the final
submission_commitment, because that is the value a Verdict and a delivery will
name.