1#![doc = "Runtime-independent Handshake block commitments and immutable mining jobs."]
2
3use std::collections::{BTreeMap, HashSet};
4use std::sync::Arc;
5
6use blake2::Blake2bVar;
7use blake2::digest::{Update, VariableOutput};
8use hns_covenants::{Covenant, CovenantKind, MAX_COVENANT_ITEMS, MAX_RESOURCE_SIZE, hash_name};
9use hns_encoding::{Decoder, Encoder};
10use hns_header_consensus::{EXTRA_NONCE_SIZE, HEADER_SIZE, Header, Network};
11use hns_primitives::{
12 BlockHash, BlockTime, CompactTarget, Dollarydoos, Height, MerkleRoot, PowMask, ReservedRoot,
13 TreeRoot, WitnessRoot,
14};
15use hns_transaction::{Address, Input, Outpoint, Transaction, Witness};
16use thiserror::Error;
17
18pub const COIN: u64 = 1_000_000;
19pub const BASE_REWARD: u64 = 2_000 * COIN;
20pub const MAX_MONEY: u64 = 2_040_000_000 * COIN;
21pub const MAX_BLOCK_BASE_SIZE: usize = 1_000_000;
22pub const MAX_BLOCK_WEIGHT: usize = 4_000_000;
23pub const MAX_BLOCK_OPENS: u32 = 300;
24pub const MAX_BLOCK_UPDATES: u32 = 600;
25pub const MAX_BLOCK_RENEWALS: u32 = 600;
26pub const MAX_COVENANT_SIZE: usize = 585;
27pub const MAX_COINBASE_WITNESS_SIZE: usize = 1_000;
28pub const MAX_COINBASE_CLAIM_WITNESS_ITEM_SIZE: usize = 10_000;
29pub const WITNESS_SCALE_FACTOR: usize = 4;
30pub const MAX_PREPARED_JOBS: usize = 16;
31
32pub type MiningGeneration = u64;
33pub type MiningJobId = [u8; 32];
34
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct Block {
37 pub header: Header,
38 pub transactions: Vec<Transaction>,
39}
40
41impl Block {
42 pub fn decode(input: &[u8]) -> Result<Self, MiningError> {
43 if input.len() > MAX_BLOCK_WEIGHT {
44 return Err(MiningError::InvalidBlockBody(
45 "serialized block exceeds allocation bound",
46 ));
47 }
48 let mut decoder = Decoder::new(input);
49 let header = Header::decode(decoder.read_slice(HEADER_SIZE)?)?;
50 let count = decoder.read_compact_usize(MAX_BLOCK_BASE_SIZE, "block transactions")?;
51 let mut transactions = Vec::with_capacity(count.min(1024));
52 for _ in 0..count {
53 transactions.push(Transaction::decode_from(&mut decoder)?);
54 }
55 decoder.finish()?;
56 let block = Self {
57 header,
58 transactions,
59 };
60 validate_block_limits(&block)?;
61 Ok(block)
62 }
63
64 pub fn encode(&self) -> Result<Vec<u8>, MiningError> {
65 let metrics = validate_block_limits(self)?;
66 let mut encoder = Encoder::with_capacity(metrics.serialized_size);
67 encoder.put_bytes(&self.header.encode());
68 encoder.put_compact_size(self.transactions.len() as u64);
69 for transaction in &self.transactions {
70 encoder.put_bytes(&transaction.encode()?);
71 }
72 Ok(encoder.into_bytes())
73 }
74
75 pub fn decode_validated(input: &[u8]) -> Result<Self, MiningError> {
76 let block = Self::decode(input)?;
77 validate_block_body(&block)?;
78 Ok(block)
79 }
80}
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub struct BlockMetrics {
84 pub base_size: usize,
85 pub witness_size: usize,
86 pub serialized_size: usize,
87 pub weight: usize,
88 pub merkle_root: MerkleRoot,
89 pub witness_root: WitnessRoot,
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub struct MiningSnapshot {
94 pub network: Network,
95 pub generation: MiningGeneration,
96 pub tip_hash: BlockHash,
97 pub tip_height: Height,
98 pub tip_time: BlockTime,
99 pub parent_median_time: BlockTime,
100 pub next_tree_root: TreeRoot,
101 pub expected_bits: CompactTarget,
102}
103
104impl MiningSnapshot {
105 pub fn next_height(self) -> Result<Height, MiningError> {
106 self.tip_height
107 .get()
108 .checked_add(1)
109 .map(Height::new)
110 .ok_or(MiningError::ArithmeticOverflow)
111 }
112
113 fn validate(self) -> Result<(), MiningError> {
114 if self.generation == 0 || self.expected_bits.get() == 0 {
115 return Err(MiningError::InvalidSnapshot);
116 }
117 Ok(())
118 }
119}
120
121#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct MiningHeaderTemplate {
123 pub parent_hash: BlockHash,
124 pub tree_root: TreeRoot,
125 pub reserved_root: ReservedRoot,
126 pub witness_root: WitnessRoot,
127 pub merkle_root: MerkleRoot,
128 pub version: u32,
129 pub bits: CompactTarget,
130 pub minimum_time: BlockTime,
131 pub mask_hash: [u8; 32],
132}
133
134impl MiningHeaderTemplate {
135 pub fn from_transactions(
136 snapshot: MiningSnapshot,
137 reserved_root: ReservedRoot,
138 version: u32,
139 minimum_time: BlockTime,
140 mask: PowMask,
141 transactions: &[Transaction],
142 ) -> Result<Self, MiningError> {
143 snapshot.validate()?;
144 if minimum_time <= snapshot.parent_median_time || transactions.is_empty() {
145 return Err(MiningError::InvalidTemplate);
146 }
147 Ok(Self {
148 parent_hash: snapshot.tip_hash,
149 tree_root: snapshot.next_tree_root,
150 reserved_root,
151 witness_root: block_witness_root(transactions)?,
152 merkle_root: block_merkle_root(transactions)?,
153 version,
154 bits: snapshot.expected_bits,
155 minimum_time,
156 mask_hash: mask_hash(snapshot.tip_hash, mask),
157 })
158 }
159}
160
161#[derive(Clone, Debug, Eq, PartialEq)]
162pub struct PreparedMiningJob {
163 job_id: MiningJobId,
164 snapshot_generation: MiningGeneration,
165 network: Network,
166 header: MiningHeaderTemplate,
167 maximum_target_time: Option<BlockTime>,
168 transactions: Arc<[Transaction]>,
169}
170
171impl PreparedMiningJob {
172 pub fn new(
173 snapshot: MiningSnapshot,
174 header: MiningHeaderTemplate,
175 transactions: Arc<[Transaction]>,
176 ) -> Result<Self, MiningError> {
177 snapshot.validate()?;
178 if header.parent_hash != snapshot.tip_hash
179 || header.tree_root != snapshot.next_tree_root
180 || header.minimum_time <= snapshot.parent_median_time
181 || header.bits != snapshot.expected_bits
182 || transactions.is_empty()
183 {
184 return Err(MiningError::InvalidJob);
185 }
186 let provisional = Block {
187 header: Header {
188 time: header.minimum_time,
189 previous_block: header.parent_hash,
190 tree_root: header.tree_root,
191 reserved_root: header.reserved_root,
192 witness_root: header.witness_root,
193 merkle_root: header.merkle_root,
194 version: header.version,
195 bits: header.bits,
196 ..Header::default()
197 },
198 transactions: transactions.to_vec(),
199 };
200 validate_block_body(&provisional)?;
201 if provisional.transactions[0].locktime != snapshot.next_height()?.get() {
202 return Err(MiningError::InvalidCoinbaseHeight);
203 }
204 let parameters = snapshot.network.parameters().pow;
205 let maximum_target_time =
206 (parameters.target_reset && header.bits != parameters.bits).then(|| {
207 BlockTime::new(
208 snapshot
209 .tip_time
210 .get()
211 .saturating_add(u64::from(parameters.target_spacing).saturating_mul(2)),
212 )
213 });
214 if maximum_target_time.is_some_and(|maximum| header.minimum_time > maximum) {
215 return Err(MiningError::InvalidJob);
216 }
217 let job_id = job_id(snapshot, &header, &transactions)?;
218 Ok(Self {
219 job_id,
220 snapshot_generation: snapshot.generation,
221 network: snapshot.network,
222 header,
223 maximum_target_time,
224 transactions,
225 })
226 }
227
228 pub fn prepare(
229 snapshot: MiningSnapshot,
230 reserved_root: ReservedRoot,
231 version: u32,
232 minimum_time: BlockTime,
233 mask: PowMask,
234 transactions: Arc<[Transaction]>,
235 ) -> Result<Self, MiningError> {
236 let header = MiningHeaderTemplate::from_transactions(
237 snapshot,
238 reserved_root,
239 version,
240 minimum_time,
241 mask,
242 &transactions,
243 )?;
244 Self::new(snapshot, header, transactions)
245 }
246
247 pub const fn job_id(&self) -> MiningJobId {
248 self.job_id
249 }
250
251 pub const fn snapshot_generation(&self) -> MiningGeneration {
252 self.snapshot_generation
253 }
254
255 pub const fn header(&self) -> &MiningHeaderTemplate {
256 &self.header
257 }
258
259 pub const fn maximum_target_time(&self) -> Option<BlockTime> {
260 self.maximum_target_time
261 }
262
263 pub fn transactions(&self) -> &[Transaction] {
264 &self.transactions
265 }
266
267 pub fn validate_for_snapshot(&self, snapshot: MiningSnapshot) -> Result<(), MiningError> {
268 snapshot.validate().map_err(|_| MiningError::StaleJob)?;
269 let parameters = snapshot.network.parameters().pow;
270 let expected_maximum = (parameters.target_reset && self.header.bits != parameters.bits)
271 .then(|| {
272 BlockTime::new(
273 snapshot
274 .tip_time
275 .get()
276 .saturating_add(u64::from(parameters.target_spacing).saturating_mul(2)),
277 )
278 });
279 if self.network != snapshot.network
280 || self.snapshot_generation != snapshot.generation
281 || self.header.parent_hash != snapshot.tip_hash
282 || self.header.tree_root != snapshot.next_tree_root
283 || self.header.bits != snapshot.expected_bits
284 || self.header.minimum_time <= snapshot.parent_median_time
285 || self.maximum_target_time != expected_maximum
286 || self.job_id != job_id(snapshot, &self.header, &self.transactions)?
287 {
288 return Err(MiningError::StaleJob);
289 }
290 Ok(())
291 }
292
293 pub fn reconstruct(
294 &self,
295 nonce: u32,
296 time: BlockTime,
297 extra_nonce: [u8; EXTRA_NONCE_SIZE],
298 mask: PowMask,
299 ) -> Result<Block, MiningError> {
300 if time < self.header.minimum_time
301 || self
302 .maximum_target_time
303 .is_some_and(|maximum| time > maximum)
304 || mask_hash(self.header.parent_hash, mask) != self.header.mask_hash
305 {
306 return Err(MiningError::InvalidReconstruction);
307 }
308 let block = Block {
309 header: Header {
310 nonce,
311 time,
312 previous_block: self.header.parent_hash,
313 tree_root: self.header.tree_root,
314 extra_nonce,
315 reserved_root: self.header.reserved_root,
316 witness_root: self.header.witness_root,
317 merkle_root: self.header.merkle_root,
318 version: self.header.version,
319 bits: self.header.bits,
320 mask,
321 },
322 transactions: self.transactions.to_vec(),
323 };
324 validate_block_body(&block).map_err(|_| MiningError::InvalidReconstruction)?;
325 if block.header.mask_hash() != self.header.mask_hash {
326 return Err(MiningError::InvalidReconstruction);
327 }
328 Ok(block)
329 }
330
331 pub fn admit_solution(
332 &self,
333 snapshot: MiningSnapshot,
334 nonce: u32,
335 time: BlockTime,
336 extra_nonce: [u8; EXTRA_NONCE_SIZE],
337 mask: PowMask,
338 ) -> Result<SolvedMiningCandidate, MiningError> {
339 self.validate_for_snapshot(snapshot)?;
340 let block = self.reconstruct(nonce, time, extra_nonce, mask)?;
341 if !block.header.verify_pow() {
342 return Err(MiningError::InsufficientProofOfWork);
343 }
344 Ok(SolvedMiningCandidate {
345 job_id: self.job_id,
346 snapshot_generation: self.snapshot_generation,
347 parent_height: snapshot.tip_height,
348 block,
349 })
350 }
351}
352
353#[derive(Clone, Debug, Eq, PartialEq)]
354pub struct SolvedMiningCandidate {
355 job_id: MiningJobId,
356 snapshot_generation: MiningGeneration,
357 parent_height: Height,
358 block: Block,
359}
360
361impl SolvedMiningCandidate {
362 pub const fn job_id(&self) -> MiningJobId {
363 self.job_id
364 }
365
366 pub const fn snapshot_generation(&self) -> MiningGeneration {
367 self.snapshot_generation
368 }
369
370 pub const fn parent_height(&self) -> Height {
371 self.parent_height
372 }
373
374 pub const fn block(&self) -> &Block {
375 &self.block
376 }
377
378 pub fn into_block(self) -> Block {
379 self.block
380 }
381}
382
383#[derive(Clone, Debug, Default)]
384pub struct PreparedJobSet {
385 jobs: BTreeMap<MiningJobId, Arc<PreparedMiningJob>>,
386}
387
388impl PreparedJobSet {
389 pub fn insert(
390 &mut self,
391 job: PreparedMiningJob,
392 ) -> Result<Arc<PreparedMiningJob>, MiningError> {
393 if let Some(existing) = self.jobs.get(&job.job_id) {
394 if existing.as_ref() == &job {
395 return Ok(Arc::clone(existing));
396 }
397 return Err(MiningError::JobConflict);
398 }
399 if self.jobs.len() >= MAX_PREPARED_JOBS {
400 return Err(MiningError::JobCapacity);
401 }
402 let job = Arc::new(job);
403 self.jobs.insert(job.job_id, Arc::clone(&job));
404 Ok(job)
405 }
406
407 pub fn activate(
408 &mut self,
409 job_id: MiningJobId,
410 snapshot: MiningSnapshot,
411 ) -> Result<Arc<PreparedMiningJob>, MiningError> {
412 let job = self
413 .jobs
414 .get(&job_id)
415 .cloned()
416 .ok_or(MiningError::UnknownJob)?;
417 job.validate_for_snapshot(snapshot)?;
418 self.jobs.retain(|_, candidate| {
419 candidate.snapshot_generation == snapshot.generation
420 && candidate.network == snapshot.network
421 });
422 Ok(job)
423 }
424
425 pub fn retain_generation(&mut self, generation: MiningGeneration) {
426 self.jobs
427 .retain(|_, candidate| candidate.snapshot_generation == generation);
428 }
429
430 pub fn clear(&mut self) {
431 self.jobs.clear();
432 }
433
434 pub fn len(&self) -> usize {
435 self.jobs.len()
436 }
437
438 pub fn is_empty(&self) -> bool {
439 self.jobs.is_empty()
440 }
441}
442
443pub fn block_subsidy(height: Height, halving_interval: u32) -> Result<Dollarydoos, MiningError> {
444 if halving_interval == 0 {
445 return Err(MiningError::InvalidHalvingInterval);
446 }
447 let halvings = height.get() / halving_interval;
448 Ok(Dollarydoos::new(if halvings >= 52 {
449 0
450 } else {
451 BASE_REWARD >> halvings
452 }))
453}
454
455pub const fn halving_interval(network: Network) -> u32 {
456 match network {
457 Network::Regtest => 2_500,
458 Network::Mainnet | Network::Testnet | Network::Simnet => 170_000,
459 }
460}
461
462pub fn create_coinbase(
463 height: Height,
464 generation: MiningGeneration,
465 subsidy: Dollarydoos,
466 fees: Dollarydoos,
467 payout_address: Address,
468 coinbase_flags: Vec<u8>,
469) -> Result<Transaction, MiningError> {
470 payout_address.validate()?;
471 let reward = subsidy.checked_add(fees)?;
472 if reward.get() > MAX_MONEY {
473 return Err(MiningError::OutputValue);
474 }
475 let generation =
476 u32::try_from(generation).map_err(|_| MiningError::GenerationOutOfRange(generation))?;
477 let transaction = Transaction {
478 version: 0,
479 inputs: vec![Input {
480 previous_output: Outpoint::NULL,
481 sequence: generation,
482 witness: Witness {
483 items: vec![coinbase_flags, vec![0; 8], vec![0; 8]],
484 },
485 }],
486 outputs: vec![hns_transaction::Output {
487 value: reward,
488 address: payout_address,
489 covenant: Covenant::default(),
490 }],
491 locktime: height.get(),
492 };
493 validate_transaction_sanity(&transaction)?;
494 Ok(transaction)
495}
496
497pub fn merkle_root(hashes: &[[u8; 32]]) -> [u8; 32] {
498 let sentinel = blake2b_256(&[]);
499 let mut nodes = hashes
500 .iter()
501 .map(|hash| blake2b_256_many(&[&[0], hash]))
502 .collect::<Vec<_>>();
503 if nodes.is_empty() {
504 return sentinel;
505 }
506 while nodes.len() > 1 {
507 let mut next = Vec::with_capacity(nodes.len().div_ceil(2));
508 for pair in nodes.chunks(2) {
509 let right = pair.get(1).unwrap_or(&sentinel);
510 next.push(blake2b_256_many(&[&[1], &pair[0], right]));
511 }
512 nodes = next;
513 }
514 nodes[0]
515}
516
517pub fn block_merkle_root(transactions: &[Transaction]) -> Result<MerkleRoot, MiningError> {
518 let hashes = transactions
519 .iter()
520 .map(|transaction| transaction.transaction_hash().map(|hash| hash.into_bytes()))
521 .collect::<Result<Vec<_>, _>>()?;
522 Ok(MerkleRoot::new(merkle_root(&hashes)))
523}
524
525pub fn block_witness_root(transactions: &[Transaction]) -> Result<WitnessRoot, MiningError> {
526 let hashes = transactions
527 .iter()
528 .map(Transaction::witness_hash)
529 .collect::<Result<Vec<_>, _>>()?;
530 Ok(WitnessRoot::new(merkle_root(&hashes)))
531}
532
533pub fn validate_block_body(block: &Block) -> Result<BlockMetrics, MiningError> {
534 let metrics = validate_block_limits(block)?;
535 if block.transactions.is_empty() {
536 return Err(MiningError::InvalidBlockBody("invalid transaction count"));
537 }
538 if metrics.merkle_root.as_bytes() == &[0; 32] {
539 return Err(MiningError::InvalidBlockBody("zero merkle root"));
540 }
541 if metrics.merkle_root != block.header.merkle_root {
542 return Err(MiningError::InvalidBlockBody("merkle root mismatch"));
543 }
544 if metrics.witness_root != block.header.witness_root {
545 return Err(MiningError::InvalidBlockBody("witness root mismatch"));
546 }
547 if !block.transactions[0].is_coinbase() {
548 return Err(MiningError::InvalidBlockBody(
549 "first transaction is not coinbase",
550 ));
551 }
552 for (index, transaction) in block.transactions.iter().enumerate() {
553 validate_transaction_sanity(transaction)?;
554 if index != 0 && transaction.is_coinbase() {
555 return Err(MiningError::InvalidBlockBody(
556 "block contains multiple coinbase transactions",
557 ));
558 }
559 }
560 validate_block_covenant_limits(block)?;
561 Ok(metrics)
562}
563
564fn validate_block_limits(block: &Block) -> Result<BlockMetrics, MiningError> {
565 if block.transactions.len() > MAX_BLOCK_BASE_SIZE {
566 return Err(MiningError::InvalidBlockBody("invalid transaction count"));
567 }
568 let metrics = block_metrics(&block.transactions)?;
569 if metrics.base_size > MAX_BLOCK_BASE_SIZE {
570 return Err(MiningError::InvalidBlockBody("base size exceeds limit"));
571 }
572 if metrics.weight > MAX_BLOCK_WEIGHT {
573 return Err(MiningError::InvalidBlockBody("weight exceeds limit"));
574 }
575 Ok(metrics)
576}
577
578pub fn block_metrics(transactions: &[Transaction]) -> Result<BlockMetrics, MiningError> {
579 let count_size = compact_size_len(transactions.len() as u64);
580 let mut base_size = HEADER_SIZE
581 .checked_add(count_size)
582 .ok_or(MiningError::ArithmeticOverflow)?;
583 let mut witness_size = 0_usize;
584 for transaction in transactions {
585 base_size = base_size
586 .checked_add(transaction.base_size()?)
587 .ok_or(MiningError::ArithmeticOverflow)?;
588 witness_size = witness_size
589 .checked_add(transaction.witness_encode()?.len())
590 .ok_or(MiningError::ArithmeticOverflow)?;
591 }
592 let serialized_size = base_size
593 .checked_add(witness_size)
594 .ok_or(MiningError::ArithmeticOverflow)?;
595 let weight = base_size
596 .checked_mul(WITNESS_SCALE_FACTOR)
597 .and_then(|base| base.checked_add(witness_size))
598 .ok_or(MiningError::ArithmeticOverflow)?;
599 Ok(BlockMetrics {
600 base_size,
601 witness_size,
602 serialized_size,
603 weight,
604 merkle_root: block_merkle_root(transactions)?,
605 witness_root: block_witness_root(transactions)?,
606 })
607}
608
609pub fn validate_transaction_sanity(transaction: &Transaction) -> Result<(), MiningError> {
610 if transaction.inputs.is_empty() {
611 return Err(MiningError::InvalidTransaction("transaction has no inputs"));
612 }
613 if transaction.outputs.is_empty() {
614 return Err(MiningError::InvalidTransaction(
615 "transaction has no outputs",
616 ));
617 }
618 if transaction.base_size()? > MAX_BLOCK_BASE_SIZE || transaction.weight()? > MAX_BLOCK_WEIGHT {
619 return Err(MiningError::InvalidTransaction(
620 "transaction exceeds consensus size",
621 ));
622 }
623 let name_operations = count_name_operations(transaction);
624 if name_operations.opens > MAX_BLOCK_OPENS {
625 return Err(MiningError::InvalidTransaction(
626 "transaction open limit exceeded",
627 ));
628 }
629 if name_operations.updates > MAX_BLOCK_UPDATES {
630 return Err(MiningError::InvalidTransaction(
631 "transaction update limit exceeded",
632 ));
633 }
634 if name_operations.renewals > MAX_BLOCK_RENEWALS {
635 return Err(MiningError::InvalidTransaction(
636 "transaction renewal limit exceeded",
637 ));
638 }
639 let mut total = 0_u64;
640 for output in &transaction.outputs {
641 output.address.validate()?;
642 total = total
643 .checked_add(output.value.get())
644 .ok_or(MiningError::OutputValue)?;
645 if total > MAX_MONEY {
646 return Err(MiningError::OutputValue);
647 }
648 }
649 if transaction.is_coinbase() {
650 if witness_size(&transaction.inputs[0].witness) > MAX_COINBASE_WITNESS_SIZE {
651 return Err(MiningError::InvalidTransaction(
652 "coinbase witness exceeds limit",
653 ));
654 }
655 for input in transaction.inputs.iter().skip(1) {
656 if !input.previous_output.is_null() {
657 return Err(MiningError::InvalidTransaction(
658 "coinbase claim input is not null",
659 ));
660 }
661 if input.witness.items.len() != 1 {
662 return Err(MiningError::InvalidTransaction(
663 "coinbase claim input must have one witness item",
664 ));
665 }
666 if input.witness.items[0].len() > MAX_COINBASE_CLAIM_WITNESS_ITEM_SIZE {
667 return Err(MiningError::InvalidTransaction(
668 "coinbase claim witness item exceeds limit",
669 ));
670 }
671 }
672 } else {
673 let mut inputs = HashSet::with_capacity(transaction.inputs.len());
674 for input in &transaction.inputs {
675 if input.previous_output.is_null() {
676 return Err(MiningError::InvalidTransaction(
677 "non-coinbase spends null outpoint",
678 ));
679 }
680 if !inputs.insert(input.previous_output) {
681 return Err(MiningError::InvalidTransaction(
682 "transaction contains duplicate inputs",
683 ));
684 }
685 }
686 }
687 if !has_sane_covenants(transaction) {
688 return Err(MiningError::InvalidTransaction(
689 "transaction covenants are structurally invalid",
690 ));
691 }
692 Ok(())
693}
694
695fn validate_block_covenant_limits(block: &Block) -> Result<(), MiningError> {
696 let mut opens = 0_u32;
697 let mut updates = 0_u32;
698 let mut renewals = 0_u32;
699 let mut exclusive_names = HashSet::new();
700 for transaction in &block.transactions {
701 let name_operations = count_name_operations(transaction);
702 opens = opens.saturating_add(name_operations.opens);
703 updates = updates.saturating_add(name_operations.updates);
704 renewals = renewals.saturating_add(name_operations.renewals);
705
706 let mut transaction_exclusive_names = HashSet::new();
709 for output in &transaction.outputs {
710 if matches!(
711 output.covenant.kind,
712 CovenantKind::Claim
713 | CovenantKind::Open
714 | CovenantKind::Register
715 | CovenantKind::Update
716 | CovenantKind::Renew
717 | CovenantKind::Transfer
718 | CovenantKind::Finalize
719 | CovenantKind::Revoke
720 ) {
721 let name_hash: [u8; 32] = output
722 .covenant
723 .item(0)
724 .and_then(|item| item.try_into().ok())
725 .ok_or(MiningError::InvalidBlockBody(
726 "name covenant hash has invalid length",
727 ))?;
728 if exclusive_names.contains(&name_hash) {
729 return Err(MiningError::InvalidBlockBody(
730 "block contains duplicate exclusive name updates",
731 ));
732 }
733 transaction_exclusive_names.insert(name_hash);
734 }
735 }
736 exclusive_names.extend(transaction_exclusive_names);
737 }
738 if opens > MAX_BLOCK_OPENS {
739 return Err(MiningError::InvalidBlockBody("block open limit exceeded"));
740 }
741 if updates > MAX_BLOCK_UPDATES {
742 return Err(MiningError::InvalidBlockBody("block update limit exceeded"));
743 }
744 if renewals > MAX_BLOCK_RENEWALS {
745 return Err(MiningError::InvalidBlockBody(
746 "block renewal limit exceeded",
747 ));
748 }
749 Ok(())
750}
751
752#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
753struct NameOperationCounts {
754 opens: u32,
755 updates: u32,
756 renewals: u32,
757}
758
759fn count_name_operations(transaction: &Transaction) -> NameOperationCounts {
760 let mut counts = NameOperationCounts::default();
761 for output in &transaction.outputs {
762 match output.covenant.kind {
763 CovenantKind::Open => {
764 counts.opens = counts.opens.saturating_add(1);
765 counts.updates = counts.updates.saturating_add(1);
766 }
767 CovenantKind::Claim
768 | CovenantKind::Update
769 | CovenantKind::Transfer
770 | CovenantKind::Revoke => {
771 counts.updates = counts.updates.saturating_add(1);
772 }
773 CovenantKind::Register | CovenantKind::Renew | CovenantKind::Finalize => {
774 counts.renewals = counts.renewals.saturating_add(1);
775 }
776 _ => {}
777 }
778 }
779 counts
780}
781
782fn has_sane_covenants(transaction: &Transaction) -> bool {
783 if transaction.is_coinbase() {
784 if transaction.inputs.len() > transaction.outputs.len() {
785 return false;
786 }
787 for (index, output) in transaction.outputs.iter().enumerate() {
788 match output.covenant.kind {
789 CovenantKind::None => {
790 if !output.covenant.items.is_empty() {
791 return false;
792 }
793 }
794 CovenantKind::Claim => {
795 let items = &output.covenant.items;
796 if index == 0
797 || index >= transaction.inputs.len()
798 || transaction.inputs[index].witness.items.len() != 1
799 || !item_lengths(items, &[32, 4, usize::MAX, 1, 32, 4])
800 || !valid_name_hash(&items[0], &items[2])
801 {
802 return false;
803 }
804 }
805 _ => return false,
806 }
807 }
808 return true;
809 }
810
811 for (index, output) in transaction.outputs.iter().enumerate() {
812 let items = &output.covenant.items;
813 let linked = index < transaction.inputs.len();
814 let sane = match output.covenant.kind {
815 CovenantKind::None => items.is_empty(),
816 CovenantKind::Claim => false,
817 CovenantKind::Open => {
818 item_lengths(items, &[32, 4, usize::MAX])
819 && item_u32(items, 1) == Some(0)
820 && valid_name_hash(&items[0], &items[2])
821 }
822 CovenantKind::Bid => {
823 item_lengths(items, &[32, 4, usize::MAX, 32])
824 && valid_name_hash(&items[0], &items[2])
825 }
826 CovenantKind::Reveal => linked && item_lengths(items, &[32, 4, 32]),
827 CovenantKind::Redeem => linked && item_lengths(items, &[32, 4]),
828 CovenantKind::Register => {
829 linked
830 && item_lengths(items, &[32, 4, usize::MAX, 32])
831 && items[2].len() <= MAX_RESOURCE_SIZE
832 }
833 CovenantKind::Update => {
834 linked
835 && item_lengths(items, &[32, 4, usize::MAX])
836 && items[2].len() <= MAX_RESOURCE_SIZE
837 }
838 CovenantKind::Renew => linked && item_lengths(items, &[32, 4, 32]),
839 CovenantKind::Transfer => {
840 linked
841 && item_lengths(items, &[32, 4, 1, usize::MAX])
842 && items[2][0] <= 31
843 && (2..=40).contains(&items[3].len())
844 }
845 CovenantKind::Finalize => {
846 linked
847 && item_lengths(items, &[32, 4, usize::MAX, 1, 4, 4, 32])
848 && valid_name_hash(&items[0], &items[2])
849 }
850 CovenantKind::Revoke => linked && item_lengths(items, &[32, 4]),
851 CovenantKind::Unknown(_) => {
852 items.len() <= MAX_COVENANT_ITEMS
853 && output
854 .covenant
855 .encode()
856 .is_ok_and(|encoded| encoded.len() <= MAX_COVENANT_SIZE)
857 }
858 };
859 if !sane {
860 return false;
861 }
862 }
863 true
864}
865
866fn item_lengths(items: &[Vec<u8>], expected: &[usize]) -> bool {
867 items.len() == expected.len()
868 && items
869 .iter()
870 .zip(expected)
871 .all(|(item, length)| *length == usize::MAX || item.len() == *length)
872}
873
874fn item_u32(items: &[Vec<u8>], index: usize) -> Option<u32> {
875 Some(u32::from_le_bytes(
876 items.get(index)?.as_slice().try_into().ok()?,
877 ))
878}
879
880fn valid_name_hash(hash: &[u8], name: &[u8]) -> bool {
881 hash_name(name)
882 .map(|expected| expected.as_bytes() == hash)
883 .unwrap_or(false)
884}
885
886fn witness_size(witness: &Witness) -> usize {
887 compact_size_len(witness.items.len() as u64).saturating_add(
888 witness
889 .items
890 .iter()
891 .map(|item| compact_size_len(item.len() as u64).saturating_add(item.len()))
892 .sum::<usize>(),
893 )
894}
895
896fn compact_size_len(value: u64) -> usize {
897 match value {
898 0..=0xfc => 1,
899 0xfd..=0xffff => 3,
900 0x1_0000..=0xffff_ffff => 5,
901 _ => 9,
902 }
903}
904
905fn mask_hash(parent: BlockHash, mask: PowMask) -> [u8; 32] {
906 blake2b_256_many(&[parent.as_bytes(), mask.as_bytes()])
907}
908
909fn job_id(
910 snapshot: MiningSnapshot,
911 header: &MiningHeaderTemplate,
912 transactions: &[Transaction],
913) -> Result<MiningJobId, MiningError> {
914 let mut body = Encoder::new();
915 body.put_u64_le(transactions.len() as u64);
916 for transaction in transactions {
917 let encoded = transaction.encode()?;
918 body.put_u64_le(encoded.len() as u64);
919 body.put_bytes(&encoded);
920 }
921 Ok(blake2b_256_many(&[
922 b"hsrd/mining-job/v1",
923 &[snapshot.network.id()],
924 &snapshot.generation.to_le_bytes(),
925 header.parent_hash.as_bytes(),
926 header.tree_root.as_bytes(),
927 header.reserved_root.as_bytes(),
928 header.witness_root.as_bytes(),
929 header.merkle_root.as_bytes(),
930 &header.version.to_le_bytes(),
931 &header.bits.get().to_le_bytes(),
932 &header.minimum_time.get().to_le_bytes(),
933 &header.mask_hash,
934 &body.into_bytes(),
935 ]))
936}
937
938fn blake2b_256(input: &[u8]) -> [u8; 32] {
939 blake2b_256_many(&[input])
940}
941
942fn blake2b_256_many(parts: &[&[u8]]) -> [u8; 32] {
943 let mut hasher = Blake2bVar::new(32).expect("valid BLAKE2b output length");
944 for part in parts {
945 hasher.update(part);
946 }
947 let mut output = [0; 32];
948 hasher
949 .finalize_variable(&mut output)
950 .expect("valid BLAKE2b output buffer");
951 output
952}
953
954#[derive(Debug, Error)]
955pub enum MiningError {
956 #[error(transparent)]
957 Decode(#[from] hns_encoding::DecodeError),
958 #[error(transparent)]
959 Header(#[from] hns_header_consensus::HeaderError),
960 #[error(transparent)]
961 Transaction(#[from] hns_transaction::TransactionError),
962 #[error(transparent)]
963 Arithmetic(#[from] hns_primitives::ArithmeticError),
964 #[error("mining snapshot is zero, stale, or inconsistent")]
965 InvalidSnapshot,
966 #[error("mining template is inconsistent with its snapshot or body")]
967 InvalidTemplate,
968 #[error("prepared mining job is inconsistent with its snapshot or body")]
969 InvalidJob,
970 #[error("candidate coinbase does not commit the next height")]
971 InvalidCoinbaseHeight,
972 #[error("prepared mining job is stale")]
973 StaleJob,
974 #[error("opened-mask block reconstruction is invalid")]
975 InvalidReconstruction,
976 #[error("opened-mask mining result does not meet the network target")]
977 InsufficientProofOfWork,
978 #[error("prepared mining job ID conflicts with different bytes")]
979 JobConflict,
980 #[error("prepared mining job capacity is exhausted")]
981 JobCapacity,
982 #[error("prepared mining job is unknown")]
983 UnknownJob,
984 #[error("halving interval must be nonzero")]
985 InvalidHalvingInterval,
986 #[error("mining generation {0} cannot be encoded in the coinbase sequence")]
987 GenerationOutOfRange(MiningGeneration),
988 #[error("numeric overflow while building mining data")]
989 ArithmeticOverflow,
990 #[error("transaction output amount is invalid")]
991 OutputValue,
992 #[error("invalid transaction: {0}")]
993 InvalidTransaction(&'static str),
994 #[error("invalid block body: {0}")]
995 InvalidBlockBody(&'static str),
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001
1002 const COINBASE_RAW: &str = "00000000010000000000000000000000000000000000000000000000000000000000000000ffffffff07000000013c943577000000000014090909090909090909090909090909090909090900000b000000030468737264080000000000000000080000000000000000";
1003
1004 fn fixture_coinbase() -> Transaction {
1005 create_coinbase(
1006 Height::new(11),
1007 7,
1008 block_subsidy(Height::new(11), 2_500).unwrap(),
1009 Dollarydoos::new(60),
1010 Address::new(0, vec![9; 20]).unwrap(),
1011 b"hsrd".to_vec(),
1012 )
1013 .unwrap()
1014 }
1015
1016 fn snapshot(generation: u64, marker: u8) -> MiningSnapshot {
1017 MiningSnapshot {
1018 network: Network::Regtest,
1019 generation,
1020 tip_hash: BlockHash::new([marker; 32]),
1021 tip_height: Height::new(10),
1022 tip_time: BlockTime::new(100),
1023 parent_median_time: BlockTime::new(99),
1024 next_tree_root: TreeRoot::new([marker.wrapping_add(1); 32]),
1025 expected_bits: Network::Regtest.parameters().pow.bits,
1026 }
1027 }
1028
1029 fn prepared(snapshot: MiningSnapshot, mask: PowMask) -> PreparedMiningJob {
1030 PreparedMiningJob::prepare(
1031 snapshot,
1032 ReservedRoot::new([3; 32]),
1033 1,
1034 BlockTime::new(101),
1035 mask,
1036 Arc::from(vec![fixture_coinbase()]),
1037 )
1038 .unwrap()
1039 }
1040
1041 #[test]
1042 fn subsidy_and_coinbase_match_hsd_template_fixture() {
1043 let cases = [
1044 (0, 170_000, 2_000_000_000),
1045 (169_999, 170_000, 2_000_000_000),
1046 (170_000, 170_000, 1_000_000_000),
1047 (340_000, 170_000, 500_000_000),
1048 (8_670_000, 170_000, 0),
1049 (2_499, 2_500, 2_000_000_000),
1050 (2_500, 2_500, 1_000_000_000),
1051 (5_000, 2_500, 500_000_000),
1052 (127_500, 2_500, 0),
1053 ];
1054 for (height, interval, expected) in cases {
1055 assert_eq!(
1056 block_subsidy(Height::new(height), interval).unwrap().get(),
1057 expected
1058 );
1059 }
1060 let transaction = fixture_coinbase();
1061 assert_eq!(
1062 transaction.encode().unwrap(),
1063 hex::decode(COINBASE_RAW).unwrap()
1064 );
1065 assert_eq!(
1066 transaction.transaction_hash().unwrap().to_string(),
1067 "34108e299d22a4114526b0d191780ca77f795430b48f1951e3e318731931078a"
1068 );
1069 assert_eq!(
1070 hex::encode(transaction.witness_hash().unwrap()),
1071 "3cd20aa3dd6ede9246e5827cd0c8bd10367dd5da1eb7d29fee04edab473fec4a"
1072 );
1073 assert_eq!(transaction.base_size().unwrap(), 82);
1074 assert_eq!(transaction.witness_encode().unwrap().len(), 24);
1075 assert_eq!(transaction.weight().unwrap(), 352);
1076 assert!(matches!(
1077 create_coinbase(
1078 Height::new(11),
1079 u64::from(u32::MAX) + 1,
1080 Dollarydoos::new(1),
1081 Dollarydoos::new(0),
1082 Address::new(0, vec![9; 20]).unwrap(),
1083 Vec::new(),
1084 ),
1085 Err(MiningError::GenerationOutOfRange(_))
1086 ));
1087 }
1088
1089 #[test]
1090 fn merkle_and_witness_roots_match_hsd_domain_separation() {
1091 let transaction = fixture_coinbase();
1092 assert_eq!(
1093 block_merkle_root(std::slice::from_ref(&transaction))
1094 .unwrap()
1095 .to_string(),
1096 "5a88b64fe244a4b3adbb73e9ae7983e62f66bf154ceea5180efcb7214b11e949"
1097 );
1098 assert_eq!(
1099 block_witness_root(&[transaction]).unwrap().to_string(),
1100 "223cf6dbd896ecb22a504884606f20e0b8d70fda48b65bc8cfb9a6be5f6798b1"
1101 );
1102 assert_eq!(merkle_root(&[]), blake2b_256(&[]));
1103 let hsd_vectors = [
1104 (
1105 1,
1106 "6bf22d230bc6f17e2dc9bdce220e8696630a067ab5029fb66d91e6ecd74c7c54",
1107 ),
1108 (
1109 2,
1110 "e7ee5228698f31758aa7e13445bc54d4c4b37303a90d5ca4677fad9976d1187b",
1111 ),
1112 (
1113 3,
1114 "7511ad764e06d5f02ea56d142da5e920242a39cfc4f4631c2597760d3fc81123",
1115 ),
1116 (
1117 5,
1118 "45148643ac4aa66c59361ab95443c0bdcc49c000d7f1365413de7a9d39be8f1c",
1119 ),
1120 ];
1121 for (count, expected) in hsd_vectors {
1122 let leaves = (1..=count)
1123 .map(|marker| [marker as u8; 32])
1124 .collect::<Vec<_>>();
1125 assert_eq!(hex::encode(merkle_root(&leaves)), expected);
1126 }
1127 }
1128
1129 #[test]
1130 fn immutable_job_reconstructs_and_stale_bindings_fail() {
1131 let current = snapshot(1, 1);
1132 let mask = PowMask::new([9; 32]);
1133 let job = prepared(current, mask);
1134 let block = job
1135 .reconstruct(7, BlockTime::new(101), [8; EXTRA_NONCE_SIZE], mask)
1136 .unwrap();
1137 assert_eq!(block.header.mask_hash(), job.header().mask_hash);
1138 assert_eq!(block.transactions[0].locktime, 11);
1139 assert!(validate_block_body(&block).is_ok());
1140 assert_eq!(
1141 Block::decode_validated(&block.encode().unwrap()).unwrap(),
1142 block
1143 );
1144 assert!(matches!(
1145 job.validate_for_snapshot(snapshot(2, 1)),
1146 Err(MiningError::StaleJob)
1147 ));
1148 assert!(matches!(
1149 job.reconstruct(7, BlockTime::new(100), [8; EXTRA_NONCE_SIZE], mask),
1150 Err(MiningError::InvalidReconstruction)
1151 ));
1152 assert!(matches!(
1153 job.reconstruct(
1154 7,
1155 BlockTime::new(101),
1156 [8; EXTRA_NONCE_SIZE],
1157 PowMask::new([8; 32])
1158 ),
1159 Err(MiningError::InvalidReconstruction)
1160 ));
1161 }
1162
1163 #[test]
1164 fn solution_admission_requires_current_generation_and_pow() {
1165 let current = snapshot(1, 1);
1166 let mask = PowMask::new([9; 32]);
1167 let job = prepared(current, mask);
1168 let mut nonce = 0_u32;
1169 let solved = loop {
1170 match job.admit_solution(
1171 current,
1172 nonce,
1173 BlockTime::new(101),
1174 [7; EXTRA_NONCE_SIZE],
1175 mask,
1176 ) {
1177 Ok(candidate) => break candidate,
1178 Err(MiningError::InsufficientProofOfWork) => {
1179 nonce = nonce.checked_add(1).expect("regtest solution")
1180 }
1181 Err(error) => panic!("unexpected solution error: {error}"),
1182 }
1183 };
1184 assert_eq!(solved.job_id(), job.job_id());
1185 assert_eq!(solved.snapshot_generation(), 1);
1186 assert!(solved.block().header.verify_pow());
1187 }
1188
1189 #[test]
1190 fn testnet_target_reset_time_bounds_prepared_work() {
1191 let mut current = snapshot(1, 1);
1192 current.network = Network::Testnet;
1193 current.expected_bits = CompactTarget::new(0x1d00_fffe);
1194 let mask = PowMask::new([7; 32]);
1195 let transactions: Arc<[Transaction]> = Arc::from(vec![fixture_coinbase()]);
1196 let header = MiningHeaderTemplate::from_transactions(
1197 current,
1198 ReservedRoot::new([3; 32]),
1199 0,
1200 BlockTime::new(101),
1201 mask,
1202 &transactions,
1203 )
1204 .unwrap();
1205 let job = PreparedMiningJob::new(current, header, transactions).unwrap();
1206 let boundary = current
1207 .tip_time
1208 .get()
1209 .saturating_add(u64::from(Network::Testnet.parameters().pow.target_spacing) * 2);
1210 assert_eq!(job.maximum_target_time(), Some(BlockTime::new(boundary)));
1211 assert!(
1212 job.reconstruct(1, BlockTime::new(boundary), [0; EXTRA_NONCE_SIZE], mask)
1213 .is_ok()
1214 );
1215 assert!(matches!(
1216 job.reconstruct(1, BlockTime::new(boundary + 1), [0; EXTRA_NONCE_SIZE], mask),
1217 Err(MiningError::InvalidReconstruction)
1218 ));
1219 }
1220
1221 #[test]
1222 fn body_and_job_identity_fail_closed() {
1223 let current = snapshot(1, 1);
1224 let mask = PowMask::new([9; 32]);
1225 let one = prepared(current, mask);
1226 let mut altered_coinbase = fixture_coinbase();
1227 altered_coinbase.outputs[0].value = Dollarydoos::new(1);
1228 let transactions: Arc<[Transaction]> = Arc::from(vec![altered_coinbase]);
1229 let header = MiningHeaderTemplate::from_transactions(
1230 current,
1231 ReservedRoot::new([3; 32]),
1232 1,
1233 BlockTime::new(101),
1234 mask,
1235 &transactions,
1236 )
1237 .unwrap();
1238 let two = PreparedMiningJob::new(current, header, transactions).unwrap();
1239 assert_ne!(one.job_id(), two.job_id());
1240
1241 let mut invalid = one
1242 .reconstruct(0, BlockTime::new(101), [0; EXTRA_NONCE_SIZE], mask)
1243 .unwrap();
1244 invalid.header.merkle_root = MerkleRoot::new([0; 32]);
1245 assert!(matches!(
1246 validate_block_body(&invalid),
1247 Err(MiningError::InvalidBlockBody("merkle root mismatch"))
1248 ));
1249
1250 let mut jobs = PreparedJobSet::default();
1251 let id = one.job_id();
1252 jobs.insert(one).unwrap();
1253 assert_eq!(jobs.activate(id, current).unwrap().job_id(), id);
1254 assert!(matches!(
1255 jobs.activate(id, snapshot(2, 1)),
1256 Err(MiningError::StaleJob)
1257 ));
1258
1259 let mut claim_coinbase = fixture_coinbase();
1260 claim_coinbase.inputs.push(Input {
1261 previous_output: Outpoint::NULL,
1262 sequence: u32::MAX,
1263 witness: Witness {
1264 items: vec![vec![1, 2, 3]],
1265 },
1266 });
1267 assert!(claim_coinbase.is_coinbase());
1268 assert!(matches!(
1269 validate_transaction_sanity(&claim_coinbase),
1270 Err(MiningError::InvalidTransaction(
1271 "transaction covenants are structurally invalid"
1272 ))
1273 ));
1274 let name = b"claimname";
1275 claim_coinbase.outputs.push(hns_transaction::Output {
1276 value: Dollarydoos::new(0),
1277 address: Address::new(0, vec![7; 20]).unwrap(),
1278 covenant: Covenant {
1279 kind: CovenantKind::Claim,
1280 items: vec![
1281 hash_name(name).unwrap().into_bytes().to_vec(),
1282 1_u32.to_le_bytes().to_vec(),
1283 name.to_vec(),
1284 vec![0],
1285 vec![2; 32],
1286 1_u32.to_le_bytes().to_vec(),
1287 ],
1288 },
1289 });
1290 validate_transaction_sanity(&claim_coinbase).unwrap();
1291 }
1292
1293 #[test]
1294 fn covenant_shapes_and_name_operation_limits_match_hsd() {
1295 let name = b"boundedname";
1296 let name_hash = hash_name(name).unwrap().into_bytes().to_vec();
1297 let input = Input {
1298 previous_output: Outpoint {
1299 transaction_hash: fixture_coinbase().transaction_hash().unwrap(),
1300 index: 0,
1301 },
1302 sequence: u32::MAX,
1303 witness: Witness::default(),
1304 };
1305 let open_output = hns_transaction::Output {
1306 value: Dollarydoos::new(0),
1307 address: Address::new(0, vec![4; 20]).unwrap(),
1308 covenant: Covenant {
1309 kind: CovenantKind::Open,
1310 items: vec![
1311 name_hash.clone(),
1312 0_u32.to_le_bytes().to_vec(),
1313 name.to_vec(),
1314 ],
1315 },
1316 };
1317 let valid_open = Transaction {
1318 version: 0,
1319 inputs: vec![input.clone()],
1320 outputs: vec![open_output.clone()],
1321 locktime: 0,
1322 };
1323 validate_transaction_sanity(&valid_open).unwrap();
1324
1325 let mut wrong_hash = valid_open.clone();
1326 wrong_hash.outputs[0].covenant.items[0][0] ^= 1;
1327 assert!(validate_transaction_sanity(&wrong_hash).is_err());
1328
1329 let too_many_opens = Transaction {
1330 version: 0,
1331 inputs: vec![input],
1332 outputs: vec![open_output; MAX_BLOCK_OPENS as usize + 1],
1333 locktime: 0,
1334 };
1335 assert!(matches!(
1336 validate_transaction_sanity(&too_many_opens),
1337 Err(MiningError::InvalidTransaction(
1338 "transaction open limit exceeded"
1339 ))
1340 ));
1341 }
1342
1343 #[test]
1344 fn exclusive_name_updates_may_repeat_only_within_one_transaction() {
1345 let name_hash = hash_name(b"exclusive").unwrap().into_bytes().to_vec();
1346 let update = |index| Transaction {
1347 version: 0,
1348 inputs: vec![Input {
1349 previous_output: Outpoint {
1350 transaction_hash: fixture_coinbase().transaction_hash().unwrap(),
1351 index,
1352 },
1353 sequence: u32::MAX,
1354 witness: Witness::default(),
1355 }],
1356 outputs: vec![hns_transaction::Output {
1357 value: Dollarydoos::new(0),
1358 address: Address::new(0, vec![5; 20]).unwrap(),
1359 covenant: Covenant {
1360 kind: CovenantKind::Update,
1361 items: vec![name_hash.clone(), 1_u32.to_le_bytes().to_vec(), vec![0]],
1362 },
1363 }],
1364 locktime: 0,
1365 };
1366 let first = update(1);
1367 let second = update(2);
1368 validate_transaction_sanity(&first).unwrap();
1369 validate_transaction_sanity(&second).unwrap();
1370 let header = prepared(snapshot(1, 1), PowMask::new([9; 32]))
1371 .reconstruct(
1372 0,
1373 BlockTime::new(101),
1374 [0; EXTRA_NONCE_SIZE],
1375 PowMask::new([9; 32]),
1376 )
1377 .unwrap()
1378 .header;
1379 let duplicated_across_transactions = Block {
1380 header: header.clone(),
1381 transactions: vec![first.clone(), second],
1382 };
1383 assert!(matches!(
1384 validate_block_covenant_limits(&duplicated_across_transactions),
1385 Err(MiningError::InvalidBlockBody(
1386 "block contains duplicate exclusive name updates"
1387 ))
1388 ));
1389
1390 let mut repeated_within_transaction = first;
1391 repeated_within_transaction
1392 .outputs
1393 .push(repeated_within_transaction.outputs[0].clone());
1394 let permitted = Block {
1395 header,
1396 transactions: vec![repeated_within_transaction],
1397 };
1398 validate_block_covenant_limits(&permitted).unwrap();
1399 }
1400}