From 103b9f228ecc2bde951aa2afe3b6ca3c545c4611 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sun, 16 Aug 2026 13:49:56 -0500 Subject: [PATCH] docs: state the contract conformance requirements authors must meet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge laws are in the whitepaper but not in the API docs the person writing a contract actually reads. An author can implement `update_state` as last-write-wins today without ever being told that is wrong, and deployed contracts have done exactly that (freenet-core#5153). Fills the gaps left by #92 (idempotent + associative merge) and #90 (delta size), without restating either: - **Canonical byte representation.** Every valid logical state must have exactly one encoding. The platform compares states by bytes and cannot know that two encodings mean the same thing, so it treats them as still-diverged and keeps trying to reconcile. Includes the practical advice: sort map and set entries, fix field order, avoid formats with nondeterministic iteration order. - **Merging is `update_state`.** This is the framing that makes the failure mode legible. Returning the current state unchanged asserts `merge(A,B) == A`; if the peer holding B does the same in reverse, the two disagree, never converge, and retry forever. It is easy to write that as a reasonable-looking "reject stale updates" check, which is why it needs saying explicitly. - **`summarize_state` determinism**, and why: peers compare summaries to decide whether they have converged, so one that varies with hash iteration order makes two identical peers look permanently divergent (freenet-core#4857). - **`get_state_delta` determinism**, plus the explicit note that deltas do NOT need a global canonical encoding, unlike state — two peers reconciling from different starting points may legitimately produce different bytes, and delta correctness is judged by applying them and comparing canonical states. - **Emitted states must be valid**, and states declared `Valid` must already be canonical. Deliberately unchanged: the existing warnings that noncompliance may get a contract deprioritized or removed. An earlier draft softened them, on the grounds that no such enforcement ships today. That is accurate but it is not ours to decide, and the direction of freenet-core#5320 is to make those sentences true, so softening now and re-hardening later is churn. Documentation only; no code changes. `cargo doc` produces no new warnings. Refs freenet/freenet-core#5320 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0131oS6YAD9gMWigX8v4QR4J --- rust/src/contract_interface/trait_def.rs | 109 +++++++++++++++++++++-- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/rust/src/contract_interface/trait_def.rs b/rust/src/contract_interface/trait_def.rs index 028c68a..0d381ee 100644 --- a/rust/src/contract_interface/trait_def.rs +++ b/rust/src/contract_interface/trait_def.rs @@ -59,30 +59,102 @@ use super::{ /// # ContractInterface /// /// This trait defines the core functionality for managing and updating a contract's state. -/// Implementations must ensure that the state merge operation is *associative*, *commutative*, -/// and *idempotent* — i.e. state forms an idempotent commutative monoid (a join-semilattice), -/// the same algebraic structure used by state-based CRDTs. In other words, when applying -/// multiple delta updates to a state, the order in which these updates are applied should not -/// affect the final state, and applying the same update more than once must not change the -/// result beyond its first application. Once all deltas are applied, the resulting state -/// should be the same, regardless of the order or repetition of application. +/// +/// # Conformance requirements +/// +/// Freenet does not have a central coordinator that decides which peer's state "wins". +/// Instead, every peer that holds a copy of a contract's state converges on the same +/// value by repeatedly merging the copies it receives from other peers, in whatever order +/// they happen to arrive. For that to actually converge, the merge operation this trait +/// implements has to obey the algebraic laws of a join-semilattice (the structure behind +/// state-based CRDTs): +/// +/// - **Associative**: `merge(merge(A, B), C) == merge(A, merge(B, C))`. Peers merge +/// updates in whatever order they arrive, not in the order they were produced. +/// - **Commutative**: `merge(A, B) == merge(B, A)`. Two peers that received the same +/// updates in a different order must still end up in the same state. +/// - **Idempotent**: `merge(A, A) == A`. Delivery on Freenet is at-least-once, so the +/// same update can and will arrive more than once. +/// +/// Merging is not a separate method on this trait: applying a full-state update through +/// [`Self::update_state`] *is* the merge, and the state it returns is the merge result. +/// This matters because it is easy to implement something that looks like a reasonable +/// "reject stale updates" check but is actually last-write-wins in disguise. A contract +/// that responds to an incoming state `B` by returning its own current state `A` +/// unchanged is asserting `merge(A, B) == A`. If the peer holding `B` runs the same +/// contract and does the same thing in reverse, it asserts `merge(B, A) == B`. Those two +/// results disagree, so commutativity is broken, the two peers never converge, and every +/// future sync between them keeps retrying and re-sending the same update forever. This +/// is a real failure mode that has been observed in deployed contracts, not a +/// hypothetical one (see [`Self::update_state`] and freenet/freenet-core#5153). +/// +/// Because delivery is at-least-once, applying the same delta twice must leave the state +/// unchanged after the first application, and deltas that arrive in a different order +/// must still land on the same final state. +/// +/// ## Canonical byte representation +/// +/// Every valid logical state must have exactly one byte representation. Freenet compares +/// states for equality by comparing bytes; it has no way to know that two different +/// encodings represent the same logical value, so it will treat them as still-diverged +/// and keep trying to reconcile them. If an encoding has more than one way to represent +/// an equivalent value, the contract must canonicalize before returning state from +/// [`Self::update_state`]. In practice this usually means: sort map and set entries by +/// key, fix the order of fields and list elements that carry no meaningful order, and +/// avoid formats whose iteration order is not deterministic. +/// +/// Deltas are not held to this rule. See [`Self::get_state_delta`]. +/// +/// Every state emitted by [`Self::update_state`] must also be a state the contract would +/// itself accept from [`Self::validate_state`]. A state the contract would reject cannot +/// be allowed to propagate to other peers in the first place. /// /// Implementations must also keep the delta negligible when the requesting peer's summary /// shows it already holds this state: the delta must not contain that state, or approach /// its size. See [`Self::get_state_delta`]. /// +/// The consequences are not hypothetical: a contract that breaks the merge laws leaves the +/// peers holding it permanently diverged, and a contract that ignores the delta-size rule +/// makes every reconciliation between an in-sync pair of peers re-ship the full state +/// forever. Both have been observed on the live network (freenet/freenet-core#5153) and +/// both amount to unbounded, unresolved bandwidth cost paid on every future sync. /// Noncompliant behavior, such as failing to obey the associativity, commutativity, or /// idempotence rules, or returning a state-sized delta to a peer that is already up to date, /// may result in the contract being deprioritized or removed from the p2p network. pub trait ContractInterface { /// Verify that the state is valid, given the parameters. + /// + /// Every state this returns [`ValidateResult::Valid`] for must already be in the + /// contract's canonical byte encoding. See the "Canonical byte representation" + /// section on [`ContractInterface`]. fn validate_state( parameters: Parameters<'static>, state: State<'static>, related: RelatedContracts<'static>, ) -> Result; - /// Update the state to account for the new data + /// Update the state to account for the new data. + /// + /// This is also the merge operation described in the "Conformance requirements" + /// section on [`ContractInterface`]. Given two states `A` and `B` for the same + /// contract, applying `B` as an update to a peer whose current state is `A` must + /// produce the same result as applying `A` as an update to a peer whose current + /// state is `B`, and that result must be stable under repetition and grouping: + /// + /// - `merge(A, A) == A` + /// - `merge(A, B) == merge(B, A)` + /// - `merge(merge(A, B), C) == merge(A, merge(B, C))` + /// + /// Returning the current state unchanged, without incorporating anything from the + /// incoming update, is a merge result like any other and is subject to these same + /// laws. It is only correct when the incoming update is already implied by the + /// current state; using it to mean "ignore updates from this source" or "keep + /// whichever state arrived first" breaks commutativity and stalls convergence + /// between the two peers indefinitely. + /// + /// The same at-least-once delivery guarantee applies to deltas: applying a delta a + /// peer has already applied must be a no-op, and deltas that arrive out of order + /// must still converge to the same state as deltas applied in order. fn update_state( parameters: Parameters<'static>, state: State<'static>, @@ -96,6 +168,13 @@ pub trait ContractInterface { /// size is comparable to the state defeats delta computation, and a summary that is /// a copy of the state is always a bug. See [`Self::get_state_delta`] for the /// delta-size requirement this summary feeds into. + /// + /// This must be deterministic: the same state and parameters must always produce + /// exactly the same summary bytes. Peers compare summaries to decide whether they + /// have converged, so a summary that varies between calls on identical input, for + /// example because it iterates a hash map or set in an order that is not fixed, + /// makes two peers holding identical state look permanently divergent to each other. + /// This class of bug has occurred in practice; see freenet/freenet-core#4857. fn summarize_state( parameters: Parameters<'static>, state: State<'static>, @@ -105,10 +184,22 @@ pub trait ContractInterface { /// This along with [`Self::summarize_state`] allows flexible and efficient /// state synchronization between peers. /// + /// This must be deterministic for identical inputs: the same state, parameters, and + /// summary must always produce the same delta bytes. Unlike state (see "Canonical + /// byte representation" on [`ContractInterface`]), a delta does not need a single + /// global canonical encoding. Two peers reconciling from different starting points + /// may legitimately produce different delta bytes that both correctly bring the + /// receiver to the same converged state, and delta correctness is checked by + /// applying the delta and comparing the resulting canonical states, not by comparing + /// delta bytes directly. + /// /// # The delta to an up-to-date peer must be negligible /// /// When `summary` shows that the requesting peer already holds everything this state - /// has, the delta carries no information, and its size has to reflect that. + /// has, the delta carries no information, and its size has to reflect that. This is + /// an efficiency requirement, not a correctness one: an oversized delta still + /// converges, it just wastes bandwidth doing so, and this has been observed in + /// deployed contracts (freenet/freenet-core#5072, freenet/freenet-core#5056). /// /// - **MUST NOT** return a delta that contains the state, or whose size approaches the /// state's. This is the actual defect. A `get_state_delta` that ignores `summary`