Ledger Replication Messages
The Vultron SYNC substrate replicates the case event log to all active participants so that each participant's local DataLayer stays consistent with the canonical log held by the CASE_MANAGER. The mechanism is described in ADR-0077.
These messages are infrastructure, not protocol messages in the 28-shorthand sense. They carry other protocol messages as ledger entries rather than being a Vultron protocol step in their own right (see ADR-0083).
For the acknowledgment semantics see Faults and Acknowledgments.
How replication works
- The CASE_MANAGER commits a new log entry and fans it out:
Announce(CaseLedgerEntry)→ each participant. - A participant first recomputes the entry's hash from its content.
An entry that does not reproduce its
entry_hashis refused and logged, with noReject(SYNC-03-006, CLP-04-008). - A participant whose local
prev_log_hashmatches the incoming entry'sprev_log_hashsilently accepts — hash-chain continuity is the implicit positive acknowledgment (MSM-05-002). - A participant whose hashes do not match rejects:
Reject(CaseLedgerEntry)withcontext= its last accepted hash, so the CASE_MANAGER can replay the gap (SYNC-03-001, SYNC-03-002).
A participant's replica of the case is the replay of its ledger: the case as it stood when the replica was seeded, plus every ledger entry since, applied in ledger order (ADR-0124, CM-23-016). A replica applies an entry by copying the objects it carries. Applying an entry mints no id, reads no clock or local policy, and runs no lifecycle logic, so two replicas at the same ledger position hold identical case files.
Each entry records one activity with its object.
An embargo act's entry carries its whole step on the embargo register: a termination's entry also cancels every open proposal, and an activation's entry also supersedes the embargo it replaces.
Each consent row the CASE_MANAGER changes as a consequence of an act is its own CASE_MANAGER Create(EmbargoConsent) or Update(EmbargoConsent) entry after the act's entry, and a replica copies that row rather than deciding the change itself (CLP-07-013, EP-09-007).
The same holds for participants: an act that moves a participant's RM state, such as a report verdict, an engagement decision or a Leave, is followed by an add_participant_status_to_participant entry for each status the CASE_MANAGER wrote, and a record the CASE_MANAGER creates is its own Create(CaseParticipant) entry; a replica copies each, ids and times included (CLP-07-013, CM-23-016).
An embargo or consent entry whose starting state does not match the replica's case is not patched over: the Announce that carried it is held as a HeldLedgerAnnounce.
Occasions
| When… | Send | Send to | Sent by | Distinguished by | Details |
|---|---|---|---|---|---|
| the Case Manager has committed a case event to the case ledger | Announce(CaseLedgerEntry) |
a participant | the case manager | — | Announce case ledger entry |
| a received ledger entry does not chain onto the Participant's own ledger | Reject(CaseLedgerEntry) |
the case manager | an active participant, including one that has left the case | — | Reject case ledger entry |
Announce case ledger entry
Fans out the newly committed ledger entry to each active participant.
- When… the Case Manager has committed a case event to the case ledger
- Send:
Announce(CaseLedgerEntry) - Send to: a participant
- Sent by: the case manager
- Mapping status: none
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Announce",
"id": "urn:uuid:b8f1f5ea-27f5-4f7a-b6d2-62036ba3aeef",
"name": "https://vultron.example/case-actors/vendorco-case-actor Announce urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/log/0",
"actor": "https://vultron.example/case-actors/vendorco-case-actor",
"object": {
"id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/log/0",
"type": "CaseLedgerEntry",
"published": "2024-01-01T12:00:00+00:00",
"updated": "2026-10-23T20:17:38+00:00",
"caseId": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
"logIndex": 0,
"logObjectId": "https://vultron.example/activities/00000000-0000-0000-0000-000000000001",
"semanticType": "create_case",
"payloadSnapshot": {},
"prevLogHash": "0000000000000000000000000000000000000000000000000000000000000000",
"entryHash": "6a4f6f8e7016eb9b177a78414c7c985f52eeccfe02484be2aa56e76f9363eb7a",
"receivedAt": "2024-01-01T12:00:00Z",
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
}
}
- Triggering transition: triggered internally after any event is recorded to the log — not a protocol shorthand.
- Spec: SYNC-09-002.
- Pattern:
AnnounceLogEntryPatterninvultron/wire/as2/extractor/_instances.py. - Factory:
announce_log_entry_activityinvultron/wire/as2/factories/sync.py. - Example artifact: announce_case_ledger_entry.json.
Each entry carries a content hash and the hash of its predecessor, forming a chain.
A participant verifies the chain on receipt and holds the Announce of any entry that arrives before its predecessor as a HeldLedgerAnnounce, replaying that Announce when the predecessor lands (ADR-0037, SYNC-14-009).
Reject case ledger entry
Tells the Case Manager that the entry's previous hash does not match the sender's ledger tail, and the Case Manager replays every entry after the last accepted hash. This negative acknowledgment serves the purpose of the formal EK and CK messages.
- When… a received ledger entry does not chain onto the Participant's own ledger
- Send:
Reject(CaseLedgerEntry) - Send to: the case manager
- Sent by: an active participant, including one that has left the case
- Mapping status: evolved
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Reject",
"id": "urn:uuid:3053084d-5273-47cc-b72c-a77786e7eccf",
"name": "https://vultron.example/organizations/vendorco Reject urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/log/0",
"context": "0000000000000000000000000000000000000000000000000000000000000000",
"actor": "https://vultron.example/organizations/vendorco",
"object": {
"id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/log/0",
"type": "CaseLedgerEntry",
"published": "2024-01-01T12:00:00+00:00",
"updated": "2026-10-23T20:17:38+00:00",
"caseId": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
"logIndex": 0,
"logObjectId": "https://vultron.example/activities/00000000-0000-0000-0000-000000000001",
"semanticType": "create_case",
"payloadSnapshot": {},
"prevLogHash": "0000000000000000000000000000000000000000000000000000000000000000",
"entryHash": "6a4f6f8e7016eb9b177a78414c7c985f52eeccfe02484be2aa56e76f9363eb7a",
"receivedAt": "2024-01-01T12:00:00Z",
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
}
}
- Context: the
contextfield carries the participant's last accepted hash so the CASE_MANAGER can determine which entries to replay (SYNC-03-001, SYNC-03-002). - Triggering transition: triggered by hash-chain mismatch detection — not a protocol shorthand.
- Spec: SYNC-03-001, SYNC-03-002, SYNC-03-005.
- Pattern:
RejectLogEntryPatterninvultron/wire/as2/extractor/_instances.py. - Factory:
reject_log_entry_activityinvultron/wire/as2/factories/sync.py. - Example artifact: reject_case_ledger_entry.json.
A participant that holds no entry has none to reject.
After its case is replaced by a copy ahead of its chain, it sends a Reject with no object, an empty context and the case as its target, which asks for a replay from genesis (SYNC-16-007).
A Reject with no object and a non-empty context is refused when it is parsed.
Buffering takes priority over rejection (ADR-0037).
A Reject is sent only for an entry genuinely missing from the chain, never for one that is merely out of order, so an out-of-order delivery does not start a reject-and-replay cycle.
The CASE_MANAGER acts on a Reject(CaseLedgerEntry) only when its sender is an active participant in the case (SYNC-03-005, CM-10-004).
From any other sender it reports REFUSED, records no replication state, and replays no entry and announces no case to that sender.
The check runs before any write, so a stranger cannot obtain case content by sending a Reject with an empty last-accepted hash.
Case seeding
A participant whose replica is being initialized for the first time — after it
accepts an Invite, or after a Create(CaseProposal) is accepted — receives
Announce(VulnerabilityCase) rather than a ledger entry.
Any Announce(CaseLedgerEntry) that arrives before the seed is held as a HeldLedgerAnnounce, keyed by prev_log_hash, and replayed once the genesis hash can be computed from the seeded case (ADR-0059).
- Pattern:
AnnounceVulnerabilityCasePatterninvultron/wire/as2/extractor/_instances.py. - Factory:
announce_vulnerability_case_activityinvultron/wire/as2/factories/case.py. - Spec: SYNC-09-001.
Each Announce(VulnerabilityCase) carries the case with its ledgerPosition: the logIndex and entryHash of the last ledger entry its case file reflects (SYNC-16-005).
A participant that already holds the case judges the copy by that position.
A copy the participant is already past is skipped, a copy naming a different entry at an index the participant holds is refused, and any other copy replaces the case, position included (SYNC-16-006).
After a replacement whose position is ahead of its chain, the participant sends Reject(CaseLedgerEntry) naming its own tail and stores each entry the copy already reflects as a no-op, without moving its case's position back (SYNC-16-007).
The activity and its rendered example are documented under Announce vulnerability case.
See also
- Case Ledger Synchronization — why the ledger is ordered this way and what the buffering guarantees
- ADR-0037 — Buffer Out-of-Order Ledger Entries
- ADR-0059 — Buffer Pre-Genesis Ledger Entries
- Spec:
specs/sync-ledger-replication.yaml(SYNC-09, SYNC-10, SYNC-14, SYNC-15)