use holo_hash::{AgentPubKey, DhtOpHash, HasHash};
use holochain_data::dht::{InsertLimboChainOp, InsertLimboWarrant, InsertScheduledFunction};
use holochain_data::kind::Dht;
use holochain_data::DbWrite;
use holochain_types::dht_op::{DhtOp, DhtOpHashed};
use holochain_types::prelude::{Schedule, ScheduledFn, Timestamp};
use holochain_zome_types::schedule::ScheduleError;
use crate::mutations::{StateMutationError, StateMutationResult};
#[derive(Debug, Clone)]
pub struct IntegratedOpSummary {
pub op_hash: holo_hash::DhtOpHash,
pub basis_hash: holo_hash::AnyLinkableHash,
pub authored_timestamp: Timestamp,
pub validation_status: holochain_zome_types::dht_v2::OpValidity,
pub when_received: Timestamp,
pub validation_attempts: u32,
pub action_author: Option<holo_hash::AgentPubKey>,
pub warrant_author: Option<holo_hash::AgentPubKey>,
pub warrantee: Option<holo_hash::AgentPubKey>,
}
#[derive(Debug, Clone, Copy)]
pub enum SysOutcome {
Accepted,
Rejected,
}
#[derive(Debug, Clone, Copy)]
pub enum AppOutcome {
Accepted,
Rejected,
}
#[derive(thiserror::Error, Debug)]
pub enum DhtStoreError {
#[error(transparent)]
Db(#[from] sqlx::Error),
#[error(transparent)]
Schedule(#[from] holochain_serialized_bytes::SerializedBytesError),
#[error(transparent)]
ScheduleParams(#[from] ScheduleError),
#[error("no ChainOpPublish row for the given op_hash")]
ChainOpPublishMissing,
}
pub type DhtStoreResult<T> = Result<T, DhtStoreError>;
pub type DhtStoreRead = DhtStore<holochain_data::DbRead<Dht>>;
pub use holochain_data::models::dht::{
K2ChainOpForWireRow, K2OpHashRow, K2OpIdSinceRow, K2OpPresentRow, K2WarrantForWireRow,
SliceHashIndexedRow,
};
#[derive(Clone, Debug)]
pub struct DhtStore<Db = DbWrite<Dht>> {
db: Db,
}
impl<Db> DhtStore<Db> {
pub fn new(db: Db) -> Self {
Self { db }
}
pub(crate) fn db(&self) -> &Db {
&self.db
}
}
impl DhtStore<DbWrite<Dht>> {
pub async fn delete_live_ephemeral_scheduled_functions(
&self,
author: &AgentPubKey,
now: Timestamp,
) -> DhtStoreResult<u64> {
Ok(self
.db
.delete_live_ephemeral_scheduled_functions(author, now)
.await?)
}
pub async fn reschedule_expired_persisted(&self, author: &AgentPubKey, now: Timestamp) {
let expired = match self
.db
.as_ref()
.get_expired_persisted_scheduled_functions(author, now)
.await
{
Ok(rows) => rows,
Err(e) => {
tracing::error!(
"error querying expired persisted scheduled functions: {:?}",
e
);
return;
}
};
for (zome_name, scheduled_fn_name, maybe_schedule_blob) in expired {
let maybe_schedule: Option<Schedule> =
match holochain_serialized_bytes::decode(&maybe_schedule_blob) {
Ok(s) => s,
Err(e) => {
tracing::error!(
"error decoding maybe_schedule for ({}, {}): {:?}",
zome_name,
scheduled_fn_name,
e
);
continue;
}
};
match crate::schedule::compute_schedule_params(&maybe_schedule, now) {
Err(e) => {
tracing::error!(
"error computing schedule params for ({}, {}): {:?}",
zome_name,
scheduled_fn_name,
e
);
}
Ok(None) => {
if let Err(e) = self
.db
.delete_scheduled_function(author, &zome_name, &scheduled_fn_name)
.await
{
tracing::error!(
"error deleting expired scheduled function ({}, {}): {:?}",
zome_name,
scheduled_fn_name,
e
);
}
}
Ok(Some((start_at, end_at, ephemeral))) => {
if let Err(e) = self
.db
.upsert_scheduled_function(InsertScheduledFunction {
author,
zome_name: &zome_name,
scheduled_fn: &scheduled_fn_name,
maybe_schedule: &maybe_schedule_blob,
start_at,
end_at,
ephemeral,
})
.await
{
tracing::error!(
"error upserting rescheduled function ({}, {}): {:?}",
zome_name,
scheduled_fn_name,
e
);
}
}
}
}
}
pub async fn upsert_scheduled_function(
&self,
author: &AgentPubKey,
scheduled_fn: &ScheduledFn,
maybe_schedule: &Option<Schedule>,
now: Timestamp,
) -> DhtStoreResult<u64> {
let zome_name = scheduled_fn.zome_name().0.as_ref();
let fn_name = scheduled_fn.fn_name().0.as_str();
let maybe_schedule_blob = crate::schedule::serialize_maybe_schedule(maybe_schedule)?;
match crate::schedule::compute_schedule_params(maybe_schedule, now)? {
None => {
Ok(self
.db
.delete_scheduled_function(author, zome_name, fn_name)
.await?)
}
Some((start_at, end_at, ephemeral)) => Ok(self
.db
.upsert_scheduled_function(InsertScheduledFunction {
author,
zome_name,
scheduled_fn: fn_name,
maybe_schedule: &maybe_schedule_blob,
start_at,
end_at,
ephemeral,
})
.await?),
}
}
pub async fn unschedule_function(
&self,
author: &AgentPubKey,
scheduled_fn: &ScheduledFn,
) -> DhtStoreResult<u64> {
Ok(self
.db
.delete_scheduled_function(
author,
scheduled_fn.zome_name().0.as_ref(),
scheduled_fn.fn_name().0.as_str(),
)
.await?)
}
pub async fn record_validation_receipt(
&self,
receipt: &holochain_types::prelude::SignedValidationReceipt,
) -> StateMutationResult<u64> {
use holo_hash::encode::blake2b_256;
let bytes =
holochain_serialized_bytes::encode(receipt).map_err(StateMutationError::from)?;
let hash_bytes = blake2b_256(&bytes);
let receipt_hash = DhtOpHash::from_raw_32(hash_bytes);
let op_hash = receipt.receipt.dht_op_hash.clone();
let validators_bytes = holochain_serialized_bytes::encode(&receipt.receipt.validators)
.map_err(StateMutationError::from)?;
let signature_bytes = holochain_serialized_bytes::encode(&receipt.validators_signatures)
.map_err(StateMutationError::from)?;
self.db
.insert_validation_receipt(
&receipt_hash,
&op_hash,
&validators_bytes,
&signature_bytes,
holochain_types::prelude::Timestamp::now(),
)
.await
.map_err(StateMutationError::from)?;
let op_hash_bytes = op_hash.get_raw_36().to_vec();
let count: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM ValidationReceipt WHERE op_hash = ?")
.bind(&op_hash_bytes)
.fetch_one(self.db.pool())
.await
.map_err(StateMutationError::from)?;
Ok(count as u64)
}
pub async fn mark_chain_op_receipts_complete(&self, op_hash: &DhtOpHash) -> DhtStoreResult<()> {
let rows = self.db.set_chain_op_receipts_complete(op_hash).await?;
if rows == 0 {
return Err(DhtStoreError::ChainOpPublishMissing);
}
Ok(())
}
pub async fn purge_all(&self) -> DhtStoreResult<()> {
let pool = self.db.pool();
let mut tx = pool.begin().await?;
sqlx::query("DELETE FROM ChainOpPublish")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM ValidationReceipt")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM WarrantPublish")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM ChainOp").execute(&mut *tx).await?;
sqlx::query("DELETE FROM LimboChainOp")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM CapGrant")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM Link").execute(&mut *tx).await?;
sqlx::query("DELETE FROM DeletedLink")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM UpdatedRecord")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM DeletedRecord")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM Action").execute(&mut *tx).await?;
sqlx::query("DELETE FROM LimboWarrantOp")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM WarrantOp")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM Warrant").execute(&mut *tx).await?;
sqlx::query("DELETE FROM Entry").execute(&mut *tx).await?;
sqlx::query("DELETE FROM PrivateEntry")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM CapClaim")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM ChainLock")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM ScheduledFunction")
.execute(&mut *tx)
.await?;
sqlx::query("DELETE FROM SliceHash")
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
pub async fn record_incoming_ops(&self, ops: Vec<DhtOpHashed>) -> StateMutationResult<()> {
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
let now = Timestamp::now();
for op in ops {
let op_hash = op.as_hash().clone();
let serialized_size = holochain_serialized_bytes::encode(op.as_content())
.map_err(StateMutationError::from)?
.len() as u32;
match op.into_inner().0 {
DhtOp::ChainOp(chain_op) => {
let signed_action = chain_op.signed_action();
let action_hash = holo_hash::ActionHash::with_data_sync(signed_action.action());
let sah = holochain_zome_types::record::SignedActionHashed::with_presigned(
holo_hash::HoloHashed::with_pre_hashed(
signed_action.action().clone(),
action_hash.clone(),
),
signed_action.signature().clone(),
);
let new_sah = holochain_zome_types::dht_v2::from_legacy_signed_action(&sah);
tx.insert_action(&new_sah, None)
.await
.map_err(StateMutationError::from)?;
if let holochain_types::prelude::RecordEntryRef::Present(entry) =
chain_op.entry()
{
let entry_hash = entry_hash_from_chain_op_action(&chain_op)?;
tx.insert_entry(&entry_hash, entry)
.await
.map_err(StateMutationError::from)?;
}
let basis_hash = chain_op.dht_basis();
let storage_center_loc = basis_hash.get_loc();
tx.insert_limbo_chain_op(InsertLimboChainOp {
op_hash: &op_hash,
action_hash: &action_hash,
op_type: i64::from(chain_op.get_type()),
basis_hash: &basis_hash,
storage_center_loc,
require_receipt: true,
when_received: now,
serialized_size,
})
.await
.map_err(StateMutationError::from)?;
}
DhtOp::WarrantOp(warrant_op) => {
let author = &warrant_op.author;
let timestamp = warrant_op.timestamp;
let warrantee = &warrant_op.warrantee;
let storage_center_loc = warrantee.get_loc();
let proof_bytes = holochain_serialized_bytes::encode(&warrant_op.proof)
.map_err(StateMutationError::from)?;
let signature_bytes = warrant_op.signature().0;
tx.insert_limbo_warrant(InsertLimboWarrant {
hash: &op_hash,
author,
timestamp,
warrantee,
proof: &proof_bytes,
signature: &signature_bytes,
reason: warrant_op.proof.reason(),
storage_center_loc,
when_received: now,
serialized_size,
})
.await
.map_err(StateMutationError::from)?;
}
}
}
tx.commit().await.map_err(StateMutationError::from)?;
Ok(())
}
pub async fn record_chain_op_sys_validation_outcomes(
&self,
outcomes: Vec<(DhtOpHash, SysOutcome)>,
) -> StateMutationResult<()> {
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
for (hash, outcome) in outcomes {
let status: i64 = match outcome {
SysOutcome::Accepted => 1,
SysOutcome::Rejected => 2,
};
tx.set_limbo_chain_op_sys_validation_status(&hash, Some(status))
.await
.map_err(StateMutationError::from)?;
}
tx.commit().await.map_err(StateMutationError::from)?;
Ok(())
}
pub async fn record_warrant_sys_validation_outcomes(
&self,
outcomes: Vec<(DhtOpHash, SysOutcome)>,
) -> StateMutationResult<()> {
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
for (hash, outcome) in outcomes {
let status: i64 = match outcome {
SysOutcome::Accepted => 1,
SysOutcome::Rejected => 2,
};
tx.set_limbo_warrant_sys_validation_status(&hash, Some(status))
.await
.map_err(StateMutationError::from)?;
}
tx.commit().await.map_err(StateMutationError::from)?;
Ok(())
}
pub async fn record_app_validation_outcomes(
&self,
outcomes: Vec<(DhtOpHash, AppOutcome)>,
) -> StateMutationResult<()> {
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
for (hash, outcome) in outcomes {
let status: i64 = match outcome {
AppOutcome::Accepted => 1,
AppOutcome::Rejected => 2,
};
tx.set_limbo_chain_op_app_validation_status(&hash, Some(status))
.await
.map_err(StateMutationError::from)?;
}
tx.commit().await.map_err(StateMutationError::from)?;
Ok(())
}
pub async fn record_locally_validated_warrants(
&self,
warrants: Vec<DhtOpHashed>,
) -> StateMutationResult<()> {
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
let now = Timestamp::now();
for op in warrants {
let serialized_size = holochain_serialized_bytes::encode(op.as_content())
.map_err(StateMutationError::from)?
.len() as u32;
let warrant_op = match op.as_content() {
DhtOp::WarrantOp(w) => w,
DhtOp::ChainOp(_) => {
tracing::warn!(
"record_locally_validated_warrants got a non-warrant DhtOp; skipping"
);
continue;
}
};
let hash = op.as_hash();
let proof_bytes = holochain_serialized_bytes::encode(&warrant_op.proof)
.map_err(StateMutationError::from)?;
let signature_bytes = warrant_op.signature().0;
tx.insert_limbo_warrant(InsertLimboWarrant {
hash,
author: &warrant_op.author,
timestamp: warrant_op.timestamp,
warrantee: &warrant_op.warrantee,
proof: &proof_bytes,
signature: &signature_bytes,
reason: warrant_op.proof.reason(),
storage_center_loc: warrant_op.warrantee.get_loc(),
when_received: now,
serialized_size,
})
.await
.map_err(StateMutationError::from)?;
tx.set_limbo_warrant_sys_validation_status(hash, Some(1))
.await
.map_err(StateMutationError::from)?;
}
tx.commit().await.map_err(StateMutationError::from)?;
Ok(())
}
pub async fn integrate_ready_ops(
&self,
when_integrated: Timestamp,
) -> StateMutationResult<Vec<IntegratedOpSummary>> {
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
let mut out: Vec<IntegratedOpSummary> = Vec::new();
let chain_ready = tx
.as_mut()
.limbo_chain_ops_ready_for_integration(10_000)
.await
.map_err(StateMutationError::from)?;
for row in chain_ready {
let op_hash = DhtOpHash::from_raw_36(row.hash.clone());
let validation_status = compute_chain_op_validation_status(&row);
let basis_hash = chain_op_basis_hash_from_row(row.op_type, row.basis_hash.clone());
let action_hash = holo_hash::ActionHash::from_raw_36(row.action_hash.clone());
let v2_action = tx
.as_mut()
.get_action(action_hash)
.await
.map_err(StateMutationError::from)?;
let (action_author, authored_timestamp) = match v2_action {
Some(sah) => (
Some(sah.hashed.content.header.author.clone()),
sah.hashed.content.header.timestamp,
),
None => (None, Timestamp::from_micros(0)),
};
let promoted_ok = tx
.promote_limbo_chain_op(&op_hash, validation_status, when_integrated)
.await
.map_err(StateMutationError::from)?;
if promoted_ok {
out.push(IntegratedOpSummary {
op_hash,
basis_hash,
authored_timestamp,
validation_status,
when_received: Timestamp::from_micros(row.when_received),
validation_attempts: (row.sys_validation_attempts + row.app_validation_attempts)
as u32,
action_author,
warrant_author: None,
warrantee: None,
});
}
}
let warrant_ready = tx
.as_mut()
.limbo_warrants_ready_for_integration(10_000)
.await
.map_err(StateMutationError::from)?;
for row in warrant_ready {
let op_hash = DhtOpHash::from_raw_36(row.hash.clone());
let promoted_ok = tx
.promote_limbo_warrant(&op_hash, when_integrated)
.await
.map_err(StateMutationError::from)?;
if promoted_ok {
let validation_status = match row.sys_validation_status {
Some(2) => holochain_zome_types::dht_v2::OpValidity::Rejected,
_ => holochain_zome_types::dht_v2::OpValidity::Accepted,
};
let warrantee = holo_hash::AgentPubKey::from_raw_36(row.warrantee.clone());
let basis_hash: holo_hash::AnyLinkableHash = warrantee.clone().into();
out.push(IntegratedOpSummary {
op_hash,
basis_hash,
authored_timestamp: Timestamp::from_micros(row.timestamp),
validation_status,
when_received: Timestamp::from_micros(row.when_received),
validation_attempts: row.sys_validation_attempts as u32,
action_author: None,
warrant_author: Some(holo_hash::AgentPubKey::from_raw_36(row.author.clone())),
warrantee: Some(warrantee),
});
}
}
tx.commit().await.map_err(StateMutationError::from)?;
Ok(out)
}
pub async fn clear_require_receipts(
&self,
op_hashes: Vec<DhtOpHash>,
) -> StateMutationResult<()> {
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
for hash in op_hashes {
tx.clear_chain_op_require_receipt(&hash)
.await
.map_err(StateMutationError::from)?;
}
tx.commit().await.map_err(StateMutationError::from)?;
Ok(())
}
pub async fn record_published_op_hashes(
&self,
op_hashes: Vec<DhtOpHash>,
now: Timestamp,
) -> StateMutationResult<()> {
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
for hash in op_hashes {
tx.set_chain_op_last_publish_time(&hash, now)
.await
.map_err(StateMutationError::from)?;
}
tx.commit().await.map_err(StateMutationError::from)?;
Ok(())
}
pub async fn clear_op_withhold_publishes(
&self,
op_hashes: Vec<DhtOpHash>,
) -> StateMutationResult<()> {
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
for hash in op_hashes {
tx.clear_chain_op_withhold_publish(&hash)
.await
.map_err(StateMutationError::from)?;
}
tx.commit().await.map_err(StateMutationError::from)?;
Ok(())
}
pub async fn reject_chain_ops(&self, op_hashes: Vec<DhtOpHash>) -> StateMutationResult<()> {
use holochain_zome_types::dht_v2::OpValidity;
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
for hash in op_hashes {
let updated = tx
.set_chain_op_validation_status(&hash, OpValidity::Rejected)
.await
.map_err(StateMutationError::from)?;
if updated == 0 {
tx.force_reject_limbo_chain_op(&hash)
.await
.map_err(StateMutationError::from)?;
}
}
tx.commit().await.map_err(StateMutationError::from)?;
Ok(())
}
pub async fn move_warranted_op_to_limbo(
&self,
action_hash: &holo_hash::ActionHash,
op_type: holochain_zome_types::op::ChainOpType,
) -> StateMutationResult<bool> {
Ok(self
.db
.move_chain_op_to_limbo(action_hash, i64::from(op_type))
.await?)
}
pub fn as_read(&self) -> DhtStoreRead {
DhtStore::new(self.db.as_ref().clone())
}
}
fn entry_hash_from_chain_op_action(
chain_op: &holochain_types::dht_op::ChainOp,
) -> StateMutationResult<holo_hash::EntryHash> {
chain_op.action().entry_hash().cloned().ok_or_else(|| {
StateMutationError::Other("op carries entry but action has no entry_hash".into())
})
}
fn chain_op_basis_hash_from_row(op_type: i64, raw: Vec<u8>) -> holo_hash::AnyLinkableHash {
match op_type {
1 | 5 | 7 => holo_hash::ActionHash::from_raw_36(raw).into(),
3 => holo_hash::AgentPubKey::from_raw_36(raw).into(),
_ => holo_hash::EntryHash::from_raw_36(raw).into(),
}
}
fn compute_chain_op_validation_status(
row: &holochain_data::models::dht::LimboChainOpRow,
) -> holochain_zome_types::dht_v2::OpValidity {
use holochain_zome_types::dht_v2::OpValidity as RecordValidity;
if row.sys_validation_status == Some(2) {
return RecordValidity::Rejected;
}
if row.app_validation_status == Some(2) {
return RecordValidity::Rejected;
}
RecordValidity::Accepted
}
impl From<DhtStore<DbWrite<Dht>>> for DhtStoreRead {
fn from(store: DhtStore<DbWrite<Dht>>) -> Self {
store.as_read()
}
}
#[cfg(feature = "test_utils")]
impl DhtStore<DbWrite<Dht>> {
pub async fn new_test(dht: Dht) -> DhtStoreResult<Self> {
let db = holochain_data::test_open_db(dht).await?;
Ok(Self::new(db))
}
pub async fn when_integrated(
&self,
op_hash: &holo_hash::DhtOpHash,
) -> DhtStoreResult<Option<Timestamp>> {
let row = self.db.as_ref().get_chain_op(op_hash.clone()).await?;
Ok(row.map(|r| Timestamp::from_micros(r.when_integrated)))
}
pub async fn test_insert_integrated_warrant(
&self,
warrant: DhtOpHashed,
) -> StateMutationResult<()> {
use holochain_data::dht::InsertWarrant;
let warrant_op = match warrant.as_content() {
DhtOp::WarrantOp(w) => w,
DhtOp::ChainOp(_) => panic!("test_insert_integrated_warrant requires a WarrantOp"),
};
let serialized_size = holochain_serialized_bytes::encode(warrant.as_content())
.map_err(StateMutationError::from)?
.len() as u32;
let proof_bytes = holochain_serialized_bytes::encode(&warrant_op.proof)
.map_err(StateMutationError::from)?;
let signature_bytes = warrant_op.signature().0;
let now = Timestamp::now();
let mut tx = self.db.begin().await.map_err(StateMutationError::from)?;
tx.insert_warrant(InsertWarrant {
hash: warrant.as_hash(),
author: &warrant_op.author,
timestamp: warrant_op.timestamp,
warrantee: &warrant_op.warrantee,
proof: &proof_bytes,
signature: &signature_bytes,
reason: warrant_op.proof.reason(),
storage_center_loc: warrant_op.warrantee.get_loc(),
when_received: now,
when_integrated: now,
serialized_size,
})
.await
.map_err(StateMutationError::from)?;
tx.commit().await.map_err(StateMutationError::from)?;
Ok(())
}
}
pub(crate) mod action_indexes;
mod cache;
mod reads;
mod sync_reads;
#[cfg(test)]
mod tests;