Gotchas & Sharp Edges
These are the sharp edges that bite — each one a concrete, non-obvious fact grounded in the code, not folklore. They are the things you only learn after a replica diverges, a write silently vanishes, or a test passes locally and fails in production. Skim the whole page once before you build; the values here (limits, defaults, merge rules) are the kind you reach for at 2am. Deeper treatments live in Advanced SDK, Storage Complexity, Permissioned Storage, and the encryption protocol.
Determinism & execution
Section titled “Determinism & execution”fetchis hard-disabled at the host boundary (a compile-timeconst BLOCKED: bool = truegate) — it always fails, so there is no outbound HTTP from app code.- There is no gas or fuel metering. Execution is bounded only by
VMLimitsplus the wasm stack; exceeding any limit TRAPS the whole call — there is no partial commit and no refund. - A guest panic (or trap) is caught and surfaced as an error; the state delta from that call is never applied — the context
root_hashis left unchanged, so a panicked write is as if it never ran. env::commitmay be called at most once per execution; a second call is rejected (the host tracks acommit_calledflag).
VMLimits & wire limits
Section titled “VMLimits & wire limits”- Default
VMLimits: 64 MiB linear memory (1024 pages of 64 KiB), 200 KiB stack, storage key 1 MiB, storage value 10 MiB, logs1024 × 16 KiB, events100 × 16 KiB(event-kind string 100 bytes), xcalls100 × 16 KiB(function name 100 bytes), wasm module 20 MiB, method name 256 bytes. - Only
max_logsandmax_log_sizeare operator-overridable via config; every other limit is compiled in, so changing it means recompiling the runtime. - RPC execution arguments are capped at 10 MB JSON (
MAX_ARGS_JSON_SIZE); a signed governance-op payload is capped at 64 KiB (MAX_SIGNED_GROUP_OP_PAYLOAD_BYTES). - A sync snapshot page is capped at 4 MiB or 1000 entities, whichever comes first; a single entity’s data is capped at 1 MiB.
- Two distinct DAG-head caps exist: governance DAG heads
MAX_GOVERNANCE_DAG_HEADS = 32, snapshot DAG headsMAX_DAG_HEADS = 100.
Identity
Section titled “Identity”- There are two distinct
PublicKeytypes:calimero_primitives::identity::PublicKey(wraps aHash) and the SDK prelude’sPublicKey(pub [u8; 32]). They are not the same type — mind which one a signature expects. - Executor identity is per-context and resolved server-side; it is never read from guest memory or supplied by the caller, so app code cannot spoof who it is running as.
context_id, the executor public key, andxcall_originare three separate 32-byte identifiers on the VM context — do not conflate them (and the author attributed to a stored entity is a fourth, separate concept).PrivateKeyzeroizes its bytes on drop via a customDropimpl — do not copy the raw bytes out into aVecthat outlives it.
Storage
Section titled “Storage”- The
SortedMap/SortedSetordered index is node-local, derived, and NOT synced. After a remote sync it is stale; the next ordered read notices a validity-marker mismatch and rebuilds the index once. SortedMapkeys must be encoded big-endian (and offset-binary for signed values) so that their byte order matches theirOrdorder — little-endian keys will iterate in the wrong order.- Map keys are bounded
K: AsRef<[u8]>on every write path — your key type must produce stable bytes. - Entity ids are domain-separated SHA-256 hashes, so entity-id (storage) order is unrelated to logical key order — never assume iteration follows key order in an unordered map.
- Private storage is node-local and NOT synced; anything you write there stays on the node that wrote it and never reaches peers.
LWWtiebreak on an equal timestamp goes to the highernode_id— concurrent same-millisecond writes are resolved by node identity, not by who you think wrote last.Counter::incrementPANICS if called inside a state migration (merge mode), because the per-node delta would be non-deterministic; useincrement_forwith an explicit node id instead.GCounter(Counter<false>) andPNCounter(Counter<true>) are distinct types with different serialization — they are not interchangeable across a schema change.AuthoredMap::mergeis a deliberate no-op: delegating would strip per-entry ownership, so authored entries only converge through their own authored merge path.- A missing merge function fails loud with
NoMergeFunctionRegisteredrather than silently falling back to LWW (which would discard one side’s CRDT state) — register the merge function. Vectoris not a positional CRDT; concurrent inserts do not preserve intent. For collaborative text use the RGA (Replicated Growable Array) instead.- The wasm ABI forces all map keys to be
String(validation rejects any other map-key type asInvalidMapKeyType) — non-string keys won’t cross the ABI boundary.
Governance you can hit
Section titled “Governance you can hit”- The default write policy is membership-based: any member can write or delete any non-restricted key.
DEFAULT_MEMBER_MASK = WRITE | DELETE— note it is NOTADMIN, so members can mutate data but not change governance. - Writes from a
ReadOnlyrole are silently dropped, not errored —ReadOnlyContextStoragesuppresses every write method and returns as if it succeeded. - Subgroup visibility defaults to
Restrictedat birth — membership in a new subgroup requires an explicitadd_group_memberscall, it is not inherited automatically. - The only capability granted by default is
CAN_JOIN_OPEN_SUBGROUPS(1 << 2); every other capability (delete-subgroup, admin, etc.) must be granted explicitly.
Sync, events, xcall
Section titled “Sync, events, xcall”- Gossip delivery is best-effort, and governance publish is best-effort too — but the local governance DAG advance is durable, so even a dropped broadcast reconciles later when peers sync the DAG. Never treat “I published it” as “everyone received it.”
- An xcall only QUEUES the call. It runs same-node, post-commit, is namespace-bounded (a permission check gates the target namespace), and the target method must be annotated
#[app::xcall]— there is no synchronous cross-context return value. - Event handlers run on receivers after commit, at-least-once, with restart replay (a node that crashes mid-handler re-delivers on restart). Handlers MUST be idempotent — assume every event can fire more than once.
- Blobs are split into 1 MiB chunks (
CHUNK_SIZE = 1 << 20). For a multi-chunk blob the rootBlobIdisSHA-256of the concatenated child chunk ids — it is NOT a hash of the file content, so you cannot derive it by hashing the bytes yourself. - Blob availability is announced per-context to the DHT (
announce_blobtakes acontext_id) — a blob announced in one context is not automatically discoverable in another. - Blobs are NOT folded into
scope_root(the merkle/state root tracks only the data, ACL, membership, admin, and capability planes) and are NOT encrypted at rest.
Confidentiality
Section titled “Confidentiality”- State-delta
eventsship as plaintext (context id, kind, data, handler are plain serialized JSON). Even where the storage delta itself is encrypted, the broadcast event is separate and observable. - Envelope metadata — topic, author, DAG shape, signer — is visible to any subscriber of the topic, regardless of payload encryption. Do not put secrets in event payloads or rely on metadata being private; see the encryption protocol for what is and isn’t protected.
Testing (TestHost)
Section titled “Testing (TestHost)”- In
TestHost, xcall,ed25519_verify, and networked blob announce/fetch ops PANIC if invoked — they have no in-process implementation, so structure tests to avoid them. - Event handlers are not auto-dispatched by
TestHost; emitting an event does not run its handler, so handler logic must be exercised directly. - The default
TestHostidentity satisfies owner checks — a test can pass on permissions that would fail in production under a real, non-owner executor. - Only one
TestHostmay be live per thread at a time — run host-using tests serially within a thread, not nested.
Next steps
Section titled “Next steps” Advanced SDK The deeper mechanics behind the determinism, merge, and xcall edges above.
Storage performance & Big-O Why writes rehash siblings and how to keep collections fast.
Access-controlled storage How the Public write boundary and merge-plane enforcement work.