App Lifecycle
Signing, installation, migration
Lifecycle at a glance
From first bundle to upgraded context: generate a key, build WASM, manifest, sign, package .mpk, install, create a context; for upgrades use the same signing key, install the new bundle, then context update with a migration method.
1. Generate signing key (Ed25519 → did:key)
↓
2. Build app.wasm
↓
3. Create manifest.json → 4. Sign (mero-sign)
↓
5. Package .mpk (tar.gz)
↓
6. app install on node → 7. context create
↓
8. Next version: same key, new WASM → 9. app install + context update --migrate-method
Key concepts
- SignerId — did:key:z{base58btc(0xed01 || public_key_32)}; multicodec prefix 0xed01 marks Ed25519.
- AppKey — {package}:{signerId}; continuity of both fixes the logical app.
- ApplicationId — for signed bundles: hash(package, signer_id) (32 bytes). Same package + same signer → same id across versions.
- Bundle (.mpk) — tar.gz of manifest.json, app.wasm, optional abi.json.
Why signing?
Only the private key holder can ship a valid bundle; the node verifies at install. AppKey continuity blocks another signer from updating an existing application. Lose the key → no updates to that app line (no recovery in v0).
Bundle structure & manifest
├── manifest.json // signed metadata
├── app.wasm
└── abi.json // optional
Manifest fields (essentials)
version, package, appVersion, minRuntimeVersion, signerId, wasm, abi (optional), migrations (often []), signature.
Example manifest shape
"version": "1.0",
"package": "com.example.app",
"appVersion": "1.0.0",
"minRuntimeVersion": "0.1.0",
"signerId": "did:key:z6Mk…",
"wasm": { "path": "app.wasm", "size": …, "hash": null },
"abi": { … },
"migrations": [],
"signature": { "algorithm": "ed25519", … }
}
Critical package must match across v1/v2; both bundles must be signed with the same key.
Signing algorithm
- Set signerId (from public key) and minRuntimeVersion if missing.
- Strip transient fields: signature and underscore-prefixed keys (_binary, etc.).
- Canonicalize JSON with JCS (RFC 8785).
- bundleHash = SHA-256(canonical_bytes)
- Ed25519_Sign(private_key, bundleHash) → attach signature object (ed25519, base64url keys).
Install-time verification repeats canonicalization (without signature), hashes, verifies Ed25519, and checks derived signerId matches the manifest.
Installation
Node extracts .mpk, verifies the manifest signature, derives ApplicationId = hash(package, signer_id), stores blob + metadata.
Context creation
New isolated instance: ContextId, #[app::init] runs, creator is first member.
--application-id <APPLICATION_ID>
Migration anatomy
Use #[app::migrate] on a parameterless function. Read old bytes with read_raw(), deserialize an old-schema struct (field order must match Borsh layout), build and return the new state struct; the macro serializes it for the runtime.
pub fn migrate_v1_to_v2() -> NewState {
let old_bytes = read_raw().expect(…);
let old: OldState = BorshDeserialize::deserialize(&mut &old_bytes[..]).expect(…);
// map fields → NewState { … }
}
Deserialization: use BorshDeserialize::deserialize(&mut &bytes[..]) — not try_from_slice. Raw bytes can include trailing storage metadata; strict slice mode fails with "Not all bytes read".
When is a migration required?
- Yes Adding/removing/renaming fields, or changing field types on root state.
- No New methods only, or logic changes without schema change.
Deployment flow (upgrade)
- Build v2 bundle: same package, same signing key as v1.
- meroctl app install --path v2.mpk (does not rewrite running contexts).
- meroctl context update --context ID --application-id ID --as MEMBER_KEY --migrate-method fn_name
CLI → admin POST …/contexts/{id}/application
→ verify signerId matches existing app (AppKey continuity)
→ load new WASM → run exported migrate_* (read_raw, transform, return)
→ persist new root state (atomic; failure keeps old state)
CLI cheat sheet
cargo run -p mero-sign -- generate-key --output key.json
cargo run -p mero-sign -- sign manifest.json --key key.json
# node
meroctl --node <X> app install --path app.mpk
meroctl --node <X> context update --context <ID> --application-id <ID> \
--as <KEY> --migrate-method <fn_name>
Troubleshooting
Not all bytes read
Use deserialize(&mut &bytes[..]) instead of try_from_slice.
signerId mismatch
Sign both versions with the same key; keep package identical.
Missing signature
Run mero-sign sign on manifest.json before tarring the .mpk.