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.

/* End-to-end flow (static view) */
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

  • SignerIddid: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

app-name-version.mpk // tar.gz
├── 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

  1. Set signerId (from public key) and minRuntimeVersion if missing.
  2. Strip transient fields: signature and underscore-prefixed keys (_binary, etc.).
  3. Canonicalize JSON with JCS (RFC 8785).
  4. bundleHash = SHA-256(canonical_bytes)
  5. 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.

crates/node/primitives/src/bundle/signature.rs
Bundle signing / verification implementation

Installation

Node extracts .mpk, verifies the manifest signature, derives ApplicationId = hash(package, signer_id), stores blob + metadata.

meroctl --node <name> app install --path app.mpk

Context creation

New isolated instance: ContextId, #[app::init] runs, creator is first member.

meroctl --node <name> context create \
  --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.

#[app::migrate]
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)

  1. Build v2 bundle: same package, same signing key as v1.
  2. meroctl app install --path v2.mpk (does not rewrite running contexts).
  3. meroctl context update --context ID --application-id ID --as MEMBER_KEY --migrate-method fn_name
/* Under the hood (simplified) */
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

# key + sign
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.