Delegated Execution Client Contract
A client that holds an account root and a device key, and runs no node of its own, reaches a context through somebody else’s node. This page is the contract for that: the exact bytes it signs, the exact shapes it sends, and which parts are live today.
It exists so a client can be built against a fixed target rather than against a guess. Every field here is quoted from the code that implements it, and anything still due to change says so where it appears.
| Contract | Status | Tracking |
|---|---|---|
| Discovery — where to write and as whom | Live | — |
| Warrant and delegated write | Live; account_heads signed but not enforced |
#3933 |
Session — challenge and LoginStatement |
Live | #3930 |
| Read — an authenticated account reads a context | Live | #3931 |
0. Primitives
Section titled “0. Primitives”Two conventions run through everything below.
Domain-separated hashing
Section titled “Domain-separated hashing”Every signature in Calimero is over a 32-byte digest produced by one function, so
a signature minted for one purpose can never be replayed as another. Length-prefixing
each segment is what stops ("ab", "c") and ("a", "bc") hashing alike.
pub fn domain_hash(domain: &[u8], parts: &[&[u8]]) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update((domain.len() as u64).to_le_bytes()); hasher.update(domain); for part in parts { hasher.update((part.len() as u64).to_le_bytes()); hasher.update(part); } hasher.finalize().into()}In other words: SHA256( u64le(len(domain)) ‖ domain ‖ ( u64le(len(part)) ‖ part )* ),
with every length a little-endian u64.
A client implementing this in another language must reproduce it byte for byte. It is the single most likely place for a cross-language client to go wrong, because a missing length prefix still produces plausible-looking digests that never verify.
Encoding on the wire
Section titled “Encoding on the wire”Signed structures cross the HTTP boundary as hex-encoded borsh, never as JSON. The canonical form of a signed statement is its borsh encoding; re-describing its fields as JSON would create a second spelling that could disagree with the bytes the signature covers.
Everything unsigned — request envelopes, arguments, responses — is JSON, camelCase.
1. Discovery — live today
Section titled “1. Discovery — live today”Before minting a warrant a client needs two facts it cannot derive: which node to
talk to, and the account and signing key the warrant must name for that node
(executor and executor_key).
Which relay, from the cloud:
GET /api/cloud/me/namespaces/{namespace_id}/relays{ "namespace_id": "…", "relays": [ { "peer_id": "12D3KooW…", "relay_url": "https://node-7.example.net", // where to send the intent "executor_account": "…", // what the warrant must name "authorship_ready": true, // may it author here yet "status": "active", "assigned_at": "…", "confirmed_at": "…", "last_seen_at": "…" } ]}Whether this context is open to delegation, from the node itself:
GET /admin-api/contexts/{context_id}/intents{ "executorAccount": "…", "executorKey": "…", "canAuthorOnBehalf": false, "groupId": "…", "grantedOnGroupId": null, "releaseBytecodeId": "…", "releaseVersion": "1.2.0"}releaseBytecodeId and releaseVersion are the release this context’s group names,
which a warrant pins (below). Read them here rather than from the application: the
relay refuses a warrant pinning any other release.
canAuthorOnBehalf: false is the default state of every context — authorship is
implied by neither membership nor admin, and is never granted implicitly. A client
should read this before signing anything and, when it is false, tell the user to ask
an admin of groupId for the grant rather than minting a warrant that will be
refused after the nonce is already spent.
A TEE relay (RelayTee) answers true by its role, with no grant. A TEE replica
(ReadOnlyTee) always answers false and no grant changes that: the namespace has
to admit its TEEs in relay mode.
See what the relay must be granted.
2. Warrant and delegated write
Section titled “2. Warrant and delegated write”What a warrant commits to
Section titled “What a warrant commits to”// crates/account/src/warrant.rs — live todaypub struct Warrant { pub context: ContextId, pub author_account: AccountId, pub author_device_key: PublicKey, pub executor: AccountId, pub executor_key: PublicKey, // the one relay device that may spend it pub release_bytecode_id: [u8; 32], // the release the relay must run pub release_version: String, // its semver, readability only; at most 256 bytes pub method: String, // in the clear pub intent_hash: [u8; 32], // H(method ‖ args) pub account_heads: Vec<[u8; 32]>, // at most 64 pub governance_floor: Vec<[u8; 32]>, // at most 64 pub nonce: u64, pub not_after: u64, pub signature: [u8; 64],}The arguments are committed to as a hash rather than carried; the method itself travels openly, so a peer can select a per-method write-set without reversing a hash against the app’s ABI:
intent_hash = domain_hash(b"calimero.warrant.intent.v1", &[method.as_bytes(), args])And the signature covers, in this order:
signature = sign(domain_hash(b"calimero.warrant.v2", &[ context.digest(), author_account.as_bytes(), author_device_key, // 32 bytes executor.as_bytes(), executor_key, // 32 bytes release_bytecode_id, // 32 bytes release_version.as_bytes(), // variable length method.as_bytes(), // variable length intent_hash, // 32 bytes &(account_heads.len() as u64).to_le_bytes(), ..account_heads, // 32 bytes each, in order &(governance_floor.len() as u64).to_le_bytes(), ..governance_floor, // 32 bytes each, in order &nonce.to_le_bytes(), ¬_after.to_le_bytes(),]))Each head list is preceded by its own count, and that is not decoration.
domain_hash length-prefixes every part it is given, so the heads are individually
unambiguous — but the two lists are adjacent, so without the counts
account_heads = [a, b], governance_floor = [] would hash identically to
[a], [b], and a relay could relabel which plane a head was cited from.
Submitting it
Section titled “Submitting it”POST /admin-api/contexts/{context_id}/intents{ "method": "set", "argsJson": { "key": "a", "value": "b" }, "warrant": "<hex borsh of Warrant>", "authorProof": "<hex borsh of AccountProof<DeviceCert>>"}The client supplies only its own half of the proof.
The executor’s proof and signing key are attached by the node from its own credentials, and the node refuses the warrant with a 403 unless its signing key is the executor_key the warrant names.
Naming one device is what makes a warrant spendable once: each relay keeps its own nonce ledger, so a warrant any device of the operator could present would be spendable once per device.
A relay that re-keys voids the warrants it has not spent yet.
// 200{ "data": { "rootHash": "…", // the context's scope root after the run "returns": { /* the method's own return value, or null */ } }}A method that ran and returned an error is not a 200. It answers 400 with the
error /jsonrpc reports for the same call: type and data are exactly the
object JSON-RPC puts under error, and error is its message for clients that
read only the admin API’s error shape. Nothing was committed or published, and
the warrant’s nonce is unspent: a nonce is spent only when the delta applies.
// 400{ "error": "function call error: <the method's message>", "type": "FunctionCallError", "data": "<the method's message>"}So returns: null always means the method returned nothing, never that it failed.
AccountProof<T> is the self-contained credential that ties a device key to an account:
pub struct AccountProof<T> { pub genesis: AccountGenesis, // hashes to the AccountId pub chain: Vec<RootKeyHandoff>, // root rollovers, epoch 0 upward pub statement: T, // here: DeviceCert}2b. Delegated context creation
Section titled “2b. Delegated context creation”A warrant writes into a context that exists. To create one, sign a
ContextCreationWarrant — the group, a 32-byte seed the node derives the context
id from, the application, optional service and name, and a hash of the init
arguments — and present it to a relay:
GET /admin-api/groups/{group_id}/context-intents?author={account}POST /admin-api/groups/{group_id}/context-intents// GET 200 — ask first; it signs nothing and spends nothing{ "data": { "executorAccount": "…", "executorKey": "…", "groupId": "…", "canCreateOnBehalf": true, "authorMayCreate": true } }
// POST body{ "warrant": "<hex borsh of ContextCreationWarrant>", "authorProof": "<hex borsh of AccountProof<DeviceCert>>", "initArgs": { /* the JSON init() receives */ } }
// POST 200{ "data": { "contextId": "…", "groupId": "…", "memberPublicKey": "…" } }init_hash = domain_hash("calimero.context-creation.init.v1", [init_args]), where
init_args is the JSON the node re-encodes from initArgs (compact, keys in the
order sent). The signing preimage is
domain_hash("calimero.context-creation-warrant.v1", parts) over, in order: group,
seed, author account, author device key, executor, executor key, application id,
then for each
of service_name and name a one-byte presence tag (00/01) followed by its
UTF-8 bytes (empty when absent), then init_hash, the u64le-counted account
heads and governance floor, u64le(nonce) and u64le(not_after).
The author needs admin or CAN_CREATE_CONTEXT in the group; the relay needs
only standing to act for members (a RelayTee, or CAN_AUTHOR_ON_BEHALF). The
nonce is spent in the new context’s ledger, so the device’s ordinary nonce
source is the right one. Refusals: 400 malformed, 403 not authorized (author,
relay, expiry, arguments, replay), 404 unknown group or application, 409 the
group targets a different application than the one signed. Vectors are under
Test vectors.
2c. Delegated governance
Section titled “2c. Delegated governance”The same pattern covers governance: adding the other person to a DM, creating a
channel’s subgroup, renaming it. Sign a GovernanceWarrant over one op in its
delegable form and present it with the op’s bytes:
GET /admin-api/groups/{group_id}/governance-intentsPOST /admin-api/groups/{group_id}/governance-intents// GET 200 — signs nothing, spends nothing{ "data": { "executorAccount": "…", "executorKey": "…", "groupId": "…", "canActOnBehalf": true } }
// POST body — every field hex borsh{ "warrant": "<GovernanceWarrant>", "authorProof": "<AccountProof<DeviceCert>>", "op": "<GroupOp, or RootOp, in its delegable form>" }
// POST 200 — the group the op acted on; the new subgroup for a creation{ "data": { "groupId": "…" } }The warrant’s kind names the plane (0 group op, published on {group_id};
1 root op, published on the namespace, so {group_id} is the namespace id) and
scope is that group. It commits to the op, not a description of it:
op_hash = domain_hash("calimero.governance-warrant.op.v1", [[kind], op_bytes]),
where op_bytes is exactly the op field. The signing preimage is
domain_hash("calimero.governance-warrant.v1", parts) over, in order: scope,
[kind], author account, author device key, executor, executor key, op_hash, the
u64le-counted account heads and governance floor, u64le(nonce) and
u64le(not_after).
The delegable form is the op as you mean it, with the fields only the relay
can compute cleared: a MemberRemoved/MemberLeft’s two expected hashes are zero
or empty, a GroupDeleted’s cascade lists are empty, and a
TargetApplicationSet’s bytecode_id is zero. The relay fills them in.
For GroupCreated, pick the new subgroup’s id at random and name yourself as
admin; the id is inside what you sign, so the relay keeps it.
Delegable: MemberAdded, MemberRemoved, MemberLeft, MemberRoleSet,
MemberCapabilitySet, DefaultCapabilitiesSet, SubgroupVisibilitySet, group,
member and context metadata, ContextDetached, context capabilities, a group’s
first TargetApplicationSet; and, as root ops, GroupCreated, GroupReparented,
GroupDeleted, and MemberJoinedOpen for yourself. A capability change may not
grant or withdraw CAN_AUTHOR_ON_BEHALF.
TargetApplicationSet is how a namespace you founded through a relay gets its
application, which a creation warrant needs (until then a creation is refused
with 409). Sign target_application_id, package and version; leave
bytecode_id zero, because it is the blob id of the bundle as the relay stores
it. The relay resolves package@version from its registry and refuses with 403
if that bundle is a different application than the one you signed (502 if its
registry has no such bundle). It is accepted only as the group’s first
application: on a group that already targets one it is refused with 403, before
anything is published, and every peer refuses it too. An upgrade is not something
a relay carries. You need admin or MANAGE_APPLICATION in the group.
Every peer applies the op as you, so your own role and capabilities decide:
MANAGE_MEMBERS to add or remove, admin to add an admin, CAN_CREATE_SUBGROUP
to create a subgroup. The relay needs only standing to act for members. A relay
that creates a subgroup for you is seated in it with that standing, and holds its
key, so it can serve it straight away. Nonces are spent per group and author
device. Refusals: 400 malformed, 403 not authorized (the reason is in the
body), 404 unknown group, 409 a subgroup id that already exists. Vectors are
under Test vectors.
3. Session — live
Section titled “3. Session — live”A device key obtains a session in two calls. Implemented by
#3930; enable the
account_proof provider on the node to use it.
Challenge
Section titled “Challenge”GET /auth/challenge{ "data": { "challenge": "<64 hex chars — 32 bytes>", "expires_at": 1789470000 // unix seconds; 60s from issue by default }, "error": null}The 32 bytes are expiry(8) ‖ nonce(8) ‖ tag(16), the tag an HMAC over the first two
under a key only the node holds. So the node stores nothing at issue time — which
matters, because this endpoint is unauthenticated and a per-request row would hand
an anonymous caller a write amplifier.
It is single-use, and spent only after a signature over it verifies. Burning it on presentation would let anyone who can see a challenge in flight invalidate it before its holder finishes signing.
404 means the provider is not enabled on this node.
LoginStatement
Section titled “LoginStatement”// domain calimero.auth.login.v1 · borsh · signed by the DEVICE keypub struct LoginStatement { pub node: PublicKey, // this node's identity, as the client pinned it pub audience: Audience, // WebOrigin(String) | CodeSigningId(String) | Cli pub challenge: [u8; 32], pub session_key: PublicKey, // the ephemeral key the session speaks with pub device_key: PublicKey, // what the signature is checked against pub issued_at: u64, pub expires_at: u64, pub signature: [u8; 64],}signature = sign(domain_hash(b"calimero.auth.login.v1", &[ node, // 32 bytes &audience_tag_and_bytes, // 1-byte variant tag, then the payload &challenge, // 32 bytes session_key, // 32 bytes device_key, // 32 bytes &issued_at.to_le_bytes(), &expires_at.to_le_bytes(),]))The audience tag is 0 for WebOrigin, 1 for CodeSigningId, 2 for Cli,
followed by the variant’s string bytes (empty for Cli). The tag is load-bearing:
without it a statement minted for the web origin "x" would verify as one minted
for the code-signing id "x".
The client learns the node’s identity from a pinned certificate, never from the
challenge — otherwise an attacker who can answer on the node’s behalf chooses what
the device signs about. That is the field’s whole purpose: a hostile relay can fetch
a real challenge from the target node and serve it to a user as its own, and only
node refuses the replay.
Exchange
Section titled “Exchange”The login goes through the node’s existing token endpoint, with
auth_method: "account_proof" — not a bespoke route. That is what gives a
device-key session the same refresh, revocation and rate limiting every other
session gets.
POST /auth/token{ "auth_method": "account_proof", "public_key": "<the session public key — must match the statement's session_key>", "client_name": "<the node URL, for node-binding the token>", "timestamp": 1789470000, "provider_data": { "challenge": "<64 hex chars>", "login_statement": "<hex borsh of LoginStatement>", "account_proof": "<hex borsh of AccountProof<DeviceCert>>" }}// 200{ "data": { "access_token": "<JWT; subject is the ACCOUNT, not the device>", "refresh_token": "<JWT>" }, "error": null}The session is granted to the account: the JWT’s sub is the account id, which
/auth/validate forwards to handlers as X-Auth-User.
Membership is not established by logging in. It is evaluated per request against the target context’s group — a relay serves several tenants, so a session must not carry a standing right to read.
4. Read — live
Section titled “4. Read — live”Implemented by #3931.
POST /admin-api/contexts/{context_id}/queryAuthorization: Bearer <session token>{ "method": "get", "argsJson": { "key": "a" }}// 200{ "data": { "returns": { /* the method's return value */ } } }The method runs as the caller: env::account_id() is the session’s account
and env::device_id() is the certified signing key of the device that opened the
session, resolved from the namespace’s bindings — the same pair a warrant for
that device would carry through /intents. So a view such as “whose turn is
it” or “who am I” answers for the caller, not for the relay executing the read.
A session that names no device runs on the relay’s key for that half only; the
account is always the caller’s.
Three refusals a client must handle distinctly, because they mean different things to a user:
| Status | Meaning | What the client should do |
|---|---|---|
401 |
no session, or it expired | re-run the login exchange |
403 |
the account is not a member of this context’s group | tell the user; do not retry |
409 |
the method is not read-only | a bug in the client’s method choice — this is a write, mint a warrant |
A read whose method returned an error answers 400 with the same body as
POST .../intents gives a method error (type: "FunctionCallError", the method’s
message in data), which is what /jsonrpc reports for the same call.
That last one is the one to get right. Reads are gated on the method being declared
read-only, and the gate fails closed: a method that declares nothing is refused
rather than guessed at. See #3932
for how that declaration is derived from the method’s receiver — which is why the
gate is usable at all, and why a client can call an ordinary &self method without
the app author having annotated anything.
Three things about this endpoint are worth knowing before building against it.
The account is never in the body. It comes from the session, so there is no
field a caller could set to read as somebody else. A session that is not
account-anchored — a node-owner or client-key token — gets 401 here, not because
it lacks permission but because this route answers “what may this account see”
and such a session names no account. A node owner reads through the ordinary
execute path.
Membership is re-checked on every call, against the group owning the context, never cached from the session. One relay serves several tenants, so a session that carried a standing right to read would keep serving somebody after the removal op the node has already applied. Inheritance resolves, so a subgroup member reads a context they hold through a parent.
No rootHash in the response, unlike the intent reply. That field answers “did
this change anything?” and for a read the answer is structurally no; returning it
would invite a client to watch it for changes that can never come.
5. Inventory — what this account can reach
Section titled “5. Inventory — what this account can reach”A client that runs no node still has to find out which namespaces and contexts it may read or write, or every id has to be handed to it out of band. Three routes answer that, all with the session token and nothing else:
GET /admin-api/namespaces → the namespaces this account is a member ofGET /admin-api/contexts → the contexts inside them (each with its groupId)GET /admin-api/contexts/{ctx} → one context, if this account is a memberAll three resolve the caller’s groups from the store on every request and return only what those admit. Membership is never read off the session: a namespace or context disappears from the listing when the removal op lands, not when the token expires. A context whose owning group cannot be resolved is excluded, not included — the listings fail closed.
GET /admin-api/contexts/{ctx} applies the same rule to a named id, so a context
the listing hides cannot be reached by guessing its id: the answer is 403, and
404 means the node genuinely does not hold it.
Node-owner sessions, and nodes running with no auth guard at all, keep the node-wide view of all three — narrowing there would empty the endpoints on every single-tenant node without closing anything.
Attachments
Section titled “Attachments”A delegated session uploads and reads blobs through a context, never by id
alone. Both routes take context_id, and for a session it is required:
PUT /admin-api/blobs?context_id={ctx}— refused403unless the account is a member ofctx’s group (resolved per request),413over 64 MiB. The node records the blob asctx’s.GET/HEAD /admin-api/blobs/{id}?context_id={ctx}— refused403unless the account is a member ofctx’s group. The blob is then served only if the node associates it withctx: it was uploaded forctx, or fetched fromctx’s peers (aGETfor a blob the relay does not hold does that fetch). Anything else is404, the same answer as a blob nobody holds, so a member of one context cannot probe for another’s.
So an app that uploads a blob and names it to its WASM later (as
blob_announce_to_context does) must pass context_id on the upload, in every
session: a blob uploaded without one is held for no context, so the announce
shares nothing.
6. End to end
Section titled “6. End to end”1. GET /api/cloud/me/namespaces/{ns}/relays → relay_url, executor_account2. GET /auth/challenge → challenge3. sign LoginStatement with the DEVICE key4. POST /auth/token (auth_method=account_proof) → session token5. GET /admin-api/namespaces → my namespaces GET /admin-api/contexts → my contexts, with groupId6. GET /admin-api/contexts/{ctx}/intents → executorKey, releaseBytecodeId, canAuthorOnBehalf? ├── false → stop; ask an admin of groupId for the grant └── true → continue7. READ POST /admin-api/contexts/{ctx}/query (token; no warrant)8. WRITE sign a Warrant with the DEVICE key POST /admin-api/contexts/{ctx}/intents (token + warrant + authorProof)Every step works today.
On a TEE relay, steps 6 and 8 can travel sealed to the relay’s attested key, so
the warrant and the arguments are readable only inside its TD. Attest with
POST /admin-api/tee/attest (bindTransportKey, includeCollateral), verify
the quote, then send those requests through /sealed/v2. On a relay that leaves
auth to a proxy (hosted nodes), the session steps (2–4) and reads (5, 7) cannot
be sealed. See how a client reaches a node.
Test vectors
Section titled “Test vectors”Check your implementation against these before pointing it at a node. They are asserted by the Rust tests named beside each block, so they cannot drift from the code without CI going red.
domain_hash
Section titled “domain_hash”crates/account/src/tests/domain.rs::domain_hash_has_known_answers
| domain | parts | digest |
|---|---|---|
"" |
— | af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc |
"ab" |
["c"] |
43ee655579de01ca739b3f95c1c2d3f46d353b2c0df818064ea594506cdb2617 |
"a" |
["bc"] |
9a8acca1b6c6c0befd3fbc756aed625da998c998f7252e738c4ef061906b9b21 |
Start with the first row. It is SHA-256 of eight zero bytes — the u64le
length of an empty domain — not SHA-256 of the empty string. If you get
e3b0c442…, you are omitting the length prefixes, and nothing below will match.
The last two rows are the whole reason the prefixes exist: both hash the letters
abc, and they must differ.
Warrant
Section titled “Warrant”crates/account/src/tests/warrant_wire_fixture.rs
Inputs: context = 0x11×32, author_account = 0x22×32, executor =
0x33×32, executor_key = 0x77×32, release_bytecode_id = 0x44×32,
release_version = "1.0.0", method = "set", args =
{"key":"k","value":"v"} (exactly those bytes, no whitespace), account_heads =
[0x55×32], governance_floor = [0x66×32], nonce = 42, not_after =
1700000000, author device secret = 0x07 seeded.
intent_hash dc066cc8524c74dc21714174009df536376e3151f5b92f0a676defde599dbae5signing preimage f38e2c9eb7e78025f8797bb34168fe48da95a20320fc62fc2da41e3efd30607cauthor_device_key ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22cThe full 392-byte encoding, field by field:
offset field bytes value 0 context 32 1111…11 32 author_account 32 2222…22 64 author_device_key 32 ea4a6c63… 96 executor 32 3333…33 128 executor_key 32 7777…77 160 release_bytecode_id 32 4444…44 192 release_version len 4 05000000 (5, little-endian) 196 release_version 5 312e302e30 ("1.0.0") 201 method len 4 03000000 (3, little-endian) 205 method 3 736574 ("set") 208 intent_hash 32 dc066cc8… 240 account_heads len 4 01000000 (1, little-endian) 244 account_heads[0] 32 5555…55 276 governance_floor len 4 01000000 (1, little-endian) 280 governance_floor[0] 32 6666…66 312 nonce 8 2a00000000000000 (42, little-endian) 320 not_after 8 00f1536500000000 (1700000000, little-endian) 328 signature 64 e42f753e…v1 was pure fixed-width concatenation — no length prefixes, no tags — and v2 gives
that up on purpose: release_version and method are Strings and the two head
lists are Vecs, so four u32 little-endian counts now sit inside the encoding. The
offsets above hold only for these inputs; a string of another length moves everything
after it.
Note the two widths. The counts inside the borsh encoding are u32; the counts
the signing preimage hashes (above) are u64. They are different serializations
of the same numbers and a builder that reuses one for the other produces a warrant
that is well-formed and verifies nowhere.
Every integer here is little-endian, in the encoding and in domain_hash alike.
That is the second most common cross-language mistake after the missing length
prefix.
Creation warrant
Section titled “Creation warrant”crates/account/src/tests/creation_wire_fixture.rs
Inputs: group = 0x11×32, seed = 0x12×32, author_account = 0x22×32,
executor = 0x33×32, executor_key = 0x77×32, application_id = 0x44×32,
service_name absent,
name = "general", init args = {"name":"general"}, account_heads =
[0x55×32], governance_floor = [0x66×32], nonce = 42, not_after =
1700000000, author device secret = 0x07 seeded.
init_hash 074dc4be8c7abe685532a48947430edd0301b42f60222e715a834e723d2a055esigning preimage 7e1126b215a794e565bb59c630cb1a36e43590c4bd8f62c6111792f77c231598author_device_key ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22cencoding 421 bytes; service_name encodes as 00, name as 01 07000000 67656e6572616csignature 19103d73752d052f747911b4b36e423221d89d120bf6f4d32122d3c4fd1fb030 9add40f3f3b80a5e5d6fefa02de5f4d6bb914c85c5c8b10a3a0e3a0e22c08400Governance warrant
Section titled “Governance warrant”crates/account/src/tests/governance_wire_fixture.rs
Inputs: scope = 0x11×32, kind = 1 (root), author_account = 0x22×32,
executor = 0x33×32, executor_key = 0x77×32, op bytes = 010203,
account_heads = [0x55×32],
governance_floor = [0x66×32], nonce = 42, not_after = 1700000000, author
device secret = 0x07 seeded.
op_hash d6bc121f9fcf7b85bea94d356d620c14dd8e1ae5fa2317cbcfc2c486cf04dfb3signing preimage b71a40cfbbe50e40d3423e8729a9ca8359e69f2762396922e6b314a971c5f720author_device_key ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22cencoding 345 bytessignature 03ce6c011f565924099b2c776032a1cfe540d1c4c09fad0b470723f8ee76138d 1d685a20948edf4dee2901d3f9187e3363a16e3b141b0bc94553f5948becae07Real ops, from delegated_governance_op_vectors_are_stable and
delegable_target_application_set_vector_is_stable in crates/governance-types:
MemberAdded { member: 0x44×32, role: Member } op 01 44…44 01 op_hash c48dce4ba9da20980a86832b133e7040ec67c293987c0f131140291a71041cd6 (kind 0)MemberRemoved { member: 0x44×32 }, delegable form op 02 44…44 00×32 00000000TargetApplicationSet { target_application_id: 0x88×32, package: "com.example.app", version: "1.2.3" }, delegable form (bytecode_id cleared) op 07 00×32 88…88 0f000000 636f6d2e6578616d706c652e617070 05000000 312e322e33 op_hash 904984c8f39e4172ea8864a65511faead68baa76af18d31e1d442a0b9fcb656b (kind 0)GroupCreated { group_id: 0x55×32, parent_id: 0x11×32, restricted: true, admin: 0x22×32 } op 00 55…55 11…11 01 22…22 op_hash 0ae00fae1b87e3b0f285246632ebe31e2e3b687a563a1f5299be997657dfd55f (kind 1)