1use 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
46fn 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
54pub const RULE_GENESIS_DOCUMENT: &str = "sidestr:rule-genesis-document";
62use crate::sighash::verify_taproot_key_path;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct Tip {
67 pub height: u32,
69 pub hash: BlockHash,
71 pub time: u32,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct Applied {
78 pub height: u32,
80 pub hash: BlockHash,
82 pub txs: usize,
84 pub fees: u64,
86 pub claims: usize,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct Submitted {
93 pub txid: Txid,
95 pub fee: u64,
97 pub vsize: u64,
99 pub dup: bool,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct CoinRef {
106 pub outpoint: OutPoint,
108 pub value: u64,
110 pub height: u32,
112 pub coinbase: bool,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct ClaimRequest {
119 pub txid: String,
121 pub vout: u32,
123 pub amount: u64,
125 pub script: ScriptBuf,
127}
128
129#[derive(Debug, Clone, Default)]
131pub struct NextBlock {
132 pub time: u32,
134 pub claims: Vec<ClaimRequest>,
136}
137
138#[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
156pub type State = StateOf<Stock>;
158
159impl<F: HeaderFamily> StateOf<F> {
160 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 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 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 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 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 pub fn add_rule(&mut self, rule: Box<dyn BlockRule<F>>) {
303 self.extra_rules.push(rule);
304 }
305
306 pub fn document(&self) -> &ChainDocument {
308 &self.doc
309 }
310 pub fn family(&self) -> &F {
312 &self.family
313 }
314 pub fn params(&self) -> &Params {
316 &self.params
317 }
318 pub fn bits(&self) -> CompactTarget {
320 self.bits
321 }
322 pub fn challenge(&self) -> &Script {
324 &self.challenge
325 }
326 pub fn federation(&self) -> Option<&Federation> {
328 self.federation.as_ref()
329 }
330 pub fn genesis_hash(&self) -> BlockHash {
332 self.hashes[0]
333 }
334 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 pub fn height(&self) -> u32 {
345 self.tip().height
346 }
347 pub fn hash_at(&self, height: u32) -> Option<BlockHash> {
349 self.hashes.get(height as usize).copied()
350 }
351 pub fn header_at(&self, height: u32) -> Option<&F::Header> {
353 self.headers.get(height as usize)
354 }
355 pub fn utxo(&self) -> &Utxo {
357 &self.utxo
358 }
359 pub fn records(&self) -> &Records {
361 &self.records
362 }
363 pub fn mempool(&self) -> impl Iterator<Item = &Transaction> {
365 self.mempool.iter().map(|(_, tx)| tx)
366 }
367 pub fn claimed(&self, txid: &str, vout: u32) -> bool {
369 self.records.claimed(txid, vout)
370 }
371 pub fn pegouts(&self) -> Vec<Burn> {
373 self.records.pegouts()
374 }
375 pub fn pegout_min(&self) -> u64 {
377 self.doc.pegout_min
378 }
379 pub fn min_fee_rate(&self) -> u64 {
381 self.doc.min_fee_rate
382 }
383 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 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 pub fn vsize(tx: &Transaction) -> u64 {
407 tx.weight().to_wu().div_ceil(4)
408 }
409 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 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 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 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 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 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 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 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 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 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}