Skip to main content
An activation has a small, fixed shape: a header, a target, a signer, an Ed25519 signature, and an opaque operations payload. This page walks through each part and explains how Grid prevents replay without using a sequential nonce. For the higher-level developer flow (build → sign → submit → poll), see Activations.

Fields

All numeric fields are little-endian.
struct ExternalActivationHeader {
    version: ExternalActivationVersion,                // u8
    chain_id: ChainId,                                 // u32
    valid_until_block: BlockIndex,                     // u64
    payload_encoding_version: PayloadEncodingVersion,  // u8
}

struct GenExternalActivation {
    header: ExternalActivationHeader,
    tag: ActivationTag,             // u32, 0 if unset
    to: Option<EntityId>,           // 32 bytes; defaults to signer entity
    signer: EddsaPublicKey,         // 32 bytes
    signature: EddsaSignature,      // 64 bytes (R || S)
    operations_payload: Bytes,      // Borsh-encoded list of operations
}
  • chain_id identifies which the Grid network the activation is for. The same payload signed for a different network produces a different signature, so cross-network replays are rejected.
  • valid_until_block is the highest block index at which this activation may execute. See The validity window below.
  • tag is a 32-bit application-defined value the signer can vary to disambiguate otherwise-identical activations. Two payloads with identical content but different tags produce two different ActivationIds.
  • to is the target entity. If left unset, it resolves to the signer’s own entity ID.
  • signer is the Ed25519 public key that authorized the activation.
  • operations_payload is a Borsh-encoded Vec<SerializedOperation>. Each entry carries a vm_id (e.g. Gci, Gvm) and the per-VM operation bytes. The protocol treats this as opaque; only the execution layer parses it.

What is signed

The Ed25519 signature is computed over a fixed-size 82-byte buffer derived from the activation. The payload itself is not signed directly: it is hashed first, and only the 32-byte hash goes into the buffer.
+--------+----------+--------+----------------+-------------+--------+--------------+
| ver(1) | chain(4) | to(32) | valid_until(8) | encoding(1) | tag(4) | payloadH(32) |
+--------+----------+--------+----------------+-------------+--------+--------------+
                                          82 bytes total
  • to is always the resolved target. If to was unset on the activation, this slot holds the signer’s entity ID. The signed pre-image is unambiguous either way.
  • payloadH = SHA256(operations_payload).
Keeping the signed buffer small and fixed-size means it is cheap to produce, cheap to verify, and trivial to reproduce anywhere, including signing environments that have no knowledge of the operations payload itself.

Activation ID

The same buffer determines the activation’s identity:
ActivationId = ( SHA256(buffer), resolved_to )
Any change to a signed field (header, target, tag, payload bytes) produces a different ID. This is the value you pass to gen_getActivation (or gen client get-activation) to look up status and receipts.

The validity window

The Grid does not use a sequential per-account nonce. There is no account.nonce + 1 to look up before signing, no nonce gap that strands later activations when an earlier one is dropped, and no requirement that activations from the same signer arrive in any particular order. Instead, every activation declares an explicit expiry: valid_until_block. At admission the protocol enforces:
  1. valid_until_block > last_finalized_block: the activation has not already expired.
  2. valid_until_block - last_finalized_block < MAX_VALIDITY_WINDOW: the expiry is bounded so the dedup window stays finite.
MAX_VALIDITY_WINDOW is 1,000,000 blocks today. At a 10ms block time that is roughly 2.7 hours of wall-clock validity. A signer can pick any value within that bound. (The exact constant and whether it remains overridable per-deployment may be tightened before mainnet.) This works because the Grid has deterministic BFT finality with no forks. There is no recent-block-hash to bind against. Once a block is final, it stays final, so a future block index alone is enough to bound the activation’s lifetime.

Replay protection

Replay protection has two parts:
  • Cryptographic binding. chain_id, valid_until_block, to, tag, and the payload hash are all in the signed buffer. The same payload signed for a different chain, target, expiry, or tag is a different signed message and a different ActivationId.
  • Mempool dedup. Each shard mempool retains every admitted ActivationId until its valid_until_block is finalized and pruned. A duplicate submission within that window is rejected at admission.
To re-submit equivalent work, change something the signature covers (typically the tag or a payload field) so the new activation has a fresh ActivationId.

Picking a valid_until_block

The typical flow is:
  1. Read the current finalized block via gen_getCurrentBlockIndex (RPC) or gen client get-current-block-index (CLI).
  2. Add a buffer for the latency you expect between signing and admission. current + 600 gives ~6 seconds at 10ms blocks; current + 6000 gives ~60 seconds. Stay under MAX_VALIDITY_WINDOW.
  3. Sign and submit.
A short window is preferred. It bounds how long a leaked-but-unsubmitted signature stays usable, and it keeps the dedup index small. Pick the smallest window that comfortably covers your client’s submission latency plus retries.

What the protocol validates at admission

The mempool admission gate enforces:
  • Structural: required fields are present and framing is valid.
  • Signature: Ed25519 verification against the 82-byte buffer and the activation’s signer.
  • Expiry: valid_until_block > last_finalized_block and within MAX_VALIDITY_WINDOW.
  • Deduplication: ActivationId not already in the dedup window.
These are protocol-level checks. They do not parse the operations payload, do not check permissions, do not check balances, and do not inspect the target entity’s state. Those concerns belong to the execution layer, which validates and applies operations after admission.