diff --git a/chronik/bitcoinsuite-slp/src/alp/consts.rs b/chronik/bitcoinsuite-slp/src/alp/consts.rs --- a/chronik/bitcoinsuite-slp/src/alp/consts.rs +++ b/chronik/bitcoinsuite-slp/src/alp/consts.rs @@ -4,7 +4,7 @@ //! Module for constants used in ALP. -use crate::structs::LokadId; +use crate::lokad_id::LokadId; /// LOKAD ID of the ALP protocol. /// It was originally intended to be called "SLPv2", but later renamed to ALP to diff --git a/chronik/bitcoinsuite-slp/src/alp/parse.rs b/chronik/bitcoinsuite-slp/src/alp/parse.rs --- a/chronik/bitcoinsuite-slp/src/alp/parse.rs +++ b/chronik/bitcoinsuite-slp/src/alp/parse.rs @@ -16,9 +16,10 @@ ParseError::*, }, consts::{BURN, GENESIS, MINT, SEND}, + lokad_id::LokadId, parsed::{ParsedData, ParsedGenesis, ParsedMintData, ParsedTxType}, slp::consts::SLP_LOKAD_ID, - structs::{Amount, GenesisInfo, LokadId, TokenMeta}, + structs::{Amount, GenesisInfo, TokenMeta}, token_id::TokenId, token_type::{AlpTokenType, TokenType}, }; diff --git a/chronik/bitcoinsuite-slp/src/lib.rs b/chronik/bitcoinsuite-slp/src/lib.rs --- a/chronik/bitcoinsuite-slp/src/lib.rs +++ b/chronik/bitcoinsuite-slp/src/lib.rs @@ -8,6 +8,7 @@ pub mod color; pub mod consts; pub mod empp; + pub mod lokad_id; pub mod parsed; pub mod slp; pub mod structs; diff --git a/chronik/bitcoinsuite-slp/src/lokad_id.rs b/chronik/bitcoinsuite-slp/src/lokad_id.rs new file mode 100644 --- /dev/null +++ b/chronik/bitcoinsuite-slp/src/lokad_id.rs @@ -0,0 +1,289 @@ +// Copyright (c) 2023 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! Module for [`LokadId`] and parsing helpers. + +use std::iter::Chain; + +use bitcoinsuite_core::{ + script::{opcode::OP_RETURN, Op, Script}, + tx::{TxInput, TxMut}, +}; +use bytes::Bytes; + +use crate::empp; + +/// A LOKAD ID is a 4 byte prefix identifying an on-chain protocol: +/// "SLP\0" for SLP +/// "SLP2" for ALP +/// ".xec" for alias +pub type LokadId = [u8; 4]; + +/// Parse the script as `OP_RETURN ...`, or [`None`] otherwise. +fn parse_opreturn_lokad_id(script: &Script) -> Option { + let mut ops = script.iter_ops(); + if ops.next()?.ok()? != Op::Code(OP_RETURN) { + return None; + } + parse_lokad_id_pushop_prefix(&ops.next()?.ok()?) +} + +fn parse_lokad_id_pushop_prefix(op: &Op) -> Option { + match op { + Op::Push(_, data) => LokadId::try_from(data.as_ref()).ok(), + _ => None, + } +} + +/// Parse script as `OP_RETURN OP_RESERVED "..." "..." ...` +#[derive(Debug, Default)] +pub struct EmppLokadIdIter { + pushdata_iter: std::vec::IntoIter, +} + +impl EmppLokadIdIter { + /// Create a new [`EmppLokadIdIter`] from a script. + pub fn new(pushdata: Vec) -> Self { + EmppLokadIdIter { + pushdata_iter: pushdata.into_iter(), + } + } +} + +impl Iterator for EmppLokadIdIter { + type Item = LokadId; + + fn next(&mut self) -> Option { + let pushdata = self.pushdata_iter.next()?; + LokadId::try_from(pushdata.get(..4)?).ok() + } +} + +fn parse_input_script_lokad_id(tx_input: &TxInput) -> Option { + let mut ops = tx_input.script.iter_ops(); + parse_lokad_id_pushop_prefix(&ops.next()?.ok()?) +} + +/// Parse the script as ` ...`, or [`None`] otherwise. +#[derive(Debug, Default)] +pub struct TxInputLokadIdIter<'a> { + inputs_iter: std::slice::Iter<'a, TxInput>, +} + +impl<'a> TxInputLokadIdIter<'a> { + /// Create a new [`TxInputLokadIdIter`]. + pub fn new(tx_inputs: &'a [TxInput]) -> Self { + TxInputLokadIdIter { + inputs_iter: tx_inputs.iter(), + } + } +} + +impl Iterator for TxInputLokadIdIter<'_> { + type Item = LokadId; + + fn next(&mut self) -> Option { + parse_input_script_lokad_id(self.inputs_iter.next()?) + } +} + +/// Return type of [`parse_tx_lokad_ids`]. +pub type LokadIdIter<'a> = Chain< + Chain, EmppLokadIdIter>, + TxInputLokadIdIter<'a>, +>; + +/// Parse all the LOKAD IDs of the tx as an iterator. +/// +/// We allow the following patterns for a LOKAD ID: +/// - `OP_RETURN ...` (first output), where the LOKAD ID is a 4-byte +/// pushdata op. This is the only allowed variant in the original spec, and +/// still kind-of the standard. +/// - `OP_RETURN OP_RESERVED "..." "..." ...` (first +/// output), where the OP_RETURN is encoded as eMPP and every pushop is +/// considered prefixed by a 4-byte LOKAD ID. This is new after the +/// introduction of eMPP. +/// - ` ...` (every input), where any input starting with a 4-byte +/// pushop is interpreted as a LOKAD ID. This allows covenants to easily +/// enforce a LOKAD ID by simply doing ` OP_EQUAL` at the end of all +/// the ops. +pub fn parse_tx_lokad_ids(tx: &TxMut) -> LokadIdIter<'_> { + let opreturn_lokad_id = tx + .outputs + .first() + .and_then(|output| parse_opreturn_lokad_id(&output.script)); + let empp_pushdata = tx + .outputs + .first() + .and_then(|output| empp::parse(&output.script).ok()) + .flatten() + .unwrap_or_default(); + opreturn_lokad_id + .into_iter() + .chain(EmppLokadIdIter::new(empp_pushdata)) + .chain(TxInputLokadIdIter::new(&tx.inputs)) +} + +#[cfg(test)] +mod tests { + use bitcoinsuite_core::{ + hash::ShaRmd160, + script::Script, + tx::{TxInput, TxMut, TxOutput}, + }; + use pretty_assertions::assert_eq; + + use crate::{ + empp, + lokad_id::{ + parse_input_script_lokad_id, parse_opreturn_lokad_id, + parse_tx_lokad_ids, EmppLokadIdIter, LokadId, + }, + }; + + #[test] + fn test_parse_lokad_id_opreturn() { + let parse = parse_opreturn_lokad_id; + let script = |script: &'static [u8]| Script::new(script.into()); + assert_eq!(parse(&Script::default()), None); + assert_eq!(parse(&Script::p2pkh(&ShaRmd160::default())), None); + assert_eq!(parse(&Script::p2sh(&ShaRmd160::default())), None); + assert_eq!(parse(&script(b"\0")), None); + assert_eq!(parse(&script(b"\x04")), None); + assert_eq!(parse(&script(b"\x04abcd")), None); + assert_eq!(parse(&script(b"\x06abcdef")), None); + assert_eq!(parse(&script(b"\x6a")), None); + assert_eq!(parse(&script(b"\x6a\0")), None); + assert_eq!(parse(&script(b"\x6a\x01\x01")), None); + assert_eq!(parse(&script(b"\x6a\x04")), None); + assert_eq!(parse(&script(b"\x6a\x04abc")), None); + assert_eq!(parse(&script(b"\x6a\x04abcd")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x4c\x04abcd")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x4d\x04\0abcd")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x4e\x04\0\0\0abcd")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x04abcdef")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x04abcd\x041234")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x4c\x04abcdef")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x05abcde")), None); + assert_eq!(parse(&script(b"\x6a\x06abcdef")), None); + } + + #[test] + fn test_parse_lokad_id_input() { + let parse = parse_input_script_lokad_id; + let input = |script: &'static [u8]| TxInput { + script: Script::new(script.into()), + ..Default::default() + }; + assert_eq!(parse(&input(b"")), None); + assert_eq!(parse(&input(b"\0")), None); + assert_eq!(parse(&input(b"\x04")), None); + assert_eq!(parse(&input(b"\x04abcd")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x4c\x04abcd")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x4d\x04\0abcd")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x4e\x04\0\0\0abcd")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x04abcdef")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x04abcd\x041234")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x06abcdef")), None); + assert_eq!(parse(&input(b"\x6a\x04abcd")), None); + } + + #[test] + fn test_parse_lokad_id_empp() { + let parse = |script| { + EmppLokadIdIter::new( + empp::parse(&script).ok().flatten().unwrap_or_default(), + ) + .collect::>() + }; + let script = |script: &'static [u8]| Script::new(script.into()); + let empty: Vec = vec![]; + assert_eq!(parse(Script::default()), empty); + assert_eq!(parse(Script::p2pkh(&ShaRmd160::default())), empty); + assert_eq!(parse(Script::p2sh(&ShaRmd160::default())), empty); + assert_eq!(parse(script(b"\0")), empty); + assert_eq!(parse(script(b"\x04")), empty); + assert_eq!(parse(script(b"\x04abcd")), empty); + assert_eq!(parse(script(b"\x06abcdef")), empty); + assert_eq!(parse(script(b"\x6a")), empty); + assert_eq!(parse(script(b"\x6a\0")), empty); + assert_eq!(parse(script(b"\x6a\x01\x01")), empty); + assert_eq!(parse(script(b"\x6a\x50\x04")), empty); + assert_eq!(parse(script(b"\x6a\x50\x04abc")), empty); + assert_eq!(parse(script(b"\x6a\x50\x04abcd")), vec![*b"abcd"]); + assert_eq!(parse(script(b"\x6a\x50\x4c\x04abcd")), vec![*b"abcd"]); + assert_eq!(parse(script(b"\x6a\x50\x4d\x04\0abcd")), vec![*b"abcd"]); + assert_eq!( + parse(script(b"\x6a\x50\x4e\x04\0\0\0abcd")), + vec![*b"abcd"] + ); + assert_eq!(parse(script(b"\x6a\x50\x04abcdef")), empty); + assert_eq!( + parse(script(b"\x6a\x50\x04abcd\x041234")), + vec![*b"abcd", *b"1234"] + ); + assert_eq!(parse(script(b"\x6a\x50\x4c\x04abcdef")), empty); + assert_eq!(parse(script(b"\x6a\x50\x04abcd")), vec![*b"abcd"]); + assert_eq!(parse(script(b"\x6a\x50\x05abcde")), vec![*b"abcd"]); + assert_eq!(parse(script(b"\x6a\x50\x06abcdef")), vec![*b"abcd"]); + } + + #[test] + fn test_parse_tx_lokad_ids() { + let script = |script: &[u8]| Script::new(script.to_vec().into()); + assert_eq!( + parse_tx_lokad_ids(&TxMut { + version: 1, + inputs: vec![ + TxInput { + script: script(b"\x04abcd"), + ..Default::default() + }, + TxInput { + script: script(b"\x4c\x041234"), + ..Default::default() + }, + TxInput { + // ignored if first pushop is not 4 bytes + script: script( + &[b"\x41".as_ref(), &[0x41; 5], b"\x04xxxx"] + .concat(), + ), + ..Default::default() + }, + ], + outputs: vec![ + TxOutput { + script: script(b"\x6a\x046789\x04yyyy"), + value: 0, + }, + // Ignored: OP_RETURN must be first + TxOutput { + script: script(b"\x6a\x04zzzz"), + value: 0, + }, + ], + locktime: 0, + }) + .collect::>(), + vec![*b"6789", *b"abcd", *b"1234"], + ); + assert_eq!( + parse_tx_lokad_ids(&TxMut { + version: 1, + inputs: vec![TxInput { + script: script(b"\x4d\x04\0efgh"), + ..Default::default() + }], + outputs: vec![TxOutput { + script: script(b"\x6a\x50\x046789\x044321"), + value: 0, + }], + locktime: 0, + }) + .collect::>(), + vec![*b"6789", *b"4321", *b"efgh"], + ); + } +} diff --git a/chronik/bitcoinsuite-slp/src/slp/consts.rs b/chronik/bitcoinsuite-slp/src/slp/consts.rs --- a/chronik/bitcoinsuite-slp/src/slp/consts.rs +++ b/chronik/bitcoinsuite-slp/src/slp/consts.rs @@ -4,7 +4,7 @@ //! Module for constants used in SLP. -use crate::structs::LokadId; +use crate::lokad_id::LokadId; /// LOKAD ID of SLP is "SLP\0" pub const SLP_LOKAD_ID: LokadId = *b"SLP\0"; diff --git a/chronik/bitcoinsuite-slp/src/structs.rs b/chronik/bitcoinsuite-slp/src/structs.rs --- a/chronik/bitcoinsuite-slp/src/structs.rs +++ b/chronik/bitcoinsuite-slp/src/structs.rs @@ -10,12 +10,6 @@ use crate::{token_id::TokenId, token_type::TokenType}; -/// A LOKAD ID is a 4 byte prefix identifying the protocol: -/// "SLP\0" for SLP -/// "SLP2" for ALP -/// ".xec" for alias -pub type LokadId = [u8; 4]; - /// SLP or ALP amount pub type Amount = u64;