Errors
Error codes returned by each on-chain program.
Every program returns custom error codes via Anchor's #[error_code] macro. The codes start at 6000 and are assigned in declaration order; meanings are documented here. Declaration order can shift when a variant is added, so read the numbers from the program's IDL at the commit you build against rather than caching them across upgrades.
entros-anchor errors
| Code | Name | When |
|---|---|---|
| 6000 | InvalidCommitment | Submitted commitment is all zeros |
| 6001 | Unauthorized | Caller is not the identity owner |
| 6002 | ArithmeticOverflow | Internal arithmetic operation overflowed |
| 6003 | InvalidProtocolConfig | Protocol config account owner or layout is wrong |
| 6004 | InvalidIdentityState | Identity state account failed to deserialize |
| 6005 | IdentitySerializationFailed | Identity state account failed to serialize |
| 6006 | VerificationResultWrongOwner | Verification result account is not owned by the verifier program |
| 6007 | StaleVerificationResult | Verification result account predates the cross-program binding patch |
| 6008 | VerifierMismatch | Verification result was signed by a different authority than the one in the request |
| 6009 | ProofExpired | The verification result is older than MAX_PROOF_AGE_SECS (10 minutes) |
| 6010 | CommitmentMismatch | The proof's commitment_new does not match the submitted new commitment |
| 6011 | PrevCommitmentMismatch | The proof's commitment_prev does not match the identity's current commitment |
| 6012 | ResetCooldownActive | A reset_identity_state was attempted before the 7-day cooldown elapsed |
| 6013 | RebaselineCooldownActive | A rebaseline_anchor was attempted before its cooldown elapsed |
| 6014 | UnauthorizedNewWallet | Caller is not authorized by the old identity to claim a new wallet |
| 6015 | ProofFromFuture | The verification result's verified_at is in the future relative to the cluster clock |
| 6016 | MissingValidatorReceipt | mint_anchor expected a preceding Ed25519 validator-signed receipt instruction; none was found |
| 6017 | ReceiptValidatorMismatch | Mint receipt was signed by a key that does not match the registered validator pubkey |
| 6018 | ReceiptCommitmentMismatch | Mint receipt commitment does not match the mint_anchor commitment argument |
| 6019 | ReceiptWalletMismatch | Mint receipt wallet does not match the mint signer |
| 6020 | ReceiptExpired | Mint receipt has aged past MAX_RECEIPT_AGE_SECS (5 minutes) |
| 6021 | ReceiptFromFuture | Mint receipt validated_at is in the future relative to the cluster clock |
| 6022 | MalformedReceiptMessage | Mint receipt message has malformed length, layout, or cross-instruction references |
| 6023 | IdentityStateNotFound | The expected IdentityState PDA was not supplied or does not exist |
| 6024 | ValidatorNotConfigured | No validator pubkey is registered in ProtocolConfig |
The Receipt* family (6016–6022) only fires on the first-verification path. Re-verification (update_anchor) binds via the verifier-program VerificationResult PDA, not via the validator-signed mint receipt.
entros-verifier errors
| Code | Name | When |
|---|---|---|
| 6000 | InvalidProofFormat | Proof bytes don't decode as a valid Groth16 proof |
| 6001 | ProofVerificationFailed | On-chain pairing check failed |
| 6002 | ChallengeExpired | The associated challenge passed its expiry window |
| 6003 | ChallengeAlreadyUsed | The challenge was already consumed by a successful verify_proof |
| 6004 | InvalidPublicInputs | Public input count, encoding, or value bounds violated |
| 6005 | ChallengeNotUsed | A close_challenge was attempted on a challenge that has not been consumed |
| 6006 | InvalidNonce | The challenge nonce is all zeros |
| 6007 | ArithmeticOverflow | Internal arithmetic operation overflowed |
entros-registry errors
| Code | Name | When |
|---|---|---|
| 6000 | InsufficientStake | Validator stake is below min_stake |
| 6001 | ValidatorAlreadyRegistered | Validator state already exists for this authority |
| 6002 | ValidatorNotActive | Validator state is registered but not currently active |
| 6003 | Unauthorized | Caller is not the expected authority for this instruction |
| 6004 | ArithmeticOverflow | Internal arithmetic operation overflowed |
| 6005 | InsufficientTreasuryBalance | Withdrawal amount exceeds the treasury's available balance |
| 6006 | ProgramDataBytes | ProgramData account bytes are malformed or shorter than expected |
| 6007 | WrongUpgradeAuthority | Caller is not the program's upgrade authority |
| 6008 | InvalidProtocolConfig | Protocol config bytes are malformed |
| 6009 | InvalidValidatorPubkey | Validator pubkey supplied to set_validator_pubkey is the zero pubkey |
entros-voter-weight errors
| Code | Name | When |
|---|---|---|
| 6000 | InvalidRealmAuthority | Caller signing the registrar instruction is not the realm authority |
| 6001 | MissingIdentityAccount | Voter's identity account was not supplied via remaining_accounts |
| 6002 | InvalidIdentityPda | Identity account address does not match the expected PDA derivation |
| 6003 | InvalidIdentityOwner | Identity account is not owned by the Entros Anchor program |
| 6004 | InvalidIdentityData | Identity account data is shorter than the expected layout |
| 6005 | InsufficientTrustScore | Voter's score is below the registrar's min_trust_score |
| 6006 | VerificationExpired | Voter's last verification is older than max_verification_age |
| 6007 | VoterWeightRecordRealmMismatch | Voter weight record realm does not match the registrar |
| 6008 | VoterWeightRecordMintMismatch | Voter weight record mint does not match the registrar |
| 6009 | VoterWeightRecordOwnerMismatch | Voter weight record owner does not match the voter authority |
| 6010 | RealmHasNoAuthority | The realm has no authority configured; registrar updates require one |
| 6011 | GoverningTokenOwnerSignerMismatch | The instruction's governing_token_owner does not match the signer |
| 6012 | InvalidMaxVerificationAge | max_verification_age must be greater than zero |
| 6013 | InvalidMaxVoterWeight | max_voter_weight must be greater than zero |
SDK and HTTP errors
@entros/pulse-sdk returns a SubmissionResult from every submission helper:
type SubmissionResult = {
success: boolean;
txSignature?: string;
attestationTx?: string;
error?: string;
// 4.1.0
failedAt?: VerificationPhase;
opaque?: boolean;
baselineRecovery?: BaselineRecoveryReason;
portableBaseline?: boolean;
};On success, txSignature is the verification transaction signature. attestationTx, when present, is the SAS attestation write. On failure, error carries the user-surfaceable message.
Route on the phase, not on the message
failedAt names the stage that failed. Read it instead of matching words in
error, which is what the SDK's own hosts used to do and what produced two
mislabelled failures in production: an on-chain revert rendered as a validation
rejection, because the matcher for a validator rejection also matched
custom program error.
import { phaseChargesAttempt } from "@entros/pulse-sdk";
const result = await session.complete(wallet, connection);
if (!result.success) {
switch (result.failedAt) {
case "capture":
case "extraction":
return promptRecapture();
case "validation":
return showValidationOutcome(result);
case "baseline":
return showBaselineRecovery(result.baselineRecovery);
case "signing":
return showWalletCancelled();
case "submission":
case "confirmation":
return showChainOutcome(result);
default:
return showGenericError(result.error);
}
}The eight phases are capture, extraction, validation, baseline,
proving, signing, submission, confirmation.
Two rules across the wallet seam, because adapters merge signing and sending
into one call and the outcome there is genuinely ambiguous. Only a declined
prompt is signing, and only a failure the cluster reported is confirmation.
Everything else, both SDK timeouts included, is submission, which says the
outcome is unknown rather than claiming either way. phaseSpend returns
none, possible or certain for the same reason.
Metering attempts
If you cap attempts, cap them on phaseChargesAttempt. Only validation
evaluated whether a person was there, so only validation may charge for one.
Charging every failure means three declined wallet prompts hard-fail someone
whose capture passed each time.
if (phaseChargesAttempt(result.failedAt) && !isClientOriginReason(result.reason)) {
attemptsUsed += 1;
}Do not describe an opaque failure
opaque: true means the cause must not be shown. A replay-floor rejection in
proving, an attack-signal rejection in validation and a program revert in
confirmation have to render identically, or the difference tells an attacker
which layer caught them.
You may still match a specific, user-actionable condition first, exactly as the
Custom 6011 and Custom 6012 examples below do. The flag decides only what to
show when nothing matched: render your generic rejection copy, not error.
Chain-side transaction failures
Since 1.5.0, the SDK reports a failure when an on-chain transaction reverts. Earlier versions silently returned success: true for transactions that were included on chain but failed during execution, an easy way to ship a "verified!" UI for a tx that mutated nothing. The error preserves the JSON InstructionError shape so callers can extract the Custom code.
Since 4.1.0 these arrive with failedAt: "confirmation" and opaque: true. Match the codes you act on first, then fall back to your generic rejection copy rather than to error:
const result = await session.complete(wallet, connection);
if (!result.success && result.failedAt === "confirmation") {
// result.error is one of:
// "Transaction failed on chain: {\"InstructionError\":[0,{\"Custom\":6011}]} (sig=...)" // PrevCommitmentMismatch
// "Transaction failed on chain: {\"InstructionError\":[1,{\"Custom\":6016}]} (sig=...)" // MissingValidatorReceipt
// "Transaction failed on chain: {\"InstructionError\":[0,\"InsufficientFundsForRent\"]}"
// ...or a non-chain failure (network, wallet rejected, etc.)
const customMatch = result.error?.match(/"Custom":(\d+)/);
if (customMatch) {
const code = Number(customMatch[1]);
if (code === 6012) showCooldownMessage();
else if (code === 6011) showStaleBaselinePrompt();
else showOpaqueRejection();
} else {
showOpaqueRejection();
}
}/attest requires wallet ownership proof
A request to the executor's /attest endpoint now requires wallet_address, nonce, signature, and message together on every call. Missing any of those returns:
{ "error": "wallet_ownership_required", "_padding": "..." }with HTTP 400. Omitting wallet_address is a different case: the request fails JSON extraction before the handler runs and returns HTTP 422 with a plain-text body, so match on the status code rather than the error field. The signed message format is Entros-ATTEST:{wallet_address}:{timestamp_secs}. The walletless attestation path was removed. Wallets that cannot sign should not call /attest at all.
Response padding
Most executor JSON responses include an opaque _padding field of repeated ASCII characters that pads the body toward a 256-byte floor. Bodies already over that floor ship an empty _padding, and /status is not padded at all. Treat it as forward-compatible noise: ignore it on parse, do not surface it to users, do not assert on its content or length. It exists to defeat byte-length analysis of response classes.
Where to look next
- Reference: Programs—instruction documentation that defines when each error fires
- Reference: SDK—submission helper signatures
- Reference: Changelog—when each error variant was introduced