Docs

Submitting, step by step

What one solver:submit run does under the hood, how to sign with your wallet, and every way it can stop.

elgora-cli solver:submit <bounty_id> ./solution

Point it at a folder, whose contents and subfolders become the Submission, or at a single file. Underneath the one line 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 packing and sealing a large Submission.

Pack and seal locally

The client reads the bounty's challenge, checks it against the on-chain spec_commitment, and reads the protocol profile it declares: that decides the Submission format. It then checks every path, file type, count and size against the package rules, builds one .tar.gz of your files, and encrypts the archive's raw bytes 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

The encrypted object is the archive plus a 16-byte authentication tag, so the client knows its size as soon as the archive is built. If it would exceed 50 MiB the run stops before any encryption work, and nothing is uploaded. Your files are also checked against the 250 MB extracted ceiling before they are read.

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 file and no key ever leaves your machine.

Emits submit.ciphertext_uploaded with content_sha256, a private digest of your files that goes nowhere else. The Guardians and, if you win, the Poster see the same value when they open your Submission.

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. With --prepare-only the client stops here and prints the exact transaction instead.

Emits submit.submission_recorded with the transaction hash, the block, and content_sha256 again.

Signing with your wallet

The command signs two API requests and sends one transaction from your Solver account. With --signer manual --signer-address 0xYourWallet, no key enters the CLI process: it writes each request to a file and your own wallet tooling answers it. A local key or a Privy server wallet works too; see Wallets and signers. Each request is signed just before it is sent; the scheme is documented in Request authorization.

To send the transaction yourself, add --prepare-only:

{"event":"submit.calls_prepared","chain_id":8453,"from":"0x…","calls":[{"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 from from. Do not substitute a different Solver, commitment, roster hash, or extra_data.

Slow signers are handled

A signed request that goes stale before it reaches Elgora is signed again: the client retries automatically with a fresh approval when the server reports the request has expired.

Where it can stop, and what it means

MessageMeaningWhat to do
Bounty N is not open for submission (status: …)The bounty already settled or timed outNothing to do
Bounty N submission deadline has passedToo lateNothing to do
Elgora API Guardian roster does not match its guardian_roster_hashThe roster the API returned is internally inconsistentStop and report it; do not encrypt to it
…does not match this bounty's guardianRosterHash in ElgoraHubThe API's roster is not the one the contract pinnedStop. Encrypting to it would produce a Submission no Guardian can open
Elgora API served no verified challenge… / …does not match bounty N's spec commitment in ElgoraHubThe challenge the API served is missing or not the one the bounty committed toTry again later; never submit against unverified bytes
Bounty N declares a protocol profile this CLI does not knowThe bounty is newer than your clientUpgrade @elgora/cli and run again
…is a symbolic link / …is a FIFO / Folder "…" is empty / No files found…Package rules — see Build the packageReplace the entry with the file it stands for, add the missing files, or remove it
"…" is not a valid Submission pathA name breaks the name ruleRename it, if the challenge allows that name
…differ only in case / …needs it to be a folderTwo paths would collide once extractedRename one
The Submission holds more than 10000 files…Too many filesCombine them into fewer files, if the challenge allows it
The Submission holds N bytes of files and exceeds…Past the 250 MB extracted ceilingReduce the files
This Submission packs to N bytes encrypted and exceeds Elgora's 50 MiB…The archive is too big to storeReduce the files, or compress large results inside them
Disqualified by a Guardian for extracted sizeAn archive inside the Submission unpacks past 250 MB, or past a lower budget the challenge setSize your archives by what they unpack to; solver:check counts them only at their own size
…returned a ciphertext locator that does not match the locally derived oneYour chain/Hub configuration does not match the API's deploymentCheck --network and --api-base-url agree
…returned a submission_commitment that does not match the locally sealed envelopeThe API is describing a different SubmissionStop. Do not sign
…returned a submit_transaction that does not match this submissionThe prepared call disagrees with the verified valuesStop. Do not sign
bounty_projection_unavailableThe read model has not caught up with a very recent on-chain changeThe client retries with backoff automatically
unauthorizedThe signed request went stale, or the signature did not verifyThe client re-signs and retries; if it persists, check your signer returns the signature for the exact typed data shown
403 forbiddenThe signing wallet is not the Solver named in the pathCheck --signer-address names the wallet your signer signs for
Solver has no ETH on chain NNo gas on a local keyFund the wallet
Revert StaleGuardianRosterThe 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 deploymentCheck --network and --api-base-url against the bounty you meant, then re-run
Revert DeadlinePassed / InvalidStatusThe window closed while you were signingNothing to do
Revert SubmissionRejectedThe 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 thisDeployment policy; ask the operator
Revert ContractPausedThe protocol owner has paused the HubWait — 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 retrieve or decrypt does not become a Verdict against it. The exception is your own commitment: stored ciphertext that does not match the hash you committed disqualifies that Submission, while the bounty's other Submissions are judged as normal.

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.

Read this page as Markdown

On this page