Il formato binario di trasmissione: tipi di frame, codifica CBOR, semantica dei campi.
Formato di trasmissione
Ogni messaggio CapDAG è un frame CBOR con prefisso di lunghezza:
┌────────────────────────────────────────────────────┐
│ 4 byte: lunghezza del payload, u32 big-endian │
├────────────────────────────────────────────────────┤
│ N byte: mappa codificata in CBOR (chiavi intere) │
└────────────────────────────────────────────────────┘
Il prefisso di 4 byte indica il numero esatto di byte del payload CBOR che segue. Il ricevente legge esattamente quel numero di byte, quindi decodifica la mappa CBOR.
Esistono due limiti di dimensione:
- max_frame negoziato (predefinito 3.5 MB / 3,670,016 byte): impostato durante l’handshake. I frame più grandi vengono rifiutati sia da chi legge sia da chi scrive.
- Limite rigido (16 MB / 16,777,216 byte): un tetto assoluto per evitare l’esaurimento della memoria. Non può essere negoziato al rialzo.
I payload che superano max_frame devono essere suddivisi in chunk (vedi Streaming).
Sorgente: capdag-rs/src/bifaci/io.rs (encode_frame, decode_frame, write_frame, read_frame).
Struttura del frame
Il payload CBOR è una mappa con chiavi intere. Ogni frame ha tre chiavi obbligatorie (version, frame_type, id). Tutte le altre chiavi sono facoltative e la loro presenza dipende dal tipo di frame.
Tabella di riferimento delle chiavi
| Chiave | Nome | Tipo | Descrizione |
|---|---|---|---|
| 0 | version | u8 | Versione del protocollo. Sempre 6. Qualsiasi altro valore viene rifiutato. |
| 1 | frame_type | u8 | Discriminante che identifica il tipo di frame (vedi sotto). |
| 2 | id | bytes[16] o uint | ID della richiesta. UUID per le richieste, uint per i frame di controllo. |
| 3 | seq | u64 | Numero di sequenza all’interno di un flusso. Assegnato da SeqAssigner in uscita. |
| 4 | content_type | text | Tipo di contenuto del payload, in stile MIME. |
| 5 | meta | map | Mappa di metadati con chiavi stringa. Il contenuto dipende dal tipo di frame. |
| 6 | payload | bytes | Dati binari del payload. |
| 7 | len | u64 | Numero totale di byte per un trasferimento a chunk. Impostato solo sul primo chunk. |
| 8 | offset | u64 | Offset in byte di questo chunk all’interno dello stream. |
| 9 | eof | bool | True sull’ultimo chunk di uno stream o sui frame END. |
| 10 | cap | text | Cap URN per i frame REQ. Identifica quale capacità invocare. |
| 11 | stream_id | text | UUID che identifica uno stream all’interno di una richiesta. |
| 12 | media_urn | text | Media URN che identifica il tipo di dati di uno stream. |
| 13 | routing_id | bytes[16] o uint | XID assegnato dal RelaySwitch per l’instradamento. |
| 14 | chunk_index | u64 | Indice, a partire da zero, di questo chunk all’interno del suo stream. |
| 15 | chunk_count | u64 | Numero totale di chunk in uno stream. Impostato su STREAM_END. |
| 16 | checksum | u64 | Hash FNV-1a a 64 bit del payload del chunk. |
| 17 | is_sequence | bool | Indica se il produttore ha usato emit_list_item (true) o write (false). Solo STREAM_START. |
| 18 | force_kill | bool | Indica se Cancel termina forzatamente il processo della cartuccia. Solo Cancel. |
| 19 | credit | u64 | Concessione di controllo di flusso in unità di CHUNK. Solo frame Credit. |
| 20 | unbounded | bool | Lo stream non promette alcuna lunghezza (L16). Solo STREAM_START. |
| 21 | output | text | L’output che uno stream di risposta è: il media URN di quell’output nella definizione della cap. Solo nello STREAM_START di una risposta, dove è obbligatorio. |
Sorgente: capdag-rs/src/bifaci/frame.rs (modulo keys; struct Frame).
Tipi di frame
Il diagramma seguente mostra come i tipi di frame si relazionano all’interno del ciclo di vita di una richiesta:
stateDiagram-v2
[*] --> Hello : impostazione della connessione
Hello --> Req : connessione attiva
state "Ciclo di vita della richiesta" as RL {
Req --> StreamStart : apre uno stream
StreamStart --> Chunk : invia dati
Chunk --> Chunk : altri dati
Chunk --> StreamEnd : stream completato
StreamEnd --> StreamStart : apre un altro stream
StreamEnd --> End : tutti gli stream completati
Req --> End : risposta vuota
Req --> Err : richiesta fallita
StreamStart --> Err : richiesta fallita
Chunk --> Err : richiesta fallita
}
state "In qualsiasi momento" as AT {
Log : Log (avanzamento, info)
Heartbeat : Heartbeat (sonda di salute)
}
Hello (0)
Inviato una volta da ciascun lato durante l’impostazione della connessione. L’host invia per primo Hello (senza manifest); la cartuccia risponde con un Hello che include un manifest codificato in JSON in meta["manifest"]. Il manifest della cartuccia ha la struttura:
{
"name": "mycartridge",
"version": "1.0.0",
"description": "...",
"cap_groups": [
{
"name": "default",
"caps": [{"urn": "cap:effect=none", "title": "Identity", ...}, ...],
"adapter_urns": []
}
]
}
La mappa meta porta anche le proposte di limiti: max_frame, max_chunk e max_reorder_buffer. I due lati negoziano i limiti prendendo il minimo delle due proposte. L’HELLO di una cartuccia porta inoltre la voce obbligatoria pools: byte JSON che codificano la sua mappa completa dello stato dei pool di concorrenza (un pool singleton per ogni cap registrata, ogni pool condiviso dichiarato nel manifest e il pool riservato all), ciascuno con declared/configured/available facoltativo/active/queued; vedi 13.1). Una mappa mancante o malformata è una violazione del protocollo, non un invito a dedurre un valore predefinito. L’heartbeat è il canale di CONFIGURAZIONE della capacità: una sonda può portare desired_capacities (byte JSON: nome del pool → valore configured dell’operatore), e ogni risposta all’heartbeat porta la mappa pools aggiornata obbligatoria. Un nome di pool sconosciuto fa rifiutare l’intero lotto desiderato con un ERR UNKNOWN_POOL al posto della risposta. Vedi Handshake per la sequenza completa dell’handshake.
Hello usa id = Uint(0) e non è associato ad alcuna richiesta.
Sorgente: frame.rs (Frame::hello, Frame::hello_with_manifest).
Req (1)
Avvia un’invocazione di capacità. Il campo cap (chiave 10) porta il cap URN da invocare. La richiesta può includere un payload e un content_type per i dati inline, anche se la maggior parte degli argomenti arriva come stream separati (STREAM_START/CHUNK/STREAM_END).
Il routing_id (chiave 13) viene assegnato dal RelaySwitch ai confini di instradamento. Le cartucce che inviano invocazioni peer non impostano routing_id; viene aggiunto dall’infrastruttura.
Il cap URN viene validato al momento della costruzione: Frame::req() va in panic se l’URN è malformato, perché un URN non valido è un errore di programmazione, non una condizione di runtime.
Sorgente: frame.rs (Frame::req).
Chunk (3)
Porta una porzione di dati in streaming all’interno di uno stream con nome. Campi obbligatori:
stream_id(chiave 11): lo stream a cui appartiene questo chunk.chunk_index(chiave 14): posizione, a partire da zero, di questo chunk all’interno dello stream. Monotonamente crescente.checksum(chiave 16): hash FNV-1a a 64 bit dei byte del payload. Obbligatorio per il rilevamento della corruzione.
Campi facoltativi impostati dal livello di suddivisione in chunk:
offset(chiave 8): offset in byte dei dati di questo chunk all’interno dello stream complessivo.len(chiave 7): numero totale di byte. Impostato solo sul primo chunk (chunk_index = 0).eof(chiave 9): true sull’ultimo chunk.content_type(chiave 4): impostato solo sul primo chunk.
Sorgente: frame.rs (Frame::chunk, Frame::chunk_with_offset).
End (4)
Frame terminale che segnala che tutti gli stream di una richiesta sono completi. Nessun frame di flusso può seguire un END per lo stesso ID di richiesta (L4); i runtime scartano e contano i frame post-terminali, senza mai scriverli. Può portare un payload finale facoltativo.
Il campo eof è impostato a true. Un END riuscito porta exit_code = 0 in meta e PUÒ portare metadati terminali: progress (f64) e message. L’avanzamento terminale è l’evento di avanzamento finale autorevole (L5). Viaggia nel frame terminale stesso e quindi non può entrare in competizione con esso. Un END riuscito senza un progress esplicito vale come 1.0; un END fallito non ne sintetizza mai uno.
Sorgente: frame.rs (Frame::end).
Log (5)
Porta la diagnostica della sorgente e gli aggiornamenti di avanzamento. La mappa meta (chiave 5) contiene:
level(testo non vuoto): uno tra"info","warn","error","progress"o un livello definito dalla sorgente.message(testo non vuoto): messaggio leggibile da una persona.-
attribution_class(testo, obbligatorio per i LOG non di avanzamento): uno trainputresourceenvironmentinternal. arg_urn(testo non vuoto, facoltativo per i LOG non di avanzamento): l’unico argomento a cui la sorgente attribuisce la diagnostica.progress(float, facoltativo): un valore tra 0.0 e 1.0. Presente solo quandolevelè"progress".
I frame di avanzamento sono frame LOG con level = "progress". Vengono costruiti solo tramite l’API di avanzamento e non portano attribuzione diagnostica. Un LOG non di avanzamento deve portare un attribution_class valido; valori mancanti o sconosciuti sono violazioni del protocollo. arg_urn è presente solo quando la sorgente che emette sa che il messaggio riguarda un solo argomento. I riceventi non deducono mai nessuno dei due campi.
I frame LOG possono comparire in qualsiasi momento durante una richiesta, interfogliati con i frame CHUNK. Non influiscono sullo stream dei dati.
Sorgente: frame.rs (Frame::log, Frame::progress, log_level, log_message, log_progress, attribution_class, attribution_arg_urn).
Err (6)
Frame terminale che segnala un fallimento. Come per END, nessun altro frame può seguire per questa richiesta. La mappa meta porta:
code(testo non vuoto): codice di errore leggibile da una macchina.-
attribution_class(testo): a quale dominio appartiene il fallimento, dichiarato nel punto di definizione o di emissione. Uno trainputresourceenvironmentinternal. message(testo non vuoto): descrizione dell’errore leggibile da una persona.arg_urn(testo non vuoto, facoltativo): il media URN dell’unico argomento a cui la sorgente attribuisce il fallimento.
Tutti e tre i campi obbligatori sono rigorosi. Un attribution_class mancante o sconosciuto è una violazione del protocollo; i riceventi non fabbricano internal. Quando la sorgente non può attribuire il fallimento a un solo argomento, arg_urn è assente, mai vuoto. Vedi Gestione degli errori.
ERR ed END sono terminali mutuamente esclusivi. Una richiesta termina con esattamente uno dei due.
Sorgente: frame.rs (Frame::err, error_code, attribution_class, attribution_arg_urn, error_message).
Heartbeat (7)
Frame di monitoraggio dello stato di salute. Entrambi i lati possono inviare un Heartbeat; il ricevente deve rispondere con un Heartbeat che porta lo stesso id. Il CartridgeHostRuntime invia heartbeat ogni 30 secondi e si aspetta una risposta entro 10 secondi. Una cartuccia che non risponde viene contrassegnata come non in salute.
I frame Heartbeat aggirano la numerazione di sequenza perché non sono frame di flusso e non partecipano all’ordinamento.
Sorgente: frame.rs (Frame::heartbeat); vedi anche host_runtime.rs (HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT).
StreamStart (8)
Annuncia un nuovo stream con nome all’interno di una richiesta. Porta:
stream_id(chiave 11): un UUID che identifica in modo univoco questo stream all’interno della richiesta.media_urn(chiave 12): il media URN (vedi Media URN) che identifica il tipo di dati dello stream.output(chiave 21): su uno stream di risposta che non è l’output principale della cap, il media URN dell’output che questo stream è. Lo stream dell’output principale non portaoutput, e nemmeno uno stream di richiesta.
Una richiesta porta uno STREAM_START per ciascun argomento che le viene dato, ognuno etichettato con l’URN con cui la definizione della cap consegna quell’argomento (CapArg::stream_urn()), così che un op trovi ciascun argomento con un URN che dichiara. Uno stream di richiesta che porta output viene rifiutato dal runtime che lo legge: non è un argomento.
Una risposta porta al più uno STREAM_START per ciascun output dichiarato. Quello che non nomina alcun output è l’output principale della cap, l’output che dichiara il out= del cap URN: chi ha chiesto la cap ne legge la risposta senza possederne la definizione, che è ciò che fanno sia una cartridge che chiama un peer sia una riga di comando che stampa un risultato. Ogni altro stream nomina l’output che è. media_urn è sempre il tipo dei dati; output dice quale degli altri output della cap è uno stream, cosa che il solo tipo non può dire quando due output portano valori dello stesso tipo. Uno stream che nomina l’output principale, uno stream che nomina un output che la cap non dichiara e un secondo stream per lo stesso output sono ciascuno una violazione del contratto. Un output facoltativo che non scatta non ha alcuno stream. Vedi Input e output.
Sorgente: frame.rs (Frame::stream_start).
StreamEnd (9)
Segna la fine di uno stream specifico. Dopo questo frame, qualsiasi CHUNK per lo stesso stream_id è un errore di protocollo. Porta:
stream_id(chiave 11): lo stream che viene chiuso.chunk_count(chiave 15): il numero totale di frame CHUNK inviati in questo stream, secondo il conteggio del mittente. Il ricevente può verificare di aver ricevuto tutti i chunk. Uno stream annunciato comeunboundednon promette alcuna lunghezza e il suo STREAM_END omettechunk_count(L16).
Sorgente: frame.rs (Frame::stream_end).
RelayNotify (10)
Inviato da un RelaySlave al RelayMaster con cui è accoppiato per annunciare le capacità disponibili su quell’host. La mappa meta porta:
manifest(bytes): oggetto codificato in JSON con la struttura:{ "caps": ["cap:effect=none", "cap:in=...;out=...;generate", ...], "installed_cartridges": [] }capsè una lista piatta di stringhe di URN di capacità provenienti da tutte le cartucce in salute sull’host.installed_cartridgesporta i metadati dei processi delle cartucce. CAP_IDENTITY (cap:effect=none) deve essere presente; in caso contrario il RelaySwitch rifiuta la connessione.max_frame,max_chunk,max_reorder_buffer: limiti di protocollo per questa connessione di relay.
I frame RelayNotify vengono intercettati dal relay e non passano mai dall’altra parte. Il master memorizza il manifest e i limiti e li rende disponibili al RelaySwitch per le decisioni di instradamento.
Sorgente: frame.rs (Frame::relay_notify); vedi anche Topologia dei relay.
Credit (13)
Concede credito di controllo di flusso per stream al mittente di uno stream (vedi
Controllo di flusso). Porta il RID della richiesta
di destinazione nella chiave 2, uno stream_id facoltativo (chiave 11; se assente indica l’unico
stream della richiesta) e il numero concesso in credit (chiave 19, unità di CHUNK).
Gli intermediari inoltrano Credit da un capo all’altro come Cancel; non lo originano né lo
assorbono mai (L11). Un frame CREDIT senza numero di crediti viene rifiutato in decodifica.
Cancel (12)
Annulla una richiesta in corso, identificata dal suo RID nella chiave 2 (e dal suo XID
nella chiave 13 quando è instradata). Un Cancel agisce sempre: il runtime ricevente
annulla la richiesta in modo cooperativo (chiude il suo input, rilascia chi è in attesa
dei suoi crediti, annulla le sue chiamate peer e risponde con un ERR terminale una volta
che l’handler è uscito), oppure, con force_kill (chiave 18), l’host termina il
processo della cartuccia.
Un Cancel è attribuito esattamente come un frame ERR, tramite meta (chiave 5),
nel vocabolario della tassonomia dei fallimenti (vedi Gestione degli errori). Ogni voce
è facoltativa, e un Cancel non attribuito annulla comunque:
| chiave meta | significato | che cosa riceve l’ERR terminale |
|---|---|---|
code |
il codice terminale con cui terminerà la richiesta: CANCELLED (l’annullamento da parte di un operatore), ABORTED_COLLATERAL (un altro passo della stessa esecuzione è fallito), ABORTED (l’host ha deciso autonomamente) |
code; se assente → CANCELLED |
attribution_class |
di chi è la decisione o la colpa dell’annullamento: user per l’annullamento da parte di un operatore; la classe del fallimento originario per uno smantellamento collaterale; la classe dell’host per un’interruzione da parte dell’host (environment per un’esecuzione obsoleta, resource per la pressione sulla memoria) |
attribution_class; se assente → internal (non classificato significa “nostro”) |
message |
il motivo: il passo fallito per il caso collaterale, il motivo dell’host | message; se assente → “Request cancelled” |
CancelReason (frame.rs) è la forma tipizzata: user(force_kill),
collateral(class, detail), host(class, detail, force_kill),
unattributed(force_kill); Frame::cancel_reason() la rilegge da
qualsiasi Cancel, senza mai fallire: un token di classe sconosciuto viene decodificato come “nessuna classe”, una
meta mancante come non attribuito. Le cascate portano il motivo invariato (una chiamata peer
di una richiesta interrotta è collaterale dello stesso fallimento), e una morte per
terminazione forzata risponde a ogni richiesta in sospeso con il codice e la classe propri
del motivo. “Annullato” significa quindi sempre che una persona ha annullato; uno smantellamento
dovuto al fallimento di qualcos’altro lo dichiara ed è attribuito a quel fallimento.
Una richiesta termina una sola volta. L’END (o l’ERR) del suo handler e l’ERR di un annullamento sono alternative, mai entrambi: chi rivendica per primo la richiesta ne è il terminale. Un annullamento che arriva dopo che l’handler ha terminato la richiesta non aggiunge nulla, e una volta che un annullamento l’ha rivendicata il terminale proprio dell’handler non viene inviato. Qualsiasi frame per la richiesta dopo il suo terminale è un ritardatario post-terminale che chi scrive sopprime (L4). Un host in-process invia l’ERR dell’annullamento prima di chiudere l’input dell’handler, così un handler che si accorge per primo dell’annullamento non può terminare la richiesta alle proprie condizioni.
Un input troncato non è un input completo. Quando un annullamento (o la fine della
connessione) chiude l’input di una richiesta, ogni stream ancora aperto, e il pacchetto
di input stesso, fallisce con un errore di abbandono anziché terminare; una risposta
peer troncata fallisce allo stesso modo. Un handler non può mai prendere una parte del suo
input per l’intero input. Gli helper di accumulo (accumulate_input) rifiutano un input
che non ha raggiunto il suo END o che ha portato un ERR.
Sorgente: frame.rs (CancelReason, Frame::cancel, Frame::cancel_reason),
cartridge_runtime.rs (il ramo Cancel, TerminalClaim, i demux),
in_process_host.rs, relay_switch.rs (cancel_request).
CloseStream (14)
Chiude gli stream di input dal vivo di una richiesta, la chiusura del rubinetto (15.2 §Le esecuzioni si arrestano). Porta il RID della richiesta di destinazione nella chiave 2 e, facoltativamente, lo
stream_id (chiave 11) dell’unico stream di input il cui feed va chiuso; se assente
indica ogni feed dal vivo detenuto dalla richiesta. Il runtime ricevente chiude il o i
rubinetti dei feed corrispondenti: la cattura termina, l’handler esaurisce ciò che era già stato
catturato e la richiesta termina naturalmente con END e output completi.
Nulla viene annullato: lo stato della richiesta sul lato host resta intatto, nessun terminale viene
sintetizzato e il frame non si propaga mai in cascata alle chiamate peer (il rubinetto riguarda solo la
richiesta che porta il feed; i suoi peer si esauriscono naturalmente). Un CloseStream per una
richiesta che non detiene alcun feed aperto corrispondente viene registrato nel log e ignorato; non
diventa mai un’interruzione.
CloseStream è un frame di controllo non di flusso come Cancel e Credit (nessun seq,
nessun buffering di riordino), instradato per RID→XID verso la destinazione della richiesta e
inoltrato dagli intermediari senza interpretazione.
Sorgente: frame.rs (Frame::close_stream), cartridge_runtime.rs (il ramo
CloseStream), relay_switch.rs (stop_request_feeds).
RelayState (11)
Inviato da un RelayMaster al RelaySlave con cui è accoppiato per fornire informazioni sulle risorse dell’host. Il payload (chiave 6) porta un blob opaco (codificato in CBOR o JSON dall’engine) che contiene lo stato delle risorse, come i percorsi dei modelli, la disponibilità della GPU o le richieste di capacità.
Come RelayNotify, i frame RelayState vengono intercettati dal relay e non passano mai.
Sorgente: frame.rs (Frame::relay_state).
Policy (15)
Ciò che l’host consente alle cartucce che serve, così com’è adesso. Viaggia in una sola direzione, dall’engine verso le cartucce, e una cartuccia non lo invia mai. Oggi dice una cosa sola, meta["network"]:
network |
Una cartuccia può raggiungere |
|---|---|
offline |
nulla oltre questa macchina |
no_telemetry |
i servizi di terzi e quelli di MachineFabric, mai l’analisi d’uso |
full |
gli stessi, e l’analisi d’uso |
Un Policy porta id = Uint(0), nessun seq, e non passa dal buffer di riordino. Un Policy senza network, o con qualsiasi altro valore, viene rifiutato alla decodifica.
Chi lo invia, e quando. L’engine dà al RelaySwitch la sua policy corrente. Lo switch la invia a ogni master come primo frame dopo il RelayNotify di quel master, prima di instradargli qualsiasi cosa, e a ogni master ogni volta che cambia. I relay la lasciano passare. Un CartridgeHostRuntime tiene l’ultima ricevuta e la invia a ogni cartuccia che ha collegato, prima della prima richiesta che le instrada, e a ogni cartuccia ogni volta che cambia. Un host che funziona senza switch (la riga di comando, un host in-process) riceve la sua policy da chi lo costruisce. Un host non è mai senza policy quando instrada una richiesta: uno che non l’ha ricevuta quando gli arriva una richiesta la rifiuta come violazione del protocollo.
Cosa ne fa una cartuccia. Il runtime tiene l’ultimo Policy e un handler lo legge dal vivo. Il valore della richiesta non è un’istantanea. Il codice che raggiunge la rete chiede prima di ogni chiamata, così un passaggio a offline ferma un download o un flusso di quotazioni in corso alla chiamata successiva. Una chiamata che la policy non consente non viene fatta: fallisce con codice NETWORK_POLICY, classe user, e un messaggio che nomina la policy. Un REQ che raggiunge una cartuccia prima di qualsiasi Policy è una violazione del protocollo, tranne la verifica di identità dell’host (CAP_IDENTITY), che fa parte dell’handshake e può precederlo: un host collega le sue cartucce prima di avere una policy da dare loro. Una cartuccia eseguita da riga di comando, senza host, è sotto full, perché chi la esegue decide per sé.
Una cartuccia che non raggiunge mai la rete non ha nulla da chiedere. Riceve comunque i frame Policy e tiene l’ultimo.
Sorgente: frame.rs (Frame::policy, HostPolicy), cartridge_runtime.rs (il ramo Policy, Request::network), host_runtime.rs (set_policy), relay_switch.rs (set_policy).
MessageId
Gli ID dei frame si presentano in due varianti:
- Uuid: 16 byte casuali (UUID v4). Usato per gli ID delle richieste e degli stream, e per qualsiasi altra cosa debba essere univoca a livello globale.
- Uint: un intero a 64 bit. Usato per i frame di controllo (Hello usa
id = 0, Heartbeat può usare qualsiasi intero).
Gli UUID sono codificati come stringhe di byte CBOR (major type 2, 16 byte). Gli interi sono codificati come interi senza segno CBOR (major type 0).
Sorgente: frame.rs (enum MessageId).
Frame di flusso e frame di controllo
I frame sono classificati come di flusso o non di flusso. La distinzione è importante per la numerazione di sequenza e il riordino:
- Frame di flusso: Req, Chunk, End, Log, Err, StreamStart, StreamEnd. Partecipano alla sequenziazione: il
SeqAssignerassegna valori diseqmonotonamente crescenti per flusso, e ilReorderBufferai confini dei relay li riordina se arrivano fuori sequenza. - Frame non di flusso: Hello, Heartbeat, RelayNotify, RelayState, Cancel, Credit, CloseStream, Policy. Aggirano completamente l’assegnazione della sequenza (il loro
seqresta a 0) e non vengono bufferizzati dal buffer di riordino. Credit in particolare non deve mai accodarsi dietro i dati di cui regola il flusso.
Il metodo is_flow_frame() su Frame restituisce questa classificazione, e FrameType::is_terminal() indica quali frame di flusso terminano un flusso (End, Err).
Entrambe sono decisioni del modello dimostrato (formal/CapDAG/Bifaci/Flow.lean, FrameType.isFlow / isTerminal), generate in ogni mirror: nessun runtime mantiene una propria lista.
Sorgente: frame.rs (FrameType::is_flow, FrameType::is_terminal, Frame::is_flow_frame).
Checksum
Ogni frame CHUNK porta un checksum nella chiave 16: un hash FNV-1a a 64 bit dei byte del payload. Il mittente lo calcola tramite Frame::compute_checksum():
const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET_BASIS;
for &byte in data {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
Il ricevente verifica il checksum dopo la decodifica. Una discrepanza è un errore di protocollo, e il frame viene rifiutato.
FNV-1a è veloce e semplice, non crittograficamente robusto. Rileva la corruzione accidentale (bit invertiti, troncamenti) ma non è progettato per resistere a manomissioni intenzionali.
Sorgente: frame.rs (compute_checksum).