Skip to main content

sidestr_core/
state.rs

1//! The chain in memory (SPEC 4, 5, 11): headers and hashes from the genesis
2//! up, the UTXO set, the overlay's records, a mempool with the producer's
3//! policy, and block production. A port of `siding/lib/chain.mjs` `Siding`
4//! and the state-machine half of `bitcoin-blake/blaketestnode`
5//! `lib/node.mjs` `ChainNode`, for level 1: one signer, no reorgs.
6//!
7//! Nothing here touches a file or a clock: [`StateOf`] is fed blocks and told
8//! the time. The file-backed [`crate::chain::ChainOf`] wraps it (feature `std`).
9//!
10//! The state is generic over the header family (SPEC 3.2): [`State`] is the
11//! stock instantiation, `StateOf<Blake2bV2>` (from `sidestr-header`) the
12//! BLAKE2b one; the document's parent must hand down the family the state is
13//! instantiated for, or [`StateOf::from_genesis`] refuses it.
14//!
15//! A state always holds its genesis. [`StateOf::with_key`] builds and seals it
16//! from the document and the signer's key (SPEC 5); [`StateOf::from_genesis`]
17//! takes a sealed block 0, judges it under every rule that applies at height
18//! 0 — the family's header rules, the solution against the challenge, a
19//! coinbase minting exactly the pegs — and only then holds it to the
20//! document's `genesisHash`, which is how a validator without the key starts.
21//! The reference trusts block 0 by its hash alone; this crate does not (see
22//! the crate docs, "Where this port departs").
23
24use std::collections::HashSet;
25
26use bitcoin::hashes::Hash;
27use bitcoin::secp256k1::SecretKey;
28use bitcoin::{
29    Amount, BlockHash, CompactTarget, OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid,
30};
31
32use crate::block::{
33    block_data, block_height, build_block, sign_block, BlockTemplate, HeaderFamily, SidestrBlock,
34    Stock, MARKER,
35};
36use crate::document::ChainDocument;
37use crate::error::{Error, Result};
38use crate::federation::Federation;
39use crate::marker::{claim_marker, looks_like_pegout, parse_pegout, Burn};
40use crate::rules::{
41    apply_block, median_time_past, validate_block_context, validate_block_structure,
42    validate_header, validate_transaction, BlockRule, Candidate, Coin, HeaderContext, Overlay,
43    Params, Records, RuleResult, Utxo, Verdict,
44};
45
46/// The outputs' total, `None` on overflow. Amounts in an unvalidated
47/// transaction are untrusted: nothing here assumes they are under max money.
48fn checked_output_sum(tx: &Transaction) -> Option<u64> {
49    tx.output
50        .iter()
51        .try_fold(0u64, |s, o| s.checked_add(o.value.to_sat()))
52}
53
54/// The rule [`StateOf::from_genesis`] adds to the kernel's at height 0: the
55/// block is *the document's* genesis, sealed — its signed block data
56/// (version, previous hash, time, and the coinbase stripped of its solution:
57/// the pegs, the marker, the height push, no other transaction) equals that
58/// of [`StateOf::build_genesis_for`], and its `bits` is the document's
59/// `powLimit` in compact form (SPEC 5; the difficulty rule has no previous
60/// header to hold it to at height 0).
61pub const RULE_GENESIS_DOCUMENT: &str = "sidestr:rule-genesis-document";
62use crate::sighash::verify_taproot_key_path;
63
64/// The tip: height, hash and header time.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct Tip {
67    /// Height of the tip.
68    pub height: u32,
69    /// Its block hash.
70    pub hash: BlockHash,
71    /// Its header time.
72    pub time: u32,
73}
74
75/// What applying a block reports.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct Applied {
78    /// The block's height.
79    pub height: u32,
80    /// Its hash.
81    pub hash: BlockHash,
82    /// Transactions in it, coinbase included.
83    pub txs: usize,
84    /// Fees the coinbase collected (production only; 0 for a block from elsewhere).
85    pub fees: u64,
86    /// Claims the block made (production only).
87    pub claims: usize,
88}
89
90/// What [`StateOf::submit`] reports.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct Submitted {
93    /// The transaction's id.
94    pub txid: Txid,
95    /// Its fee in sats.
96    pub fee: u64,
97    /// Its virtual size.
98    pub vsize: u64,
99    /// It was already in the mempool; nothing changed.
100    pub dup: bool,
101}
102
103/// A coin as [`StateOf::coins`] lists it.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct CoinRef {
106    /// The outpoint.
107    pub outpoint: OutPoint,
108    /// Sats.
109    pub value: u64,
110    /// The creating block's height.
111    pub height: u32,
112    /// Whether it is a coinbase output (maturity applies).
113    pub coinbase: bool,
114}
115
116/// A peg-in the producer claims in the next block (SPEC 6).
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct ClaimRequest {
119    /// Parent txid, display order.
120    pub txid: String,
121    /// Parent vout.
122    pub vout: u32,
123    /// The peg's amount in sats.
124    pub amount: u64,
125    /// The sidechain script the peg-in named.
126    pub script: ScriptBuf,
127}
128
129/// What the next block is built from.
130#[derive(Debug, Clone, Default)]
131pub struct NextBlock {
132    /// The header time wanted; at least the tip's time plus one is used.
133    pub time: u32,
134    /// Peg-ins to claim.
135    pub claims: Vec<ClaimRequest>,
136}
137
138/// The chain in memory, for header family `F`.
139#[derive(Debug)]
140pub struct StateOf<F: HeaderFamily> {
141    doc: ChainDocument,
142    family: F,
143    params: Params,
144    bits: CompactTarget,
145    challenge: ScriptBuf,
146    federation: Option<Federation>,
147    headers: Vec<F::Header>,
148    hashes: Vec<BlockHash>,
149    utxo: Utxo,
150    records: Records,
151    mempool: Vec<(Txid, Transaction)>,
152    mempool_spent: HashSet<OutPoint>,
153    extra_rules: Vec<Box<dyn BlockRule<F>>>,
154}
155
156/// The chain in memory beside a stock parent: [`StateOf`] over [`Stock`].
157pub type State = StateOf<Stock>;
158
159impl<F: HeaderFamily> StateOf<F> {
160    /// The family marker, if the document's parent hands down the family this
161    /// state is instantiated for; [`Error::UnsupportedFamily`] otherwise. The
162    /// first thing every constructor checks, before a block is decoded.
163    pub fn family_of(doc: &ChainDocument) -> Result<F> {
164        let family = doc.family()?;
165        if family != F::FAMILY {
166            return Err(Error::UnsupportedFamily(family));
167        }
168        Ok(F::default())
169    }
170
171    fn empty(doc: ChainDocument) -> Result<Self> {
172        doc.validate()?;
173        let family = Self::family_of(&doc)?;
174        let bits = doc.bits()?;
175        let challenge = doc.challenge_script()?;
176        let federation = Federation::for_document(&doc)?;
177        Ok(Self {
178            doc,
179            family,
180            params: Params::default(),
181            bits,
182            challenge,
183            federation,
184            headers: Vec::new(),
185            hashes: Vec::new(),
186            utxo: Utxo::new(),
187            records: Records::default(),
188            mempool: Vec::new(),
189            mempool_spent: HashSet::new(),
190            extra_rules: Vec::new(),
191        })
192    }
193
194    /// SPEC 5: the genesis block, unsigned, minting the document's pegs at the
195    /// document's time, marker `sidestr genesis <chain id>`
196    /// (`siding/lib/chain.mjs buildGenesis`). A pure function of the document.
197    pub fn build_genesis_for(doc: &ChainDocument) -> Result<F::Block> {
198        let family = Self::family_of(doc)?;
199        let outputs = doc
200            .pegs
201            .iter()
202            .map(|p| {
203                Ok(TxOut {
204                    value: Amount::from_sat(p.amount),
205                    script_pubkey: ScriptBuf::from_bytes(
206                        hex::decode(&p.script).map_err(|e| Error::Encoding(e.to_string()))?,
207                    ),
208                })
209            })
210            .collect::<Result<Vec<_>>>()?;
211        Ok(build_block(
212            &family,
213            &BlockTemplate {
214                height: 0,
215                prev: BlockHash::all_zeros(),
216                time: doc.genesis_time,
217                transactions: vec![],
218                outputs,
219                bits: doc.bits()?,
220                marker: format!("sidestr genesis {}", doc.id),
221            },
222        ))
223    }
224
225    /// The genesis sealed by the signer's key, deterministically (zero aux),
226    /// so it is reproducible from document and key (`siding/lib/chain.mjs genesisBlock`).
227    pub fn genesis_block_for(doc: &ChainDocument, key: &SecretKey) -> Result<F::Block> {
228        let family = Self::family_of(doc)?;
229        sign_block(
230            &family,
231            &Self::build_genesis_for(doc)?,
232            &doc.challenge_script()?,
233            key,
234            &[0u8; 32],
235        )
236    }
237
238    /// A state at its genesis, sealed with the signer's key.
239    pub fn with_key(doc: ChainDocument, key: &SecretKey) -> Result<Self> {
240        let genesis = Self::genesis_block_for(&doc, key)?;
241        Self::from_genesis(doc, &genesis, None)
242    }
243
244    /// A state at a sealed genesis. Block 0 is judged first, under every rule
245    /// that applies at height 0 — the header rules with no previous header
246    /// (proof of work against `bits`, the version, the family's own:
247    /// `knots:rule-header-v2-from-fork`, `-height`, `-flags-reserved`), the
248    /// block rules (`sidestr:rule-block-signature`: the solution against the
249    /// challenge, or the federation's leaf), the block-context rules with the
250    /// pegs as the one subsidy, and [`RULE_GENESIS_DOCUMENT`]. A failure is
251    /// [`Error::Rejected`] at height 0 naming the rules. Only then is the hash
252    /// held to `expect` when given, and to the document's `genesisHash` when
253    /// the document has one ([`Error::GenesisMismatch`]).
254    ///
255    /// **Departure**: `siding/lib/chain.mjs #apply` at `h === 0` applies the
256    /// genesis on the hash alone. A hash pin says which block 0 you hold, not
257    /// that it is well-formed; here an unsigned genesis whose hash the
258    /// document happens to name is still refused. There is no trusted import.
259    pub fn from_genesis(
260        doc: ChainDocument,
261        genesis: &F::Block,
262        expect: Option<BlockHash>,
263    ) -> Result<Self> {
264        let mut s = Self::empty(doc)?;
265        if genesis.txdata().is_empty() {
266            return Err(Error::Chain("genesis has no coinbase".into()));
267        }
268        let (mut verdict, _) = s.judge(0, genesis, None);
269        let expected = Self::build_genesis_for(&s.doc)?;
270        verdict.results.push(RuleResult::new(
271            RULE_GENESIS_DOCUMENT,
272            Some(
273                block_data(&s.family, genesis) == block_data(&s.family, &expected)
274                    && s.family.bits(genesis.header()) == s.bits,
275            ),
276        ));
277        if !verdict.ok() {
278            return Err(Error::Rejected {
279                height: 0,
280                rules: verdict.failed(),
281            });
282        }
283        let hash = s.family.block_hash(genesis.header());
284        if expect.is_some_and(|e| e != hash) {
285            return Err(Error::Chain("genesis hash mismatch".into()));
286        }
287        if let Some(want) = &s.doc.genesis_hash {
288            if *want != hash.to_string() {
289                return Err(Error::GenesisMismatch {
290                    found: hash.to_string(),
291                    expected: want.clone(),
292                });
293            }
294        }
295        apply_block(&mut s.utxo, genesis.txdata(), 0);
296        s.headers.push(genesis.header().clone());
297        s.hashes.push(hash);
298        Ok(s)
299    }
300
301    /// Add a block-context rule beyond the core (SPEC 12).
302    pub fn add_rule(&mut self, rule: Box<dyn BlockRule<F>>) {
303        self.extra_rules.push(rule);
304    }
305
306    /// The document.
307    pub fn document(&self) -> &ChainDocument {
308        &self.doc
309    }
310    /// The header family.
311    pub fn family(&self) -> &F {
312        &self.family
313    }
314    /// The network parameters.
315    pub fn params(&self) -> &Params {
316        &self.params
317    }
318    /// The compact target every block carries.
319    pub fn bits(&self) -> CompactTarget {
320        self.bits
321    }
322    /// The challenge.
323    pub fn challenge(&self) -> &Script {
324        &self.challenge
325    }
326    /// The federation the document names (level 2), or `None` for one signer.
327    pub fn federation(&self) -> Option<&Federation> {
328        self.federation.as_ref()
329    }
330    /// The genesis hash.
331    pub fn genesis_hash(&self) -> BlockHash {
332        self.hashes[0]
333    }
334    /// The tip.
335    pub fn tip(&self) -> Tip {
336        let h = self.hashes.len() - 1;
337        Tip {
338            height: h as u32,
339            hash: self.hashes[h],
340            time: self.family.time(&self.headers[h]),
341        }
342    }
343    /// The tip's height.
344    pub fn height(&self) -> u32 {
345        self.tip().height
346    }
347    /// The hash at a height.
348    pub fn hash_at(&self, height: u32) -> Option<BlockHash> {
349        self.hashes.get(height as usize).copied()
350    }
351    /// The header at a height.
352    pub fn header_at(&self, height: u32) -> Option<&F::Header> {
353        self.headers.get(height as usize)
354    }
355    /// The UTXO set.
356    pub fn utxo(&self) -> &Utxo {
357        &self.utxo
358    }
359    /// The overlay's records: claims and burns.
360    pub fn records(&self) -> &Records {
361        &self.records
362    }
363    /// Transactions waiting for a block, in arrival order.
364    pub fn mempool(&self) -> impl Iterator<Item = &Transaction> {
365        self.mempool.iter().map(|(_, tx)| tx)
366    }
367    /// Whether a parent outpoint is claimed on this chain (SPEC 6).
368    pub fn claimed(&self, txid: &str, vout: u32) -> bool {
369        self.records.claimed(txid, vout)
370    }
371    /// Every burn the chain has validated, oldest first (SPEC 7).
372    pub fn pegouts(&self) -> Vec<Burn> {
373        self.records.pegouts()
374    }
375    /// The least a burn may carry (`siding/lib/chain.mjs pegoutMin`).
376    pub fn pegout_min(&self) -> u64 {
377        self.doc.pegout_min
378    }
379    /// The producer's fee floor, sat/vB (`siding/lib/chain.mjs minFeeRate`).
380    pub fn min_fee_rate(&self) -> u64 {
381        self.doc.min_fee_rate
382    }
383    /// The coins paying a script, in no particular order (`siding/lib/chain.mjs coins`).
384    pub fn coins(&self, script_pubkey: &Script) -> Vec<CoinRef> {
385        let mut out: Vec<CoinRef> = self
386            .utxo
387            .iter()
388            .filter(|(_, c)| c.output.script_pubkey.as_script() == script_pubkey)
389            .map(|(op, c)| CoinRef {
390                outpoint: *op,
391                value: c.output.value.to_sat(),
392                height: c.height,
393                coinbase: c.coinbase,
394            })
395            .collect();
396        out.sort_by_key(|c| (c.height, c.outpoint.txid, c.outpoint.vout));
397        out
398    }
399    /// Whether a coin may be spent in the next block: not a coinbase, or a mature one.
400    pub fn spendable(&self, coin: &Coin) -> bool {
401        !coin.coinbase
402            || (u64::from(self.height()) + 1).saturating_sub(u64::from(coin.height))
403                >= u64::from(self.params.coinbase_maturity)
404    }
405    /// A transaction's virtual size: weight over four, rounded up.
406    pub fn vsize(tx: &Transaction) -> u64 {
407        tx.weight().to_wu().div_ceil(4)
408    }
409    /// The fee a transaction pays, from the UTXO set. `None` if an input is
410    /// not an unspent coin, if the outputs exceed the inputs, or if either
411    /// total overflows — the transaction is untrusted, so every sum is
412    /// checked and no amount is assumed to be under max money.
413    pub fn fees(&self, tx: &Transaction) -> Option<u64> {
414        let ins = tx.input.iter().try_fold(0u64, |s, i| {
415            let c = self.utxo.get(&i.previous_output)?;
416            s.checked_add(c.output.value.to_sat())
417        })?;
418        let outs = checked_output_sum(tx)?;
419        ins.checked_sub(outs)
420    }
421
422    /// Validate and apply block `height` (must be the tip plus one)
423    /// (`node.mjs applyNext` + `siding/lib/chain.mjs #apply`). `expect` is the
424    /// hash a mirror's index promised; `now` is the clock for the future-time
425    /// rule (`None` skips it). Every failed rule is named in the error.
426    pub fn apply(
427        &mut self,
428        height: u32,
429        block: &F::Block,
430        expect: Option<BlockHash>,
431        now: Option<u32>,
432    ) -> Result<Applied> {
433        let tip = self.tip();
434        if u64::from(height) != u64::from(tip.height) + 1 {
435            return Err(Error::Chain(format!(
436                "apply {height} at height {}",
437                tip.height
438            )));
439        }
440        let hash = self.family.block_hash(block.header());
441        if self.family.prev(block.header()) != tip.hash {
442            return Err(Error::Chain(format!(
443                "block {height} does not link to {}",
444                tip.hash
445            )));
446        }
447        let (verdict, next) = self.judge(height, block, now);
448        if !verdict.ok() {
449            return Err(Error::Rejected {
450                height,
451                rules: verdict.failed(),
452            });
453        }
454        if expect.is_some_and(|e| e != hash) {
455            return Err(Error::Chain(format!(
456                "block {height} hash {hash} is not {}",
457                expect.unwrap()
458            )));
459        }
460        apply_block(&mut self.utxo, block.txdata(), height);
461        self.records.claims.extend(next.claims);
462        self.records.pegouts.extend(next.pegouts);
463        self.headers.push(block.header().clone());
464        self.hashes.push(hash);
465        self.mempool.retain(|(_, tx)| {
466            tx.input
467                .iter()
468                .all(|i| self.utxo.contains_key(&i.previous_output))
469        });
470        self.mempool_spent = self
471            .mempool
472            .iter()
473            .flat_map(|(_, tx)| tx.input.iter().map(|i| i.previous_output))
474            .collect();
475        Ok(Applied {
476            height,
477            hash,
478            txs: block.txdata().len(),
479            fees: 0,
480            claims: 0,
481        })
482    }
483
484    /// Every phase's verdict on a candidate for `height`, and the records it
485    /// would leave, without applying anything. At height 0 there is no
486    /// previous header, so the rules that need one are skipped; a `height`
487    /// beyond the tip sees whatever headers exist below it.
488    pub fn judge(&self, height: u32, block: &F::Block, now: Option<u32>) -> (Verdict, Records) {
489        let h = height as usize;
490        let end = h.min(self.headers.len());
491        let window = &self.headers[end.saturating_sub(11)..end];
492        let mut verdict = validate_header(
493            &self.family,
494            &self.params,
495            block.header(),
496            &HeaderContext {
497                height,
498                prev: h.checked_sub(1).and_then(|i| self.headers.get(i)),
499                mtp_window: window,
500                now: now.map(|n| n.saturating_add(7_200)),
501            },
502        );
503        let overlay = Overlay {
504            challenge: &self.challenge,
505            pegout_min: self.doc.pegout_min,
506            genesis_subsidy: self
507                .doc
508                .pegs
509                .iter()
510                .fold(0u64, |s, p| s.saturating_add(p.amount)),
511        };
512        verdict.extend(validate_block_structure(
513            &self.family,
514            &self.params,
515            &overlay,
516            block,
517        ));
518        let mtp = (!window.is_empty()).then(|| median_time_past(&self.family, window));
519        let candidate = Candidate {
520            block,
521            height,
522            utxo: &self.utxo,
523            mtp,
524            records: &self.records,
525            extra: &self.extra_rules,
526        };
527        let (ctx, _, next) =
528            validate_block_context(&self.family, &self.params, &overlay, &candidate);
529        verdict.extend(ctx);
530        (verdict, next)
531    }
532
533    /// Accept a block from elsewhere (a mirror): its height read from it,
534    /// validated, applied (`siding/lib/chain.mjs addBlock`).
535    pub fn add_block(
536        &mut self,
537        block: &F::Block,
538        expect: Option<BlockHash>,
539        now: Option<u32>,
540    ) -> Result<Applied> {
541        let h = block_height(&self.family, block)?;
542        self.apply(h, block, expect, now)
543    }
544
545    /// [`StateOf::add_block`] from consensus bytes.
546    pub fn add_block_bytes(
547        &mut self,
548        bytes: &[u8],
549        expect: Option<BlockHash>,
550        now: Option<u32>,
551    ) -> Result<Applied> {
552        let block = F::Block::decode(bytes)?;
553        self.add_block(&block, expect, now)
554    }
555
556    /// SPEC 11: a transaction reaches the producer; it is included when it
557    /// validates (`siding/lib/chain.mjs submit`). The mempool's policy, in
558    /// order: the transaction rules; every input an unspent, unreserved,
559    /// mature coin; outputs at most inputs; a burn well-formed and at least
560    /// `pegoutMin` (SPEC 7); the fee at least `minFeeRate` sat/vB; every
561    /// input's signature under the sighash rules the next block is judged by.
562    pub fn submit(&mut self, tx: Transaction) -> Result<Submitted> {
563        let txid = tx.compute_txid();
564        if self.mempool.iter().any(|(id, _)| *id == txid) {
565            return Ok(Submitted {
566                txid,
567                fee: 0,
568                vsize: Self::vsize(&tx),
569                dup: true,
570            });
571        }
572        let refuse = |m: String| Err(Error::Transaction(m));
573        let v = validate_transaction(&self.params, &tx, false);
574        if !v.ok() {
575            return refuse(format!("transaction: {}", v.failed().join(", ")));
576        }
577        let mut prevouts = Vec::with_capacity(tx.input.len());
578        let mut in_sum = 0u64;
579        for i in &tx.input {
580            let key = i.previous_output;
581            if self.mempool_spent.contains(&key) {
582                return refuse(format!("input {key} already spent in the mempool"));
583            }
584            let Some(c) = self.utxo.get(&key) else {
585                return refuse(format!("input {key} is not an unspent coin"));
586            };
587            if !self.spendable(c) {
588                return refuse(format!("input {key} is an immature coinbase"));
589            }
590            prevouts.push(c.output.clone());
591            in_sum = in_sum.saturating_add(c.output.value.to_sat());
592        }
593        let Some(out_sum) = checked_output_sum(&tx) else {
594            return refuse("outputs overflow".into());
595        };
596        if out_sum > in_sum {
597            return refuse("outputs exceed inputs".into());
598        }
599        // SPEC 7: a burn names a parent script and carries at least pegoutMin, as the block rule will demand
600        for o in &tx.output {
601            if !o.script_pubkey.is_op_return() {
602                continue;
603            }
604            let script = parse_pegout(&o.script_pubkey);
605            if looks_like_pegout(o) && script.is_none() {
606                return refuse(
607                    "a peg-out names a parent output script of 2 to 40 bytes as hex".into(),
608                );
609            }
610            if script.is_some() && o.value.to_sat() < self.pegout_min() {
611                return refuse(format!(
612                    "a peg-out burns at least {} sats",
613                    self.pegout_min()
614                ));
615            }
616        }
617        // producer policy, published in chain.json so a wallet can compute it: at least minFeeRate sat/vB
618        let vsize = Self::vsize(&tx);
619        let min_fee = vsize.saturating_mul(self.min_fee_rate());
620        let fee = in_sum - out_sum;
621        if fee < min_fee {
622            return refuse(format!(
623                "fee {fee} is below the minimum {min_fee} sats ({vsize} vB at {} sat/vB)",
624                self.min_fee_rate()
625            ));
626        }
627        let sighash = self.family.sighash_rules(self.height().saturating_add(1));
628        for i in 0..tx.input.len() {
629            if let Err(e) = verify_taproot_key_path(&tx, i, &prevouts, sighash) {
630                return refuse(format!("input {i}: {e}"));
631            }
632        }
633        for i in &tx.input {
634            self.mempool_spent.insert(i.previous_output);
635        }
636        self.mempool.push((txid, tx));
637        Ok(Submitted {
638            txid,
639            fee,
640            vsize,
641            dup: false,
642        })
643    }
644
645    /// The next block, unsigned: the mempool in order, fees to the challenge,
646    /// the claims (SPEC 4, 6) (`siding/lib/chain.mjs buildNext`). A claim pays
647    /// the peg's amount to the script the peg-in named, followed by its marker.
648    pub fn build_next(&self, next: &NextBlock) -> Result<(F::Block, u64, usize)> {
649        let tip = self.tip();
650        let height = tip
651            .height
652            .checked_add(1)
653            .ok_or_else(|| Error::Chain("the chain is at the last height".into()))?;
654        let time = next.time.max(tip.time.saturating_add(1));
655        let txs: Vec<Transaction> = self.mempool.iter().map(|(_, tx)| tx.clone()).collect();
656        let fees = txs
657            .iter()
658            .try_fold(0u64, |s, tx| s.checked_add(self.fees(tx).unwrap_or(0)))
659            .ok_or_else(|| Error::Transaction("the mempool's fees overflow".into()))?;
660        let mut outputs = Vec::new();
661        if fees > 0 {
662            outputs.push(TxOut {
663                value: Amount::from_sat(fees),
664                script_pubkey: self.challenge.clone(),
665            });
666        }
667        for c in &next.claims {
668            if self.claimed(&c.txid, c.vout) {
669                return Err(Error::Transaction(format!(
670                    "{}:{} is already claimed",
671                    c.txid, c.vout
672                )));
673            }
674            outputs.push(TxOut {
675                value: Amount::from_sat(c.amount),
676                script_pubkey: c.script.clone(),
677            });
678            outputs.push(TxOut {
679                value: Amount::ZERO,
680                script_pubkey: claim_marker(&c.txid, c.vout),
681            });
682        }
683        let block = build_block(
684            &self.family,
685            &BlockTemplate {
686                height,
687                prev: tip.hash,
688                time,
689                transactions: txs,
690                outputs,
691                bits: self.bits,
692                marker: MARKER.to_string(),
693            },
694        );
695        Ok((block, fees, next.claims.len()))
696    }
697
698    /// One signer: build, sign, add (`siding/lib/chain.mjs produce`). Returns
699    /// the report and the sealed block, for the caller to write down. A
700    /// federated chain is refused: its blocks are sealed by `k` signatures
701    /// gathered above this crate and enter through [`StateOf::add_block`].
702    pub fn produce(
703        &mut self,
704        key: &SecretKey,
705        next: &NextBlock,
706        now: Option<u32>,
707    ) -> Result<(Applied, F::Block)> {
708        if self.federation.is_some() {
709            return Err(Error::Federation(
710                "a federated chain makes blocks through the round (proposals/level-2.md), not produce()".into(),
711            ));
712        }
713        let (block, fees, claims) = self.build_next(next)?;
714        let signed = sign_block(&self.family, &block, &self.challenge, key, &[0u8; 32])?;
715        let mut r = self.add_block(&signed, None, now)?;
716        r.fees = fees;
717        r.claims = claims;
718        Ok((r, signed))
719    }
720}