Skip to content

Ordered log in depth

View Markdown ↗

ordered-log is Yano’s built-in append-only state machine for opaque events. It gives applications one threshold-finalized order, replicated history, provable message positions, and an optionally Cardano-anchored state root. It does not parse payloads or implement business rules.

The configured id is exactly ordered-log. Names such as orders-chain are chain ids chosen by the operator; they do not create an order-specific data model.

Use ordered-log when participants need to agree that an event was finalized at a particular position, while applications interpret the event body:

  • cross-organization event and audit logs;
  • order, shipment, case, or compliance histories;
  • immutable evidence and document-hash journals;
  • a shared event stream consumed by external services; and
  • notarization of arbitrary application bytes.

Choose another state machine when the chain must maintain or enforce business state. ordered-log does not enforce schemas, unique order ids, lifecycle transitions, balances, ownership, approvals, or application-level authorization.

These are independent concepts:

Concept Example Meaning
Chain id orders-chain Independent ledger, ordering, finality, state root, proofs, and optional anchor
State-machine id ordered-log Deterministic logic applied to finalized blocks
Topic order-created Caller-supplied label used for routing and filtering
Payload {"orderId":"A-1001"} Opaque bytes owned and interpreted by the application

One chain can carry many topics:

orders-chain
├── order-created
├── order-paid
├── order-shipped
└── order-cancelled

The state machine does not assign meaning to those topic names. Ordinary topics may be any valid UTF-8 value within the framework limit; names starting with ~ are reserved for Yano.

From an extracted JVM release directory (recommended for app chains):

Terminal window
./yano.sh start:devnet,appchain

This starts Yano’s built-in orders-chain as a single-member ordered log. The identity in config/application-appchain.yml is deterministic and intended only for local testing.

For the multi-node commands in the rest of this walkthrough, use an extracted Yano X showcase distribution:

Terminal window
./yano.sh appchain cluster start 3

The showcase also hosts orders-chain as an ordered-log chain. Submit through member 1:

Terminal window
./yano.sh appchain cluster submit orders-chain order-created \
'{"orderId":"A-1001","quantity":4}' \
--node 1

The command shape is:

./yano.sh appchain cluster submit <chain-id> <topic> <payload> [--node <index>]

--node 1 selects the ingress node. That member authenticates and signs the envelope, then gossips it to the cluster. It does not make node 1 the sequencer, state owner, or business-event processor.

The payload need not be JSON:

Terminal window
./yano.sh appchain cluster submit orders-chain notes \
'order A-1001 was checked manually'

The launcher submits command-line payloads as UTF-8 text. Use the REST bodyHex field or the Java client’s byte-array method for arbitrary binary payloads.

A single configured chain can select the machine directly:

yano:
app-chain:
enabled: true
chain-id: orders-chain
state-machine: ordered-log

The multi-chain form is:

yano:
app-chain:
chains[0]:
chain-id: orders-chain
state-machine: ordered-log
membership:
mode: governed
chains[1]:
chain-id: shipments-chain
state-machine: ordered-log
membership:
mode: governed

Both chains use the same implementation but remain independent. Each has its own blocks, pending pool, finality certificates, state root, membership, sequencing policy, storage, proofs, and optional L1 anchor.

Use multiple topics in one chain when the events share the same membership, finality, retention, anchoring, and operational lifecycle. Use separate chains when any of those boundaries should differ.

Direct Yano startup reads config/application-appchain.yml as a complete single-node configuration. The Yano X showcase launcher reads its packaged yano/config/application-appchain.yml and injects node-specific member keys, peer addresses, threshold, and proposer. Production deployments must supply those values through their generated per-node configuration and secret-management flow.

ordered-log has no machine-specific settings. It uses the common chain settings for capacity, latency, expiry, retention, sequencing, membership, and anchoring. For example:

yano:
app-chain:
chains[0]:
chain-id: orders-chain
state-machine: ordered-log
max-message-bytes: 65536
default-ttl-seconds: 600
max-ttl-seconds: 3600
block:
interval-ms: 1000
max-bytes: 4194304
max-messages: 5000
pool:
max-messages: 10000
retention:
enabled: true
keep-blocks: 1000

Keep consensus-affecting settings identical across members. When retention is enabled, eligible old message bodies below the confirmed anchor horizon may be stripped, while headers, ids, roots, and certificates remain so inclusion evidence is preserved. Archive bodies or evidence bundles separately when the original content must remain independently verifiable.

In the default local cluster, node indices 0, 1, and 2 use HTTP ports 7070, 7071, and 7072. This request is equivalent to CLI submission with --node 1:

Terminal window
RESPONSE=$(curl -sS -X POST \
http://127.0.0.1:7071/api/v1/app-chain/chains/orders-chain/messages \
-H 'Content-Type: application/json' \
-d '{
"topic":"order-created",
"body":"{\"orderId\":\"A-1001\",\"quantity\":4}"
}')
echo "$RESPONSE" | jq .
MESSAGE_ID=$(echo "$RESPONSE" | jq -r .messageId)

A successful submission returns HTTP 202:

{
"messageId": "<64 lowercase hex characters>",
"chainId": "orders-chain",
"topic": "order-created"
}

For arbitrary bytes, send hexadecimal data instead:

Terminal window
curl -sS -X POST \
http://127.0.0.1:7071/api/v1/app-chain/chains/orders-chain/messages \
-H 'Content-Type: application/json' \
-d '{"topic":"binary-event","bodyHex":"010203ff"}' | jq .

When REST authentication is enabled, also send X-API-Key. A topic-scoped API key can restrict which topics a caller may submit to, but that is an ingress policy rather than an ordered-log consensus rule.

The following client belongs to Yano X and is installed separately from the Yano host.

Use the lightweight yano-appchain-client artifact with the same version as the Yano nodes:

implementation "org.yanoproject:yano-appchain-client:${yanoVersion}"
import org.yanoproject.x.client.AppChainClient;
AppChainClient client = AppChainClient
.builder("http://127.0.0.1:7071/api/v1")
.chainId("orders-chain")
// .apiKey("secret") // when REST authentication is enabled
.build();
String payload = """
{"event":"order-created","orderId":"A-1001","quantity":4}
""".strip();
var submitted = client.submitText("orders", payload);
System.out.println(submitted.messageId());

Use client.submit(topic, byte[]) for arbitrary bytes or client.submitTyped(topic, value, encoder) with an application-owned JSON, CBOR, or protobuf encoder.

Submitting identical payload bytes twice normally creates two messages. The signed envelope also contains sender sequence and expiry data, so each submission has its own message id. Business-level idempotency, such as uniqueness by orderId, requires application logic or a custom state machine.

  1. The ingress node checks framework bounds, signs the envelope with its member key, retains it in the pending pool, and gossips it.
  2. The current proposer orders pending messages into an app block.
  3. Every voting member deterministically executes ordered-log and derives the same state root.
  4. The configured threshold certifies the block.
  5. Members commit the block, message index, state, and finality certificate.
  6. If anchoring is enabled, a later anchor commits the certified application root to Cardano.

HTTP 202 means the ingress node accepted the envelope; it does not mean the message is finalized. Confirm finalization before treating the event as committed:

Terminal window
until curl -sf \
"http://127.0.0.1:7070/api/v1/app-chain/chains/orders-chain/messages/$MESSAGE_ID" \
| jq .; do
sleep 1
done

Applications can also follow finalized messages through the SSE endpoint:

GET /api/v1/app-chain/chains/orders-chain/stream?fromHeight=0&topic=orders

The Java client exposes the same behavior through subscribe(...) and subscribeTyped(...).

For each finalized message, ordered-log writes:

sha256("~yano/finalized-message/v1/" || message-id)
-> cbor([schema-version, block-height, message-index, topic, sender])

It also maintains:

~tip -> cbor(block-height)

The message body remains in finalized block history and the message index; it is not duplicated in the state value. Use the typed proof subject to resolve a public message ID to the reserved physical state key and request its MPF proof:

Terminal window
curl -s -X POST \
"http://127.0.0.1:7070/api/v1/app-chain/chains/orders-chain/proof-subjects/finalized-message-v1/proof" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg id "$MESSAGE_ID" '
{coordinates:{"message-id":$id}, view:"latest",
claim:{claimId:"recorded",operands:{}}, includeEvidence:false}')" \
| jq '{stateRoot:.proof.stateRoot,presence:.proof.presence,position:.fact.fields,claim:.claimResult.satisfied}'

The lower-level state/proof/{keyHex} route accepts the resolved physical key, not the public message ID. The separate messages/{messageId}/proof route proves membership in the finalized block’s compact messagesRoot.

That proof binds the message’s finalized position, topic, and sender to the returned committed state root. For audit-grade verification, verify it against an independently trusted root, such as the matching Cardano anchor, rather than trusting a root supplied by the same node.

Other useful endpoints are:

GET /chains/{chainId}/messages/{messageId}
GET /chains/{chainId}/messages/by-topic/{topic}?fromHeight=0&limit=100
GET /chains/{chainId}/blocks?from=1&limit=100
GET /chains/{chainId}/evidence/{messageId}
GET /chains/{chainId}/status

All paths above are relative to /api/v1/app-chain.

The public submission path and default framework profile apply these constraints:

  • a non-empty body on REST and CLI submission;
  • body size at most 65,536 bytes by default (max-message-bytes);
  • topic size at most 256 UTF-8 bytes with no NUL character;
  • topics starting with ~ are reserved;
  • envelope signature and current member authorization;
  • pending-pool capacity and message expiry; and
  • structural, replay, block-size, and finality checks.

These checks protect the protocol. They do not validate a JSON schema or any business meaning. Do not put secrets or unnecessary personal data in payloads: every member receives the body and finalized history may be retained or exported. Encrypt application bodies before submission when confidentiality is required, and manage decryption keys outside consensus.

Use payload conventions when validation is external

Section titled “Use payload conventions when validation is external”

Applications may agree on a versioned envelope such as:

{
"schemaVersion": 1,
"event": "order-created",
"eventId": "evt-9001",
"orderId": "A-1001",
"quantity": 4
}

Producers and consumers can validate this schema without changing Yano. The chain still accepts other bytes, so this is appropriate only when business validation is deliberately outside consensus.

Topics let consumers filter one ordered history. They are useful for event families such as order-created, order-paid, and order-shipped, but the machine does not enforce a topic allow-list or a topic-specific payload shape.

Run multiple ordered-log chains when applications require distinct member sets, sequencing, retention, anchoring, or failure boundaries. Reusing the same state-machine implementation does not share state between chains.

Write a custom state-machine plugin for business rules

Section titled “Write a custom state-machine plugin for business rules”

Use a custom AppStateMachine when consensus must enforce rules such as:

  • strict payload decoding and bounds;
  • unique order ids;
  • allowed transitions such as CREATED -> PAID -> SHIPPED;
  • sender- or role-based authorization; or
  • committed current state keyed by orderId.

Package the implementation behind AppStateMachineProvider, a ServiceLoader entry, and a Yano plugin manifest. Deploy the identical bundle to every voting member of a JVM cluster and select its id with state-machine. Start with the Yano X plugin template.

Admission hooks improve feedback and keep malformed commands out of blocks built by honest proposers, but deterministic apply() logic remains the consensus authority. It must re-decode input and safely handle invalid or stale commands without external I/O, randomness, wall-clock reads, or other node-local behavior.

Do not switch an existing ordered-log ledger to incompatible application semantics in place. Use a fresh chain id/storage or a deliberately designed, versioned migration and activation plan.

Use consumers or effects for external actions

Section titled “Use consumers or effects for external actions”

ordered-log itself does not call webhooks, Kafka, an ERP, or another external system. A consumer can subscribe to finalized messages and perform idempotent off-chain work. When an external action and its outcome must participate in the committed workflow, use an effect-emitting stock/composite machine or a custom state-machine and executor plugin.

Never perform network, database, filesystem, or other external I/O from a state machine’s deterministic apply() method.