diff --git a/components/chainhook-event-observer/src/hord/db/mod.rs b/components/chainhook-event-observer/src/hord/db/mod.rs index 7ca02d0..7b7a5ef 100644 --- a/components/chainhook-event-observer/src/hord/db/mod.rs +++ b/components/chainhook-event-observer/src/hord/db/mod.rs @@ -362,7 +362,7 @@ pub fn find_latest_inscription_number_at_block_height( block_height: &u64, inscriptions_db_conn: &Connection, _ctx: &Context, -) -> Result, String> { +) -> Result, String> { let args: &[&dyn ToSql] = &[&block_height.to_sql().unwrap()]; let mut stmt = inscriptions_db_conn .prepare( @@ -373,25 +373,28 @@ pub fn find_latest_inscription_number_at_block_height( .query(args) .map_err(|e| format!("unable to query inscriptions: {}", e.to_string()))?; while let Ok(Some(row)) = rows.next() { - let inscription_number: u64 = row.get(0).unwrap(); + let inscription_number: i64 = row.get(0).unwrap(); return Ok(Some(inscription_number)); } Ok(None) } -pub fn find_latest_inscription_number( +pub fn find_latest_cursed_inscription_number_at_block_height( + block_height: &u64, inscriptions_db_conn: &Connection, _ctx: &Context, -) -> Result, String> { - let args: &[&dyn ToSql] = &[]; +) -> Result, String> { + let args: &[&dyn ToSql] = &[&block_height.to_sql().unwrap()]; let mut stmt = inscriptions_db_conn .prepare( - "SELECT inscription_number FROM inscriptions ORDER BY inscription_number DESC LIMIT 1", + "SELECT inscription_number FROM inscriptions WHERE block_height < ? ORDER BY inscription_number ASC LIMIT 1", ) - .unwrap(); - let mut rows = stmt.query(args).unwrap(); + .map_err(|e| format!("unable to query inscriptions: {}", e.to_string()))?; + let mut rows = stmt + .query(args) + .map_err(|e| format!("unable to query inscriptions: {}", e.to_string()))?; while let Ok(Some(row)) = rows.next() { - let inscription_number: u64 = row.get(0).unwrap(); + let inscription_number: i64 = row.get(0).unwrap(); return Ok(Some(inscription_number)); } Ok(None) @@ -428,7 +431,7 @@ pub fn find_inscription_with_id( while let Ok(Some(row)) = rows.next() { let inscription_block_hash: String = row.get(2).unwrap(); if block_hash.eq(&inscription_block_hash) { - let inscription_number: u64 = row.get(0).unwrap(); + let inscription_number: i64 = row.get(0).unwrap(); let ordinal_number: u64 = row.get(1).unwrap(); let traversal = TraversalResult { inscription_number, @@ -451,7 +454,7 @@ pub fn find_all_inscriptions( let mut results: BTreeMap> = BTreeMap::new(); let mut rows = stmt.query(args).unwrap(); while let Ok(Some(row)) = rows.next() { - let inscription_number: u64 = row.get(0).unwrap(); + let inscription_number: i64 = row.get(0).unwrap(); let ordinal_number: u64 = row.get(1).unwrap(); let block_height: u64 = row.get(2).unwrap(); let transaction_id = { @@ -476,7 +479,7 @@ pub fn find_all_inscriptions( #[derive(Clone, Debug)] pub struct WatchedSatpoint { pub inscription_id: String, - pub inscription_number: u64, + pub inscription_number: i64, pub ordinal_number: u64, pub offset: u64, } @@ -503,7 +506,7 @@ pub fn find_watched_satpoint_for_inscription( .map_err(|e| format!("unable to query inscriptions table: {}", e.to_string()))?; while let Ok(Some(row)) = rows.next() { let inscription_id: String = row.get(0).unwrap(); - let inscription_number: u64 = row.get(1).unwrap(); + let inscription_number: i64 = row.get(1).unwrap(); let ordinal_number: u64 = row.get(2).unwrap(); let offset: u64 = row.get(3).unwrap(); let block_height: u64 = row.get(4).unwrap(); @@ -537,7 +540,7 @@ pub fn find_inscriptions_at_wached_outpoint( .map_err(|e| format!("unable to query inscriptions table: {}", e.to_string()))?; while let Ok(Some(row)) = rows.next() { let inscription_id: String = row.get(0).unwrap(); - let inscription_number: u64 = row.get(1).unwrap(); + let inscription_number: i64 = row.get(1).unwrap(); let ordinal_number: u64 = row.get(2).unwrap(); let offset: u64 = row.get(3).unwrap(); results.push(WatchedSatpoint { @@ -813,7 +816,7 @@ pub async fn fetch_and_cache_blocks_in_hord_db( #[derive(Clone, Debug)] pub struct TraversalResult { - pub inscription_number: u64, + pub inscription_number: i64, pub ordinal_number: u64, pub transfers: u32, } @@ -834,7 +837,7 @@ pub fn retrieve_satoshi_point_using_lazy_storage( blocks_db: &DB, block_identifier: &BlockIdentifier, transaction_identifier: &TransactionIdentifier, - inscription_number: u64, + inscription_number: i64, traversals_cache: Arc< DashMap<(u32, [u8; 8]), LazyBlockTransaction, BuildHasherDefault>, >, diff --git a/components/chainhook-event-observer/src/hord/mod.rs b/components/chainhook-event-observer/src/hord/mod.rs index 4eb4f77..fcb9c75 100644 --- a/components/chainhook-event-observer/src/hord/mod.rs +++ b/components/chainhook-event-observer/src/hord/mod.rs @@ -36,21 +36,23 @@ use crate::{ }; use self::db::{ - find_inscription_with_id, find_latest_inscription_number_at_block_height, - open_readonly_hord_db_conn_rocks_db, remove_entry_from_blocks, remove_entry_from_inscriptions, - LazyBlock, LazyBlockTransaction, TraversalResult, WatchedSatpoint, + find_inscription_with_id, find_latest_cursed_inscription_number_at_block_height, + find_latest_inscription_number_at_block_height, open_readonly_hord_db_conn_rocks_db, + remove_entry_from_blocks, remove_entry_from_inscriptions, LazyBlock, LazyBlockTransaction, + TraversalResult, WatchedSatpoint, }; use self::inscription::InscriptionParser; use self::ord::inscription_id::InscriptionId; -pub fn try_parse_ordinal_operation( +pub fn parse_ordinal_operations( tx: &BitcoinTransactionFullBreakdown, _block_height: u64, _ctx: &Context, -) -> Option { +) -> Vec { // This should eventually become a loop once/if there is settlement on https://github.com/casey/ord/issues/2000. - if let Some(first_input) = tx.vin.get(0) { - if let Some(ref witnesses) = first_input.txinwitness { + let mut operations = vec![]; + for (input_index, input) in tx.vin.iter().enumerate() { + if let Some(ref witnesses) = input.txinwitness { for bytes in witnesses.iter() { let script = Script::from(bytes.to_vec()); let parser = InscriptionParser { @@ -64,7 +66,7 @@ pub fn try_parse_ordinal_operation( let inscription_id = InscriptionId { txid: tx.txid.clone(), - index: 0, + index: input_index as u32, }; let inscription_output_value = tx @@ -85,27 +87,53 @@ pub fn try_parse_ordinal_operation( None }; - return Some(OrdinalOperation::InscriptionRevealed( - OrdinalInscriptionRevealData { - content_type: inscription.content_type().unwrap_or("unknown").to_string(), - content_bytes: format!("0x{}", hex::encode(&inscription_content_bytes)), - content_length: inscription_content_bytes.len(), - inscription_id: inscription_id.to_string(), - inscriber_address, - inscription_output_value, - inscription_fee: 0, - inscription_number: 0, - ordinal_number: 0, - ordinal_block_height: 0, - ordinal_offset: 0, - transfers_pre_inscription: 0, - satpoint_post_inscription: format!("{}:0:0", tx.txid.clone()), - }, - )); + if input_index == 0 { + operations.push(OrdinalOperation::InscriptionRevealed( + OrdinalInscriptionRevealData { + content_type: inscription + .content_type() + .unwrap_or("unknown") + .to_string(), + content_bytes: format!("0x{}", hex::encode(&inscription_content_bytes)), + content_length: inscription_content_bytes.len(), + inscription_id: inscription_id.to_string(), + inscriber_address, + inscription_output_value, + inscription_fee: 0, + inscription_number: 0, + ordinal_number: 0, + ordinal_block_height: 0, + ordinal_offset: 0, + transfers_pre_inscription: 0, + satpoint_post_inscription: format!("{}:0:0", tx.txid.clone()), + }, + )) + } else { + operations.push(OrdinalOperation::CursedInscriptionRevealed( + OrdinalInscriptionRevealData { + content_type: inscription + .content_type() + .unwrap_or("unknown") + .to_string(), + content_bytes: format!("0x{}", hex::encode(&inscription_content_bytes)), + content_length: inscription_content_bytes.len(), + inscription_id: inscription_id.to_string(), + inscriber_address, + inscription_output_value, + inscription_fee: 0, + inscription_number: 0, + ordinal_number: 0, + ordinal_block_height: 0, + ordinal_offset: 0, + transfers_pre_inscription: 0, + satpoint_post_inscription: format!("{}:0:0", tx.txid.clone()), + }, + )) + } } } } - None + operations } pub fn get_inscriptions_revealed_in_block( @@ -135,7 +163,8 @@ pub fn revert_hord_db_with_augmented_bitcoin_block( let tx = &block.transactions[block.transactions.len() - tx_index]; for ordinal_event in tx.metadata.ordinal_operations.iter() { match ordinal_event { - OrdinalOperation::InscriptionRevealed(data) => { + OrdinalOperation::InscriptionRevealed(data) + | OrdinalOperation::CursedInscriptionRevealed(data) => { // We remove any new inscription created remove_entry_from_inscriptions( &data.inscription_id, @@ -376,6 +405,27 @@ pub fn update_storage_and_augment_bitcoin_block_with_inscription_reveal_data( return; } }; + + let mut latest_cursed_inscription_number = + match find_latest_cursed_inscription_number_at_block_height( + &block.block_identifier.index, + &inscription_db_conn, + &ctx, + ) { + Ok(None) => -1, + Ok(Some(inscription_number)) => inscription_number + 1, + Err(e) => { + ctx.try_log(|logger| { + slog::error!( + logger, + "unable to retrieve inscription number: {}", + e.to_string() + ); + }); + return; + } + }; + let mut sats_overflow = vec![]; for new_tx in block.transactions.iter_mut().skip(1) { @@ -384,90 +434,105 @@ pub fn update_storage_and_augment_bitcoin_block_with_inscription_reveal_data( for (ordinal_event_index, ordinal_event) in new_tx.metadata.ordinal_operations.iter_mut().enumerate() { - if let OrdinalOperation::InscriptionRevealed(inscription) = ordinal_event { - let inscription_number = latest_inscription_number; - let traversal = match traversals.get(&new_tx.transaction_identifier) { - Some(traversal) => traversal, - None => { - ctx.try_log(|logger| { - slog::info!( - logger, - "Unable to retrieve cached inscription data for inscription {}", - new_tx.transaction_identifier.hash - ); - }); - ordinals_events_indexes_to_discard.push_front(ordinal_event_index); - continue; - } - }; - inscription.ordinal_offset = traversal.get_ordinal_coinbase_offset(); - inscription.ordinal_block_height = traversal.get_ordinal_coinbase_height(); - inscription.ordinal_number = traversal.ordinal_number; - inscription.inscription_number = traversal.inscription_number; - inscription.transfers_pre_inscription = traversal.transfers; - inscription.inscription_fee = new_tx.metadata.fee; + let (inscription, is_cursed) = match ordinal_event { + OrdinalOperation::InscriptionRevealed(inscription) => (inscription, false), + OrdinalOperation::CursedInscriptionRevealed(inscription) => (inscription, true), + OrdinalOperation::InscriptionTransferred(_) => continue, + }; - match storage { - Storage::Sqlite(rw_hord_db_conn) => { - if traversal.ordinal_number > 0 { - if let Some(_entry) = find_inscription_with_ordinal_number( - &traversal.ordinal_number, - &inscription_db_conn, - &ctx, - ) { - ctx.try_log(|logger| { - slog::warn!( - logger, - "Transaction {} in block {} is overriding an existing inscription {}", - new_tx.transaction_identifier.hash, - block.block_identifier.index, - traversal.ordinal_number - ); - }); - ordinals_events_indexes_to_discard.push_front(ordinal_event_index); - continue; - } - } else { - // If the satoshi inscribed correspond to a sat overflow, we will store the inscription - // but exclude it from the block data - sats_overflow.push(inscription.clone()); + let inscription_number = if is_cursed { + latest_cursed_inscription_number + } else { + latest_inscription_number + }; + + let traversal = match traversals.get(&new_tx.transaction_identifier) { + Some(traversal) => traversal, + None => { + ctx.try_log(|logger| { + slog::info!( + logger, + "Unable to retrieve cached inscription data for inscription {}", + new_tx.transaction_identifier.hash + ); + }); + ordinals_events_indexes_to_discard.push_front(ordinal_event_index); + continue; + } + }; + inscription.ordinal_offset = traversal.get_ordinal_coinbase_offset(); + inscription.ordinal_block_height = traversal.get_ordinal_coinbase_height(); + inscription.ordinal_number = traversal.ordinal_number; + inscription.inscription_number = traversal.inscription_number; + inscription.transfers_pre_inscription = traversal.transfers; + inscription.inscription_fee = new_tx.metadata.fee; + + match storage { + Storage::Sqlite(rw_hord_db_conn) => { + if traversal.ordinal_number > 0 { + if let Some(_entry) = find_inscription_with_ordinal_number( + &traversal.ordinal_number, + &inscription_db_conn, + &ctx, + ) { + ctx.try_log(|logger| { + slog::warn!( + logger, + "Transaction {} in block {} is overriding an existing inscription {}", + new_tx.transaction_identifier.hash, + block.block_identifier.index, + traversal.ordinal_number + ); + }); ordinals_events_indexes_to_discard.push_front(ordinal_event_index); continue; } + } else { + // If the satoshi inscribed correspond to a sat overflow, we will store the inscription + // but exclude it from the block data + sats_overflow.push(inscription.clone()); + ordinals_events_indexes_to_discard.push_front(ordinal_event_index); + continue; + } + + if is_cursed { + latest_cursed_inscription_number -= 1; + } else { latest_inscription_number += 1; - inscription.inscription_number = inscription_number; - ctx.try_log(|logger| { - slog::info!( - logger, - "Inscription {} (#{}) detected on Satoshi {} (block {}, {} transfers)", - inscription.inscription_id, - inscription.inscription_number, - inscription.ordinal_number, - block.block_identifier.index, - inscription.transfers_pre_inscription, - ); - }); - store_new_inscription( - &inscription, - &block.block_identifier, - &rw_hord_db_conn, - &ctx, + }; + + inscription.inscription_number = inscription_number; + ctx.try_log(|logger| { + slog::info!( + logger, + "Inscription {} (#{}) detected on Satoshi {} (block {}, {} transfers)", + inscription.inscription_id, + inscription.inscription_number, + inscription.ordinal_number, + block.block_identifier.index, + inscription.transfers_pre_inscription, ); - } - Storage::Memory(map) => { - let outpoint = inscription.satpoint_post_inscription - [0..inscription.satpoint_post_inscription.len() - 2] - .to_string(); - map.insert( - outpoint, - vec![WatchedSatpoint { - inscription_id: inscription.inscription_id.clone(), - inscription_number: inscription.inscription_number, - ordinal_number: inscription.ordinal_number, - offset: 0, - }], - ); - } + }); + store_new_inscription( + &inscription, + &block.block_identifier, + &rw_hord_db_conn, + &ctx, + ); + } + Storage::Memory(map) => { + let outpoint = inscription.satpoint_post_inscription + [0..inscription.satpoint_post_inscription.len() - 2] + .to_string(); + map.insert( + outpoint, + vec![WatchedSatpoint { + inscription_id: inscription.inscription_id.clone(), + inscription_number: inscription.inscription_number, + ordinal_number: inscription.ordinal_number, + offset: 0, + }], + ); } } } diff --git a/components/chainhook-event-observer/src/indexer/bitcoin/mod.rs b/components/chainhook-event-observer/src/indexer/bitcoin/mod.rs index 2805057..31df685 100644 --- a/components/chainhook-event-observer/src/indexer/bitcoin/mod.rs +++ b/components/chainhook-event-observer/src/indexer/bitcoin/mod.rs @@ -6,6 +6,7 @@ use crate::chainhooks::types::{ get_canonical_pox_config, get_stacks_canonical_magic_bytes, PoxConfig, StacksOpcodes, }; +use crate::hord; use crate::observer::BitcoinConfig; use crate::utils::Context; use bitcoincore_rpc::bitcoin::hashes::hex::FromHex; @@ -23,7 +24,6 @@ use chainhook_types::{ StacksBlockCommitmentData, TransactionIdentifier, TransferSTXData, }; use hiro_system_kit::slog; - use serde::Deserialize; #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] @@ -355,9 +355,7 @@ pub fn standardize_bitcoin_block( let mut ordinal_operations = vec![]; #[cfg(feature = "ordinals")] - if let Some(op) = crate::hord::try_parse_ordinal_operation(&tx, block_height, ctx) { - ordinal_operations.push(op); - } + ordinal_operations.append(&mut hord::parse_ordinal_operations(&tx, block_height, ctx)); let mut inputs = vec![]; let mut sats_in = 0; diff --git a/components/chainhook-types-rs/src/rosetta.rs b/components/chainhook-types-rs/src/rosetta.rs index 01bb78a..63ce6f8 100644 --- a/components/chainhook-types-rs/src/rosetta.rs +++ b/components/chainhook-types-rs/src/rosetta.rs @@ -303,12 +303,13 @@ pub struct BitcoinTransactionMetadata { #[serde(rename_all = "snake_case")] pub enum OrdinalOperation { InscriptionRevealed(OrdinalInscriptionRevealData), + CursedInscriptionRevealed(OrdinalInscriptionRevealData), InscriptionTransferred(OrdinalInscriptionTransferData), } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct OrdinalInscriptionTransferData { - pub inscription_number: u64, + pub inscription_number: i64, pub inscription_id: String, pub ordinal_number: u64, pub updated_address: Option, @@ -322,7 +323,7 @@ pub struct OrdinalInscriptionRevealData { pub content_bytes: String, pub content_type: String, pub content_length: usize, - pub inscription_number: u64, + pub inscription_number: i64, pub inscription_fee: u64, pub inscription_output_value: u64, pub inscription_id: String,