urma-runtime 0.2.2

URMA publication planning, network transports and recovery
Documentation
use crate::config;
use crate::error::{Context, Error, ensure};
use crate::reveal;
use crate::transaction::{decode, transaction};
use bitcoin::{Amount, OutPoint, ScriptBuf, TxOut, Witness, consensus::serialize};
use serde::{Deserialize, Serialize};
use urma_core::{
    envelope,
    format::{PublicRecord, Urma},
    multipart::MultipartRecord,
};
use urma_identity::identity::IdentitySigner;

use urma_chain::observation::Chain;
use urma_wallet::{
    funding::Funding,
    wallet::{FeeBudget, SpendRequest, WalletError},
};

#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PublicPlan {
    pub version: u8,
    pub chain: Chain,
    pub funding: Funding,
    pub commit: String,
    pub reveal: String,
    pub fee_rate: u64,
}

pub fn prepare(
    record: &PublicRecord,
    author: &impl IdentitySigner,
    funding: Funding,
    chain: Chain,
    fee_rate: u64,
) -> Result<PublicPlan, Error> {
    prepare_bytes(&record.encode()?, author, funding, chain, fee_rate)
}

pub fn prepare_multipart(
    record: &MultipartRecord,
    author: &impl IdentitySigner,
    funding: Funding,
    chain: Chain,
    fee_rate: u64,
) -> Result<PublicPlan, Error> {
    prepare_bytes(&record.encode()?, author, funding, chain, fee_rate)
}

pub fn prepare_bytes(
    record: &[u8],
    author: &impl IdentitySigner,
    funding: Funding,
    chain: Chain,
    fee_rate: u64,
) -> Result<PublicPlan, Error> {
    ensure!(
        (1..=100).contains(&fee_rate),
        "client fee rate must be 1..100 base units/vB"
    );
    let (outpoint, previous) = funding.prevout()?;
    let (script, info) =
        envelope::build_for_author(record, author.public_key().inner.x_only_public_key().0)?;
    let retained = config::publication_return(chain);
    let return_output = TxOut {
        value: Amount::from_sat(retained),
        script_pubkey: previous.script_pubkey.clone(),
    };
    let mut reveal = reveal::preview(return_output.clone(), &script, &info)?;
    let reveal_fee = reveal::fee(&reveal, fee_rate)?;
    let publication_output = TxOut {
        value: Amount::from_sat(retained + reveal_fee),
        script_pubkey: ScriptBuf::new_p2tr_tweaked(info.output_key()),
    };
    let mut commit = transaction(outpoint, publication_output.clone());
    commit.output.push(return_output);
    commit.input[0].witness = Witness::from_slice(&[vec![0; 73], vec![0; 33]]);
    let commit_fee = u64::try_from(commit.vsize())?
        .checked_mul(fee_rate)
        .context("fee overflow")?;
    commit.input[0].witness = Witness::new();
    let total_fee = commit_fee.checked_add(reveal_fee).context("fee overflow")?;
    ensure!(
        total_fee <= 500_000,
        "client publication fee capacity exceeded"
    );
    let change = previous
        .value
        .to_sat()
        .checked_sub(total_fee + retained)
        .context("insufficient funding")?;
    ensure!(change >= retained, "funding must leave native change");
    commit.output[1].value = Amount::from_sat(change);
    reveal.input[0].previous_output = OutPoint {
        txid: commit.compute_txid(),
        vout: 0,
    };
    let signature = author.sign_author(reveal::digest(&reveal, &publication_output, &script)?)?;
    reveal.input[0].witness = envelope::witness(signature.as_ref(), &script, &info)?;
    envelope::verify_reveal(&reveal, &commit)?;
    Ok(PublicPlan {
        version: Urma::VERSION,
        chain,
        funding,
        commit: hex::encode(serialize(&commit)),
        reveal: hex::encode(serialize(&reveal)),
        fee_rate,
    })
}

pub fn quote_record(
    record: &[u8],
    author: bitcoin::XOnlyPublicKey,
    return_script: &ScriptBuf,
    fee_rate: u64,
) -> Result<u64, Error> {
    ensure!((1..=100).contains(&fee_rate), "invalid fee rate");
    ensure!(return_script.is_p2wpkh(), "funding requires native P2WPKH");
    let (script, info) = envelope::build_for_author(record, author)?;
    let output = TxOut {
        value: Amount::from_sat(1000),
        script_pubkey: return_script.clone(),
    };
    let reveal = reveal::preview(output.clone(), &script, &info)?;
    let mut commit = transaction(
        OutPoint::null(),
        TxOut {
            value: Amount::from_sat(1000),
            script_pubkey: ScriptBuf::new_p2tr_tweaked(info.output_key()),
        },
    );
    commit.output.push(output);
    commit.input[0].witness = Witness::from_slice(&[vec![0; 73], vec![0; 33]]);
    u64::try_from(commit.vsize() + reveal.vsize())?
        .checked_mul(fee_rate)
        .context("quote fee overflow")
}

pub struct SignedPublicPair {
    pub plan: PublicPlan,
    pub fee: u64,
}

pub fn prepare_signed_bytes(
    record: &[u8],
    signer: &impl IdentitySigner,
    funding: Funding,
    chain: Chain,
    budget: FeeBudget,
) -> Result<SignedPublicPair, Error> {
    ensure!(
        budget.chain == chain.genesis()?,
        "funding budget chain mismatch"
    );
    let (_, previous) = funding.prevout()?;
    if previous.script_pubkey != urma_wallet::signing::script(signer)? {
        return Err(WalletError::WrongIdentity.into());
    }
    let mut plan = prepare_bytes(record, signer, funding, chain, budget.rate.0.get())?;
    let commit = decode(
        &plan.commit,
        usize::try_from(config::STANDARD_TX_WEIGHT)? * 2,
    )?;
    let signed = urma_wallet::signing::sign(
        signer,
        &SpendRequest {
            transaction: &commit,
            prevouts: std::slice::from_ref(&previous),
            budget,
        },
    )?;
    let reveal = decode(
        &plan.reveal,
        usize::try_from(config::STANDARD_TX_WEIGHT)? * 2,
    )?;
    let outputs = signed.output[1]
        .value
        .to_sat()
        .checked_add(reveal.output[0].value.to_sat())
        .ok_or(WalletError::Overflow)?;
    let fee = previous
        .value
        .to_sat()
        .checked_sub(outputs)
        .ok_or(WalletError::InsufficientFunds)?;
    if fee > budget.maximum_base_units {
        return Err(WalletError::BudgetExceeded.into());
    }
    plan.commit = hex::encode(serialize(&signed));
    Ok(SignedPublicPair { plan, fee })
}