Skip to main content

exo_node/
crosschecked_anchor_store.rs

1// Copyright 2026 Exochain Foundation
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at:
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Persistent, fail-closed CrossChecked commitment recording.
18//!
19//! This module is intentionally not registered as an HTTP route. It provides
20//! the durable authority, idempotency, signature-journal, and authenticated
21//! readback boundary required before a route can be activated in a later,
22//! separately reviewed change.
23
24use std::{
25    collections::BTreeMap,
26    path::{Path, PathBuf},
27    time::Duration,
28};
29
30use ciborium::Value;
31use exo_api::crosschecked_anchor::{
32    ANCHOR_PATH, CrossCheckedAnchorResponseV1, RequestValidationContext, ResponseValidationContext,
33    decode_and_validate_request, decode_and_validate_response, decode_unverified_replay_locator,
34};
35use exo_authority::{
36    AuthorityChain, Permission,
37    chain::{has_permission, verify_chain},
38};
39use exo_core::{
40    crypto,
41    types::{Did, Hash256, PublicKey, ReceiptOutcome, Signature, Timestamp, TrustReceipt},
42};
43use exo_identity::{
44    did::{DidDocument, VerificationMethod},
45    did_verification::validate_verification_method_document_binding,
46};
47use frost_ed25519 as frost;
48use rusqlite::{Connection, OptionalExtension, Transaction, TransactionBehavior, params};
49use serde::{Deserialize, Serialize};
50use thiserror::Error;
51
52const SCOPE_BINDING_DOMAIN: &str = "exo.crosschecked.scope_binding.v1";
53const AUTHORITY_CHAIN_DOMAIN: &str = "exo.crosschecked.authority_chain.v1";
54const AUTHORITY_CHAIN_HASH_DOMAIN: &str = "exo.crosschecked.authority_chain_hash.v1";
55const RETIREMENT_DOMAIN: &str = "exo.crosschecked.authority_retirement.v1";
56const SIGNING_OPERATION_DOMAIN: &str = "exo.crosschecked.signing_operation.v1";
57const RECEIPT_ACTION_TYPE: &str = "crosschecked.receipt_commitment.record.v1";
58const ANCHOR_PERMISSION_CODE: &str = "anchor_receipt_commitment";
59const STORE_SCHEMA_VERSION: &str = "crosschecked_anchor_store_v2_frost_governed";
60const AUTHORITY_GOVERNANCE_DOMAIN: &str = "exo.crosschecked.authority_governance.v1";
61const ANCHOR_NODE_POLICY_DOMAIN: &str = "exo.crosschecked.anchor_node_policy.v1";
62const GOVERNANCE_SIGNATURE_ALGORITHM: &str = "frost-ed25519-sha512-rfc9591";
63pub const CROSSCHECKED_GOVERNANCE_THRESHOLD: u16 = 7;
64pub const CROSSCHECKED_GOVERNANCE_PARTICIPANTS: u16 = 13;
65
66/// Node identity pinned into both receipt and response signatures.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct AnchorNodeIdentity {
69    pub did: String,
70    pub key_id: String,
71    pub public_key: PublicKey,
72}
73
74/// Immutable runtime policy persisted at store creation and checked on reopen.
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct AnchorStoreConfig {
77    pub expected_audience: String,
78    pub crosschecked_intermediate_did: String,
79    pub crosschecked_intermediate_key_id: String,
80    pub crosschecked_intermediate_public_key: PublicKey,
81    pub governance_frost_group_public_key: [u8; 32],
82    pub governance_frost_key_epoch: u64,
83    pub node_identity: AnchorNodeIdentity,
84}
85
86/// Closed authority lifecycle event signed by the CrossChecked governance group.
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum AuthorityLifecycleEventV1 {
90    Provision,
91    Retire,
92    Revoke,
93}
94
95impl AuthorityLifecycleEventV1 {
96    #[must_use]
97    pub fn code(self) -> &'static str {
98        match self {
99            Self::Provision => "provision",
100            Self::Retire => "retire",
101            Self::Revoke => "revoke",
102        }
103    }
104}
105
106/// RFC 9591 FROST(Ed25519, SHA-512) authorization for one exact authority mutation.
107#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
108pub struct AuthorityGovernanceAuthorizationV1 {
109    pub protocol_version: u16,
110    pub authorization_id: [u8; 32],
111    pub ceremony_id: [u8; 32],
112    pub governance_key_epoch: u64,
113    pub threshold: u16,
114    pub participant_count: u16,
115    pub signer_ids: Vec<u16>,
116    pub lifecycle_event: AuthorityLifecycleEventV1,
117    pub authority_did: Did,
118    pub authority_key_id: String,
119    pub authority_key_epoch: u64,
120    pub scope_alias: [u8; 32],
121    pub package_hash: Hash256,
122    pub node_policy_hash: Hash256,
123    pub authorization_sequence: u64,
124    pub prior_authorization_hash: Hash256,
125    pub valid_from_ms: u64,
126    pub valid_until_ms: u64,
127    pub signature_algorithm: String,
128    pub signature: Vec<u8>,
129}
130
131impl AuthorityGovernanceAuthorizationV1 {
132    /// Deterministic CBOR bytes signed by the pinned FROST group key.
133    pub fn signing_preimage(&self) -> Result<Vec<u8>, AnchorStoreError> {
134        encode_value(&Value::Array(vec![
135            text(AUTHORITY_GOVERNANCE_DOMAIN),
136            unsigned(u64::from(self.protocol_version)),
137            bytes(&self.authorization_id),
138            bytes(&self.ceremony_id),
139            unsigned(self.governance_key_epoch),
140            unsigned(u64::from(self.threshold)),
141            unsigned(u64::from(self.participant_count)),
142            Value::Array(
143                self.signer_ids
144                    .iter()
145                    .map(|identifier| unsigned(u64::from(*identifier)))
146                    .collect(),
147            ),
148            text(self.lifecycle_event.code()),
149            text(self.authority_did.as_str()),
150            text(&self.authority_key_id),
151            unsigned(self.authority_key_epoch),
152            bytes(&self.scope_alias),
153            bytes(self.package_hash.as_bytes()),
154            bytes(self.node_policy_hash.as_bytes()),
155            unsigned(self.authorization_sequence),
156            bytes(self.prior_authorization_hash.as_bytes()),
157            unsigned(self.valid_from_ms),
158            unsigned(self.valid_until_ms),
159            text(&self.signature_algorithm),
160        ]))
161    }
162
163    /// Canonical CBOR authorization artifact persisted with its target mutation.
164    pub fn to_cbor_bytes(&self) -> Result<Vec<u8>, AnchorStoreError> {
165        encode_value(&Value::Array(vec![
166            Value::Bytes(self.signing_preimage()?),
167            bytes(&self.signature),
168        ]))
169    }
170
171    /// BLAKE3 commitment to the exact canonical authorization artifact.
172    pub fn authorization_hash(&self) -> Result<Hash256, AnchorStoreError> {
173        Ok(Hash256::digest(&self.to_cbor_bytes()?))
174    }
175}
176
177/// Signed binding between one child authority and one opaque workspace scope.
178#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
179pub struct CrossCheckedScopeBindingV1 {
180    pub protocol_version: u16,
181    pub authority_did: Did,
182    pub authority_key_id: String,
183    pub grant_id: [u8; 32],
184    pub scope_alias: [u8; 32],
185    pub audience: String,
186    pub permission: Permission,
187    pub key_epoch: u64,
188    pub valid_from_ms: u64,
189    pub valid_until_ms: u64,
190    pub chain_fingerprint: Hash256,
191    pub binding_signer_did: Did,
192    pub binding_signer_key_id: String,
193    pub signature: Signature,
194}
195
196impl CrossCheckedScopeBindingV1 {
197    /// Deterministic, map-free CBOR preimage signed by the CrossChecked
198    /// intermediate authority.
199    ///
200    /// # Errors
201    /// Returns a serialization error if canonical CBOR encoding fails.
202    pub fn signing_preimage(&self) -> Result<Vec<u8>, AnchorStoreError> {
203        encode_value(&Value::Array(vec![
204            text(SCOPE_BINDING_DOMAIN),
205            unsigned(u64::from(self.protocol_version)),
206            text(self.authority_did.as_str()),
207            text(&self.authority_key_id),
208            bytes(&self.grant_id),
209            bytes(&self.scope_alias),
210            text(&self.audience),
211            text(permission_code(self.permission)?),
212            unsigned(self.key_epoch),
213            unsigned(self.valid_from_ms),
214            unsigned(self.valid_until_ms),
215            bytes(self.chain_fingerprint.as_bytes()),
216            text(self.binding_signer_did.as_str()),
217            text(&self.binding_signer_key_id),
218        ]))
219    }
220}
221
222/// Complete owner-authorized provisioning package persisted as one snapshot.
223#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
224pub struct AuthorityProvisioningV1 {
225    pub protocol_version: u16,
226    pub did_documents: Vec<DidDocument>,
227    pub authority_chain: AuthorityChain,
228    pub scope_binding: CrossCheckedScopeBindingV1,
229}
230
231impl AuthorityProvisioningV1 {
232    /// Encode the exact CBOR bytes persisted by [`AnchorStore::provision_authority`].
233    ///
234    /// # Errors
235    /// Returns a serialization error if CBOR encoding fails.
236    pub fn to_cbor_bytes(&self) -> Result<Vec<u8>, AnchorStoreError> {
237        encode_serde(self)
238    }
239}
240
241/// Signed retirement of one exact authority key epoch.
242#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
243pub struct AuthorityRetirementV1 {
244    pub protocol_version: u16,
245    pub authority_did: Did,
246    pub authority_key_id: String,
247    pub key_epoch: u64,
248    pub retired_at_ms: u64,
249    pub signer_did: Did,
250    pub signer_key_id: String,
251    pub signature: Signature,
252}
253
254impl AuthorityRetirementV1 {
255    /// Deterministic, map-free CBOR retirement preimage.
256    ///
257    /// # Errors
258    /// Returns a serialization error if canonical CBOR encoding fails.
259    pub fn signing_preimage(&self) -> Result<Vec<u8>, AnchorStoreError> {
260        encode_value(&Value::Array(vec![
261            text(RETIREMENT_DOMAIN),
262            unsigned(u64::from(self.protocol_version)),
263            text(self.authority_did.as_str()),
264            text(&self.authority_key_id),
265            unsigned(self.key_epoch),
266            unsigned(self.retired_at_ms),
267            text(self.signer_did.as_str()),
268            text(&self.signer_key_id),
269        ]))
270    }
271
272    /// Encode the exact CBOR bytes persisted by [`AnchorStore::retire_authority`].
273    ///
274    /// # Errors
275    /// Returns a serialization error if CBOR encoding fails.
276    pub fn to_cbor_bytes(&self) -> Result<Vec<u8>, AnchorStoreError> {
277        encode_serde(self)
278    }
279}
280
281/// A transport-bound submission supplied by a future route adapter.
282#[derive(Clone, Copy, Debug)]
283pub struct SubmissionContext<'a> {
284    pub method: &'a str,
285    pub path: &'a str,
286    pub content_type: &'a str,
287    pub body: &'a [u8],
288    pub now: Timestamp,
289}
290
291/// Whether a response was newly committed or returned from exact durable replay.
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
293pub enum AnchorRecordDisposition {
294    Created,
295    Replayed,
296}
297
298/// Cryptographically validated record returned after commit or exact replay.
299#[derive(Clone, Debug, PartialEq, Eq)]
300pub struct AnchorRecord {
301    pub disposition: AnchorRecordDisposition,
302    pub request_hash: Hash256,
303    pub action_hash: Hash256,
304    pub response: CrossCheckedAnchorResponseV1,
305    pub response_body: Vec<u8>,
306}
307
308/// Cryptographically authenticated stored record returned by readback.
309#[derive(Clone, Debug, PartialEq, Eq)]
310pub struct AuthenticatedAnchorReadback {
311    pub request_body: Vec<u8>,
312    pub response: CrossCheckedAnchorResponseV1,
313    pub response_body: Vec<u8>,
314}
315
316/// Stable idempotency conflict categories suitable for later HTTP 409 mapping.
317#[derive(Clone, Copy, Debug, PartialEq, Eq)]
318pub enum AnchorConflictKind {
319    IdempotencyKey,
320    ActionHash,
321}
322
323/// Errors from a signer that durably binds one operation ID to one payload.
324#[derive(Clone, Debug, Error, PartialEq, Eq)]
325pub enum SignOnceError {
326    #[error("durable signer unavailable: {0}")]
327    Unavailable(String),
328    #[error("signing operation ID was already bound to another payload")]
329    OperationPayloadConflict,
330}
331
332/// Signer boundary required by the persistence service.
333///
334/// Implementations must durably journal `operation_id -> payload hash ->
335/// signature` before returning. Repeating the same operation and payload must
336/// return the same Ed25519 signature without a second signing ceremony;
337/// repeating an operation with another payload must fail closed.
338pub trait DurableAnchorSigner: Send + Sync {
339    fn identity(&self) -> AnchorNodeIdentity;
340    fn reserved_recorded_at(
341        &self,
342        request_hash: Hash256,
343    ) -> Result<Option<Timestamp>, SignOnceError>;
344    fn reserve_recorded_at(
345        &self,
346        request_hash: Hash256,
347        proposed: Timestamp,
348    ) -> Result<Timestamp, SignOnceError>;
349    fn sign_once(&self, operation_id: Hash256, payload: &[u8]) -> Result<Signature, SignOnceError>;
350}
351
352/// Closed storage and authorization errors.
353#[derive(Clone, Debug, Error, PartialEq, Eq)]
354pub enum AnchorStoreError {
355    #[error("anchor codec rejected request or response: {0}")]
356    Codec(String),
357    #[error("authority validation failed: {0}")]
358    AuthorityValidation(String),
359    #[error("authority binding was not found")]
360    AuthorityNotFound,
361    #[error("authority binding is not yet valid")]
362    AuthorityNotYetValid,
363    #[error("authority binding is expired")]
364    AuthorityExpired,
365    #[error("authority key epoch is retired")]
366    AuthorityRetired,
367    #[error("authority governance authorization failed: {0}")]
368    GovernanceAuthorization(String),
369    #[error("authority governance authorization conflicts with durable state")]
370    GovernanceAuthorizationConflict,
371    #[error("anchor conflict: {0:?}")]
372    Conflict(AnchorConflictKind),
373    #[error("durable signer failed: {0}")]
374    Signer(SignOnceError),
375    #[error("storage failure: {0}")]
376    Storage(String),
377    #[error("authenticated readback failed: {0}")]
378    ReadbackValidation(String),
379}
380
381/// SQLite-backed store. Each operation opens its own connection so callers
382/// may safely share the store across worker threads.
383#[derive(Clone, Debug)]
384pub struct AnchorStore {
385    path: PathBuf,
386    config: AnchorStoreConfig,
387}
388
389#[derive(Clone, Debug)]
390struct ValidatedProvisioning {
391    authority_public_key: PublicKey,
392    authority_chain_hash: Hash256,
393}
394
395#[derive(Clone, Debug)]
396struct StoredAuthority {
397    provisioning: AuthorityProvisioningV1,
398    provisioning_cbor: Vec<u8>,
399    authority_public_key: PublicKey,
400    authority_chain_hash: Hash256,
401    retired_at_ms: Option<u64>,
402    retirement_cbor: Option<Vec<u8>>,
403}
404
405#[derive(Clone, Debug)]
406struct StoredAnchorRow {
407    authority_did: String,
408    authority_key_id: String,
409    idempotency_key: [u8; 32],
410    source_code: String,
411    action_hash: [u8; 32],
412    request_hash: Hash256,
413    request_body: Vec<u8>,
414    receipt_body: Vec<u8>,
415    response_body: Vec<u8>,
416    authority_chain_hash: Hash256,
417    authority_public_key: PublicKey,
418    node_recorded_at: Timestamp,
419}
420
421#[derive(Clone, Debug)]
422struct StoredGovernanceAuthorization {
423    authorization: AuthorityGovernanceAuthorizationV1,
424    authorization_id: [u8; 32],
425    scope_alias: [u8; 32],
426    authorization_sequence: u64,
427    authorization_hash: Hash256,
428    lifecycle_event: AuthorityLifecycleEventV1,
429    authority_did: String,
430    authority_key_id: String,
431    authority_key_epoch: u64,
432    package_hash: Hash256,
433    applied_at_ms: u64,
434    authorization_cbor: Vec<u8>,
435}
436
437struct PersistedStoreConfig {
438    schema_version: String,
439    expected_audience: String,
440    intermediate_did: String,
441    intermediate_key_id: String,
442    intermediate_public_key: Vec<u8>,
443    node_did: String,
444    governance_frost_group_public_key: Vec<u8>,
445    governance_frost_key_epoch: i64,
446    governance_threshold: i64,
447    governance_participant_count: i64,
448    node_key_id: String,
449    node_public_key: Vec<u8>,
450}
451
452impl AnchorStore {
453    /// Open or create the store and bind it to immutable runtime policy.
454    ///
455    /// # Errors
456    /// Fails closed when the path, schema, or previously persisted runtime
457    /// policy is unavailable or inconsistent.
458    pub fn open(
459        path: impl AsRef<Path>,
460        config: AnchorStoreConfig,
461    ) -> Result<Self, AnchorStoreError> {
462        validate_config(&config)?;
463        let store = Self {
464            path: path.as_ref().to_path_buf(),
465            config,
466        };
467        let connection = store.connection()?;
468        initialize_schema(&connection, &store.config)?;
469        Ok(store)
470    }
471
472    /// Validate and persist a signed DID/chain/scope authority snapshot.
473    /// Identical provisioning is idempotent; any replacement under the same
474    /// DID/key ID is rejected.
475    ///
476    /// # Errors
477    /// Fails for invalid identity documents, delegation signatures, scope,
478    /// key epoch, audience, validity, or persistent conflicts.
479    pub fn provision_authority(
480        &self,
481        provisioning: &AuthorityProvisioningV1,
482        authorization: &AuthorityGovernanceAuthorizationV1,
483        applied_at_ms: u64,
484    ) -> Result<(), AnchorStoreError> {
485        let validated = validate_provisioning(&self.config, provisioning)?;
486        let provisioning_cbor = provisioning.to_cbor_bytes()?;
487        let package_hash = Hash256::digest(&provisioning_cbor);
488        validate_governance_authorization(
489            &self.config,
490            authorization,
491            AuthorityLifecycleEventV1::Provision,
492            provisioning.scope_binding.authority_did.as_str(),
493            &provisioning.scope_binding.authority_key_id,
494            provisioning.scope_binding.key_epoch,
495            &provisioning.scope_binding.scope_alias,
496            package_hash,
497        )?;
498        let authorization_cbor = authorization.to_cbor_bytes()?;
499        let authorization_hash = authorization.authorization_hash()?;
500        let mut connection = self.connection()?;
501        let transaction = connection
502            .transaction_with_behavior(TransactionBehavior::Immediate)
503            .map_err(storage)?;
504
505        if let Some(existing) =
506            load_governance_authorization_by_id(&transaction, &authorization.authorization_id)?
507        {
508            validate_exact_governance_replay(
509                &existing,
510                authorization,
511                &authorization_cbor,
512                authorization_hash,
513                package_hash,
514            )?;
515            let existing_authority = load_authority(
516                &transaction,
517                provisioning.scope_binding.authority_did.as_str(),
518                &provisioning.scope_binding.authority_key_id,
519            )?
520            .ok_or(AnchorStoreError::GovernanceAuthorizationConflict)?;
521            if existing_authority.provisioning_cbor != provisioning_cbor {
522                return Err(AnchorStoreError::GovernanceAuthorizationConflict);
523            }
524            validate_stored_authority_governed(&transaction, &self.config, &existing_authority)?;
525            transaction.commit().map_err(storage)?;
526            return Ok(());
527        }
528
529        validate_governance_application_time(authorization, applied_at_ms)?;
530        validate_governance_sequence(&transaction, authorization, applied_at_ms)?;
531
532        let existing: Option<Vec<u8>> = transaction
533            .query_row(
534                "SELECT provisioning_cbor FROM crosschecked_anchor_authorities
535                 WHERE authority_did = ?1 AND authority_key_id = ?2",
536                params![
537                    provisioning.scope_binding.authority_did.as_str(),
538                    provisioning.scope_binding.authority_key_id
539                ],
540                |row| row.get(0),
541            )
542            .optional()
543            .map_err(storage)?;
544        if existing.is_some() {
545            return Err(AnchorStoreError::AuthorityValidation(
546                "authority DID/key ID already has different provisioning".into(),
547            ));
548        }
549
550        let max_epoch: Option<i64> = transaction
551            .query_row(
552                "SELECT MAX(key_epoch) FROM crosschecked_anchor_authorities
553                 WHERE authority_did = ?1",
554                [provisioning.scope_binding.authority_did.as_str()],
555                |row| row.get(0),
556            )
557            .map_err(storage)?;
558        if max_epoch.is_some_and(|epoch| {
559            u64::try_from(epoch)
560                .ok()
561                .is_some_and(|epoch| provisioning.scope_binding.key_epoch <= epoch)
562        }) {
563            return Err(AnchorStoreError::AuthorityValidation(
564                "key epoch must increase monotonically".into(),
565            ));
566        }
567
568        transaction
569            .execute(
570                "INSERT INTO crosschecked_anchor_authorities (
571                    authority_did, authority_key_id, grant_id, scope_alias,
572                    audience, permission_code, key_epoch, valid_from_ms,
573                    valid_until_ms, retired_at_ms, authority_public_key,
574                    authority_chain_hash, provisioning_cbor, retirement_cbor
575                 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, NULL, ?10, ?11, ?12, NULL)",
576                params![
577                    provisioning.scope_binding.authority_did.as_str(),
578                    provisioning.scope_binding.authority_key_id,
579                    provisioning.scope_binding.grant_id.as_slice(),
580                    provisioning.scope_binding.scope_alias.as_slice(),
581                    provisioning.scope_binding.audience,
582                    ANCHOR_PERMISSION_CODE,
583                    to_i64(provisioning.scope_binding.key_epoch, "key_epoch")?,
584                    to_i64(provisioning.scope_binding.valid_from_ms, "valid_from_ms")?,
585                    to_i64(provisioning.scope_binding.valid_until_ms, "valid_until_ms")?,
586                    validated.authority_public_key.as_bytes().as_slice(),
587                    validated.authority_chain_hash.as_bytes().as_slice(),
588                    provisioning_cbor,
589                ],
590            )
591            .map_err(|error| {
592                AnchorStoreError::AuthorityValidation(format!(
593                    "persistent authority uniqueness rejected provisioning: {error}"
594                ))
595            })?;
596        persist_governance_authorization(
597            &transaction,
598            authorization,
599            &authorization_cbor,
600            authorization_hash,
601            applied_at_ms,
602        )?;
603        transaction.commit().map_err(storage)
604    }
605
606    /// Persist a signed key-epoch retirement. Identical retirement is
607    /// idempotent; conflicting retirement evidence is rejected.
608    ///
609    /// # Errors
610    /// Fails if the retirement is not signed by the configured intermediate,
611    /// targets another authority epoch, predates the binding, or conflicts
612    /// with existing evidence.
613    pub fn retire_authority(
614        &self,
615        retirement: &AuthorityRetirementV1,
616        authorization: &AuthorityGovernanceAuthorizationV1,
617        applied_at_ms: u64,
618    ) -> Result<(), AnchorStoreError> {
619        self.end_authority(
620            retirement,
621            AuthorityLifecycleEventV1::Retire,
622            authorization,
623            applied_at_ms,
624        )
625    }
626
627    /// Persist a governance-authorized revocation of one exact authority epoch.
628    pub fn revoke_authority(
629        &self,
630        retirement: &AuthorityRetirementV1,
631        authorization: &AuthorityGovernanceAuthorizationV1,
632        applied_at_ms: u64,
633    ) -> Result<(), AnchorStoreError> {
634        self.end_authority(
635            retirement,
636            AuthorityLifecycleEventV1::Revoke,
637            authorization,
638            applied_at_ms,
639        )
640    }
641
642    fn end_authority(
643        &self,
644        retirement: &AuthorityRetirementV1,
645        lifecycle_event: AuthorityLifecycleEventV1,
646        authorization: &AuthorityGovernanceAuthorizationV1,
647        applied_at_ms: u64,
648    ) -> Result<(), AnchorStoreError> {
649        let retirement_cbor = retirement.to_cbor_bytes()?;
650        let package_hash = Hash256::digest(&retirement_cbor);
651        let mut connection = self.connection()?;
652        let transaction = connection
653            .transaction_with_behavior(TransactionBehavior::Immediate)
654            .map_err(storage)?;
655        let authority = load_authority(
656            &transaction,
657            retirement.authority_did.as_str(),
658            &retirement.authority_key_id,
659        )?
660        .ok_or(AnchorStoreError::AuthorityNotFound)?;
661        validate_stored_authority_governed(&transaction, &self.config, &authority)?;
662        validate_retirement(&self.config, &authority.provisioning, retirement)?;
663        validate_governance_authorization(
664            &self.config,
665            authorization,
666            lifecycle_event,
667            retirement.authority_did.as_str(),
668            &retirement.authority_key_id,
669            retirement.key_epoch,
670            &authority.provisioning.scope_binding.scope_alias,
671            package_hash,
672        )?;
673        let authorization_cbor = authorization.to_cbor_bytes()?;
674        let authorization_hash = authorization.authorization_hash()?;
675
676        if let Some(existing) =
677            load_governance_authorization_by_id(&transaction, &authorization.authorization_id)?
678        {
679            validate_exact_governance_replay(
680                &existing,
681                authorization,
682                &authorization_cbor,
683                authorization_hash,
684                package_hash,
685            )?;
686            if authority.retired_at_ms != Some(retirement.retired_at_ms)
687                || authority.retirement_cbor.as_deref() != Some(retirement_cbor.as_slice())
688            {
689                return Err(AnchorStoreError::GovernanceAuthorizationConflict);
690            }
691            transaction.commit().map_err(storage)?;
692            return Ok(());
693        }
694
695        validate_governance_application_time(authorization, applied_at_ms)?;
696        validate_governance_sequence(&transaction, authorization, applied_at_ms)?;
697
698        if authority.retired_at_ms.is_some() {
699            return Err(AnchorStoreError::AuthorityValidation(
700                "authority epoch already has different retirement evidence".into(),
701            ));
702        }
703
704        transaction
705            .execute(
706                "UPDATE crosschecked_anchor_authorities
707                 SET retired_at_ms = ?1, retirement_cbor = ?2
708                 WHERE authority_did = ?3 AND authority_key_id = ?4",
709                params![
710                    to_i64(retirement.retired_at_ms, "retired_at_ms")?,
711                    retirement_cbor,
712                    retirement.authority_did.as_str(),
713                    retirement.authority_key_id,
714                ],
715            )
716            .map_err(storage)?;
717        persist_governance_authorization(
718            &transaction,
719            authorization,
720            &authorization_cbor,
721            authorization_hash,
722            applied_at_ms,
723        )?;
724        transaction.commit().map_err(storage)
725    }
726
727    /// Validate, authorize, sign exactly once, and atomically commit one
728    /// request/idempotency/receipt/response record. Exact durable replay is
729    /// checked before live authority expiry or retirement.
730    ///
731    /// # Errors
732    /// Fails closed for invalid protocol bytes, authority state, conflicts,
733    /// signer errors, persistence errors, or failed self-verification.
734    pub fn record(
735        &self,
736        submission: SubmissionContext<'_>,
737        signer: &dyn DurableAnchorSigner,
738    ) -> Result<AnchorRecord, AnchorStoreError> {
739        let locator = decode_unverified_replay_locator(
740            submission.body,
741            submission.method,
742            submission.path,
743            submission.content_type,
744        )
745        .map_err(codec)?;
746        let mut connection = self.connection()?;
747        let transaction = connection
748            .transaction_with_behavior(TransactionBehavior::Immediate)
749            .map_err(storage)?;
750
751        if let Some(stored) = load_anchor_by_idempotency(
752            &transaction,
753            &locator.authority_did,
754            &locator.idempotency_key,
755        )? {
756            if stored.request_body != submission.body {
757                return Err(AnchorStoreError::Conflict(
758                    AnchorConflictKind::IdempotencyKey,
759                ));
760            }
761            let readback = validate_stored_anchor(&transaction, &self.config, &stored)?;
762            transaction.commit().map_err(storage)?;
763            return Ok(AnchorRecord {
764                disposition: AnchorRecordDisposition::Replayed,
765                request_hash: stored.request_hash,
766                action_hash: Hash256::from_bytes(stored.action_hash),
767                response: readback.response,
768                response_body: readback.response_body,
769            });
770        }
771
772        if load_anchor_by_action(&transaction, &locator.source_code, &locator.action_hash)?
773            .is_some()
774        {
775            return Err(AnchorStoreError::Conflict(AnchorConflictKind::ActionHash));
776        }
777
778        let authority = load_authority(
779            &transaction,
780            &locator.authority_did,
781            &locator.authority_key_id,
782        )?
783        .ok_or(AnchorStoreError::AuthorityNotFound)?;
784        validate_stored_authority_governed(&transaction, &self.config, &authority)?;
785        let signer_identity = signer.identity();
786        if signer_identity != self.config.node_identity {
787            return Err(AnchorStoreError::Signer(SignOnceError::Unavailable(
788                "signer identity does not match store policy".into(),
789            )));
790        }
791
792        let recorded_at = match signer
793            .reserved_recorded_at(locator.request_hash)
794            .map_err(AnchorStoreError::Signer)?
795        {
796            Some(recorded_at) => recorded_at,
797            None => {
798                validate_authority_time(&authority, &locator, submission.now.physical_ms)?;
799                let live_validated = decode_and_validate_request(
800                    submission.body,
801                    RequestValidationContext {
802                        method: submission.method,
803                        path: submission.path,
804                        content_type: submission.content_type,
805                        expected_audience: &self.config.expected_audience,
806                        now_ms: submission.now.physical_ms,
807                        authority_public_key: &authority.authority_public_key,
808                    },
809                )
810                .map_err(codec)?;
811                validate_request_binding(
812                    &live_validated.request,
813                    &authority.provisioning.scope_binding,
814                )?;
815                signer
816                    .reserve_recorded_at(locator.request_hash, submission.now)
817                    .map_err(AnchorStoreError::Signer)?
818            }
819        };
820
821        validate_authority_time(&authority, &locator, recorded_at.physical_ms)?;
822        let validated = decode_and_validate_request(
823            submission.body,
824            RequestValidationContext {
825                method: submission.method,
826                path: submission.path,
827                content_type: submission.content_type,
828                expected_audience: &self.config.expected_audience,
829                now_ms: recorded_at.physical_ms,
830                authority_public_key: &authority.authority_public_key,
831            },
832        )
833        .map_err(codec)?;
834        validate_request_binding(&validated.request, &authority.provisioning.scope_binding)?;
835
836        let action_hash = Hash256::from_bytes(validated.request.action_hash);
837        let mut receipt = TrustReceipt::new(
838            Did::new(&signer_identity.did).map_err(|error| {
839                AnchorStoreError::Signer(SignOnceError::Unavailable(error.to_string()))
840            })?,
841            authority.authority_chain_hash,
842            None,
843            RECEIPT_ACTION_TYPE.to_owned(),
844            action_hash,
845            ReceiptOutcome::Executed,
846            recorded_at,
847            &|_| Signature::empty(),
848        )
849        .map_err(|error| AnchorStoreError::Signer(SignOnceError::Unavailable(error.to_string())))?;
850        let receipt_payload = receipt.signing_payload().map_err(|error| {
851            AnchorStoreError::Signer(SignOnceError::Unavailable(error.to_string()))
852        })?;
853        let receipt_operation = signing_operation_id(validated.request_hash, "receipt")?;
854        receipt.signature = signer
855            .sign_once(receipt_operation, &receipt_payload)
856            .map_err(AnchorStoreError::Signer)?;
857        if !receipt
858            .verify_signature(&signer_identity.public_key)
859            .map_err(|error| {
860                AnchorStoreError::Signer(SignOnceError::Unavailable(error.to_string()))
861            })?
862        {
863            return Err(AnchorStoreError::Signer(SignOnceError::Unavailable(
864                "receipt signature did not verify with pinned node key".into(),
865            )));
866        }
867
868        let mut response = CrossCheckedAnchorResponseV1 {
869            protocol_version: 1,
870            request_hash: validated.request_hash,
871            action_hash,
872            exochain_receipt: receipt.clone(),
873            recording_status: "node_recorded".to_owned(),
874            consensus_finality: "not_asserted".to_owned(),
875            node_did: signer_identity.did.clone(),
876            node_key_id: signer_identity.key_id.clone(),
877            node_recorded_at: recorded_at,
878            wrapper_signature: [0; 64],
879        };
880        let wrapper_payload = response.signing_preimage().map_err(codec)?;
881        let wrapper_operation = signing_operation_id(validated.request_hash, "response")?;
882        response.wrapper_signature = ed25519_bytes(
883            signer
884                .sign_once(wrapper_operation, &wrapper_payload)
885                .map_err(AnchorStoreError::Signer)?,
886        )?;
887        let response_body = response.to_canonical_cbor().map_err(codec)?;
888        let verified_response = decode_and_validate_response(
889            &response_body,
890            ResponseValidationContext {
891                expected_request_hash: validated.request_hash,
892                expected_action_hash: action_hash,
893                expected_authority_chain_hash: authority.authority_chain_hash,
894                expected_node_did: &signer_identity.did,
895                expected_node_key_id: &signer_identity.key_id,
896                expected_node_recorded_at: recorded_at,
897                expected_node_public_key: &signer_identity.public_key,
898            },
899        )
900        .map_err(codec)?;
901        let receipt_body = encode_serde(&receipt)?;
902
903        insert_atomic_record(
904            &transaction,
905            &validated,
906            &authority,
907            recorded_at,
908            &receipt,
909            &receipt_body,
910            &response_body,
911            receipt_operation,
912            &receipt_payload,
913            wrapper_operation,
914            &wrapper_payload,
915        )?;
916        transaction.commit().map_err(storage)?;
917
918        Ok(AnchorRecord {
919            disposition: AnchorRecordDisposition::Created,
920            request_hash: validated.request_hash,
921            action_hash,
922            response: verified_response.response,
923            response_body,
924        })
925    }
926
927    /// Read and cryptographically authenticate one stored action record.
928    ///
929    /// # Errors
930    /// Returns a readback error if any persisted request, receipt, response,
931    /// signature journal, authority snapshot, or binding commitment changed.
932    pub fn readback_action(
933        &self,
934        source_code: &str,
935        action_hash: &[u8; 32],
936    ) -> Result<Option<AuthenticatedAnchorReadback>, AnchorStoreError> {
937        let connection = self.connection()?;
938        let Some(stored) = load_anchor_by_action(&connection, source_code, action_hash)? else {
939            return Ok(None);
940        };
941        validate_stored_anchor(&connection, &self.config, &stored).map(Some)
942    }
943
944    fn connection(&self) -> Result<Connection, AnchorStoreError> {
945        let connection = Connection::open(&self.path).map_err(storage)?;
946        connection
947            .busy_timeout(Duration::from_secs(30))
948            .map_err(storage)?;
949        connection
950            .execute_batch("PRAGMA foreign_keys = ON; PRAGMA synchronous = FULL;")
951            .map_err(storage)?;
952        Ok(connection)
953    }
954}
955
956/// Hash the complete ordered delegation chain through its signed link IDs.
957///
958/// # Errors
959/// Fails when a link ID or CBOR payload cannot be encoded.
960pub fn authority_chain_fingerprint(chain: &AuthorityChain) -> Result<Hash256, AnchorStoreError> {
961    if chain.links.is_empty() {
962        return Err(AnchorStoreError::AuthorityValidation(
963            "authority chain must not be empty".into(),
964        ));
965    }
966    let mut values = vec![text(AUTHORITY_CHAIN_DOMAIN)];
967    for link in &chain.links {
968        let link_id = link.id().map_err(authority)?;
969        let signature = link.signature.ed25519_bytes().ok_or_else(|| {
970            AnchorStoreError::AuthorityValidation(
971                "authority chain link signature must be Ed25519".into(),
972            )
973        })?;
974        values.push(Value::Array(vec![
975            bytes(link_id.as_bytes()),
976            bytes(signature),
977        ]));
978    }
979    Ok(Hash256::digest(&encode_value(&Value::Array(values))?))
980}
981
982/// Commitment to every immutable node policy field governing authority mutations.
983pub fn anchor_node_policy_hash(config: &AnchorStoreConfig) -> Result<Hash256, AnchorStoreError> {
984    Ok(Hash256::digest(&encode_value(&Value::Array(vec![
985        text(ANCHOR_NODE_POLICY_DOMAIN),
986        text(&config.expected_audience),
987        text(&config.crosschecked_intermediate_did),
988        text(&config.crosschecked_intermediate_key_id),
989        bytes(config.crosschecked_intermediate_public_key.as_bytes()),
990        text(&config.node_identity.did),
991        text(&config.node_identity.key_id),
992        bytes(config.node_identity.public_key.as_bytes()),
993        unsigned(config.governance_frost_key_epoch),
994        unsigned(u64::from(CROSSCHECKED_GOVERNANCE_THRESHOLD)),
995        unsigned(u64::from(CROSSCHECKED_GOVERNANCE_PARTICIPANTS)),
996        bytes(&config.governance_frost_group_public_key),
997    ]))?))
998}
999
1000#[allow(clippy::too_many_arguments)]
1001fn validate_governance_authorization(
1002    config: &AnchorStoreConfig,
1003    authorization: &AuthorityGovernanceAuthorizationV1,
1004    expected_event: AuthorityLifecycleEventV1,
1005    authority_did: &str,
1006    authority_key_id: &str,
1007    authority_key_epoch: u64,
1008    scope_alias: &[u8; 32],
1009    package_hash: Hash256,
1010) -> Result<(), AnchorStoreError> {
1011    validate_did_key_id(
1012        authorization.authority_did.as_str(),
1013        &authorization.authority_key_id,
1014    )?;
1015    let valid_signer_set = authorization.signer_ids.len()
1016        == usize::from(CROSSCHECKED_GOVERNANCE_THRESHOLD)
1017        && authorization
1018            .signer_ids
1019            .iter()
1020            .all(|identifier| (1..=CROSSCHECKED_GOVERNANCE_PARTICIPANTS).contains(identifier))
1021        && authorization
1022            .signer_ids
1023            .windows(2)
1024            .all(|pair| pair[0] < pair[1]);
1025    if authorization.protocol_version != 1
1026        || authorization.authorization_id.iter().all(|byte| *byte == 0)
1027        || authorization.ceremony_id.iter().all(|byte| *byte == 0)
1028        || authorization.governance_key_epoch != config.governance_frost_key_epoch
1029        || authorization.threshold != CROSSCHECKED_GOVERNANCE_THRESHOLD
1030        || authorization.participant_count != CROSSCHECKED_GOVERNANCE_PARTICIPANTS
1031        || !valid_signer_set
1032        || authorization.lifecycle_event != expected_event
1033        || authorization.authority_did.as_str() != authority_did
1034        || authorization.authority_key_id != authority_key_id
1035        || authorization.authority_key_epoch != authority_key_epoch
1036        || authorization.scope_alias != *scope_alias
1037        || authorization.package_hash != package_hash
1038        || authorization.node_policy_hash != anchor_node_policy_hash(config)?
1039        || authorization.authorization_sequence == 0
1040        || authorization.valid_from_ms >= authorization.valid_until_ms
1041        || authorization.signature_algorithm != GOVERNANCE_SIGNATURE_ALGORITHM
1042        || authorization.signature.len() != 64
1043    {
1044        return Err(AnchorStoreError::GovernanceAuthorization(
1045            "authorization target, policy, roster, sequence, or validity mismatch".into(),
1046        ));
1047    }
1048
1049    let verifying_key = frost::VerifyingKey::deserialize(&config.governance_frost_group_public_key)
1050        .map_err(|error| {
1051            AnchorStoreError::GovernanceAuthorization(format!(
1052                "pinned FROST group public key rejected: {error}"
1053            ))
1054        })?;
1055    let signature = frost::Signature::deserialize(&authorization.signature).map_err(|error| {
1056        AnchorStoreError::GovernanceAuthorization(format!(
1057            "FROST authorization signature encoding rejected: {error}"
1058        ))
1059    })?;
1060    verifying_key
1061        .verify(&authorization.signing_preimage()?, &signature)
1062        .map_err(|_| {
1063            AnchorStoreError::GovernanceAuthorization(
1064                "FROST authorization signature rejected".into(),
1065            )
1066        })
1067}
1068
1069fn validate_governance_application_time(
1070    authorization: &AuthorityGovernanceAuthorizationV1,
1071    applied_at_ms: u64,
1072) -> Result<(), AnchorStoreError> {
1073    if applied_at_ms < authorization.valid_from_ms || applied_at_ms >= authorization.valid_until_ms
1074    {
1075        return Err(AnchorStoreError::GovernanceAuthorization(
1076            "governance authorization is not valid at first application".into(),
1077        ));
1078    }
1079    Ok(())
1080}
1081
1082fn validate_config(config: &AnchorStoreConfig) -> Result<(), AnchorStoreError> {
1083    validate_did_key_id(&config.node_identity.did, &config.node_identity.key_id)?;
1084    validate_did_key_id(
1085        &config.crosschecked_intermediate_did,
1086        &config.crosschecked_intermediate_key_id,
1087    )?;
1088    if config.expected_audience.is_empty()
1089        || config.expected_audience.len() > 128
1090        || !config.expected_audience.bytes().all(|byte| {
1091            byte.is_ascii_lowercase()
1092                || byte.is_ascii_digit()
1093                || matches!(byte, b'.' | b'_' | b':' | b'-')
1094        })
1095    {
1096        return Err(AnchorStoreError::AuthorityValidation(
1097            "invalid configured audience".into(),
1098        ));
1099    }
1100    if config
1101        .node_identity
1102        .public_key
1103        .as_bytes()
1104        .iter()
1105        .all(|byte| *byte == 0)
1106    {
1107        return Err(AnchorStoreError::AuthorityValidation(
1108            "node public key must be non-zero".into(),
1109        ));
1110    }
1111    if config
1112        .crosschecked_intermediate_public_key
1113        .as_bytes()
1114        .iter()
1115        .all(|byte| *byte == 0)
1116    {
1117        return Err(AnchorStoreError::AuthorityValidation(
1118            "intermediate public key must be non-zero".into(),
1119        ));
1120    }
1121    if config.governance_frost_key_epoch == 0 {
1122        return Err(AnchorStoreError::AuthorityValidation(
1123            "FROST governance key epoch must be non-zero".into(),
1124        ));
1125    }
1126    frost::VerifyingKey::deserialize(&config.governance_frost_group_public_key).map_err(
1127        |error| {
1128            AnchorStoreError::AuthorityValidation(format!(
1129                "invalid FROST governance group public key: {error}"
1130            ))
1131        },
1132    )?;
1133    Ok(())
1134}
1135
1136fn validate_did_key_id(did: &str, key_id: &str) -> Result<(), AnchorStoreError> {
1137    Did::new(did)
1138        .map_err(|error| AnchorStoreError::AuthorityValidation(format!("invalid DID: {error}")))?;
1139    let prefix = format!("{did}#");
1140    let fragment = key_id.strip_prefix(&prefix).ok_or_else(|| {
1141        AnchorStoreError::AuthorityValidation("key ID is not rooted at DID".into())
1142    })?;
1143    if fragment.is_empty()
1144        || fragment.len() > 64
1145        || !fragment
1146            .bytes()
1147            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
1148    {
1149        return Err(AnchorStoreError::AuthorityValidation(
1150            "invalid key ID fragment".into(),
1151        ));
1152    }
1153    Ok(())
1154}
1155
1156fn validate_provisioning(
1157    config: &AnchorStoreConfig,
1158    provisioning: &AuthorityProvisioningV1,
1159) -> Result<ValidatedProvisioning, AnchorStoreError> {
1160    if provisioning.protocol_version != 1 || provisioning.scope_binding.protocol_version != 1 {
1161        return Err(AnchorStoreError::AuthorityValidation(
1162            "unsupported provisioning protocol version".into(),
1163        ));
1164    }
1165    let binding = &provisioning.scope_binding;
1166    validate_did_key_id(binding.authority_did.as_str(), &binding.authority_key_id)?;
1167    validate_did_key_id(
1168        binding.binding_signer_did.as_str(),
1169        &binding.binding_signer_key_id,
1170    )?;
1171    if binding.permission != Permission::AnchorReceiptCommitment {
1172        return Err(AnchorStoreError::AuthorityValidation(
1173            "scope binding lacks AnchorReceiptCommitment permission".into(),
1174        ));
1175    }
1176    if binding.audience != config.expected_audience {
1177        return Err(AnchorStoreError::AuthorityValidation(
1178            "scope binding audience mismatch".into(),
1179        ));
1180    }
1181    if binding.grant_id.iter().all(|byte| *byte == 0)
1182        || binding.scope_alias.iter().all(|byte| *byte == 0)
1183        || binding.key_epoch == 0
1184        || binding.valid_from_ms >= binding.valid_until_ms
1185    {
1186        return Err(AnchorStoreError::AuthorityValidation(
1187            "invalid scope binding identifiers, epoch, or validity".into(),
1188        ));
1189    }
1190    if binding.binding_signer_did.as_str() != config.crosschecked_intermediate_did {
1191        return Err(AnchorStoreError::AuthorityValidation(
1192            "scope binding signer is not configured CrossChecked intermediate".into(),
1193        ));
1194    }
1195    if binding.binding_signer_key_id != config.crosschecked_intermediate_key_id {
1196        return Err(AnchorStoreError::AuthorityValidation(
1197            "scope binding signer key is not the pinned intermediate key".into(),
1198        ));
1199    }
1200    if provisioning.authority_chain.leaf() != Some(&binding.authority_did) {
1201        return Err(AnchorStoreError::AuthorityValidation(
1202            "authority chain leaf does not match scope-bound authority".into(),
1203        ));
1204    }
1205    let Some(last_link) = provisioning.authority_chain.links.last() else {
1206        return Err(AnchorStoreError::AuthorityValidation(
1207            "authority chain is empty".into(),
1208        ));
1209    };
1210    if last_link.delegator_did.as_str() != config.crosschecked_intermediate_did {
1211        return Err(AnchorStoreError::AuthorityValidation(
1212            "child authority is not directly beneath configured intermediate".into(),
1213        ));
1214    }
1215    if !has_permission(
1216        &provisioning.authority_chain,
1217        &Permission::AnchorReceiptCommitment,
1218    ) {
1219        return Err(AnchorStoreError::AuthorityValidation(
1220            "verified chain lacks AnchorReceiptCommitment permission".into(),
1221        ));
1222    }
1223    let fingerprint = authority_chain_fingerprint(&provisioning.authority_chain)?;
1224    if fingerprint != binding.chain_fingerprint {
1225        return Err(AnchorStoreError::AuthorityValidation(
1226            "authority chain fingerprint mismatch".into(),
1227        ));
1228    }
1229
1230    let documents = document_map(&provisioning.did_documents)?;
1231    let authority_document = documents
1232        .get(binding.authority_did.as_str())
1233        .ok_or_else(|| {
1234            AnchorStoreError::AuthorityValidation("authority DID document missing".into())
1235        })?;
1236    let authority_method = exact_method(authority_document, &binding.authority_key_id)?;
1237    if authority_method.version != binding.key_epoch
1238        || authority_method.valid_from > binding.valid_from_ms
1239        || authority_method.revoked_at.is_some()
1240        || !authority_method.active
1241    {
1242        return Err(AnchorStoreError::AuthorityValidation(
1243            "authority key epoch or lifecycle does not cover binding".into(),
1244        ));
1245    }
1246    let authority_public_key =
1247        validate_verification_method_document_binding(authority_document, authority_method)
1248            .map_err(identity)?;
1249
1250    let mut chain_keys = BTreeMap::new();
1251    for link in &provisioning.authority_chain.links {
1252        if link.created.physical_ms > binding.valid_from_ms
1253            || link
1254                .expires
1255                .is_none_or(|expires| expires.physical_ms < binding.valid_until_ms)
1256        {
1257            return Err(AnchorStoreError::AuthorityValidation(
1258                "delegation link does not cover complete binding validity".into(),
1259            ));
1260        }
1261        let document = documents.get(link.delegator_did.as_str()).ok_or_else(|| {
1262            AnchorStoreError::AuthorityValidation(format!(
1263                "delegator DID document missing for {}",
1264                link.delegator_did
1265            ))
1266        })?;
1267        let active: Vec<&VerificationMethod> = document
1268            .verification_methods
1269            .iter()
1270            .filter(|method| {
1271                method.active
1272                    && method.revoked_at.is_none()
1273                    && method.valid_from <= binding.valid_from_ms
1274            })
1275            .collect();
1276        if active.len() != 1 {
1277            return Err(AnchorStoreError::AuthorityValidation(format!(
1278                "delegator {} must have exactly one active binding-time Ed25519 key",
1279                link.delegator_did
1280            )));
1281        }
1282        let key =
1283            validate_verification_method_document_binding(document, active[0]).map_err(identity)?;
1284        chain_keys.insert(link.delegator_did.as_str().to_owned(), key);
1285    }
1286    let verify_at = Timestamp::new(binding.valid_from_ms, 0);
1287    verify_chain(&provisioning.authority_chain, &verify_at, |did| {
1288        chain_keys.get(did.as_str()).copied()
1289    })
1290    .map_err(authority)?;
1291
1292    let signer_document = documents
1293        .get(binding.binding_signer_did.as_str())
1294        .ok_or_else(|| {
1295            AnchorStoreError::AuthorityValidation("binding signer DID document missing".into())
1296        })?;
1297    let signer_method = exact_method(signer_document, &binding.binding_signer_key_id)?;
1298    if signer_method.valid_from > binding.valid_from_ms {
1299        return Err(AnchorStoreError::AuthorityValidation(
1300            "binding signer key is not valid at binding start".into(),
1301        ));
1302    }
1303    let signer_key = validate_verification_method_document_binding(signer_document, signer_method)
1304        .map_err(identity)?;
1305    if signer_key != config.crosschecked_intermediate_public_key {
1306        return Err(AnchorStoreError::AuthorityValidation(
1307            "scope binding intermediate public key substitution".into(),
1308        ));
1309    }
1310    if !crypto::verify(
1311        &binding.signing_preimage()?,
1312        &binding.signature,
1313        &signer_key,
1314    ) {
1315        return Err(AnchorStoreError::AuthorityValidation(
1316            "scope binding signature is invalid".into(),
1317        ));
1318    }
1319    let authority_chain_hash = authority_chain_hash(&provisioning.authority_chain, binding)?;
1320    Ok(ValidatedProvisioning {
1321        authority_public_key,
1322        authority_chain_hash,
1323    })
1324}
1325
1326fn validate_retirement(
1327    config: &AnchorStoreConfig,
1328    provisioning: &AuthorityProvisioningV1,
1329    retirement: &AuthorityRetirementV1,
1330) -> Result<(), AnchorStoreError> {
1331    let binding = &provisioning.scope_binding;
1332    if retirement.protocol_version != 1
1333        || retirement.authority_did != binding.authority_did
1334        || retirement.authority_key_id != binding.authority_key_id
1335        || retirement.key_epoch != binding.key_epoch
1336        || retirement.signer_did.as_str() != config.crosschecked_intermediate_did
1337        || retirement.signer_key_id != config.crosschecked_intermediate_key_id
1338        || retirement.retired_at_ms < binding.valid_from_ms
1339    {
1340        return Err(AnchorStoreError::AuthorityValidation(
1341            "retirement target, signer, epoch, or time mismatch".into(),
1342        ));
1343    }
1344    validate_did_key_id(retirement.signer_did.as_str(), &retirement.signer_key_id)?;
1345    let documents = document_map(&provisioning.did_documents)?;
1346    let signer_document = documents
1347        .get(retirement.signer_did.as_str())
1348        .ok_or_else(|| {
1349            AnchorStoreError::AuthorityValidation("retirement signer document missing".into())
1350        })?;
1351    let signer_method = exact_method(signer_document, &retirement.signer_key_id)?;
1352    let signer_key = validate_verification_method_document_binding(signer_document, signer_method)
1353        .map_err(identity)?;
1354    if signer_key != config.crosschecked_intermediate_public_key {
1355        return Err(AnchorStoreError::AuthorityValidation(
1356            "retirement intermediate public key substitution".into(),
1357        ));
1358    }
1359    if !crypto::verify(
1360        &retirement.signing_preimage()?,
1361        &retirement.signature,
1362        &signer_key,
1363    ) {
1364        return Err(AnchorStoreError::AuthorityValidation(
1365            "retirement signature is invalid".into(),
1366        ));
1367    }
1368    Ok(())
1369}
1370
1371fn validate_stored_authority(
1372    config: &AnchorStoreConfig,
1373    authority: &StoredAuthority,
1374) -> Result<(), AnchorStoreError> {
1375    if encode_serde(&authority.provisioning)? != authority.provisioning_cbor {
1376        return Err(AnchorStoreError::ReadbackValidation(
1377            "authority provisioning bytes are not exact".into(),
1378        ));
1379    }
1380    let validated = validate_provisioning(config, &authority.provisioning)?;
1381    if validated.authority_public_key != authority.authority_public_key
1382        || validated.authority_chain_hash != authority.authority_chain_hash
1383    {
1384        return Err(AnchorStoreError::ReadbackValidation(
1385            "authority derived commitments changed".into(),
1386        ));
1387    }
1388    match (
1389        authority.retired_at_ms,
1390        authority.retirement_cbor.as_deref(),
1391    ) {
1392        (None, None) => {}
1393        (Some(retired_at_ms), Some(retirement_cbor)) => {
1394            let retirement: AuthorityRetirementV1 = decode_serde(retirement_cbor)?;
1395            if encode_serde(&retirement)? != retirement_cbor
1396                || retirement.retired_at_ms != retired_at_ms
1397            {
1398                return Err(AnchorStoreError::ReadbackValidation(
1399                    "authority retirement bytes do not match persisted state".into(),
1400                ));
1401            }
1402            validate_retirement(config, &authority.provisioning, &retirement)?;
1403        }
1404        _ => {
1405            return Err(AnchorStoreError::ReadbackValidation(
1406                "authority retirement state is incomplete".into(),
1407            ));
1408        }
1409    }
1410    Ok(())
1411}
1412
1413fn validate_stored_authority_governed(
1414    connection: &Connection,
1415    config: &AnchorStoreConfig,
1416    authority: &StoredAuthority,
1417) -> Result<(), AnchorStoreError> {
1418    validate_stored_authority(config, authority)?;
1419    let binding = &authority.provisioning.scope_binding;
1420    let provisioning_hash = Hash256::digest(&authority.provisioning_cbor);
1421    let provisioning_authorization =
1422        load_governance_authorization_by_package(connection, provisioning_hash)?.ok_or_else(
1423            || {
1424                AnchorStoreError::ReadbackValidation(
1425                    "authority provisioning lacks durable FROST authorization".into(),
1426                )
1427            },
1428        )?;
1429    validate_stored_governance_authorization(
1430        connection,
1431        config,
1432        &provisioning_authorization,
1433        AuthorityLifecycleEventV1::Provision,
1434        binding.authority_did.as_str(),
1435        &binding.authority_key_id,
1436        binding.key_epoch,
1437        &binding.scope_alias,
1438        provisioning_hash,
1439    )?;
1440
1441    if let (Some(_retired_at_ms), Some(retirement_cbor)) = (
1442        authority.retired_at_ms,
1443        authority.retirement_cbor.as_deref(),
1444    ) {
1445        let retirement_hash = Hash256::digest(retirement_cbor);
1446        let retirement_authorization =
1447            load_governance_authorization_by_package(connection, retirement_hash)?.ok_or_else(
1448                || {
1449                    AnchorStoreError::ReadbackValidation(
1450                        "authority retirement lacks durable FROST authorization".into(),
1451                    )
1452                },
1453            )?;
1454        let event = retirement_authorization.authorization.lifecycle_event;
1455        if !matches!(
1456            event,
1457            AuthorityLifecycleEventV1::Retire | AuthorityLifecycleEventV1::Revoke
1458        ) {
1459            return Err(AnchorStoreError::ReadbackValidation(
1460                "authority retirement has wrong governance lifecycle event".into(),
1461            ));
1462        }
1463        validate_stored_governance_authorization(
1464            connection,
1465            config,
1466            &retirement_authorization,
1467            event,
1468            binding.authority_did.as_str(),
1469            &binding.authority_key_id,
1470            binding.key_epoch,
1471            &binding.scope_alias,
1472            retirement_hash,
1473        )?;
1474    }
1475    Ok(())
1476}
1477
1478#[allow(clippy::too_many_arguments)]
1479fn validate_stored_governance_authorization(
1480    connection: &Connection,
1481    config: &AnchorStoreConfig,
1482    stored: &StoredGovernanceAuthorization,
1483    expected_event: AuthorityLifecycleEventV1,
1484    authority_did: &str,
1485    authority_key_id: &str,
1486    authority_key_epoch: u64,
1487    scope_alias: &[u8; 32],
1488    package_hash: Hash256,
1489) -> Result<(), AnchorStoreError> {
1490    validate_stored_governance_record(config, stored)?;
1491    let authorization = &stored.authorization;
1492    if authorization.lifecycle_event != expected_event
1493        || authorization.authority_did.as_str() != authority_did
1494        || authorization.authority_key_id != authority_key_id
1495        || authorization.authority_key_epoch != authority_key_epoch
1496        || authorization.scope_alias != *scope_alias
1497        || authorization.package_hash != package_hash
1498    {
1499        return Err(AnchorStoreError::ReadbackValidation(
1500            "governance authorization target does not match authority state".into(),
1501        ));
1502    }
1503
1504    let mut child = stored.clone();
1505    loop {
1506        if child.authorization_sequence == 1 {
1507            validate_governance_root(&child.authorization).map_err(|_| {
1508                AnchorStoreError::ReadbackValidation(
1509                    "governance authorization root is invalid".into(),
1510                )
1511            })?;
1512            break;
1513        }
1514        let predecessor = load_governance_authorization_by_scope_sequence(
1515            connection,
1516            &child.scope_alias,
1517            child.authorization_sequence - 1,
1518        )?
1519        .ok_or_else(|| {
1520            AnchorStoreError::ReadbackValidation(
1521                "governance authorization predecessor is missing".into(),
1522            )
1523        })?;
1524        validate_stored_governance_record(config, &predecessor)?;
1525        if predecessor.authorization_hash != child.authorization.prior_authorization_hash {
1526            return Err(AnchorStoreError::ReadbackValidation(
1527                "governance authorization predecessor hash mismatch".into(),
1528            ));
1529        }
1530        validate_governance_transition(
1531            &predecessor.authorization,
1532            predecessor.applied_at_ms,
1533            &child.authorization,
1534            child.applied_at_ms,
1535        )
1536        .map_err(|_| {
1537            AnchorStoreError::ReadbackValidation(
1538                "governance authorization transition is invalid".into(),
1539            )
1540        })?;
1541        child = predecessor;
1542    }
1543    Ok(())
1544}
1545
1546fn validate_stored_governance_record(
1547    config: &AnchorStoreConfig,
1548    stored: &StoredGovernanceAuthorization,
1549) -> Result<(), AnchorStoreError> {
1550    let authorization = &stored.authorization;
1551    if stored.authorization_id != authorization.authorization_id
1552        || stored.scope_alias != authorization.scope_alias
1553        || stored.authorization_sequence != authorization.authorization_sequence
1554        || stored.authorization_hash != authorization.authorization_hash()?
1555        || stored.lifecycle_event != authorization.lifecycle_event
1556        || stored.authority_did != authorization.authority_did.as_str()
1557        || stored.authority_key_id != authorization.authority_key_id
1558        || stored.authority_key_epoch != authorization.authority_key_epoch
1559        || stored.package_hash != authorization.package_hash
1560    {
1561        return Err(AnchorStoreError::ReadbackValidation(
1562            "governance authorization indexes do not match canonical artifact".into(),
1563        ));
1564    }
1565    validate_governance_authorization(
1566        config,
1567        authorization,
1568        authorization.lifecycle_event,
1569        authorization.authority_did.as_str(),
1570        &authorization.authority_key_id,
1571        authorization.authority_key_epoch,
1572        &authorization.scope_alias,
1573        authorization.package_hash,
1574    )
1575}
1576
1577fn validate_authority_time(
1578    authority: &StoredAuthority,
1579    locator: &exo_api::crosschecked_anchor::UnverifiedAnchorReplayLocatorV1,
1580    now_ms: u64,
1581) -> Result<(), AnchorStoreError> {
1582    let binding = &authority.provisioning.scope_binding;
1583    if binding.valid_from_ms > now_ms || locator.issued_at_ms < binding.valid_from_ms {
1584        return Err(AnchorStoreError::AuthorityNotYetValid);
1585    }
1586    if binding.valid_until_ms <= now_ms || locator.expires_at_ms > binding.valid_until_ms {
1587        return Err(AnchorStoreError::AuthorityExpired);
1588    }
1589    if authority
1590        .retired_at_ms
1591        .is_some_and(|retired_at| retired_at <= now_ms || retired_at <= locator.issued_at_ms)
1592    {
1593        return Err(AnchorStoreError::AuthorityRetired);
1594    }
1595    Ok(())
1596}
1597
1598fn validate_request_binding(
1599    request: &exo_api::crosschecked_anchor::CrossCheckedAnchorRequestV1,
1600    binding: &CrossCheckedScopeBindingV1,
1601) -> Result<(), AnchorStoreError> {
1602    if request.authority_did != binding.authority_did.as_str()
1603        || request.authority_key_id != binding.authority_key_id
1604        || request.grant_id != binding.grant_id
1605        || request.scope_alias != binding.scope_alias
1606        || request.audience != binding.audience
1607    {
1608        return Err(AnchorStoreError::AuthorityValidation(
1609            "request does not match persistent authority scope binding".into(),
1610        ));
1611    }
1612    Ok(())
1613}
1614
1615#[allow(clippy::too_many_arguments)]
1616fn insert_atomic_record(
1617    transaction: &Transaction<'_>,
1618    validated: &exo_api::crosschecked_anchor::ValidatedCrossCheckedAnchorRequestV1,
1619    authority: &StoredAuthority,
1620    recorded_at: Timestamp,
1621    receipt: &TrustReceipt,
1622    receipt_body: &[u8],
1623    response_body: &[u8],
1624    receipt_operation: Hash256,
1625    receipt_payload: &[u8],
1626    wrapper_operation: Hash256,
1627    wrapper_payload: &[u8],
1628) -> Result<(), AnchorStoreError> {
1629    let request = &validated.request;
1630    transaction
1631        .execute(
1632            "INSERT INTO crosschecked_anchor_requests (
1633                request_hash, authority_did, authority_key_id, source_code,
1634                action_hash, canonical_request_body, accepted_physical_ms,
1635                accepted_logical
1636             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
1637            params![
1638                validated.request_hash.as_bytes().as_slice(),
1639                request.authority_did,
1640                request.authority_key_id,
1641                request.source_code,
1642                request.action_hash.as_slice(),
1643                validated.canonical_body,
1644                to_i64(recorded_at.physical_ms, "accepted_physical_ms")?,
1645                i64::from(recorded_at.logical),
1646            ],
1647        )
1648        .map_err(storage)?;
1649    transaction
1650        .execute(
1651            "INSERT INTO crosschecked_anchor_idempotency (
1652                authority_did, idempotency_key, request_hash
1653             ) VALUES (?1, ?2, ?3)",
1654            params![
1655                request.authority_did,
1656                request.idempotency_key.as_slice(),
1657                validated.request_hash.as_bytes().as_slice(),
1658            ],
1659        )
1660        .map_err(storage)?;
1661    transaction
1662        .execute(
1663            "INSERT INTO crosschecked_anchor_receipts (
1664                receipt_hash, request_hash, authority_chain_hash,
1665                canonical_receipt_body
1666             ) VALUES (?1, ?2, ?3, ?4)",
1667            params![
1668                receipt.receipt_hash.as_bytes().as_slice(),
1669                validated.request_hash.as_bytes().as_slice(),
1670                authority.authority_chain_hash.as_bytes().as_slice(),
1671                receipt_body,
1672            ],
1673        )
1674        .map_err(storage)?;
1675    transaction
1676        .execute(
1677            "INSERT INTO crosschecked_anchor_signatures (
1678                operation_id, request_hash, purpose_code, payload_hash, signature
1679             ) VALUES (?1, ?2, 'receipt', ?3, ?4),
1680                      (?5, ?2, 'response', ?6, ?7)",
1681            params![
1682                receipt_operation.as_bytes().as_slice(),
1683                validated.request_hash.as_bytes().as_slice(),
1684                Hash256::digest(receipt_payload).as_bytes().as_slice(),
1685                ed25519_bytes(receipt.signature.clone())?.as_slice(),
1686                wrapper_operation.as_bytes().as_slice(),
1687                Hash256::digest(wrapper_payload).as_bytes().as_slice(),
1688                response_wrapper_signature(response_body)?.as_slice(),
1689            ],
1690        )
1691        .map_err(storage)?;
1692    transaction
1693        .execute(
1694            "INSERT INTO crosschecked_anchor_responses (
1695                request_hash, receipt_hash, canonical_response_body,
1696                node_did, node_key_id, node_recorded_physical_ms,
1697                node_recorded_logical
1698             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
1699            params![
1700                validated.request_hash.as_bytes().as_slice(),
1701                receipt.receipt_hash.as_bytes().as_slice(),
1702                response_body,
1703                receipt.actor_did.as_str(),
1704                // The response was self-verified before this insertion. The
1705                // immutable store policy contains the exact node key ID.
1706                authority_node_key_id(transaction)?,
1707                to_i64(recorded_at.physical_ms, "node_recorded_physical_ms")?,
1708                i64::from(recorded_at.logical),
1709            ],
1710        )
1711        .map_err(storage)?;
1712    Ok(())
1713}
1714
1715fn authority_node_key_id(transaction: &Transaction<'_>) -> Result<String, AnchorStoreError> {
1716    transaction
1717        .query_row(
1718            "SELECT node_key_id FROM crosschecked_anchor_store_config WHERE singleton = 1",
1719            [],
1720            |row| row.get(0),
1721        )
1722        .map_err(storage)
1723}
1724
1725fn validate_stored_anchor(
1726    connection: &Connection,
1727    config: &AnchorStoreConfig,
1728    stored: &StoredAnchorRow,
1729) -> Result<AuthenticatedAnchorReadback, AnchorStoreError> {
1730    let authority = load_authority(connection, &stored.authority_did, &stored.authority_key_id)?
1731        .ok_or_else(|| {
1732            AnchorStoreError::ReadbackValidation("stored authority is missing".into())
1733        })?;
1734    validate_stored_authority_governed(connection, config, &authority)?;
1735    if stored.authority_chain_hash != authority.authority_chain_hash
1736        || stored.authority_public_key != authority.authority_public_key
1737    {
1738        return Err(AnchorStoreError::ReadbackValidation(
1739            "stored authority commitments do not match receipt row".into(),
1740        ));
1741    }
1742    let locator = decode_unverified_replay_locator(
1743        &stored.request_body,
1744        "POST",
1745        ANCHOR_PATH,
1746        "application/cbor",
1747    )
1748    .map_err(readback_codec)?;
1749    if locator.request_hash != stored.request_hash
1750        || locator.source_code != stored.source_code
1751        || locator.action_hash != stored.action_hash
1752        || locator.idempotency_key != stored.idempotency_key
1753    {
1754        return Err(AnchorStoreError::ReadbackValidation(
1755            "stored request indexes do not match canonical request".into(),
1756        ));
1757    }
1758    let validated_request = decode_and_validate_request(
1759        &stored.request_body,
1760        RequestValidationContext {
1761            method: "POST",
1762            path: ANCHOR_PATH,
1763            content_type: "application/cbor",
1764            expected_audience: &config.expected_audience,
1765            now_ms: locator.issued_at_ms,
1766            authority_public_key: &stored.authority_public_key,
1767        },
1768    )
1769    .map_err(readback_codec)?;
1770    validate_request_binding(
1771        &validated_request.request,
1772        &authority.provisioning.scope_binding,
1773    )?;
1774    if authority
1775        .retired_at_ms
1776        .is_some_and(|retired_at| retired_at <= locator.issued_at_ms)
1777    {
1778        return Err(AnchorStoreError::ReadbackValidation(
1779            "authority was retired before stored request issuance".into(),
1780        ));
1781    }
1782    let response = decode_and_validate_response(
1783        &stored.response_body,
1784        ResponseValidationContext {
1785            expected_request_hash: stored.request_hash,
1786            expected_action_hash: Hash256::from_bytes(stored.action_hash),
1787            expected_authority_chain_hash: stored.authority_chain_hash,
1788            expected_node_did: &config.node_identity.did,
1789            expected_node_key_id: &config.node_identity.key_id,
1790            expected_node_recorded_at: stored.node_recorded_at,
1791            expected_node_public_key: &config.node_identity.public_key,
1792        },
1793    )
1794    .map_err(readback_codec)?;
1795    let stored_receipt: TrustReceipt = decode_serde(&stored.receipt_body)?;
1796    if stored_receipt != response.response.exochain_receipt
1797        || encode_serde(&stored_receipt)? != stored.receipt_body
1798    {
1799        return Err(AnchorStoreError::ReadbackValidation(
1800            "stored TrustReceipt is not the exact signed response receipt".into(),
1801        ));
1802    }
1803    validate_signature_journal(connection, stored.request_hash, &response.response)?;
1804    Ok(AuthenticatedAnchorReadback {
1805        request_body: stored.request_body.clone(),
1806        response: response.response,
1807        response_body: response.canonical_body,
1808    })
1809}
1810
1811fn validate_signature_journal(
1812    connection: &Connection,
1813    request_hash: Hash256,
1814    response: &CrossCheckedAnchorResponseV1,
1815) -> Result<(), AnchorStoreError> {
1816    let receipt_payload = response
1817        .exochain_receipt
1818        .signing_payload()
1819        .map_err(|error| AnchorStoreError::ReadbackValidation(error.to_string()))?;
1820    let wrapper_payload = response.signing_preimage().map_err(readback_codec)?;
1821    for (purpose, operation, payload, signature) in [
1822        (
1823            "receipt",
1824            signing_operation_id(request_hash, "receipt")?,
1825            receipt_payload,
1826            ed25519_bytes(response.exochain_receipt.signature.clone())?,
1827        ),
1828        (
1829            "response",
1830            signing_operation_id(request_hash, "response")?,
1831            wrapper_payload,
1832            response.wrapper_signature,
1833        ),
1834    ] {
1835        let journal: Option<(Vec<u8>, Vec<u8>, Vec<u8>)> = connection
1836            .query_row(
1837                "SELECT operation_id, payload_hash, signature
1838                 FROM crosschecked_anchor_signatures
1839                 WHERE request_hash = ?1 AND purpose_code = ?2",
1840                params![request_hash.as_bytes().as_slice(), purpose],
1841                |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1842            )
1843            .optional()
1844            .map_err(storage)?;
1845        let Some((stored_operation, stored_payload_hash, stored_signature)) = journal else {
1846            return Err(AnchorStoreError::ReadbackValidation(format!(
1847                "missing {purpose} signing journal"
1848            )));
1849        };
1850        if stored_operation != operation.as_bytes()
1851            || stored_payload_hash != Hash256::digest(&payload).as_bytes()
1852            || stored_signature != signature
1853        {
1854            return Err(AnchorStoreError::ReadbackValidation(format!(
1855                "{purpose} signing journal mismatch"
1856            )));
1857        }
1858    }
1859    Ok(())
1860}
1861
1862fn initialize_schema(
1863    connection: &Connection,
1864    config: &AnchorStoreConfig,
1865) -> Result<(), AnchorStoreError> {
1866    let config_table_exists: bool = connection
1867        .query_row(
1868            "SELECT EXISTS(
1869                SELECT 1 FROM sqlite_master
1870                WHERE type = 'table' AND name = 'crosschecked_anchor_store_config'
1871             )",
1872            [],
1873            |row| row.get(0),
1874        )
1875        .map_err(storage)?;
1876    if config_table_exists {
1877        let existing_version: String = connection
1878            .query_row(
1879                "SELECT schema_version FROM crosschecked_anchor_store_config WHERE singleton = 1",
1880                [],
1881                |row| row.get(0),
1882            )
1883            .map_err(storage)?;
1884        if existing_version != STORE_SCHEMA_VERSION {
1885            return Err(AnchorStoreError::Storage(
1886                "legacy or incompatible authority registry must be rebuilt under FROST governance"
1887                    .into(),
1888            ));
1889        }
1890    }
1891    connection
1892        .execute_batch(
1893            "PRAGMA journal_mode = WAL;
1894             PRAGMA foreign_keys = ON;
1895             PRAGMA synchronous = FULL;
1896             CREATE TABLE IF NOT EXISTS crosschecked_anchor_store_config (
1897                 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
1898                 schema_version TEXT NOT NULL,
1899                 expected_audience TEXT NOT NULL,
1900                 intermediate_did TEXT NOT NULL,
1901                 intermediate_key_id TEXT NOT NULL,
1902                 intermediate_public_key BLOB NOT NULL CHECK (length(intermediate_public_key) = 32),
1903                 governance_frost_group_public_key BLOB NOT NULL CHECK (length(governance_frost_group_public_key) = 32),
1904                 governance_frost_key_epoch INTEGER NOT NULL CHECK (governance_frost_key_epoch > 0),
1905                 governance_threshold INTEGER NOT NULL CHECK (governance_threshold = 7),
1906                 governance_participant_count INTEGER NOT NULL CHECK (governance_participant_count = 13),
1907                 node_did TEXT NOT NULL,
1908                 node_key_id TEXT NOT NULL,
1909                 node_public_key BLOB NOT NULL CHECK (length(node_public_key) = 32)
1910             );
1911             CREATE TABLE IF NOT EXISTS crosschecked_anchor_authorities (
1912                 authority_did TEXT NOT NULL,
1913                 authority_key_id TEXT NOT NULL,
1914                 grant_id BLOB NOT NULL UNIQUE CHECK (length(grant_id) = 32),
1915                 scope_alias BLOB NOT NULL CHECK (length(scope_alias) = 32),
1916                 audience TEXT NOT NULL,
1917                 permission_code TEXT NOT NULL CHECK (permission_code = 'anchor_receipt_commitment'),
1918                 key_epoch INTEGER NOT NULL CHECK (key_epoch > 0),
1919                 valid_from_ms INTEGER NOT NULL CHECK (valid_from_ms >= 0),
1920                 valid_until_ms INTEGER NOT NULL CHECK (valid_until_ms > valid_from_ms),
1921                 retired_at_ms INTEGER,
1922                 authority_public_key BLOB NOT NULL CHECK (length(authority_public_key) = 32),
1923                 authority_chain_hash BLOB NOT NULL CHECK (length(authority_chain_hash) = 32),
1924                 provisioning_cbor BLOB NOT NULL,
1925                 retirement_cbor BLOB,
1926                 PRIMARY KEY (authority_did, authority_key_id),
1927                 UNIQUE (authority_did, key_epoch)
1928             );
1929             CREATE TABLE IF NOT EXISTS crosschecked_anchor_governance_authorizations (
1930                 authorization_id BLOB PRIMARY KEY CHECK (length(authorization_id) = 32),
1931                 ceremony_id BLOB NOT NULL UNIQUE CHECK (length(ceremony_id) = 32),
1932                 scope_alias BLOB NOT NULL CHECK (length(scope_alias) = 32),
1933                 authorization_sequence INTEGER NOT NULL CHECK (authorization_sequence > 0),
1934                 prior_authorization_hash BLOB NOT NULL CHECK (length(prior_authorization_hash) = 32),
1935                 authorization_hash BLOB NOT NULL UNIQUE CHECK (length(authorization_hash) = 32),
1936                 governance_key_epoch INTEGER NOT NULL CHECK (governance_key_epoch > 0),
1937                 lifecycle_event TEXT NOT NULL CHECK (lifecycle_event IN ('provision', 'retire', 'revoke')),
1938                 authority_did TEXT NOT NULL,
1939                 authority_key_id TEXT NOT NULL,
1940                 authority_key_epoch INTEGER NOT NULL CHECK (authority_key_epoch > 0),
1941                 package_hash BLOB NOT NULL UNIQUE CHECK (length(package_hash) = 32),
1942                 node_policy_hash BLOB NOT NULL CHECK (length(node_policy_hash) = 32),
1943                 valid_from_ms INTEGER NOT NULL CHECK (valid_from_ms >= 0),
1944                 valid_until_ms INTEGER NOT NULL CHECK (valid_until_ms > valid_from_ms),
1945                 applied_at_ms INTEGER NOT NULL CHECK (applied_at_ms >= valid_from_ms AND applied_at_ms < valid_until_ms),
1946                 authorization_cbor BLOB NOT NULL,
1947                 authorization_record_cbor BLOB NOT NULL,
1948                 UNIQUE (scope_alias, authorization_sequence)
1949             );
1950             CREATE TABLE IF NOT EXISTS crosschecked_anchor_requests (
1951                 request_hash BLOB PRIMARY KEY CHECK (length(request_hash) = 32),
1952                 authority_did TEXT NOT NULL,
1953                 authority_key_id TEXT NOT NULL,
1954                 source_code TEXT NOT NULL CHECK (source_code = 'crosschecked'),
1955                 action_hash BLOB NOT NULL CHECK (length(action_hash) = 32),
1956                 canonical_request_body BLOB NOT NULL,
1957                 accepted_physical_ms INTEGER NOT NULL CHECK (accepted_physical_ms >= 0),
1958                 accepted_logical INTEGER NOT NULL CHECK (accepted_logical >= 0),
1959                 UNIQUE (source_code, action_hash),
1960                 FOREIGN KEY (authority_did, authority_key_id)
1961                     REFERENCES crosschecked_anchor_authorities(authority_did, authority_key_id)
1962             );
1963             CREATE TABLE IF NOT EXISTS crosschecked_anchor_idempotency (
1964                 authority_did TEXT NOT NULL,
1965                 idempotency_key BLOB NOT NULL CHECK (length(idempotency_key) = 32),
1966                 request_hash BLOB NOT NULL UNIQUE,
1967                 PRIMARY KEY (authority_did, idempotency_key),
1968                 FOREIGN KEY (request_hash) REFERENCES crosschecked_anchor_requests(request_hash)
1969             );
1970             CREATE TABLE IF NOT EXISTS crosschecked_anchor_receipts (
1971                 receipt_hash BLOB PRIMARY KEY CHECK (length(receipt_hash) = 32),
1972                 request_hash BLOB NOT NULL UNIQUE,
1973                 authority_chain_hash BLOB NOT NULL CHECK (length(authority_chain_hash) = 32),
1974                 canonical_receipt_body BLOB NOT NULL,
1975                 FOREIGN KEY (request_hash) REFERENCES crosschecked_anchor_requests(request_hash)
1976             );
1977             CREATE TABLE IF NOT EXISTS crosschecked_anchor_signatures (
1978                 operation_id BLOB PRIMARY KEY CHECK (length(operation_id) = 32),
1979                 request_hash BLOB NOT NULL,
1980                 purpose_code TEXT NOT NULL CHECK (purpose_code IN ('receipt', 'response')),
1981                 payload_hash BLOB NOT NULL CHECK (length(payload_hash) = 32),
1982                 signature BLOB NOT NULL CHECK (length(signature) = 64),
1983                 UNIQUE (request_hash, purpose_code),
1984                 FOREIGN KEY (request_hash) REFERENCES crosschecked_anchor_requests(request_hash)
1985             );
1986             CREATE TABLE IF NOT EXISTS crosschecked_anchor_responses (
1987                 request_hash BLOB PRIMARY KEY,
1988                 receipt_hash BLOB NOT NULL UNIQUE,
1989                 canonical_response_body BLOB NOT NULL,
1990                 node_did TEXT NOT NULL,
1991                 node_key_id TEXT NOT NULL,
1992                 node_recorded_physical_ms INTEGER NOT NULL CHECK (node_recorded_physical_ms >= 0),
1993                 node_recorded_logical INTEGER NOT NULL CHECK (node_recorded_logical >= 0),
1994                 FOREIGN KEY (request_hash) REFERENCES crosschecked_anchor_requests(request_hash),
1995                 FOREIGN KEY (receipt_hash) REFERENCES crosschecked_anchor_receipts(receipt_hash)
1996             );",
1997        )
1998        .map_err(storage)?;
1999    connection
2000        .execute(
2001            "INSERT OR IGNORE INTO crosschecked_anchor_store_config (
2002                singleton, schema_version, expected_audience, intermediate_did,
2003                intermediate_key_id, intermediate_public_key, node_did,
2004                governance_frost_group_public_key, governance_frost_key_epoch,
2005                governance_threshold, governance_participant_count,
2006                node_key_id, node_public_key
2007             ) VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
2008            params![
2009                STORE_SCHEMA_VERSION,
2010                config.expected_audience,
2011                config.crosschecked_intermediate_did,
2012                config.crosschecked_intermediate_key_id,
2013                config
2014                    .crosschecked_intermediate_public_key
2015                    .as_bytes()
2016                    .as_slice(),
2017                config.node_identity.did,
2018                config.governance_frost_group_public_key.as_slice(),
2019                to_i64(
2020                    config.governance_frost_key_epoch,
2021                    "governance_frost_key_epoch"
2022                )?,
2023                i64::from(CROSSCHECKED_GOVERNANCE_THRESHOLD),
2024                i64::from(CROSSCHECKED_GOVERNANCE_PARTICIPANTS),
2025                config.node_identity.key_id,
2026                config.node_identity.public_key.as_bytes().as_slice(),
2027            ],
2028        )
2029        .map_err(storage)?;
2030    let persisted: PersistedStoreConfig = connection
2031        .query_row(
2032            "SELECT schema_version, expected_audience, intermediate_did,
2033                    intermediate_key_id, intermediate_public_key, node_did,
2034                    governance_frost_group_public_key, governance_frost_key_epoch,
2035                    governance_threshold, governance_participant_count,
2036                    node_key_id, node_public_key
2037             FROM crosschecked_anchor_store_config WHERE singleton = 1",
2038            [],
2039            |row| {
2040                Ok(PersistedStoreConfig {
2041                    schema_version: row.get(0)?,
2042                    expected_audience: row.get(1)?,
2043                    intermediate_did: row.get(2)?,
2044                    intermediate_key_id: row.get(3)?,
2045                    intermediate_public_key: row.get(4)?,
2046                    node_did: row.get(5)?,
2047                    governance_frost_group_public_key: row.get(6)?,
2048                    governance_frost_key_epoch: row.get(7)?,
2049                    governance_threshold: row.get(8)?,
2050                    governance_participant_count: row.get(9)?,
2051                    node_key_id: row.get(10)?,
2052                    node_public_key: row.get(11)?,
2053                })
2054            },
2055        )
2056        .map_err(storage)?;
2057    if persisted.schema_version != STORE_SCHEMA_VERSION
2058        || persisted.expected_audience != config.expected_audience
2059        || persisted.intermediate_did != config.crosschecked_intermediate_did
2060        || persisted.intermediate_key_id != config.crosschecked_intermediate_key_id
2061        || persisted.intermediate_public_key
2062            != config.crosschecked_intermediate_public_key.as_bytes()
2063        || persisted.node_did != config.node_identity.did
2064        || persisted.governance_frost_group_public_key != config.governance_frost_group_public_key
2065        || persisted.governance_frost_key_epoch
2066            != to_i64(
2067                config.governance_frost_key_epoch,
2068                "governance_frost_key_epoch",
2069            )?
2070        || persisted.governance_threshold != i64::from(CROSSCHECKED_GOVERNANCE_THRESHOLD)
2071        || persisted.governance_participant_count != i64::from(CROSSCHECKED_GOVERNANCE_PARTICIPANTS)
2072        || persisted.node_key_id != config.node_identity.key_id
2073        || persisted.node_public_key != config.node_identity.public_key.as_bytes()
2074    {
2075        return Err(AnchorStoreError::Storage(
2076            "persisted store policy differs from requested policy".into(),
2077        ));
2078    }
2079    Ok(())
2080}
2081
2082fn lifecycle_event_from_code(code: &str) -> Result<AuthorityLifecycleEventV1, AnchorStoreError> {
2083    match code {
2084        "provision" => Ok(AuthorityLifecycleEventV1::Provision),
2085        "retire" => Ok(AuthorityLifecycleEventV1::Retire),
2086        "revoke" => Ok(AuthorityLifecycleEventV1::Revoke),
2087        _ => Err(AnchorStoreError::ReadbackValidation(
2088            "unknown governance lifecycle event".into(),
2089        )),
2090    }
2091}
2092
2093fn load_governance_authorization_by_id(
2094    connection: &Connection,
2095    authorization_id: &[u8; 32],
2096) -> Result<Option<StoredGovernanceAuthorization>, AnchorStoreError> {
2097    query_governance_authorization(
2098        connection,
2099        "WHERE authorization_id = ?1",
2100        params![authorization_id.as_slice()],
2101    )
2102}
2103
2104fn load_governance_authorization_by_package(
2105    connection: &Connection,
2106    package_hash: Hash256,
2107) -> Result<Option<StoredGovernanceAuthorization>, AnchorStoreError> {
2108    query_governance_authorization(
2109        connection,
2110        "WHERE package_hash = ?1",
2111        params![package_hash.as_bytes().as_slice()],
2112    )
2113}
2114
2115fn load_governance_authorization_by_scope_sequence(
2116    connection: &Connection,
2117    scope_alias: &[u8; 32],
2118    sequence: u64,
2119) -> Result<Option<StoredGovernanceAuthorization>, AnchorStoreError> {
2120    query_governance_authorization(
2121        connection,
2122        "WHERE scope_alias = ?1 AND authorization_sequence = ?2",
2123        params![
2124            scope_alias.as_slice(),
2125            to_i64(sequence, "authorization_sequence")?,
2126        ],
2127    )
2128}
2129
2130fn load_latest_governance_authorization(
2131    connection: &Connection,
2132    scope_alias: &[u8; 32],
2133) -> Result<Option<StoredGovernanceAuthorization>, AnchorStoreError> {
2134    query_governance_authorization(
2135        connection,
2136        "WHERE scope_alias = ?1 ORDER BY authorization_sequence DESC LIMIT 1",
2137        params![scope_alias.as_slice()],
2138    )
2139}
2140
2141fn query_governance_authorization<P: rusqlite::Params>(
2142    connection: &Connection,
2143    predicate: &str,
2144    query_params: P,
2145) -> Result<Option<StoredGovernanceAuthorization>, AnchorStoreError> {
2146    let sql = format!(
2147        "SELECT authorization_id, scope_alias, authorization_sequence,
2148                authorization_hash, lifecycle_event, authority_did,
2149                authority_key_id, authority_key_epoch, package_hash,
2150                applied_at_ms, authorization_cbor, authorization_record_cbor
2151         FROM crosschecked_anchor_governance_authorizations {predicate}"
2152    );
2153    connection
2154        .query_row(&sql, query_params, |row| {
2155            Ok((
2156                row.get::<_, Vec<u8>>(0)?,
2157                row.get::<_, Vec<u8>>(1)?,
2158                row.get::<_, i64>(2)?,
2159                row.get::<_, Vec<u8>>(3)?,
2160                row.get::<_, String>(4)?,
2161                row.get::<_, String>(5)?,
2162                row.get::<_, String>(6)?,
2163                row.get::<_, i64>(7)?,
2164                row.get::<_, Vec<u8>>(8)?,
2165                row.get::<_, i64>(9)?,
2166                row.get::<_, Vec<u8>>(10)?,
2167                row.get::<_, Vec<u8>>(11)?,
2168            ))
2169        })
2170        .optional()
2171        .map_err(storage)?
2172        .map(
2173            |(
2174                authorization_id,
2175                scope_alias,
2176                sequence,
2177                authorization_hash,
2178                lifecycle_event,
2179                authority_did,
2180                authority_key_id,
2181                authority_key_epoch,
2182                package_hash,
2183                applied_at_ms,
2184                authorization_cbor,
2185                authorization_record_cbor,
2186            )| {
2187                let authorization: AuthorityGovernanceAuthorizationV1 =
2188                    decode_serde(&authorization_record_cbor)?;
2189                let applied_at_ms =
2190                    from_i64(applied_at_ms, "governance authorization application time")?;
2191                if encode_serde(&authorization)? != authorization_record_cbor
2192                    || authorization.to_cbor_bytes()? != authorization_cbor
2193                {
2194                    return Err(AnchorStoreError::ReadbackValidation(
2195                        "governance authorization bytes are not exact".into(),
2196                    ));
2197                }
2198                validate_governance_application_time(&authorization, applied_at_ms).map_err(
2199                    |_| {
2200                        AnchorStoreError::ReadbackValidation(
2201                            "governance authorization application time is invalid".into(),
2202                        )
2203                    },
2204                )?;
2205                Ok(StoredGovernanceAuthorization {
2206                    authorization,
2207                    authorization_id: exact_bytes(authorization_id, "authorization id")?,
2208                    scope_alias: exact_bytes(scope_alias, "governance scope alias")?,
2209                    authorization_sequence: from_i64(sequence, "authorization sequence")?,
2210                    authorization_hash: Hash256::from_bytes(exact_bytes(
2211                        authorization_hash,
2212                        "authorization hash",
2213                    )?),
2214                    lifecycle_event: lifecycle_event_from_code(&lifecycle_event)?,
2215                    authority_did,
2216                    authority_key_id,
2217                    authority_key_epoch: from_i64(
2218                        authority_key_epoch,
2219                        "governance authority key epoch",
2220                    )?,
2221                    package_hash: Hash256::from_bytes(exact_bytes(
2222                        package_hash,
2223                        "governance package hash",
2224                    )?),
2225                    applied_at_ms,
2226                    authorization_cbor,
2227                })
2228            },
2229        )
2230        .transpose()
2231}
2232
2233fn validate_exact_governance_replay(
2234    stored: &StoredGovernanceAuthorization,
2235    authorization: &AuthorityGovernanceAuthorizationV1,
2236    authorization_cbor: &[u8],
2237    authorization_hash: Hash256,
2238    package_hash: Hash256,
2239) -> Result<(), AnchorStoreError> {
2240    if stored.authorization_id != authorization.authorization_id
2241        || stored.scope_alias != authorization.scope_alias
2242        || stored.authorization_sequence != authorization.authorization_sequence
2243        || stored.authorization_hash != authorization_hash
2244        || stored.lifecycle_event != authorization.lifecycle_event
2245        || stored.authority_did != authorization.authority_did.as_str()
2246        || stored.authority_key_id != authorization.authority_key_id
2247        || stored.authority_key_epoch != authorization.authority_key_epoch
2248        || stored.package_hash != package_hash
2249        || stored.authorization_cbor != authorization_cbor
2250    {
2251        return Err(AnchorStoreError::GovernanceAuthorizationConflict);
2252    }
2253    Ok(())
2254}
2255
2256fn validate_governance_sequence(
2257    transaction: &Transaction<'_>,
2258    authorization: &AuthorityGovernanceAuthorizationV1,
2259    applied_at_ms: u64,
2260) -> Result<(), AnchorStoreError> {
2261    let latest = load_latest_governance_authorization(transaction, &authorization.scope_alias)?;
2262    let Some(latest) = latest else {
2263        return validate_governance_root(authorization);
2264    };
2265
2266    validate_governance_transition(
2267        &latest.authorization,
2268        latest.applied_at_ms,
2269        authorization,
2270        applied_at_ms,
2271    )
2272}
2273
2274fn validate_governance_root(
2275    authorization: &AuthorityGovernanceAuthorizationV1,
2276) -> Result<(), AnchorStoreError> {
2277    if authorization.authorization_sequence != 1
2278        || authorization.prior_authorization_hash != Hash256::from_bytes([0; 32])
2279        || authorization.lifecycle_event != AuthorityLifecycleEventV1::Provision
2280    {
2281        return Err(AnchorStoreError::GovernanceAuthorization(
2282            "first scope authorization must be sequence-one provisioning".into(),
2283        ));
2284    }
2285    Ok(())
2286}
2287
2288fn validate_governance_transition(
2289    latest: &AuthorityGovernanceAuthorizationV1,
2290    latest_applied_at_ms: u64,
2291    authorization: &AuthorityGovernanceAuthorizationV1,
2292    applied_at_ms: u64,
2293) -> Result<(), AnchorStoreError> {
2294    if applied_at_ms < latest_applied_at_ms {
2295        return Err(AnchorStoreError::GovernanceAuthorization(
2296            "governance application time regressed below the persistent lifecycle watermark".into(),
2297        ));
2298    }
2299    if authorization.authorization_sequence != latest.authorization_sequence.saturating_add(1)
2300        || authorization.prior_authorization_hash != latest.authorization_hash()?
2301        || authorization.authority_did != latest.authority_did
2302        || authorization.scope_alias != latest.scope_alias
2303    {
2304        return Err(AnchorStoreError::GovernanceAuthorization(
2305            "authorization sequence or prior authorization hash mismatch".into(),
2306        ));
2307    }
2308    match authorization.lifecycle_event {
2309        AuthorityLifecycleEventV1::Provision => {
2310            if latest.lifecycle_event == AuthorityLifecycleEventV1::Provision
2311                || authorization.authority_key_epoch <= latest.authority_key_epoch
2312            {
2313                return Err(AnchorStoreError::GovernanceAuthorization(
2314                    "authority rotation requires a prior retirement and increasing key epoch"
2315                        .into(),
2316                ));
2317            }
2318        }
2319        AuthorityLifecycleEventV1::Retire | AuthorityLifecycleEventV1::Revoke => {
2320            if latest.lifecycle_event != AuthorityLifecycleEventV1::Provision
2321                || authorization.authority_key_id != latest.authority_key_id
2322                || authorization.authority_key_epoch != latest.authority_key_epoch
2323            {
2324                return Err(AnchorStoreError::GovernanceAuthorization(
2325                    "retirement or revocation must target the current provisioned epoch".into(),
2326                ));
2327            }
2328        }
2329    }
2330    Ok(())
2331}
2332
2333fn persist_governance_authorization(
2334    transaction: &Transaction<'_>,
2335    authorization: &AuthorityGovernanceAuthorizationV1,
2336    authorization_cbor: &[u8],
2337    authorization_hash: Hash256,
2338    applied_at_ms: u64,
2339) -> Result<(), AnchorStoreError> {
2340    transaction
2341        .execute(
2342            "INSERT INTO crosschecked_anchor_governance_authorizations (
2343                authorization_id, ceremony_id, scope_alias, authorization_sequence,
2344                prior_authorization_hash, authorization_hash, governance_key_epoch,
2345                lifecycle_event, authority_did, authority_key_id, authority_key_epoch,
2346                package_hash, node_policy_hash, valid_from_ms, valid_until_ms,
2347                applied_at_ms, authorization_cbor, authorization_record_cbor
2348             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)",
2349            params![
2350                authorization.authorization_id.as_slice(),
2351                authorization.ceremony_id.as_slice(),
2352                authorization.scope_alias.as_slice(),
2353                to_i64(
2354                    authorization.authorization_sequence,
2355                    "authorization_sequence",
2356                )?,
2357                authorization.prior_authorization_hash.as_bytes().as_slice(),
2358                authorization_hash.as_bytes().as_slice(),
2359                to_i64(
2360                    authorization.governance_key_epoch,
2361                    "governance_key_epoch",
2362                )?,
2363                authorization.lifecycle_event.code(),
2364                authorization.authority_did.as_str(),
2365                authorization.authority_key_id,
2366                to_i64(authorization.authority_key_epoch, "authority_key_epoch")?,
2367                authorization.package_hash.as_bytes().as_slice(),
2368                authorization.node_policy_hash.as_bytes().as_slice(),
2369                to_i64(authorization.valid_from_ms, "authorization_valid_from_ms")?,
2370                to_i64(authorization.valid_until_ms, "authorization_valid_until_ms")?,
2371                to_i64(applied_at_ms, "authorization_applied_at_ms")?,
2372                authorization_cbor,
2373                encode_serde(authorization)?,
2374            ],
2375        )
2376        .map_err(|_| AnchorStoreError::GovernanceAuthorizationConflict)?;
2377    Ok(())
2378}
2379
2380fn load_authority(
2381    connection: &Connection,
2382    authority_did: &str,
2383    authority_key_id: &str,
2384) -> Result<Option<StoredAuthority>, AnchorStoreError> {
2385    connection
2386        .query_row(
2387            "SELECT provisioning_cbor, authority_public_key,
2388                    authority_chain_hash, retired_at_ms, retirement_cbor
2389             FROM crosschecked_anchor_authorities
2390             WHERE authority_did = ?1 AND authority_key_id = ?2",
2391            params![authority_did, authority_key_id],
2392            |row| {
2393                let provisioning_cbor: Vec<u8> = row.get(0)?;
2394                let authority_public_key: Vec<u8> = row.get(1)?;
2395                let authority_chain_hash: Vec<u8> = row.get(2)?;
2396                let retired_at_ms: Option<i64> = row.get(3)?;
2397                let retirement_cbor: Option<Vec<u8>> = row.get(4)?;
2398                Ok((
2399                    provisioning_cbor,
2400                    authority_public_key,
2401                    authority_chain_hash,
2402                    retired_at_ms,
2403                    retirement_cbor,
2404                ))
2405            },
2406        )
2407        .optional()
2408        .map_err(storage)?
2409        .map(
2410            |(provisioning_cbor, public_key, chain_hash, retired_at, retirement_cbor)| {
2411                Ok(StoredAuthority {
2412                    provisioning: decode_serde(&provisioning_cbor)?,
2413                    provisioning_cbor,
2414                    authority_public_key: PublicKey::from_bytes(exact_bytes(
2415                        public_key,
2416                        "authority public key",
2417                    )?),
2418                    authority_chain_hash: Hash256::from_bytes(exact_bytes(
2419                        chain_hash,
2420                        "authority chain hash",
2421                    )?),
2422                    retired_at_ms: retired_at
2423                        .map(|value| from_i64(value, "retired_at_ms"))
2424                        .transpose()?,
2425                    retirement_cbor,
2426                })
2427            },
2428        )
2429        .transpose()
2430}
2431
2432fn load_anchor_by_idempotency(
2433    connection: &Connection,
2434    authority_did: &str,
2435    idempotency_key: &[u8; 32],
2436) -> Result<Option<StoredAnchorRow>, AnchorStoreError> {
2437    query_anchor(
2438        connection,
2439        "WHERE i.authority_did = ?1 AND i.idempotency_key = ?2",
2440        params![authority_did, idempotency_key.as_slice()],
2441    )
2442}
2443
2444fn load_anchor_by_action(
2445    connection: &Connection,
2446    source_code: &str,
2447    action_hash: &[u8; 32],
2448) -> Result<Option<StoredAnchorRow>, AnchorStoreError> {
2449    query_anchor(
2450        connection,
2451        "WHERE q.source_code = ?1 AND q.action_hash = ?2",
2452        params![source_code, action_hash.as_slice()],
2453    )
2454}
2455
2456fn query_anchor<P: rusqlite::Params>(
2457    connection: &Connection,
2458    predicate: &str,
2459    query_params: P,
2460) -> Result<Option<StoredAnchorRow>, AnchorStoreError> {
2461    let sql = format!(
2462        "SELECT q.authority_did, q.authority_key_id, i.idempotency_key,
2463                q.source_code, q.action_hash, q.request_hash,
2464                q.canonical_request_body, c.canonical_receipt_body,
2465                s.canonical_response_body, c.authority_chain_hash,
2466                a.authority_public_key, s.node_recorded_physical_ms,
2467                s.node_recorded_logical
2468         FROM crosschecked_anchor_requests q
2469         JOIN crosschecked_anchor_idempotency i ON i.request_hash = q.request_hash
2470         JOIN crosschecked_anchor_receipts c ON c.request_hash = q.request_hash
2471         JOIN crosschecked_anchor_responses s ON s.request_hash = q.request_hash
2472         JOIN crosschecked_anchor_authorities a
2473           ON a.authority_did = q.authority_did
2474          AND a.authority_key_id = q.authority_key_id
2475         {predicate}"
2476    );
2477    connection
2478        .query_row(&sql, query_params, |row| {
2479            Ok((
2480                row.get::<_, String>(0)?,
2481                row.get::<_, String>(1)?,
2482                row.get::<_, Vec<u8>>(2)?,
2483                row.get::<_, String>(3)?,
2484                row.get::<_, Vec<u8>>(4)?,
2485                row.get::<_, Vec<u8>>(5)?,
2486                row.get::<_, Vec<u8>>(6)?,
2487                row.get::<_, Vec<u8>>(7)?,
2488                row.get::<_, Vec<u8>>(8)?,
2489                row.get::<_, Vec<u8>>(9)?,
2490                row.get::<_, Vec<u8>>(10)?,
2491                row.get::<_, i64>(11)?,
2492                row.get::<_, i64>(12)?,
2493            ))
2494        })
2495        .optional()
2496        .map_err(storage)?
2497        .map(
2498            |(
2499                authority_did,
2500                authority_key_id,
2501                idempotency_key,
2502                source_code,
2503                action_hash,
2504                request_hash,
2505                request_body,
2506                receipt_body,
2507                response_body,
2508                authority_chain_hash,
2509                authority_public_key,
2510                physical_ms,
2511                logical,
2512            )| {
2513                Ok(StoredAnchorRow {
2514                    authority_did,
2515                    authority_key_id,
2516                    idempotency_key: exact_bytes(idempotency_key, "idempotency key")?,
2517                    source_code,
2518                    action_hash: exact_bytes(action_hash, "action hash")?,
2519                    request_hash: Hash256::from_bytes(exact_bytes(request_hash, "request hash")?),
2520                    request_body,
2521                    receipt_body,
2522                    response_body,
2523                    authority_chain_hash: Hash256::from_bytes(exact_bytes(
2524                        authority_chain_hash,
2525                        "authority chain hash",
2526                    )?),
2527                    authority_public_key: PublicKey::from_bytes(exact_bytes(
2528                        authority_public_key,
2529                        "authority public key",
2530                    )?),
2531                    node_recorded_at: Timestamp::new(
2532                        from_i64(physical_ms, "node recorded physical")?,
2533                        u32::try_from(logical).map_err(|_| {
2534                            AnchorStoreError::ReadbackValidation(
2535                                "node recorded logical timestamp is invalid".into(),
2536                            )
2537                        })?,
2538                    ),
2539                })
2540            },
2541        )
2542        .transpose()
2543}
2544
2545fn document_map(
2546    documents: &[DidDocument],
2547) -> Result<BTreeMap<&str, &DidDocument>, AnchorStoreError> {
2548    let mut by_did = BTreeMap::new();
2549    for document in documents {
2550        if document.revoked {
2551            return Err(AnchorStoreError::AuthorityValidation(format!(
2552                "DID document {} is revoked",
2553                document.id
2554            )));
2555        }
2556        if by_did.insert(document.id.as_str(), document).is_some() {
2557            return Err(AnchorStoreError::AuthorityValidation(format!(
2558                "duplicate DID document {}",
2559                document.id
2560            )));
2561        }
2562    }
2563    Ok(by_did)
2564}
2565
2566fn exact_method<'a>(
2567    document: &'a DidDocument,
2568    key_id: &str,
2569) -> Result<&'a VerificationMethod, AnchorStoreError> {
2570    let matches: Vec<&VerificationMethod> = document
2571        .verification_methods
2572        .iter()
2573        .filter(|method| method.id == key_id)
2574        .collect();
2575    if matches.len() != 1 {
2576        return Err(AnchorStoreError::AuthorityValidation(format!(
2577            "DID document must contain exactly one method {key_id}"
2578        )));
2579    }
2580    Ok(matches[0])
2581}
2582
2583fn authority_chain_hash(
2584    chain: &AuthorityChain,
2585    binding: &CrossCheckedScopeBindingV1,
2586) -> Result<Hash256, AnchorStoreError> {
2587    let signature = ed25519_bytes(binding.signature.clone())?;
2588    let encoded = encode_value(&Value::Array(vec![
2589        text(AUTHORITY_CHAIN_HASH_DOMAIN),
2590        bytes(authority_chain_fingerprint(chain)?.as_bytes()),
2591        bytes(Hash256::digest(&binding.signing_preimage()?).as_bytes()),
2592        bytes(&signature),
2593    ]))?;
2594    Ok(Hash256::digest(&encoded))
2595}
2596
2597fn signing_operation_id(request_hash: Hash256, purpose: &str) -> Result<Hash256, AnchorStoreError> {
2598    Ok(Hash256::digest(&encode_value(&Value::Array(vec![
2599        text(SIGNING_OPERATION_DOMAIN),
2600        bytes(request_hash.as_bytes()),
2601        text(purpose),
2602    ]))?))
2603}
2604
2605fn response_wrapper_signature(body: &[u8]) -> Result<[u8; 64], AnchorStoreError> {
2606    let value: Value = ciborium::from_reader(body)
2607        .map_err(|error| AnchorStoreError::Storage(format!("response CBOR decode: {error}")))?;
2608    let Value::Map(fields) = value else {
2609        return Err(AnchorStoreError::Storage(
2610            "response CBOR root is not map".into(),
2611        ));
2612    };
2613    for (key, value) in fields {
2614        if key == text("wrapper_signature") {
2615            let Value::Bytes(signature) = value else {
2616                return Err(AnchorStoreError::Storage(
2617                    "wrapper signature is not bytes".into(),
2618                ));
2619            };
2620            return exact_bytes(signature, "wrapper signature");
2621        }
2622    }
2623    Err(AnchorStoreError::Storage(
2624        "wrapper signature field missing".into(),
2625    ))
2626}
2627
2628fn ed25519_bytes(signature: Signature) -> Result<[u8; 64], AnchorStoreError> {
2629    signature.ed25519_bytes().copied().ok_or_else(|| {
2630        AnchorStoreError::Signer(SignOnceError::Unavailable(
2631            "signer returned a non-Ed25519 signature".into(),
2632        ))
2633    })
2634}
2635
2636fn permission_code(permission: Permission) -> Result<&'static str, AnchorStoreError> {
2637    match permission {
2638        Permission::AnchorReceiptCommitment => Ok(ANCHOR_PERMISSION_CODE),
2639        _ => Err(AnchorStoreError::AuthorityValidation(
2640            "permission is not AnchorReceiptCommitment".into(),
2641        )),
2642    }
2643}
2644
2645fn encode_serde<T: Serialize>(value: &T) -> Result<Vec<u8>, AnchorStoreError> {
2646    let mut encoded = Vec::new();
2647    ciborium::into_writer(value, &mut encoded)
2648        .map_err(|error| AnchorStoreError::Storage(format!("CBOR encode: {error}")))?;
2649    Ok(encoded)
2650}
2651
2652fn decode_serde<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, AnchorStoreError> {
2653    ciborium::from_reader(bytes)
2654        .map_err(|error| AnchorStoreError::ReadbackValidation(format!("CBOR decode: {error}")))
2655}
2656
2657fn encode_value(value: &Value) -> Result<Vec<u8>, AnchorStoreError> {
2658    let mut encoded = Vec::new();
2659    ciborium::into_writer(value, &mut encoded)
2660        .map_err(|error| AnchorStoreError::Storage(format!("canonical CBOR encode: {error}")))?;
2661    Ok(encoded)
2662}
2663
2664fn exact_bytes<const N: usize>(bytes: Vec<u8>, name: &str) -> Result<[u8; N], AnchorStoreError> {
2665    bytes.try_into().map_err(|_| {
2666        AnchorStoreError::ReadbackValidation(format!("{name} has invalid byte length"))
2667    })
2668}
2669
2670fn to_i64(value: u64, name: &str) -> Result<i64, AnchorStoreError> {
2671    i64::try_from(value)
2672        .map_err(|_| AnchorStoreError::Storage(format!("{name} exceeds SQLite INTEGER")))
2673}
2674
2675fn from_i64(value: i64, name: &str) -> Result<u64, AnchorStoreError> {
2676    u64::try_from(value)
2677        .map_err(|_| AnchorStoreError::ReadbackValidation(format!("{name} is negative in storage")))
2678}
2679
2680fn text(value: &str) -> Value {
2681    Value::Text(value.to_owned())
2682}
2683
2684fn bytes(value: &[u8]) -> Value {
2685    Value::Bytes(value.to_vec())
2686}
2687
2688fn unsigned(value: u64) -> Value {
2689    Value::Integer(value.into())
2690}
2691
2692fn storage(error: rusqlite::Error) -> AnchorStoreError {
2693    AnchorStoreError::Storage(error.to_string())
2694}
2695
2696fn codec(error: exo_api::crosschecked_anchor::AnchorCodecError) -> AnchorStoreError {
2697    AnchorStoreError::Codec(error.to_string())
2698}
2699
2700fn readback_codec(error: exo_api::crosschecked_anchor::AnchorCodecError) -> AnchorStoreError {
2701    AnchorStoreError::ReadbackValidation(error.to_string())
2702}
2703
2704fn identity(error: exo_identity::did_verification::DidVerificationError) -> AnchorStoreError {
2705    AnchorStoreError::AuthorityValidation(error.to_string())
2706}
2707
2708fn authority(error: exo_authority::AuthorityError) -> AnchorStoreError {
2709    AnchorStoreError::AuthorityValidation(error.to_string())
2710}