# 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.
Logos LIP (Logos Improvement Proposals) Index
NOTE: This repo is still evolving while the LIP process is refined.
This repository contains specifications from the Messaging, Blockchain, Storage, and AnonComms components of the IFT portfolio. LIPs are Requests for Comments that document protocols, processes, and system interfaces in a consistent, reviewable format.
LIP process
This repository replaces the old rfc.vac.dev resource. Specs are maintained in Markdown here and progress through statuses such as raw, draft, stable, or deprecated. The process and lifecycle are defined in:
- 1/COSS:
docs/research/draft/1/coss.md
Contributing
- Open a pull request against this repo.
- Add or update the LIP in the appropriate component folder.
- Include status and category metadata in the header table.
If you are unsure where a document belongs, open an issue first and we will help route it.
Component indexes
- Messaging:
docs/messaging/README.md - Blockchain:
docs/blockchain/README.md - Storage:
docs/storage/README.md - AnonComms:
docs/anoncomms/README.md - Research:
docs/research/README.md
Local setup
- Install mdBook (pick the version that matches your Rust toolchain).
- Install Python dependencies if needed.
To install mdBook via Make:
make install
Build and serve
Run the generators before building or serving:
python scripts/run_runtime_generators.py
mdbook build
History timelines are generated only in CI. Local generator runs skip
scripts/gen_history.py unless LIPS_GENERATE_HISTORY=1 is set.
To serve locally:
make serve
Or run the commands directly:
python scripts/run_runtime_generators.py
mdbook serve -p 3000 -n 0.0.0.0
CI/CD
- CI builds
masterand pushes todeploy-masterbranch, which is hosted at https://lip.logos.co/. - CI builds
developand pushes todeploy-developbranch, which is hosted at https://dev-lip.logos.co.dev/.
The hosting is done using Caddy server with Git plugin for handling GitHub webhooks.
Information about deployed build can be also found in /build.json available on the website.