syntax = "proto3";

package makechain;

// ============================================================
// ENVELOPE
// ============================================================

message Message {
  MessageData data       = 1;
  bytes hash             = 2;  // BLAKE3(data), 32 bytes
  bytes signature        = 3;  // Ed25519 signature over hash, 64 bytes
  bytes signer           = 4;  // Ed25519 public key, 32 bytes
  optional bytes data_bytes = 5;  // Cached canonical encoding of MessageData (used by verify_message)
}

message MessageData {
  MessageType type    = 1;
  uint64 timestamp    = 2;  // Makechain epoch seconds
  Network network     = 3;
  bytes owner_address = 4;  // 20-byte owner address

  oneof body {
    ProjectCreateBody      project_create      = 5;
    ProjectMetadataBody    project_metadata    = 6;
    ProjectArchiveBody     project_archive     = 7;
    ForkBody               fork                = 8;
    ProjectRemoveBody      project_remove      = 9;

    RefUpdateBody          ref_update          = 10;
    RefDeleteBody          ref_delete          = 11;

    CommitBundleBody       commit_bundle       = 12;

    CollaboratorAddBody    collaborator_add    = 13;
    CollaboratorRemoveBody collaborator_remove = 14;
    AccountDataBody        account_data        = 15;

    VerificationAddBody    verification_add    = 16;
    VerificationRemoveBody verification_remove = 17;
    StorageClaimBody       storage_claim       = 18;
    UsernameCreateBody     username_create     = 19;
    UsernameUpdateBody     username_update     = 20;

    LinkAddBody            link_add            = 21;
    LinkRemoveBody         link_remove         = 22;
    ReactionAddBody        reaction_add        = 23;
    ReactionRemoveBody     reaction_remove     = 24;

    SignerAddBody          signer_add          = 25;
    SignerRemoveBody       signer_remove       = 26;

    MergeRequestAddBody    merge_request_add    = 27;
    MergeRequestRemoveBody merge_request_remove = 28;

    KeychainAuthorizeBody  keychain_authorize  = 29;
    KeychainRevokeBody     keychain_revoke     = 30;
  }
}

enum MessageType {
  MESSAGE_TYPE_NONE                = 0;

  MESSAGE_TYPE_PROJECT_CREATE      = 1;
  MESSAGE_TYPE_PROJECT_METADATA    = 2;
  MESSAGE_TYPE_PROJECT_ARCHIVE     = 3;
  MESSAGE_TYPE_FORK                = 4;
  MESSAGE_TYPE_PROJECT_REMOVE      = 5;

  MESSAGE_TYPE_REF_UPDATE          = 6;
  MESSAGE_TYPE_REF_DELETE          = 7;
  MESSAGE_TYPE_SIGNER_ADD          = 8;
  MESSAGE_TYPE_SIGNER_REMOVE       = 9;

  MESSAGE_TYPE_COMMIT_BUNDLE       = 10;

  MESSAGE_TYPE_COLLABORATOR_ADD    = 11;
  MESSAGE_TYPE_COLLABORATOR_REMOVE = 12;
  MESSAGE_TYPE_ACCOUNT_DATA        = 13;

  MESSAGE_TYPE_VERIFICATION_ADD    = 14;
  MESSAGE_TYPE_VERIFICATION_REMOVE = 15;

  MESSAGE_TYPE_STORAGE_CLAIM       = 16;
  MESSAGE_TYPE_USERNAME_CREATE     = 17;
  MESSAGE_TYPE_USERNAME_UPDATE     = 18;

  MESSAGE_TYPE_LINK_ADD            = 19;
  MESSAGE_TYPE_LINK_REMOVE         = 20;
  MESSAGE_TYPE_REACTION_ADD        = 21;
  MESSAGE_TYPE_REACTION_REMOVE     = 22;
  MESSAGE_TYPE_MERGE_REQUEST_ADD   = 23;
  MESSAGE_TYPE_MERGE_REQUEST_REMOVE = 24;
  MESSAGE_TYPE_KEYCHAIN_AUTHORIZE  = 25;
  MESSAGE_TYPE_KEYCHAIN_REVOKE     = 26;
}

enum Network {
  NETWORK_NONE    = 0;
  NETWORK_MAINNET = 1;
  NETWORK_TESTNET = 2;
  NETWORK_DEVNET  = 3;
}

// ============================================================
// 2P: PROJECT SET — Create/Remove
//
// project_id = BLAKE3 hash of the PROJECT_CREATE message (32 bytes).
// Content-addressed: two projects with the same name get different IDs
// because the hash includes owner_address, timestamp, etc.
// ============================================================

message ProjectCreateBody {
  string name          = 1;  // Alphanumeric + hyphens, 1-100 chars
  Visibility visibility = 2;
  string description   = 3;
  string license       = 4;  // SPDX identifier
  // project_id is not a field — it IS the Message.hash of this message
}

message ProjectRemoveBody {
  bytes project_id = 1;  // BLAKE3 hash of the PROJECT_CREATE message, 32 bytes
}

enum Visibility {
  VISIBILITY_PUBLIC  = 0;
  VISIBILITY_PRIVATE = 1;
}

// ============================================================
// 1P: SINGLETON
// ============================================================

message ForkBody {
  bytes source_project_id  = 1;  // 32 bytes
  string name              = 2;
  bytes source_commit_hash = 3;  // Target hash: the commit being forked from, 32 bytes
  Visibility visibility    = 4;  // Visibility of forked project (default: PUBLIC)
  // forked project_id = Message.hash of this FORK message
}

// ============================================================
// 1P: LWW REGISTER — Metadata
// ============================================================

message ProjectMetadataBody {
  bytes project_id               = 1;  // 32 bytes
  ProjectMetadataType field      = 2;
  string value                   = 3;
}

enum ProjectMetadataType {
  PROJECT_METADATA_NONE        = 0;
  PROJECT_METADATA_NAME        = 1;
  PROJECT_METADATA_DESCRIPTION = 2;
  PROJECT_METADATA_LICENSE     = 3;
  PROJECT_METADATA_VISIBILITY  = 4;
}

message AccountDataBody {
  AccountDataType field = 1;
  string value          = 2;
}

enum AccountDataType {
  ACCOUNT_DATA_NONE     = 0;
  ACCOUNT_DATA_DISPLAY_NAME = 1;
  ACCOUNT_DATA_AVATAR   = 2;
  ACCOUNT_DATA_BIO      = 3;
  ACCOUNT_DATA_WEBSITE  = 4;
}

// ============================================================
// 1P: APPEND-ONLY — Commits
// ============================================================

message CommitBundleBody {
  bytes project_id           = 1;  // 32 bytes
  repeated CommitMeta commits = 2;  // Ordered parent-first
  bytes content_digest       = 3;  // Optional: integrity proof (e.g. Zoda commitment), 32 bytes
  string url                 = 4;  // Optional: content locator URL
}

message CommitMeta {
  bytes hash             = 1;  // BLAKE3 of full commit object, 32 bytes
  repeated bytes parents = 2;  // Parent commit hashes
  bytes tree_root        = 3;  // BLAKE3 of tree object
  uint64 author_timestamp = 4;
  string title           = 5;  // First line of commit message, max 200 chars
  bytes message_hash     = 6;  // BLAKE3 of full commit message
  bytes author_address   = 7;  // 20-byte owner address when known
}

// ============================================================
// 1P: STATE TRANSITION
// ============================================================

message ProjectArchiveBody {
  bytes project_id = 1;  // 32 bytes
}

// ============================================================
// 2P: REF SET — CAS-ordered add/remove
// ============================================================

message RefUpdateBody {
  bytes project_id = 1;   // 32 bytes
  bytes ref_name   = 2;   // UTF-8, e.g. "refs/heads/main"
  RefType ref_type = 3;
  bytes old_hash   = 4;   // CAS: expected current value (empty = new ref)
  bytes new_hash   = 5;   // New commit hash, BLAKE3, 32 bytes
  bool force       = 6;   // Skip fast-forward check
  uint64 nonce     = 7;   // Replay protection per (owner_address, project, ref)
}

enum RefType {
  REF_TYPE_BRANCH = 0;
  REF_TYPE_TAG    = 1;
}

message RefDeleteBody {
  bytes project_id    = 1;  // 32 bytes
  bytes ref_name      = 2;
  bytes expected_hash = 3;  // CAS
  uint64 nonce        = 4;
}

// ============================================================
// 2P: COLLABORATOR SET — Add/Remove
// ============================================================

message CollaboratorAddBody {
  bytes project_id         = 1;  // 32 bytes
  Permission permission    = 2;
  bytes target_owner_address = 3; // 20-byte owner address
}

message CollaboratorRemoveBody {
  bytes project_id         = 1;  // 32 bytes
  bytes target_owner_address = 2; // 20-byte owner address
}

enum Permission {
  PERMISSION_READ  = 0;
  PERMISSION_WRITE = 1;
  PERMISSION_ADMIN = 2;
  PERMISSION_OWNER = 3;
}

// ============================================================
// KEY SCOPES
// ============================================================

enum KeyScope {
  KEY_SCOPE_OWNER   = 0;
  KEY_SCOPE_SIGNING = 1;
  KEY_SCOPE_AGENT   = 2;
}

// Self-contained Tempo settlement evidence: a BLS finalization certificate, the
// RLP block header it commits, and a receipt-trie inclusion proof. makechain
// verifies all of this with ZERO Tempo RPC; every node derives the economic
// values (actor/owner/units/timestamp) from the proven Rent log + header.
message StorageClaimBody {
  uint64 settlement_chain_id = 1;   // Host-chain EIP-155 chain id (namespacing + claim_id)
  uint32 tx_index = 2;              // Transaction index in the Tempo block (receipts-trie key)
  uint32 settlement_log_index = 3;  // Receipt-local log index of the Rent event
  bytes finalization_cert = 4;      // Tempo BLS finalization certificate (commonware Finalization)
  bytes header_rlp = 5;             // RLP-encoded Tempo block header (tempo_primitives::TempoHeader)
  bytes receipt_rlp = 6;            // EIP-2718 receipt trie value at tx_index
  repeated bytes proof_nodes = 7;   // Ordered receipts-trie Merkle-Patricia inclusion proof
  bytes settlement_tx_hash = 8;     // 32-byte settlement tx hash; DISPLAY-ONLY, not part of claim_id
}

message UsernameCreateBody {
  string username = 1;              // Canonical lowercase username for first registration
}

message UsernameUpdateBody {
  string username = 1;              // Canonical lowercase replacement username
}

// ============================================================
// 2P: VERIFICATION SET — Add/Remove (external identity proofs)
// ============================================================

message VerificationAddBody {
  VerificationType verification_type = 1;
  bytes address                      = 2;  // External address/identifier (e.g. 20-byte Ethereum address)
  bytes claim_signature              = 3;  // Signature from the external key proving ownership
  bytes chain_id                     = 4;  // Chain identifier (e.g. 1 = Ethereum mainnet, 10 = Optimism)
}

message VerificationRemoveBody {
  bytes address = 1;  // Same address as the VerificationAdd being removed
}

enum VerificationType {
  VERIFICATION_TYPE_NONE          = 0;
  VERIFICATION_TYPE_ETH_ADDRESS   = 1;  // Ethereum EOA
  VERIFICATION_TYPE_SOL_ADDRESS   = 2;  // Solana address
}

// ============================================================
// 2P: LINK SET — Social graph relationships (follow, star)
// ============================================================

message LinkAddBody {
  LinkType link_type          = 1;
  oneof target {
    bytes target_project_id   = 2;  // For STAR (account → project), 32 bytes
    bytes target_owner_address = 3; // For FOLLOW (account → account), 20-byte owner address
  }
}

message LinkRemoveBody {
  LinkType link_type          = 1;
  oneof target {
    bytes target_project_id   = 2;
    bytes target_owner_address = 3;
  }
}

enum LinkType {
  LINK_TYPE_NONE   = 0;
  LINK_TYPE_FOLLOW = 1;  // Account follows another account
  LINK_TYPE_STAR   = 2;  // Account stars a project
}

// ============================================================
// 2P: REACTION SET — Commit reactions (likes)
// ============================================================

message ReactionAddBody {
  ReactionType reaction_type     = 1;
  bytes target_project_id        = 2;  // 32 bytes
  bytes target_commit_hash       = 3;  // 32 bytes
}

message ReactionRemoveBody {
  ReactionType reaction_type     = 1;
  bytes target_project_id        = 2;  // 32 bytes
  bytes target_commit_hash       = 3;  // 32 bytes
}

enum ReactionType {
  REACTION_TYPE_NONE = 0;
  REACTION_TYPE_LIKE = 1;
}

// ============================================================
// 2P: MERGE REQUEST SET — Cross-project contribution proposals
// ============================================================

message MergeRequestAddBody {
  bytes project_id         = 1;  // target project_id, 32 bytes
  bytes source_project_id  = 2;  // source fork-descendant project_id, 32 bytes
  bytes source_ref         = 3;  // 1-254 bytes, no 0x00
  bytes source_commit_hash = 4;  // 32 bytes
  bytes target_ref         = 5;  // 1-254 bytes, no 0x00
  string title             = 6;  // 1-200 UTF-8 bytes
}

message MergeRequestRemoveBody {
  bytes project_id = 1;  // target project_id, 32 bytes
  bytes request_id = 2;  // original MERGE_REQUEST_ADD Message.hash, 32 bytes
}

// ============================================================
// 2P: SIGNER SET — AccountKeychain-authorized key operations
//
// These are submitted directly by clients.
// Authorization comes from AccountKeychain admin signatures, not from the
// message envelope signer.
// ============================================================

message SignerAddBody {
  bytes key = 1;                        // Ed25519 public key, 32 bytes
  uint32 scope = 2;                     // KeyScope: 0 = Owner, 1 = Signing, 2 = Agent
  bytes custody_signature = 3;          // AccountKeychain admin signature over this signer-add operation
  repeated bytes allowed_projects = 4;  // Agent scope: project IDs (32 bytes each)
  uint64 nonce = 5;                     // Must match account custody_nonce (replay protection)
  bytes request_signature = 6;          // AccountKeychain signature from request_owner_address for app attribution
  uint64 valid_after = 7;               // Earliest unix timestamp accepted for this authorization
  uint64 valid_before = 8;              // Latest unix timestamp accepted for this authorization
  bytes request_owner_address = 9;      // 20-byte owner address of requesting app when known
}

message SignerRemoveBody {
  bytes key = 1;                        // Ed25519 public key to remove, 32 bytes
  bytes custody_signature = 2;          // AccountKeychain admin signature over this signer-remove operation
  uint64 nonce = 3;                     // Must match account custody_nonce (replay protection)
  uint64 valid_after = 4;               // Earliest unix timestamp accepted for this authorization
  uint64 valid_before = 5;              // Latest unix timestamp accepted for this authorization
}

enum CustodySignatureType {
  CUSTODY_SIGNATURE_TYPE_NONE = 0;
  CUSTODY_SIGNATURE_TYPE_SECP256K1 = 1;
  CUSTODY_SIGNATURE_TYPE_P256 = 2;
  CUSTODY_SIGNATURE_TYPE_WEBAUTHN_P256 = 3;
}

// Lifecycle status of an AccountKeychain custody key in query responses.
// NOTE: this wire enum (UNSPECIFIED=0/ACTIVE=1/REVOKED=2) does NOT share
// discriminants with the state `CustodyKeyState::status` (Active=0/Revoked=1);
// the API layer converts explicitly. ACTIVE means "not revoked"; expiry is a
// separate concern carried by `expires_at`, never folded into REVOKED.
enum CustodyKeyStatus {
  CUSTODY_KEY_STATUS_UNSPECIFIED = 0;
  CUSTODY_KEY_STATUS_ACTIVE = 1;
  CUSTODY_KEY_STATUS_REVOKED = 2;
}

// ============================================================
// 2P: ACCOUNT KEYCHAIN — Custody key authorization/revocation
// ============================================================

message KeychainAuthorizeBody {
  bytes key_id = 1;                                // 20-byte custody key id derived from public_key
  CustodySignatureType signature_type = 2;
  bytes public_key = 3;                            // secp256k1 SEC1 uncompressed 65 bytes, or P256 x||y 64 bytes
  bool admin = 4;                                  // Admin keys can authorize account-level mutations
  uint64 expires_at = 5;                           // 0 means never expires; admin keys must never expire
  uint64 valid_after = 6;                          // Earliest unix timestamp accepted for this authorization
  uint64 valid_before = 7;                         // Latest unix timestamp accepted for this authorization
  uint64 nonce = 8;                                // Must match account custody_nonce (replay protection)
  bytes authorization_signature = 9;               // AccountKeychain admin signature over this authorize operation
  bytes witness = 10;                              // Optional app challenge binding; not burned on success
}

message KeychainRevokeBody {
  bytes key_id = 1;                                // 20-byte custody key id to revoke
  uint64 valid_after = 2;                          // Earliest unix timestamp accepted for this revocation
  uint64 valid_before = 3;                         // Latest unix timestamp accepted for this revocation
  uint64 nonce = 4;                                // Must match account custody_nonce (replay protection)
  bytes revocation_signature = 5;                  // AccountKeychain admin signature over this revoke operation
  bytes witness = 6;                               // Optional app challenge binding; not burned on success
}

// ============================================================
// PROJECT MESSAGE GROUPING
// ============================================================

message ProjectMessages {
  bytes project_id = 1;           // 32 bytes
  repeated Message messages = 2;
}

// The finalized `Block` body is the single canonical carrier of a block's
// messages (`account_messages` + flat `user_messages`, authenticated by
// `BlockHeader.transactions_root`); verifiers execute the body directly.

// DEPRECATED — to be removed in Phase F (Path B port).
message SubblockPayload {
  bytes validator_key = 1;              // 32 bytes Ed25519 public key
  bytes parent_hash = 2;               // 32 bytes chain tip (staleness check)
  bytes signature = 3;                  // 64 bytes Ed25519 over BLAKE3(content)
  repeated Message account_messages = 4;
  repeated ProjectMessages project_messages = 5;
  uint64 timestamp = 6;
}

// ============================================================
// BLOCKS AND SHARDS
// ============================================================

message Height {
  // Single-chain height. Tempo uses a raw u64 BlockNumber; Commonware's
  // Heightable trait carries a single u64 — there is no shard dimension.
  uint64 block_number = 1;
}

message ProjectTransaction {
  bytes project_id                 = 1;  // 32 bytes
  repeated Message user_messages   = 2;
  bytes project_root               = 3;  // Merkle root after applying
}

message Block {
  BlockHeader header = 1;
  // The block envelope is split into a `BlockHeader` + `BlockBody` per
  // Tempo / Commonware idiom. Header verification (including
  // `compute_block_hash`) does not decode the body; light clients can
  // fetch headers without bodies.
  BlockBody   body   = 2;
}

// Application-defined body for a `Block`. Carries the user/account
// message stream, the optional finalization certificate, the optional
// dealer-log evidence, and the optional DKG outcome bytes. Every
// hash-bound piece of body data is already pinned through the header
// (`state_root`, `dealer_log_hash`, `dkg_outcome_hash`), so the body is
// content-addressed by the header and the header can be verified in
// isolation.
message BlockBody {
  // Flat user-message stream. Per-project grouping is recovered on
  // demand from `extract_project_id` (see
  // `execution_grouping::project_txns_from_block`).
  repeated Message user_messages    = 1;
  // Account-level messages (executed serially during account pre-pass).
  // Account messages live directly on the body alongside project
  // messages; the body is the sole canonical message carrier.
  repeated Message account_messages = 2;
  // #651: the consensus finalization certificate does not ride the
  // hash-committed body. It travels as a separate wire item on the
  // `GetBlockResponse` / `GetSyncTargetResponse` envelopes (and is
  // verified before the block is trusted), mirroring commonware's
  // `(finalization, block)` marshal delivery.
  //
  // Raw `commonware-codec`-encoded `SignedDealerLog<MinSig, ed25519>` —
  // the proposer's onchain dealer-log evidence. Present only on
  // late-phase non-boundary blocks; empty otherwise. Header-bound by
  // `BlockHeader.dealer_log_hash`. Dealer identity is authenticated by
  // the signature inside the log; epoch is derivable from block height
  // via `Epocher::containing(height)`. Verifier rejects:
  //   - non-empty bytes outside late-phase blocks
  //   - bytes that do not decode as exactly one `SignedDealerLog`
  //   - any entry whose dealer != block proposer
  bytes dealer_log                  = 3;
  // Canonical OnchainDkgOutcome bytes — present on the boundary block
  // of each epoch; absent otherwise. Header-bound by
  // `BlockHeader.dkg_outcome_hash`. Decoded via
  // `makechain_consensus::dkg::read_dkg_outcome`. Carries the group
  // public key + verifier set + next-ceremony scheduling for
  // `epoch + 1`.
  bytes dkg_outcome                 = 4;
}

// Consensus context committed to in the block header. Mirrors upstream
// `commonware_consensus::simplex::types::Context<D, P>` (commonware-consensus
// v2026.4 src/simplex/types.rs:21-33), which carries the round, leader, and
// parent (view, digest) used by the certifiable-block deferred-verification
// path.
//
// Fields:
//   - `round_epoch` / `round_view`  -> `Round { epoch, view }`
//   - `leader_public_key`           -> `P` (32-byte Ed25519 PublicKey)
//   - `parent_view`                 -> `parent: (View, D)` (D = BlockDigest)
//
// The simplex `parent.1` digest is NOT serialised here — it is recovered
// from `BlockHeader.parent_hash` (the canonical chain link), avoiding the
// redundant copy that produced extra surface area to keep in sync.
//
// All numeric fields are wire-encoded as proto3 varints; the leader public
// key is verified by consensus on decode (32 bytes).
message ConsensusContext {
  uint64 round_epoch        = 1;  // Round.epoch (commonware Epoch, u64)
  uint64 round_view         = 2;  // Round.view  (commonware View,  u64)
  bytes leader_public_key   = 3;  // 32 bytes Ed25519 public key of the round leader
  uint64 parent_view        = 4;  // simplex View of the parent proposal
}

// No per-block protocol `version` or `chain_id`: both added zero security
// over height-based hardfork dispatch and network-bound signature
// namespaces, and a proposer-supplied version invited fork-ambiguity at
// activation heights. Version dispatch reads `chainspec.hardfork_at(height)`
// (see `makechain_consensus::config::ConsensusConfig::hardfork_at`); chain
// identity lives in the consensus signing namespace
// (`crate::engine::consensus_namespace`). Don't re-add these fields.
message BlockHeader {
  Height height            = 1;
  uint64 timestamp         = 2;
  bytes parent_hash        = 3;
  bytes state_root         = 4;
  // Path B foundation (Phase 1): consensus context the proposer used when
  // building this block. Mirrors `simplex::types::Context<D, P>` so that
  // CertifiableBlock::context() can reconstruct it for deferred verification.
  // Optional in Phase 1 (zero-value default ⇒ Round{0,0}, empty leader and
  // parent_digest); Phase 3 will populate it on every propose path.
  ConsensusContext context = 5;
  // BLAKE3 over `Block.dkg_outcome` bytes when present; zero-length when
  // the block carries no outcome. Verifier checks
  // `BLAKE3(block.dkg_outcome) == header.dkg_outcome_hash` whenever either
  // side is non-empty.
  bytes dkg_outcome_hash   = 6;
  // BLAKE3 over `Block.dealer_log` bytes when present; zero-length when the
  // block carries no dealer log. Verifier checks
  // `BLAKE3(block.dealer_log) == header.dealer_log_hash` whenever either side
  // is non-empty. Header-bound for finality anchoring, paralleling
  // `dkg_outcome_hash`.
  bytes dealer_log_hash    = 7;
  // Cleanup #18 (#636 / #648): commonware-storage BMT root over the block's
  // message hashes — `account_messages` first, then `user_messages`, each
  // leaf being the 32-byte `Message.hash`. Hashed with BLAKE3 via
  // `commonware_cryptography::blake3::Blake3`. Zero-length when the block
  // carries no messages; 32 bytes when at least one is present. Verifier
  // re-derives the root from the body and compares; mismatch is a fork-
  // evidence error. Enables light-client message-inclusion proofs without
  // shipping block bodies — see `GetMessageInclusionProof` RPC. Pairs with
  // the Tempo `transactions_root` field on `alloy_consensus::Header`.
  bytes transactions_root  = 8;
  // #698 (glue adoption, step 3): the QMDB ops-only MMR root and the op-count
  // range `[sync_boundary, total_size)` the glue `Stateful` wrapper's sync
  // target is built from. `state_root` (field 4) stays the CANONICAL QMDB root;
  // these are ADDITIONAL fields the witness-free `qmdb::sync::Target { root,
  // range }` (commonware PR #3887) needs to reconstruct a sync target from a
  // block header alone — values produced by the merkleized batch
  // (`ops_root()`, `sync_boundary()`, `bounds().total_size`). Zero on blocks
  // built without a merkleized batch (genesis, tests, sync-replayed bodies).
  bytes  ops_root         = 9;
  uint64 ops_range_start  = 10;
  uint64 ops_range_end    = 11;
}

// ============================================================
// API SERVICE
// ============================================================

service MakechainService {
  // Write path
  rpc SubmitMessage(SubmitMessageRequest) returns (SubmitMessageResponse);
  rpc BatchSubmitMessages(BatchSubmitRequest) returns (BatchSubmitResponse);

  // Read path — projects
  // GetProject is polymorphic: caller selects one of `project_id`
  // (32-byte hash lookup) or `name` (requires `owner_address`).
  rpc GetProject(GetProjectRequest) returns (GetProjectResponse);
  // ListProjects is the unified list+search endpoint. Optional
  // `owner_address` filters by owner; optional `name_prefix` performs
  // a case-insensitive prefix match (at least 2 chars).
  rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse);

  // Read path — refs
  rpc GetRef(GetRefRequest) returns (GetRefResponse);
  rpc ListRefs(ListRefsRequest) returns (ListRefsResponse);

  // Read path — commits
  rpc GetCommit(GetCommitRequest) returns (GetCommitResponse);
  rpc ListCommits(ListCommitsRequest) returns (ListCommitsResponse);

  // Read path — collaborators
  rpc ListCollaborators(ListCollaboratorsRequest) returns (ListCollaboratorsResponse);

  // Read path — account
  // GetAccount is polymorphic: caller selects one of `owner_address`
  // (20-byte EVM address) or `key` (32-byte Ed25519 public key).
  rpc GetAccount(GetAccountRequest) returns (GetAccountResponse);
  // ListAccounts is the unified list+search endpoint. Optional
  // `username_prefix` performs a case-insensitive prefix match.
  rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse);

  // Unified activity feed (replaces GetProjectActivity + GetAccountActivity).
  // Scope selects which entity the feed is for; `scope_id` is the project
  // id (32 bytes) or owner_address (20 bytes) depending on scope.
  rpc GetActivity(GetActivityRequest) returns (GetActivityResponse);

  // Read path — keys
  rpc GetKey(GetKeyRequest) returns (GetKeyResponse);
  rpc ListKeys(ListKeysRequest) returns (ListKeysResponse);
  rpc GetCustodyKey(GetCustodyKeyRequest) returns (GetCustodyKeyResponse);
  rpc ListCustodyKeys(ListCustodyKeysRequest) returns (ListCustodyKeysResponse);

  // Read path — verifications
  rpc ListVerifications(ListVerificationsRequest) returns (ListVerificationsResponse);

  // Read path — links
  rpc ListLinks(ListLinksRequest) returns (ListLinksResponse);
  rpc ListLinksByTarget(ListLinksByTargetRequest) returns (ListLinksByTargetResponse);

  // Read path — reactions
  rpc ListReactions(ListReactionsRequest) returns (ListReactionsResponse);
  rpc ListReactionsByTarget(ListReactionsByTargetRequest) returns (ListReactionsByTargetResponse);

  // Read path — merge requests
  rpc GetMergeRequest(GetMergeRequestRequest) returns (GetMergeRequestResponse);
  rpc ListMergeRequests(ListMergeRequestsRequest) returns (ListMergeRequestsResponse);
  rpc ListMergeRequestsByRequester(ListMergeRequestsByRequesterRequest) returns (ListMergeRequestsByRequesterResponse);

  // Read path — blocks
  rpc GetBlock(GetBlockRequest) returns (GetBlockResponse);
  rpc ListBlocks(ListBlocksRequest) returns (ListBlocksResponse);

  // Read path — messages
  rpc GetMessage(GetMessageRequest) returns (GetMessageResponse);
  rpc ListMessages(ListMessagesRequest) returns (ListMessagesResponse);
  // Light-client message inclusion proof (Cleanup #18, #636 / #648).
  // Returns a commonware-storage BMT proof that proves the supplied
  // `message_hash` is one of the messages committed to in the block
  // identified by `block_hash`'s header `transactions_root` field.
  // The verifier reconstructs the proof and calls
  // `verify_element_inclusion(message_hash, position, transactions_root)`
  // — no need to fetch the block body.
  rpc GetMessageInclusionProof(GetMessageInclusionProofRequest)
      returns (GetMessageInclusionProofResponse);

  // Node status
  rpc GetNodeStatus(GetNodeStatusRequest) returns (GetNodeStatusResponse);
  rpc GetChainStats(GetChainStatsRequest) returns (GetChainStatsResponse);

  // State attestation (#698): the threshold-BLS-certified post-execution QMDB
  // root at a height, produced by the consensus::aggregation track.
  rpc GetStateAttestation(GetStateAttestationRequest) returns (GetStateAttestationResponse);

  // Snapshots
  rpc GetSnapshotInfo(GetSnapshotInfoRequest) returns (GetSnapshotInfoResponse);

  // Validation
  rpc DryRunMessage(DryRunMessageRequest) returns (DryRunMessageResponse);

  // Commit graph
  rpc GetCommitAncestors(GetCommitAncestorsRequest) returns (GetCommitAncestorsResponse);

  // Ref history
  rpc GetRefLog(GetRefLogRequest) returns (GetRefLogResponse);

  // Admin
  rpc GetMempoolInfo(GetMempoolInfoRequest) returns (GetMempoolInfoResponse);

  // Health
  rpc GetHealth(GetHealthRequest) returns (GetHealthResponse);

  // Streaming
  rpc SubscribeMessages(SubscribeRequest) returns (stream Message);
  rpc SubscribeBlocks(SubscribeBlocksRequest) returns (stream BlockEvent);

  // Sync — finalized tip discovery for cold-start QMDB sync.
  // Body sources height + digest + finalization cert via the canonical
  // commonware-marshal mailbox (MarshalCoreMailbox::get_info +
  // get_finalization). Block bytes are fetched separately via GetBlock.
  rpc GetSyncTarget(GetSyncTargetRequest) returns (GetSyncTargetResponse);

  // Late-rejoin — fetch a past epoch's public DKG output
  // (dealers + threshold polynomial) so a node that wasn't in the DKG
  // round can boot in observer mode after authenticated state sync.
  // The threshold group identity is constant across Commonware reshares,
  // so the caller authenticates the response by checking
  // Output.public() == its locally trusted identity. The server NEVER
  // returns the local share — public material only.
  rpc GetEpochState(GetEpochStateRequest) returns (GetEpochStateResponse);

  // Proofs — QMDB current-state proofs (Cleanup #23: single polymorphic GetStateProof
  // subsumes the former GetOperationProof / GetExclusionProof / GetStorageQuotaProof /
  // GetCompoundProof RPCs; per-key existence is determined server-side)
  rpc GetStateProof(GetStateProofRequest) returns (GetStateProofResponse);
  rpc VerifyOperationProof(VerifyOperationProofRequest) returns (VerifyOperationProofResponse);
  rpc VerifyExclusionProof(VerifyExclusionProofRequest) returns (VerifyExclusionProofResponse);
  rpc VerifyOperationProofAtBlock(VerifyOperationProofAtBlockRequest) returns (VerifyOperationProofAtBlockResponse);
  rpc VerifyExclusionProofAtBlock(VerifyExclusionProofAtBlockRequest) returns (VerifyExclusionProofAtBlockResponse);

  // PR-a of tracker #651 — fetch a stored direct finalization certificate by
  // block digest. Reads from the never-pruned immutable certificates
  // archive opened at validator/exporter init. Returns the
  // commonware-codec-encoded `Finalization<MakechainScheme, BlockDigest>`
  // bytes when the requested block has a direct certificate. Indirectly
  // finalized ancestors may not have one.
  rpc GetFinalizationCertificate(GetFinalizationCertificateRequest)
      returns (GetFinalizationCertificateResponse);
}

// --- Write ---

message SubmitMessageRequest {
  Message message = 1;
}

message SubmitMessageResponse {
  bytes hash = 1;       // BLAKE3 hash of accepted message
  bool accepted = 2;
  string error = 3;     // Non-empty if !accepted
  SubmitMessageErrorCode error_code = 4; // Stable machine-readable rejection code
}

enum SubmitMessageErrorCode {
  SUBMIT_MESSAGE_ERROR_CODE_UNSPECIFIED = 0;
  SUBMIT_MESSAGE_ERROR_CODE_MESSAGE_ALREADY_COMMITTED = 1;
  SUBMIT_MESSAGE_ERROR_CODE_STORAGE_CLAIM_ALREADY_APPLIED = 2;
  SUBMIT_MESSAGE_ERROR_CODE_STORAGE_CLAIM_ALREADY_PENDING = 3;
}

message BatchSubmitRequest {
  repeated Message messages = 1;  // Up to 100 messages
}

message BatchSubmitResult {
  bytes hash = 1;
  bool accepted = 2;
  string error = 3;
  SubmitMessageErrorCode error_code = 4;
}

message BatchSubmitResponse {
  repeated BatchSubmitResult results = 1;
  uint32 accepted_count = 2;
  uint32 rejected_count = 3;
}

// --- Projects ---

// Polymorphic project lookup. Exactly one branch of the `identifier`
// oneof must be set: `project_id` for a 32-byte hash lookup, or
// `name` (paired with the owner_address scope) for a name lookup.
message GetProjectRequest {
  oneof identifier {
    bytes project_id = 1;   // 32-byte hash
    ProjectNameLookup name = 2;
  }
}

// Scope for a name-based project lookup. Project names are unique
// per owner_address, not globally, so both fields are required.
message ProjectNameLookup {
  bytes owner_address = 1;  // 20-byte owner address
  string name = 2;          // Project name
}

message GetProjectResponse {
  bytes project_id = 1;
  string name = 2;
  string description = 3;
  string license = 4;
  Visibility visibility = 5;
  string status = 6;     // "active", "archived", "removed"
  bytes fork_source = 7; // Source project_id if forked, empty otherwise
  uint32 ref_count = 8;
  uint32 collaborator_count = 9;
  uint32 commit_count = 10;
  uint32 max_refs = 11;           // Per-project ref limit (spec §10)
  uint32 max_collaborators = 12;  // Per-project collaborator limit
  uint32 max_commits = 13;        // Per-project commit limit (before pruning)
  bytes owner_address = 14;       // 20-byte owner address when known
  uint32 merge_request_count = 15;
}

// Unified list + search request for projects. Filters compose with
// AND semantics. `name_prefix` performs a case-insensitive prefix
// match (minimum 2 characters when set).
message ListProjectsRequest {
  uint32 limit = 1;
  bytes cursor = 2;         // Opaque pagination cursor
  bytes owner_address = 3;  // optional 20-byte owner_address filter
  string name_prefix = 4;   // optional case-insensitive name prefix
}

message ListProjectsResponse {
  repeated GetProjectResponse projects = 1;
  bytes next_cursor = 2;
}

// --- Refs ---

message GetRefRequest {
  bytes project_id = 1;
  bytes ref_name = 2;
}

message GetRefResponse {
  bytes project_id = 1;
  bytes ref_name = 2;
  RefType ref_type = 3;
  bytes hash = 4;           // Current commit hash
  uint64 nonce = 5;
}

message ListRefsRequest {
  bytes project_id = 1;
  uint32 limit = 2;
  bytes cursor = 3;
}

message ListRefsResponse {
  repeated GetRefResponse refs = 1;
  bytes next_cursor = 2;
}

// --- Commits ---

message GetCommitRequest {
  bytes project_id = 1;
  bytes commit_hash = 2;    // 32 bytes
}

message GetCommitResponse {
  bytes project_id = 1;
  CommitMeta commit = 2;
  bytes content_digest = 3;  // Optional: integrity proof from the commit bundle
  string url = 4;            // Optional: content locator URL from the commit bundle
}

message ListCommitsRequest {
  bytes project_id = 1;
  uint32 limit = 2;
  bytes cursor = 3;
}

message ListCommitsResponse {
  repeated GetCommitResponse commits = 1;
  bytes next_cursor = 2;
}

// --- Collaborators ---

message ListCollaboratorsRequest {
  bytes project_id = 1;
  uint32 limit = 2;
  bytes cursor = 3;
}

message CollaboratorEntry {
  Permission permission = 1;
  bytes owner_address = 2;  // 20-byte owner address
}

message ListCollaboratorsResponse {
  repeated CollaboratorEntry collaborators = 1;
  bytes next_cursor = 2;
}

// --- Account ---

// Unified list + search request for accounts. `username_prefix`
// performs a case-insensitive prefix match (minimum 2 characters
// when set).
message ListAccountsRequest {
  uint32 limit = 1;
  bytes cursor = 2;          // Opaque pagination cursor
  string username_prefix = 3; // optional case-insensitive username prefix
}

// Polymorphic account lookup. Exactly one branch of the `identifier`
// oneof must be set: `owner_address` for a 20-byte EVM address lookup,
// or `key` for a 32-byte Ed25519 public-key reverse lookup.
message GetAccountRequest {
  oneof identifier {
    bytes owner_address = 1;  // 20-byte EVM owner address
    bytes key = 2;            // 32-byte Ed25519 public key
  }
}

message GetAccountResponse {
  string display_name = 1;
  string avatar = 2;
  string bio = 3;
  string website = 4;
  repeated KeyEntry keys = 5;
  uint32 storage_units = 6;  // Raw active storage grants; max_* fields reflect username-gated usable quota
  uint32 project_count = 7;
  repeated VerificationEntry verifications = 8;
  bytes owner_address = 9;  // 20-byte EVM wallet address (empty if not yet set)
  uint32 link_count = 10;    // Number of links (follows + stars) from this account
  uint32 reaction_count = 11; // Number of reactions (likes) from this account
  uint64 custody_nonce = 12; // Next expected signer nonce for custody-authorized signer changes
  uint32 max_projects = 13;
  uint32 max_links = 14;
  uint32 max_verifications = 15;
  uint32 max_reactions = 16;
  uint32 max_collaborators_per_project = 17;
  uint32 max_merge_requests_per_requester = 18;
  uint32 max_merge_requests_per_project = 19;
  string username = 20;       // Canonical active username (empty when no active username)
  uint32 key_count = 21;
  uint32 verification_count = 22;
  uint64 username_last_set_at = 23;          // Authoritative consensus timestamp of last successful USERNAME_CREATE/USERNAME_UPDATE (0 when no active username)
  uint64 username_update_available_at = 24;  // Server-computed convenience timestamp when USERNAME_UPDATE may succeed (0 when no active username)
  repeated CustodyKeyEntry custody_keys = 25; // Convenience snapshot of AccountKeychain custody keys; ListCustodyKeys is the paginated source of truth
}

message AccountSummary {
  bytes owner_address = 1;  // 20-byte EVM wallet address
  string username = 2;      // Canonical active username (empty when no active username)
  string display_name = 3;
  string avatar = 4;
  uint32 storage_units = 5;
  uint32 project_count = 6;
  uint32 key_count = 7;
  uint32 verification_count = 8;
  uint32 link_count = 9;
  uint32 reaction_count = 10;
}

message ListAccountsResponse {
  repeated AccountSummary accounts = 1;
  bytes next_cursor = 2;
}

message KeyEntry {
  bytes key = 1;
  KeyScope scope = 2;
  repeated bytes allowed_projects = 3;
  bytes request_owner_address = 4;     // 20-byte owner address of requesting app when known
}

message GetKeyRequest {
  bytes key = 1;   // Ed25519 public key, 32 bytes
  bytes owner_address = 2;  // 20-byte owner address
}

message GetKeyResponse {
  bytes key = 1;
  KeyScope scope = 2;
  repeated bytes allowed_projects = 3;
  uint64 added_at = 4;
  bytes request_owner_address = 5;     // 20-byte owner address of requesting app when known
}

message ListKeysRequest {
  uint32 limit = 1;
  bytes cursor = 2;     // public key to start after (for pagination)
  bytes owner_address = 3;  // 20-byte owner address
}

message ListKeysResponse {
  repeated GetKeyResponse keys = 1;
  bytes next_cursor = 2;
}

// --- AccountKeychain custody keys (read surface) ---

// A custody-family key under an account keyring. Distinct from KeyEntry
// (envelope Ed25519 signers): key_id is a 20-byte EVM-derived address.
message CustodyKeyEntry {
  bytes key_id = 1;                       // 20-byte EVM-derived custody key id
  CustodySignatureType signature_type = 2;
  bytes public_key = 3;                   // explicit pubkey material (secp256k1 65B SEC1 / P256 64B x||y)
  bool admin = 4;
  uint64 expires_at = 5;                  // 0 = non-expiring; admin keys are always 0
  uint64 added_at = 6;
  uint64 revoked_at = 7;                  // 0 while active
  CustodyKeyStatus status = 8;
}

message GetCustodyKeyRequest {
  bytes owner_address = 1;  // 20-byte owner address
  bytes key_id = 2;         // 20-byte custody key id
}

message GetCustodyKeyResponse {
  CustodyKeyEntry custody_key = 1;
}

message ListCustodyKeysRequest {
  uint32 limit = 1;
  // Opaque pagination cursor: echo `next_cursor` from the previous page
  // verbatim. It is the full internal storage key (NOT a bare key_id) and
  // must be treated as opaque by clients.
  bytes cursor = 2;
  bytes owner_address = 3;  // 20-byte owner address
}

message ListCustodyKeysResponse {
  repeated CustodyKeyEntry custody_keys = 1;
  // Opaque cursor for the next page; empty when there are no more results.
  bytes next_cursor = 2;
}

// --- Activity (unified) ---

// Scope for the unified `GetActivity` feed. Selects which entity the
// caller wants activity for; the matching identifier travels in
// `scope_id` (project_id for PROJECT scope, owner_address for ACCOUNT
// scope; empty for GLOBAL).
enum ActivityScope {
  ACTIVITY_SCOPE_UNSPECIFIED = 0;
  ACTIVITY_SCOPE_PROJECT = 1;
  ACTIVITY_SCOPE_ACCOUNT = 2;
  ACTIVITY_SCOPE_GLOBAL = 3;
}

message GetActivityRequest {
  ActivityScope scope = 1;
  // For PROJECT: 32-byte project_id. For ACCOUNT: 20-byte
  // owner_address. For GLOBAL: empty.
  bytes scope_id = 2;
  uint32 limit = 3;                // Max messages (default 50, max 200)
  repeated MessageType types = 4;  // Filter by type (empty = all)
}

message GetActivityResponse {
  repeated MessageEntry messages = 1;
}

// --- Blocks ---

message GetBlockRequest {
  // Height-keyed lookup (legacy + sync-target path). When set, the server
  // returns block + finalization cert (if it has one). Used by validators
  // for QMDB sync target fetch where the requested height is the sync
  // boundary (always cert-bearing).
  uint64 block_number = 1;
  // Digest-keyed lookup (canonical Commonware backfill, per
  // `consensus/src/marshal/core/actor.rs:1641 try_repair_gaps` ancestor
  // walk). When set (non-empty 32-byte digest), the server looks up by
  // block digest and returns block ONLY — no finalization cert is
  // attached because intermediate blocks are finalized transitively via
  // descendant certs (`marshal/core/actor.rs:1466-1468`). Used by
  // follower marshal+resolver to walk parent digests backward from a
  // verified-cert tip.
  bytes block_digest = 2;
}

message GetBlockResponse {
  Block block = 1;
  // keccak256 over the encoded `BlockHeader` of `block`. Server-computed
  // on demand (no longer cached on the block envelope). Sibling field
  // here so REST/JSON consumers don't have to re-encode + hash to
  // identify the block. 32 bytes.
  bytes hash = 2;
  // commonware-codec `Finalization<scheme, BlockDigest>` for `block`, when
  // the serving node holds a direct cert (#651). The finalization cert is a
  // separate wire item — NOT a field on the hash-committed `BlockBody` —
  // mirroring commonware's `(finalization, block)` marshal delivery. The
  // fetching node verifies it against the epoch scheme before trusting the
  // block. Empty when the serving node has no direct cert for this height
  // (intermediate heights are finalized transitively via descendant certs).
  bytes consensus_finalization = 3;
}

message ListBlocksRequest {
  uint64 start = 1;          // Starting block number (0 = latest)
  uint32 limit = 2;          // Max blocks to return
}

message ListBlocksResponse {
  repeated GetBlockResponse blocks = 1;
  uint64 latest_block = 2;   // Current chain height
}

// --- Messages ---

message GetMessageRequest {
  bytes hash = 1;             // 32-byte BLAKE3 hash of the message
}

message GetMessageResponse {
  Message message = 1;        // The full message
  uint64 block_number = 2;    // Block that included this message
}

// --- Message Inclusion Proof (Cleanup #18, #636 / #648) ---

message GetMessageInclusionProofRequest {
  // 32-byte keccak256 hash of the block whose `transactions_root` we are
  // proving inclusion against. Required.
  bytes block_hash   = 1;
  // 32-byte BLAKE3 hash of the message whose inclusion we want to
  // prove. Must be a member of the block's
  // `(account_messages || user_messages)` stream. Required.
  bytes message_hash = 2;
}

message GetMessageInclusionProofResponse {
  // commonware-codec-encoded `commonware_storage::bmt::Proof<BlockDigest>`.
  // Decoded via `Proof::decode_cfg(proof, &1)`.
  bytes proof              = 1;
  // The canonical leaf position used during proof construction.
  // `account_messages` occupy `[0, account_len)`; `user_messages`
  // occupy `[account_len, account_len + user_len)`.
  uint32 position          = 2;
  // The 32-byte `transactions_root` from the block header, returned
  // for client convenience so a thin REST client can verify without
  // a second `GetBlock` round-trip. Server-trusted, not part of the
  // proof — verifiers that don't trust the server should fetch the
  // header separately.
  bytes transactions_root  = 3;
}

// --- List Messages ---

message ListMessagesRequest {
  uint64 start_block = 1;          // Starting block number (inclusive, 0 = latest)
  uint64 end_block = 2;            // Ending block number (inclusive, 0 = start_block)
  uint32 limit = 3;                // Max messages to return
  repeated MessageType types = 4;  // Filter by message type (empty = all)
}

message MessageEntry {
  Message message = 1;
  uint64 block_number = 2;
}

message ListMessagesResponse {
  repeated MessageEntry messages = 1;
  uint64 start_block = 2;         // Actual start block used
  uint64 end_block = 3;           // Actual end block used
}

// --- Snapshot Info ---

message GetSnapshotInfoRequest {}

message GetSnapshotInfoResponse {
  uint64 block_number = 1;          // Last verified durable snapshot fence height
  bytes state_root = 2;             // Live canonical qmdb::current root (may lead block_number); informational only — trust GetSyncTarget for sync verification
  uint32 entry_count = 3;           // Number of key-value entries
  uint64 estimated_size_bytes = 4;  // Estimated encoded snapshot size
}

// --- Node Status ---

message GetNodeStatusRequest {}

enum ReplayVerificationStatus {
  REPLAY_VERIFICATION_STATUS_UNSPECIFIED = 0;
  REPLAY_VERIFICATION_STATUS_VERIFIED = 1;
  REPLAY_VERIFICATION_STATUS_TRUSTED_SNAPSHOT = 2;
}

message ReplayVerificationInfo {
  ReplayVerificationStatus status = 1;
  string detail = 2;
  uint64 blocked_block_number = 3;
}

message GetNodeStatusResponse {
  uint64 current_block = 1;       // Latest committed block number
  uint32 mempool_size = 2;        // Number of messages in the mempool
  uint32 pending_blocks = 3;      // Pending blocks awaiting commit
  Network network = 4;            // Network this node operates on
  string version = 5;             // Node software version
  uint64 uptime_secs = 6;         // Seconds since node started
  uint64 total_messages = 7;      // Cumulative messages committed (survives GC)
  uint32 state_entries = 8;       // Number of entries in the state store
  ReplayVerificationInfo replay_verification = 9;
  // Local validator's encoded Ed25519 public key (32 bytes). Empty for
  // non-validator nodes (readers, exporters). Used by sync peers to map
  // a gRPC endpoint back to its validator identity for qmdb-sync target
  // selection. See Task #121.
  bytes validator_public_key = 10;
}

// --- Chain Stats ---

message GetChainStatsRequest {}

message GetChainStatsResponse {
  uint64 block_height = 1;
  uint64 total_messages = 2;
  uint32 total_projects = 3;
  uint32 total_accounts = 4;
  uint32 total_refs = 5;
  uint32 total_commits = 6;
  uint32 total_collaborators = 7;
  uint32 total_keys = 8;
  uint32 total_verifications = 9;
  uint32 state_entries = 10;
  uint32 total_links = 11;
  uint32 total_reactions = 12;
}

// --- State Attestation (#698) ---

message GetStateAttestationRequest {
  // Block height whose certified post-execution QMDB root is requested.
  uint64 height = 1;
}

message GetStateAttestationResponse {
  // Whether a quorum certificate exists for this height yet. Attestation lags
  // agreement, so a recently-finalized height may not be certified yet.
  bool present = 1;
  // Echoed request height.
  uint64 height = 2;
  // The certified post-execution QMDB root (32 bytes). Empty when !present.
  bytes root = 3;
  // commonware-codec-encoded threshold-BLS certificate over (height, root).
  // Verifiable offline against the epoch group public key. Empty when !present.
  bytes certificate = 4;
}

// --- Verifications ---

message ListVerificationsRequest {
  uint32 limit = 1;
  bytes cursor = 2;
  bytes owner_address = 3;   // 20-byte owner address
}

message VerificationEntry {
  VerificationType verification_type = 1;
  bytes address = 2;
  bytes chain_id = 3;
}

message ListVerificationsResponse {
  repeated VerificationEntry verifications = 1;
  bytes next_cursor = 2;
}

// --- Links ---

message ListLinksRequest {
  LinkType link_type = 1;   // Filter by type (NONE = all)
  uint32 limit = 2;
  bytes cursor = 3;
  bytes owner_address = 4;  // 20-byte owner address
}

message LinkEntry {
  LinkType link_type = 1;
  bytes target_project_id = 2;     // Set for STAR links, 32 bytes
  uint64 added_at = 3;             // Consensus timestamp when the link was created
  bytes target_owner_address = 4;  // Set for FOLLOW links when known
  bytes source_owner_address = 5;  // Set when known
}

message ListLinksResponse {
  repeated LinkEntry links = 1;
  bytes next_cursor = 2;
}

message ListLinksByTargetRequest {
  LinkType link_type = 1;          // Required: FOLLOW or STAR
  oneof target {
    bytes target_project_id = 2;   // For STAR: which project is starred
  }
  uint32 limit = 3;
  bytes cursor = 4;
  bytes target_owner_address = 5;  // For FOLLOW: 20-byte target owner address
}

message ListLinksByTargetResponse {
  repeated LinkEntry links = 1;
  bytes next_cursor = 2;
}

// --- Reactions ---

message ListReactionsRequest {
  ReactionType reaction_type = 1;    // Filter by type (NONE = all)
  bytes target_project_id = 2;       // Filter by project (empty = all)
  uint32 limit = 3;
  bytes cursor = 4;
  bytes owner_address = 5;           // 20-byte owner address
}

message ReactionEntry {
  ReactionType reaction_type = 1;
  bytes target_project_id = 2;       // 32 bytes
  bytes target_commit_hash = 3;      // 32 bytes
  uint64 added_at = 4;               // Consensus timestamp
  bytes source_owner_address = 5;    // 20-byte owner address when known
}

message ListReactionsResponse {
  repeated ReactionEntry reactions = 1;
  bytes next_cursor = 2;
}

message ListReactionsByTargetRequest {
  ReactionType reaction_type = 1;    // Required: LIKE
  bytes target_project_id = 2;       // 32 bytes
  bytes target_commit_hash = 3;      // 32 bytes
  uint32 limit = 4;
  bytes cursor = 5;
}

message ListReactionsByTargetResponse {
  repeated ReactionEntry reactions = 1;
  bytes next_cursor = 2;
}

// --- Merge Requests ---

message GetMergeRequestRequest {
  bytes project_id = 1;  // 32 bytes
  bytes request_id = 2;  // 32 bytes
}

message GetMergeRequestResponse {
  MergeRequestSummary merge_request = 1;
}

message ListMergeRequestsRequest {
  bytes project_id = 1;               // 32 bytes
  uint32 limit = 2;
  bytes cursor = 3;                   // opaque forward-key cursor
  bytes requester_owner_address = 4;  // optional 20-byte filter
}

message ListMergeRequestsResponse {
  repeated MergeRequestSummary merge_requests = 1;
  bytes next_cursor = 2;
}

message ListMergeRequestsByRequesterRequest {
  bytes owner_address = 1;  // 20 bytes
  uint32 limit = 2;
  bytes cursor = 3;         // opaque reverse-key cursor
}

message ListMergeRequestsByRequesterResponse {
  repeated MergeRequestSummary merge_requests = 1;
  bytes next_cursor = 2;
}

message MergeRequestSummary {
  bytes request_id = 1;
  bytes project_id = 2;
  bytes requester_owner_address = 3;
  bytes source_project_id = 4;
  bytes source_ref = 5;
  bytes source_commit_hash = 6;
  bytes target_ref = 7;
  string title = 8;
  uint64 added_at = 9;
}

// --- Streaming ---

message SubscribeRequest {
  bytes project_id = 1;     // Filter by project (empty = all)
  repeated MessageType types = 2;  // Filter by type (empty = all)
}

// Empty filter — all finalized blocks. Knobs (height range, etc.) can be
// added later without a wire break.
//
// Filter convention (applies to any field added here later):
//   - Fields are additive filters.
//   - Semantics are AND across fields: a block matches iff it satisfies
//     every set filter.
//   - Numeric ranges are half-open `[from, to)` (proto convention).
message SubscribeBlocksRequest {}

// Compact event the gateway used to synthesize as SSE
// (`/v1/subscribe/blocks`). Promoted to a first-class Connect-ES
// server-streaming RPC in #564 so browser clients consume the typed
// stream directly over HTTP/1.1 chunked or HTTP/2 streaming without an
// SSE translation layer.
message BlockEvent {
  uint64 block_number  = 1;
  bytes  hash          = 2;  // 32-byte block hash
  uint64 timestamp     = 3;  // unix seconds
  uint32 message_count = 4;  // total messages in the block (account + project)
}

// --- Admin ---

message GetMempoolInfoRequest {}

message MempoolTypeCount {
  MessageType message_type = 1;
  uint32 count = 2;
}

message GetMempoolInfoResponse {
  uint32 total = 1;
  uint32 capacity = 2;
  uint32 account_messages = 3;
  uint32 project_messages = 4;
  repeated MempoolTypeCount type_breakdown = 5;
  uint64 oldest_timestamp = 6;
  uint64 newest_timestamp = 7;
}

// --- Ref History ---

message GetRefLogRequest {
  bytes project_id = 1;
  bytes ref_name = 2;
  uint32 limit = 3;
}

message RefLogEntry {
  bytes old_hash = 1;
  bytes new_hash = 2;
  uint64 timestamp = 3;
  uint64 block_number = 4;
  bool force = 5;
  uint64 nonce = 6;
  bytes owner_address = 7;
}

message GetRefLogResponse {
  repeated RefLogEntry entries = 1;
}

// --- Health ---

message GetHealthRequest {}

message GetHealthResponse {
  bool serving = 1;          // True if the gRPC server is accepting requests
  bool ready = 2;            // True if the node has loaded state and can serve queries
  uint64 current_block = 3;  // Latest committed block number (0 if not ready)
  uint64 uptime_secs = 4;    // Seconds since node started
  ReplayVerificationInfo replay_verification = 5;
}

// --- Dry Run ---

message DryRunMessageRequest {
  Message message = 1;
}

message DryRunMessageResponse {
  bool would_accept = 1;
  string error = 2;           // Non-empty if would_accept is false
  string error_stage = 3;     // "verification", "validation", "authorization", "state_transition"
}

// --- Commit Graph ---

message GetCommitAncestorsRequest {
  bytes project_id = 1;
  bytes commit_hash = 2;     // Start commit (walk backwards from here)
  uint32 limit = 3;          // Max commits to return (default 50)
}

message GetCommitAncestorsResponse {
  repeated CommitMeta commits = 1;
}

// --- QMDB Proof-Verified Sync (Task #117 canonical restoration) ---
//
// Tip-discovery primitive for cold-start QMDB sync. The bulk operation
// transfer that follows lives on P2P channel 8 (see
// `crates/makechain-sync/src/p2p_sync.rs`); this gRPC RPC only carries the
// finalized-tip metadata + finalization cert needed to compute the byzantine
// `verify_fetched_sync_target` decision before opening the channel-8 fetch.

message GetSyncTargetRequest {}

message GetSyncTargetResponse {
  bytes root = 1;              // 32-byte QMDB ops MMR root digest (Blake3)
  uint64 range_start = 2;     // Operation location range start (inclusive)
  uint64 range_end = 3;       // Operation location range end (exclusive)
  uint64 block_number = 4;    // Block number at this sync point
  bytes canonical_root = 5;   // 32-byte canonical state root (consensus-authenticated)
  Block block = 6;            // Certified committed tip block
  // Bootstrap boundary the serving peer used (if any). 0 means
  // "BLS-from-genesis" (no Ed era at all). Non-zero is the height past
  // which the peer transitioned from Ed25519 simplex consensus to BLS
  // threshold consensus. A fresh `BlsFromGenesis` validator that probes
  // a peer with a non-zero boundary must upgrade its decode mode to
  // `EdBootstrap` for any committed tip block whose height is < boundary,
  // because the `consensus_finalization` (field 8) for those blocks is
  // Ed25519-encoded, not BLS-encoded.
  uint64 bootstrap_boundary = 7;
  // commonware-codec `Finalization<scheme, BlockDigest>` for the certified
  // tip `block` (#651). Carried as a separate wire item here rather than on
  // the hash-committed `BlockBody`; `verify_fetched_sync_target` decodes it
  // (dispatching Ed/BLS via `bootstrap_boundary`) and verifies it against the
  // epoch scheme before binding the peer-advertised root/range to the
  // certified header. Must be non-empty for a non-genesis target.
  bytes consensus_finalization = 8;
}

// --- Late-rejoin epoch state ---

message GetEpochStateRequest {
  uint64 epoch = 1;
}

message GetEpochStateResponse {
  uint64 epoch = 1;
  // Encoded `dkg::Output<MinSig, ed25519::PublicKey>` — public material
  // only (players + threshold polynomial). Contains no secret share.
  // Empty when the responding peer has no persisted state for the
  // requested epoch.
  bytes output = 2;
  bool found = 3;
}

// --- Current-State Proofs ---

// GetStateProof — polymorphic per-key state proof RPC (Cleanup #23, issue #647).
// Replaces the former GetOperationProof / GetExclusionProof / GetStorageQuotaProof /
// GetCompoundProof RPCs. For each requested key the server inspects existence and
// returns either an operation proof (key present, with value) or an exclusion proof
// (key absent). All returned proofs are anchored at a single shared QMDB root.
//
// Compound 2P-set and storage-quota use cases compose at the client side by sending
// the relevant set of keys (active + tombstone + prune_marker; or storage_grant keys
// derived against an account) in one call.
message GetStateProofRequest {
  repeated bytes keys = 1;  // Raw supported public state keys (not QMDB-padded). May be empty
                            // to request the current state root only with no per-key proofs.
}

message KeyProof {
  bytes key = 1;          // Echoed raw state key from the request
  bytes value = 2;        // Current value of the key. Empty iff is_exclusion = true
  bytes proof = 3;        // Native commonware-codec proof bytes (operation or exclusion body)
  bool is_exclusion = 4;  // True iff proof is an exclusion proof; false iff operation proof
}

message GetStateProofResponse {
  bytes root = 1;            // 32-byte canonical current QMDB root shared by all proofs
  repeated KeyProof proofs = 2;
}

message VerifyOperationProofRequest {
  bytes proof = 1;  // Native commonware-codec operation-proof bytes (from GetOperationProof.proof)
  bytes key = 2;    // Raw state key (same format as GetOperationProofRequest.key)
  bytes root = 3;   // 32-byte root the proof was generated against (from GetOperationProof.root)
  bytes value = 4;  // The current value of the key (from GetOperationProof.value)
}

message VerifyOperationProofResponse {
  bool valid = 1;        // Whether the proof verifies against the current committed root
  string error = 2;      // Human-readable error if invalid (empty if valid)
  bytes root = 3;        // The committed root used for verification
}

message VerifyExclusionProofRequest {
  bytes proof = 1;  // Native commonware-codec exclusion-proof bytes (from GetExclusionProof.proof)
  bytes key = 2;    // Raw state key (same format as GetExclusionProofRequest.key)
  bytes root = 3;   // 32-byte root the proof was generated against (from GetExclusionProof.root)
}

message VerifyExclusionProofResponse {
  bool valid = 1;        // Whether the proof verifies against the current committed root
  string error = 2;      // Human-readable error if invalid (empty if valid)
  bytes root = 3;        // The committed root used for verification
}

message VerifyOperationProofAtBlockRequest {
  bytes proof = 1;         // Native commonware-codec operation-proof bytes (from GetOperationProof.proof)
  bytes key = 2;           // Raw state key (same format as GetOperationProofRequest.key)
  uint64 block_number = 3; // Canonical finalized block number whose state root must match the proof root
  bytes value = 4;         // The value of the key (from GetOperationProof.value)
}

message VerifyOperationProofAtBlockResponse {
  bool valid = 1;        // Whether the proof verifies against the requested finalized block's state root
  string error = 2;      // Human-readable error if invalid (empty if valid)
  bytes root = 3;        // Canonical finalized state root resolved for block_number
  uint64 block_number = 4;  // Requested finalized block number used for verification (0 if unavailable)
}

message VerifyExclusionProofAtBlockRequest {
  bytes proof = 1;         // Native commonware-codec exclusion-proof bytes (from GetExclusionProof.proof)
  bytes key = 2;           // Raw state key (same format as GetExclusionProofRequest.key)
  uint64 block_number = 3; // Canonical finalized block number whose state root must match the proof root
}

message VerifyExclusionProofAtBlockResponse {
  bool valid = 1;        // Whether the proof verifies against the requested finalized block's state root
  string error = 2;      // Human-readable error if invalid (empty if valid)
  bytes root = 3;        // Canonical finalized state root resolved for block_number
  uint64 block_number = 4;  // Requested finalized block number used for verification (0 if unavailable)
}

// GetStorageQuotaProof / GetCompoundProof / VerifyStorageQuotaProof messages removed
// in Cleanup #23 (issue #647). Callers compose equivalent bundles client-side by
// sending the relevant set of keys to GetStateProof in one call.

// PR-a of tracker #651 — GetFinalizationCertificate request/response.
message GetFinalizationCertificateRequest {
  bytes block_hash = 1;  // 32-byte block digest
}

message GetFinalizationCertificateResponse {
  // commonware-codec-encoded direct `Finalization<MakechainScheme, BlockDigest>`
  // bytes. Empty when the archive does not have a direct cert for the requested
  // digest (treat as NotFound).
  bytes certificate = 1;
}
