Build, Deploy, and Test
Most Lyquid development starts on a local Lyquor devnet. Use a single node while you are iterating on exported functions, state changes, and UI packaging. Move to a four-node devnet when the Lyquid's behavior depends on multiple hosts, such as UPC, oracle committees, or node-local instance state that differs across nodes.
Once the devnet is running, the workflow is the same across these setups. shaker builds the Lyquid
crate, makes the build artifact available to the nodes that need it, deploys or updates the Lyquid on
the local sequence backend, and can expose any hosted UI or HTTP routes through a local proxy.
Start with Quick Start if the LDK is not installed yet:
curl ldk.lyquor.dev -L | bash
The installer places the LDK, example Lyquids, shaker, ladle, and the Rust/Foundry toolchains
under ~/.shakenup.
Local Devnets
You can start local devnets either with Docker Compose or with native shell scripts. The Compose path
requires Docker Engine and
Docker Compose v2. The native shell scripts run the released
Lyquor binaries directly and require the LDK tools plus Foundry on your PATH.
Single-Node
The single-node Docker Compose setup starts one Lyquor node and a local
Anvil-backed sequence backend. It also bootstraps
bartender, the registry and control-plane Lyquid associated with that
sequence backend, so shaker deploy can register and look up Lyquids:
docker compose -f ~/.shakenup/ldk/docker/single/docker-compose.yaml up -d
The native single-node script exposes the same endpoint and keeps the node running until you press Ctrl-C:
~/.shakenup/ldk/scripts/localnet.sh single
Both single-node setups expose:
| Endpoint | URL |
|---|---|
| API | http://127.0.0.1:10087/api |
| WebSocket | ws://127.0.0.1:10087/ws |
Multi-Node
Use a four-node devnet when you need several Lyquor hosts.
Docker Compose starts a shared Anvil-backed sequence backend, four connected Lyquor nodes, a local OCI
registry, and one bartender registry Lyquid:
docker compose -f ~/.shakenup/ldk/docker/multi/docker-compose.yaml up -d
Both Compose setups start setup-devnet asynchronously; wait for it to finish before deploying. To
follow bootstrap, run docker compose -f <compose-file> logs -f setup-devnet.
The native shell scripts provide the same four-node setup without Docker. They run released lyquor
and shaker binaries directly on your machine and keep the network alive until you press Ctrl-C:
~/.shakenup/ldk/scripts/localnet.sh multi
The native multi script exposes the same node ports as Docker Compose. Instead of running a separate
registry container, it serves a local OCI registry endpoint from node 1 backed by that node's
image directory.
Both four-node setups expose:
| Node | API | WebSocket |
|---|---|---|
| Node 1 | http://127.0.0.1:10087/api | ws://127.0.0.1:10087/ws |
| Node 2 | http://127.0.0.1:11087/api | ws://127.0.0.1:11087/ws |
| Node 3 | http://127.0.0.1:12087/api | ws://127.0.0.1:12087/ws |
| Node 4 | http://127.0.0.1:13087/api | ws://127.0.0.1:13087/ws |
Stop a native devnet with Ctrl-C. Stop a Docker Compose devnet with the corresponding
docker compose ... down command.
agent-consensus is a bundled hosted UI demo that needs four nodes because it configures an oracle
committee across node-local agents. To run the whole flow, use its demo script:
~/.shakenup/ldk/lyquid-examples/agent-consensus/scripts/run.sh
The script starts the native four-node topology, deploys agent-consensus with shaker, activates
its oracle committee, and serves the hosted UI with shaker serve. The deploy step uses node 1's
local registry endpoint with a reference like
http://127.0.0.1:10087/lyquids:agent-consensus-local.
The script also requires jq and python3 and serves the UI at http://127.0.0.1:8080/.
Build and Deploy
To build and deploy a Lyquid, pass its Cargo.toml to shaker deploy. It compiles the crate, packages
a Lyquid pack, publishes it where the target Lyquor nodes can fetch it, and reports the resulting
Lyquid-* ID.
For local devnets, pass --endpoint explicitly. The examples use node 1's WebSocket endpoint, which
is also the single-node endpoint. LYQUOR_ENDPOINT is only used to keep the commands short:
export LYQUOR_ENDPOINT=ws://127.0.0.1:10087/ws
Here is a single-node deploy using the bundled hello example. For project layout and the template, see
Project Anatomy:
shaker deploy \
--endpoint "$LYQUOR_ENDPOINT" \
~/.shakenup/ldk/lyquid-examples/hello/Cargo.toml
Single-node deploys do not need an explicit registry reference; shaker can use the target node's
local registry path. For a multi-node devnet, pass --reference so the other local nodes know where
to fetch the built Lyquid. The value is an
OCI image reference, which in this local
workflow means a registry location plus a tag in the shape <registry>/<repo>:<tag>. In the examples
below, hello-local is just the local tag for the hello example; use a different tag for a
different project.
The repository and tag stay the same in both examples; only the registry location differs.
For Docker Compose multi-node, use the separate registry container:
export LYQUID_REFERENCE=http://127.0.0.1:8000/lyquids:hello-local
For native shell-script multi-node, use node 1's local registry endpoint:
export LYQUID_REFERENCE=http://127.0.0.1:10087/lyquids:hello-local
Then deploy with that reference:
shaker deploy \
--endpoint "$LYQUOR_ENDPOINT" \
--reference "$LYQUID_REFERENCE" \
~/.shakenup/ldk/lyquid-examples/hello/Cargo.toml
Most constructors need no extra input. If yours does, pass encoded bytes with --input. When using
the generated Ethereum-compatible ABI, cast abi-encode gives the expected encoding:
INPUT=$(cast abi-encode "constructor(string)" "Hello, World!")
shaker deploy \
--endpoint "$LYQUOR_ENDPOINT" \
--input "$INPUT" \
~/.shakenup/ldk/lyquid-examples/hello/Cargo.toml
Add --reference "$LYQUID_REFERENCE" to that command when deploying to a multi-node devnet.
The deploy output reports whether a Lyquid was created or updated, plus two identifiers. Save the
Lyquid-* ID; it is the stable ID you use with shaker console, shaker serve, updates, and other
Lyquor tooling. The hex address is backend-specific. In a local Anvil devnet, it is the current EVM
contract address, but most application workflows should track the Lyquid ID. To list deployed
Lyquids later, query the target node:
shaker list --endpoint "$LYQUOR_ENDPOINT"
Updating Deployed Code
To keep the same Lyquid ID while changing code, run shaker deploy with the crate's Cargo.toml and
--update; shaker rebuilds the crate and deploys the new pack:
shaker deploy \
--endpoint "$LYQUOR_ENDPOINT" \
--update <LYQUID_ID> \
path/to/Cargo.toml
Use --input on updates too when the new constructor needs arguments. Today, an update that changes
the Lyquid code digest starts from fresh state: network and instance state for that
Lyquid are reset, and the constructor initializes the new deployment state.
If the update only changes packaged UI/static assets and the Lyquid code digest stays the same, the runtime does not reset state and does not invoke the constructor again.
State-preserving code upgrades are planned through migration hooks. Those hooks will let new code read old persistent memory objects during migration, but they are not available yet. For the current state rules, see the memory model.
Serve a Hosted UI
A Lyquid can include a static frontend in its assets/ directory and can expose HTTP routes with
export = http; see External Access. Local devnets do not normally provide wildcard
DNS for per-Lyquid virtual hosts, so use shaker serve after deployment:
shaker serve <LYQUID_ID> --endpoint "$LYQUOR_ENDPOINT"
Open the local URL printed by shaker serve. The proxy forwards browser requests to the selected
node using the Lyquid host identity, so clients can use /lyquid/info, /lyquid/statusz,
/lyquid/api, /lyquid/ws, exported HTTP routes, and packaged static assets through that localhost
URL.
On a multi-node devnet, the endpoint selects which node serves the UI. This matters when the page reads instance state, calls node-local HTTP exports, or acts as a proposer in a UPC or oracle flow. To serve from node 2 instead of node 1, for example:
shaker serve <LYQUID_ID> --endpoint ws://127.0.0.1:11087/ws
Packaging
shaker build produces a local Lyquid pack: the artifact that nodes run and registries store.
shaker push uploads that pack to an OCI registry, while shaker deploy can either build and publish a
pack from a manifest or deploy one from an existing OCI reference. The pack contains the Lyquid WASM
layer, generated sequencing stub bytecode, metadata, and optional layers such as auxiliary sequencing
bytecode or hosted static assets. The local .lyquid.pack records the SHA-256 digest of its canonical
OCI manifest.
shaker packages static files from an assets/ directory next to the Lyquid manifest. Projects may
keep UI source elsewhere, but packaging reads assets/; agent-consensus uses build.rs to recreate
assets/ by copying its UI files and src/ tree from ui/. The node serves those files from the
Lyquid virtual host after HTTP export route matching. Asset lookup maps / to index.html, so a
single-page app can usually ship its production build output by placing it in assets/.
Build artifacts are written under lyquid_tools_target/ in the current working directory. The most
commonly inspected paths are:
| Artifact | Path |
|---|---|
| Raw WASM | lyquid_tools_target/wasm32-unknown-unknown/release/<crate>.wasm |
| Sequencing stub source | lyquid_tools_target/solidity/<crate>/<crate>.sol |
| Sequencing stub bytecode | lyquid_tools_target/solidity/<crate>/<crate>.bin |
| Lyquid pack | lyquid_tools_target/release/<crate>/lyquid.pack |
Use --debug on build or deploy when you want Cargo's dev profile. Debug artifacts are written under
the corresponding debug directories.
Inspect a local pack, a raw WASM binary, or an OCI reference with:
shaker inspect path/to/pack-or-wasm
Use inspection when you need to confirm metadata, image digests, or the constructor argument types
before encoding --input.
Testing and Debugging
Keep pure application logic in ordinary Rust functions and, from the Lyquid crate directory, unit-test it with the standard Rust toolchain:
cargo test
Use a single-node devnet for end-to-end checks that only need one host: constructor behavior,
Ethereum exports, HTTP exports, static assets, and basic state changes. Deploy the Lyquid, interact
with it through its generated Ethereum ABI or hosted routes, then confirm state through a read-only
exported function (ctx: &_) or HTTP endpoint.
Use a multi-node devnet for behavior that depends on several hosts: node-local instance state
differences, committee-based certification, UPC calls, or hosted UI flows where each node acts
differently. In those tests, deploy with a shared --reference and choose the serving or calling node
by changing --endpoint.
Use Lyquid console output for application-level traces:
lyquid::println!("handling request {}", request_id);
Stream that output from a running Lyquid with:
shaker console --endpoint "$LYQUOR_ENDPOINT" <LYQUID_ID>
Node logs depend on how you started the devnet. For Docker Compose, follow the node service:
# single-node Docker
docker compose -f ~/.shakenup/ldk/docker/single/docker-compose.yaml logs -f node
# multi-node Docker
docker compose -f ~/.shakenup/ldk/docker/multi/docker-compose.yaml logs -f node1 node2 node3 node4
For native shell scripts, set LOCALNET_WORK_DIR when you start the devnet if you want stable log
paths, then tail the node logs from another terminal:
# terminal 1
LOCALNET_WORK_DIR=/tmp/lyquor-localnet ~/.shakenup/ldk/scripts/localnet.sh multi
# terminal 2
tail -f /tmp/lyquor-localnet/node*/lyquor.log
To increase node verbosity, set LYQUOR_LOG=debug where your devnet starts:
- Docker Compose: add
LYQUOR_LOG=debugto theenvironmentfor thenodeservice in single-node, or for thenode1/node2/node3/node4services you want to inspect in multi-node, then recreate the containers. - Native shell scripts: prefix the script command:
LYQUOR_LOG=debug LOCALNET_WORK_DIR=/tmp/lyquor-localnet ~/.shakenup/ldk/scripts/localnet.sh multi
When debugging a hosted UI, separate host routing from asset packaging. First request a reserved
Lyquid path through the shaker serve URL:
| Check | Meaning |
|---|---|
/lyquid/info | Browser discovery metadata is reachable |
/lyquid/statusz | The selected node is hosting the Lyquid |
/lyquid/api | The per-Lyquid API route is alive |
If those paths work but / or /index.html returns 404, inspect the pack and the project's
assets/ directory first. If reserved paths fail, check that you are serving the right Lyquid ID, that
--endpoint points at a node hosting that Lyquid, and that the devnet is still running.