Skip to main content

Contract identity

In Aster, every service contract has a deterministic, content-derived identity. The contract_id is the BLAKE3 hash of the contract's canonical byte representation. It is not assigned, not incremented, and not coordinated -- it is computed from the content itself.

Content-addressed identity

A contract_id is derived from the contract definition, not assigned by a registry or chosen by a developer. Two implementations of the same service interface -- whether written in Python, Java, Rust, or any other supported language -- produce the same contract_id if they define the same methods, types, and semantics.

This is the same principle behind content-addressed storage systems like Git (SHA-1 hashes of objects) or IPFS (CIDs). The content determines the identity. No coordination required.

contract_id = blake3(canonical_xlang_bytes(ServiceContract))

Canonical XLANG profile

For content addressing to work across languages, the serialized bytes must be identical regardless of which language produces them. Aster defines a constrained serialization profile called the canonical XLANG profile:

  • Fields emitted in ascending field ID order. Not the language's natural field order, not alphabetical -- a fixed, deterministic order based on field IDs.
  • Schema-consistent mode. No per-object type metadata in the payload. Types are known statically from the contract definition.
  • No reference tracking. Contract descriptors are acyclic trees (with self-references handled by a placeholder mechanism), so reference tracking overhead is unnecessary.
  • Standalone serialization. No session context, no shared caches, no state from prior objects. Each canonical byte sequence is self-contained.
  • No compression. Canonical bytes are stored and hashed uncompressed.
  • UTF-8 strings. All strings are encoded as UTF-8, even though Fory may normally choose LATIN1 or UTF-16 opportunistically.
  • NFC-normalized identifiers. Method names, type names, package names, and other identifiers are normalized to Unicode NFC form before encoding. This prevents different Unicode representations of the same visual string from producing different hashes.

With these constraints, a conforming Fory implementation in any language produces byte-identical output for the same contract definition.

Type graph: a Merkle DAG

Types reference each other by hash, forming a Merkle DAG (directed acyclic graph):

Each TypeDef is independently hashed. A MethodDef references its request and response types by hash. A ServiceContract references its methods. Changing any type in the graph changes its hash, which propagates upward through every contract that references it.

This gives you automatic change detection. If a type changes, every contract that uses it gets a new contract_id. Consumers looking up the old contract_id will find no providers -- a clean signal that the interface has changed, rather than a runtime deserialization error.

Self-references and cycles

Type graphs can contain self-references (e.g., a tree node type that references itself). The canonical encoding handles this with an SCC-based (strongly connected component) cycle-breaking algorithm. Self-referential types use a placeholder hash in place of their own hash when computing canonical bytes, ensuring the encoding terminates and remains deterministic.

ServiceContract

A ServiceContract is the top-level descriptor for a service:

FieldDescription
nameService name (e.g., "TaskManager")
versionInteger version number
methodsList of MethodDef entries, sorted by name
serializationDefault serialization modes for the service
scopeService scope: shared (stateless) or stream (session-scoped)

The contract_id is the BLAKE3 hash of the canonical XLANG serialization of this structure.

MethodDef

Each method in a service has a MethodDef:

FieldDescription
nameMethod name (e.g., "assign_task")
patternRPC pattern: UNARY, SERVER_STREAM, CLIENT_STREAM, BIDI_STREAM
request_typeBLAKE3 hash of the request TypeDef
response_typeBLAKE3 hash of the response TypeDef
idempotentWhether the method is safe to retry
timeout_msDefault timeout in milliseconds
capabilitiesOptional CapabilityRequirement for authorization

CapabilityRequirement

Methods can declare authorization requirements that the trust model enforces:

KindMeaning
ROLECaller must have a specific role attribute
ANY_OFCaller must have at least one of the listed capabilities
ALL_OFCaller must have all of the listed capabilities

These requirements are checked by framework interceptors against the attributes in the caller's CallContext, which are populated from their verified enrollment credential.

What contract_id enables

Version-safe discovery. A consumer looks up a contract_id in the registry. If the service interface has changed, the old contract_id returns no results. The consumer knows immediately that it needs an updated client, rather than discovering the mismatch at runtime through deserialization failures.

Compatibility detection. The registry can store compatibility reports between contract versions. A consumer can check whether its known contract_id is compatible with a provider's contract_id before attempting a call.

Cross-language interop verification. A Python service and a Java client that compute the same contract_id are guaranteed to be wire-compatible. The hash proves that both sides agree on the exact same method signatures, type definitions, and serialization semantics.

ContractManifest

When a contract is published to the registry, it is packaged as an iroh blob collection with a ContractManifest at index 0. The manifest is a JSON document that maps logical names to blob hashes:

MemberContentRequired
manifest.jsonContractManifest metadataYes
contract.xlangCanonical XLANG bytes of ServiceContractYes
types/{hash}.xlangCanonical XLANG bytes of each TypeDefYes
schema.fdlHuman-readable Fory IDL sourceNo
docs/Documentation bundleNo

The manifest also carries VCS provenance information (commit hash, repository URL) when available, linking the published contract back to its source. Consumers verify the contract by checking that blake3(contract.xlang bytes) == contract_id before trusting the bundle.