diff --git a/Cargo.lock b/Cargo.lock index 909a2d2b0c..195d7b63e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -856,6 +856,7 @@ dependencies = [ "ckb-stop-handler", "ckb-store", "ckb-types", + "hex", "numext-fixed-uint", "rand 0.8.5", "rhai", diff --git a/util/indexer/Cargo.toml b/util/indexer/Cargo.toml index ec706546ce..a15b7a9347 100644 --- a/util/indexer/Cargo.toml +++ b/util/indexer/Cargo.toml @@ -29,3 +29,4 @@ numext-fixed-uint = "0.1" [dev-dependencies] tempfile.workspace = true rand = "0.8" +hex = "0.4" diff --git a/util/indexer/src/service.rs b/util/indexer/src/service.rs index 8b918c364d..af35e7c5ba 100644 --- a/util/indexer/src/service.rs +++ b/util/indexer/src/service.rs @@ -13,7 +13,7 @@ use ckb_async_runtime::{ use ckb_db_schema::{COLUMN_BLOCK_BODY, COLUMN_BLOCK_HEADER, COLUMN_INDEX, COLUMN_META}; use ckb_jsonrpc_types::{ IndexerCell, IndexerCellType, IndexerCellsCapacity, IndexerOrder, IndexerPagination, - IndexerScriptSearchMode, IndexerScriptType, IndexerSearchKey, IndexerTip, IndexerTx, + IndexerScriptType, IndexerSearchKey, IndexerSearchMode, IndexerTip, IndexerTx, IndexerTxWithCell, IndexerTxWithCells, JsonBytes, Uint32, }; use ckb_logger::{error, info}; @@ -333,6 +333,17 @@ impl IndexerHandle { limit: Uint32, after_cursor: Option, ) -> Result, Error> { + if search_key + .script_search_mode + .as_ref() + .map(|mode| *mode == IndexerSearchMode::Partial) + .unwrap_or(false) + { + return Err(Error::invalid_params( + "the CKB indexer doesn't support script partial search mode.", + )); + } + let (prefix, from_key, direction, skip) = build_query_options( &search_key, KeyPrefix::CellLockScript, @@ -351,7 +362,7 @@ impl IndexerHandle { }; let script_search_exact = matches!( search_key.script_search_mode, - Some(IndexerScriptSearchMode::Exact) + Some(IndexerSearchMode::Exact) ); let filter_options: FilterOptions = search_key.try_into()?; let mode = IteratorMode::From(from_key.as_ref(), direction); @@ -433,6 +444,30 @@ impl IndexerHandle { } } + if let Some((data, mode)) = &filter_options.output_data { + match mode { + IndexerSearchMode::Prefix => { + if !output_data.raw_data().starts_with(data) { + return None; + } + } + IndexerSearchMode::Exact => { + if output_data.raw_data() != data { + return None; + } + } + IndexerSearchMode::Partial => { + if !output_data + .raw_data() + .windows(data.len()) + .any(|window| window == data) + { + return None; + } + } + } + } + if let Some([r0, r1]) = filter_options.output_data_len_range { if output_data.len() < r0 || output_data.len() >= r1 { return None; @@ -480,6 +515,33 @@ impl IndexerHandle { limit: Uint32, after_cursor: Option, ) -> Result, Error> { + let limit = limit.value() as usize; + if limit == 0 { + return Err(Error::invalid_params("limit should be greater than 0")); + } + + if search_key + .script_search_mode + .as_ref() + .map(|mode| *mode == IndexerSearchMode::Partial) + .unwrap_or(false) + { + return Err(Error::invalid_params( + "the CKB indexer doesn't support script partial search mode.", + )); + } + + if search_key + .filter + .as_ref() + .map(|filter| filter.output_data.is_some()) + .unwrap_or(false) + { + return Err(Error::invalid_params( + "the CKB indexer doesn't support data filtering.", + )); + } + let (prefix, from_key, direction, skip) = build_query_options( &search_key, KeyPrefix::TxLockScript, @@ -487,10 +549,6 @@ impl IndexerHandle { order, after_cursor, )?; - let limit = limit.value() as usize; - if limit == 0 { - return Err(Error::invalid_params("limit should be greater than 0")); - } let (filter_script, filter_block_range) = if let Some(filter) = search_key.filter.as_ref() { if filter.script_len_range.is_some() { @@ -525,7 +583,7 @@ impl IndexerHandle { }; let script_search_exact = matches!( search_key.script_search_mode, - Some(IndexerScriptSearchMode::Exact) + Some(IndexerSearchMode::Exact) ); let mode = IteratorMode::From(from_key.as_ref(), direction); @@ -747,6 +805,17 @@ impl IndexerHandle { &self, search_key: IndexerSearchKey, ) -> Result, Error> { + if search_key + .script_search_mode + .as_ref() + .map(|mode| *mode == IndexerSearchMode::Partial) + .unwrap_or(false) + { + return Err(Error::invalid_params( + "the CKB indexer doesn't support script partial search mode", + )); + } + let (prefix, from_key, direction, skip) = build_query_options( &search_key, KeyPrefix::CellLockScript, @@ -760,7 +829,7 @@ impl IndexerHandle { }; let script_search_exact = matches!( search_key.script_search_mode, - Some(IndexerScriptSearchMode::Exact) + Some(IndexerSearchMode::Exact) ); let filter_options: FilterOptions = search_key.try_into()?; let mode = IteratorMode::From(from_key.as_ref(), direction); @@ -841,6 +910,30 @@ impl IndexerHandle { } } + if let Some((data, mode)) = &filter_options.output_data { + match mode { + IndexerSearchMode::Prefix => { + if !output_data.as_slice().starts_with(data) { + return None; + } + } + IndexerSearchMode::Exact => { + if output_data.as_slice() != data { + return None; + } + } + IndexerSearchMode::Partial => { + if !output_data + .as_slice() + .windows(data.len()) + .any(|window| window == data) + { + return None; + } + } + } + } + if let Some([r0, r1]) = filter_options.output_data_len_range { if output_data.len() < r0 || output_data.len() >= r1 { return None; @@ -929,6 +1022,7 @@ fn build_query_options( struct FilterOptions { script_prefix: Option>, script_len_range: Option<[usize; 2]>, + output_data: Option<(Vec, IndexerSearchMode)>, output_data_len_range: Option<[usize; 2]>, output_capacity_range: Option<[core::Capacity; 2]>, block_range: Option<[core::BlockNumber; 2]>, @@ -964,6 +1058,13 @@ impl TryInto for IndexerSearchKey { ] }); + let output_data = filter.output_data.map(|data| { + let mode = filter + .output_data_filter_mode + .unwrap_or(IndexerSearchMode::Prefix); + (data.as_bytes().to_vec(), mode) + }); + let output_data_len_range = filter.output_data_len_range.map(|range| { [ Into::::into(range.start()) as usize, @@ -983,6 +1084,7 @@ impl TryInto for IndexerSearchKey { Ok(FilterOptions { script_prefix, script_len_range, + output_data, output_data_len_range, output_capacity_range, block_range, @@ -1763,7 +1865,7 @@ mod tests { .get_cells( IndexerSearchKey { script: lock_script1.clone().into(), - script_search_mode: Some(IndexerScriptSearchMode::Exact), + script_search_mode: Some(IndexerSearchMode::Exact), ..Default::default() }, IndexerOrder::Asc, @@ -1783,7 +1885,7 @@ mod tests { .get_transactions( IndexerSearchKey { script: lock_script1.clone().into(), - script_search_mode: Some(IndexerScriptSearchMode::Exact), + script_search_mode: Some(IndexerSearchMode::Exact), ..Default::default() }, IndexerOrder::Asc, @@ -1799,7 +1901,7 @@ mod tests { .get_transactions( IndexerSearchKey { script: lock_script1.clone().into(), - script_search_mode: Some(IndexerScriptSearchMode::Exact), + script_search_mode: Some(IndexerSearchMode::Exact), group_by_transaction: Some(true), ..Default::default() }, @@ -1819,7 +1921,7 @@ mod tests { let capacity = rpc .get_cells_capacity(IndexerSearchKey { script: lock_script1.clone().into(), - script_search_mode: Some(IndexerScriptSearchMode::Exact), + script_search_mode: Some(IndexerSearchMode::Exact), ..Default::default() }) .unwrap() @@ -1845,4 +1947,138 @@ mod tests { capacity.capacity.value() ); } + + #[test] + fn output_data_filter_mode_rpc() { + let store = new_store("script_search_mode_rpc"); + let indexer = Indexer::new(store.clone(), 10, 100, None, CustomFilters::new(None, None)); + let rpc = IndexerHandle { store, pool: None }; + + // setup test data + let lock_script1 = ScriptBuilder::default() + .code_hash(H256(rand::random()).pack()) + .hash_type(ScriptHashType::Type.into()) + .args(Bytes::from(b"lock_script1".to_vec()).pack()) + .build(); + + let lock_script11 = ScriptBuilder::default() + .code_hash(lock_script1.code_hash()) + .hash_type(ScriptHashType::Type.into()) + .args(Bytes::from(b"lock_script11".to_vec()).pack()) + .build(); + + let type_script1 = ScriptBuilder::default() + .code_hash(H256(rand::random()).pack()) + .hash_type(ScriptHashType::Data.into()) + .args(Bytes::from(b"type_script1".to_vec()).pack()) + .build(); + + let type_script11 = ScriptBuilder::default() + .code_hash(type_script1.code_hash()) + .hash_type(ScriptHashType::Data.into()) + .args(Bytes::from(b"type_script11".to_vec()).pack()) + .build(); + + let cellbase0 = TransactionBuilder::default() + .input(CellInput::new_cellbase_input(0)) + .witness(Script::default().into_witness()) + .output( + CellOutputBuilder::default() + .capacity(capacity_bytes!(1000).pack()) + .lock(lock_script1.clone()) + .build(), + ) + .output_data(Default::default()) + .build(); + + let tx00 = TransactionBuilder::default() + .output( + CellOutputBuilder::default() + .capacity(capacity_bytes!(1000).pack()) + .lock(lock_script1.clone()) + .type_(Some(type_script1.clone()).pack()) + .build(), + ) + .output_data(Default::default()) + .build(); + + let tx01 = TransactionBuilder::default() + .output( + CellOutputBuilder::default() + .capacity(capacity_bytes!(2000).pack()) + .lock(lock_script11.clone()) + .type_(Some(type_script11.clone()).pack()) + .build(), + ) + .output_data(hex::decode("62e907b15cbfaa").unwrap().pack()) + .build(); + + let block0 = BlockBuilder::default() + .transaction(cellbase0) + .transaction(tx00.clone()) + .transaction(tx01.clone()) + .header(HeaderBuilder::default().number(0.pack()).build()) + .build(); + + indexer.append(&block0).unwrap(); + + // test get_cells rpc with output_data Prefix search mode + let cells = rpc + .get_cells( + IndexerSearchKey { + script: lock_script11.clone().into(), + filter: Some(IndexerSearchKeyFilter { + output_data: Some(JsonBytes::from_vec(hex::decode("62").unwrap())), + output_data_filter_mode: Some(IndexerSearchMode::Prefix), + ..Default::default() + }), + ..Default::default() + }, + IndexerOrder::Asc, + 1000.into(), + None, + ) + .unwrap(); + assert_eq!(1, cells.objects.len(),); + + // test get_cells rpc with output_data Partial search mode + let cells = rpc + .get_cells( + IndexerSearchKey { + script: lock_script11.clone().into(), + filter: Some(IndexerSearchKeyFilter { + output_data: Some(JsonBytes::from_vec(hex::decode("e907b1").unwrap())), + output_data_filter_mode: Some(IndexerSearchMode::Partial), + ..Default::default() + }), + ..Default::default() + }, + IndexerOrder::Asc, + 1000.into(), + None, + ) + .unwrap(); + assert_eq!(1, cells.objects.len(),); + + // test get_cells rpc with output_data Exact search mode + let cells = rpc + .get_cells( + IndexerSearchKey { + script: lock_script11.into(), + filter: Some(IndexerSearchKeyFilter { + output_data: Some(JsonBytes::from_vec( + hex::decode("62e907b15cbfaa").unwrap(), + )), + output_data_filter_mode: Some(IndexerSearchMode::Exact), + ..Default::default() + }), + ..Default::default() + }, + IndexerOrder::Asc, + 1000.into(), + None, + ) + .unwrap(); + assert_eq!(1, cells.objects.len(),); + } } diff --git a/util/jsonrpc-types/src/indexer.rs b/util/jsonrpc-types/src/indexer.rs index 86675031f0..4d38d847d8 100644 --- a/util/jsonrpc-types/src/indexer.rs +++ b/util/jsonrpc-types/src/indexer.rs @@ -53,7 +53,7 @@ pub struct IndexerSearchKey { /// Script Type pub script_type: IndexerScriptType, /// Script search mode, optional default is `prefix`, means search script with prefix - pub script_search_mode: Option, + pub script_search_mode: Option, /// filter cells by following conditions, all conditions are optional pub filter: Option, /// bool, optional default is `true`, if with_data is set to false, the field of returning cell.output_data is null in the result @@ -75,17 +75,19 @@ impl Default for IndexerSearchKey { } } -/// IndexerScriptSearchMode represent script search mode, default is prefix search -#[derive(Deserialize)] +/// IndexerSearchMode represent search mode, default is prefix search +#[derive(Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] -pub enum IndexerScriptSearchMode { - /// Mode `prefix` search script with prefix +pub enum IndexerSearchMode { + /// Mode `prefix` search with prefix Prefix, - /// Mode `exact` search script with exact match + /// Mode `exact` search with exact match Exact, + /// Mode `partial` search with partial match + Partial, } -impl Default for IndexerScriptSearchMode { +impl Default for IndexerSearchMode { fn default() -> Self { Self::Prefix } @@ -135,6 +137,10 @@ pub struct IndexerSearchKeyFilter { pub script: Option