Sign with mortal eras, not immortal ones #11

Open
opened 2026-09-15 11:18:11 +00:00 by grenade · 0 comments
Owner

Tier 1 (#7) signs with an immortal era. That was deliberate — it makes
CheckMortality's implicit the genesis hash, so there is no birth block for the
signer and the node to disagree about, and it kept the first end-to-end proof
about the crypto rather than about mortality. It is not what should ship.

An immortal extrinsic is valid forever. A signed transfer that fails to get into
a block — pool eviction, a node restart, a user closing the popup after
approving — stays replayable at that nonce indefinitely, so anyone holding the
bytes can submit it later, at a moment the signer did not choose. Substrate's
default for user transactions is mortal for exactly this reason.

What it needs

  • Era encoding. The registry describes it as a 256-variant enum (Immortal,
    Mortal1Mortal255) whose SCALE form is two bytes, so @quantus/codec can
    already encode one — the caller has to name the right variant and phase, which
    is the part nobody should hand-compute. A helper belongs in the codec next to
    standardExtensions.
  • The birth block hash as CheckMortality's implicit, which must be the hash
    of the block the era is anchored to, not simply the current head. Getting this
    wrong produces a valid signature the chain rejects as BadProof — the same
    indistinguishable failure described on quantus/api#1.
  • A decision on period. 64 blocks is the polkadot-js default, but Heisenberg is
    proof-of-work with a block time measured in minutes rather than seconds, so 64
    blocks is hours and the mortality window wants choosing against the actual rate
    rather than inherited.

Acceptance

The tier-1 harness submits with a mortal era and the transfer is included; and a
signed extrinsic whose era has lapsed is rejected by the node, which is the
property being bought.

Tier 1 (#7) signs with an immortal era. That was deliberate — it makes `CheckMortality`'s implicit the genesis hash, so there is no birth block for the signer and the node to disagree about, and it kept the first end-to-end proof about the crypto rather than about mortality. It is not what should ship. An immortal extrinsic is valid forever. A signed transfer that fails to get into a block — pool eviction, a node restart, a user closing the popup after approving — stays replayable at that nonce indefinitely, so anyone holding the bytes can submit it later, at a moment the signer did not choose. Substrate's default for user transactions is mortal for exactly this reason. ## What it needs - `Era` encoding. The registry describes it as a 256-variant enum (`Immortal`, `Mortal1`…`Mortal255`) whose SCALE form is two bytes, so `@quantus/codec` can already encode one — the caller has to name the right variant and phase, which is the part nobody should hand-compute. A helper belongs in the codec next to `standardExtensions`. - The **birth block hash** as `CheckMortality`'s implicit, which must be the hash of the block the era is anchored to, not simply the current head. Getting this wrong produces a valid signature the chain rejects as `BadProof` — the same indistinguishable failure described on quantus/api#1. - A decision on period. 64 blocks is the polkadot-js default, but Heisenberg is proof-of-work with a block time measured in minutes rather than seconds, so 64 blocks is hours and the mortality window wants choosing against the actual rate rather than inherited. ## Acceptance The tier-1 harness submits with a mortal era and the transfer is included; and a signed extrinsic whose era has lapsed is rejected by the node, which is the property being bought.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: quantus/extension#11