Skip to content

authenticated-map value validation

Each authenticated-map collection chooses a value encoding and, optionally, a validator: a declarative schema or a plugin. These choices are compiled into genesis and are identical on every member. This page covers validation, the offline preflight, and every result code.

Start with the authenticated-map guide for the collection model, operations, configuration and submission.

SettingDefaultEffect
valueEncodingopaqueAny byte string within the value bound
validatornoneNo schema or plugin check
authorizationopenAny member’s message may write
restoreAllowedfalseRevoked entries stay revoked

Validation is optional. Opting into an encoding, schema or plugin changes consensus validity and therefore the genesis id.

Where a value is checkedIllustrative data

What you're seeing: a product value on its way into the products collection, which requires canonical CBOR and the product-v1 schema. Try a “What if” to see where a bad value stops.

  1. Preflight Before submitting, check the value offline against the exact genesis. The result is advisory: ACCEPTED, REJECTED with a code, or UNAVAILABLE when the collection uses a plugin the CLI cannot run.
    ./yano.sh appchain state validate --genesis-file product-registry/config/authenticated-map-genesis.hex --collection products --key 736b752d31 --value-file product.cbor
    • passes: The collection exists in genesis
    • passes: Key and value fit the collection bounds
    • passes: The value has the collection’s encoding
    • passes: The collection’s schema or plugin accepts the value
  2. Ingress The receiving member runs the same value rules as admission. Every rule passes, so it answers 202 and gossips the command.
    • passes: Topic is authenticated-map.command.v1
    • passes: Canonical command within the genesis byte limit
    • passes: Known collection; key and value within its bounds
    • passes: Encoding, then schema, then plugin validator
    • passes: Authorization kinds match the collection genesis
  3. Candidate block A command is checked again for the height of the block that would include it. Here that matters for member collections: the sender must be a member at that height.
    • passes: For a member collection, the sender is a member at height h
  4. Apply Every member applies the command: authorization and status rules first, then encoding, schema and validator once more. This is the authoritative check. The receipt records APPLIED with the new revision.
    • passes: Authorization and status rules
    • passes: Encoding, then schema, then plugin validator

What if: Breaks the schema

The value says status: "unknown"; product-v1 allows only active, held or retired.

  1. Preflight Preflight evaluates the compiled schema and reports REJECTED with code 11, VALUE_SCHEMA.
    ./yano.sh appchain state validate --genesis-file product-registry/config/authenticated-map-genesis.hex --collection products --key 736b752d31 --value-file product.cbor
    • passes: The collection exists in genesis
    • passes: Key and value fit the collection bounds
    • passes: The value has the collection’s encoding
    • fails: The collection’s schema or plugin accepts the value VALUE_SCHEMA (11)
  2. Ingress Submitted anyway, the value fails the same schema at admission. REST keeps only a safe code: 400 APPLICATION_REJECTED. Nothing is pooled and no receipt will exist.
    • passes: Topic is authenticated-map.command.v1
    • passes: Canonical command within the genesis byte limit
    • passes: Known collection; key and value within its bounds
    • fails: Encoding, then schema, then plugin validator APPLICATION_REJECTED
    • not reached: Authorization kinds match the collection genesis

What if: Not canonical CBOR

The same map with its keys out of canonical order.

  1. Preflight Canonical CBOR requires map keys in canonical order. Preflight reports REJECTED with code 10, VALUE_ENCODING; the schema is never reached.
    ./yano.sh appchain state validate --genesis-file product-registry/config/authenticated-map-genesis.hex --collection products --key 736b752d31 --value-file product.cbor
    • passes: The collection exists in genesis
    • passes: Key and value fit the collection bounds
    • fails: The value has the collection’s encoding VALUE_ENCODING (10)
    • not reached: The collection’s schema or plugin accepts the value
  2. Ingress Admission applies the encoding rule before the schema and refuses the command with 400.
    • passes: Topic is authenticated-map.command.v1
    • passes: Canonical command within the genesis byte limit
    • passes: Known collection; key and value within its bounds
    • fails: Encoding, then schema, then plugin validator APPLICATION_REJECTED
    • not reached: Authorization kinds match the collection genesis

What if: A plugin collection

The showcase gtins collection uses the gs1-gtin-v1 plugin. 95012345 has a wrong check digit.

  1. Preflight The CLI has no adapter for a custom plugin, so it reports UNAVAILABLE (code 12). That never means accepted.
    ./yano.sh appchain state validate --genesis-file <genesis.hex> --collection gtins --key <hex> --value-hex <hex>
    • passes: The collection exists in genesis
    • passes: Key and value fit the collection bounds
    • passes: The value has the collection’s encoding
    • not reached: The collection’s schema or plugin accepts the value UNAVAILABLE (12)
  2. Ingress Every member runs the genesis-pinned plugin at admission. It rejects the check digit, so the member answers 400 APPLICATION_REJECTED.
    • passes: Topic is authenticated-map.command.v1
    • passes: Canonical command within the genesis byte limit
    • passes: Known collection; key and value within its bounds
    • fails: Encoding, then schema, then plugin validator APPLICATION_REJECTED
    • not reached: Authorization kinds match the collection genesis

What if: An unknown collection

Codes 1 and 2 come only from preflight. On a node the same faults are refused before any receipt.

  1. Preflight The collection is not in genesis. Preflight reports REJECTED with code 1, UNKNOWN_COLLECTION.
    ./yano.sh appchain state validate --genesis-file product-registry/config/authenticated-map-genesis.hex --collection products --key 736b752d31 --value-file product.cbor
    • fails: The collection exists in genesis UNKNOWN_COLLECTION (1)
    • not reached: Key and value fit the collection bounds
    • not reached: The value has the collection’s encoding
    • not reached: The collection’s schema or plugin accepts the value
  2. Ingress Admission refuses a command naming an unknown collection, so no receipt can ever carry code 1.
    • passes: Topic is authenticated-map.command.v1
    • passes: Canonical command within the genesis byte limit
    • fails: Known collection; key and value within its bounds APPLICATION_REJECTED
    • not reached: Encoding, then schema, then plugin validator
    • not reached: Authorization kinds match the collection genesis
  • your tooling
  • member node
  • deterministic state machine
  • refused

The value rules run in a fixed order: the collection’s bounds, then the encoding, then the schema, then the plugin. A value refused at ingress gets HTTP 400 with APPLICATION_REJECTED, is not pooled, and has no receipt.

Genesis commits the initial membership, so supply every member public key when you initialize:

Terminal window
./yano.sh appchain init --non-interactive \
--recipe authenticated-map --network preprod --members 3 \
--member-key <member-0-64-hex> \
--member-key <member-1-64-hex> \
--member-key <member-2-64-hex> \
--name product-registry --chain-id product-registry \
--output product-registry

For a disposable local devnet, use --network devnet and replace the --member-key options with --generate-local-member-keys. It keeps the private keys in the project’s owner-only secrets/ directory and pins only the public keys. On public networks, operators always supply their own keys.

The generated project starts with one open, opaque records collection. Edit spec.chains[0].authenticatedMap in appchain.yaml, then render again:

authenticatedMap:
profile: mpf-blake2b256-v1
# All-zero is the initializer's development/no-policy placeholder.
# Replace it with the reviewed 32-byte commitment before an anchored chain
# generation is created.
anchorPolicyCommitment: "0000000000000000000000000000000000000000000000000000000000000000"
maxBatchItems: 128
maxBatchBytes: 65536
collections:
- id: attachments
authorization: owner
restoreAllowed: false
maxKeyBytes: 64
maxValueBytes: 1048576
valueEncoding: opaque
- id: canonical-events
authorization: member
maxKeyBytes: 64
maxValueBytes: 16384
valueEncoding: canonical-cbor
- id: products
authorization: owner
maxKeyBytes: 64
maxValueBytes: 4096
valueEncoding: canonical-cbor
validator: product-v1
schemas:
- id: product-v1
root: product
source: |
product = {
sku: tstr .size (1..32),
quantity: uint .le 1000000,
status: "active" / "held" / "retired",
? note: tstr .size (0..256)
}

Run the renderer and doctor after every edit:

Terminal window
./yano.sh appchain render product-registry
./yano.sh appchain doctor product-registry --distribution /path/to/yano-x-jvm-<version>

Doctor reports authenticated-map-schema-encoding. A collection with a schema must use canonical-cbor; opaque plus a schema is rejected before genesis is written.

The generated outputs include:

  • config/shared-consensus.yaml, with the exact genesis and state-identity settings the nodes use;
  • config/authenticated-map-genesis.hex, the canonical genesis for offline tooling; and
  • docs/VALUE_VALIDATION.md, with project-specific validation commands.
  • opaque checks only the byte length. Use it for formats that are already canonical, encrypted payloads, large attachments, or record shapes that will change during the chain’s life.
  • canonical-cbor accepts exactly one bounded, deterministic CBOR item. It rejects indefinite lengths, non-minimal integers, duplicate or misordered map keys, trailing bytes, invalid UTF-8, and unsupported tags or simple values. Equal logical values then have equal bytes and equal value hashes.
  • A schema is written in cddl-yano-subset-v1. Devtools compile it to canonical yano-cbor-schema-ir-v1, and nodes evaluate the IR; they never parse CDDL. It supports exact text-keyed maps, bounded arrays, integer ranges, bounded text and byte strings, literals, choices and optional fields. External references, recursion, unbounded repetition, regular expressions and host-dependent extensions are rejected.

Schemas are exact: undeclared map fields are rejected. A schema is immutable for the chain generation, so leave generous bounds for fields that may grow.

The commitment profiles are mpf-blake2b256-v1 and jmt-blake2b256-v1. The Poseidon JMT profile is unavailable until ADR-025 Phase 4 and its pinned ZeroJ dependency are complete; blueprints refuse it.

Inspect the collections and validators committed by genesis:

Terminal window
./yano.sh appchain state validators \
--genesis-file product-registry/config/authenticated-map-genesis.hex

The result shows the profile, format fingerprint, genesisId, each collection’s encoding and bounds, each validator’s kind and contract version, the schema and parameter SHA-256, and a plugin’s artifact-closure SHA-256.

Validate a candidate from a file or canonical lowercase hex:

Terminal window
./yano.sh appchain state validate \
--genesis-file product-registry/config/authenticated-map-genesis.hex \
--collection products --key 736b752d31 --value-file product.cbor
./yano.sh appchain state validate \
--genesis-file product-registry/config/authenticated-map-genesis.hex \
--collection products --key 736b752d31 \
--value-hex a363736b7565736b752d316673746174757366616374697665687175616e7469747905

The answer is ACCEPTED, REJECTED with a code, or UNAVAILABLE. UNAVAILABLE means the collection uses a plugin that the offline CLI cannot run; it never means accepted. All three are advisory: every node validates again.

./yano.sh appchain state explain --code <0..12> explains codes 0 to 12.

Preflight returns codes 0 to 12. A receipt carries the code of the first check that rejected a command that reached apply.

CodeNameMeaningWhere it appears
0NONEApplied without an errorreceipt
1UNKNOWN_COLLECTIONThe collection is not in genesispreflight only; a node refuses it at admission
2COLLECTION_BOUNDSKey or value exceeds the collection’s boundspreflight only; a node refuses it at admission
3UNAUTHORIZEDThe sender does not satisfy the collection’s authorization, or a transfer outside an owner collectionreceipt
4ALREADY_EXISTSPUT_IF_ABSENT on an active entryreceipt
5ABSENTThe operation needs an existing entryreceipt
6REVOKEDThe entry is a tombstone and the operation is not RESTOREreceipt
7ACTIVERESTORE on an active entryreceipt
8PRECONDITIONExpected revision or value hash did not matchreceipt
9RESTORE_FORBIDDENThe collection does not allow restorereceipt
10VALUE_ENCODINGThe encoding rejected the valuepreflight; normally refused at admission
11VALUE_SCHEMAThe schema rejected the valuepreflight; normally refused at admission
12VALUE_VALIDATORThe plugin rejected the value; UNAVAILABLE in preflight without an adapterpreflight; normally refused at admission
13AUTHORIZATION_ASSIGNMENTA mutation’s authorization kind or policy differs from its collection’s genesisreceipt
14UNKNOWN_POLICYThe direct-role policy has no current revisionreceipt
15POLICY_INACTIVEThe direct-role policy’s current revision is not activereceipt
16ACTOR_INELIGIBLEActor or organization not current and active, role missing, or key not active at this heightreceipt
17ACTOR_SIGNATUREThe actor’s signature does not verifyreceipt; normally refused at admission with INVALID_SIGNATURE
18AUTHORIZATION_DEADLINEIssued in the future, past its deadline height, or longer-lived than the policy allowsreceipt
19DIRECT_AUTHORIZATION_REPLAYThe actor’s authorization id was already consumedreceipt
20APPROVAL_NOT_APPROVEDThe referenced proposal is missing, not approved, or past its deadline heightreceipt
21APPROVAL_MISMATCHThe approved proposal is for another action, payload or policy revisionreceipt
22APPROVAL_REPLAYThe proposal was already consumedreceipt
23CAPACITY_EXCEEDEDDefined in the contract; the map does not produce it in this releasenone
24CRYPTO_WORK_EXCEEDEDThe block’s signature-work budget for governed evidence is used upreceipt
25GOVERNED_ROUTE_UNSUPPORTEDA governed mutation on a chain whose genesis has no governed configurationreceipt
26WRONG_GENESISThe evidence was signed for another chain, genesis or actionreceipt
27WRONG_REVISIONThe evidence names a policy revision that is not currentreceipt

Only a command that reached apply has a receipt. A preflight rejection, or a command refused at admission or in candidate-block validation, has none and makes no finality claim. In a declarative composite, a rejected map command rejects the cascade with the code MAP_<n>.

The client can decode genesis and reuse the encoding and schema checks without the runtime:

byte[] genesisBytes = HexFormat.of().parseHex(Files.readString(
Path.of("config/authenticated-map-genesis.hex")).strip());
var preflight = AuthenticatedMapPreflight.fromEncodedGenesis(genesisBytes);
var mutation = AuthenticatedMapContract.Mutation.put("products", skuBytes, canonicalProductCbor);
var result = preflight.validate(mutation);
if (result.accepted()) {
var action = AuthenticatedMapAuthoring.action(AuthenticatedMapContract.Command.single(mutation),
List.of(new AuthenticatedMapAuthorizationContract.AuthorizationAssignmentV1(
0, AuthenticatedMapContract.AUTH_OWNER, "", 0)));
stock.authenticatedMapGovernedCommand(AuthenticatedMapAuthoring.command(action, List.of()));
}

The chain admits only this action-and-evidence envelope; the client’s older authenticatedMapMutate(mutation, preflight) overloads send a mutation-only encoding that it refuses.

A custom plugin needs an application-supplied adapter, chosen from the exact genesis descriptor:

var preflight = AuthenticatedMapPreflight.fromEncodedGenesis(genesisBytes, descriptor -> {
if (!descriptor.providerId().equals("gs1-gtin-v1")) {
return Optional.empty();
}
verifyPinnedArtifactClosure(descriptor.definition());
return Optional.of((collection, key, value) -> applicationGs1Check(key, value));
});

Returning Optional.empty() gives UNAVAILABLE, not acceptance. The adapter is a convenience: a client can skip it, so the genesis-pinned validator always runs on the nodes.

Schemas cover record shape. Use a plugin only for a deterministic, self-contained rule over (collectionId, applicationKey, value) that the CDDL subset cannot express. Through the SPI, a plugin cannot read state, sender, height, membership, clock, randomness, environment, files or network.

Java bytecode is not sandboxed. A plugin can still call JDK APIs directly, so validators are trusted consensus code: review them, allow-list them, test them, and pin them by their exact ARTIFACT_CLOSURE digest. Never load untrusted code.

The descriptor lives in the no-SPI contracts artifact; the factory SPI lives in core-api. Programmatic genesis uses:

var validator = AuthenticatedMapContract.ValidatorDescriptor.plugin(
"gtin-v1",
"gs1-gtin-v1",
exactArtifactClosureSha256,
canonicalCborParameterMap);
var collection = new AuthenticatedMapContract.CollectionDescriptor(
"products",
AuthenticatedMapContract.AUTH_OWNER,
false,
64,
4096,
AuthenticatedMapContract.VALUE_ENCODING_CANONICAL_CBOR,
validator.id());
var genesis = AuthenticatedMapGenesisFactory.mpf(
config,
anchorPolicyCommitment,
128,
65536,
List.of(collection),
List.of(validator),
List.of());

The first-party appchain-authenticated-map-validators module provides gs1-gtin-v1 as a worked example for GTIN-8, GTIN-12, GTIN-13 and GTIN-14. Production resolution also requires the bundle in the runtime allow-list with catalog digest mode ARTIFACT_CLOSURE.

Blueprints do not expose custom plugin descriptors in v1alpha1, so a generated project never implies that an arbitrary bundle is safe or installed. Build plugin genesis programmatically, or stay with canonical encoding and schemas.

Value validation sees one key and one value. A rule that reads another key, relates collections, checks membership or emits effects belongs in a composite component or a custom state machine.

Validation proves that committed bytes satisfy the declared encoding and rule. It does not prove the record is true, keep it confidential, or replace authorization.

In v1, encodings, schemas, plugins and their parameters are immutable for the chain generation. Changing one needs a new chain generation and an explicit import plan. For a record shape likely to evolve, prefer canonical-cbor without a validator.