urma-runtime 0.1.0

URMA publication planning, network transports and recovery
Documentation
use crate::config;
use crate::disk_journal;
use crate::error::{Context, Error, ensure};
use crate::publication::PublicPlan;
use crate::transaction::decode;
use crate::{
    disk_plan::DiskPlan,
    node::{Node, Presence},
    publish::{PublishReport, advance, start, store},
};
use std::path::Path;
use urma_core::multipart::{MultipartRecord, VerifiedRecord};

pub fn publish(
    node: &Node,
    plan: &DiskPlan,
    approved_id: &str,
    journal: &Path,
) -> Result<PublishReport, Error> {
    plan.validate()?;
    disk_journal::guard(plan, journal)?;
    let (mut report, anchor) = start(
        node,
        plan.id()?,
        &plan.root_txid,
        plan.chain,
        approved_id,
        journal,
    )?;
    reconcile(node, plan, journal, &mut report)?;
    ensure!(
        node.block_hash(anchor.0)?.to_string() == anchor.1,
        "chain changed during publication reconciliation; resume against the new chain"
    );
    disk_journal::observe(node, journal, &report)?;
    store(journal, &report)?;
    Ok(report)
}

fn reconcile(
    node: &Node,
    plan: &DiskPlan,
    journal: &Path,
    report: &mut PublishReport,
) -> Result<(), Error> {
    let mut data_confirmed = true;
    let mut leaves_confirmed = true;
    let mut root_confirmed = false;
    let mut pending_commits = 0u32;
    for index in 0..plan.record_count {
        let pair = plan.record(index)?;
        report.transactions.clear();
        if !advance(node, &pair.commit, report)? {
            return Ok(());
        }
        let commit_confirmed = confirmed(report)?;
        if !known(report)? {
            report.blocked_reason = ConfirmationWait::Visibility.reason().into();
            return Ok(());
        }
        if !commit_confirmed {
            pending_commits += 1;
        }
        let kind = record(&pair)?;
        let dependencies = match &kind {
            MultipartRecord::Data(_) => true,
            MultipartRecord::Leaf(_) => data_confirmed,
            MultipartRecord::Root(_) => data_confirmed && leaves_confirmed,
        };
        let eligible = commit_confirmed && dependencies;
        let mut reveal_confirmed = false;
        if eligible {
            if !advance(node, &pair.reveal, report)? {
                return Ok(());
            }
            reveal_confirmed = confirmed(report)?;
        }
        match kind {
            MultipartRecord::Data(_) => data_confirmed &= reveal_confirmed,
            MultipartRecord::Leaf(_) => leaves_confirmed &= reveal_confirmed,
            MultipartRecord::Root(_) => root_confirmed = reveal_confirmed,
        }
        disk_journal::observe(node, journal, report)?;
        store(journal, report)?;
        if pending_commits >= 8 {
            report.blocked_reason = ConfirmationWait::Commits.reason().into();
            return Ok(());
        }
    }
    report.complete = data_confirmed && leaves_confirmed && root_confirmed;
    report.confirmed = report.complete;
    if !report.complete {
        report.blocked_reason = ConfirmationWait::Reveals.reason().into();
    }
    Ok(())
}

fn confirmed(report: &PublishReport) -> Result<bool, Error> {
    Ok(matches!(
        &report
            .transactions
            .last()
            .context("transaction observation missing")?
            .presence,
        Presence::Confirmed { .. }
    ))
}

fn known(report: &PublishReport) -> Result<bool, Error> {
    Ok(!matches!(
        &report
            .transactions
            .last()
            .context("transaction observation missing")?
            .presence,
        Presence::Missing
    ))
}

fn record(pair: &PublicPlan) -> Result<MultipartRecord, Error> {
    let commit = decode(
        &pair.commit,
        usize::try_from(config::STANDARD_TX_WEIGHT)? * 2,
    )?;
    let reveal = decode(
        &pair.reveal,
        usize::try_from(config::STANDARD_TX_WEIGHT)? * 2,
    )?;
    let record = VerifiedRecord::verify(reveal.compute_txid(), &reveal, &commit)?;
    Ok(record.decode()?)
}

enum ConfirmationWait {
    Visibility,
    Commits,
    Reveals,
}

impl ConfirmationWait {
    fn reason(&self) -> &'static str {
        match self {
            Self::Visibility => {
                "commit submission is not yet observable; resume before spending its change"
            }
            Self::Commits => {
                "eight funding commits are pending; resume after confirmation to advance the bounded pipeline"
            }
            Self::Reveals => {
                "awaiting confirmations; resume to publish eligible reveals and dependent manifests"
            }
        }
    }
}

pub fn may_resume_automatically(report: &PublishReport) -> bool {
    !report.complete
        && [
            ConfirmationWait::Visibility,
            ConfirmationWait::Commits,
            ConfirmationWait::Reveals,
        ]
        .iter()
        .any(|state| state.reason() == report.blocked_reason)
}