Files
thomaslavaur 48fbc82c35 docs(blockchain): order channel configurations and prevent their replay (#396)
# docs(blockchain): give channel configurations their own lineage

**Authors**: Thomas Lavaur <thomas@logos.co>

## Reviewer Orientation

Read **Motivation** first. The three entries are one change and its
consequences, so read them in order.

| # | Priority | Document / Change | What to look for |
| --- | --- | --- | --- |
| 1 | Critical | **Start here**, [Mantle](#affected-specifications):
[the configuration lineage](#1-the-configuration-lineage) | the new
`config_tip_hash` state field, the `parent` payload field and the two
validation branches; check that a configuration is valid at exactly one
point of the configuration history, that no inscription can invalidate a
pending configuration nor a configuration a pending inscription, and
that the just-in-time creation branch is still reachable |
| 2 | Critical | [Mantle Transaction
Encoding](#affected-specifications): [encoding of the
parent](#2-encoding-of-the-parent-reference) | wire format change,
`Parent` placed between `ChannelId` and `KeyCount`; no backwards
compatibility |
| 3 | High | [Mantle](#affected-specifications) and
[Cryptarchia](#affected-specifications): [regenerated test
vectors](#3-test-vectors-regenerated-for-the-new-field) | the published
values that derive from the `CHANNEL_CONFIG` payload, and that the
`ChannelConfig` leaf of the block root agrees with the transaction hash
vector |
| 4 | Low | [Chores](#chores) | skim, revision history rows only |

## Status tracker

- [x]  🚧 **Raw (make sure that all below is completed)**
    -  Template applied
    -  Authors filled in
    -  Authors agree on the RFC content
- [x]  📘 **Draft (make sure that all below is completed)**
    -  All dependent specifications added (Notion backlinks checked)
    -  Specifications to deprecate added, if applicable
    -  Specifications to retire added, if applicable
- Research Lead assigned, or Project Lead assigned if the Research Lead
is an author
    -  Relevant Research Domain Experts assigned (cannot be authors)
- [ ]  ⚙️ **Verified (make sure that all below is completed)**
    -  Researchers’ comments addressed
    -  All logical changes documented
    -  All Research reviewers approve the latest version
    -  Engineering Lead assigned
    -  Relevant Engineering Domain Experts assigned
- [ ]  🔀 **Merged (make sure that all below is completed)**
    -  Engineers’ comments addressed
    -  Every change added to the change log
    -  All Engineering reviewers approve the latest version
    -  Specification version numbers assigned
    -  Implementation reviewed and merged
    -  Branch updated to master and all conflicts resolved
    -  PR merged

## Change log

| **Revision** | **Description** | **Date** |
| --- | --- | --- |
| v1 | Initial PR description | 2026-08-05 |
| v2 | Split the configuration lineage from the message lineage:
`ChannelState` tracks the last configuration and `parent` names the
previous configuration, closing the reconfiguration censorship race |
2026-08-06 |
| v3 | Leave the message tip untouched on configuration execution:
pending inscriptions survive a reconfiguration | 2026-08-10 |

# Motivation

Channels are virtual chains: every message names the message it extends,
and the channel tip is what gives the messages of a channel a single
order that survives forks and reorganizations of Cryptarchia.
`CHANNEL_CONFIG` moves that tip, since its execution overwrites
`tip_hash` and `tip_slot`, but its payload names no parent. A
configuration is therefore authorized by the committee without reference
to the channel state it was meant to apply to, and it validates against
any tip.

Two consequences follow. First, ordering is not determined by the
signers. When a committee authorizes two configurations, both remain
valid whatever the state of the channel, so the surviving configuration
is the one that block producers happen to include last rather than the
one the signers intended to apply last. Second, a configuration is
replayable for as long as its enclosing Mantle Transaction can be
included in a block. The proof binds the configuration to a single
`mantle_txhash` and the fee payment consumes notes, which prevents a
second inclusion on the same fork, but a transaction abandoned by a
reorganization can be included later on the surviving fork, where the
configuration is applied on top of the configurations that succeeded it.
The worst case is a key rotation being undone: a committee that rotates
its accredited keys after a compromise can see the transaction carrying
the superseded configuration re-included and the compromised key
reinstated, together with the sequencing parameters and thresholds that
came with it.

Binding the configuration to the message tip would close both holes but
open a third. A configuration carries a MultiEd25519 threshold signature
over the transaction hash, which commits to the parent, and every
inscription moves the message tip, so a malicious accredited sequencer
could invalidate a pending configuration simply by posting. A single key
racing a threshold committee wins indefinitely, and what it censors is
exactly the reconfiguration that matters most, the rotation of the
accredited keys after a compromise.

Giving configurations a lineage of their own makes the signed material
describe the configuration history the committee approved, which orders
configurations and makes them single use, while leaving nothing an
inscription can do to invalidate them.

# Proposal

`ChannelState` gains a `config_tip_hash` field holding the hash of the
last configuration of the channel, initialised to `ZERO` on channel
creation. `ChannelConfig` gains a `parent` field naming the
configuration it extends. Validation requires `parent` to equal the
current `config_tip_hash` of the channel, and requires it to be `ZERO`
when the configuration creates the channel just in time. Only another
configuration moves `config_tip_hash`, so a pending configuration stays
valid no matter how many inscriptions extend the channel.

Execution sets only the configuration tip to the hash of the encoded
configuration and leaves the message tip untouched, so an inscription in
flight survives a reconfiguration for as long as the new configuration
keeps its signer authorized. The `tip_slot` reset joins the sequencing
reset, so the round-robin timeout restarts from the configuration rather
than from the last message.

The wire encoding of `ChannelConfig` gains the corresponding 32 byte
`Parent` field. `CHANNEL_WITHDRAW` and `CHANNEL_TRANSFER` are left
untouched: they do not move the tip, and their inputs are consumed
notes, which already makes them single use.

# Discussion

## Ordering and replay

Configurations are totally ordered among themselves by their own chain:
each one is valid at exactly one point of the configuration history, the
point its signers observed. Replay of a superseded configuration fails
as a consequence: once a later configuration executes, the parent of the
superseded one no longer matches the configuration tip, so re-inclusion
after a reorganization fails validation instead of rolling the channel
back, and a rotated-out key stays out.

One nuance is deliberate. A configuration abandoned by a reorganization
can be re-included while no later configuration exists, whatever
inscriptions have accumulated in between. This is not a replay but the
intended application: the committee authorized the configuration to
extend the last configuration of the channel, wherever the inscription
chain stands.

## Interleaving across forks

Neither chain commits to the other, so the position of a configuration
relative to the inscriptions is decided by the blocks that include them,
and a reorganization can interleave them differently: an inscription
signed before a configuration can be re-included after it. The channel
state stays well defined on every fork, because an inscription only
executes when its signer is the sequencer authorized by the
configuration in force at that point. In exchange, nothing a
configuration does invalidates an inscription in flight: sequencers keep
posting through a reconfiguration for as long as the new configuration
keeps them authorized, instead of watching for configurations and
re-signing against a new tip.

## Alternatives considered

- **A single lineage shared with inscriptions.** Requiring `parent` to
equal the message tip orders a configuration and prevents its replay,
but every inscription moves that tip and the threshold signature commits
to it, so a malicious accredited sequencer invalidates a pending
configuration by posting anything. One key defeats a committee
indefinitely, censoring precisely the key rotation after a compromise.
- **A configuration counter in `ChannelState`.** A monotonic counter
carried in the payload and incremented on execution orders
configurations against each other and prevents their replay, but it
names nothing, whereas a parent hash commits to the exact configuration
content being extended, reuses the hash chain mechanism the channel
already relies on, and costs the same single state field.
- **An expiry expressed in slots.** Binding a configuration to a slot
range narrows the window in which it can be replayed but never closes
it, and it gives no ordering between two configurations authorized
within the same window.
- **Advancing the message tip on configuration execution.** Setting
`tip_hash` to the configuration hash pins the configuration into the
inscription order, so the state of a channel after a given message is a
function of the message chain alone, but every inscription signed and
not yet included is invalidated each time a configuration executes:
sequencers must watch for configurations and re-sign in-flight
inscriptions, and a reconfiguration disrupts the posting of the very
sequencers it keeps.

## Backwards compatibility

The change alters the encoding of an existing Operation, so it is not
backwards compatible: implementations must adopt it together, and
configurations encoded under the previous format are rejected. No
migration path is specified since Bedrock is not live. Clients building
configurations must read the configuration tip of the channel before
signing, which is a new input for wallets and Zone tooling.

# Details

## 1. The configuration lineage

The channel state tracks the last configuration next to the last
message, and a fresh channel starts both chains at `ZERO`.

```diff
     # Message Ordering
-    tip_hash: hash
+    tip_hash: hash         # Last message of the channel
+    config_tip_hash: hash  # Last configuration of the channel
```

```diff
         tip_hash = ZERO,
+        config_tip_hash = ZERO,
         tip_slot = block_slot,
```

The payload names the configuration being extended.

```diff
 class ChannelConfig:
     channel: ChannelId
+    parent: hash             # Previous configuration of the channel
     keys: list[Ed25519PublicKey]
     # ... unchanged fields elided
```

Validation gains the continuity check against the configuration tip on
an existing channel, and a genesis check on the branch that creates the
channel just in time. The checks on the thresholds and on the key list
are unchanged and still apply to both branches.

```diff
 if config.channel in channels:
     chan = channels[config.channel]
+
+    # Ensure the configuration is extending the last configuration
+    # of the channel
+    assert config.parent == chan.config_tip_hash
+
     # Verify the configuration_threshold signatures (see Appendix)
     MultiEd25519_verify(txhash,
                         proof.signatures,
                         proof.indexes,
                         chan.accredited_keys,
                         chan.configuration_threshold)
+else:
+    # Channel will be created automatically upon execution
+    # Ensure that this configuration is the genesis configuration
+    assert config.parent == ZERO
```

Execution extends only the configuration lineage, and the `tip_slot`
reset joins the sequencing reset.

```diff
       # Update Decentralized Sequencing Parameters
+      chan.tip_slot = block_slot
       chan.tip_sequencer = 0
```

```diff
       chan = channels[config.channel]
-      chan.tip_slot = block_slot
-      chan.tip_hash = hash(encode(config))
+      chan.config_tip_hash = hash(encode(config))
```

## 2. Encoding of the parent reference

`Parent` is a 32 byte hash already defined in the channel operations
grammar and already used by `ChannelInscribe`. It is placed immediately
after `ChannelId`, so that the fixed size prefix of the payload carries
the channel and the configuration link, ahead of the variable length key
list.

```diff
-ChannelConfig     = ChannelId KeyCount *Signer PostingTimeframe PostingTimeout ConfigThreshold TransferThreshold
+ChannelConfig     = ChannelId Parent KeyCount *Signer PostingTimeframe PostingTimeout ConfigThreshold TransferThreshold
```

## 3. Test vectors regenerated for the new field

The encoded `CHANNEL_CONFIG` payload grows by 32 bytes, so every
published value derived from it is regenerated. The values are all
BLAKE2b derived, no `zkhash` is involved.

| Specification | Vector | Reason |
| --- | --- | --- |
| Mantle, Operation Id | `CHANNEL_CONFIG` payload and `op_id` | the
payload carries the new field and `op_id` hashes the payload |
| Mantle, Mantle Transaction Hash | transaction with one of each
operation | the transaction embeds the `CHANNEL_CONFIG` payload |
| Cryptarchia v1, Test Vectors | `leaf[1]`, both occurrences of
`block_root`, and `block_id` | the leaf is the transaction hash of the
configuration, the root is over the leaves and the block id commits to
the root |

The two families cross-check each other: `leaf[1]` of the block root is
the Mantle Transaction hash of the single Operation transaction carrying
the configuration, so it can only agree with the Mantle vectors if both
were derived from the same payload.

The configuration tip is validator state and is never encoded, so no
vector derives from it; the published configuration payload carries
`parent = ZERO`, the genesis branch.

## Chores

- Revision history rows: `1.10.0` in Mantle, `1.6.0` in Mantle
Transaction Encoding, `1.1.1` in Cryptarchia v1.
- Dropped a stray space in the `block_id` of the Cryptarchia v1 header
vector.

# Implementation

- [ ] Add `config_tip_hash` to the channel state and initialise it to
`ZERO` on channel creation
- [x] Add the `parent` field to the `ChannelConfig` structure and to its
encoder and decoder, between the channel identifier and the key count
- [ ] Enforce `parent == config_tip_hash` for an existing channel and
`parent == ZERO` for a channel created just in time
- [ ] Set `config_tip_hash` to the hash of the encoded configuration on
execution, leaving the message tip untouched
- [ ] Update the tooling that builds configurations so that the
configuration tip is read and signed by the committee
- [x] Align the implementation test vectors with the regenerated ones:
`op_id` and Mantle Transaction hash for the configuration, and the
resulting `block_root` and `block_id`
- [ ] Add a test that a configuration signed against configuration tip
`C` remains valid after any number of inscriptions extend the channel
- [ ] Add a test that a configuration is rejected once a later
configuration executes, including re-inclusion after a reorganization
- [ ] Add a test that an inscription signed before a configuration
executes remains valid afterwards while its signer is still the
authorized sequencer
- [ ] Add a test that a configuration creating a channel just in time is
rejected unless its parent is `ZERO`
- [ ]  Verify the implementation matches this specification

# Affected Specifications

| Specification | Status | Note |
| --- | --- | --- |
| [Mantle](docs/blockchain/raw/bedrock-v1.1-mantle-specification.md) |
Modified | version `1.10.0`, channel state, payload, validation and
execution of `CHANNEL_CONFIG`, and the two test vectors deriving from it
|
| [Mantle Transaction
Encoding](docs/blockchain/raw/mantle-transaction-encoding.md) | Modified
| version `1.6.0`, `ChannelConfig` grammar |
| [Cryptarchia v1](docs/blockchain/raw/cryptarchia-v1-protocol.md) |
Modified | version `1.1.1`, test vectors only, no normative change |

The remaining specifications that mention `CHANNEL_CONFIG` were checked
and are unaffected: [Gas Cost
Determination](docs/blockchain/raw/analysis-gas-cost-determination.md)
prices the Operation on `configuration_threshold` and not on payload
size, and [Cryptoeconomics
Overview](docs/blockchain/raw/overview-cryptoeconomics.md) only cites
the Operation as one whose Execution Gas is small relative to its
storage footprint, which the extra 32 bytes do not change.
2026-08-27 09:14:53 +02:00
..
2026-05-11 18:05:13 +03:00
2026-05-11 18:05:13 +03:00

Logos LIP Index

An IETF-style index of Logos-managed LIPs across Storage, Messaging, Blockchain, AnonComms, and Research sections. Use the filters below to jump straight to a specification.

Current All Stable Draft Raw Deprecated Deleted
All components Messaging Blockchain Storage AnonComms Research
All time Latest Last 90 days
Loading RFC index...
Click a column to sort