gioser

tawasuyu/hifas

Motor en Rust para decidir, con demostración y no a ojo, cuándo un conjunto de réplicas local-first puede operar sin coordinación y cuándo no — y, cuando la respuesta es "sí, dentro de límites", proveer el mecanismo que lo garantiza.

Rust 11Markdown 1TOML 187 Definiciones

⎇ 74acdb9dbca5fc2399bce6f3d65a3bf865c01c22 T11.md Markdown · 207 líneas · 11.6 KB Historia crudo

T11a: réplica por dispositivo (cura de F2) b4bd2e19b · Sergio ·

1# SDD — Addendum: T11 · Monotonía en la fuente (el supuesto que T10 daba gratis)
2
3**Extiende:** `DESIGN.md` + addenda T5–T7, T8, T9, T10.
4**Estado:** Diseño — habilitado por un "no rotundo" a la pregunta de T10:
5Tawasuyu/Hammer **no** garantizan réplicas single-writer con estado durable.
6**Precondición:** T10 cerrado (`certificado_dinamico` verde bajo el supuesto de fuente monótona-durable).
7**Última edición:** 2026-07-01
8
9---
10
11## A. El supuesto oculto de T10, ahora explícito
12
13El teorema de T10 (`total_avail` invariante ⟹ `valor ≥ K`) asume silenciosamente que cada `spent_i` es
14**durable y monótono en la fuente**: un escritor nunca olvida ni retrocede lo que ya cometió, y no hay dos
15escritores concurrentes sobre la misma celda. La fusión CRDT converge *dado* que cada fuente solo crece.
16Si la fuente misma retrocede o se bifurca, la premisa se viola **antes** de fusionar, y el certificado no
17aplica.
18
19El "no rotundo" rompe justo eso, por dos vías distintas + una frontera:
20
21| Fallo | Qué pasa | Cura | ¿Coordination-free? |
22|---|---|---|---|
23| **F1 · Rollback** | un nodo vuelve de snapshot viejo, `spent_i` baja, re-gasta → doble gasto | durabilidad (WAL, persistir antes del ack) | **sí**, local |
24| **F2 · Multi-writer** | dos dispositivos = "misma réplica", cada uno gasta su `avail` | réplica-por-dispositivo (identidad = conjunto de réplicas) | **sí**, sin fencing |
25| **Frontera · Clon** | un dispositivo clonado a dos copias vivas, misma celda, sin comunicarse | fencing por época: acota + detecta al reconectar | **no del todo** (ver §D) |
26
27Fencing NO es la cura de F1 ni de F2. Es solo la defensa acotada de la frontera del clon.
28
29---
30
31## B. F1 — Durabilidad: `spent` no puede olvidar (coordination-free)
32
33**Contrato:** un gasto se reconoce al usuario (`ack`) **solo después** de que `spent_i` esté persistido
34en almacenamiento durable (write-ahead log, fsync). Un crash puede perder gastos *no reconocidos* (nunca
35prometidos a nadie), jamás uno reconocido.
36
37Con esto, `spent_i` es monótono en la fuente por construcción: tras cualquier crash, al recuperar desde el
38WAL, `spent_i` es ≥ el valor en el último ack. No retrocede por debajo de lo comprometido. Cero
39coordinación — cada nodo fsyncea lo suyo.
40
41**Lo que T11 debe verificar (con dientes):** el modelo de crash de T10 era el *benigno* (un `grant` sin
42propagar). T11 mete el crash *malo*: una réplica que **revierte su propio `spent_i`** por debajo de un
43valor ya reconocido. El test debe demostrar que la capa de durabilidad lo **impide** (la recuperación
44restaura `spent_i` desde el WAL), o que se **detecta y rechaza** en el merge (§D). Un `spent_i` que
45retrocede sin ser atrapado = certificado roto.
46
47---
48
49## C. F2 — Réplica por dispositivo: la identidad es un conjunto (coordination-free)
50
51**No modeles la identidad como una réplica.** Un usuario/identidad es un **conjunto de réplicas-
52dispositivo**, cada una con su propia celda de presupuesto `b0[device]` y su propio `spent[device]`.
53
54Consecuencias:
55- Dos dispositivos de una persona son dos réplicas del esquema de T10, sin cambios. Cada uno gasta su
56  presupuesto local sin serializar con el otro. No hay "dos escritores de la misma celda" → no hay carrera
57  → no hace falta fencing para F2.
58- Un dispositivo seco rebalancea desde otro dispositivo de la misma identidad: es el rebalanceo edge-local
59  de T10b, ya construido.
60- Un dispositivo offline **estranca** su presupuesto (no lo puede gastar nadie más hasta que vuelva): es el
61  costo de disponibilidad honesto de `AVAILABILITY.md`, sin novedad.
62
63Esto convierte F2 en un no-problema por diseño, sin coordinación. La disciplina "single-writer" de T10 se
64cumple **a nivel de celda-dispositivo**, no a nivel de identidad.
65
66---
67
68## D. La frontera del clon (lo que NINGÚN sistema coordination-free puede cerrar)
69
70F2 resuelve *dos dispositivos distintos*. Queda el caso duro: **una celda con dos escritores vivos** — un
71dispositivo restaurado a dos copias (VM snapshot, proceso duplicado) que comparten `device_id` y celda, y
72**no se comunican**. Cada clon puede gastar la celda entera → doble gasto de un presupuesto.
73
74**Teorema de imposibilidad (hay que decirlo).** Ningún mecanismo local puede detectar un clon silencioso
75antes de que los clones se comuniquen. Es el mismo CAP que el sobregiro particionado, ahora dentro de una
76celda: sin comunicación, no hay forma de saber que el presupuesto ya se gastó en otra copia.
77
78**Lo mejor posible — fencing por época device-local, enforced en el merge:**
79- Cada instancia viva de un dispositivo tiene una **época** monótona, incrementada y persistida en cada
80  arranque (`epoch`, en el WAL de §B).
81- Cada gasto lleva `(device_id, epoch, seq)`. `spent` se indexa por `(device_id, epoch)`.
82- El **merge** es el punto de enforcement (no un servidor central, que no existe): al fusionar, si aparecen
83  dos linajes con el mismo `device_id` y épocas distintas, el de **época menor es un clon obsoleto** — sus
84  gastos post-bifurcación se **rechazan/aíslan** y se levanta una alarma (`CloneDetected`).
85- Esto **no previene** el doble gasto mientras los clones están aislados; lo **acota** (a una celda por
86  clon no detectado) y lo **detecta** en cuanto reconectan. Convierte "silencioso e ilimitado" en "acotado
87  y delatado".
88
89**Honestidad (no repetir el sobre-reclamo).** El fencing token clásico (Kleppmann) asume un recurso
90linealizable que rechaza tokens viejos; tu malla no lo tiene, por eso el enforcement vive en el merge y la
91garantía es *bounded-detect*, no *prevent*. Y generar épocas *globalmente* monótonas pediría consenso — por
92eso son **device-local** (cada dispositivo cuenta sus propios arranques), lo que es coordination-free a
93costa de que la detección sea al reconectar, no instantánea.
94
95---
96
97## E. El terminus honesto del proyecto
98
99Tras T11, el enunciado correcto —el que respeta CAP y no promete de más— es:
100
101> **Seguridad coordination-free módulo un supuesto físico nombrado:** cada celda de presupuesto tiene un
102> único escritor durable. Bajo ese supuesto (garantizado por §B durabilidad + §C réplica-por-dispositivo),
103> el certificado de T10 aplica sin coordinación. Cuando el supuesto se rompe (clon, §D), el daño está
104> **acotado a una celda por clon no detectado y se detecta al reconectar** — nunca es silencioso ni
105> ilimitado.
106
107Ese es el sitio honesto: no "seguro pase lo que pase", sino "seguro salvo esta condición física, y aquí
108está exactamente qué pasa cuando se rompe y cuánto cuesta".
109
110---
111
112## F. Modelo de tipos (bocetos)
113
114```rust
115/// spent indexado por (device, epoch): detecta linajes bifurcados en el merge.
116pub struct DurableSpent {
117    pub device_id: DeviceId,
118    pub epoch: u64,                     // monótona por dispositivo, persistida al arrancar
119    pub entries: BTreeMap<u64, i64>,    // seq -> monto, solo crece
120}
121
122pub struct WriteAheadLog { /* persiste spent+epoch ANTES del ack */ }
123impl WriteAheadLog {
124    pub fn commit_spend(&mut self, seq: u64, amount: i64) -> AckToken { /* fsync, luego ack */ }
125    pub fn recover(&self) -> DurableSpent { /* spent_i nunca por debajo del último ack */ }
126}
127
128pub enum MergeOutcome {
129    Ok,
130    CloneDetected { device_id: DeviceId, stale_epoch: u64, live_epoch: u64 },
131}
132
133/// EscrowState de T10, ahora con spent durable por (device, epoch) y detección de clon en merge.
134impl EscrowState {
135    pub fn merge(&mut self, other: &EscrowState) -> MergeOutcome { /* ... */ }
136}
137```
138
139---
140
141## G. Tests (el modelo de crash MALO, no el benigno)
142
1431. **Durabilidad contra rollback (§B).** `proptest`: una réplica gasta y ackea, "crashea" (se descarta su
144   estado en memoria), recupera desde el WAL, y re-opera. Aserción: `spent_i` recuperado ≥ el del último
145   ack; nunca re-gasta lo ya comprometido. Incluye el adversario: intentar restaurar un snapshot *por
146   debajo* del WAL debe ser rechazado, no aceptado.
1472. **Réplica-por-dispositivo (§C).** Dos dispositivos de una identidad, cada uno con su celda, gastan
148   concurrentemente: `total_avail` de la identidad = Σ celdas − Σ spent, invariante. Rebalanceo entre
149   dispositivos conserva. Ningún gasto de un dispositivo pisa el `avail` del otro.
1503. **Detección de clon (§D).** Bifurca un `device_id` en dos épocas, cada clon gasta su celda entera
151   aislado, luego fusiona: el merge devuelve `CloneDetected`, aísla los gastos de la época menor, y
152   reporta el daño acotado (≤ una celda). Aserción: tras la detección, el estado reconciliado no propaga el
153   doble gasto hacia el resto de la malla.
1544. **No-regresión del certificado dinámico de T10** bajo el nuevo modelo de `spent` durable-por-época:
155   sigue verde con fuente monótona; y **falla ruidosamente** (no silenciosamente) cuando se inyecta un
156   rollback no atrapado — para probar que el test de §G.1 tiene dientes.
157
158---
159
160## H. M4 — el contrato, ahora escribible en piedra
161
162Solo con T11 el adaptador a Tawasuyu/Hammer puede declarar sus precondiciones honestamente:
163
164**Contrato del adaptador (`M4`):**
165- **Requiere** WAL durable de `spent` por dispositivo (persistir antes de ack). Sin esto, sin garantía.
166- **Requiere** identidad-como-conjunto-de-dispositivos: cada dispositivo es una réplica con su celda.
167- **Garantiza** seguridad coordination-free del invariante bajo el supuesto de un-escritor-durable-por-celda.
168- **Acota y detecta** (no previene) el doble gasto por clon; expone `CloneDetected` como señal que la capa
169  de aplicación debe manejar (compensación, alarma). Documentado en `BOUNDARY.md`.
170- **Limita** disponibilidad bajo partición al presupuesto local (`AVAILABILITY.md`).
171
172Si Tawasuyu/Hammer no pueden dar durabilidad por dispositivo, el adaptador debe **rehusar** en vez de
173prometer seguridad que no tiene. Esa negativa es parte del diseño, no un fallo.
174
175---
176
177## I. Tickets
178
179**T11a — Réplica por dispositivo (F2).**
180"Refactoriza el modelo de réplica: `identity = conjunto de device-replicas`, cada una con su `b0[device]` y
181`spent[device]`. Ajusta el reparto (`Σ b0 ≤ S` sobre dispositivos) y el rebalanceo (T10b) para operar entre
182dispositivos de una identidad. Test §G.2. Ninguna celda tiene más de un escritor lógico."
183
184**T11b — Durabilidad de `spent` (F1).**
185"Implementa `WriteAheadLog` (§F): `commit_spend` persiste antes de devolver `AckToken`; `recover`
186reconstruye `spent_i` ≥ último ack. Conecta al gasto: no hay ack sin commit durable. Test §G.1, incluido el
187adversario que intenta restaurar por debajo del WAL. `spent_i` monótono en la fuente, garantizado."
188
189**T11c — Fencing por época + detección de clon (frontera §D).**
190"Indexa `spent` por `(device_id, epoch)`; `epoch` monótona por dispositivo, persistida en el WAL al
191arrancar. `merge` devuelve `CloneDetected` cuando halla dos épocas para un `device_id`, aísla la época
192menor y acota el daño a una celda. Escribe `BOUNDARY.md`: el teorema de imposibilidad (no hay detección
193coordination-free de clon silencioso), la garantía real (bounded + detect-on-reconnect), y por qué las
194épocas son device-local (evitar consenso). Tests §G.3 y §G.4. Cuando pase, actualiza `STRONG_RESULT.md`
195con el terminus honesto de §E y declara el contrato de M4 (§H) listo para escribir."
196
197---
198
199## J. Referencias
200
201- M. Kleppmann — *How to do distributed locking* (2016) y *Designing Data-Intensive Applications*, cap. 8–9
202  (fencing tokens; y el punto de que su enforcement clásico necesita un recurso linealizable — de ahí que
203  aquí viva en el merge).
204- P. E. O'Neil 1986; V. Balegas et al. SRDS 2015 (escrow / Bounded Counter, base de T9–T10).
205- E. Brewer — CAP (el teorema que §D y §E respetan: safety > availability bajo partición).
206- (De addenda previos) Shapiro et al. 2011 (CRDT, base de la representación durable-monótona); Bailis et
207  al. 2014; Ghrist–Riess 2022; Hansen–Ghrist 2019; Hellerstein–Alvaro.

T11.md

SDD — Addendum: T11 · Monotonía en la fuente (el supuesto que T10 daba gratis)

Extiende: DESIGN.md + addenda T5–T7, T8, T9, T10. Estado: Diseño — habilitado por un "no rotundo" a la pregunta de T10: Tawasuyu/Hammer no garantizan réplicas single-writer con estado durable. Precondición: T10 cerrado (certificado_dinamico verde bajo el supuesto de fuente monótona-durable). Última edición: 2026-07-01


A. El supuesto oculto de T10, ahora explícito

El teorema de T10 (total_avail invariante ⟹ valor ≥ K) asume silenciosamente que cada spent_i es durable y monótono en la fuente: un escritor nunca olvida ni retrocede lo que ya cometió, y no hay dos escritores concurrentes sobre la misma celda. La fusión CRDT converge dado que cada fuente solo crece. Si la fuente misma retrocede o se bifurca, la premisa se viola antes de fusionar, y el certificado no aplica.

El "no rotundo" rompe justo eso, por dos vías distintas + una frontera:

FalloQué pasaCura¿Coordination-free?
F1 · Rollbackun nodo vuelve de snapshot viejo, spent_i baja, re-gasta → doble gastodurabilidad (WAL, persistir antes del ack)sí, local
F2 · Multi-writerdos dispositivos = "misma réplica", cada uno gasta su availréplica-por-dispositivo (identidad = conjunto de réplicas)sí, sin fencing
Frontera · Clonun dispositivo clonado a dos copias vivas, misma celda, sin comunicarsefencing por época: acota + detecta al reconectarno del todo (ver §D)

Fencing NO es la cura de F1 ni de F2. Es solo la defensa acotada de la frontera del clon.


B. F1 — Durabilidad: spent no puede olvidar (coordination-free)

Contrato: un gasto se reconoce al usuario (ack) solo después de que spent_i esté persistido en almacenamiento durable (write-ahead log, fsync). Un crash puede perder gastos no reconocidos (nunca prometidos a nadie), jamás uno reconocido.

Con esto, spent_i es monótono en la fuente por construcción: tras cualquier crash, al recuperar desde el WAL, spent_i es ≥ el valor en el último ack. No retrocede por debajo de lo comprometido. Cero coordinación — cada nodo fsyncea lo suyo.

Lo que T11 debe verificar (con dientes): el modelo de crash de T10 era el benigno (un grant sin propagar). T11 mete el crash malo: una réplica que revierte su propio spent_i por debajo de un valor ya reconocido. El test debe demostrar que la capa de durabilidad lo impide (la recuperación restaura spent_i desde el WAL), o que se detecta y rechaza en el merge (§D). Un spent_i que retrocede sin ser atrapado = certificado roto.


C. F2 — Réplica por dispositivo: la identidad es un conjunto (coordination-free)

No modeles la identidad como una réplica. Un usuario/identidad es un conjunto de réplicas- dispositivo, cada una con su propia celda de presupuesto b0[device] y su propio spent[device].

Consecuencias:

Esto convierte F2 en un no-problema por diseño, sin coordinación. La disciplina "single-writer" de T10 se cumple a nivel de celda-dispositivo, no a nivel de identidad.


D. La frontera del clon (lo que NINGÚN sistema coordination-free puede cerrar)

F2 resuelve dos dispositivos distintos. Queda el caso duro: una celda con dos escritores vivos — un dispositivo restaurado a dos copias (VM snapshot, proceso duplicado) que comparten device_id y celda, y no se comunican. Cada clon puede gastar la celda entera → doble gasto de un presupuesto.

Teorema de imposibilidad (hay que decirlo). Ningún mecanismo local puede detectar un clon silencioso antes de que los clones se comuniquen. Es el mismo CAP que el sobregiro particionado, ahora dentro de una celda: sin comunicación, no hay forma de saber que el presupuesto ya se gastó en otra copia.

Lo mejor posible — fencing por época device-local, enforced en el merge:

Honestidad (no repetir el sobre-reclamo). El fencing token clásico (Kleppmann) asume un recurso linealizable que rechaza tokens viejos; tu malla no lo tiene, por eso el enforcement vive en el merge y la garantía es bounded-detect, no prevent. Y generar épocas globalmente monótonas pediría consenso — por eso son device-local (cada dispositivo cuenta sus propios arranques), lo que es coordination-free a costa de que la detección sea al reconectar, no instantánea.


E. El terminus honesto del proyecto

Tras T11, el enunciado correcto —el que respeta CAP y no promete de más— es:

Seguridad coordination-free módulo un supuesto físico nombrado: cada celda de presupuesto tiene un único escritor durable. Bajo ese supuesto (garantizado por §B durabilidad + §C réplica-por-dispositivo), el certificado de T10 aplica sin coordinación. Cuando el supuesto se rompe (clon, §D), el daño está acotado a una celda por clon no detectado y se detecta al reconectar — nunca es silencioso ni ilimitado.

Ese es el sitio honesto: no "seguro pase lo que pase", sino "seguro salvo esta condición física, y aquí está exactamente qué pasa cuando se rompe y cuánto cuesta".


F. Modelo de tipos (bocetos)

/// spent indexado por (device, epoch): detecta linajes bifurcados en el merge.
pub struct DurableSpent {
    pub device_id: DeviceId,
    pub epoch: u64,                     // monótona por dispositivo, persistida al arrancar
    pub entries: BTreeMap<u64, i64>,    // seq -> monto, solo crece
}

pub struct WriteAheadLog { /* persiste spent+epoch ANTES del ack */ }
impl WriteAheadLog {
    pub fn commit_spend(&mut self, seq: u64, amount: i64) -> AckToken { /* fsync, luego ack */ }
    pub fn recover(&self) -> DurableSpent { /* spent_i nunca por debajo del último ack */ }
}

pub enum MergeOutcome {
    Ok,
    CloneDetected { device_id: DeviceId, stale_epoch: u64, live_epoch: u64 },
}

/// EscrowState de T10, ahora con spent durable por (device, epoch) y detección de clon en merge.
impl EscrowState {
    pub fn merge(&mut self, other: &EscrowState) -> MergeOutcome { /* ... */ }
}

G. Tests (el modelo de crash MALO, no el benigno)

  1. Durabilidad contra rollback (§B). proptest: una réplica gasta y ackea, "crashea" (se descarta su estado en memoria), recupera desde el WAL, y re-opera. Aserción: spent_i recuperado ≥ el del último ack; nunca re-gasta lo ya comprometido. Incluye el adversario: intentar restaurar un snapshot por debajo del WAL debe ser rechazado, no aceptado.
  2. Réplica-por-dispositivo (§C). Dos dispositivos de una identidad, cada uno con su celda, gastan concurrentemente: total_avail de la identidad = Σ celdas − Σ spent, invariante. Rebalanceo entre dispositivos conserva. Ningún gasto de un dispositivo pisa el avail del otro.
  3. Detección de clon (§D). Bifurca un device_id en dos épocas, cada clon gasta su celda entera aislado, luego fusiona: el merge devuelve CloneDetected, aísla los gastos de la época menor, y reporta el daño acotado (≤ una celda). Aserción: tras la detección, el estado reconciliado no propaga el doble gasto hacia el resto de la malla.
  4. No-regresión del certificado dinámico de T10 bajo el nuevo modelo de spent durable-por-época: sigue verde con fuente monótona; y falla ruidosamente (no silenciosamente) cuando se inyecta un rollback no atrapado — para probar que el test de §G.1 tiene dientes.

H. M4 — el contrato, ahora escribible en piedra

Solo con T11 el adaptador a Tawasuyu/Hammer puede declarar sus precondiciones honestamente:

Contrato del adaptador (M4):

Si Tawasuyu/Hammer no pueden dar durabilidad por dispositivo, el adaptador debe rehusar en vez de prometer seguridad que no tiene. Esa negativa es parte del diseño, no un fallo.


I. Tickets

T11a — Réplica por dispositivo (F2). "Refactoriza el modelo de réplica: identity = conjunto de device-replicas, cada una con su b0[device] y spent[device]. Ajusta el reparto (Σ b0 ≤ S sobre dispositivos) y el rebalanceo (T10b) para operar entre dispositivos de una identidad. Test §G.2. Ninguna celda tiene más de un escritor lógico."

T11b — Durabilidad de spent (F1). "Implementa WriteAheadLog (§F): commit_spend persiste antes de devolver AckToken; recover reconstruye spent_i ≥ último ack. Conecta al gasto: no hay ack sin commit durable. Test §G.1, incluido el adversario que intenta restaurar por debajo del WAL. spent_i monótono en la fuente, garantizado."

T11c — Fencing por época + detección de clon (frontera §D). "Indexa spent por (device_id, epoch); epoch monótona por dispositivo, persistida en el WAL al arrancar. merge devuelve CloneDetected cuando halla dos épocas para un device_id, aísla la época menor y acota el daño a una celda. Escribe BOUNDARY.md: el teorema de imposibilidad (no hay detección coordination-free de clon silencioso), la garantía real (bounded + detect-on-reconnect), y por qué las épocas son device-local (evitar consenso). Tests §G.3 y §G.4. Cuando pase, actualiza STRONG_RESULT.md con el terminus honesto de §E y declara el contrato de M4 (§H) listo para escribir."


J. Referencias