From 9fcb078cb248023ad38f0d87e8b946e0af27205b Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Fri, 19 Jul 2024 10:54:49 +0800 Subject: [PATCH 1/3] feat: support dob combination ring --- src/client.rs | 5 +- src/decoder/helpers.rs | 30 ++++- src/decoder/mod.rs | 41 ++++-- src/server.rs | 8 +- src/tests/dob0/decoder.rs | 8 +- src/tests/dob0/legacy_decoder.rs | 7 +- src/tests/dob1/decoder.rs | 5 +- src/types.rs | 8 ++ src/vm.rs | 212 ++++++++++++++++++++++++++++++- 9 files changed, 290 insertions(+), 34 deletions(-) diff --git a/src/client.rs b/src/client.rs index 46860da..42199c5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -66,9 +66,10 @@ pub struct RpcClient { } impl RpcClient { - pub fn new(ckb_uri: &str, indexer_uri: &str) -> Self { + pub fn new(ckb_uri: &str, indexer_uri: Option<&str>) -> Self { + let indexer_uri = Url::parse(indexer_uri.unwrap_or(ckb_uri)) + .expect("ckb uri, e.g. \"http://127.0.0.1:8116\""); let ckb_uri = Url::parse(ckb_uri).expect("ckb uri, e.g. \"http://127.0.0.1:8114\""); - let indexer_uri = Url::parse(indexer_uri).expect("ckb uri, e.g. \"http://127.0.0.1:8116\""); RpcClient { raw: Client::new(), diff --git a/src/decoder/helpers.rs b/src/decoder/helpers.rs index ae77a52..567b266 100644 --- a/src/decoder/helpers.rs +++ b/src/decoder/helpers.rs @@ -4,7 +4,7 @@ use ckb_sdk::{constants::TYPE_ID_CODE_HASH, traits::CellQueryOptions}; use ckb_types::{ core::ScriptHashType, packed::{OutPoint, Script}, - prelude::{Builder, Entity, Pack}, + prelude::{Builder, Entity, Pack, Unpack}, H256, }; use serde_json::Value; @@ -82,7 +82,7 @@ pub async fn fetch_dob_content( rpc: &RpcClient, settings: &Settings, spore_id: [u8; 32], -) -> Result<((Value, String), [u8; 32]), Error> { +) -> Result<((Value, String), [u8; 32], H256), Error> { let mut spore_cell = None; for spore_search_option in build_batch_search_options(spore_id, &settings.available_spores) { spore_cell = rpc @@ -99,14 +99,25 @@ pub async fn fetch_dob_content( let Some(spore_cell) = spore_cell else { return Err(Error::SporeIdNotFound); }; + extract_dob_information( + spore_cell.output_data.unwrap_or_default().as_bytes(), + spore_cell.output.type_.unwrap().into(), + &settings.protocol_versions, + ) +} + +#[allow(clippy::type_complexity)] +pub fn extract_dob_information( + output_data: &[u8], + spore_type: Script, + protocol_versions: &[String], +) -> Result<((Value, String), [u8; 32], H256), Error> { let molecule_spore_data = - SporeData::from_compatible_slice(spore_cell.output_data.unwrap_or_default().as_bytes()) - .map_err(|_| Error::SporeDataUncompatible)?; + SporeData::from_compatible_slice(output_data).map_err(|_| Error::SporeDataUncompatible)?; let content_type = String::from_utf8(molecule_spore_data.content_type().raw_data().to_vec()) .map_err(|_| Error::SporeDataContentTypeUncompatible)?; if !content_type.is_empty() - && !settings - .protocol_versions + && !protocol_versions .iter() .any(|version| content_type.starts_with(version)) { @@ -118,7 +129,12 @@ pub async fn fetch_dob_content( .ok_or(Error::ClusterIdNotSet)? .raw_data(); let dob_content = decode_spore_data(&molecule_spore_data.content().raw_data())?; - Ok((dob_content, cluster_id.to_vec().try_into().unwrap())) + let spore_type_hash = spore_type.calc_script_hash().unpack(); + Ok(( + dob_content, + cluster_id.to_vec().try_into().unwrap(), + spore_type_hash, + )) } // search on-chain cluster cell and return its description field, which contains dob metadata diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index f0dde14..a8ae091 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1,3 +1,4 @@ +use ckb_types::H256; use serde_json::Value; use crate::{ @@ -19,7 +20,7 @@ pub struct DOBDecoder { impl DOBDecoder { pub fn new(settings: Settings) -> Self { Self { - rpc: RpcClient::new(&settings.ckb_rpc, &settings.ckb_rpc), + rpc: RpcClient::new(&settings.ckb_rpc, None), settings, } } @@ -35,10 +36,11 @@ impl DOBDecoder { pub async fn fetch_decode_ingredients( &self, spore_id: [u8; 32], - ) -> Result<((Value, String), ClusterDescriptionField), Error> { - let (content, cluster_id) = fetch_dob_content(&self.rpc, &self.settings, spore_id).await?; + ) -> Result<((Value, String), ClusterDescriptionField, H256), Error> { + let (content, cluster_id, type_hash) = + fetch_dob_content(&self.rpc, &self.settings, spore_id).await?; let dob_metadata = fetch_dob_metadata(&self.rpc, &self.settings, cluster_id).await?; - Ok((content, dob_metadata)) + Ok((content, dob_metadata, type_hash)) } // decode DNA under target spore_id @@ -46,16 +48,22 @@ impl DOBDecoder { &self, dna: &str, dob_metadata: ClusterDescriptionField, + spore_type_hash: H256, ) -> Result { let dob = dob_metadata.unbox_dob()?; match dob { - Dob::V0(dob0) => self.decode_dob0_dna(dna, dob0).await, - Dob::V1(dob1) => self.decode_dob1_dna(dna, dob1).await, + Dob::V0(dob0) => self.decode_dob0_dna(dna, dob0, spore_type_hash).await, + Dob::V1(dob1) => self.decode_dob1_dna(dna, dob1, spore_type_hash).await, } } // decode specificly for objects under DOB/0 protocol - async fn decode_dob0_dna(&self, dna: &str, dob0: &DOBClusterFormatV0) -> Result { + async fn decode_dob0_dna( + &self, + dna: &str, + dob0: &DOBClusterFormatV0, + spore_type_hash: H256, + ) -> Result { let decoder_path = parse_decoder_path(&self.rpc, &dob0.decoder, &self.settings).await?; let pattern = match &dob0.pattern { Value::String(string) => string.to_owned(), @@ -65,6 +73,8 @@ impl DOBDecoder { let (exit_code, outputs) = crate::vm::execute_riscv_binary( &decoder_path.to_string_lossy(), vec![dna.to_owned().into(), pattern.into()], + spore_type_hash, + &self.settings, ) .map_err(|_| Error::DecoderExecutionError)?; #[cfg(feature = "render_debug")] @@ -82,7 +92,12 @@ impl DOBDecoder { } // decode specificly for objects under DOB/1 protocol - async fn decode_dob1_dna(&self, dna: &str, dob1: &DOBClusterFormatV1) -> Result { + async fn decode_dob1_dna( + &self, + dna: &str, + dob1: &DOBClusterFormatV1, + spore_type_hash: H256, + ) -> Result { let mut output = Option::>::None; for (i, value) in dob1.decoders.iter().enumerate() { let decoder_path = @@ -103,9 +118,13 @@ impl DOBDecoder { } else { vec![dna.to_owned().into(), pattern.into()] }; - let (exit_code, outputs) = - crate::vm::execute_riscv_binary(&decoder_path.to_string_lossy(), args) - .map_err(|_| Error::DecoderExecutionError)?; + let (exit_code, outputs) = crate::vm::execute_riscv_binary( + &decoder_path.to_string_lossy(), + args, + spore_type_hash.clone(), + &self.settings, + ) + .map_err(|_| Error::DecoderExecutionError)?; #[cfg(feature = "render_debug")] { println!("\n-------- DOB/1 DECODE RESULT ({i} => {exit_code}) ---------"); diff --git a/src/server.rs b/src/server.rs index 0e11852..3e4c604 100644 --- a/src/server.rs +++ b/src/server.rs @@ -47,8 +47,12 @@ impl DecoderStandaloneServer { spore_id: [u8; 32], cache_path: PathBuf, ) -> Result<(String, Value), Error> { - let ((content, dna), metadata) = self.decoder.fetch_decode_ingredients(spore_id).await?; - let render_output = self.decoder.decode_dna(&dna, metadata).await?; + let ((content, dna), metadata, spore_type_hash) = + self.decoder.fetch_decode_ingredients(spore_id).await?; + let render_output = self + .decoder + .decode_dna(&dna, metadata, spore_type_hash) + .await?; write_dob_to_cache(&render_output, &content, cache_path, self.cache_expiration)?; Ok((render_output, content)) } diff --git a/src/tests/dob0/decoder.rs b/src/tests/dob0/decoder.rs index 7a2120b..aefc1aa 100644 --- a/src/tests/dob0/decoder.rs +++ b/src/tests/dob0/decoder.rs @@ -86,12 +86,12 @@ fn generate_example_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes async fn test_fetch_and_decode_unicorn_dna() { let settings = prepare_settings("text/plain"); let decoder = DOBDecoder::new(settings); - let ((_, dna), dob_metadata) = decoder + let ((_, dna), dob_metadata, type_hash) = decoder .fetch_decode_ingredients(UNICORN_SPORE_ID.into()) .await .expect("fetch"); let render_result = decoder - .decode_dna(&dna, dob_metadata) + .decode_dna(&dna, dob_metadata, type_hash) // array type .await .expect("decode"); @@ -117,12 +117,12 @@ fn test_unicorn_json_serde() { async fn test_fetch_and_decode_example_dna() { let settings = prepare_settings("text/plain"); let decoder = DOBDecoder::new(settings); - let ((_, dna), dob_metadata) = decoder + let ((_, dna), dob_metadata, type_hash) = decoder .fetch_decode_ingredients(EXAMPLE_SPORE_ID.into()) .await .expect("fetch"); let render_result = decoder - .decode_dna(&dna, dob_metadata) + .decode_dna(&dna, dob_metadata, type_hash) // array type .await .expect("decode"); diff --git a/src/tests/dob0/legacy_decoder.rs b/src/tests/dob0/legacy_decoder.rs index 52ddc4a..30032ac 100644 --- a/src/tests/dob0/legacy_decoder.rs +++ b/src/tests/dob0/legacy_decoder.rs @@ -82,8 +82,9 @@ async fn decode_unicorn_dna(onchain_decoder: bool) -> String { let settings = prepare_settings("text/plain"); let decoder = DOBDecoder::new(settings); let (unicorn_content, unicorn_metadata) = generate_unicorn_dob_ingredients(onchain_decoder); + let dna = unicorn_content["dna"].as_str().unwrap(); decoder - .decode_dna(&unicorn_content["dna"].as_str().unwrap(), unicorn_metadata) + .decode_dna(dna, unicorn_metadata, Default::default()) .await .expect("decode") } @@ -101,12 +102,12 @@ async fn test_decode_unicorn_dna() { async fn test_fetch_and_decode_nervape_dna() { let settings = prepare_settings("text/plain"); let decoder = DOBDecoder::new(settings); - let ((_, dna), dob_metadata) = decoder + let ((_, dna), dob_metadata, type_hash) = decoder .fetch_decode_ingredients(NERVAPE_SPORE_ID.into()) .await .expect("fetch"); let render_result = decoder - .decode_dna(&dna, dob_metadata) + .decode_dna(&dna, dob_metadata, type_hash) // array type .await .expect("decode"); diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs index 84e5b98..e2ff98b 100644 --- a/src/tests/dob1/decoder.rs +++ b/src/tests/dob1/decoder.rs @@ -64,6 +64,9 @@ async fn test_dob1_basic_decode() { let (content, dob_metadata) = generate_dob1_ingredients(); let decoder = DOBDecoder::new(settings); let dna = content.get("dna").unwrap().as_str().unwrap(); - let render_result = decoder.decode_dna(dna, dob_metadata).await.expect("decode"); + let render_result = decoder + .decode_dna(dna, dob_metadata, Default::default()) + .await + .expect("decode"); println!("\nrender_result: {}", render_result); } diff --git a/src/types.rs b/src/types.rs index ff49b45..f232e04 100644 --- a/src/types.rs +++ b/src/types.rs @@ -76,6 +76,14 @@ pub enum Error { DecoderScriptNotFound, #[error("decoder chain list cannot be empty")] DecoderChainIsEmpty, + #[error("DOB ring broken with disconnected outpoint pointer")] + CellOutputNotFound, + #[error("invalid DOB spore cell")] + InvalidDOBCell, + #[error("DOB ring broken with invalid ring pointer")] + InvalidNextDobRingPointer, + #[error("DOB ring is not a circle")] + DobRingUncirclelized, } pub enum Dob<'a> { diff --git a/src/vm.rs b/src/vm.rs index 537dacd..33d0122 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -1,10 +1,35 @@ // refer to https://github.com/nervosnetwork/ckb-vm/blob/develop/examples/ckb-vm-runner.rs -use std::sync::{Arc, Mutex}; +use std::collections::HashMap; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; +use ckb_types::packed::{CellOutput, OutPoint}; +use ckb_types::prelude::Entity; +use ckb_types::H256; use ckb_vm::cost_model::estimate_cycles; -use ckb_vm::registers::{A0, A7}; -use ckb_vm::{Bytes, Memory, Register, SupportMachine, Syscalls}; +use ckb_vm::registers::{A0, A1, A2, A3, A7}; +use ckb_vm::{Bytes, CoreMachine, Memory, Register, SupportMachine, Syscalls}; + +use crate::client::RpcClient; +use crate::decoder::helpers::extract_dob_information; +use crate::types::{Error, Settings}; + +macro_rules! error { + ($err: expr) => {{ + let error = $err.to_string(); + #[cfg(test)] + println!("[ERROR] {error}"); + #[cfg(not(test))] + jsonrpsee::tracing::error!("{error}"); + ckb_vm::error::Error::Unexpected(error) + }}; +} + +enum DobRingPointer { + OutPoint(OutPoint), + TypeHash([u8; 32]), +} struct DebugSyscall { output: Arc>>, @@ -46,14 +71,190 @@ impl Syscalls for DebugSyscall { } } +struct DobRingMatchSyscall { + ckb_rpc: RpcClient, + ring_tail_confirmation_type_hash: [u8; 32], + cluster_dnas: HashMap<[u8; 32], Vec>, + protocol_versions: Vec, +} + +impl DobRingMatchSyscall { + fn update_dob_ring_cluster_dnas(&mut self, mut out_point: OutPoint) -> Result<(), Error> { + let (tx, rx) = mpsc::channel(); + let ckb_rpc = self.ckb_rpc.clone(); + let confirmation_type_hash = self.ring_tail_confirmation_type_hash; + let protocol_versions = self.protocol_versions.clone(); + let mut cluster_dnas = HashMap::<[u8; 32], Vec>::new(); + thread::spawn(move || loop { + let rt = tokio::runtime::Runtime::new().unwrap(); + let dob_cell = match rt.block_on(ckb_rpc.get_live_cell(&out_point.into(), true)) { + Ok(cell) => cell, + Err(err) => { + return tx.send(Err(err)).expect("send"); + } + }; + // extract every single dob information in the ring + let (cluster_id, dna, next_outpoint) = if let Some(cell) = dob_cell.cell { + let dob_output = CellOutput::from(cell.output); + let args = dob_output.lock().args().raw_data(); + let ring_pointer = match OutPoint::from_compatible_slice(&args) { + Ok(out_point) => DobRingPointer::OutPoint(out_point), + Err(_) => { + if args.len() == 32 { + DobRingPointer::TypeHash(args.to_vec().try_into().unwrap()) + } else { + return tx + .send(Err(Error::InvalidNextDobRingPointer)) + .expect("send"); + } + } + }; + let Some(spore_type) = dob_output.type_().to_opt() else { + return tx.send(Err(Error::InvalidDOBCell)).expect("send"); + }; + let ((_, dna), cluster_id, _) = match extract_dob_information( + cell.data.unwrap().content.as_bytes(), + spore_type, + &protocol_versions, + ) { + Ok(info) => info, + Err(err) => { + return tx.send(Err(err)).expect("send"); + } + }; + (cluster_id, dna, ring_pointer) + } else { + return tx.send(Err(Error::CellOutputNotFound)).expect("send"); + }; + // record cluster and dna + cluster_dnas.entry(cluster_id).or_default().push(dna); + // check ring pointer + match next_outpoint { + DobRingPointer::OutPoint(next_outpoint) => { + out_point = next_outpoint; + } + DobRingPointer::TypeHash(type_hash) => { + if type_hash != confirmation_type_hash { + return tx.send(Err(Error::DobRingUncirclelized)).expect("send"); + } else { + tx.send(Ok(cluster_dnas)).expect("send"); + break; + } + } + } + }); + self.cluster_dnas = rx.recv().expect("recv")?; + Ok(()) + } + + fn handle_cluster_type_hash( + &self, + machine: &mut Mac, + buffer_addr: u64, + buffer_size_addr: &::REG, + buffer_size: u64, + cluster_type_hash: &[u8; 32], + ) -> Result { + if let Some(dnas) = self.cluster_dnas.get(cluster_type_hash) { + let dna_stream = dnas.join("|"); + machine.memory_mut().store64( + buffer_size_addr, + &Mac::REG::from_u64(dna_stream.len() as u64), + )?; + if buffer_size > 0 { + // return real result + machine + .memory_mut() + .store_bytes(buffer_addr, &dna_stream.as_bytes()[..buffer_size as usize])?; + } + Ok(true) + } else if !self.cluster_dnas.is_empty() { + // this branch means the cluster type_hash has missed out + machine + .memory_mut() + .store64(buffer_size_addr, &Mac::REG::from_u64(0))?; + Ok(true) + } else { + Ok(false) + } + } +} + +impl Syscalls for DobRingMatchSyscall { + fn initialize(&mut self, _machine: &mut Mac) -> Result<(), ckb_vm::error::Error> { + Ok(()) + } + + fn ecall(&mut self, machine: &mut Mac) -> Result { + let code = &machine.registers()[A7]; + if code.to_i32() != 2077 { + return Ok(false); + } + + // prepare input arguments + let buffer_addr = machine.registers()[A0].to_u64(); + let buffer_size_addr = machine.registers()[A1].clone(); + let buffer_size = machine.memory_mut().load64(&buffer_size_addr)?.to_u64(); + let outpoint_addr = machine.registers()[A2].to_u64(); + let cluster_type_hash_addr = machine.registers()[A3].to_u64(); + + // extract cluster type_hash from addr + let cluster_type_hash_bytes = machine + .memory_mut() + .load_bytes(cluster_type_hash_addr, 32)?; + let cluster_type_hash: [u8; 32] = cluster_type_hash_bytes.to_vec().try_into().unwrap(); + + // checkpoint check for a quick return + if self.handle_cluster_type_hash( + machine, + buffer_addr, + &buffer_size_addr, + buffer_size, + &cluster_type_hash, + )? { + return Ok(true); + } + + // extract outpoint from addr + let outpoint_bytes = machine + .memory_mut() + .load_bytes(outpoint_addr, OutPoint::TOTAL_SIZE as u64)?; + let ring_tail_outpoint = + OutPoint::from_compatible_slice(&outpoint_bytes).map_err(|err| error!(err))?; + + // search dob ring that starts from ring_tail_outpoint + self.update_dob_ring_cluster_dnas(ring_tail_outpoint) + .map_err(|err| error!(err))?; + + // handle cluster type hash after filling cluster_dnas + self.handle_cluster_type_hash( + machine, + buffer_addr, + &buffer_size_addr, + buffer_size, + &cluster_type_hash, + )?; + + Ok(true) + } +} + fn main_asm( code: Bytes, args: Vec, + type_hash: H256, + settings: &Settings, ) -> Result<(i8, Vec), Box> { let debug_result = Arc::new(Mutex::new(Vec::new())); let debug = Box::new(DebugSyscall { output: debug_result.clone(), }); + let dob_ring_match = Box::new(DobRingMatchSyscall { + ckb_rpc: RpcClient::new(&settings.ckb_rpc, None), + ring_tail_confirmation_type_hash: type_hash.into(), + cluster_dnas: HashMap::new(), + protocol_versions: settings.protocol_versions.clone(), + }); let asm_core = ckb_vm::machine::asm::AsmCoreMachine::new( ckb_vm::ISA_IMC | ckb_vm::ISA_B | ckb_vm::ISA_MOP | ckb_vm::ISA_A, @@ -63,6 +264,7 @@ fn main_asm( let core = ckb_vm::DefaultMachineBuilder::new(asm_core) .instruction_cycle_func(Box::new(estimate_cycles)) .syscall(debug) + .syscall(dob_ring_match) .build(); let mut machine = ckb_vm::machine::asm::AsmMachine::new(core); machine.load_program(&code, &args)?; @@ -75,7 +277,9 @@ fn main_asm( pub fn execute_riscv_binary( binary_path: &str, args: Vec, + spore_type_hash: H256, + settings: &Settings, ) -> Result<(i8, Vec), Box> { let code = std::fs::read(binary_path)?.into(); - main_asm(code, args) + main_asm(code, args, spore_type_hash, settings) } From cdacd6a91a4f7a9e21313f35527d5b36d8941ade Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Mon, 22 Jul 2024 11:15:48 +0800 Subject: [PATCH 2/3] refact: use trait to implement RpcClient --- src/client.rs | 17 ++++++++++++++--- src/decoder/helpers.rs | 22 +++++++++++----------- src/decoder/mod.rs | 15 ++++++--------- src/main.rs | 4 +++- src/server.rs | 11 ++++++----- src/tests/dob0/decoder.rs | 7 +++++-- src/tests/dob0/legacy_decoder.rs | 10 +++++++--- src/tests/dob1/decoder.rs | 4 +++- src/vm.rs | 10 +++++----- 9 files changed, 60 insertions(+), 40 deletions(-) diff --git a/src/client.rs b/src/client.rs index 42199c5..3188936 100644 --- a/src/client.rs +++ b/src/client.rs @@ -57,6 +57,17 @@ macro_rules! jsonrpc { }} } +#[allow(clippy::upper_case_acronyms)] +pub trait RPC: Clone + Send + Sync { + fn get_live_cell(&self, out_point: &OutPoint, with_data: bool) -> Rpc; + fn get_cells( + &self, + search_key: SearchKey, + limit: u32, + cursor: Option, + ) -> Rpc>; +} + #[derive(Clone)] pub struct RpcClient { raw: Client, @@ -80,8 +91,8 @@ impl RpcClient { } } -impl RpcClient { - pub fn get_live_cell(&self, out_point: &OutPoint, with_data: bool) -> Rpc { +impl RPC for RpcClient { + fn get_live_cell(&self, out_point: &OutPoint, with_data: bool) -> Rpc { jsonrpc!( "get_live_cell", Target::CKB, @@ -93,7 +104,7 @@ impl RpcClient { .boxed() } - pub fn get_cells( + fn get_cells( &self, search_key: SearchKey, limit: u32, diff --git a/src/decoder/helpers.rs b/src/decoder/helpers.rs index 567b266..109c9b0 100644 --- a/src/decoder/helpers.rs +++ b/src/decoder/helpers.rs @@ -11,7 +11,7 @@ use serde_json::Value; use spore_types::{generated::spore::ClusterData, SporeData}; use crate::{ - client::RpcClient, + client::RPC, types::{ ClusterDescriptionField, DOBDecoderFormat, DecoderLocationType, Error, ScriptId, Settings, }, @@ -78,8 +78,8 @@ pub fn decode_spore_data(spore_data: &[u8]) -> Result<(Value, String), Error> { } // search on-chain spore cell and return its content field, which represents dob content -pub async fn fetch_dob_content( - rpc: &RpcClient, +pub async fn fetch_dob_content( + rpc: &T, settings: &Settings, spore_id: [u8; 32], ) -> Result<((Value, String), [u8; 32], H256), Error> { @@ -138,8 +138,8 @@ pub fn extract_dob_information( } // search on-chain cluster cell and return its description field, which contains dob metadata -pub async fn fetch_dob_metadata( - rpc: &RpcClient, +pub async fn fetch_dob_metadata( + rpc: &T, settings: &Settings, cluster_id: [u8; 32], ) -> Result { @@ -170,8 +170,8 @@ pub async fn fetch_dob_metadata( } // search on-chain decoder cell, deployed with type_id feature enabled -async fn fetch_decoder_binary( - rpc: &RpcClient, +async fn fetch_decoder_binary( + rpc: &T, decoder_search_option: CellQueryOptions, ) -> Result, Error> { let decoder_cell = rpc @@ -190,8 +190,8 @@ async fn fetch_decoder_binary( } // search on-chain decoder cell, directly by its tx_hash and out_index -async fn fetch_decoder_binary_directly( - rpc: &RpcClient, +async fn fetch_decoder_binary_directly( + rpc: &T, tx_hash: H256, out_index: u32, ) -> Result, Error> { @@ -208,8 +208,8 @@ async fn fetch_decoder_binary_directly( Ok(decoder_binary.as_bytes().to_vec()) } -pub async fn parse_decoder_path( - rpc: &RpcClient, +pub async fn parse_decoder_path( + rpc: &T, decoder: &DOBDecoderFormat, settings: &Settings, ) -> Result { diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index a8ae091..4ce3135 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -2,7 +2,7 @@ use ckb_types::H256; use serde_json::Value; use crate::{ - client::RpcClient, + client::RPC, types::{ ClusterDescriptionField, DOBClusterFormatV0, DOBClusterFormatV1, Dob, Error, Settings, StandardDOBOutput, @@ -12,17 +12,14 @@ use crate::{ pub(crate) mod helpers; use helpers::*; -pub struct DOBDecoder { - rpc: RpcClient, +pub struct DOBDecoder { + rpc: T, settings: Settings, } -impl DOBDecoder { - pub fn new(settings: Settings) -> Self { - Self { - rpc: RpcClient::new(&settings.ckb_rpc, None), - settings, - } +impl DOBDecoder { + pub fn new(rpc: T, settings: Settings) -> Self { + Self { rpc, settings } } pub fn protocol_versions(&self) -> Vec { diff --git a/src/main.rs b/src/main.rs index 375225f..3a9d3cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use std::fs; +use client::RpcClient; use jsonrpsee::{server::ServerBuilder, tracing}; use server::DecoderRpcServer; use tracing_subscriber::EnvFilter; @@ -27,7 +28,8 @@ async fn main() { ); let rpc_server_address = settings.rpc_server_address.clone(); let cache_expiration = settings.dobs_cache_expiration_sec; - let decoder = decoder::DOBDecoder::new(settings); + let rpc = RpcClient::new(&settings.ckb_rpc, None); + let decoder = decoder::DOBDecoder::new(rpc, settings); tracing::info!("running decoder server at {}", rpc_server_address); let http_server = ServerBuilder::new() diff --git a/src/server.rs b/src/server.rs index 3e4c604..4e3990d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -7,6 +7,7 @@ use jsonrpsee::{proc_macros::rpc, tracing, types::ErrorCode}; use serde::Serialize; use serde_json::Value; +use crate::client::RPC; use crate::decoder::DOBDecoder; use crate::types::Error; @@ -29,13 +30,13 @@ trait DecoderRpc { async fn batch_decode(&self, hexed_spore_ids: Vec) -> Result, ErrorCode>; } -pub struct DecoderStandaloneServer { - decoder: DOBDecoder, +pub struct DecoderStandaloneServer { + decoder: DOBDecoder, cache_expiration: u64, } -impl DecoderStandaloneServer { - pub fn new(decoder: DOBDecoder, cache_expiration: u64) -> Self { +impl DecoderStandaloneServer { + pub fn new(decoder: DOBDecoder, cache_expiration: u64) -> Self { Self { decoder, cache_expiration, @@ -59,7 +60,7 @@ impl DecoderStandaloneServer { } #[async_trait] -impl DecoderRpcServer for DecoderStandaloneServer { +impl DecoderRpcServer for DecoderStandaloneServer { async fn protocol_versions(&self) -> Vec { self.decoder.protocol_versions() } diff --git a/src/tests/dob0/decoder.rs b/src/tests/dob0/decoder.rs index aefc1aa..304da5a 100644 --- a/src/tests/dob0/decoder.rs +++ b/src/tests/dob0/decoder.rs @@ -1,6 +1,7 @@ use ckb_types::{h256, H256}; use serde_json::{json, Value}; +use crate::client::RpcClient; use crate::decoder::DOBDecoder; use crate::tests::prepare_settings; use crate::types::{ @@ -85,7 +86,8 @@ fn generate_example_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes #[tokio::test] async fn test_fetch_and_decode_unicorn_dna() { let settings = prepare_settings("text/plain"); - let decoder = DOBDecoder::new(settings); + let rpc = RpcClient::new(&settings.ckb_rpc, None); + let decoder = DOBDecoder::new(rpc, settings); let ((_, dna), dob_metadata, type_hash) = decoder .fetch_decode_ingredients(UNICORN_SPORE_ID.into()) .await @@ -116,7 +118,8 @@ fn test_unicorn_json_serde() { #[tokio::test] async fn test_fetch_and_decode_example_dna() { let settings = prepare_settings("text/plain"); - let decoder = DOBDecoder::new(settings); + let rpc = RpcClient::new(&settings.ckb_rpc, None); + let decoder = DOBDecoder::new(rpc, settings); let ((_, dna), dob_metadata, type_hash) = decoder .fetch_decode_ingredients(EXAMPLE_SPORE_ID.into()) .await diff --git a/src/tests/dob0/legacy_decoder.rs b/src/tests/dob0/legacy_decoder.rs index 30032ac..fdaae14 100644 --- a/src/tests/dob0/legacy_decoder.rs +++ b/src/tests/dob0/legacy_decoder.rs @@ -1,5 +1,6 @@ use ckb_types::{h256, H256}; +use crate::client::RpcClient; use crate::decoder::{helpers::decode_spore_data, DOBDecoder}; use crate::tests::prepare_settings; use crate::types::{ @@ -80,7 +81,8 @@ fn generate_unicorn_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes async fn decode_unicorn_dna(onchain_decoder: bool) -> String { let settings = prepare_settings("text/plain"); - let decoder = DOBDecoder::new(settings); + let rpc = RpcClient::new(&settings.ckb_rpc, None); + let decoder = DOBDecoder::new(rpc, settings); let (unicorn_content, unicorn_metadata) = generate_unicorn_dob_ingredients(onchain_decoder); let dna = unicorn_content["dna"].as_str().unwrap(); decoder @@ -101,7 +103,8 @@ async fn test_decode_unicorn_dna() { #[tokio::test] async fn test_fetch_and_decode_nervape_dna() { let settings = prepare_settings("text/plain"); - let decoder = DOBDecoder::new(settings); + let rpc = RpcClient::new(&settings.ckb_rpc, None); + let decoder = DOBDecoder::new(rpc, settings); let ((_, dna), dob_metadata, type_hash) = decoder .fetch_decode_ingredients(NERVAPE_SPORE_ID.into()) .await @@ -118,7 +121,8 @@ async fn test_fetch_and_decode_nervape_dna() { #[should_panic = "fetch: DOBVersionUnexpected"] async fn test_fetch_onchain_dob_failed() { let settings = prepare_settings("dob/0"); - DOBDecoder::new(settings) + let rpc = RpcClient::new(&settings.ckb_rpc, None); + DOBDecoder::new(rpc, settings) .fetch_decode_ingredients(NERVAPE_SPORE_ID.into()) .await .expect("fetch"); diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs index e2ff98b..8a39684 100644 --- a/src/tests/dob1/decoder.rs +++ b/src/tests/dob1/decoder.rs @@ -2,6 +2,7 @@ use ckb_types::h256; use serde_json::{json, Value}; use crate::{ + client::RpcClient, decoder::DOBDecoder, tests::prepare_settings, types::{ @@ -62,7 +63,8 @@ fn test_print_dob1_ingreidents() { async fn test_dob1_basic_decode() { let settings = prepare_settings("dob/1"); let (content, dob_metadata) = generate_dob1_ingredients(); - let decoder = DOBDecoder::new(settings); + let rpc = RpcClient::new(&settings.ckb_rpc, None); + let decoder = DOBDecoder::new(rpc, settings); let dna = content.get("dna").unwrap().as_str().unwrap(); let render_result = decoder .decode_dna(dna, dob_metadata, Default::default()) diff --git a/src/vm.rs b/src/vm.rs index 33d0122..14e8119 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -11,7 +11,7 @@ use ckb_vm::cost_model::estimate_cycles; use ckb_vm::registers::{A0, A1, A2, A3, A7}; use ckb_vm::{Bytes, CoreMachine, Memory, Register, SupportMachine, Syscalls}; -use crate::client::RpcClient; +use crate::client::{RpcClient, RPC}; use crate::decoder::helpers::extract_dob_information; use crate::types::{Error, Settings}; @@ -71,14 +71,14 @@ impl Syscalls for DebugSyscall { } } -struct DobRingMatchSyscall { - ckb_rpc: RpcClient, +struct DobRingMatchSyscall { + ckb_rpc: T, ring_tail_confirmation_type_hash: [u8; 32], cluster_dnas: HashMap<[u8; 32], Vec>, protocol_versions: Vec, } -impl DobRingMatchSyscall { +impl DobRingMatchSyscall { fn update_dob_ring_cluster_dnas(&mut self, mut out_point: OutPoint) -> Result<(), Error> { let (tx, rx) = mpsc::channel(); let ckb_rpc = self.ckb_rpc.clone(); @@ -180,7 +180,7 @@ impl DobRingMatchSyscall { } } -impl Syscalls for DobRingMatchSyscall { +impl Syscalls for DobRingMatchSyscall { fn initialize(&mut self, _machine: &mut Mac) -> Result<(), ckb_vm::error::Error> { Ok(()) } From 73da429628728c8edb910ce9df197b3c51263209 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Mon, 22 Jul 2024 16:20:08 +0800 Subject: [PATCH 3/3] feat: complete dob ring mock test --- ...3df086bc9fb3867716f4e203005c501b172f00.bin | Bin 0 -> 77232 bytes src/decoder/mod.rs | 2 + src/tests/dob_ring/client.rs | 85 ++++++++++++++++++ src/tests/dob_ring/decoder.rs | 73 +++++++++++++++ src/tests/dob_ring/mod.rs | 2 + src/tests/mod.rs | 1 + src/types.rs | 2 +- src/vm.rs | 15 ++-- 8 files changed, 174 insertions(+), 6 deletions(-) create mode 100755 cache/decoders/code_hash_198c5ccb3fbd3309f110b8bdbc3df086bc9fb3867716f4e203005c501b172f00.bin create mode 100644 src/tests/dob_ring/client.rs create mode 100644 src/tests/dob_ring/decoder.rs create mode 100644 src/tests/dob_ring/mod.rs diff --git a/cache/decoders/code_hash_198c5ccb3fbd3309f110b8bdbc3df086bc9fb3867716f4e203005c501b172f00.bin b/cache/decoders/code_hash_198c5ccb3fbd3309f110b8bdbc3df086bc9fb3867716f4e203005c501b172f00.bin new file mode 100755 index 0000000000000000000000000000000000000000..90c1b92ad9d57fa8bb71a25f8c2a75a092f42a28 GIT binary patch literal 77232 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

gO}6$z$kI{Gnp{6F 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 literal 0 HcmV?d00001 diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 4ce3135..3aa5143 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -71,6 +71,7 @@ impl DOBDecoder { &decoder_path.to_string_lossy(), vec![dna.to_owned().into(), pattern.into()], spore_type_hash, + self.rpc.clone(), &self.settings, ) .map_err(|_| Error::DecoderExecutionError)?; @@ -119,6 +120,7 @@ impl DOBDecoder { &decoder_path.to_string_lossy(), args, spore_type_hash.clone(), + self.rpc.clone(), &self.settings, ) .map_err(|_| Error::DecoderExecutionError)?; diff --git a/src/tests/dob_ring/client.rs b/src/tests/dob_ring/client.rs new file mode 100644 index 0000000..4314320 --- /dev/null +++ b/src/tests/dob_ring/client.rs @@ -0,0 +1,85 @@ +use ckb_jsonrpc_types::{CellData, CellInfo, CellWithStatus, JsonBytes, OutPoint}; +use ckb_sdk::rpc::ckb_indexer::{Cell, Pagination, SearchKey}; +use ckb_types::{ + packed, + prelude::{Builder, Entity, Pack}, + H256, +}; +use futures::FutureExt; +use spore_types::{Bytes, BytesOpt, SporeData}; + +use crate::client::{Rpc, RpcClient, RPC}; + +#[derive(Clone)] +pub struct MockRpcClient { + raw: RpcClient, +} + +impl MockRpcClient { + pub fn new(ckb_uri: &str, indexer_uri: Option<&str>) -> Self { + let raw = RpcClient::new(ckb_uri, indexer_uri); + Self { raw } + } +} + +impl RPC for MockRpcClient { + fn get_live_cell(&self, out_point: &OutPoint, _with_data: bool) -> Rpc { + let index: u32 = out_point.index.into(); + let cluster_id = [index as u8; 32]; + let spore_data = SporeData::new_builder() + .cluster_id( + BytesOpt::new_builder() + .set(Some( + Bytes::new_builder() + .set(cluster_id.map(Into::into).to_vec()) + .build(), + )) + .build(), + ) + .content( + Bytes::new_builder() + .set( + "\"ac7b88aabbcc687474703a2f2f3132372e302e302e313a383039300000\"" + .as_bytes() + .into_iter() + .map(|v| v.clone().into()) + .collect(), + ) + .build(), + ) + .build(); + let next_outpoint = packed::OutPoint::new(Default::default(), index + 1); + println!("index: {}", index); + let args = if index < 5 { + next_outpoint.as_slice().to_vec() + } else { + H256::default().as_bytes().to_vec() + }; + let live_cell = packed::CellOutput::new_builder() + .lock(packed::Script::new_builder().args(args.pack()).build()) + .type_(Some(packed::Script::new_builder().build()).pack()) + .build(); + async move { + Ok(CellWithStatus { + cell: Some(CellInfo { + output: live_cell.into(), + data: Some(CellData { + content: JsonBytes::from_vec(spore_data.as_slice().to_vec()), + hash: Default::default(), + }), + }), + status: "live".to_string(), + }) + } + .boxed() + } + + fn get_cells( + &self, + search_key: SearchKey, + limit: u32, + cursor: Option, + ) -> Rpc> { + self.raw.get_cells(search_key, limit, cursor) + } +} diff --git a/src/tests/dob_ring/decoder.rs b/src/tests/dob_ring/decoder.rs new file mode 100644 index 0000000..4bb97bf --- /dev/null +++ b/src/tests/dob_ring/decoder.rs @@ -0,0 +1,73 @@ +use ckb_types::{h256, packed::OutPoint, prelude::Entity}; +use serde_json::{json, Value}; + +use crate::{ + decoder::DOBDecoder, + tests::{prepare_settings, dob_ring::client::MockRpcClient}, + types::{ + ClusterDescriptionField, DOBClusterFormat, DOBClusterFormatV0, DOBClusterFormatV1, + DOBDecoderFormat, DecoderLocationType, + }, +}; + +fn generate_dob_ring_ingredients() -> (Value, ClusterDescriptionField) { + let content = json!({ + "dna": hex::encode(OutPoint::new(Default::default(), 0).as_bytes()) + }); + let metadata = ClusterDescriptionField { + description: "Ring DOB Test".to_string(), + dob: DOBClusterFormat::new_dob1(DOBClusterFormatV1 { + decoders: vec![ + DOBClusterFormatV0 { + decoder: DOBDecoderFormat { + location: DecoderLocationType::CodeHash, + hash: Some(h256!( + "0x198c5ccb3fbd3309f110b8bdbc3df086bc9fb3867716f4e203005c501b172f00" + )), + script: None + }, + pattern: serde_json::from_str("[[\"Name\",\"String\",\"0000000000000000000000000000000000000000000000000000000000000000\",0,1,\"options\",[\"Alice\",\"Bob\",\"Charlie\",\"David\",\"Ethan\",\"Florence\",\"Grace\",\"Helen\"]],[\"Age\",\"Number\",\"0101010101010101010101010101010101010101010101010101010101010101\",1,1,\"range\",[0,100]],[\"Score\",\"Number\",\"0202020202020202020202020202020202020202020202020202020202020202\",2,1,\"rawNumber\"],[\"DNA\",\"String\",\"0303030303030303030303030303030303030303030303030303030303030303\",3,3,\"rawString\"],[\"URL\",\"String\",\"0404040404040404040404040404040404040404040404040404040404040404\",6,30,\"utf8\"],[\"Value\",\"Timestamp\",\"0505050505050505050505050505050505050505050505050505050505050505\",3,3,\"rawNumber\"]]").unwrap(), + }, + DOBClusterFormatV0 { + decoder: DOBDecoderFormat { + location: DecoderLocationType::TypeScript, + script: Some(serde_json::from_str(r#" + { + "code_hash": "0x00000000000000000000000000000000000000000000000000545950455f4944", + "hash_type": "type", + "args": "0x784e32cef202b9d4759ea96e80d806f94051e8069fd34d761f452553700138d7" + } + "#).unwrap() + ), + hash: None, + }, + pattern: serde_json::from_str("[[\"IMAGE.0\",\"attributes\",\"\",\"raw\",\"width='200' height='200' xmlns='http://www.w3.org/2000/svg'\"],[\"IMAGE.0\",\"elements\",\"Name\",\"options\",[[\"Alice\",\"\"],[\"Bob\",\"\"],[\"Ethan\",\"\"],[[\"*\"],\"\"]]],[\"IMAGE.0\",\"elements\",\"Age\",\"range\",[[[0,50],\"\"],[[51,100],\"\"],[[\"*\"],\"\"]]],[\"IMAGE.1\",\"attributes\",\"\",\"raw\",\"xmlns='http://www.w3.org/2000/svg'\"],[\"IMAGE.1\",\"elements\",\"Score\",\"range\",[[[0,1000],\"\"],[[\"*\"],\"\"]]]]").unwrap(), + } + ], + }), + }; + (content, metadata) +} + +#[test] +fn test_print_dob_ring_ingreidents() { + let (_, dob_metadata) = generate_dob_ring_ingredients(); + println!( + "cluster_description: {}", + serde_json::to_string(&dob_metadata).unwrap() + ); +} + +#[tokio::test] +async fn test_dob_ring_decode() { + let settings = prepare_settings("dob/1"); + let (content, dob_metadata) = generate_dob_ring_ingredients(); + let rpc = MockRpcClient::new(&settings.ckb_rpc, None); + let decoder = DOBDecoder::new(rpc, settings); + let dna = content.get("dna").unwrap().as_str().unwrap(); + let render_result = decoder + .decode_dna(dna, dob_metadata, Default::default()) + .await + .expect("decode"); + println!("\nrender_result: {}", render_result); +} diff --git a/src/tests/dob_ring/mod.rs b/src/tests/dob_ring/mod.rs new file mode 100644 index 0000000..3bedd0f --- /dev/null +++ b/src/tests/dob_ring/mod.rs @@ -0,0 +1,2 @@ +mod client; +mod decoder; diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 4896f15..76aac55 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -4,6 +4,7 @@ use crate::types::{HashType, OnchainDecoderDeployment, ScriptId, Settings}; mod dob0; mod dob1; +mod dob_ring; fn prepare_settings(version: &str) -> Settings { Settings { diff --git a/src/types.rs b/src/types.rs index f232e04..b17ee9d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -76,7 +76,7 @@ pub enum Error { DecoderScriptNotFound, #[error("decoder chain list cannot be empty")] DecoderChainIsEmpty, - #[error("DOB ring broken with disconnected outpoint pointer")] + #[error("DOB ring broken with disconnected cell output")] CellOutputNotFound, #[error("invalid DOB spore cell")] InvalidDOBCell, diff --git a/src/vm.rs b/src/vm.rs index 14e8119..de20975 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -11,7 +11,7 @@ use ckb_vm::cost_model::estimate_cycles; use ckb_vm::registers::{A0, A1, A2, A3, A7}; use ckb_vm::{Bytes, CoreMachine, Memory, Register, SupportMachine, Syscalls}; -use crate::client::{RpcClient, RPC}; +use crate::client::RPC; use crate::decoder::helpers::extract_dob_information; use crate::types::{Error, Settings}; @@ -144,6 +144,7 @@ impl DobRingMatchSyscall { } }); self.cluster_dnas = rx.recv().expect("recv")?; + println!("cluster_dnas = {:?}", self.cluster_dnas); Ok(()) } @@ -155,7 +156,9 @@ impl DobRingMatchSyscall { buffer_size: u64, cluster_type_hash: &[u8; 32], ) -> Result { + println!("handle"); if let Some(dnas) = self.cluster_dnas.get(cluster_type_hash) { + println!("dnas.len = {}", dnas.len()); let dna_stream = dnas.join("|"); machine.memory_mut().store64( buffer_size_addr, @@ -239,10 +242,11 @@ impl Syscalls for DobRingMatchSyscall { } } -fn main_asm( +fn main_asm( code: Bytes, args: Vec, type_hash: H256, + rpc: T, settings: &Settings, ) -> Result<(i8, Vec), Box> { let debug_result = Arc::new(Mutex::new(Vec::new())); @@ -250,7 +254,7 @@ fn main_asm( output: debug_result.clone(), }); let dob_ring_match = Box::new(DobRingMatchSyscall { - ckb_rpc: RpcClient::new(&settings.ckb_rpc, None), + ckb_rpc: rpc, ring_tail_confirmation_type_hash: type_hash.into(), cluster_dnas: HashMap::new(), protocol_versions: settings.protocol_versions.clone(), @@ -274,12 +278,13 @@ fn main_asm( Ok((error_code, result)) } -pub fn execute_riscv_binary( +pub fn execute_riscv_binary( binary_path: &str, args: Vec, spore_type_hash: H256, + rpc: T, settings: &Settings, ) -> Result<(i8, Vec), Box> { let code = std::fs::read(binary_path)?.into(); - main_asm(code, args, spore_type_hash, settings) + main_asm(code, args, spore_type_hash, rpc, settings) }