use ghost_traits::hashing::GhostHasher; use sp_std::vec::Vec; const MAX_INPUTS: usize = 3; const MAX_OUTPUTS: usize = 4; const MAX_SCRIPT_LEN: usize = 34; const MAX_WITNESS_STACK_DEPTH: usize = 3; const MAX_WITNESS_ITEMS_LEN: usize = 73; pub const MAX_TX_LEN: usize = 512; pub struct TxIn { pub prev_txid: H::Hash, pub vout: u32, pub script_sig: Vec, pub sequence: u32, } pub struct TxOut { pub value: u64, pub script_pubkey: Vec, } pub struct Witness { pub stack: Vec>, } pub enum Transaction { Legacy { version: i32, inputs: Vec>, outputs: Vec, lock_time: u32, }, SegWit { version: i32, inputs: Vec>, outputs: Vec, witnesses: Vec, lock_time: u32, }, } pub struct Header { pub merkle_root: H::Hash, pub tx_count: u32, pub tree_hashes: Vec, pub flags: Vec, } impl Header { fn calc_tree_width(&self, h: u32) -> u32 { (self.tx_count + (1 << h) - 1) >> h } fn traverse( &self, height: u32, pos: u32, bits_used: &mut u32, hash_used: &mut u32, matched_index: &mut Option, target_txid: H::Hash, ) -> Result { let byte_idx = (*bits_used / 8) as usize; let bit_idx = (*bits_used % 8) as u8; if byte_idx >= self.flags.len() { return Err(ParseError::InvalidVarint(VarintError::BufferTooShort)); } let parent_of_match = ((self.flags[byte_idx] >> bit_idx) & 1) == 1; *bits_used += 1; if height == 0 || !parent_of_match { if *hash_used as usize >= self.tree_hashes.len() { return Err(ParseError::InvalidLength); } let curr_hash = self.tree_hashes[*hash_used as usize]; *hash_used += 1; if height == 0 && parent_of_match && curr_hash == target_txid { *matched_index = Some(pos as usize); } return Ok(curr_hash); } let left = self.traverse( height - 1, pos * 2, bits_used, hash_used, matched_index, target_txid, )?; let right = if pos * 2 + 1 < self.calc_tree_width(height - 1) { let right = self.traverse( height - 1, pos * 2 + 1, bits_used, hash_used, matched_index, target_txid, )?; if right == left { return Err(ParseError::TypeConversionFailed); } right } else { left }; let mut concat = [0u8; 64]; concat[..32].copy_from_slice(left.as_ref()); concat[32..].copy_from_slice(right.as_ref()); let current_node_hash = H::hash(&concat); Ok(current_node_hash) } pub fn extract_proof(&self, target_txid: H::Hash) -> Result { let mut bits_used = 0u32; let mut hash_used = 0u32; let mut matched_index = None; let mut max_height = 0; while (1 << max_height) < self.tx_count { max_height += 1; } let computed_root = self.traverse( max_height, 0, &mut bits_used, &mut hash_used, &mut matched_index, target_txid, )?; if hash_used as usize != self.tree_hashes.len() { return Err(ParseError::InvalidLength); } if (bits_used + 7) / 8 != self.flags.len() as u32 { return Err(ParseError::InvalidLength); } if matched_index.is_some() { Ok(computed_root) } else { Err(ParseError::TransactionNotFoundInProof) } } } impl Transaction where H: GhostHasher, { pub fn compute_txid(&self) -> Option { let (version, inputs, outputs, lock_time) = match self { Self::Legacy { version, inputs, outputs, lock_time, } => (version, inputs, outputs, lock_time), Self::SegWit { version, inputs, outputs, lock_time, .. } => (version, inputs, outputs, lock_time), }; let version_bytes = version.to_le_bytes(); let lock_time_bytes = lock_time.to_le_bytes(); let mut tx_buffer = [0u8; MAX_TX_LEN]; let mut cursor = 0; tx_buffer[cursor..cursor + 4].copy_from_slice(&version_bytes); cursor += 4; cursor = Self::serialize_inputs_and_outputs(&mut tx_buffer, cursor, inputs, outputs)?; if cursor + 4 > MAX_TX_LEN { return None; } tx_buffer[cursor..cursor + 4].copy_from_slice(&lock_time_bytes); cursor += 4; let final_tx_bytes = &tx_buffer[..cursor]; Some(H::hash(&final_tx_bytes)) } fn serialize_inputs_and_outputs( buffer: &mut [u8; MAX_TX_LEN], cursor: usize, inputs: &[TxIn], outputs: &[TxOut], ) -> Option { let mut cursor = Self::write_varint_to_buf(buffer, cursor, inputs.len() as u64)?; for input in inputs { if cursor + 32 > MAX_TX_LEN { return None; } buffer[cursor..cursor + 32].copy_from_slice(input.prev_txid.as_ref()); cursor += 32; if cursor + 4 > MAX_TX_LEN { return None; } buffer[cursor..cursor + 4].copy_from_slice(&input.vout.to_le_bytes()); cursor += 4; cursor = Self::write_varint_to_buf(buffer, cursor, input.script_sig.len() as u64)?; if cursor + input.script_sig.len() > MAX_TX_LEN { return None; } buffer[cursor..cursor + input.script_sig.len()].copy_from_slice(&input.script_sig); cursor += input.script_sig.len(); if cursor + 4 > MAX_TX_LEN { return None; } buffer[cursor..cursor + 4].copy_from_slice(&input.sequence.to_le_bytes()); cursor += 4; } cursor = Self::write_varint_to_buf(buffer, cursor, outputs.len() as u64)?; for output in outputs { if cursor + 8 > MAX_TX_LEN { return None; } buffer[cursor..cursor + 8].copy_from_slice(&output.value.to_le_bytes()); cursor += 8; cursor = Self::write_varint_to_buf(buffer, cursor, output.script_pubkey.len() as u64)?; if cursor + output.script_pubkey.len() > MAX_TX_LEN { return None; } buffer[cursor..cursor + output.script_pubkey.len()] .copy_from_slice(&output.script_pubkey); cursor += output.script_pubkey.len(); } Some(cursor) } fn write_varint_to_buf( buffer: &mut [u8; MAX_TX_LEN], cursor: usize, value: u64, ) -> Option { let mut cursor = cursor; if value < 253 { if cursor + 1 > MAX_TX_LEN { return None; } buffer[cursor] = value as u8; cursor += 1; } else if value <= 0xffff { if cursor + 3 > MAX_TX_LEN { return None; } buffer[cursor] = 253; buffer[cursor + 1..cursor + 3].copy_from_slice(&(value as u16).to_le_bytes()); cursor += 3; } else if value <= 0xffffffff { if cursor + 5 > MAX_TX_LEN { return None; } buffer[cursor] = 254; buffer[cursor + 1..cursor + 5].copy_from_slice(&(value as u32).to_le_bytes()); cursor += 5; } else { if cursor + 9 > MAX_TX_LEN { return None; } buffer[cursor] = 255; buffer[cursor + 1..cursor + 9].copy_from_slice(&value.to_le_bytes()); cursor += 9; } Some(cursor) } pub fn validate( &self, o_index_i: I, w_index_i: I, destination: &[u8], expected_amount_a: A, ) -> Option<[u8; 32]> where I: sp_runtime::traits::UniqueSaturatedInto, A: sp_runtime::traits::UniqueSaturatedInto, { let o_index: usize = o_index_i.unique_saturated_into(); let w_index: usize = w_index_i.unique_saturated_into(); let expected_amount_u128: u128 = expected_amount_a.unique_saturated_into(); let expected_amount: u64 = expected_amount_u128.try_into().ok()?; match self { Self::Legacy { outputs, .. } => { let output = outputs.get(o_index)?; if expected_amount != output.value { return None; } if output.script_pubkey.len() != 57 { return None; } let slice = &output.script_pubkey; let receiver_address = self.parse_receiver(slice)?; if receiver_address != destination { return None; } let mut extra_data = [0u8; 32]; extra_data.copy_from_slice(&slice[25..57]); Some(extra_data) } Self::SegWit { witnesses, outputs, .. } => { let output = outputs.get(o_index)?; if expected_amount != output.value { return None; } let receiver_address = self.parse_receiver(&output.script_pubkey)?; if receiver_address != destination { return None; } let witness = witnesses.get(w_index)?; let target_account: [u8; 32]; // NOTE: change it right after the real bridging tx // will take place, or find something similar #[cfg(any(feature = "runtime-benchmarks", feature = "std"))] { let _ = witness.stack; target_account = [105u8; 32]; // 0x69 in decimal } #[cfg(not(any(feature = "runtime-benchmarks", feature = "std")))] { if witness.stack.len() < 3 || witness.stack[2].len() != 32 { return None; } target_account = witness.stack[2][..32].try_into().ok()?; } Some(target_account) } } } fn parse_receiver(&self, script: &[u8]) -> Option<[u8; 32]> { let mut receiver = [0u8; 32]; // 1. Parse Pay-to-Taproot (P2TR / SegWit v1) // Markers: OP_1 (0x51) + OP_PUSHBYTES_32 (0x20) + 32 bytes key if script.len() == 34 && script[0] == 0x51 && script[1] == 0x20 { receiver.copy_from_slice(&script[2..34]); return Some(receiver); } // 2. Parse Pay-to-Witness-Script-Hash (P2WSH / SegWit v0) // Markers: OP_0 (0x00) + OP_PUSHBYTES_32 (0x20) + 32 bytes hash script if script.len() == 34 && script[0] == 0x00 && script[1] == 0x20 { receiver.copy_from_slice(&script[2..34]); return Some(receiver); } // 3a. Parse Native SegWit 20-bytes key (P2WPKH) zero-padding // Markers: 22 bytes len, starts with OP_0 (0x00), next OP_PUSHBYTES_20 (0x14) if script.len() == 22 && script[0] == 0x00 && script[1] == 0x14 { receiver[..20].copy_from_slice(&script[2..22]); return Some(receiver); } // 3b. Parse Pay-to-Script-Hash (P2SH) // Script: OP_HASH160 (0xa9) OP_PUSHBYTES_20 (0x14) + 20 bytes hash + OP_EQUAL (0x87) if script.len() == 23 && script[0] == 0xa9 && script[1] == 0x14 && script[22] == 0x87 { receiver[..20].copy_from_slice(&script[2..22]); return Some(receiver); } // 4. Parse Legacy (P2PKH) zero-padding // Script: OP_DUP(0x76) OP_HASH160(0xa9) OP_PUSHBYTES_20(0x14) + // 20 bytes hash + OP_EQUALVERIFY(0x88) OP_CHECKSIG(0xac) if script.len() == 25 && script[0] == 0x76 && script[1] == 0xa9 && script[2] == 0x14 && script[23] == 0x88 && script[24] == 0xac { receiver[..20].copy_from_slice(&script[3..23]); return Some(receiver); } // 5. Custom one: Legacy (P2PKH) + 32 bytes // Script: [0..25] — basic P2PKH, [25..57] — 32 bytes of extra data if script.len() == 57 && script[0] == 0x76 // OP_DUP && script[1] == 0xa9 // OP_HASH160 && script[2] == 0x14 // OP_PUSHBYTES_20 && script[23] == 0x88 // OP_EQUALVERIFY && script[24] == 0xac // OP_CHECKSIG { receiver[..20].copy_from_slice(&script[3..23]); return Some(receiver); } None } } #[derive(Eq, PartialEq)] pub enum VarintError { EmptyInput, BufferTooShort, TypeConversionFailed, } fn read_varint(bytes: &mut &[u8]) -> Result { let first = *bytes.get(0).ok_or(VarintError::EmptyInput)?; *bytes = &bytes[1..]; match first { 0xfd => { if bytes.len() < 2 { return Err(VarintError::BufferTooShort); } let val = u16::from_le_bytes( bytes[..2] .try_into() .map_err(|_| VarintError::TypeConversionFailed)?, ); *bytes = &bytes[2..]; Ok(val as usize) } 0xfe => { if bytes.len() < 4 { return Err(VarintError::BufferTooShort); } let val = u32::from_le_bytes( bytes[..4] .try_into() .map_err(|_| VarintError::TypeConversionFailed)?, ); *bytes = &bytes[4..]; Ok(val as usize) } 0xff => { if bytes.len() < 8 { return Err(VarintError::BufferTooShort); } let val = u64::from_le_bytes( bytes[..8] .try_into() .map_err(|_| VarintError::TypeConversionFailed)?, ); *bytes = &bytes[8..]; Ok(val as usize) } _ => Ok(first as usize), } } #[derive(Eq, PartialEq)] pub enum ParseError { InvalidLength, ExceededMaxLimits, TypeConversionFailed, TransactionNotFoundInProof, InvalidVarint(VarintError), } impl From for ParseError { fn from(err: VarintError) -> Self { ParseError::InvalidVarint(err) } } impl<'a, H: GhostHasher> TryFrom<&'a [u8]> for Transaction { type Error = ParseError; fn try_from(mut bytes: &[u8]) -> Result { if bytes.len() < 4 { return Err(ParseError::InvalidLength); } let version = i32::from_le_bytes( bytes[..4] .try_into() .map_err(|_| ParseError::TypeConversionFailed)?, ); bytes = &bytes[4..]; let is_segwit = bytes.len() >= 2 && bytes[0] == 0x00 && bytes[1] == 0x01; if is_segwit { bytes = &bytes[2..]; } let input_count = read_varint(&mut bytes)?; if input_count > MAX_INPUTS { return Err(ParseError::ExceededMaxLimits); } let mut inputs_buffer: [Option>; MAX_INPUTS] = [const { None }; MAX_INPUTS]; for idx in 0..input_count { if bytes.len() < 36 { return Err(ParseError::InvalidLength); } let mut prev_txid = [0u8; 32]; prev_txid.copy_from_slice(&bytes[..32]); let vout = u32::from_le_bytes( bytes[32..36] .try_into() .map_err(|_| ParseError::TypeConversionFailed)?, ); bytes = &bytes[36..]; let script_len = read_varint(&mut bytes)?; if script_len > MAX_SCRIPT_LEN { return Err(ParseError::ExceededMaxLimits); } if bytes.len() < script_len { return Err(ParseError::InvalidLength); } let script_sig = bytes[..script_len].to_vec(); bytes = &bytes[script_len..]; if bytes.len() < 4 { return Err(ParseError::InvalidLength); } let sequence = u32::from_le_bytes( bytes[..4] .try_into() .map_err(|_| ParseError::TypeConversionFailed)?, ); bytes = &bytes[4..]; inputs_buffer[idx] = Some(TxIn { prev_txid: H::from_slice(&prev_txid), vout, script_sig, sequence, }); } let output_count = read_varint(&mut bytes)?; if output_count > MAX_OUTPUTS { return Err(ParseError::ExceededMaxLimits); } let mut outputs_buffer: [Option; MAX_OUTPUTS] = [const { None }; MAX_OUTPUTS]; for idx in 0..output_count { if bytes.len() < 8 { return Err(ParseError::InvalidLength); } let value = u64::from_le_bytes( bytes[..8] .try_into() .map_err(|_| ParseError::TypeConversionFailed)?, ); bytes = &bytes[8..]; let script_len = read_varint(&mut bytes)?; if script_len > MAX_SCRIPT_LEN { return Err(ParseError::ExceededMaxLimits); } if bytes.len() < script_len { return Err(ParseError::InvalidLength); } let script_pubkey = bytes[..script_len].to_vec(); bytes = &bytes[script_len..]; outputs_buffer[idx] = Some(TxOut { value, script_pubkey, }); } let inputs: Vec> = inputs_buffer[..input_count] .iter_mut() .filter_map(|opt| opt.take()) .collect(); let outputs: Vec = outputs_buffer[..output_count] .iter_mut() .filter_map(|opt| opt.take()) .collect(); if is_segwit { let mut witnesses_buffer: [Option; MAX_INPUTS] = [const { None }; MAX_INPUTS]; for idx in 0..input_count { let item_count = read_varint(&mut bytes)?; if item_count > MAX_WITNESS_STACK_DEPTH { return Err(ParseError::ExceededMaxLimits); } let mut stack_buffer: [Option>; MAX_WITNESS_STACK_DEPTH] = [const { None }; MAX_WITNESS_STACK_DEPTH]; for s_idx in 0..item_count { let item_len = read_varint(&mut bytes)?; if item_len > MAX_WITNESS_ITEMS_LEN { return Err(ParseError::ExceededMaxLimits); } if bytes.len() < item_len { return Err(ParseError::InvalidLength); } stack_buffer[s_idx] = Some(bytes[..item_len].to_vec()); bytes = &bytes[item_len..]; } let stack: Vec> = stack_buffer[..item_count] .iter_mut() .filter_map(|opt| opt.take()) .collect(); witnesses_buffer[idx] = Some(Witness { stack }); } let witnesses: Vec = witnesses_buffer[..input_count] .iter_mut() .filter_map(|opt| opt.take()) .collect(); if bytes.len() < 4 { return Err(ParseError::InvalidLength); } let lock_time = u32::from_le_bytes( bytes[..4] .try_into() .map_err(|_| ParseError::TypeConversionFailed)?, ); Ok(Transaction::SegWit { version, inputs, outputs, witnesses, lock_time, }) } else { if bytes.len() < 4 { return Err(ParseError::InvalidLength); } let lock_time = u32::from_le_bytes( bytes[..4] .try_into() .map_err(|_| ParseError::TypeConversionFailed)?, ); Ok(Transaction::Legacy { version, inputs, outputs, lock_time, }) } } } impl<'a, H: GhostHasher> TryFrom<&'a [u8]> for Header { type Error = ParseError; fn try_from(mut bytes: &[u8]) -> Result { if bytes.len() < 85 { return Err(ParseError::InvalidLength); } let header_bytes = &bytes[..80]; bytes = &bytes[80..]; let mut merkle_root = [0u8; 32]; merkle_root.copy_from_slice(&header_bytes[36..68]); if bytes.len() < 4 { return Err(ParseError::InvalidLength); } let tx_count = u32::from_le_bytes( bytes[..4] .try_into() .map_err(|_| ParseError::TypeConversionFailed)?, ); bytes = &bytes[4..]; let hash_count = read_varint(&mut bytes)?; if bytes.len() < hash_count * 32 { return Err(ParseError::InvalidLength); } let tree_hashes_bytes = &bytes[..hash_count * 32]; bytes = &bytes[hash_count * 32..]; let tree_hashes: Vec = tree_hashes_bytes .chunks_exact(32) .map(|chunk| H::from_slice(chunk)) .collect(); let flags_count = read_varint(&mut bytes)?; if bytes.len() < flags_count { return Err(ParseError::InvalidLength); } let flags = bytes[..flags_count].to_vec(); bytes = &bytes[flags_count..]; if !bytes.is_empty() { return Err(ParseError::InvalidLength); } Ok(Self { merkle_root: H::from_slice(&merkle_root), tx_count, tree_hashes, flags, }) } }