Skip to main content

urma_runtime/
publication.rs

1use crate::config;
2use crate::error::{Context, Error, ensure};
3use crate::reveal;
4use crate::transaction::{decode, transaction};
5use bitcoin::{Amount, OutPoint, ScriptBuf, TxOut, Witness, consensus::serialize};
6use serde::{Deserialize, Serialize};
7use urma_core::{
8    envelope,
9    format::{PublicRecord, Urma},
10    multipart::MultipartRecord,
11};
12use urma_identity::identity::IdentitySigner;
13
14use urma_chain::observation::Chain;
15use urma_wallet::{
16    funding::Funding,
17    wallet::{FeeBudget, SpendRequest, WalletError},
18};
19
20#[derive(Clone, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct PublicPlan {
23    pub version: u8,
24    pub chain: Chain,
25    pub funding: Funding,
26    pub commit: String,
27    pub reveal: String,
28    pub fee_rate: u64,
29}
30
31pub fn prepare(
32    record: &PublicRecord,
33    author: &impl IdentitySigner,
34    funding: Funding,
35    chain: Chain,
36    fee_rate: u64,
37) -> Result<PublicPlan, Error> {
38    prepare_bytes(&record.encode()?, author, funding, chain, fee_rate)
39}
40
41pub fn prepare_multipart(
42    record: &MultipartRecord,
43    author: &impl IdentitySigner,
44    funding: Funding,
45    chain: Chain,
46    fee_rate: u64,
47) -> Result<PublicPlan, Error> {
48    prepare_bytes(&record.encode()?, author, funding, chain, fee_rate)
49}
50
51pub fn prepare_bytes(
52    record: &[u8],
53    author: &impl IdentitySigner,
54    funding: Funding,
55    chain: Chain,
56    fee_rate: u64,
57) -> Result<PublicPlan, Error> {
58    ensure!(
59        (1..=100).contains(&fee_rate),
60        "client fee rate must be 1..100 base units/vB"
61    );
62    let (outpoint, previous) = funding.prevout()?;
63    let (script, info) =
64        envelope::build_for_author(record, author.public_key().inner.x_only_public_key().0)?;
65    let retained = config::publication_return(chain);
66    let return_output = TxOut {
67        value: Amount::from_sat(retained),
68        script_pubkey: previous.script_pubkey.clone(),
69    };
70    let mut reveal = reveal::preview(return_output.clone(), &script, &info)?;
71    let reveal_fee = reveal::fee(&reveal, fee_rate)?;
72    let publication_output = TxOut {
73        value: Amount::from_sat(retained + reveal_fee),
74        script_pubkey: ScriptBuf::new_p2tr_tweaked(info.output_key()),
75    };
76    let mut commit = transaction(outpoint, publication_output.clone());
77    commit.output.push(return_output);
78    commit.input[0].witness = Witness::from_slice(&[vec![0; 73], vec![0; 33]]);
79    let commit_fee = u64::try_from(commit.vsize())?
80        .checked_mul(fee_rate)
81        .context("fee overflow")?;
82    commit.input[0].witness = Witness::new();
83    let total_fee = commit_fee.checked_add(reveal_fee).context("fee overflow")?;
84    ensure!(
85        total_fee <= 500_000,
86        "client publication fee capacity exceeded"
87    );
88    let change = previous
89        .value
90        .to_sat()
91        .checked_sub(total_fee + retained)
92        .context("insufficient funding")?;
93    ensure!(change >= retained, "funding must leave native change");
94    commit.output[1].value = Amount::from_sat(change);
95    reveal.input[0].previous_output = OutPoint {
96        txid: commit.compute_txid(),
97        vout: 0,
98    };
99    let signature = author.sign_author(reveal::digest(&reveal, &publication_output, &script)?)?;
100    reveal.input[0].witness = envelope::witness(signature.as_ref(), &script, &info)?;
101    envelope::verify_reveal(&reveal, &commit)?;
102    Ok(PublicPlan {
103        version: Urma::VERSION,
104        chain,
105        funding,
106        commit: hex::encode(serialize(&commit)),
107        reveal: hex::encode(serialize(&reveal)),
108        fee_rate,
109    })
110}
111
112pub fn quote_record(
113    record: &[u8],
114    author: bitcoin::XOnlyPublicKey,
115    return_script: &ScriptBuf,
116    fee_rate: u64,
117) -> Result<u64, Error> {
118    ensure!((1..=100).contains(&fee_rate), "invalid fee rate");
119    ensure!(return_script.is_p2wpkh(), "funding requires native P2WPKH");
120    let (script, info) = envelope::build_for_author(record, author)?;
121    let output = TxOut {
122        value: Amount::from_sat(1000),
123        script_pubkey: return_script.clone(),
124    };
125    let reveal = reveal::preview(output.clone(), &script, &info)?;
126    let mut commit = transaction(
127        OutPoint::null(),
128        TxOut {
129            value: Amount::from_sat(1000),
130            script_pubkey: ScriptBuf::new_p2tr_tweaked(info.output_key()),
131        },
132    );
133    commit.output.push(output);
134    commit.input[0].witness = Witness::from_slice(&[vec![0; 73], vec![0; 33]]);
135    u64::try_from(commit.vsize() + reveal.vsize())?
136        .checked_mul(fee_rate)
137        .context("quote fee overflow")
138}
139
140pub struct SignedPublicPair {
141    pub plan: PublicPlan,
142    pub fee: u64,
143}
144
145pub fn prepare_signed_bytes(
146    record: &[u8],
147    signer: &impl IdentitySigner,
148    funding: Funding,
149    chain: Chain,
150    budget: FeeBudget,
151) -> Result<SignedPublicPair, Error> {
152    ensure!(
153        budget.chain == chain.genesis()?,
154        "funding budget chain mismatch"
155    );
156    let (_, previous) = funding.prevout()?;
157    if previous.script_pubkey != urma_wallet::signing::script(signer)? {
158        return Err(WalletError::WrongIdentity.into());
159    }
160    let mut plan = prepare_bytes(record, signer, funding, chain, budget.rate.0.get())?;
161    let commit = decode(
162        &plan.commit,
163        usize::try_from(config::STANDARD_TX_WEIGHT)? * 2,
164    )?;
165    let signed = urma_wallet::signing::sign(
166        signer,
167        &SpendRequest {
168            transaction: &commit,
169            prevouts: std::slice::from_ref(&previous),
170            budget,
171        },
172    )?;
173    let reveal = decode(
174        &plan.reveal,
175        usize::try_from(config::STANDARD_TX_WEIGHT)? * 2,
176    )?;
177    let outputs = signed.output[1]
178        .value
179        .to_sat()
180        .checked_add(reveal.output[0].value.to_sat())
181        .ok_or(WalletError::Overflow)?;
182    let fee = previous
183        .value
184        .to_sat()
185        .checked_sub(outputs)
186        .ok_or(WalletError::InsufficientFunds)?;
187    if fee > budget.maximum_base_units {
188        return Err(WalletError::BudgetExceeded.into());
189    }
190    plan.commit = hex::encode(serialize(&signed));
191    Ok(SignedPublicPair { plan, fee })
192}