examples/formats-registry/README.md
This example application maintains an on-chain registry that maps a
ModuleId
to an arbitrary blob of bytes — typically the BCS serialization of an application's
Formats value (i.e. its
serde_reflection registry plus the four top-level
formats for Operation, Response, Message and EventValue).
The intended consumer is the linera-explorer: when it observes an operation, message
or event for some application, it can look up the application's ModuleId in this
registry, fetch the registered bytes, and use them to decode raw BCS payloads as JSON
for display.
The application's operations all carry the owner on whose behalf they run:
enum Operation {
Write { owner: AccountOwner, module_id: ModuleId, blob_hash: DataBlobHash },
SetAdmins { owner: AccountOwner, admins: Option<Vec<AccountOwner>> },
}
Write is intentionally kept as the first variant with the exact fields of
linera_sdk::abis::formats_registry::Operation::Write,
so that the operation produced by linera publish-module-with-formats decodes
correctly here. Extra, implementation-specific admin commands (here SetAdmins) are
appended after it.
A module can be registered from any chain, but every mutation is ultimately applied on the application's creation chain, where a single admin policy is enforced:
runtime.check_account_permission(owner) to verify the declared owner really
signed (or is the caller of) the operation.Message.owner must be one of the admins;This mirrors the policy used by examples/controller. To bootstrap, an admin first
runs SetAdmins locally on the creation chain; afterwards, the listed admins can
register modules remotely from their own chains.
Write carries the DataBlobHash of an
immutable DataBlob holding the formats
description. The caller publishes that data blob (e.g. linera publish-module-with-formats emits a PublishDataBlob operation in the same block);
the contract asserts the blob exists and records its hash in a
MapView<ModuleId, DataBlobHash> keyed by module_id, after checking that no entry
exists yet — entries are immutable (first-write-wins; a ModuleId cannot be
overwritten). Only the 32-byte hash travels to the creation chain inside the Message,
so state stays compact (one hash per ModuleId) and identical values are
content-addressed and deduplicated by the blob layer.
The service exposes:
query { read(moduleId: "...") } — returns the bytes registered for moduleId, or
null if none. Internally it reads the stored hash and fetches the data blob.query { admins } — returns the configured admin accounts, or null if none.mutation { write(owner: "...", moduleId: "...", blobHash: "...") } — schedules a
Write operation (the data blob must be published separately).mutation { setAdmins(owner: "...", admins: ["..."]) } — schedules a SetAdmins
operation (pass null to clear the set).Before getting started, make sure that the binary tools linera* corresponding to
your version of linera-sdk are in your PATH. For scripting purposes, we also assume
that the BASH function linera_spawn is defined.
From the root of the Linera repository, this can be achieved as follows:
export PATH="$PWD/target/debug:$PATH"
eval "$(linera net helper 2>/dev/null)"
Next, start the local Linera network and run a faucet:
LINERA_FAUCET_PORT=8079
LINERA_FAUCET_URL=http://localhost:$LINERA_FAUCET_PORT
linera_spawn linera net up --with-faucet --faucet-port $LINERA_FAUCET_PORT
Create the user wallet and request a chain from the faucet:
export LINERA_WALLET="$LINERA_TMP_DIR/wallet.json"
export LINERA_KEYSTORE="$LINERA_TMP_DIR/keystore.json"
export LINERA_STORAGE="rocksdb:$LINERA_TMP_DIR/client.db"
linera wallet init --faucet $LINERA_FAUCET_URL
INFO=($(linera wallet request-chain --faucet $LINERA_FAUCET_URL))
CHAIN="${INFO[0]}"
OWNER="${INFO[1]}"
Build the WebAssembly binaries and deploy the application:
cd examples/formats-registry
cargo build --release --target wasm32-unknown-unknown
LINERA_APPLICATION_ID=$(linera publish-and-create \
../target/wasm32-unknown-unknown/release/formats_registry_{contract,service}.wasm)
Start a node service for the wallet:
PORT=8080
linera service --port $PORT &
echo "http://localhost:$PORT/chains/$CHAIN/applications/$LINERA_APPLICATION_ID"
Open the printed URL to land in a GraphiQL session connected to the registry.
To register a module's formats, first publish the formats description as a data blob
(e.g. linera publish-data-blob <FILE>, which prints the <BLOB_HASH>). The blob file
is the BCS serialization of the application's Formats; the extract-formats binary in
this example turns an app's SNAP snapshot into such a file ready for
linera publish-data-blob:
cargo run --bin extract-formats -- ../counter/tests/snapshots/format__format.snap counter-formats.bcs
Then bind the published blob to the module with the mutation below (replace
<MODULE_ID_HEX>, <OWNER> and <BLOB_HASH> accordingly; ModuleId, AccountOwner
and DataBlobHash are encoded as strings). <OWNER> must be the chain's signer; once
an admin set is configured it must also be one of the admins:
mutation {
write(
owner: "<OWNER>",
moduleId: "<MODULE_ID_HEX>",
blobHash: "<BLOB_HASH>"
)
}
To read it back:
query {
read(moduleId: "<MODULE_ID_HEX>")
}
A second write for the same moduleId will fail — registered entries are
immutable.