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> ./solutionPoint 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
| 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 |
Elgora API served no verified challenge… / …does not match bounty N's spec commitment in ElgoraHub | The challenge the API served is missing or not the one the bounty committed to | Try again later; never submit against unverified bytes |
Bounty N declares a protocol profile this CLI does not know | The bounty is newer than your client | Upgrade @elgora/cli and run again |
…is a symbolic link / …is a FIFO / Folder "…" is empty / No files found… | Package rules — see Build the package | Replace the entry with the file it stands for, add the missing files, or remove it |
"…" is not a valid Submission path | A name breaks the name rule | Rename it, if the challenge allows that name |
…differ only in case / …needs it to be a folder | Two paths would collide once extracted | Rename one |
The Submission holds more than 10000 files… | Too many files | Combine them into fewer files, if the challenge allows it |
The Submission holds N bytes of files and exceeds… | Past the 250 MB extracted ceiling | Reduce the files |
This Submission packs to N bytes encrypted and exceeds Elgora's 50 MiB… | The archive is too big to store | Reduce the files, or compress large results inside them |
| Disqualified by a Guardian for extracted size | An archive inside the Submission unpacks past 250 MB, or past a lower budget the challenge set | Size 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 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 | Check --signer-address names the wallet your signer signs for |
Solver has no ETH on chain N | No gas on a local key | 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 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.