Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ordinals): track multiple sat transfers in the same block correctly #460

Merged
merged 2 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 29 additions & 21 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"ordinals",
"service",
"start",
"--config-path=${workspaceFolder}/.vscode/Indexer.toml",
"--config-path=${workspaceFolder}/.vscode/Indexer.toml"
],
"cwd": "${workspaceFolder}"
},
Expand All @@ -35,30 +35,42 @@
"runes",
"service",
"start",
"--config-path=${workspaceFolder}/.vscode/Indexer.toml",
"--config-path=${workspaceFolder}/.vscode/Indexer.toml"
],
"cwd": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "run: ordinals-api",
"cwd": "${workspaceFolder}/api/ordinals",
"runtimeArgs": ["-r", "ts-node/register"],
"args": ["${workspaceFolder}/api/ordinals/src/index.ts"],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"envFile": "${workspaceFolder}/api/ordinals/.env",
"env": {
"NODE_ENV": "development",
"TS_NODE_SKIP_IGNORE": "true"
},
"killBehavior": "polite"
},
{
"type": "node",
"request": "launch",
"name": "test: ordinals-api",
"program": "${workspaceFolder}/api/ordinals/node_modules/jest/bin/jest",
"cwd": "${workspaceFolder}/api/ordinals/",
"args": [
"--testTimeout=3600000",
"--runInBand",
"--no-cache"
],
"args": ["--testTimeout=3600000", "--runInBand", "--no-cache"],
"outputCapture": "std",
"console": "integratedTerminal",
"preLaunchTask": "npm: testenv:run",
"postDebugTask": "npm: testenv:stop",
"env": {
"PGHOST": "localhost",
"PGUSER": "postgres",
"PGPASSWORD": "postgres",
},
"PGPASSWORD": "postgres"
}
},
{
"type": "node",
Expand All @@ -79,8 +91,8 @@
"env": {
"PGHOST": "localhost",
"PGUSER": "postgres",
"PGPASSWORD": "postgres",
},
"PGPASSWORD": "postgres"
}
},
{
"type": "node",
Expand All @@ -101,29 +113,25 @@
"env": {
"PGHOST": "localhost",
"PGUSER": "postgres",
"PGPASSWORD": "postgres",
},
"PGPASSWORD": "postgres"
}
},
{
"type": "node",
"request": "launch",
"name": "test: runes-api",
"program": "${workspaceFolder}/api/runes/node_modules/jest/bin/jest",
"cwd": "${workspaceFolder}/api/runes/",
"args": [
"--testTimeout=3600000",
"--runInBand",
"--no-cache",
],
"args": ["--testTimeout=3600000", "--runInBand", "--no-cache"],
"outputCapture": "std",
"console": "integratedTerminal",
"preLaunchTask": "npm: testenv:run",
"postDebugTask": "npm: testenv:stop",
"env": {
"PGHOST": "localhost",
"PGUSER": "postgres",
"PGPASSWORD": "postgres",
},
},
"PGPASSWORD": "postgres"
}
}
]
}
217 changes: 204 additions & 13 deletions components/ordhook-core/src/core/protocol/satoshi_tracking.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};

use bitcoin::{Address, Network, ScriptBuf};
use chainhook_sdk::utils::Context;
Expand Down Expand Up @@ -52,10 +52,12 @@ pub async fn augment_block_with_transfers(
ctx: &Context,
) -> Result<(), String> {
let network = get_bitcoin_network(&block.metadata.network);
let mut block_transferred_satpoints = HashMap::new();
for (tx_index, tx) in block.transactions.iter_mut().enumerate() {
let _ = augment_transaction_with_ordinal_transfers(
augment_transaction_with_ordinal_transfers(
tx,
tx_index,
&mut block_transferred_satpoints,
&block.block_identifier,
&network,
db_tx,
Expand Down Expand Up @@ -146,13 +148,12 @@ pub fn compute_satpoint_post_transfer(
pub async fn augment_transaction_with_ordinal_transfers(
tx: &mut BitcoinTransactionData,
tx_index: usize,
block_transferred_satpoints: &mut HashMap<String, Vec<WatchedSatpoint>>,
block_identifier: &BlockIdentifier,
network: &Network,
db_tx: &Transaction<'_>,
ctx: &Context,
) -> Result<Vec<OrdinalInscriptionTransferData>, String> {
let mut transfers = vec![];

) -> Result<(), String> {
// The transfers are inserted in storage after the inscriptions.
// We have a unicity constraing, and can only have 1 ordinals per satpoint.
let mut updated_sats = HashSet::new();
Expand All @@ -162,11 +163,33 @@ pub async fn augment_transaction_with_ordinal_transfers(
}
}

// For each satpoint inscribed retrieved, we need to compute the next outpoint to watch
let input_entries =
ordinals_pg::get_inscribed_satpoints_at_tx_inputs(&tx.metadata.inputs, db_tx).await?;
// Load all sats that will be transferred with this transaction i.e. loop through all tx inputs and look for previous
// satpoints we need to move.
//
// Since the DB state is currently at the end of the previous block, and there may be multiple transfers for the same sat in
// this new block, we'll use a memory cache to keep all sats that have been transferred but have not yet been written into the
// DB.
let mut cached_satpoints = HashMap::new();
let mut inputs_for_db_lookup = vec![];
for (vin, input) in tx.metadata.inputs.iter().enumerate() {
let output_key = format_outpoint_to_watch(
&input.previous_output.txid,
input.previous_output.vout as usize,
);
// Look in memory cache, or save for a batched DB lookup later.
if let Some(watched_satpoints) = block_transferred_satpoints.remove(&output_key) {
cached_satpoints.insert(vin, watched_satpoints);
} else {
inputs_for_db_lookup.push((vin, output_key));
}
}
let mut input_satpoints =
ordinals_pg::get_inscribed_satpoints_at_tx_inputs(&inputs_for_db_lookup, db_tx).await?;
input_satpoints.extend(cached_satpoints);

// Process all transfers across all inputs.
for (input_index, input) in tx.metadata.inputs.iter().enumerate() {
let Some(entries) = input_entries.get(&input_index) else {
let Some(entries) = input_satpoints.get(&input_index) else {
continue;
};
for watched_satpoint in entries.into_iter() {
Expand Down Expand Up @@ -199,6 +222,12 @@ pub async fn augment_transaction_with_ordinal_transfers(
satpoint_post_transfer: satpoint_post_transfer.clone(),
post_transfer_output_value,
};
// Keep an in-memory copy of this watchpoint at its new tx output for later retrieval.
let (output, _) = parse_output_and_offset_from_satpoint(&satpoint_post_transfer)?;
let entry = block_transferred_satpoints
.entry(output)
.or_insert(vec![]);
entry.push(watched_satpoint.clone());

try_info!(
ctx,
Expand All @@ -208,26 +237,188 @@ pub async fn augment_transaction_with_ordinal_transfers(
satpoint_post_transfer,
block_identifier.index
);
transfers.push(transfer_data.clone());
tx.metadata
.ordinal_operations
.push(OrdinalOperation::InscriptionTransferred(transfer_data));
}
}

Ok(transfers)
Ok(())
}

#[cfg(test)]
mod test {
use bitcoin::Network;
use chainhook_postgres::{pg_begin, pg_pool_client};
use chainhook_sdk::utils::Context;
use chainhook_types::OrdinalInscriptionTransferDestination;
use chainhook_types::{
OrdinalInscriptionNumber, OrdinalInscriptionRevealData, OrdinalInscriptionTransferData,
OrdinalInscriptionTransferDestination, OrdinalOperation,
};

use crate::core::test_builders::{TestTransactionBuilder, TestTxInBuilder, TestTxOutBuilder};
use crate::{
core::{
protocol::satoshi_tracking::augment_block_with_transfers,
test_builders::{
TestBlockBuilder, TestTransactionBuilder, TestTxInBuilder, TestTxOutBuilder,
},
},
db::{ordinals_pg, pg_reset_db, pg_test_connection, pg_test_connection_pool},
};

use super::compute_satpoint_post_transfer;

#[tokio::test]
async fn tracks_chained_satoshi_transfers_in_block() -> Result<(), String> {
let ordinal_number: u64 = 283888212016616;
let inscription_id =
"cbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8adi1245".to_string();
let block_height_1: u64 = 874387;
let block_height_2: u64 = 875364;

let ctx = Context::empty();
let mut pg_client = pg_test_connection().await;
ordinals_pg::migrate(&mut pg_client).await?;
let result = {
let mut ord_client = pg_pool_client(&pg_test_connection_pool()).await?;
let client = pg_begin(&mut ord_client).await?;

// 1. Insert inscription in a previous block first
let block = TestBlockBuilder::new()
.height(block_height_1)
.hash("0x000000000000000000021668d82e096a1aad3934b5a6f8f707ad29ade2505580".into())
.add_transaction(
TestTransactionBuilder::new()
.hash(
"0xcbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad"
.into(),
)
.add_ordinal_operation(
OrdinalOperation::InscriptionRevealed(
OrdinalInscriptionRevealData {
content_bytes: "0x".into(),
content_type: "".into(),
content_length: 0,
inscription_number: OrdinalInscriptionNumber { classic: 79754112, jubilee: 79754112 },
inscription_fee: 1161069,
inscription_output_value: 546,
inscription_id,
inscription_input_index: 0,
inscription_pointer: Some(0),
inscriber_address: Some("bc1p3qus9j7ucg0c4s2pf7k70nlpkk7r3ddt4u2ek54wn6nuwkzm9twqfenmjm".into()),
delegate: None,
metaprotocol: None,
metadata: None,
parents: vec![],
ordinal_number,
ordinal_block_height: 56777,
ordinal_offset: 0,
tx_index: 0,
transfers_pre_inscription: 0,
satpoint_post_inscription: "cbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad:0:0".into(),
curse_type: None,
charms: 0,
unbound_sequence: None,
},
),
)
.build(),
)
.build();
ordinals_pg::insert_block(&block, &client).await?;

// 2. Simulate a new block which transfers that same inscription back and forth across 2 transactions
let mut block = TestBlockBuilder::new()
.height(block_height_2)
.hash("0x00000000000000000001efc5fba69f0ebd5645a18258ec3cf109ca3636327242".into())
.add_transaction(TestTransactionBuilder::new().build())
.add_transaction(
TestTransactionBuilder::new()
.hash(
"0x30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd"
.into(),
)
.add_input(
TestTxInBuilder::new()
.prev_out_block_height(block_height_1)
.prev_out_tx_hash("0xcbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad".into())
.value(546)
.build()
)
.add_output(
TestTxOutBuilder::new()
.value(546)
.script_pubkey("0x51200944f1eef1a8f34ef4d0b58286a51115878abddbec2a3d3d8c581b71ff1c4bbc".into())
.build()
)
.build(),
)
.add_transaction(
TestTransactionBuilder::new()
.hash(
"0x0029b328fee7ab916ba98c194f21a084a4a781170610644de518dd0733c0d5d2"
.into(),
)
.add_input(
TestTxInBuilder::new()
.prev_out_block_height(block_height_2)
.prev_out_tx_hash("0x30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd".into())
.value(546)
.build()
)
.add_output(
TestTxOutBuilder::new()
.value(546)
.script_pubkey("0x5120883902cbdcc21f8ac1414fade7cfe1b5bc38b5abaf159b52ae9ea7c7585b2adc".into())
.build()
)
.build()
)
.build();
augment_block_with_transfers(&mut block, &client, &ctx).await?;

// 3. Make sure the correct transfers were produced
assert_eq!(
&block.transactions[1].metadata.ordinal_operations[0],
&OrdinalOperation::InscriptionTransferred(OrdinalInscriptionTransferData {
ordinal_number,
destination: OrdinalInscriptionTransferDestination::Transferred(
"bc1pp9z0rmh34re5aaxskkpgdfg3zkrc40wmas4r60vvtqdhrlcufw7qmgufuz".into()
),
satpoint_pre_transfer:
"cbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad:0:0"
.into(),
satpoint_post_transfer:
"30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd:0:0"
.into(),
post_transfer_output_value: Some(546),
tx_index: 1,
})
);
assert_eq!(
&block.transactions[2].metadata.ordinal_operations[0],
&OrdinalOperation::InscriptionTransferred(OrdinalInscriptionTransferData {
ordinal_number,
destination: OrdinalInscriptionTransferDestination::Transferred(
"bc1p3qus9j7ucg0c4s2pf7k70nlpkk7r3ddt4u2ek54wn6nuwkzm9twqfenmjm".into()
),
satpoint_pre_transfer:
"30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd:0:0"
.into(),
satpoint_post_transfer:
"0029b328fee7ab916ba98c194f21a084a4a781170610644de518dd0733c0d5d2:0:0"
.into(),
post_transfer_output_value: Some(546),
tx_index: 2,
})
);

Ok(())
};
pg_reset_db(&mut pg_client).await?;
result
}

#[test]
fn computes_satpoint_spent_as_fee() {
let ctx = Context::empty();
Expand Down
Loading
Loading