1use 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#[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#[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#[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#[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 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 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 pub fn authorization_hash(&self) -> Result<Hash256, AnchorStoreError> {
173 Ok(Hash256::digest(&self.to_cbor_bytes()?))
174 }
175}
176
177#[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 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#[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 pub fn to_cbor_bytes(&self) -> Result<Vec<u8>, AnchorStoreError> {
237 encode_serde(self)
238 }
239}
240
241#[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 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 pub fn to_cbor_bytes(&self) -> Result<Vec<u8>, AnchorStoreError> {
277 encode_serde(self)
278 }
279}
280
281#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
293pub enum AnchorRecordDisposition {
294 Created,
295 Replayed,
296}
297
298#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
318pub enum AnchorConflictKind {
319 IdempotencyKey,
320 ActionHash,
321}
322
323#[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
332pub 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#[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#[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 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 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 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 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 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 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
956pub 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
982pub 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 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}