1use abtc_domain::primitives::{Block, BlockHeader, Transaction};
26use std::collections::HashMap;
27
28fn siphash_2_4(key0: u64, key1: u64, data: &[u8]) -> u64 {
35 let mut v0: u64 = 0x736f6d6570736575 ^ key0;
37 let mut v1: u64 = 0x646f72616e646f6d ^ key1;
38 let mut v2: u64 = 0x6c7967656e657261 ^ key0;
39 let mut v3: u64 = 0x7465646279746573 ^ key1;
40
41 let blocks = data.len() / 8;
43 for i in 0..blocks {
44 let mut m = 0u64;
45 for j in 0..8 {
46 m |= (data[i * 8 + j] as u64) << (j * 8);
47 }
48 v3 ^= m;
49 for _ in 0..2 {
50 sipround(&mut v0, &mut v1, &mut v2, &mut v3);
51 }
52 v0 ^= m;
53 }
54
55 let mut last = ((data.len() & 0xff) as u64) << 56;
57 let remaining = data.len() % 8;
58 for i in 0..remaining {
59 last |= (data[blocks * 8 + i] as u64) << (i * 8);
60 }
61
62 v3 ^= last;
63 for _ in 0..2 {
64 sipround(&mut v0, &mut v1, &mut v2, &mut v3);
65 }
66 v0 ^= last;
67
68 v2 ^= 0xff;
70 for _ in 0..4 {
71 sipround(&mut v0, &mut v1, &mut v2, &mut v3);
72 }
73
74 v0 ^ v1 ^ v2 ^ v3
75}
76
77#[inline]
78fn sipround(v0: &mut u64, v1: &mut u64, v2: &mut u64, v3: &mut u64) {
79 *v0 = v0.wrapping_add(*v1);
80 *v1 = v1.rotate_left(13);
81 *v1 ^= *v0;
82 *v0 = v0.rotate_left(32);
83 *v2 = v2.wrapping_add(*v3);
84 *v3 = v3.rotate_left(16);
85 *v3 ^= *v2;
86 *v0 = v0.wrapping_add(*v3);
87 *v3 = v3.rotate_left(21);
88 *v3 ^= *v0;
89 *v2 = v2.wrapping_add(*v1);
90 *v1 = v1.rotate_left(17);
91 *v1 ^= *v2;
92 *v2 = v2.rotate_left(32);
93}
94
95pub type ShortTxId = [u8; 6];
99
100fn compute_siphash_keys(header: &BlockHeader, nonce: u64) -> (u64, u64) {
105 use sha2::{Digest, Sha256};
106
107 let header_hash = header.block_hash();
109 let mut hasher = Sha256::new();
110 hasher.update(header_hash.as_bytes());
111 hasher.update(nonce.to_le_bytes());
112 let first_hash = hasher.finalize();
113
114 let mut hasher2 = Sha256::new();
115 hasher2.update(first_hash);
116 let result = hasher2.finalize();
117
118 let key0 = u64::from_le_bytes(result[0..8].try_into().unwrap());
119 let key1 = u64::from_le_bytes(result[8..16].try_into().unwrap());
120
121 (key0, key1)
122}
123
124pub fn compute_short_txid(key0: u64, key1: u64, txid_bytes: &[u8]) -> ShortTxId {
126 let hash = siphash_2_4(key0, key1, txid_bytes);
127 let bytes = hash.to_le_bytes();
128 let mut short_id = [0u8; 6];
129 short_id.copy_from_slice(&bytes[0..6]);
130 short_id
131}
132
133#[derive(Clone, Debug)]
137pub struct PrefilledTransaction {
138 pub index: u16,
140 pub tx: Transaction,
142}
143
144#[derive(Clone, Debug)]
149pub struct CompactBlock {
150 pub header: BlockHeader,
152 pub nonce: u64,
154 pub short_ids: Vec<ShortTxId>,
156 pub prefilled_txs: Vec<PrefilledTransaction>,
158}
159
160#[derive(Clone, Debug)]
162pub struct GetBlockTransactions {
163 pub block_hash: abtc_domain::primitives::BlockHash,
165 pub indices: Vec<u16>,
167}
168
169#[derive(Clone, Debug)]
171pub struct BlockTransactions {
172 pub block_hash: abtc_domain::primitives::BlockHash,
174 pub transactions: Vec<Transaction>,
176}
177
178#[derive(Debug)]
180pub enum ReconstructResult {
181 Success(Block),
183 NeedTransactions(GetBlockTransactions),
185}
186
187impl CompactBlock {
190 pub fn from_block(block: &Block, nonce: u64) -> Self {
195 let (key0, key1) = compute_siphash_keys(&block.header, nonce);
196
197 let mut short_ids = Vec::new();
198 let mut prefilled_txs = Vec::new();
199
200 for (i, tx) in block.transactions.iter().enumerate() {
201 if i == 0 {
202 prefilled_txs.push(PrefilledTransaction {
204 index: 0,
205 tx: tx.clone(),
206 });
207 } else {
208 let txid = tx.txid();
209 let short_id = compute_short_txid(key0, key1, txid.as_bytes());
210 short_ids.push(short_id);
211 }
212 }
213
214 CompactBlock {
215 header: block.header.clone(),
216 nonce,
217 short_ids,
218 prefilled_txs,
219 }
220 }
221
222 pub fn reconstruct(
228 &self,
229 mempool_lookup: &HashMap<ShortTxId, Transaction>,
230 ) -> ReconstructResult {
231 let total_tx_count = self.prefilled_txs.len() + self.short_ids.len();
232 let mut transactions: Vec<Option<Transaction>> = vec![None; total_tx_count];
233 let mut missing_indices = Vec::new();
234
235 let mut prefill_offset = 0u16;
237 for prefill in &self.prefilled_txs {
238 let actual_index = (prefill.index + prefill_offset) as usize;
239 if actual_index < total_tx_count {
240 transactions[actual_index] = Some(prefill.tx.clone());
241 }
242 prefill_offset = prefill.index + prefill_offset + 1;
243 }
244
245 let mut short_id_idx = 0;
247 #[allow(clippy::needless_range_loop)]
248 for i in 0..total_tx_count {
249 if transactions[i].is_some() {
250 continue; }
252
253 if short_id_idx >= self.short_ids.len() {
254 missing_indices.push(i as u16);
255 continue;
256 }
257
258 let short_id = &self.short_ids[short_id_idx];
259 short_id_idx += 1;
260
261 if let Some(tx) = mempool_lookup.get(short_id) {
262 transactions[i] = Some(tx.clone());
263 } else {
264 missing_indices.push(i as u16);
265 }
266 }
267
268 if missing_indices.is_empty() {
269 let txs: Vec<Transaction> = transactions
271 .into_iter()
272 .map(|t| t.expect("all slots filled"))
273 .collect();
274
275 ReconstructResult::Success(Block {
276 header: self.header.clone(),
277 transactions: txs,
278 })
279 } else {
280 ReconstructResult::NeedTransactions(GetBlockTransactions {
281 block_hash: self.header.block_hash(),
282 indices: missing_indices,
283 })
284 }
285 }
286
287 pub fn transaction_count(&self) -> usize {
289 self.prefilled_txs.len() + self.short_ids.len()
290 }
291
292 pub fn build_mempool_lookup<'a, I>(&self, mempool_txs: I) -> HashMap<ShortTxId, Transaction>
297 where
298 I: IntoIterator<Item = &'a Transaction>,
299 {
300 let (key0, key1) = compute_siphash_keys(&self.header, self.nonce);
301 let mut lookup = HashMap::new();
302
303 for tx in mempool_txs {
304 let txid = tx.txid();
305 let short_id = compute_short_txid(key0, key1, txid.as_bytes());
306 lookup.insert(short_id, tx.clone());
307 }
308
309 lookup
310 }
311}
312
313#[cfg(test)]
316mod tests {
317 use super::*;
318 use abtc_domain::primitives::{Amount, BlockHash, Hash256, OutPoint, TxIn, TxOut, Txid};
319 use abtc_domain::script::Script;
320
321 fn make_test_tx(value: i64) -> Transaction {
322 Transaction::v1(
323 vec![TxIn::final_input(
324 OutPoint::new(Txid::zero(), 0),
325 Script::new(),
326 )],
327 vec![TxOut::new(Amount::from_sat(value), Script::new())],
328 0,
329 )
330 }
331
332 fn make_test_block(num_txs: usize) -> Block {
333 let coinbase = Transaction::v1(
334 vec![TxIn::final_input(OutPoint::coinbase(), Script::new())],
335 vec![TxOut::new(Amount::from_sat(50_0000_0000), Script::new())],
336 0,
337 );
338
339 let mut txs = vec![coinbase];
340 for i in 1..=num_txs {
341 txs.push(make_test_tx(i as i64 * 1000));
342 }
343
344 Block {
345 header: BlockHeader {
346 version: 1,
347 prev_block_hash: BlockHash::zero(),
348 merkle_root: Hash256::zero(),
349 time: 1234567890,
350 bits: 0x207fffff,
351 nonce: 0,
352 },
353 transactions: txs,
354 }
355 }
356
357 #[test]
358 fn test_compact_block_from_block() {
359 let block = make_test_block(5);
360 let compact = CompactBlock::from_block(&block, 42);
361
362 assert_eq!(compact.transaction_count(), 6); assert_eq!(compact.prefilled_txs.len(), 1); assert_eq!(compact.short_ids.len(), 5);
365 }
366
367 #[test]
368 fn test_compact_block_roundtrip_all_in_mempool() {
369 let block = make_test_block(3);
370 let compact = CompactBlock::from_block(&block, 42);
371
372 let mempool_txs: Vec<&Transaction> = block.transactions[1..].iter().collect();
374 let lookup = compact.build_mempool_lookup(mempool_txs);
375
376 match compact.reconstruct(&lookup) {
377 ReconstructResult::Success(reconstructed) => {
378 assert_eq!(reconstructed.transactions.len(), 4);
379 for (orig, recon) in block
381 .transactions
382 .iter()
383 .zip(reconstructed.transactions.iter())
384 {
385 assert_eq!(orig.txid(), recon.txid());
386 }
387 }
388 ReconstructResult::NeedTransactions(_) => {
389 panic!("Expected successful reconstruction");
390 }
391 }
392 }
393
394 #[test]
395 fn test_compact_block_missing_transactions() {
396 let block = make_test_block(3);
397 let compact = CompactBlock::from_block(&block, 42);
398
399 let empty_lookup = HashMap::new();
401
402 match compact.reconstruct(&empty_lookup) {
403 ReconstructResult::NeedTransactions(req) => {
404 assert_eq!(req.indices.len(), 3); assert_eq!(req.block_hash, block.header.block_hash());
406 }
407 ReconstructResult::Success(_) => {
408 panic!("Expected missing transactions");
409 }
410 }
411 }
412
413 #[test]
414 fn test_compact_block_partial_mempool() {
415 let block = make_test_block(3);
416 let compact = CompactBlock::from_block(&block, 42);
417
418 let partial_mempool = vec![&block.transactions[1]];
420 let lookup = compact.build_mempool_lookup(partial_mempool);
421
422 match compact.reconstruct(&lookup) {
423 ReconstructResult::NeedTransactions(req) => {
424 assert_eq!(req.indices.len(), 2); }
426 ReconstructResult::Success(_) => {
427 panic!("Expected partial miss");
428 }
429 }
430 }
431
432 #[test]
433 fn test_short_txid_deterministic() {
434 let block = make_test_block(1);
435 let (key0, key1) = compute_siphash_keys(&block.header, 42);
436 let txid = block.transactions[1].txid();
437
438 let id1 = compute_short_txid(key0, key1, txid.as_bytes());
439 let id2 = compute_short_txid(key0, key1, txid.as_bytes());
440 assert_eq!(id1, id2);
441 }
442
443 #[test]
444 fn test_short_txid_different_nonces() {
445 let block = make_test_block(1);
446 let txid = block.transactions[1].txid();
447
448 let (k0a, k1a) = compute_siphash_keys(&block.header, 42);
449 let (k0b, k1b) = compute_siphash_keys(&block.header, 99);
450
451 let id_a = compute_short_txid(k0a, k1a, txid.as_bytes());
452 let id_b = compute_short_txid(k0b, k1b, txid.as_bytes());
453
454 assert_ne!(id_a, id_b);
456 }
457
458 #[test]
459 fn test_short_txid_different_txids() {
460 let block = make_test_block(2);
461 let (key0, key1) = compute_siphash_keys(&block.header, 42);
462
463 let id1 = compute_short_txid(key0, key1, block.transactions[1].txid().as_bytes());
464 let id2 = compute_short_txid(key0, key1, block.transactions[2].txid().as_bytes());
465
466 assert_ne!(id1, id2);
467 }
468
469 #[test]
470 fn test_coinbase_always_prefilled() {
471 let block = make_test_block(10);
472 let compact = CompactBlock::from_block(&block, 0);
473
474 assert!(!compact.prefilled_txs.is_empty());
475 let coinbase_prefill = &compact.prefilled_txs[0];
476 assert_eq!(coinbase_prefill.index, 0);
477 assert_eq!(coinbase_prefill.tx.txid(), block.transactions[0].txid());
478 }
479
480 #[test]
481 fn test_empty_block_compact() {
482 let block = make_test_block(0);
484 let compact = CompactBlock::from_block(&block, 42);
485
486 assert_eq!(compact.short_ids.len(), 0);
487 assert_eq!(compact.prefilled_txs.len(), 1);
488 assert_eq!(compact.transaction_count(), 1);
489
490 let empty = HashMap::new();
492 match compact.reconstruct(&empty) {
493 ReconstructResult::Success(recon) => {
494 assert_eq!(recon.transactions.len(), 1);
495 assert_eq!(recon.transactions[0].txid(), block.transactions[0].txid());
496 }
497 _ => panic!("Empty block should reconstruct without mempool"),
498 }
499 }
500
501 #[test]
502 fn test_siphash_basic() {
503 let result = siphash_2_4(0, 0, b"hello");
505 assert_ne!(result, 0);
506
507 let result2 = siphash_2_4(0, 0, b"hello");
509 assert_eq!(result, result2);
510
511 let result3 = siphash_2_4(0, 0, b"world");
513 assert_ne!(result, result3);
514 }
515}