syntax = "proto3"; package tidal.replication.v1; // Globally unique identifier for a WAL segment. message WalSegmentId { uint32 region_id = 1; uint32 shard_id = 2; uint64 seqno = 3; } // A WAL segment ready for shipping to a peer shard. message ShipSegmentRequest { WalSegmentId id = 1; bytes payload = 2; uint64 event_count = 3; // The segment's authoritative last WAL sequence number, computed by the // leader from the ORIGINAL (pre-community-overlay-filter) bytes. Lets the // receiver advance its replication-lag leader high-water-mark even for an // all-local segment that filters to an empty payload (obs-REPL-1). A 0 value // (e.g. from an older sender that omits this field) means "unknown" and the // receiver falls back to the per-batch boundaries it decodes. uint64 leader_last_seq = 4; // The shipping stream's baseline (m11p2): the WAL seqno at which this // leader's stream STARTED. Non-zero only on catch-up stream chunks from a // promoted leader; seqnos at or below it are pre-stream history the // receiver jumps its frontier past instead of treating as a gap. 0 (the // default, and what every live unary ship carries) means "stream from the // beginning". uint64 stream_baseline = 5; // The sender's leadership term (m11p4 fencing). 0 = the topology era or a // pre-m11p4 sender; a receiver at term >= 1 rejects term-0 traffic, and any // receiver rejects a term below its own (FAILED_PRECONDITION) — a deposed // leader's ships can never apply. uint64 term = 6; // The region claiming leadership of `term` (m11p4). uint32 leader_region = 7; } // Response to a segment shipment. message ShipSegmentResponse { bool accepted = 1; // The receiver's contiguous applied seqno for the request's source shard at // acceptance time (m11p2): a monotonic hint the sender folds into its acked // frontier so retries of already-applied data prune and heal needs no // separate status fetch. 0 = unknown (older peer / no applied source wired). uint64 applied_seqno = 2; // The receiver's current term (m11p4): a value above the sender's term is // the sender's step-down signal. uint64 term = 3; } // Request to stream segments from a given sequence number. message StreamRequest { uint32 shard_id = 1; uint64 from_seqno = 2; // The puller's current term (m11p4). The source serves only when the terms // match: a stale puller must rejoin first, and a stale SOURCE must step // down rather than serve its possibly-divergent tail. uint64 term = 3; } // Heartbeat request matching ControlPlane's ShardStats. message HeartbeatRequest { uint32 shard_id = 1; uint32 region_id = 2; uint64 entity_count = 3; double signal_throughput_eps = 4; uint64 disk_bytes = 5; // Replication lag per peer region (region_id -> lag in events). map replication_lag = 6; uint64 last_heartbeat_ns = 7; // The sender's leadership term (m11p4): a leader heartbeat is the lease // assertion + failure-detector input. 0 = the topology era / a non-leader // health probe. uint64 term = 8; // The region asserting leadership of `term` (m11p4). uint32 leader_region = 9; // The asserted term's activation stream baseline (m11p4): immutable for // the term; a joining follower jumps its applied frontier for the leader's // stream to it (seqnos at or below are pre-stream history). uint64 stream_baseline = 10; // The leader's ELECTION-TIME log position (m11p4): the term and frontier // of its log in the PREVIOUS stream's numbering — the same pair the vote // restriction compares. A joining node is DIVERGENT iff its own position // exceeds this lexicographically (it holds entries the new leadership's // history does not subsume). uint64 prev_log_term = 11; uint64 prev_log_seq = 12; } // Heartbeat acknowledgement. message HeartbeatResponse { bool acknowledged = 1; // The responder's current term (m11p4): above the sender's term = the // sender's step-down signal. uint64 term = 2; // Whether the responder accepted the sender's leadership assertion // (false = the sender's term is stale). bool accepted = 3; // The responder binary's capability bit-field (m11p5). See the BIT REGISTRY // below `WalShipping`. proto3 zero-default = a pre-m11p5 follower (incapable // of folding kind-4 membership records): a `0` here gates the leader off // appending any conf-change until every voter reports the kind-4 bit. uint64 capabilities = 4; // The TYPED REMOVED SIGNAL (m11p5 §3.3): true iff the responder's APPLIED // roster lists the heartbeat SENDER (`region_id`) as a `Removed` member. // proto3 zero-default = false = "not removed" = backward-compatible with a // pre-m11p5 responder. A sender that sees `true` was decommissioned: it // flips its readiness to 503 and suppresses campaigning WITHOUT latching a // reseed marker (a remove is not a reseed). This is the delivery path for a // node that MISSED the `Removed` record in the stream (it was down during // the removal-delivery grace) — it learns of its removal from the leader's // heartbeat refusal rather than the log. bool removed = 5; } // A follower's self-report of its durable frontier (m11p3). // // Pushed by the receiver once per apply round — fully decoupled from ship // acks, so the leader's quorum commit index stays fresh even when its // outbound ships stall (gap-parked follower, quiet leader, pull catch-up). message AppliedReport { // The reporting node's shard id. uint32 reporter_shard = 1; // The stream's source shard (the leader being reported to). uint32 source_shard = 2; // The reporter's contiguous durably-applied seqno for that stream. uint64 applied_seqno = 3; // The reporter's current term (m11p4): the leader folds a report into its // quorum commit index ONLY when this matches the index's activation term — // a stale or cross-leadership report can never advance commitment. uint64 reporter_term = 4; // The reporter binary's capability bit-field (m11p5). See the BIT REGISTRY // below `WalShipping`. proto3 zero-default = a pre-m11p5 reporter. Reported // on the frontier push (decoupled from heartbeats) so the leader learns a // follower's capabilities even when it is the one being pulled from — the // join/conf-change gate (§3.1) reads the leader's last-seen-per-peer map. uint64 capabilities = 5; } // Applied-report acknowledgement. message AppliedReportAck { bool acknowledged = 1; } // A pre-vote or vote request (m11p4 leader election). message VoteRequest { // The term votes are requested for. For a pre-vote this is the PROPOSED // term (candidate's current + 1) — nothing has been bumped. uint64 term = 1; uint32 candidate_region = 2; // The candidate's log position for the up-to-date restriction, // compared lexicographically: (last_log_term, last_log_seq). uint64 last_log_term = 3; uint64 last_log_seq = 4; // Pre-vote probe: changes no voter state, never inflates terms. bool prevote = 5; // Leadership-transfer election (`TimeoutNow`): voters skip the // leader-freshness refusal — the current leader sanctioned this. bool transfer = 6; } // A vote (or pre-vote) reply. message VoteResponse { // The voter's current term (above the candidate's = step-down signal). uint64 term = 1; bool granted = 2; // The TYPED REMOVED SIGNAL (m11p5 §3.3): true iff the voter's APPLIED roster // lists the CANDIDATE (`candidate_region`) as a `Removed` member. proto3 // zero-default = false = backward-compatible. A candidate that sees `true` // was decommissioned: it flips readiness to 503 and suppresses campaigning, // exempt from the reseed marker. Vote refusals are a second delivery channel // (alongside heartbeats) for a removed node that missed the `Removed` record. bool removed = 3; } // The current leader tells `target` to start an immediate transfer election // (m11p4 fenced promote). message TimeoutNowRequest { // The sanctioning leader's current term. uint64 term = 1; uint32 leader_region = 2; } message TimeoutNowResponse { // Whether the target started an election. bool accepted = 1; } // ── Snapshot transfer (m11p5 §2): the reseed and joiner catch-up path ────── // // A joiner (or a node behind a compacted leader) cannot stream from the WAL // when the leader has compacted past its `from_seqno` (the structural hole // `StreamSegments` answers with FAILED_PRECONDITION + an `x-tidal-catchup` // trailer). `FetchSnapshot` ships a staged `create_backup` artifact as a // chunked server-stream, term-fenced exactly like `StreamSegments`, after // which the joiner resumes `StreamSegments` from the snapshot's seqno. // A puller's request for a snapshot to install before resuming the stream. message SnapshotRequest { uint32 shard_id = 1; // The puller's frontier+1: the first seqno it still needs. The source // answers `needed=false` (just stream) when the live WAL can still serve // this, else stages an artifact whose recovered tail is >= from_seqno. uint64 from_seqno = 2; // The puller's current term (m11p5 fencing): a deposed source must step // down rather than serve a snapshot of its possibly-divergent state; a // stale puller must rejoin the current term first. uint64 term = 3; } // One file entry in a snapshot manifest: a relative path under the artifact // root plus its size and content hash. The BLAKE3 is the END-TO-END integrity // contract — computed once when the artifact is staged (node-side), forwarded // verbatim here, and verified by the installing puller. The server NEVER // recomputes it per chunk (the manifest hash is authoritative). message SnapshotFileEntry { string path = 1; uint64 size = 2; bytes blake3 = 3; } // The leading chunk of a `FetchSnapshot` stream: either "no snapshot needed, // just stream" or the manifest of files that follow. message SnapshotHeader { // false = the live WAL still serves `from_seqno`; the puller resumes // `StreamSegments` directly and NO file chunks follow this header. true = // the file chunks below install before the stream resumes. bool needed = 1; // The snapshot's recovered WAL tail: the seqno the installed artifact is // valid at (the puller advances its frontier to this, then resumes from // `snapshot_seq + 1`). 0 when `needed=false`. uint64 snapshot_seq = 2; // The term and region of the staging leader (m11p5 fencing visibility). uint64 term = 3; uint32 leader_region = 4; // The complete manifest: every file the puller must receive and verify. // Empty when `needed=false`. repeated SnapshotFileEntry files = 5; } // One chunk of one file's bytes. Files stream in manifest order; a file is // complete when a chunk with `last=true` arrives (offset + data.len() then // equals the manifest size). message SnapshotFileChunk { string path = 1; uint64 offset = 2; bytes data = 3; bool last = 4; } // A message in the `FetchSnapshot` stream: the header (always first) then file // chunks. message SnapshotChunk { oneof chunk { SnapshotHeader header = 1; SnapshotFileChunk file = 2; } } // ── Membership conf-changes (m11p5 §3.3): the join verb ──────────────────── // // A joining node POSTs/dials any seed; a non-leader seed refuses with a leader // hint and the joiner re-targets. The leader assigns a PERMANENT id, appends a // Learner kind-4 record, waits for same-term quorum commit, and answers with // the full roster so the joiner can build its peer tables before catching up. // One member in a `JoinResponse` roster (the wire shape of a `MemberEntry`). message MemberInfo { uint32 id = 1; string name = 2; string grpc_addr = 3; string http_addr = 4; // 0 = Voter, 1 = Learner, 2 = Removed (matches MemberRole's on-disk byte). uint32 role = 5; } // A node's request to join the cluster (m11p5 §3.3). Idempotent by `name`: // a re-join from a known member returns its existing id/role and appends // nothing. message JoinRequest { string name = 1; // Advertised gRPC address (host:port; DNS name or literal IP). string grpc_addr = 2; // Advertised HTTP address (scheme://host:port). string http_addr = 3; // The joiner binary's capability bit-field (m11p5 §3.1). A joiner that is // not kind-4 capable can never participate in conf-changes; the leader's // gate also requires every current voter to be capable. uint64 capabilities = 4; } // The leader's answer to a join (m11p5 §3.3). message JoinResponse { // false = refused; `refusal_reason` says why and (for a non-leader seed) // `leader_*` names where to re-target. bool accepted = 1; string refusal_reason = 2; // The id the joiner was assigned (or its existing id on an idempotent // re-join). Meaningful only when `accepted`. uint32 assigned_id = 3; // The current leadership term + the leader's addresses, so the joiner can // persist the term and dial the leader directly. uint64 term = 4; string leader_region = 5; string leader_grpc_addr = 6; string leader_http_addr = 7; // The full roster after the join (the joiner's source of truth for its peer // tables), and the conf version it is at. repeated MemberInfo members = 8; uint64 membership_version = 9; } // WAL segment shipping service between tidalDB shards. // // ── CAPABILITY BIT REGISTRY (m11p5 §3.1) ─────────────────────────────────── // `capabilities` is a `uint64` bit-field on `HeartbeatResponse` and // `AppliedReport`. proto3's zero-default means a pre-m11p5 binary reports `0` // (no bits) — conservatively "incapable". Allocate bits append-only; NEVER // reuse or renumber a retired bit. // bit 0 (0x1): kind-4 membership record capable — this binary can fold a // `MembershipRecord` WAL blob without halting its receiver. The // leader refuses `JoinCluster`/conf-changes until every voter // reports this bit (a kind-4 record to a pre-p5 follower is an // unknown batch kind → permanent receiver halt). service WalShipping { // Ship a single WAL segment to a peer shard (unary). rpc ShipSegment(ShipSegmentRequest) returns (ShipSegmentResponse); // Stream WAL segments from a given sequence number (server-streaming). rpc StreamSegments(StreamRequest) returns (stream ShipSegmentRequest); // Stream a staged snapshot artifact to a joining/reseeding peer (m11p5). // The header chunk arrives first (needed / manifest), then file chunks. rpc FetchSnapshot(SnapshotRequest) returns (stream SnapshotChunk); // Periodic health check for the ControlPlane; with m11p4, the leader's // lease assertion and the failure detector's input. rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse); // Follower -> leader durable-frontier report (m11p3 quorum acks). rpc ReportApplied(AppliedReport) returns (AppliedReportAck); // Pre-vote / vote (m11p4 leader election). rpc RequestVote(VoteRequest) returns (VoteResponse); // Fenced leadership transfer: start an immediate election (m11p4). rpc TimeoutNow(TimeoutNowRequest) returns (TimeoutNowResponse); // Join the cluster (m11p5 §3.3). Dialled by a joiner against any seed; a // non-leader seed refuses with a leader hint. The leader assigns a permanent // id, appends a Learner kind-4 record, waits for same-term quorum commit, and // answers with the roster. Idempotent by name. rpc JoinCluster(JoinRequest) returns (JoinResponse); }