Skip to content

Commit b611ff3

Browse files
authored
fix(ordinals): track multiple sat transfers in the same block correctly (#460)
1 parent 8e4502b commit b611ff3

File tree

3 files changed

+241
-50
lines changed

3 files changed

+241
-50
lines changed

.vscode/launch.json

+29-21
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"ordinals",
1717
"service",
1818
"start",
19-
"--config-path=${workspaceFolder}/.vscode/Indexer.toml",
19+
"--config-path=${workspaceFolder}/.vscode/Indexer.toml"
2020
],
2121
"cwd": "${workspaceFolder}"
2222
},
@@ -35,30 +35,42 @@
3535
"runes",
3636
"service",
3737
"start",
38-
"--config-path=${workspaceFolder}/.vscode/Indexer.toml",
38+
"--config-path=${workspaceFolder}/.vscode/Indexer.toml"
3939
],
4040
"cwd": "${workspaceFolder}"
4141
},
42+
{
43+
"type": "node",
44+
"request": "launch",
45+
"name": "run: ordinals-api",
46+
"cwd": "${workspaceFolder}/api/ordinals",
47+
"runtimeArgs": ["-r", "ts-node/register"],
48+
"args": ["${workspaceFolder}/api/ordinals/src/index.ts"],
49+
"outputCapture": "std",
50+
"internalConsoleOptions": "openOnSessionStart",
51+
"envFile": "${workspaceFolder}/api/ordinals/.env",
52+
"env": {
53+
"NODE_ENV": "development",
54+
"TS_NODE_SKIP_IGNORE": "true"
55+
},
56+
"killBehavior": "polite"
57+
},
4258
{
4359
"type": "node",
4460
"request": "launch",
4561
"name": "test: ordinals-api",
4662
"program": "${workspaceFolder}/api/ordinals/node_modules/jest/bin/jest",
4763
"cwd": "${workspaceFolder}/api/ordinals/",
48-
"args": [
49-
"--testTimeout=3600000",
50-
"--runInBand",
51-
"--no-cache"
52-
],
64+
"args": ["--testTimeout=3600000", "--runInBand", "--no-cache"],
5365
"outputCapture": "std",
5466
"console": "integratedTerminal",
5567
"preLaunchTask": "npm: testenv:run",
5668
"postDebugTask": "npm: testenv:stop",
5769
"env": {
5870
"PGHOST": "localhost",
5971
"PGUSER": "postgres",
60-
"PGPASSWORD": "postgres",
61-
},
72+
"PGPASSWORD": "postgres"
73+
}
6274
},
6375
{
6476
"type": "node",
@@ -79,8 +91,8 @@
7991
"env": {
8092
"PGHOST": "localhost",
8193
"PGUSER": "postgres",
82-
"PGPASSWORD": "postgres",
83-
},
94+
"PGPASSWORD": "postgres"
95+
}
8496
},
8597
{
8698
"type": "node",
@@ -101,29 +113,25 @@
101113
"env": {
102114
"PGHOST": "localhost",
103115
"PGUSER": "postgres",
104-
"PGPASSWORD": "postgres",
105-
},
116+
"PGPASSWORD": "postgres"
117+
}
106118
},
107119
{
108120
"type": "node",
109121
"request": "launch",
110122
"name": "test: runes-api",
111123
"program": "${workspaceFolder}/api/runes/node_modules/jest/bin/jest",
112124
"cwd": "${workspaceFolder}/api/runes/",
113-
"args": [
114-
"--testTimeout=3600000",
115-
"--runInBand",
116-
"--no-cache",
117-
],
125+
"args": ["--testTimeout=3600000", "--runInBand", "--no-cache"],
118126
"outputCapture": "std",
119127
"console": "integratedTerminal",
120128
"preLaunchTask": "npm: testenv:run",
121129
"postDebugTask": "npm: testenv:stop",
122130
"env": {
123131
"PGHOST": "localhost",
124132
"PGUSER": "postgres",
125-
"PGPASSWORD": "postgres",
126-
},
127-
},
133+
"PGPASSWORD": "postgres"
134+
}
135+
}
128136
]
129137
}

components/ordhook-core/src/core/protocol/satoshi_tracking.rs

+204-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::HashSet;
1+
use std::collections::{HashMap, HashSet};
22

33
use bitcoin::{Address, Network, ScriptBuf};
44
use chainhook_sdk::utils::Context;
@@ -52,10 +52,12 @@ pub async fn augment_block_with_transfers(
5252
ctx: &Context,
5353
) -> Result<(), String> {
5454
let network = get_bitcoin_network(&block.metadata.network);
55+
let mut block_transferred_satpoints = HashMap::new();
5556
for (tx_index, tx) in block.transactions.iter_mut().enumerate() {
56-
let _ = augment_transaction_with_ordinal_transfers(
57+
augment_transaction_with_ordinal_transfers(
5758
tx,
5859
tx_index,
60+
&mut block_transferred_satpoints,
5961
&block.block_identifier,
6062
&network,
6163
db_tx,
@@ -146,13 +148,12 @@ pub fn compute_satpoint_post_transfer(
146148
pub async fn augment_transaction_with_ordinal_transfers(
147149
tx: &mut BitcoinTransactionData,
148150
tx_index: usize,
151+
block_transferred_satpoints: &mut HashMap<String, Vec<WatchedSatpoint>>,
149152
block_identifier: &BlockIdentifier,
150153
network: &Network,
151154
db_tx: &Transaction<'_>,
152155
ctx: &Context,
153-
) -> Result<Vec<OrdinalInscriptionTransferData>, String> {
154-
let mut transfers = vec![];
155-
156+
) -> Result<(), String> {
156157
// The transfers are inserted in storage after the inscriptions.
157158
// We have a unicity constraing, and can only have 1 ordinals per satpoint.
158159
let mut updated_sats = HashSet::new();
@@ -162,11 +163,33 @@ pub async fn augment_transaction_with_ordinal_transfers(
162163
}
163164
}
164165

165-
// For each satpoint inscribed retrieved, we need to compute the next outpoint to watch
166-
let input_entries =
167-
ordinals_pg::get_inscribed_satpoints_at_tx_inputs(&tx.metadata.inputs, db_tx).await?;
166+
// Load all sats that will be transferred with this transaction i.e. loop through all tx inputs and look for previous
167+
// satpoints we need to move.
168+
//
169+
// Since the DB state is currently at the end of the previous block, and there may be multiple transfers for the same sat in
170+
// 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
171+
// DB.
172+
let mut cached_satpoints = HashMap::new();
173+
let mut inputs_for_db_lookup = vec![];
174+
for (vin, input) in tx.metadata.inputs.iter().enumerate() {
175+
let output_key = format_outpoint_to_watch(
176+
&input.previous_output.txid,
177+
input.previous_output.vout as usize,
178+
);
179+
// Look in memory cache, or save for a batched DB lookup later.
180+
if let Some(watched_satpoints) = block_transferred_satpoints.remove(&output_key) {
181+
cached_satpoints.insert(vin, watched_satpoints);
182+
} else {
183+
inputs_for_db_lookup.push((vin, output_key));
184+
}
185+
}
186+
let mut input_satpoints =
187+
ordinals_pg::get_inscribed_satpoints_at_tx_inputs(&inputs_for_db_lookup, db_tx).await?;
188+
input_satpoints.extend(cached_satpoints);
189+
190+
// Process all transfers across all inputs.
168191
for (input_index, input) in tx.metadata.inputs.iter().enumerate() {
169-
let Some(entries) = input_entries.get(&input_index) else {
192+
let Some(entries) = input_satpoints.get(&input_index) else {
170193
continue;
171194
};
172195
for watched_satpoint in entries.into_iter() {
@@ -199,6 +222,12 @@ pub async fn augment_transaction_with_ordinal_transfers(
199222
satpoint_post_transfer: satpoint_post_transfer.clone(),
200223
post_transfer_output_value,
201224
};
225+
// Keep an in-memory copy of this watchpoint at its new tx output for later retrieval.
226+
let (output, _) = parse_output_and_offset_from_satpoint(&satpoint_post_transfer)?;
227+
let entry = block_transferred_satpoints
228+
.entry(output)
229+
.or_insert(vec![]);
230+
entry.push(watched_satpoint.clone());
202231

203232
try_info!(
204233
ctx,
@@ -208,26 +237,188 @@ pub async fn augment_transaction_with_ordinal_transfers(
208237
satpoint_post_transfer,
209238
block_identifier.index
210239
);
211-
transfers.push(transfer_data.clone());
212240
tx.metadata
213241
.ordinal_operations
214242
.push(OrdinalOperation::InscriptionTransferred(transfer_data));
215243
}
216244
}
217245

218-
Ok(transfers)
246+
Ok(())
219247
}
220248

221249
#[cfg(test)]
222250
mod test {
223251
use bitcoin::Network;
252+
use chainhook_postgres::{pg_begin, pg_pool_client};
224253
use chainhook_sdk::utils::Context;
225-
use chainhook_types::OrdinalInscriptionTransferDestination;
254+
use chainhook_types::{
255+
OrdinalInscriptionNumber, OrdinalInscriptionRevealData, OrdinalInscriptionTransferData,
256+
OrdinalInscriptionTransferDestination, OrdinalOperation,
257+
};
226258

227-
use crate::core::test_builders::{TestTransactionBuilder, TestTxInBuilder, TestTxOutBuilder};
259+
use crate::{
260+
core::{
261+
protocol::satoshi_tracking::augment_block_with_transfers,
262+
test_builders::{
263+
TestBlockBuilder, TestTransactionBuilder, TestTxInBuilder, TestTxOutBuilder,
264+
},
265+
},
266+
db::{ordinals_pg, pg_reset_db, pg_test_connection, pg_test_connection_pool},
267+
};
228268

229269
use super::compute_satpoint_post_transfer;
230270

271+
#[tokio::test]
272+
async fn tracks_chained_satoshi_transfers_in_block() -> Result<(), String> {
273+
let ordinal_number: u64 = 283888212016616;
274+
let inscription_id =
275+
"cbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8adi1245".to_string();
276+
let block_height_1: u64 = 874387;
277+
let block_height_2: u64 = 875364;
278+
279+
let ctx = Context::empty();
280+
let mut pg_client = pg_test_connection().await;
281+
ordinals_pg::migrate(&mut pg_client).await?;
282+
let result = {
283+
let mut ord_client = pg_pool_client(&pg_test_connection_pool()).await?;
284+
let client = pg_begin(&mut ord_client).await?;
285+
286+
// 1. Insert inscription in a previous block first
287+
let block = TestBlockBuilder::new()
288+
.height(block_height_1)
289+
.hash("0x000000000000000000021668d82e096a1aad3934b5a6f8f707ad29ade2505580".into())
290+
.add_transaction(
291+
TestTransactionBuilder::new()
292+
.hash(
293+
"0xcbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad"
294+
.into(),
295+
)
296+
.add_ordinal_operation(
297+
OrdinalOperation::InscriptionRevealed(
298+
OrdinalInscriptionRevealData {
299+
content_bytes: "0x".into(),
300+
content_type: "".into(),
301+
content_length: 0,
302+
inscription_number: OrdinalInscriptionNumber { classic: 79754112, jubilee: 79754112 },
303+
inscription_fee: 1161069,
304+
inscription_output_value: 546,
305+
inscription_id,
306+
inscription_input_index: 0,
307+
inscription_pointer: Some(0),
308+
inscriber_address: Some("bc1p3qus9j7ucg0c4s2pf7k70nlpkk7r3ddt4u2ek54wn6nuwkzm9twqfenmjm".into()),
309+
delegate: None,
310+
metaprotocol: None,
311+
metadata: None,
312+
parents: vec![],
313+
ordinal_number,
314+
ordinal_block_height: 56777,
315+
ordinal_offset: 0,
316+
tx_index: 0,
317+
transfers_pre_inscription: 0,
318+
satpoint_post_inscription: "cbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad:0:0".into(),
319+
curse_type: None,
320+
charms: 0,
321+
unbound_sequence: None,
322+
},
323+
),
324+
)
325+
.build(),
326+
)
327+
.build();
328+
ordinals_pg::insert_block(&block, &client).await?;
329+
330+
// 2. Simulate a new block which transfers that same inscription back and forth across 2 transactions
331+
let mut block = TestBlockBuilder::new()
332+
.height(block_height_2)
333+
.hash("0x00000000000000000001efc5fba69f0ebd5645a18258ec3cf109ca3636327242".into())
334+
.add_transaction(TestTransactionBuilder::new().build())
335+
.add_transaction(
336+
TestTransactionBuilder::new()
337+
.hash(
338+
"0x30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd"
339+
.into(),
340+
)
341+
.add_input(
342+
TestTxInBuilder::new()
343+
.prev_out_block_height(block_height_1)
344+
.prev_out_tx_hash("0xcbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad".into())
345+
.value(546)
346+
.build()
347+
)
348+
.add_output(
349+
TestTxOutBuilder::new()
350+
.value(546)
351+
.script_pubkey("0x51200944f1eef1a8f34ef4d0b58286a51115878abddbec2a3d3d8c581b71ff1c4bbc".into())
352+
.build()
353+
)
354+
.build(),
355+
)
356+
.add_transaction(
357+
TestTransactionBuilder::new()
358+
.hash(
359+
"0x0029b328fee7ab916ba98c194f21a084a4a781170610644de518dd0733c0d5d2"
360+
.into(),
361+
)
362+
.add_input(
363+
TestTxInBuilder::new()
364+
.prev_out_block_height(block_height_2)
365+
.prev_out_tx_hash("0x30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd".into())
366+
.value(546)
367+
.build()
368+
)
369+
.add_output(
370+
TestTxOutBuilder::new()
371+
.value(546)
372+
.script_pubkey("0x5120883902cbdcc21f8ac1414fade7cfe1b5bc38b5abaf159b52ae9ea7c7585b2adc".into())
373+
.build()
374+
)
375+
.build()
376+
)
377+
.build();
378+
augment_block_with_transfers(&mut block, &client, &ctx).await?;
379+
380+
// 3. Make sure the correct transfers were produced
381+
assert_eq!(
382+
&block.transactions[1].metadata.ordinal_operations[0],
383+
&OrdinalOperation::InscriptionTransferred(OrdinalInscriptionTransferData {
384+
ordinal_number,
385+
destination: OrdinalInscriptionTransferDestination::Transferred(
386+
"bc1pp9z0rmh34re5aaxskkpgdfg3zkrc40wmas4r60vvtqdhrlcufw7qmgufuz".into()
387+
),
388+
satpoint_pre_transfer:
389+
"cbc9fcf9373cbae36f4868d73a0ad78bbdc58af7c813e6319163e101a8cac8ad:0:0"
390+
.into(),
391+
satpoint_post_transfer:
392+
"30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd:0:0"
393+
.into(),
394+
post_transfer_output_value: Some(546),
395+
tx_index: 1,
396+
})
397+
);
398+
assert_eq!(
399+
&block.transactions[2].metadata.ordinal_operations[0],
400+
&OrdinalOperation::InscriptionTransferred(OrdinalInscriptionTransferData {
401+
ordinal_number,
402+
destination: OrdinalInscriptionTransferDestination::Transferred(
403+
"bc1p3qus9j7ucg0c4s2pf7k70nlpkk7r3ddt4u2ek54wn6nuwkzm9twqfenmjm".into()
404+
),
405+
satpoint_pre_transfer:
406+
"30a5a4861a28436a229a6a08872057bd3970382955e6be8fb7f0fde31c3424bd:0:0"
407+
.into(),
408+
satpoint_post_transfer:
409+
"0029b328fee7ab916ba98c194f21a084a4a781170610644de518dd0733c0d5d2:0:0"
410+
.into(),
411+
post_transfer_output_value: Some(546),
412+
tx_index: 2,
413+
})
414+
);
415+
416+
Ok(())
417+
};
418+
pg_reset_db(&mut pg_client).await?;
419+
result
420+
}
421+
231422
#[test]
232423
fn computes_satpoint_spent_as_fee() {
233424
let ctx = Context::empty();

0 commit comments

Comments
 (0)