diff --git a/cache/decoders/code_hash_198c5ccb3fbd3309f110b8bdbc3df086bc9fb3867716f4e203005c501b172f00.bin b/cache/decoders/code_hash_198c5ccb3fbd3309f110b8bdbc3df086bc9fb3867716f4e203005c501b172f00.bin new file mode 100755 index 0000000..90c1b92 Binary files /dev/null and b/cache/decoders/code_hash_198c5ccb3fbd3309f110b8bdbc3df086bc9fb3867716f4e203005c501b172f00.bin differ diff --git a/src/client.rs b/src/client.rs index 46860da..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, @@ -66,9 +77,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(), @@ -79,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, @@ -92,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 ae77a52..109c9b0 100644 --- a/src/decoder/helpers.rs +++ b/src/decoder/helpers.rs @@ -4,14 +4,14 @@ 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; use spore_types::{generated::spore::ClusterData, SporeData}; use crate::{ - client::RpcClient, + client::RPC, types::{ ClusterDescriptionField, DOBDecoderFormat, DecoderLocationType, Error, ScriptId, Settings, }, @@ -78,11 +78,11 @@ 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]), 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,12 +129,17 @@ 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 -pub async fn fetch_dob_metadata( - rpc: &RpcClient, +pub async fn fetch_dob_metadata( + rpc: &T, settings: &Settings, cluster_id: [u8; 32], ) -> Result { @@ -154,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 @@ -174,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> { @@ -192,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 f0dde14..3aa5143 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1,7 +1,8 @@ +use ckb_types::H256; use serde_json::Value; use crate::{ - client::RpcClient, + client::RPC, types::{ ClusterDescriptionField, DOBClusterFormatV0, DOBClusterFormatV1, Dob, Error, Settings, StandardDOBOutput, @@ -11,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, &settings.ckb_rpc), - settings, - } +impl DOBDecoder { + pub fn new(rpc: T, settings: Settings) -> Self { + Self { rpc, settings } } pub fn protocol_versions(&self) -> Vec { @@ -35,10 +33,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 +45,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 +70,9 @@ 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.rpc.clone(), + &self.settings, ) .map_err(|_| Error::DecoderExecutionError)?; #[cfg(feature = "render_debug")] @@ -82,7 +90,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 +116,14 @@ 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.rpc.clone(), + &self.settings, + ) + .map_err(|_| Error::DecoderExecutionError)?; #[cfg(feature = "render_debug")] { println!("\n-------- DOB/1 DECODE RESULT ({i} => {exit_code}) ---------"); 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 0e11852..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, @@ -47,15 +48,19 @@ 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)) } } #[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 7a2120b..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,13 +86,14 @@ 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 ((_, dna), dob_metadata) = decoder + 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 .expect("fetch"); let render_result = decoder - .decode_dna(&dna, dob_metadata) + .decode_dna(&dna, dob_metadata, type_hash) // array type .await .expect("decode"); @@ -116,13 +118,14 @@ 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 ((_, dna), dob_metadata) = decoder + 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 .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..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,10 +81,12 @@ 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 - .decode_dna(&unicorn_content["dna"].as_str().unwrap(), unicorn_metadata) + .decode_dna(dna, unicorn_metadata, Default::default()) .await .expect("decode") } @@ -100,13 +103,14 @@ 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 ((_, dna), dob_metadata) = decoder + 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 .expect("fetch"); let render_result = decoder - .decode_dna(&dna, dob_metadata) + .decode_dna(&dna, dob_metadata, type_hash) // array type .await .expect("decode"); @@ -117,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 84e5b98..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,8 +63,12 @@ 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).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/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 ff49b45..b17ee9d 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 cell output")] + 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..de20975 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::RPC; +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,194 @@ impl Syscalls for DebugSyscall { } } -fn main_asm( +struct DobRingMatchSyscall { + ckb_rpc: T, + 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")?; + println!("cluster_dnas = {:?}", self.cluster_dnas); + 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 { + 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, + &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, + rpc: T, + 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: rpc, + 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 +268,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)?; @@ -72,10 +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) + main_asm(code, args, spore_type_hash, rpc, settings) }