From 86c0b8d46fac852ba06874a8c7bed08a69f947aa Mon Sep 17 00:00:00 2001 From: liyukun Date: Wed, 5 Jun 2024 22:20:21 +0800 Subject: [PATCH 01/17] feat: complete initial implementation of DOB1 --- Cargo.lock | 607 ++++++++++++++++++++++++- Cargo.toml | 2 +- settings.mainnet.toml | 9 + settings.toml | 9 + src/client.rs | 61 ++- src/decoder/helpers.rs | 237 ++++++++++ src/decoder/mod.rs | 113 +++++ src/tests/{ => dob0}/decoder.rs | 13 +- src/tests/{ => dob0}/legacy_decoder.rs | 15 +- src/tests/dob0/mod.rs | 2 + src/tests/mod.rs | 3 +- src/types.rs | 75 ++- src/vm.rs | 110 ++++- 13 files changed, 1230 insertions(+), 26 deletions(-) create mode 100644 src/decoder/helpers.rs create mode 100644 src/decoder/mod.rs rename src/tests/{ => dob0}/decoder.rs (97%) rename src/tests/{ => dob0}/legacy_decoder.rs (98%) create mode 100644 src/tests/dob0/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3fcfc30..84645ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,12 +37,41 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "anyhow" version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "async-trait" version = "0.1.79" @@ -60,6 +89,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +dependencies = [ + "arrayvec", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -114,6 +166,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -126,6 +184,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bitstream-io" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c12d1856e42f0d817a835fe55853957c85c8c8a470114029143d3f12671446e" + [[package]] name = "blake2b-ref" version = "0.3.1" @@ -160,18 +224,36 @@ dependencies = [ "generic-array", ] +[[package]] +name = "built" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a6c0b39c38fd754ac338b00a88066436389c0f029da5d37d1e01091d9b7c17" + [[package]] name = "bumpalo" version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.6.0" @@ -213,6 +295,20 @@ name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] [[package]] name = "cfg-if" @@ -631,6 +727,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "convert_case" version = "0.4.0" @@ -680,12 +782,37 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -769,9 +896,9 @@ dependencies = [ "ckb-vm", "futures", "hex", + "image", "jsonrpc-core", "jsonrpsee", - "lazy_static", "reqwest 0.12.4", "serde", "serde_json", @@ -837,6 +964,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "faster-hex" version = "0.6.1" @@ -849,6 +992,15 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + [[package]] name = "flate2" version = "1.0.28" @@ -859,6 +1011,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1010,6 +1171,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1085,6 +1256,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if 1.0.0", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1115,6 +1296,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1298,6 +1485,45 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d730b085583c4d789dfd07fdcf185be59501666a90c97c40162b37e4fdad272d" +dependencies = [ + "byteorder-lite", + "thiserror", +] + +[[package]] +name = "imgref" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" + [[package]] name = "includedir" version = "0.6.0" @@ -1329,18 +1555,53 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.69" @@ -1407,7 +1668,7 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7c2416c400c94b2e864603c51a5bbd5b103386da1f5e58cbf01e7bb3ef0833" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-crate", "proc-macro2", "quote", @@ -1466,12 +1727,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -1494,6 +1772,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.7.8" @@ -1512,6 +1799,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if 1.0.0", + "rayon", +] + [[package]] name = "memchr" version = "2.7.2" @@ -1565,6 +1862,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -1572,6 +1875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -1614,6 +1918,28 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1624,6 +1950,56 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1868,6 +2244,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1916,6 +2305,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.57", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.35" @@ -2006,6 +2429,76 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if 1.0.0", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2152,6 +2645,15 @@ dependencies = [ "winreg 0.52.0", ] +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + [[package]] name = "route-recognizer" version = "0.3.1" @@ -2451,6 +2953,21 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2509,6 +3026,15 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spore-types" version = "0.1.0" @@ -2583,6 +3109,25 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.12", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + [[package]] name = "tempfile" version = "3.10.1" @@ -2625,6 +3170,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2900,6 +3456,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.0" @@ -2912,6 +3479,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.4" @@ -3025,6 +3598,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -3260,3 +3839,27 @@ name = "xxhash-rust" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 2edf7a2..b1cec59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,11 @@ ckb-hash = "0.116.1" thiserror = "1.0" serde_json = "1.0" hex = "0.4.3" +image = "0.25.1" reqwest = { version = "0.12.4", features = ["json"] } jsonrpc-core = "18.0" serde = { version = "1.0", features = ["serde_derive"] } futures = "0.3" -lazy_static = { version = "1.4" } ckb-vm = { version = "0.24", features = ["asm"] } spore-types = { git = "https://github.com/sporeprotocol/spore-contract", rev = "81315ca" } diff --git a/settings.mainnet.toml b/settings.mainnet.toml index 18b3567..3b7735a 100644 --- a/settings.mainnet.toml +++ b/settings.mainnet.toml @@ -6,6 +6,9 @@ protocol_versions = [ # connect to the RPC of CKB node ckb_rpc = "https://mainnet.ckb.dev/" +# connect to the image fetcher service +image_fetcher_url = "https://dobfs.dobby.market" + # address that rpc server running at in case of standalone server mode rpc_server_address = "0.0.0.0:8090" @@ -21,6 +24,12 @@ dobs_cache_directory = "cache/dobs" # expiration time indicator for cleaning whole dobs cache, zero means never clean dobs_cache_expiration_sec = 300 +# identifier that represents the maximum combination of DOB/1 +dob1_max_combination = 5 + +# identifier that represents the maximum number of caching images +dob1_max_cache_size = 100 + # all deployed on-chain Spore contracts binary hash (order from new to old) # refer to: https://github.com/sporeprotocol/spore-contract/blob/master/docs/VERSIONS.md [[available_spores]] diff --git a/settings.toml b/settings.toml index 0ff1ae0..75bacbc 100644 --- a/settings.toml +++ b/settings.toml @@ -6,6 +6,9 @@ protocol_versions = [ # connect to the RPC of CKB node ckb_rpc = "https://testnet.ckbapp.dev/" +# connect to the image fetcher service +image_fetcher_url = "https://dobfs.dobby.market/testnet" + # address that rpc server running at in case of standalone server mode rpc_server_address = "0.0.0.0:8090" @@ -21,6 +24,12 @@ dobs_cache_directory = "cache/dobs" # expiration time indicator for cleaning whole dobs cache, zero means never clean dobs_cache_expiration_sec = 300 +# identifier that represents the maximum combination of DOB/1 +dob1_max_combination = 5 + +# identifier that represents the maximum number of caching images +dob1_max_cache_size = 100 + # all deployed on-chain Spore contracts binary hash (order from new to old) # refer to: https://github.com/sporeprotocol/spore-contract/blob/master/docs/VERSIONS.md [[available_spores]] diff --git a/src/client.rs b/src/client.rs index 46860da..2fbf660 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,3 +1,4 @@ +use std::collections::VecDeque; use std::future::Future; use std::pin::Pin; use std::sync::atomic::{AtomicU64, Ordering}; @@ -5,6 +6,7 @@ use std::sync::Arc; use ckb_jsonrpc_types::{CellWithStatus, JsonBytes, OutPoint, Uint32}; use ckb_sdk::rpc::ckb_indexer::{Cell, Order, Pagination, SearchKey}; +use ckb_vm::Bytes; use jsonrpc_core::futures::FutureExt; use reqwest::{Client, Url}; @@ -77,9 +79,7 @@ impl RpcClient { id: Arc::new(AtomicU64::new(0)), } } -} -impl RpcClient { pub fn get_live_cell(&self, out_point: &OutPoint, with_data: bool) -> Rpc { jsonrpc!( "get_live_cell", @@ -114,3 +114,60 @@ impl RpcClient { .boxed() } } + +pub struct ImageFetchClient { + raw: Client, + base_url: Url, + images_cache: VecDeque<(Url, Bytes)>, + max_cache_size: usize, +} + +impl ImageFetchClient { + pub fn new(base_url: &str, cache_size: usize) -> Self { + let base_url = Url::parse(base_url).expect("base url, e.g. \"http://127.0.0.1"); + Self { + raw: Client::new(), + base_url, + images_cache: VecDeque::new(), + max_cache_size: cache_size, + } + } + + pub async fn fetch_images(&mut self, images_uri: &[String]) -> Result>, Error> { + let requests = images_uri + .iter() + .map(|uri| self.base_url.join(uri).expect("valid url")) + .map(|url| { + let image = self.images_cache.iter().find(|v| v.0 == url); + if let Some((_, image)) = image { + async move { Ok((url, true, image.clone())) }.boxed() + } else { + let send = self.raw.get(url.clone()).send(); + async move { + let bytes = send + .await + .map_err(|_| Error::JsonRpcRequestError)? + .bytes() + .await + .map_err(|_| Error::JsonRpcRequestError)?; + Ok((url, false, bytes)) + } + .boxed() + } + }) + .collect::>(); + let mut images = vec![]; + let responses = futures::future::join_all(requests).await; + for response in responses { + let (url, from_cache, result) = response?; + images.push(result.to_vec()); + if !from_cache { + self.images_cache.push_back((url, result)); + if self.images_cache.len() > self.max_cache_size { + self.images_cache.pop_front(); + } + } + } + Ok(images) + } +} diff --git a/src/decoder/helpers.rs b/src/decoder/helpers.rs new file mode 100644 index 0000000..69b1a5f --- /dev/null +++ b/src/decoder/helpers.rs @@ -0,0 +1,237 @@ +use std::path::PathBuf; + +use ckb_sdk::{constants::TYPE_ID_CODE_HASH, traits::CellQueryOptions}; +use ckb_types::{ + core::ScriptHashType, + packed::{OutPoint, Script}, + prelude::{Builder, Entity, Pack}, + H256, +}; +use serde_json::Value; +use spore_types::{generated::spore::ClusterData, SporeData}; + +use crate::{ + client::RpcClient, + types::{ + ClusterDescriptionField, DOBDecoderFormat, DecoderLocationType, Error, ScriptId, Settings, + }, +}; + +pub fn build_type_id_search_option(type_id_args: [u8; 32]) -> CellQueryOptions { + let type_script = Script::new_builder() + .code_hash(TYPE_ID_CODE_HASH.0.pack()) + .hash_type(ScriptHashType::Type.into()) + .args(type_id_args.to_vec().pack()) + .build(); + CellQueryOptions::new_type(type_script) +} + +pub fn build_batch_search_options( + type_args: [u8; 32], + available_script_ids: &[ScriptId], +) -> Vec { + available_script_ids + .iter() + .map( + |ScriptId { + code_hash, + hash_type, + }| { + let hash_type: ScriptHashType = hash_type.into(); + let type_script = Script::new_builder() + .code_hash(code_hash.0.pack()) + .hash_type(hash_type.into()) + .args(type_args.to_vec().pack()) + .build(); + CellQueryOptions::new_type(type_script) + }, + ) + .collect() +} + +pub fn decode_spore_data(spore_data: &[u8]) -> Result<(Value, String), Error> { + if spore_data[0] == 0u8 { + let dna = hex::encode(&spore_data[1..]); + return Ok((serde_json::Value::String(dna.clone()), dna)); + } + + let value: Value = + serde_json::from_slice(spore_data).map_err(|_| Error::DOBContentUnexpected)?; + let dna = match &value { + serde_json::Value::String(_) => &value, + serde_json::Value::Array(array) => array.first().ok_or(Error::DOBContentUnexpected)?, + serde_json::Value::Object(object) => { + object.get("dna").ok_or(Error::DOBContentUnexpected)? + } + _ => return Err(Error::DOBContentUnexpected), + }; + let dna = match dna { + serde_json::Value::String(string) => string.to_owned(), + _ => return Err(Error::DOBContentUnexpected), + }; + + Ok((value, dna)) +} + +// search on-chain spore cell and return its content field, which represents dob content +pub async fn fetch_dob_content( + rpc: &RpcClient, + settings: &Settings, + spore_id: [u8; 32], +) -> Result<((Value, String), [u8; 32]), Error> { + let mut spore_cell = None; + for spore_search_option in build_batch_search_options(spore_id, &settings.available_spores) { + spore_cell = rpc + .get_cells(spore_search_option.into(), 1, None) + .await + .map_err(|_| Error::FetchLiveCellsError)? + .objects + .first() + .cloned(); + if spore_cell.is_some() { + break; + } + } + let Some(spore_cell) = spore_cell else { + return Err(Error::SporeIdNotFound); + }; + let molecule_spore_data = + SporeData::from_compatible_slice(spore_cell.output_data.unwrap_or_default().as_bytes()) + .map_err(|_| Error::SporeDataUncompatible)?; + let content_type = String::from_utf8(molecule_spore_data.content_type().raw_data().to_vec()) + .map_err(|_| Error::SporeDataContentTypeUncompatible)?; + if !settings + .protocol_versions + .iter() + .any(|version| content_type.starts_with(version)) + { + return Err(Error::DOBVersionUnexpected); + } + let cluster_id = molecule_spore_data + .cluster_id() + .to_opt() + .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())) +} + +// search on-chain cluster cell and return its description field, which contains dob metadata +pub async fn fetch_dob_metadata( + rpc: &RpcClient, + settings: &Settings, + cluster_id: [u8; 32], +) -> Result { + let mut cluster_cell = None; + for cluster_search_option in + build_batch_search_options(cluster_id, &settings.available_clusters) + { + cluster_cell = rpc + .get_cells(cluster_search_option.into(), 1, None) + .await + .map_err(|_| Error::FetchLiveCellsError)? + .objects + .first() + .cloned(); + if cluster_cell.is_some() { + break; + } + } + let Some(cluster_cell) = cluster_cell else { + return Err(Error::ClusterIdNotFound); + }; + let molecule_cluster_data = + ClusterData::from_compatible_slice(cluster_cell.output_data.unwrap_or_default().as_bytes()) + .map_err(|_| Error::ClusterDataUncompatible)?; + let dob_metadata = serde_json::from_slice(&molecule_cluster_data.description().raw_data()) + .map_err(|_| Error::DOBMetadataUnexpected)?; + Ok(dob_metadata) +} + +// search on-chain decoder cell, deployed with type_id feature enabled +pub async fn fetch_decoder_binary(rpc: &RpcClient, decoder_id: [u8; 32]) -> Result, Error> { + let decoder_search_option = build_type_id_search_option(decoder_id); + let decoder_cell = rpc + .get_cells(decoder_search_option.into(), 1, None) + .await + .map_err(|_| Error::FetchLiveCellsError)? + .objects + .first() + .cloned() + .ok_or(Error::DecoderIdNotFound)?; + Ok(decoder_cell + .output_data + .unwrap_or_default() + .as_bytes() + .into()) +} + +// search on-chain decoder cell, directly by its tx_hash and out_index +pub async fn fetch_decoder_binary_directly( + rpc: &RpcClient, + tx_hash: H256, + out_index: u32, +) -> Result, Error> { + let decoder_cell = rpc + .get_live_cell(&OutPoint::new(tx_hash.pack(), out_index).into(), true) + .await + .map_err(|_| Error::FetchTransactionError)?; + let decoder_binary = decoder_cell + .cell + .ok_or(Error::NoOutputCellInTransaction)? + .data + .ok_or(Error::DecoderBinaryNotFoundInCell)? + .content; + Ok(decoder_binary.as_bytes().to_vec()) +} + +pub async fn parse_decoder_path( + rpc: &RpcClient, + decoder: &DOBDecoderFormat, + settings: &Settings, +) -> Result { + let decoder_path = match decoder.location { + DecoderLocationType::CodeHash => { + let mut decoder_path = settings.decoders_cache_directory.clone(); + decoder_path.push(format!("code_hash_{}.bin", hex::encode(&decoder.hash))); + if !decoder_path.exists() { + let onchain_decoder = + settings + .onchain_decoder_deployment + .iter() + .find_map(|deployment| { + if deployment.code_hash == decoder.hash { + Some(fetch_decoder_binary_directly( + rpc, + deployment.tx_hash.clone(), + deployment.out_index, + )) + } else { + None + } + }); + let Some(decoder_binary) = onchain_decoder else { + return Err(Error::NativeDecoderNotFound); + }; + let decoder_file_content = decoder_binary.await?; + if ckb_hash::blake2b_256(&decoder_file_content) != decoder.hash.0 { + return Err(Error::DecoderBinaryHashInvalid); + } + std::fs::write(decoder_path.clone(), decoder_file_content) + .map_err(|_| Error::DecoderBinaryPathInvalid)?; + } + decoder_path + } + DecoderLocationType::TypeId => { + let mut decoder_path = settings.decoders_cache_directory.clone(); + decoder_path.push(format!("type_id_{}.bin", hex::encode(&decoder.hash))); + if !decoder_path.exists() { + let decoder_binary = fetch_decoder_binary(rpc, decoder.hash.clone().into()).await?; + std::fs::write(decoder_path.clone(), decoder_binary) + .map_err(|_| Error::DecoderBinaryPathInvalid)?; + } + decoder_path + } + }; + Ok(decoder_path) +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs new file mode 100644 index 0000000..2cba564 --- /dev/null +++ b/src/decoder/mod.rs @@ -0,0 +1,113 @@ +use serde_json::Value; + +use crate::{ + client::RpcClient, + types::{ + ClusterDescriptionField, DOBClusterFormatV0, DOBClusterFormatV1, Dob, Error, Settings, + }, +}; + +pub(crate) mod helpers; +use helpers::*; + +pub struct DOBDecoder { + rpc: RpcClient, + settings: Settings, +} + +impl DOBDecoder { + pub fn new(settings: Settings) -> Self { + Self { + rpc: RpcClient::new(&settings.ckb_rpc, &settings.ckb_rpc), + settings, + } + } + + pub fn protocol_versions(&self) -> Vec { + self.settings.protocol_versions.clone() + } + + pub fn setting(&self) -> &Settings { + &self.settings + } + + 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?; + let dob_metadata = fetch_dob_metadata(&self.rpc, &self.settings, cluster_id).await?; + Ok((content, dob_metadata)) + } + + // decode DNA under target spore_id + pub async fn decode_dna( + &self, + dna: &str, + dob_metadata: ClusterDescriptionField, + ) -> 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, + } + } + + // decode specificly for objects under DOB/0 protocol + async fn decode_dob0_dna(&self, dna: &str, dob0: &DOBClusterFormatV0) -> 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(), + pattern => pattern.to_string(), + }; + let raw_render_result = { + let (exit_code, outputs) = crate::vm::execute_riscv_binary( + &decoder_path.to_string_lossy(), + vec![dna.to_owned().into(), pattern.into()], + &self.settings, + ) + .map_err(|_| Error::DecoderExecutionError)?; + #[cfg(feature = "render_debug")] + { + println!("-------- DOB/0 DECODE RESULT ({exit_code}) ---------"); + outputs.iter().for_each(|output| println!("{output}")); + println!("-------- DOB/0 DECODE RESULT END ---------"); + } + if exit_code != 0 { + return Err(Error::DecoderExecutionInternalError); + } + outputs.first().ok_or(Error::DecoderOutputInvalid)?.clone() + }; + Ok(raw_render_result) + } + + // decode specificly for objects under DOB/1 protocol + async fn decode_dob1_dna(&self, dna: &str, dob1: &DOBClusterFormatV1) -> Result { + let traits_output = self.decode_dob0_dna(dna, &dob1.traits).await?; + let image_decoder_path = + parse_decoder_path(&self.rpc, &dob1.images.decoder, &self.settings).await?; + let image_pattern = match &dob1.images.pattern { + Value::String(string) => string.to_owned(), + pattern => pattern.to_string(), + }; + let raw_render_result = { + let (exit_code, outputs) = crate::vm::execute_riscv_binary( + &image_decoder_path.to_string_lossy(), + vec![traits_output.into(), image_pattern.into()], + &self.settings, + ) + .map_err(|_| Error::DecoderExecutionError)?; + #[cfg(feature = "render_debug")] + { + println!("-------- DOB/1 DECODE RESULT ({exit_code}) ---------"); + outputs.iter().for_each(|output| println!("{output}")); + println!("-------- DOB/1 DECODE RESULT END ---------"); + } + if exit_code != 0 { + return Err(Error::DecoderExecutionInternalError); + } + outputs.first().ok_or(Error::DecoderOutputInvalid)?.clone() + }; + Ok(raw_render_result) + } +} diff --git a/src/tests/decoder.rs b/src/tests/dob0/decoder.rs similarity index 97% rename from src/tests/decoder.rs rename to src/tests/dob0/decoder.rs index ea4ba14..65b6bce 100644 --- a/src/tests/decoder.rs +++ b/src/tests/dob0/decoder.rs @@ -4,7 +4,8 @@ use serde_json::{json, Value}; use crate::decoder::DOBDecoder; use crate::tests::prepare_settings; use crate::types::{ - ClusterDescriptionField, DOBClusterFormat, DOBDecoderFormat, DecoderLocationType, + ClusterDescriptionField, DOBClusterFormat, DOBClusterFormatV0, DOBDecoderFormat, + DecoderLocationType, }; const EXPECTED_UNICORN_RENDER_RESULT: &str = "[{\"name\":\"wuxing_yinyang\",\"traits\":[{\"String\":\"3<_>\"}]},{\"name\":\"prev.bgcolor\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['#DBAB00', '#09D3FF', '#A028E9', '#FF3939', '#(135deg, #FE4F4F, #66C084, #00E2E2, #E180E2, #F4EC32)']\"}]},{\"name\":\"prev<%v>\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['#000000', '#000000', '#000000', '#000000', '#000000', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF'])\"}]},{\"name\":\"Spirits\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['Metal, Golden Body', 'Wood, Blue Body', 'Water, White Body', 'Fire, Red Body', 'Earth, Colorful Body']\"}]},{\"name\":\"Yin Yang\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair']\"}]},{\"name\":\"Talents\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['Guard<~>', 'Death<~>', 'Forget<~>', 'Curse<~>', 'Hermit<~>', 'Attack<~>', 'Revival<~>', 'Summon<~>', 'Prophet<~>', 'Crown<~>']\"}]},{\"name\":\"Horn\",\"traits\":[{\"String\":\"(%wuxing_yinyang):['Praetorian Horn', 'Hel Horn', 'Lethe Horn', 'Necromancer Horn', 'Lao Tsu Horn', 'Warrior Horn', 'Shaman Horn', 'Bard Horn', 'Sibyl Horn', 'Caesar Horn']\"}]},{\"name\":\"Wings\",\"traits\":[{\"String\":\"Sun Wings\"}]},{\"name\":\"Tail\",\"traits\":[{\"String\":\"Meteor Tail\"}]},{\"name\":\"Horseshoes\",\"traits\":[{\"String\":\"Silver Horseshoes\"}]},{\"name\":\"Destiny Number\",\"traits\":[{\"Number\":65321}]},{\"name\":\"Lucky Number\",\"traits\":[{\"Number\":35}]}]"; @@ -34,11 +35,10 @@ fn generate_unicorn_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes }; let unicorn_metadata = ClusterDescriptionField { description: "Unicorns are the first series of digital objects generated based on time and space on CKB. Combining the Birth Time-location Determining Destiny Theory, Five Element Theory and YinYang Theory, it provide a special way for people to get Unicorn's on-chain DNA. Now all the seeds(DNAs) are on chain, and a magic world can expand.".to_string(), - dob: DOBClusterFormat { - ver: Some(0), + dob: DOBClusterFormat::new_dob0(DOBClusterFormatV0 { decoder, pattern: serde_json::from_str("[[\"wuxing_yinyang\",\"string\",0,1,\"options\",[\"0<_>\",\"1<_>\",\"2<_>\",\"3<_>\",\"4<_>\",\"5<_>\",\"6<_>\",\"7<_>\",\"8<_>\",\"9<_>\"]],[\"prev.bgcolor\",\"string\",1,1,\"options\",[\"(%wuxing_yinyang):['#DBAB00', '#09D3FF', '#A028E9', '#FF3939', '#(135deg, #FE4F4F, #66C084, #00E2E2, #E180E2, #F4EC32)']\"]],[\"prev<%v>\",\"string\",2,1,\"options\",[\"(%wuxing_yinyang):['#000000', '#000000', '#000000', '#000000', '#000000', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFFFF'])\"]],[\"Spirits\",\"string\",3,1,\"options\",[\"(%wuxing_yinyang):['Metal, Golden Body', 'Wood, Blue Body', 'Water, White Body', 'Fire, Red Body', 'Earth, Colorful Body']\"]],[\"Yin Yang\",\"string\",4,1,\"options\",[\"(%wuxing_yinyang):['Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yin, Long hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair', 'Yang, Short Hair']\"]],[\"Talents\",\"string\",5,1,\"options\",[\"(%wuxing_yinyang):['Guard<~>', 'Death<~>', 'Forget<~>', 'Curse<~>', 'Hermit<~>', 'Attack<~>', 'Revival<~>', 'Summon<~>', 'Prophet<~>', 'Crown<~>']\"]],[\"Horn\",\"string\",6,1,\"options\",[\"(%wuxing_yinyang):['Praetorian Horn', 'Hel Horn', 'Lethe Horn', 'Necromancer Horn', 'Lao Tsu Horn', 'Warrior Horn', 'Shaman Horn', 'Bard Horn', 'Sibyl Horn', 'Caesar Horn']\"]],[\"Wings\",\"string\",7,1,\"options\",[\"Wind Wings\",\"Night Shadow Wings\",\"Lightning Wings\",\"Sun Wings\",\"Golden Wings\",\"Cloud Wings\",\"Morning Glow Wings\",\"Star Wings\",\"Spring Wings\",\"Moon Wings\",\"Angel Wings\"]],[\"Tail\",\"string\",8,1,\"options\",[\"Meteor Tail\",\"Rainbow Tail\",\"Willow Tail\",\"Phoenix Tail\",\"Sunset Shadow Tail\",\"Socrates Tail\",\"Dumbledore Tail\",\"Venus Tail\",\"Gaia Tail\"]],[\"Horseshoes\",\"string\",9,1,\"options\",[\"Ice Horseshoes\",\"Crystal Horseshoes\",\"Maple Horseshoes\",\"Flame Horseshoes\",\"Thunder Horseshoes\",\"Lotus Horseshoes\",\"Silver Horseshoes\"]],[\"Destiny Number\",\"number\",10,4,\"range\",[50000,100000]],[\"Lucky Number\",\"number\",14,1,\"range\",[1,49]]]").unwrap(), - }, + }), }; (unicorn_content, unicorn_metadata) } @@ -62,11 +62,10 @@ fn generate_example_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes }; let unicorn_metadata = ClusterDescriptionField { description: "DOB/0 example.".to_string(), - dob: DOBClusterFormat { - ver: Some(0), + dob: DOBClusterFormat::new_dob0(DOBClusterFormatV0 { decoder, pattern: serde_json::from_str("[[\"Name\",\"string\",0,1,\"options\",[\"Alice\",\"Bob\",\"Charlie\",\"David\",\"Ethan\",\"Florence\",\"Grace\",\"Helen\"]],[\"Age\",\"number\",1,1,\"range\",[0,100]],[\"Score\",\"number\",2,1,\"raw\"],[\"DNA\",\"string\",3,3,\"raw\"],[\"URL\",\"string\",6,21,\"utf8\"],[\"Value\",\"number\",3,3,\"raw\"]]").unwrap(), - }, + }), }; (unicorn_content, unicorn_metadata) } diff --git a/src/tests/legacy_decoder.rs b/src/tests/dob0/legacy_decoder.rs similarity index 98% rename from src/tests/legacy_decoder.rs rename to src/tests/dob0/legacy_decoder.rs index 2d435c6..d511e9b 100644 --- a/src/tests/legacy_decoder.rs +++ b/src/tests/dob0/legacy_decoder.rs @@ -1,9 +1,10 @@ use ckb_types::{h256, H256}; -use crate::decoder::{decode_spore_data, DOBDecoder}; +use crate::decoder::{helpers::decode_spore_data, DOBDecoder}; use crate::tests::prepare_settings; use crate::types::{ - ClusterDescriptionField, DOBClusterFormat, DOBDecoderFormat, DecoderLocationType, + ClusterDescriptionField, DOBClusterFormat, DOBClusterFormatV0, DOBDecoderFormat, + DecoderLocationType, }; use serde_json::{json, Value}; @@ -30,11 +31,10 @@ fn generate_nervape_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes }; let nervape_metadata = ClusterDescriptionField { description: "Nervape, multi-chain composable digital objects built on Bitcoin.".to_string(), - dob: DOBClusterFormat { - ver: Some(0), + dob: DOBClusterFormat::new_dob0(DOBClusterFormatV0 { decoder, pattern: Value::String("830900004400000087000000370500004206000085060000c2060000050700004807000089070000c6070000060800004408000081080000c00800000209000043090000430000000c0000001900000009000000707265762e747970652a00000008000000220000000c0000000d0000000100000000110000000800000005000000696d616765b00400000c0000001700000007000000707265762e62679904000008000000910400000c0000000d0000000100000000800400003c0000008a000000d80000002601000074010000c2010000100200005e020000ac020000fa0200004803000096030000e4030000320400004a00000062746366733a2f2f3162633234333531613064663265363836353734636431623633343661316635356638316366663061326535323037386136653361643061333563666238333369304a00000062746366733a2f2f3634663536326431366532613461323965386334383231333730666666343733656466613232633236656635383038616462323430346533396463303133653569304a00000062746366733a2f2f6332396665636436643764376565633063623361326233646664636236616132363038316462386639383531313130623763323061306633633631373239396169304a00000062746366733a2f2f3539653837636131373765663066643435376538376539663933363237363630303232636635313962353331653166346533613664646139653565333338323769304a00000062746366733a2f2f6133353839646463663462376133633664613532666536616534656433323936663165646531333966653931323766323639376365306463663237303362363169304a00000062746366733a2f2f3739393732396666366131366464366166353764623161386361363134366435363733613330616439613539373664643836316433343861356565633238633469304a00000062746366733a2f2f3838646432616230356262386639633732646134326166633730363737616330356634373665313765306631363535316463303036333561653765393534366569304a00000062746366733a2f2f6233326533626262373363623837376339623431313532393933306135623665623332383039323762323832633132343836636532363930316233633232393169304a00000062746366733a2f2f6138623139646461623333386462306335326639613238346237643935666665616130646533346530623837343137373930316562393265306639663964386469304a00000062746366733a2f2f6261386231626239643862616565346266323461303666616132356235363934313066326462393662343633396638653038636362656330356338386437396269304a00000062746366733a2f2f6161383938366630656636363738303764346232333937306536343834346464653366303632323534326237396135633330323533396465306333356233316569304a00000062746366733a2f2f3130306637653066303936356463353435313561333833316133323038383133313563663563613634616430316265643262343232363136623135666433313469304a00000062746366733a2f2f6238346563306337373061613139363161336439343938656138613637653132383235333239313366633163313365336561663561343864653231363466623969304a00000062746366733a2f2f6130366261326531363134613530393931373665356363346439356465373663626562343730356138626437653134323333363237386562633239306664623369300b0100000c0000001c0000000c000000707265762e6267636f6c6f72ef00000008000000e70000000c0000000d0000000100000000d60000003c00000047000000520000005d00000068000000730000007e00000089000000940000009f000000aa000000b5000000c0000000cb00000007000000234646453345420700000023464643324645070000002343454241463707000000234237453646390700000023414246344430070000002345304446424407000000234639463741370700000023453242453931070000002346394336363207000000234637443642320700000023464341383633070000002346394143414307000000234530453145320700000023413341374141430000000c0000001a0000000a0000004261636b67726f756e642900000008000000210000000c0000000d00000001030000000000000000000000ff000000000000003d0000000c0000001400000004000000537569742900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000430000000c0000001a0000000a000000557070657220626f64792900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000430000000c0000001a0000000a0000004c6f77657220626f64792900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000410000000c000000180000000800000048656164776561722900000008000000210000000c0000000d00000001030000000000000000000000ff000000000000003d0000000c00000014000000040000004d61736b2900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000400000000c0000001700000007000000457965776561722900000008000000210000000c0000000d00000001030000000000000000000000ff000000000000003e0000000c00000015000000050000004d6f7574682900000008000000210000000c0000000d00000001030000000000000000000000ff000000000000003d0000000c0000001400000004000000456172732900000008000000210000000c0000000d00000001030000000000000000000000ff000000000000003f0000000c0000001600000006000000546174746f6f2900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000420000000c00000019000000090000004163636573736f72792900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000410000000c000000180000000800000048616e6468656c642900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000400000000c00000017000000070000005370656369616c2900000008000000210000000c0000000d00000001030000000000000000000000ff00000000000000".to_string()), - }, + }), }; (nervape_content, nervape_metadata) } @@ -58,11 +58,10 @@ fn generate_unicorn_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes }; let unicorn_metadata = ClusterDescriptionField { description: "Unicorns are the first series of digital objects generated based on time and space on CKB. Combining the Birth Time-location Determining Destiny Theory, Five Element Theory and YinYang Theory, it provide a special way for people to get Unicorn's on-chain DNA. Now all the seeds(DNAs) are on chain, and a magic world can expand.".to_string(), - dob: DOBClusterFormat { - ver: Some(0), + dob: DOBClusterFormat::new_dob0(DOBClusterFormatV0 { decoder, pattern: Value::String("3d09000034000000e7000000a00100005e0200001403000021040000ef040000d4050000e6060000cf070000b1080000f8080000b30000000c0000001e0000000e000000777578696e675f79696e79616e6795000000080000008d0000000c0000000d00000001000000007c0000002c000000340000003c000000440000004c000000540000005c000000640000006c0000007400000004000000303c5f3e04000000313c5f3e04000000323c5f3e04000000333c5f3e04000000343c5f3e04000000353c5f3e04000000363c5f3e04000000373c5f3e04000000383c5f3e04000000393c5f3eb90000000c0000001c0000000c000000707265762e6267636f6c6f729d00000008000000950000000c0000000d00000001000000008400000008000000780000002825777578696e675f79696e79616e67293a5b2723444241423030272c202723303944334646272c202723413032384539272c202723464633393339272c202723283133356465672c20234645344634462c20233636433038342c20233030453245322c20234531383045322c202346344543333229275dbe0000000c0000001800000008000000707265763c25763ea6000000080000009e0000000c0000000d00000001000000008d00000008000000810000002825777578696e675f79696e79616e67293a5b2723303030303030272c202723303030303030272c202723303030303030272c202723303030303030272c202723303030303030272c202723464646464646272c202723464646464646272c202723464646464646272c202723464646464646272c202723464646464646275d29b60000000c0000001700000007000000537069726974739f00000008000000970000000c0000000d000000010000000086000000080000007a0000002825777578696e675f79696e79616e67293a5b274d6574616c2c20476f6c64656e20426f6479272c2027576f6f642c20426c756520426f6479272c202757617465722c20576869746520426f6479272c2027466972652c2052656420426f6479272c202745617274682c20436f6c6f7266756c20426f6479275d0d0100000c000000180000000800000059696e2059616e67f500000008000000ed0000000c0000000d0000000100000000dc00000008000000d00000002825777578696e675f79696e79616e67293a5b2759696e2c204c6f6e672068616972272c202759696e2c204c6f6e672068616972272c202759696e2c204c6f6e672068616972272c202759696e2c204c6f6e672068616972272c202759696e2c204c6f6e672068616972272c202759616e672c2053686f72742048616972272c202759616e672c2053686f72742048616972272c202759616e672c2053686f72742048616972272c202759616e672c2053686f72742048616972272c202759616e672c2053686f72742048616972275dce0000000c000000170000000700000054616c656e7473b700000008000000af0000000c0000000d00000001000000009e00000008000000920000002825777578696e675f79696e79616e67293a5b2747756172643c7e3e272c202744656174683c7e3e272c2027466f726765743c7e3e272c202743757273653c7e3e272c20274865726d69743c7e3e272c202741747461636b3c7e3e272c20275265766976616c3c7e3e272c202753756d6d6f6e3c7e3e272c202750726f706865743c7e3e272c202743726f776e3c7e3e275de50000000c0000001400000004000000486f726ed100000008000000c90000000c0000000d0000000100000000b800000008000000ac0000002825777578696e675f79696e79616e67293a5b2750726165746f7269616e20486f726e272c202748656c20486f726e272c20274c6574686520486f726e272c20274e6563726f6d616e63657220486f726e272c20274c616f2054737520486f726e272c202757617272696f7220486f726e272c20275368616d616e20486f726e272c20274261726420486f726e272c2027536962796c20486f726e272c202743616573617220486f726e275d120100000c000000150000000500000057696e6773fd00000008000000f50000000c0000000d0000000100000000e4000000300000003e0000005400000067000000740000008400000093000000a9000000b7000000c7000000d50000000a00000057696e642057696e6773120000004e6967687420536861646f772057696e67730f0000004c696768746e696e672057696e67730900000053756e2057696e67730c000000476f6c64656e2057696e67730b000000436c6f75642057696e6773120000004d6f726e696e6720476c6f772057696e67730a000000537461722057696e67730c000000537072696e672057696e67730a0000004d6f6f6e2057696e67730b000000416e67656c2057696e6773e90000000c00000015000000050000005461696c73d400000008000000cc0000000c0000000d0000000100000000bb00000028000000370000004700000056000000660000007c0000008d000000a0000000ae0000000b0000004d6574656f72205461696c0c0000005261696e626f77205461696c0b00000057696c6c6f77205461696c0c00000050686f656e6978205461696c1200000053756e73657420536861646f77205461696c0d000000536f637261746573205461696c0f00000044756d626c65646f7265205461696c0a00000056656e7573205461696c0900000047616961205461696ce20000000c0000001a0000000a000000486f72736573686f6573c800000008000000c00000000c0000000d0000000100000000af0000002000000032000000480000005c00000070000000860000009a0000000e00000049636520486f72736573686f6573120000004372797374616c20486f72736573686f6573100000004d61706c6520486f72736573686f657310000000466c616d6520486f72736573686f6573120000005468756e64657220486f72736573686f6573100000004c6f74757320486f72736573686f65731100000053696c76657220486f72736573686f6573470000000c0000001e0000000e00000044657374696e79204e756d6265722900000008000000210000000c0000000d000000040300000050c3000000000000a086010000000000450000000c0000001c0000000c0000004c75636b79204e756d6265722900000008000000210000000c0000000d000000010300000001000000000000003100000000000000".to_string()), - }, + }), }; (unicorn_content, unicorn_metadata) } diff --git a/src/tests/dob0/mod.rs b/src/tests/dob0/mod.rs new file mode 100644 index 0000000..4685c1b --- /dev/null +++ b/src/tests/dob0/mod.rs @@ -0,0 +1,2 @@ +mod decoder; +mod legacy_decoder; diff --git a/src/tests/mod.rs b/src/tests/mod.rs index dcc0f7d..ec87759 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -2,8 +2,7 @@ use ckb_types::h256; use crate::types::{HashType, OnchainDecoderDeployment, ScriptId, Settings}; -mod decoder; -mod legacy_decoder; +mod dob0; fn prepare_settings(version: &str) -> Settings { Settings { diff --git a/src/types.rs b/src/types.rs index ce84d6f..fc31f60 100644 --- a/src/types.rs +++ b/src/types.rs @@ -71,6 +71,11 @@ pub enum Error { SystemTimeError, } +pub enum Dob<'a> { + V0(&'a DOBClusterFormatV0), + V1(&'a DOBClusterFormatV1), +} + #[cfg(feature = "standalone_server")] impl From for ErrorCode { fn from(value: Error) -> Self { @@ -86,17 +91,80 @@ pub struct ClusterDescriptionField { pub dob: DOBClusterFormat, } +impl ClusterDescriptionField { + pub fn unbox_dob(&self) -> Result { + match self.dob.ver { + Some(0) | None => { + let dob0 = self + .dob + .dob_ver_0 + .as_ref() + .ok_or(Error::ClusterDataUncompatible)?; + Ok(Dob::V0(dob0)) + } + Some(1) => { + let dob1 = self + .dob + .dob_ver_1 + .as_ref() + .ok_or(Error::ClusterDataUncompatible)?; + Ok(Dob::V1(dob1)) + } + _ => Err(Error::DOBVersionUnexpected), + } + } +} + // contains `decoder` and `pattern` identifiers +// +// note: if `ver` is empty, `dob_ver_0` must uniquely exist #[derive(Deserialize)] #[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] pub struct DOBClusterFormat { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub ver: Option, + #[serde(flatten)] + pub dob_ver_0: Option, + #[serde(flatten)] + pub dob_ver_1: Option, +} + +#[cfg(test)] +impl DOBClusterFormat { + #[allow(dead_code)] + pub fn new_dob0(dob_ver_0: DOBClusterFormatV0) -> Self { + Self { + ver: Some(0), + dob_ver_0: Some(dob_ver_0), + dob_ver_1: None, + } + } + + #[allow(dead_code)] + pub fn new_dob1(dob_ver_1: DOBClusterFormatV1) -> Self { + Self { + ver: Some(1), + dob_ver_0: None, + dob_ver_1: Some(dob_ver_1), + } + } +} + +#[derive(Deserialize)] +#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +pub struct DOBClusterFormatV0 { pub decoder: DOBDecoderFormat, pub pattern: Value, } +#[derive(Deserialize)] +#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +pub struct DOBClusterFormatV1 { + pub traits: DOBClusterFormatV0, + pub images: DOBClusterFormatV0, +} + // restricted decoder locator type #[derive(Deserialize)] #[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] @@ -140,8 +208,8 @@ pub enum HashType { } impl From<&HashType> for ScriptHashType { - fn from(value: &HashType) -> Self { - match value { + fn from(hash_type: &HashType) -> ScriptHashType { + match hash_type { HashType::Data => ScriptHashType::Data, HashType::Data1 => ScriptHashType::Data1, HashType::Data2 => ScriptHashType::Data2, @@ -163,11 +231,14 @@ pub struct ScriptId { pub struct Settings { pub protocol_versions: Vec, pub ckb_rpc: String, + pub image_fetcher_url: String, pub rpc_server_address: String, pub ckb_vm_runner: String, pub decoders_cache_directory: PathBuf, pub dobs_cache_directory: PathBuf, pub dobs_cache_expiration_sec: u64, + pub dob1_max_combination: usize, + pub dob1_max_cache_size: usize, pub onchain_decoder_deployment: Vec, pub available_spores: Vec, pub available_clusters: Vec, diff --git a/src/vm.rs b/src/vm.rs index 537dacd..4cad951 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -3,9 +3,14 @@ use std::sync::{Arc, Mutex}; use ckb_vm::cost_model::estimate_cycles; -use ckb_vm::registers::{A0, A7}; +use ckb_vm::registers::{A0, A1, A2, A3, A4, A7}; use ckb_vm::{Bytes, Memory, Register, SupportMachine, Syscalls}; +use image::load_from_memory; +use crate::client::ImageFetchClient; +use crate::types::Settings; + +// ckb-vm syscall for printing debug information struct DebugSyscall { output: Arc>>, } @@ -46,14 +51,113 @@ impl Syscalls for DebugSyscall { } } +// ckb-vm syscall for image combination +struct ImageCombinationSyscall { + client: ImageFetchClient, + max_combination: usize, +} + +impl Syscalls for ImageCombinationSyscall { + 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 images_uri_array_addr = machine.registers()[A2].to_u64(); + let images_uri_array_count = machine.registers()[A3].to_u64(); + let iamges_uri_array_uint_size = machine.registers()[A4].to_u64(); + + // parse all of images uri + let array_size = images_uri_array_count * iamges_uri_array_uint_size; + let images_uri_array_bytes = machine + .memory_mut() + .load_bytes(images_uri_array_addr, array_size)?; + let images_uri_array = images_uri_array_bytes + .chunks_exact(iamges_uri_array_uint_size as usize) + .map(|uri_bytes| String::from_utf8_lossy(uri_bytes).to_string()) + .collect::>(); + + #[cfg(feature = "render_debug")] + { + println!("-------- DOB/1 IMAGES ---------"); + images_uri_array.iter().for_each(|uri| println!("{uri}")); + println!("-------- DOB/1 IMAGES END ---------"); + } + + // fetch images from uri + let mut images = futures::executor::block_on(self.client.fetch_images(&images_uri_array)) + .map_err(|_| ckb_vm::error::Error::Unexpected("failed to fetch images".to_owned()))? + .into_iter() + .map(|image| load_from_memory(&image)) + .collect::, _>>() + .map_err(|_| ckb_vm::error::Error::Unexpected("failed to parse images".to_owned()))?; + if images.is_empty() { + return Err(ckb_vm::error::Error::Unexpected("empty images".to_owned())); + } + if images.len() > self.max_combination { + return Err(ckb_vm::error::Error::Unexpected( + "exceesive images".to_owned(), + )); + } + + // resize images to the maximum + let mut max_width = 0; + let mut max_height = 0; + images.iter().for_each(|image| { + max_width = max_width.max(image.width()); + max_height = max_height.max(image.height()); + }); + images.iter_mut().for_each(|image| { + image.resize(max_width, max_height, image::imageops::FilterType::Nearest); + }); + + // combine images into a single one + let mut combination = images.remove(0); + if buffer_size == 0 { + machine.memory_mut().store64( + &buffer_size_addr, + &Mac::REG::from_u64(combination.as_bytes().len() as u64), + )?; + return Ok(true); + } + images.into_iter().for_each(|image| { + image::imageops::overlay(&mut combination, &image, 0, 0); + }); + + // return output + let output = combination.as_bytes(); + let buffer_size = buffer_size.min(output.len() as u64); + machine.memory_mut().store_bytes(buffer_addr, output)?; + machine + .memory_mut() + .store64(&buffer_size_addr, &Mac::REG::from_u64(buffer_size))?; + + Ok(true) + } +} + fn main_asm( code: Bytes, args: Vec, + 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 image = Box::new(ImageCombinationSyscall { + client: ImageFetchClient::new(&settings.image_fetcher_url, settings.dob1_max_cache_size), + max_combination: settings.dob1_max_combination, + }); 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 +167,7 @@ fn main_asm( let core = ckb_vm::DefaultMachineBuilder::new(asm_core) .instruction_cycle_func(Box::new(estimate_cycles)) .syscall(debug) + .syscall(image) .build(); let mut machine = ckb_vm::machine::asm::AsmMachine::new(core); machine.load_program(&code, &args)?; @@ -75,7 +180,8 @@ fn main_asm( pub fn execute_riscv_binary( binary_path: &str, args: Vec, + settings: &Settings, ) -> Result<(i8, Vec), Box> { let code = std::fs::read(binary_path)?.into(); - main_asm(code, args) + main_asm(code, args, settings) } From c4394314e0687860cef18b909374f2284e03ef61 Mon Sep 17 00:00:00 2001 From: liyukun Date: Thu, 6 Jun 2024 11:35:17 +0800 Subject: [PATCH 02/17] feat: add dob1 testcase --- src/tests/dob1/decoder.rs | 0 src/tests/dob1/mod.rs | 1 + src/tests/mod.rs | 4 ++++ 3 files changed, 5 insertions(+) create mode 100644 src/tests/dob1/decoder.rs create mode 100644 src/tests/dob1/mod.rs diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/dob1/mod.rs b/src/tests/dob1/mod.rs new file mode 100644 index 0000000..f201bd0 --- /dev/null +++ b/src/tests/dob1/mod.rs @@ -0,0 +1 @@ +mod decoder; diff --git a/src/tests/mod.rs b/src/tests/mod.rs index ec87759..e4f99fd 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -3,14 +3,18 @@ use ckb_types::h256; use crate::types::{HashType, OnchainDecoderDeployment, ScriptId, Settings}; mod dob0; +mod dob1; fn prepare_settings(version: &str) -> Settings { Settings { ckb_rpc: "https://testnet.ckbapp.dev/".to_string(), + image_fetcher_url: "https://dobfs.dobby.market/testnet".to_string(), protocol_versions: vec![version.to_string()], ckb_vm_runner: "ckb-vm-runner".to_string(), decoders_cache_directory: "cache/decoders".parse().unwrap(), dobs_cache_directory: "cache/dobs".parse().unwrap(), + dob1_max_combination: 5, + dob1_max_cache_size: 100, available_spores: vec![ ScriptId { code_hash: h256!( From 0497a3c4a30cbe70e73b8a69d76a78ab765bdbf6 Mon Sep 17 00:00:00 2001 From: liyukun Date: Thu, 6 Jun 2024 17:41:39 +0800 Subject: [PATCH 03/17] feat: complete decoder test case for dob1 --- ...00000000000000000000000000000000000000.bin | Bin 0 -> 21912 bytes settings.mainnet.toml | 2 +- settings.toml | 2 +- src/client.rs | 104 ++++++++++++++---- src/decoder/mod.rs | 4 +- src/tests/dob0/decoder.rs | 6 +- src/tests/dob1/decoder.rs | 62 +++++++++++ src/tests/mod.rs | 2 +- src/types.rs | 12 ++ src/vm.rs | 40 +++++-- 10 files changed, 191 insertions(+), 43 deletions(-) create mode 100755 cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin diff --git a/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin b/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin new file mode 100755 index 0000000000000000000000000000000000000000..5137253737a60a0dd2fbe80498a46826f68bcbe1 GIT binary patch literal 21912 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 literal 0 HcmV?d00001 diff --git a/settings.mainnet.toml b/settings.mainnet.toml index 3b7735a..f679cdd 100644 --- a/settings.mainnet.toml +++ b/settings.mainnet.toml @@ -7,7 +7,7 @@ protocol_versions = [ ckb_rpc = "https://mainnet.ckb.dev/" # connect to the image fetcher service -image_fetcher_url = "https://dobfs.dobby.market" +image_fetcher_url = "https://mempool.space/api/tx/" # address that rpc server running at in case of standalone server mode rpc_server_address = "0.0.0.0:8090" diff --git a/settings.toml b/settings.toml index 75bacbc..11b6293 100644 --- a/settings.toml +++ b/settings.toml @@ -7,7 +7,7 @@ protocol_versions = [ ckb_rpc = "https://testnet.ckbapp.dev/" # connect to the image fetcher service -image_fetcher_url = "https://dobfs.dobby.market/testnet" +image_fetcher_url = "https://mempool.space/testnet/api/tx/" # address that rpc server running at in case of standalone server mode rpc_server_address = "0.0.0.0:8090" diff --git a/src/client.rs b/src/client.rs index 2fbf660..27f74ef 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,3 +1,5 @@ +#![allow(clippy::assigning_clones)] + use std::collections::VecDeque; use std::future::Future; use std::pin::Pin; @@ -6,9 +8,9 @@ use std::sync::Arc; use ckb_jsonrpc_types::{CellWithStatus, JsonBytes, OutPoint, Uint32}; use ckb_sdk::rpc::ckb_indexer::{Cell, Order, Pagination, SearchKey}; -use ckb_vm::Bytes; use jsonrpc_core::futures::FutureExt; use reqwest::{Client, Url}; +use serde_json::Value; use crate::types::Error; @@ -116,9 +118,8 @@ impl RpcClient { } pub struct ImageFetchClient { - raw: Client, base_url: Url, - images_cache: VecDeque<(Url, Bytes)>, + images_cache: VecDeque<(Url, Vec)>, max_cache_size: usize, } @@ -126,7 +127,6 @@ impl ImageFetchClient { pub fn new(base_url: &str, cache_size: usize) -> Self { let base_url = Url::parse(base_url).expect("base url, e.g. \"http://127.0.0.1"); Self { - raw: Client::new(), base_url, images_cache: VecDeque::new(), max_cache_size: cache_size, @@ -134,28 +134,23 @@ impl ImageFetchClient { } pub async fn fetch_images(&mut self, images_uri: &[String]) -> Result>, Error> { - let requests = images_uri - .iter() - .map(|uri| self.base_url.join(uri).expect("valid url")) - .map(|url| { - let image = self.images_cache.iter().find(|v| v.0 == url); - if let Some((_, image)) = image { - async move { Ok((url, true, image.clone())) }.boxed() - } else { - let send = self.raw.get(url.clone()).send(); + let mut requests = vec![]; + for uri in images_uri { + let (tx_hash, index) = parse_uri(uri)?; + let url = self.base_url.join(&tx_hash).expect("image url"); + let cached_image = self.images_cache.iter().find(|(v, _)| v == &url); + if let Some((_, image)) = cached_image { + requests.push(async { Ok((url, true, image.clone())) }.boxed()); + } else { + requests.push( async move { - let bytes = send - .await - .map_err(|_| Error::JsonRpcRequestError)? - .bytes() - .await - .map_err(|_| Error::JsonRpcRequestError)?; - Ok((url, false, bytes)) + let image = parse_image_from_btcfs(&url, index).await?; + Ok((url, false, image)) } - .boxed() - } - }) - .collect::>(); + .boxed(), + ); + } + } let mut images = vec![]; let responses = futures::future::join_all(requests).await; for response in responses { @@ -171,3 +166,64 @@ impl ImageFetchClient { Ok(images) } } + +fn parse_uri(uri: &str) -> Result<(String, usize), Error> { + let header = "xxxfs://".len(); + let body = uri.chars().skip(header).collect::(); + let parts: Vec<&str> = body.split('i').collect::>(); + if parts.len() != 2 { + return Err(Error::InvalidOnchainFsuriFormat); + } + Ok(( + parts[0].to_string(), + parts[1] + .parse() + .map_err(|_| Error::InvalidOnchainFsuriFormat)?, + )) +} + +async fn parse_image_from_btcfs(url: &Url, index: usize) -> Result, Error> { + // parse btc transaction + let btc_tx = reqwest::get(url.clone()) + .await + .map_err(|_| Error::FetchFromBtcNodeError)? + .json::() + .await + .map_err(|_| Error::FetchFromBtcNodeError)?; + let vin = btc_tx + .get("vin") + .ok_or(Error::InvalidBtcTransactionFormat)? + .as_array() + .ok_or(Error::InvalidBtcTransactionFormat)? + .get(index) + .ok_or(Error::InvalidBtcTransactionFormat)?; + let mut witness = vin + .get("inner_witnessscript_asm") + .ok_or(Error::InvalidBtcTransactionFormat)? + .as_str() + .ok_or(Error::InvalidBtcTransactionFormat)? + .to_owned(); + + // parse inscription body + let mut images = vec![]; + let header = "OP_IF OP_PUSHBYTES_3 444f42 OP_PUSHBYTES_1 01 OP_PUSHBYTES_9 696d6167652f706e67 OP_0 OP_PUSHDATA2 "; + while let (Some(start), Some(end)) = (witness.find("OP_IF"), witness.find("OP_ENDIF")) { + let inscription = &witness[start..end + "OP_ENDIF".len()]; + if !inscription.contains(header) { + return Err(Error::InvalidInscriptionFormat); + } + let hexed_image = inscription + .replace(header, "") + .replace(" OP_ENDIF", "") + .replace(" OP_PUSHDATA2 ", ""); + let image = + hex::decode(hexed_image).map_err(|_| Error::InvalidInscriptionContentHexFormat)?; + images.push(image); + witness = witness[end + "OP_ENDIF".len()..].to_owned(); + } + if images.is_empty() { + return Err(Error::EmptyInscriptionContent); + } + + Ok(images.remove(0)) +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 2cba564..3c13acd 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -71,7 +71,7 @@ impl DOBDecoder { { println!("-------- DOB/0 DECODE RESULT ({exit_code}) ---------"); outputs.iter().for_each(|output| println!("{output}")); - println!("-------- DOB/0 DECODE RESULT END ---------"); + println!("-------- DOB/0 DECODE RESULT END ---------\n"); } if exit_code != 0 { return Err(Error::DecoderExecutionInternalError); @@ -101,7 +101,7 @@ impl DOBDecoder { { println!("-------- DOB/1 DECODE RESULT ({exit_code}) ---------"); outputs.iter().for_each(|output| println!("{output}")); - println!("-------- DOB/1 DECODE RESULT END ---------"); + println!("-------- DOB/1 DECODE RESULT END ---------\n"); } if exit_code != 0 { return Err(Error::DecoderExecutionInternalError); diff --git a/src/tests/dob0/decoder.rs b/src/tests/dob0/decoder.rs index 65b6bce..07ce6f7 100644 --- a/src/tests/dob0/decoder.rs +++ b/src/tests/dob0/decoder.rs @@ -44,7 +44,7 @@ fn generate_unicorn_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes } fn generate_example_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDescriptionField) { - let unicorn_content = json!({ + let example_content = json!({ "block_number": 120, "cell_id": 11844, "dna": "df4ffcb5e7a283ea7e6f09a504d0e256" @@ -60,14 +60,14 @@ fn generate_example_dob_ingredients(onchain_decoder: bool) -> (Value, ClusterDes hash: h256!("0x32f29aba4b17f3d05bec8cec55d50ef86766fd0bf82fdedaa14269f344d3784a"), } }; - let unicorn_metadata = ClusterDescriptionField { + let example_metadata = ClusterDescriptionField { description: "DOB/0 example.".to_string(), dob: DOBClusterFormat::new_dob0(DOBClusterFormatV0 { decoder, pattern: serde_json::from_str("[[\"Name\",\"string\",0,1,\"options\",[\"Alice\",\"Bob\",\"Charlie\",\"David\",\"Ethan\",\"Florence\",\"Grace\",\"Helen\"]],[\"Age\",\"number\",1,1,\"range\",[0,100]],[\"Score\",\"number\",2,1,\"raw\"],[\"DNA\",\"string\",3,3,\"raw\"],[\"URL\",\"string\",6,21,\"utf8\"],[\"Value\",\"number\",3,3,\"raw\"]]").unwrap(), }), }; - (unicorn_content, unicorn_metadata) + (example_content, example_metadata) } #[tokio::test] diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs index e69de29..0489361 100644 --- a/src/tests/dob1/decoder.rs +++ b/src/tests/dob1/decoder.rs @@ -0,0 +1,62 @@ +use ckb_types::h256; + +use crate::client::ImageFetchClient; +use crate::decoder::DOBDecoder; +use crate::tests::prepare_settings; +use crate::types::{ + ClusterDescriptionField, DOBClusterFormat, DOBClusterFormatV0, DOBClusterFormatV1, + DOBDecoderFormat, DecoderLocationType, +}; +use serde_json::{json, Value}; + +fn generate_dob1_ingredients() -> (Value, ClusterDescriptionField) { + let content = json!({ + "dna": "ac7b88aabbcc687474703a2f2f3132372e302e302e313a383039300000" + }); + let metadata = ClusterDescriptionField { + description: "DOB/1 Test".to_string(), + dob: DOBClusterFormat::new_dob1(DOBClusterFormatV1 { + traits: DOBClusterFormatV0 { + decoder: DOBDecoderFormat { + location: DecoderLocationType::CodeHash, + hash: h256!( + "0x32f29aba4b17f3d05bec8cec55d50ef86766fd0bf82fdedaa14269f344d3784a" + ), + }, + pattern: serde_json::from_str("[[\"Name\",\"string\",0,1,\"options\",[\"Alice\",\"Bob\",\"Charlie\",\"David\",\"Ethan\",\"Florence\",\"Grace\",\"Helen\"]],[\"Age\",\"number\",1,1,\"range\",[0,100]],[\"Score\",\"number\",2,1,\"raw\"],[\"DNA\",\"string\",3,3,\"raw\"],[\"URL\",\"string\",6,21,\"utf8\"],[\"Value\",\"number\",3,3,\"raw\"]]").unwrap(), + }, + images: DOBClusterFormatV0 { + decoder: DOBDecoderFormat { + location: DecoderLocationType::CodeHash, + hash: h256!( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ), + }, + pattern: Value::String("hello world".to_string()), + }, + }), + }; + (content, metadata) +} + +#[tokio::test] +async fn check_fetched_image() { + let mut fetcher = ImageFetchClient::new("https://mempool.space/api/tx/", 100); + let uris = vec![ + "btcfs://b2f4560f17679d3e3fca66209ac425c660d28a252ef72444c3325c6eb0364393i0".to_string(), + ]; + let images = fetcher.fetch_images(&uris).await.expect("fetch images"); + let image_raw_bytes = images.first().expect("image"); + image::load_from_memory(&image_raw_bytes).expect("load image"); +} + +#[tokio::test] +async fn test_dob1() { + let settings = prepare_settings("text/plain"); + let decoder = DOBDecoder::new(settings); + let (content, metadata) = generate_dob1_ingredients(); + decoder + .decode_dna(&content["dna"].as_str().unwrap(), metadata) + .await + .expect("decode dob/1"); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index e4f99fd..1a818c1 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -8,7 +8,7 @@ mod dob1; fn prepare_settings(version: &str) -> Settings { Settings { ckb_rpc: "https://testnet.ckbapp.dev/".to_string(), - image_fetcher_url: "https://dobfs.dobby.market/testnet".to_string(), + image_fetcher_url: "https://mempool.space/api/tx/".to_string(), protocol_versions: vec![version.to_string()], ckb_vm_runner: "ckb-vm-runner".to_string(), decoders_cache_directory: "cache/decoders".parse().unwrap(), diff --git a/src/types.rs b/src/types.rs index fc31f60..6055536 100644 --- a/src/types.rs +++ b/src/types.rs @@ -69,6 +69,18 @@ pub enum Error { JsonRpcRequestError, #[error("error ocurred while requiring system timestamp")] SystemTimeError, + #[error("BTC node responsed bad")] + FetchFromBtcNodeError, + #[error("BTC transaction format broken")] + InvalidBtcTransactionFormat, + #[error("Inscription format broken")] + InvalidInscriptionFormat, + #[error("Inscription content must be hex format")] + InvalidInscriptionContentHexFormat, + #[error("Inscription content must be filled")] + EmptyInscriptionContent, + #[error("fs header like 'btcfs://' and 'ckbfs://' are not contained")] + InvalidOnchainFsuriFormat, } pub enum Dob<'a> { diff --git a/src/vm.rs b/src/vm.rs index 4cad951..baa65d4 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -1,6 +1,7 @@ // refer to https://github.com/nervosnetwork/ckb-vm/blob/develop/examples/ckb-vm-runner.rs -use std::sync::{Arc, Mutex}; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; use ckb_vm::cost_model::estimate_cycles; use ckb_vm::registers::{A0, A1, A2, A3, A4, A7}; @@ -10,6 +11,14 @@ use image::load_from_memory; use crate::client::ImageFetchClient; use crate::types::Settings; +macro_rules! error { + ($err: expr) => {{ + let error = $err.to_string(); + println!("[DOB/1 ERROR] {error}"); + ckb_vm::error::Error::Unexpected(error) + }}; +} + // ckb-vm syscall for printing debug information struct DebugSyscall { output: Arc>>, @@ -53,7 +62,7 @@ impl Syscalls for DebugSyscall { // ckb-vm syscall for image combination struct ImageCombinationSyscall { - client: ImageFetchClient, + client: Arc>, max_combination: usize, } @@ -90,23 +99,31 @@ impl Syscalls for ImageCombinationSyscall { { println!("-------- DOB/1 IMAGES ---------"); images_uri_array.iter().for_each(|uri| println!("{uri}")); - println!("-------- DOB/1 IMAGES END ---------"); + println!("-------- DOB/1 IMAGES END ---------\n"); } // fetch images from uri - let mut images = futures::executor::block_on(self.client.fetch_images(&images_uri_array)) - .map_err(|_| ckb_vm::error::Error::Unexpected("failed to fetch images".to_owned()))? + let client = self.client.clone(); + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let mut client = client.lock().unwrap(); + tx.send(rt.block_on(client.fetch_images(&images_uri_array))) + .expect("send"); + }); + let mut images = rx + .recv() + .expect("recv") + .map_err(|err| error!(err))? .into_iter() .map(|image| load_from_memory(&image)) .collect::, _>>() - .map_err(|_| ckb_vm::error::Error::Unexpected("failed to parse images".to_owned()))?; + .map_err(|err| error!(err))?; if images.is_empty() { - return Err(ckb_vm::error::Error::Unexpected("empty images".to_owned())); + return Err(error!("empty images")); } if images.len() > self.max_combination { - return Err(ckb_vm::error::Error::Unexpected( - "exceesive images".to_owned(), - )); + return Err(error!("exceesive images")); } // resize images to the maximum @@ -154,8 +171,9 @@ fn main_asm( let debug = Box::new(DebugSyscall { output: debug_result.clone(), }); + let client = ImageFetchClient::new(&settings.image_fetcher_url, settings.dob1_max_cache_size); let image = Box::new(ImageCombinationSyscall { - client: ImageFetchClient::new(&settings.image_fetcher_url, settings.dob1_max_cache_size), + client: Arc::new(Mutex::new(client)), max_combination: settings.dob1_max_combination, }); From ceebca50562a4b2d3cc3e058e9f3630bde671188 Mon Sep 17 00:00:00 2001 From: liyukun Date: Fri, 7 Jun 2024 12:52:11 +0800 Subject: [PATCH 04/17] feat: use molecule as parameter for syscall --- Cargo.lock | 18 +- Cargo.toml | 3 +- ...00000000000000000000000000000000000000.bin | Bin 21912 -> 27416 bytes src/decoder/mod.rs | 8 +- src/tests/dob1/decoder.rs | 10 +- src/types/error.rs | 71 + src/types/generated/dob1_syscall.mol | 11 + src/types/generated/mod.rs | 1455 +++++++++++++++++ src/types/mod.rs | 6 + src/{types.rs => types/object.rs} | 4 + src/vm.rs | 178 +- 11 files changed, 1685 insertions(+), 79 deletions(-) create mode 100644 src/types/error.rs create mode 100644 src/types/generated/dob1_syscall.mol create mode 100644 src/types/generated/mod.rs create mode 100644 src/types/mod.rs rename src/{types.rs => types/object.rs} (98%) diff --git a/Cargo.lock b/Cargo.lock index 84645ff..2e30b91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,7 +442,7 @@ dependencies = [ "ckb-fixed-hash", "ckb-hash", "ckb-occupied-capacity", - "molecule", + "molecule 0.7.5", "numext-fixed-uint", ] @@ -670,7 +670,7 @@ dependencies = [ "derive_more", "golomb-coded-set", "merkle-cbt", - "molecule", + "molecule 0.7.5", "numext-fixed-uint", "once_cell", "paste", @@ -899,6 +899,7 @@ dependencies = [ "image", "jsonrpc-core", "jsonrpsee", + "molecule 0.8.0", "reqwest 0.12.4", "serde", "serde_json", @@ -1900,6 +1901,17 @@ dependencies = [ "faster-hex", ] +[[package]] +name = "molecule" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6efe1c7efcd0bdf4ca590e104bcb13087d9968956ae4ae98e92fb8c1da0f3730" +dependencies = [ + "bytes", + "cfg-if 1.0.0", + "faster-hex", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -3040,7 +3052,7 @@ name = "spore-types" version = "0.1.0" source = "git+https://github.com/sporeprotocol/spore-contract?rev=81315ca#81315ca8c3865719a5ab71eb2dcc4bf4332cb09c" dependencies = [ - "molecule", + "molecule 0.7.5", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b1cec59..fad7c6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ ckb-sdk = "3.2.0" ckb-types = "0.116.1" ckb-jsonrpc-types = "0.116.1" ckb-hash = "0.116.1" +ckb-vm = { version = "0.24", features = ["asm"] } thiserror = "1.0" serde_json = "1.0" hex = "0.4.3" @@ -18,7 +19,7 @@ reqwest = { version = "0.12.4", features = ["json"] } jsonrpc-core = "18.0" serde = { version = "1.0", features = ["serde_derive"] } futures = "0.3" -ckb-vm = { version = "0.24", features = ["asm"] } +molecule = "0.8.0" spore-types = { git = "https://github.com/sporeprotocol/spore-contract", rev = "81315ca" } diff --git a/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin b/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin index 5137253737a60a0dd2fbe80498a46826f68bcbe1..8f1a6a7f976d658d7b12b44f00325fb1651e94e5 100755 GIT binary patch delta 12687 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
hL2{Bdwx!WHuXkot8+o_1O(e6>C&Qpv|d6GEZ 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 D_bNQC delta 7595 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 o>7DNM^?CO#Ev6; +vector Color ; +vector RawImage ; + +union Item { + URI, + Color, + RawImage, +} + +vector ItemVec ; diff --git a/src/types/generated/mod.rs b/src/types/generated/mod.rs new file mode 100644 index 0000000..8f93e5c --- /dev/null +++ b/src/types/generated/mod.rs @@ -0,0 +1,1455 @@ +// Generated by Molecule 0.8.0 +#![allow(dead_code)] + +use molecule::prelude::*; +#[derive(Clone)] +pub struct URI(molecule::bytes::Bytes); +impl ::core::fmt::LowerHex for URI { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl ::core::fmt::Debug for URI { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl ::core::fmt::Display for URI { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl ::core::default::Default for URI { + fn default() -> Self { + let v = molecule::bytes::Bytes::from_static(&Self::DEFAULT_VALUE); + URI::new_unchecked(v) + } +} +impl URI { + const DEFAULT_VALUE: [u8; 4] = [0, 0, 0, 0]; + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> Byte { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + Byte::new_unchecked(self.0.slice(start..end)) + } + pub fn raw_data(&self) -> molecule::bytes::Bytes { + self.0.slice(molecule::NUMBER_SIZE..) + } + pub fn as_reader<'r>(&'r self) -> URIReader<'r> { + URIReader::new_unchecked(self.as_slice()) + } +} +impl molecule::prelude::Entity for URI { + type Builder = URIBuilder; + const NAME: &'static str = "URI"; + fn new_unchecked(data: molecule::bytes::Bytes) -> Self { + URI(data) + } + fn as_bytes(&self) -> molecule::bytes::Bytes { + self.0.clone() + } + fn as_slice(&self) -> &[u8] { + &self.0[..] + } + fn from_slice(slice: &[u8]) -> molecule::error::VerificationResult { + URIReader::from_slice(slice).map(|reader| reader.to_entity()) + } + fn from_compatible_slice(slice: &[u8]) -> molecule::error::VerificationResult { + URIReader::from_compatible_slice(slice).map(|reader| reader.to_entity()) + } + fn new_builder() -> Self::Builder { + ::core::default::Default::default() + } + fn as_builder(self) -> Self::Builder { + Self::new_builder().extend(self.into_iter()) + } +} +#[derive(Clone, Copy)] +pub struct URIReader<'r>(&'r [u8]); +impl<'r> ::core::fmt::LowerHex for URIReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl<'r> ::core::fmt::Debug for URIReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl<'r> ::core::fmt::Display for URIReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl<'r> URIReader<'r> { + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option> { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> ByteReader<'r> { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + ByteReader::new_unchecked(&self.as_slice()[start..end]) + } + pub fn raw_data(&self) -> &'r [u8] { + &self.as_slice()[molecule::NUMBER_SIZE..] + } +} +impl<'r> molecule::prelude::Reader<'r> for URIReader<'r> { + type Entity = URI; + const NAME: &'static str = "URIReader"; + fn to_entity(&self) -> Self::Entity { + Self::Entity::new_unchecked(self.as_slice().to_owned().into()) + } + fn new_unchecked(slice: &'r [u8]) -> Self { + URIReader(slice) + } + fn as_slice(&self) -> &'r [u8] { + self.0 + } + fn verify(slice: &[u8], _compatible: bool) -> molecule::error::VerificationResult<()> { + use molecule::verification_error as ve; + let slice_len = slice.len(); + if slice_len < molecule::NUMBER_SIZE { + return ve!(Self, HeaderIsBroken, molecule::NUMBER_SIZE, slice_len); + } + let item_count = molecule::unpack_number(slice) as usize; + if item_count == 0 { + if slice_len != molecule::NUMBER_SIZE { + return ve!(Self, TotalSizeNotMatch, molecule::NUMBER_SIZE, slice_len); + } + return Ok(()); + } + let total_size = molecule::NUMBER_SIZE + Self::ITEM_SIZE * item_count; + if slice_len != total_size { + return ve!(Self, TotalSizeNotMatch, total_size, slice_len); + } + Ok(()) + } +} +#[derive(Clone, Debug, Default)] +pub struct URIBuilder(pub(crate) Vec); +impl URIBuilder { + pub const ITEM_SIZE: usize = 1; + pub fn set(mut self, v: Vec) -> Self { + self.0 = v; + self + } + pub fn push(mut self, v: Byte) -> Self { + self.0.push(v); + self + } + pub fn extend>(mut self, iter: T) -> Self { + for elem in iter { + self.0.push(elem); + } + self + } + pub fn replace(&mut self, index: usize, v: Byte) -> Option { + self.0 + .get_mut(index) + .map(|item| ::core::mem::replace(item, v)) + } +} +impl molecule::prelude::Builder for URIBuilder { + type Entity = URI; + const NAME: &'static str = "URIBuilder"; + fn expected_length(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.0.len() + } + fn write(&self, writer: &mut W) -> molecule::io::Result<()> { + writer.write_all(&molecule::pack_number(self.0.len() as molecule::Number))?; + for inner in &self.0[..] { + writer.write_all(inner.as_slice())?; + } + Ok(()) + } + fn build(&self) -> Self::Entity { + let mut inner = Vec::with_capacity(self.expected_length()); + self.write(&mut inner) + .unwrap_or_else(|_| panic!("{} build should be ok", Self::NAME)); + URI::new_unchecked(inner.into()) + } +} +pub struct URIIterator(URI, usize, usize); +impl ::core::iter::Iterator for URIIterator { + type Item = Byte; + fn next(&mut self) -> Option { + if self.1 >= self.2 { + None + } else { + let ret = self.0.get_unchecked(self.1); + self.1 += 1; + Some(ret) + } + } +} +impl ::core::iter::ExactSizeIterator for URIIterator { + fn len(&self) -> usize { + self.2 - self.1 + } +} +impl ::core::iter::IntoIterator for URI { + type Item = Byte; + type IntoIter = URIIterator; + fn into_iter(self) -> Self::IntoIter { + let len = self.len(); + URIIterator(self, 0, len) + } +} +impl ::core::iter::FromIterator for URI { + fn from_iter>(iter: T) -> Self { + Self::new_builder().extend(iter).build() + } +} +impl ::core::iter::FromIterator for URI { + fn from_iter>(iter: T) -> Self { + Self::new_builder() + .extend(iter.into_iter().map(Into::into)) + .build() + } +} +#[derive(Clone)] +pub struct Color(molecule::bytes::Bytes); +impl ::core::fmt::LowerHex for Color { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl ::core::fmt::Debug for Color { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl ::core::fmt::Display for Color { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl ::core::default::Default for Color { + fn default() -> Self { + let v = molecule::bytes::Bytes::from_static(&Self::DEFAULT_VALUE); + Color::new_unchecked(v) + } +} +impl Color { + const DEFAULT_VALUE: [u8; 4] = [0, 0, 0, 0]; + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> Byte { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + Byte::new_unchecked(self.0.slice(start..end)) + } + pub fn raw_data(&self) -> molecule::bytes::Bytes { + self.0.slice(molecule::NUMBER_SIZE..) + } + pub fn as_reader<'r>(&'r self) -> ColorReader<'r> { + ColorReader::new_unchecked(self.as_slice()) + } +} +impl molecule::prelude::Entity for Color { + type Builder = ColorBuilder; + const NAME: &'static str = "Color"; + fn new_unchecked(data: molecule::bytes::Bytes) -> Self { + Color(data) + } + fn as_bytes(&self) -> molecule::bytes::Bytes { + self.0.clone() + } + fn as_slice(&self) -> &[u8] { + &self.0[..] + } + fn from_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ColorReader::from_slice(slice).map(|reader| reader.to_entity()) + } + fn from_compatible_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ColorReader::from_compatible_slice(slice).map(|reader| reader.to_entity()) + } + fn new_builder() -> Self::Builder { + ::core::default::Default::default() + } + fn as_builder(self) -> Self::Builder { + Self::new_builder().extend(self.into_iter()) + } +} +#[derive(Clone, Copy)] +pub struct ColorReader<'r>(&'r [u8]); +impl<'r> ::core::fmt::LowerHex for ColorReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl<'r> ::core::fmt::Debug for ColorReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl<'r> ::core::fmt::Display for ColorReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl<'r> ColorReader<'r> { + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option> { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> ByteReader<'r> { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + ByteReader::new_unchecked(&self.as_slice()[start..end]) + } + pub fn raw_data(&self) -> &'r [u8] { + &self.as_slice()[molecule::NUMBER_SIZE..] + } +} +impl<'r> molecule::prelude::Reader<'r> for ColorReader<'r> { + type Entity = Color; + const NAME: &'static str = "ColorReader"; + fn to_entity(&self) -> Self::Entity { + Self::Entity::new_unchecked(self.as_slice().to_owned().into()) + } + fn new_unchecked(slice: &'r [u8]) -> Self { + ColorReader(slice) + } + fn as_slice(&self) -> &'r [u8] { + self.0 + } + fn verify(slice: &[u8], _compatible: bool) -> molecule::error::VerificationResult<()> { + use molecule::verification_error as ve; + let slice_len = slice.len(); + if slice_len < molecule::NUMBER_SIZE { + return ve!(Self, HeaderIsBroken, molecule::NUMBER_SIZE, slice_len); + } + let item_count = molecule::unpack_number(slice) as usize; + if item_count == 0 { + if slice_len != molecule::NUMBER_SIZE { + return ve!(Self, TotalSizeNotMatch, molecule::NUMBER_SIZE, slice_len); + } + return Ok(()); + } + let total_size = molecule::NUMBER_SIZE + Self::ITEM_SIZE * item_count; + if slice_len != total_size { + return ve!(Self, TotalSizeNotMatch, total_size, slice_len); + } + Ok(()) + } +} +#[derive(Clone, Debug, Default)] +pub struct ColorBuilder(pub(crate) Vec); +impl ColorBuilder { + pub const ITEM_SIZE: usize = 1; + pub fn set(mut self, v: Vec) -> Self { + self.0 = v; + self + } + pub fn push(mut self, v: Byte) -> Self { + self.0.push(v); + self + } + pub fn extend>(mut self, iter: T) -> Self { + for elem in iter { + self.0.push(elem); + } + self + } + pub fn replace(&mut self, index: usize, v: Byte) -> Option { + self.0 + .get_mut(index) + .map(|item| ::core::mem::replace(item, v)) + } +} +impl molecule::prelude::Builder for ColorBuilder { + type Entity = Color; + const NAME: &'static str = "ColorBuilder"; + fn expected_length(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.0.len() + } + fn write(&self, writer: &mut W) -> molecule::io::Result<()> { + writer.write_all(&molecule::pack_number(self.0.len() as molecule::Number))?; + for inner in &self.0[..] { + writer.write_all(inner.as_slice())?; + } + Ok(()) + } + fn build(&self) -> Self::Entity { + let mut inner = Vec::with_capacity(self.expected_length()); + self.write(&mut inner) + .unwrap_or_else(|_| panic!("{} build should be ok", Self::NAME)); + Color::new_unchecked(inner.into()) + } +} +pub struct ColorIterator(Color, usize, usize); +impl ::core::iter::Iterator for ColorIterator { + type Item = Byte; + fn next(&mut self) -> Option { + if self.1 >= self.2 { + None + } else { + let ret = self.0.get_unchecked(self.1); + self.1 += 1; + Some(ret) + } + } +} +impl ::core::iter::ExactSizeIterator for ColorIterator { + fn len(&self) -> usize { + self.2 - self.1 + } +} +impl ::core::iter::IntoIterator for Color { + type Item = Byte; + type IntoIter = ColorIterator; + fn into_iter(self) -> Self::IntoIter { + let len = self.len(); + ColorIterator(self, 0, len) + } +} +impl ::core::iter::FromIterator for Color { + fn from_iter>(iter: T) -> Self { + Self::new_builder().extend(iter).build() + } +} +impl ::core::iter::FromIterator for Color { + fn from_iter>(iter: T) -> Self { + Self::new_builder() + .extend(iter.into_iter().map(Into::into)) + .build() + } +} +#[derive(Clone)] +pub struct RawImage(molecule::bytes::Bytes); +impl ::core::fmt::LowerHex for RawImage { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl ::core::fmt::Debug for RawImage { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl ::core::fmt::Display for RawImage { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl ::core::default::Default for RawImage { + fn default() -> Self { + let v = molecule::bytes::Bytes::from_static(&Self::DEFAULT_VALUE); + RawImage::new_unchecked(v) + } +} +impl RawImage { + const DEFAULT_VALUE: [u8; 4] = [0, 0, 0, 0]; + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> Byte { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + Byte::new_unchecked(self.0.slice(start..end)) + } + pub fn raw_data(&self) -> molecule::bytes::Bytes { + self.0.slice(molecule::NUMBER_SIZE..) + } + pub fn as_reader<'r>(&'r self) -> RawImageReader<'r> { + RawImageReader::new_unchecked(self.as_slice()) + } +} +impl molecule::prelude::Entity for RawImage { + type Builder = RawImageBuilder; + const NAME: &'static str = "RawImage"; + fn new_unchecked(data: molecule::bytes::Bytes) -> Self { + RawImage(data) + } + fn as_bytes(&self) -> molecule::bytes::Bytes { + self.0.clone() + } + fn as_slice(&self) -> &[u8] { + &self.0[..] + } + fn from_slice(slice: &[u8]) -> molecule::error::VerificationResult { + RawImageReader::from_slice(slice).map(|reader| reader.to_entity()) + } + fn from_compatible_slice(slice: &[u8]) -> molecule::error::VerificationResult { + RawImageReader::from_compatible_slice(slice).map(|reader| reader.to_entity()) + } + fn new_builder() -> Self::Builder { + ::core::default::Default::default() + } + fn as_builder(self) -> Self::Builder { + Self::new_builder().extend(self.into_iter()) + } +} +#[derive(Clone, Copy)] +pub struct RawImageReader<'r>(&'r [u8]); +impl<'r> ::core::fmt::LowerHex for RawImageReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl<'r> ::core::fmt::Debug for RawImageReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl<'r> ::core::fmt::Display for RawImageReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + let raw_data = hex_string(&self.raw_data()); + write!(f, "{}(0x{})", Self::NAME, raw_data) + } +} +impl<'r> RawImageReader<'r> { + pub const ITEM_SIZE: usize = 1; + pub fn total_size(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.item_count() + } + pub fn item_count(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option> { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> ByteReader<'r> { + let start = molecule::NUMBER_SIZE + Self::ITEM_SIZE * idx; + let end = start + Self::ITEM_SIZE; + ByteReader::new_unchecked(&self.as_slice()[start..end]) + } + pub fn raw_data(&self) -> &'r [u8] { + &self.as_slice()[molecule::NUMBER_SIZE..] + } +} +impl<'r> molecule::prelude::Reader<'r> for RawImageReader<'r> { + type Entity = RawImage; + const NAME: &'static str = "RawImageReader"; + fn to_entity(&self) -> Self::Entity { + Self::Entity::new_unchecked(self.as_slice().to_owned().into()) + } + fn new_unchecked(slice: &'r [u8]) -> Self { + RawImageReader(slice) + } + fn as_slice(&self) -> &'r [u8] { + self.0 + } + fn verify(slice: &[u8], _compatible: bool) -> molecule::error::VerificationResult<()> { + use molecule::verification_error as ve; + let slice_len = slice.len(); + if slice_len < molecule::NUMBER_SIZE { + return ve!(Self, HeaderIsBroken, molecule::NUMBER_SIZE, slice_len); + } + let item_count = molecule::unpack_number(slice) as usize; + if item_count == 0 { + if slice_len != molecule::NUMBER_SIZE { + return ve!(Self, TotalSizeNotMatch, molecule::NUMBER_SIZE, slice_len); + } + return Ok(()); + } + let total_size = molecule::NUMBER_SIZE + Self::ITEM_SIZE * item_count; + if slice_len != total_size { + return ve!(Self, TotalSizeNotMatch, total_size, slice_len); + } + Ok(()) + } +} +#[derive(Clone, Debug, Default)] +pub struct RawImageBuilder(pub(crate) Vec); +impl RawImageBuilder { + pub const ITEM_SIZE: usize = 1; + pub fn set(mut self, v: Vec) -> Self { + self.0 = v; + self + } + pub fn push(mut self, v: Byte) -> Self { + self.0.push(v); + self + } + pub fn extend>(mut self, iter: T) -> Self { + for elem in iter { + self.0.push(elem); + } + self + } + pub fn replace(&mut self, index: usize, v: Byte) -> Option { + self.0 + .get_mut(index) + .map(|item| ::core::mem::replace(item, v)) + } +} +impl molecule::prelude::Builder for RawImageBuilder { + type Entity = RawImage; + const NAME: &'static str = "RawImageBuilder"; + fn expected_length(&self) -> usize { + molecule::NUMBER_SIZE + Self::ITEM_SIZE * self.0.len() + } + fn write(&self, writer: &mut W) -> molecule::io::Result<()> { + writer.write_all(&molecule::pack_number(self.0.len() as molecule::Number))?; + for inner in &self.0[..] { + writer.write_all(inner.as_slice())?; + } + Ok(()) + } + fn build(&self) -> Self::Entity { + let mut inner = Vec::with_capacity(self.expected_length()); + self.write(&mut inner) + .unwrap_or_else(|_| panic!("{} build should be ok", Self::NAME)); + RawImage::new_unchecked(inner.into()) + } +} +pub struct RawImageIterator(RawImage, usize, usize); +impl ::core::iter::Iterator for RawImageIterator { + type Item = Byte; + fn next(&mut self) -> Option { + if self.1 >= self.2 { + None + } else { + let ret = self.0.get_unchecked(self.1); + self.1 += 1; + Some(ret) + } + } +} +impl ::core::iter::ExactSizeIterator for RawImageIterator { + fn len(&self) -> usize { + self.2 - self.1 + } +} +impl ::core::iter::IntoIterator for RawImage { + type Item = Byte; + type IntoIter = RawImageIterator; + fn into_iter(self) -> Self::IntoIter { + let len = self.len(); + RawImageIterator(self, 0, len) + } +} +impl ::core::iter::FromIterator for RawImage { + fn from_iter>(iter: T) -> Self { + Self::new_builder().extend(iter).build() + } +} +impl ::core::iter::FromIterator for RawImage { + fn from_iter>(iter: T) -> Self { + Self::new_builder() + .extend(iter.into_iter().map(Into::into)) + .build() + } +} +#[derive(Clone)] +pub struct Item(molecule::bytes::Bytes); +impl ::core::fmt::LowerHex for Item { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl ::core::fmt::Debug for Item { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl ::core::fmt::Display for Item { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}(", Self::NAME)?; + self.to_enum().display_inner(f)?; + write!(f, ")") + } +} +impl ::core::default::Default for Item { + fn default() -> Self { + let v = molecule::bytes::Bytes::from_static(&Self::DEFAULT_VALUE); + Item::new_unchecked(v) + } +} +impl Item { + const DEFAULT_VALUE: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 0]; + pub const ITEMS_COUNT: usize = 3; + pub fn item_id(&self) -> molecule::Number { + molecule::unpack_number(self.as_slice()) + } + pub fn to_enum(&self) -> ItemUnion { + let inner = self.0.slice(molecule::NUMBER_SIZE..); + match self.item_id() { + 0 => URI::new_unchecked(inner).into(), + 1 => Color::new_unchecked(inner).into(), + 2 => RawImage::new_unchecked(inner).into(), + _ => panic!("{}: invalid data", Self::NAME), + } + } + pub fn as_reader<'r>(&'r self) -> ItemReader<'r> { + ItemReader::new_unchecked(self.as_slice()) + } +} +impl molecule::prelude::Entity for Item { + type Builder = ItemBuilder; + const NAME: &'static str = "Item"; + fn new_unchecked(data: molecule::bytes::Bytes) -> Self { + Item(data) + } + fn as_bytes(&self) -> molecule::bytes::Bytes { + self.0.clone() + } + fn as_slice(&self) -> &[u8] { + &self.0[..] + } + fn from_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ItemReader::from_slice(slice).map(|reader| reader.to_entity()) + } + fn from_compatible_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ItemReader::from_compatible_slice(slice).map(|reader| reader.to_entity()) + } + fn new_builder() -> Self::Builder { + ::core::default::Default::default() + } + fn as_builder(self) -> Self::Builder { + Self::new_builder().set(self.to_enum()) + } +} +#[derive(Clone, Copy)] +pub struct ItemReader<'r>(&'r [u8]); +impl<'r> ::core::fmt::LowerHex for ItemReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl<'r> ::core::fmt::Debug for ItemReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl<'r> ::core::fmt::Display for ItemReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}(", Self::NAME)?; + self.to_enum().display_inner(f)?; + write!(f, ")") + } +} +impl<'r> ItemReader<'r> { + pub const ITEMS_COUNT: usize = 3; + pub fn item_id(&self) -> molecule::Number { + molecule::unpack_number(self.as_slice()) + } + pub fn to_enum(&self) -> ItemUnionReader<'r> { + let inner = &self.as_slice()[molecule::NUMBER_SIZE..]; + match self.item_id() { + 0 => URIReader::new_unchecked(inner).into(), + 1 => ColorReader::new_unchecked(inner).into(), + 2 => RawImageReader::new_unchecked(inner).into(), + _ => panic!("{}: invalid data", Self::NAME), + } + } +} +impl<'r> molecule::prelude::Reader<'r> for ItemReader<'r> { + type Entity = Item; + const NAME: &'static str = "ItemReader"; + fn to_entity(&self) -> Self::Entity { + Self::Entity::new_unchecked(self.as_slice().to_owned().into()) + } + fn new_unchecked(slice: &'r [u8]) -> Self { + ItemReader(slice) + } + fn as_slice(&self) -> &'r [u8] { + self.0 + } + fn verify(slice: &[u8], compatible: bool) -> molecule::error::VerificationResult<()> { + use molecule::verification_error as ve; + let slice_len = slice.len(); + if slice_len < molecule::NUMBER_SIZE { + return ve!(Self, HeaderIsBroken, molecule::NUMBER_SIZE, slice_len); + } + let item_id = molecule::unpack_number(slice); + let inner_slice = &slice[molecule::NUMBER_SIZE..]; + match item_id { + 0 => URIReader::verify(inner_slice, compatible), + 1 => ColorReader::verify(inner_slice, compatible), + 2 => RawImageReader::verify(inner_slice, compatible), + _ => ve!(Self, UnknownItem, Self::ITEMS_COUNT, item_id), + }?; + Ok(()) + } +} +#[derive(Clone, Debug, Default)] +pub struct ItemBuilder(pub(crate) ItemUnion); +impl ItemBuilder { + pub const ITEMS_COUNT: usize = 3; + pub fn set(mut self, v: I) -> Self + where + I: ::core::convert::Into, + { + self.0 = v.into(); + self + } +} +impl molecule::prelude::Builder for ItemBuilder { + type Entity = Item; + const NAME: &'static str = "ItemBuilder"; + fn expected_length(&self) -> usize { + molecule::NUMBER_SIZE + self.0.as_slice().len() + } + fn write(&self, writer: &mut W) -> molecule::io::Result<()> { + writer.write_all(&molecule::pack_number(self.0.item_id()))?; + writer.write_all(self.0.as_slice()) + } + fn build(&self) -> Self::Entity { + let mut inner = Vec::with_capacity(self.expected_length()); + self.write(&mut inner) + .unwrap_or_else(|_| panic!("{} build should be ok", Self::NAME)); + Item::new_unchecked(inner.into()) + } +} +#[derive(Debug, Clone)] +pub enum ItemUnion { + URI(URI), + Color(Color), + RawImage(RawImage), +} +#[derive(Debug, Clone, Copy)] +pub enum ItemUnionReader<'r> { + URI(URIReader<'r>), + Color(ColorReader<'r>), + RawImage(RawImageReader<'r>), +} +impl ::core::default::Default for ItemUnion { + fn default() -> Self { + ItemUnion::URI(::core::default::Default::default()) + } +} +impl ::core::fmt::Display for ItemUnion { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + ItemUnion::URI(ref item) => { + write!(f, "{}::{}({})", Self::NAME, URI::NAME, item) + } + ItemUnion::Color(ref item) => { + write!(f, "{}::{}({})", Self::NAME, Color::NAME, item) + } + ItemUnion::RawImage(ref item) => { + write!(f, "{}::{}({})", Self::NAME, RawImage::NAME, item) + } + } + } +} +impl<'r> ::core::fmt::Display for ItemUnionReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + ItemUnionReader::URI(ref item) => { + write!(f, "{}::{}({})", Self::NAME, URI::NAME, item) + } + ItemUnionReader::Color(ref item) => { + write!(f, "{}::{}({})", Self::NAME, Color::NAME, item) + } + ItemUnionReader::RawImage(ref item) => { + write!(f, "{}::{}({})", Self::NAME, RawImage::NAME, item) + } + } + } +} +impl ItemUnion { + pub(crate) fn display_inner(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + ItemUnion::URI(ref item) => write!(f, "{}", item), + ItemUnion::Color(ref item) => write!(f, "{}", item), + ItemUnion::RawImage(ref item) => write!(f, "{}", item), + } + } +} +impl<'r> ItemUnionReader<'r> { + pub(crate) fn display_inner(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + ItemUnionReader::URI(ref item) => write!(f, "{}", item), + ItemUnionReader::Color(ref item) => write!(f, "{}", item), + ItemUnionReader::RawImage(ref item) => write!(f, "{}", item), + } + } +} +impl ::core::convert::From for ItemUnion { + fn from(item: URI) -> Self { + ItemUnion::URI(item) + } +} +impl ::core::convert::From for ItemUnion { + fn from(item: Color) -> Self { + ItemUnion::Color(item) + } +} +impl ::core::convert::From for ItemUnion { + fn from(item: RawImage) -> Self { + ItemUnion::RawImage(item) + } +} +impl<'r> ::core::convert::From> for ItemUnionReader<'r> { + fn from(item: URIReader<'r>) -> Self { + ItemUnionReader::URI(item) + } +} +impl<'r> ::core::convert::From> for ItemUnionReader<'r> { + fn from(item: ColorReader<'r>) -> Self { + ItemUnionReader::Color(item) + } +} +impl<'r> ::core::convert::From> for ItemUnionReader<'r> { + fn from(item: RawImageReader<'r>) -> Self { + ItemUnionReader::RawImage(item) + } +} +impl ItemUnion { + pub const NAME: &'static str = "ItemUnion"; + pub fn as_bytes(&self) -> molecule::bytes::Bytes { + match self { + ItemUnion::URI(item) => item.as_bytes(), + ItemUnion::Color(item) => item.as_bytes(), + ItemUnion::RawImage(item) => item.as_bytes(), + } + } + pub fn as_slice(&self) -> &[u8] { + match self { + ItemUnion::URI(item) => item.as_slice(), + ItemUnion::Color(item) => item.as_slice(), + ItemUnion::RawImage(item) => item.as_slice(), + } + } + pub fn item_id(&self) -> molecule::Number { + match self { + ItemUnion::URI(_) => 0, + ItemUnion::Color(_) => 1, + ItemUnion::RawImage(_) => 2, + } + } + pub fn item_name(&self) -> &str { + match self { + ItemUnion::URI(_) => "URI", + ItemUnion::Color(_) => "Color", + ItemUnion::RawImage(_) => "RawImage", + } + } + pub fn as_reader<'r>(&'r self) -> ItemUnionReader<'r> { + match self { + ItemUnion::URI(item) => item.as_reader().into(), + ItemUnion::Color(item) => item.as_reader().into(), + ItemUnion::RawImage(item) => item.as_reader().into(), + } + } +} +impl<'r> ItemUnionReader<'r> { + pub const NAME: &'r str = "ItemUnionReader"; + pub fn as_slice(&self) -> &'r [u8] { + match self { + ItemUnionReader::URI(item) => item.as_slice(), + ItemUnionReader::Color(item) => item.as_slice(), + ItemUnionReader::RawImage(item) => item.as_slice(), + } + } + pub fn item_id(&self) -> molecule::Number { + match self { + ItemUnionReader::URI(_) => 0, + ItemUnionReader::Color(_) => 1, + ItemUnionReader::RawImage(_) => 2, + } + } + pub fn item_name(&self) -> &str { + match self { + ItemUnionReader::URI(_) => "URI", + ItemUnionReader::Color(_) => "Color", + ItemUnionReader::RawImage(_) => "RawImage", + } + } +} +impl From for Item { + fn from(value: URI) -> Self { + Self::new_builder().set(value).build() + } +} +impl From for Item { + fn from(value: Color) -> Self { + Self::new_builder().set(value).build() + } +} +impl From for Item { + fn from(value: RawImage) -> Self { + Self::new_builder().set(value).build() + } +} +#[derive(Clone)] +pub struct ItemVec(molecule::bytes::Bytes); +impl ::core::fmt::LowerHex for ItemVec { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl ::core::fmt::Debug for ItemVec { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl ::core::fmt::Display for ItemVec { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{} [", Self::NAME)?; + for i in 0..self.len() { + if i == 0 { + write!(f, "{}", self.get_unchecked(i))?; + } else { + write!(f, ", {}", self.get_unchecked(i))?; + } + } + write!(f, "]") + } +} +impl ::core::default::Default for ItemVec { + fn default() -> Self { + let v = molecule::bytes::Bytes::from_static(&Self::DEFAULT_VALUE); + ItemVec::new_unchecked(v) + } +} +impl ItemVec { + const DEFAULT_VALUE: [u8; 4] = [4, 0, 0, 0]; + pub fn total_size(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn item_count(&self) -> usize { + if self.total_size() == molecule::NUMBER_SIZE { + 0 + } else { + (molecule::unpack_number(&self.as_slice()[molecule::NUMBER_SIZE..]) as usize / 4) - 1 + } + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> Item { + let slice = self.as_slice(); + let start_idx = molecule::NUMBER_SIZE * (1 + idx); + let start = molecule::unpack_number(&slice[start_idx..]) as usize; + if idx == self.len() - 1 { + Item::new_unchecked(self.0.slice(start..)) + } else { + let end_idx = start_idx + molecule::NUMBER_SIZE; + let end = molecule::unpack_number(&slice[end_idx..]) as usize; + Item::new_unchecked(self.0.slice(start..end)) + } + } + pub fn as_reader<'r>(&'r self) -> ItemVecReader<'r> { + ItemVecReader::new_unchecked(self.as_slice()) + } +} +impl molecule::prelude::Entity for ItemVec { + type Builder = ItemVecBuilder; + const NAME: &'static str = "ItemVec"; + fn new_unchecked(data: molecule::bytes::Bytes) -> Self { + ItemVec(data) + } + fn as_bytes(&self) -> molecule::bytes::Bytes { + self.0.clone() + } + fn as_slice(&self) -> &[u8] { + &self.0[..] + } + fn from_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ItemVecReader::from_slice(slice).map(|reader| reader.to_entity()) + } + fn from_compatible_slice(slice: &[u8]) -> molecule::error::VerificationResult { + ItemVecReader::from_compatible_slice(slice).map(|reader| reader.to_entity()) + } + fn new_builder() -> Self::Builder { + ::core::default::Default::default() + } + fn as_builder(self) -> Self::Builder { + Self::new_builder().extend(self.into_iter()) + } +} +#[derive(Clone, Copy)] +pub struct ItemVecReader<'r>(&'r [u8]); +impl<'r> ::core::fmt::LowerHex for ItemVecReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + use molecule::hex_string; + if f.alternate() { + write!(f, "0x")?; + } + write!(f, "{}", hex_string(self.as_slice())) + } +} +impl<'r> ::core::fmt::Debug for ItemVecReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{}({:#x})", Self::NAME, self) + } +} +impl<'r> ::core::fmt::Display for ItemVecReader<'r> { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "{} [", Self::NAME)?; + for i in 0..self.len() { + if i == 0 { + write!(f, "{}", self.get_unchecked(i))?; + } else { + write!(f, ", {}", self.get_unchecked(i))?; + } + } + write!(f, "]") + } +} +impl<'r> ItemVecReader<'r> { + pub fn total_size(&self) -> usize { + molecule::unpack_number(self.as_slice()) as usize + } + pub fn item_count(&self) -> usize { + if self.total_size() == molecule::NUMBER_SIZE { + 0 + } else { + (molecule::unpack_number(&self.as_slice()[molecule::NUMBER_SIZE..]) as usize / 4) - 1 + } + } + pub fn len(&self) -> usize { + self.item_count() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn get(&self, idx: usize) -> Option> { + if idx >= self.len() { + None + } else { + Some(self.get_unchecked(idx)) + } + } + pub fn get_unchecked(&self, idx: usize) -> ItemReader<'r> { + let slice = self.as_slice(); + let start_idx = molecule::NUMBER_SIZE * (1 + idx); + let start = molecule::unpack_number(&slice[start_idx..]) as usize; + if idx == self.len() - 1 { + ItemReader::new_unchecked(&self.as_slice()[start..]) + } else { + let end_idx = start_idx + molecule::NUMBER_SIZE; + let end = molecule::unpack_number(&slice[end_idx..]) as usize; + ItemReader::new_unchecked(&self.as_slice()[start..end]) + } + } +} +impl<'r> molecule::prelude::Reader<'r> for ItemVecReader<'r> { + type Entity = ItemVec; + const NAME: &'static str = "ItemVecReader"; + fn to_entity(&self) -> Self::Entity { + Self::Entity::new_unchecked(self.as_slice().to_owned().into()) + } + fn new_unchecked(slice: &'r [u8]) -> Self { + ItemVecReader(slice) + } + fn as_slice(&self) -> &'r [u8] { + self.0 + } + fn verify(slice: &[u8], compatible: bool) -> molecule::error::VerificationResult<()> { + use molecule::verification_error as ve; + let slice_len = slice.len(); + if slice_len < molecule::NUMBER_SIZE { + return ve!(Self, HeaderIsBroken, molecule::NUMBER_SIZE, slice_len); + } + let total_size = molecule::unpack_number(slice) as usize; + if slice_len != total_size { + return ve!(Self, TotalSizeNotMatch, total_size, slice_len); + } + if slice_len == molecule::NUMBER_SIZE { + return Ok(()); + } + if slice_len < molecule::NUMBER_SIZE * 2 { + return ve!( + Self, + TotalSizeNotMatch, + molecule::NUMBER_SIZE * 2, + slice_len + ); + } + let offset_first = molecule::unpack_number(&slice[molecule::NUMBER_SIZE..]) as usize; + if offset_first % molecule::NUMBER_SIZE != 0 || offset_first < molecule::NUMBER_SIZE * 2 { + return ve!(Self, OffsetsNotMatch); + } + if slice_len < offset_first { + return ve!(Self, HeaderIsBroken, offset_first, slice_len); + } + let mut offsets: Vec = slice[molecule::NUMBER_SIZE..offset_first] + .chunks_exact(molecule::NUMBER_SIZE) + .map(|x| molecule::unpack_number(x) as usize) + .collect(); + offsets.push(total_size); + if offsets.windows(2).any(|i| i[0] > i[1]) { + return ve!(Self, OffsetsNotMatch); + } + for pair in offsets.windows(2) { + let start = pair[0]; + let end = pair[1]; + ItemReader::verify(&slice[start..end], compatible)?; + } + Ok(()) + } +} +#[derive(Clone, Debug, Default)] +pub struct ItemVecBuilder(pub(crate) Vec); +impl ItemVecBuilder { + pub fn set(mut self, v: Vec) -> Self { + self.0 = v; + self + } + pub fn push(mut self, v: Item) -> Self { + self.0.push(v); + self + } + pub fn extend>(mut self, iter: T) -> Self { + for elem in iter { + self.0.push(elem); + } + self + } + pub fn replace(&mut self, index: usize, v: Item) -> Option { + self.0 + .get_mut(index) + .map(|item| ::core::mem::replace(item, v)) + } +} +impl molecule::prelude::Builder for ItemVecBuilder { + type Entity = ItemVec; + const NAME: &'static str = "ItemVecBuilder"; + fn expected_length(&self) -> usize { + molecule::NUMBER_SIZE * (self.0.len() + 1) + + self + .0 + .iter() + .map(|inner| inner.as_slice().len()) + .sum::() + } + fn write(&self, writer: &mut W) -> molecule::io::Result<()> { + let item_count = self.0.len(); + if item_count == 0 { + writer.write_all(&molecule::pack_number( + molecule::NUMBER_SIZE as molecule::Number, + ))?; + } else { + let (total_size, offsets) = self.0.iter().fold( + ( + molecule::NUMBER_SIZE * (item_count + 1), + Vec::with_capacity(item_count), + ), + |(start, mut offsets), inner| { + offsets.push(start); + (start + inner.as_slice().len(), offsets) + }, + ); + writer.write_all(&molecule::pack_number(total_size as molecule::Number))?; + for offset in offsets.into_iter() { + writer.write_all(&molecule::pack_number(offset as molecule::Number))?; + } + for inner in self.0.iter() { + writer.write_all(inner.as_slice())?; + } + } + Ok(()) + } + fn build(&self) -> Self::Entity { + let mut inner = Vec::with_capacity(self.expected_length()); + self.write(&mut inner) + .unwrap_or_else(|_| panic!("{} build should be ok", Self::NAME)); + ItemVec::new_unchecked(inner.into()) + } +} +pub struct ItemVecIterator(ItemVec, usize, usize); +impl ::core::iter::Iterator for ItemVecIterator { + type Item = Item; + fn next(&mut self) -> Option { + if self.1 >= self.2 { + None + } else { + let ret = self.0.get_unchecked(self.1); + self.1 += 1; + Some(ret) + } + } +} +impl ::core::iter::ExactSizeIterator for ItemVecIterator { + fn len(&self) -> usize { + self.2 - self.1 + } +} +impl ::core::iter::IntoIterator for ItemVec { + type Item = Item; + type IntoIter = ItemVecIterator; + fn into_iter(self) -> Self::IntoIter { + let len = self.len(); + ItemVecIterator(self, 0, len) + } +} +impl<'r> ItemVecReader<'r> { + pub fn iter<'t>(&'t self) -> ItemVecReaderIterator<'t, 'r> { + ItemVecReaderIterator(&self, 0, self.len()) + } +} +pub struct ItemVecReaderIterator<'t, 'r>(&'t ItemVecReader<'r>, usize, usize); +impl<'t: 'r, 'r> ::core::iter::Iterator for ItemVecReaderIterator<'t, 'r> { + type Item = ItemReader<'t>; + fn next(&mut self) -> Option { + if self.1 >= self.2 { + None + } else { + let ret = self.0.get_unchecked(self.1); + self.1 += 1; + Some(ret) + } + } +} +impl<'t: 'r, 'r> ::core::iter::ExactSizeIterator for ItemVecReaderIterator<'t, 'r> { + fn len(&self) -> usize { + self.2 - self.1 + } +} +impl ::core::iter::FromIterator for ItemVec { + fn from_iter>(iter: T) -> Self { + Self::new_builder().extend(iter).build() + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..65c2782 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,6 @@ +mod error; +mod object; + +pub mod generated; +pub use error::Error; +pub use object::*; diff --git a/src/types.rs b/src/types/object.rs similarity index 98% rename from src/types.rs rename to src/types/object.rs index 6055536..f3d60ba 100644 --- a/src/types.rs +++ b/src/types/object.rs @@ -9,6 +9,7 @@ use jsonrpsee::types::ErrorCode; #[cfg(feature = "standalone_server")] use serde::Serialize; +<<<<<<< HEAD:src/types.rs #[allow(clippy::enum_variant_names)] #[derive(thiserror::Error, Debug)] #[repr(i32)] @@ -82,6 +83,9 @@ pub enum Error { #[error("fs header like 'btcfs://' and 'ckbfs://' are not contained")] InvalidOnchainFsuriFormat, } +======= +use crate::types::Error; +>>>>>>> 6e23044 (feat: use molecule as parameter for syscall):src/types/object.rs pub enum Dob<'a> { V0(&'a DOBClusterFormatV0), diff --git a/src/vm.rs b/src/vm.rs index baa65d4..92dd89a 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -1,20 +1,24 @@ // refer to https://github.com/nervosnetwork/ckb-vm/blob/develop/examples/ckb-vm-runner.rs +use std::io::Cursor; use std::sync::{mpsc, Arc, Mutex}; use std::thread; use ckb_vm::cost_model::estimate_cycles; -use ckb_vm::registers::{A0, A1, A2, A3, A4, A7}; +use ckb_vm::registers::{A0, A1, A2, A3, A7}; use ckb_vm::{Bytes, Memory, Register, SupportMachine, Syscalls}; -use image::load_from_memory; +use image::codecs::png::{CompressionType, FilterType, PngEncoder}; +use image::{imageops, load_from_memory, DynamicImage, Pixel, Rgb, RgbaImage}; +use jsonrpsee::tracing; +use molecule::prelude::Entity; use crate::client::ImageFetchClient; -use crate::types::Settings; +use crate::types::{generated, Error, Settings}; macro_rules! error { ($err: expr) => {{ let error = $err.to_string(); - println!("[DOB/1 ERROR] {error}"); + tracing::error!("{error}"); ckb_vm::error::Error::Unexpected(error) }}; } @@ -54,7 +58,7 @@ impl Syscalls for DebugSyscall { .clone() .lock() .unwrap() - .push(String::from_utf8(buffer).unwrap()); + .push(String::from_utf8(buffer).map_err(|err| error!(err))?); Ok(true) } @@ -66,6 +70,24 @@ struct ImageCombinationSyscall { max_combination: usize, } +impl ImageCombinationSyscall { + // fetch one image synchronously + // + // note: for saving space purpose, we take time to trade memory space through + // fetching images one by one + fn fetch_one_image_sync(&mut self, image_uri: String) -> Result, Error> { + let (tx, rx) = mpsc::channel(); + let client = self.client.clone(); + thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let mut client = client.lock().unwrap(); + tx.send(rt.block_on(client.fetch_images(&[image_uri]))) + .expect("send"); + }); + Ok(rx.recv().expect("recv")?.remove(0)) + } +} + impl Syscalls for ImageCombinationSyscall { fn initialize(&mut self, _machine: &mut Mac) -> Result<(), ckb_vm::error::Error> { Ok(()) @@ -80,80 +102,84 @@ impl Syscalls for ImageCombinationSyscall { // 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 images_uri_array_addr = machine.registers()[A2].to_u64(); - let images_uri_array_count = machine.registers()[A3].to_u64(); - let iamges_uri_array_uint_size = machine.registers()[A4].to_u64(); - - // parse all of images uri - let array_size = images_uri_array_count * iamges_uri_array_uint_size; - let images_uri_array_bytes = machine + let mut buffer_size = machine.memory_mut().load64(&buffer_size_addr)?.to_u64(); + let molecule_addr = machine.registers()[A2].to_u64(); + let molecule_size = machine.registers()[A3].to_u64(); + + // parse all of images uri/color/raw + let pattern_bytes = machine .memory_mut() - .load_bytes(images_uri_array_addr, array_size)?; - let images_uri_array = images_uri_array_bytes - .chunks_exact(iamges_uri_array_uint_size as usize) - .map(|uri_bytes| String::from_utf8_lossy(uri_bytes).to_string()) - .collect::>(); + .load_bytes(molecule_addr, molecule_size)?; + let pattern = + generated::ItemVec::from_compatible_slice(&pattern_bytes).map_err(|err| error!(err))?; + if pattern.len() > self.max_combination { + return Err(error!("too many combine operations")); + } + // handle DOB/1 pattern #[cfg(feature = "render_debug")] { - println!("-------- DOB/1 IMAGES ---------"); - images_uri_array.iter().for_each(|uri| println!("{uri}")); - println!("-------- DOB/1 IMAGES END ---------\n"); + println!("\n-------- DOB/1 IMAGES ---------"); } - - // fetch images from uri - let client = self.client.clone(); - let (tx, rx) = mpsc::channel(); - thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().unwrap(); - let mut client = client.lock().unwrap(); - tx.send(rt.block_on(client.fetch_images(&images_uri_array))) - .expect("send"); - }); - let mut images = rx - .recv() - .expect("recv") - .map_err(|err| error!(err))? - .into_iter() - .map(|image| load_from_memory(&image)) - .collect::, _>>() - .map_err(|err| error!(err))?; - if images.is_empty() { - return Err(error!("empty images")); - } - if images.len() > self.max_combination { - return Err(error!("exceesive images")); + let mut combination = image::DynamicImage::new_rgba8(1, 1); + for item in pattern.into_iter() { + match item.to_enum() { + generated::ItemUnion::Color(color) => { + let color_code = String::from_utf8_lossy(&color.raw_data()).to_string(); + #[cfg(feature = "render_debug")] + { + println!("COLOR => #{color_code}"); + } + let rgb = hex::decode(color_code).map_err(|err| error!(err))?; + if rgb.len() != 3 { + return Err(error!("invalid color code")); + } + let pixel = Rgb([rgb[0], rgb[1], rgb[2]]).to_rgba(); + let image = DynamicImage::ImageRgba8(RgbaImage::from_pixel(1, 1, pixel)); + overlay_both_images(&mut combination, image); + } + generated::ItemUnion::RawImage(raw_image) => { + #[cfg(feature = "render_debug")] + { + println!("IMAGE => (bytes length: {})", raw_image.raw_data().len()); + } + let image = + load_from_memory(&raw_image.raw_data()).map_err(|err| error!(err))?; + overlay_both_images(&mut combination, image); + } + generated::ItemUnion::URI(uri) => { + let uri = String::from_utf8_lossy(&uri.raw_data()).to_string(); + #[cfg(feature = "render_debug")] + { + println!("FSURI => {uri}"); + } + let raw_image = self.fetch_one_image_sync(uri).map_err(|err| error!(err))?; + let image = load_from_memory(&raw_image).map_err(|err| error!(err))?; + overlay_both_images(&mut combination, image); + } + } } - - // resize images to the maximum - let mut max_width = 0; - let mut max_height = 0; - images.iter().for_each(|image| { - max_width = max_width.max(image.width()); - max_height = max_height.max(image.height()); - }); - images.iter_mut().for_each(|image| { - image.resize(max_width, max_height, image::imageops::FilterType::Nearest); - }); - - // combine images into a single one - let mut combination = images.remove(0); - if buffer_size == 0 { - machine.memory_mut().store64( - &buffer_size_addr, - &Mac::REG::from_u64(combination.as_bytes().len() as u64), - )?; - return Ok(true); + #[cfg(feature = "render_debug")] + { + println!("-------- DOB/1 IMAGES END ---------"); } - images.into_iter().for_each(|image| { - image::imageops::overlay(&mut combination, &image, 0, 0); - }); // return output - let output = combination.as_bytes(); - let buffer_size = buffer_size.min(output.len() as u64); - machine.memory_mut().store_bytes(buffer_addr, output)?; + let mut output = Vec::new(); + let cursor = Cursor::new(&mut output); + let png = PngEncoder::new_with_quality(cursor, CompressionType::Best, FilterType::NoFilter); + combination + .write_with_encoder(png) + .map_err(|err| error!(err))?; + if buffer_size > 0 { + buffer_size = buffer_size.min(output.len() as u64); + machine + .memory_mut() + .store_bytes(buffer_addr, &output[..buffer_size as usize])?; + } else { + buffer_size = output.len() as u64; + } + std::fs::write("nervape.png", &output).unwrap(); machine .memory_mut() .store64(&buffer_size_addr, &Mac::REG::from_u64(buffer_size))?; @@ -162,6 +188,18 @@ impl Syscalls for ImageCombinationSyscall { } } +fn overlay_both_images(base: &mut DynamicImage, mut overlayer: DynamicImage) { + let width = base.width().max(overlayer.width()); + let height = base.height().max(overlayer.height()); + if base.width() != width || base.height() != height { + *base = base.resize(width, height, imageops::FilterType::Nearest); + } + if overlayer.width() != width || overlayer.height() != height { + overlayer = overlayer.resize(width, height, imageops::FilterType::Nearest); + } + imageops::overlay(base, &overlayer, 0, 0); +} + fn main_asm( code: Bytes, args: Vec, From a0cc581770d939d878771414365f99d9dc207f55 Mon Sep 17 00:00:00 2001 From: liyukun Date: Fri, 7 Jun 2024 12:53:32 +0800 Subject: [PATCH 05/17] chore: remove persistence code --- src/vm.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vm.rs b/src/vm.rs index 92dd89a..12eed59 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -179,7 +179,6 @@ impl Syscalls for ImageCombinationSyscall { } else { buffer_size = output.len() as u64; } - std::fs::write("nervape.png", &output).unwrap(); machine .memory_mut() .store64(&buffer_size_addr, &Mac::REG::from_u64(buffer_size))?; From e45a525d89336e8a13cf51b37d111d4c7edeea99 Mon Sep 17 00:00:00 2001 From: liyukun Date: Sun, 9 Jun 2024 13:15:03 +0800 Subject: [PATCH 06/17] feat: add base64 supported --- Cargo.lock | 9 +++-- Cargo.toml | 1 + src/types/generated/mod.rs | 1 + src/types/object.rs | 76 -------------------------------------- src/vm.rs | 22 +++++++---- 5 files changed, 22 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e30b91..1be5de8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,9 +141,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bech32" @@ -889,6 +889,7 @@ dependencies = [ name = "dob-decoder-server" version = "0.1.0" dependencies = [ + "base64 0.22.1", "ckb-hash", "ckb-jsonrpc-types", "ckb-sdk", @@ -2621,7 +2622,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2721,7 +2722,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index fad7c6d..b79a0c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +base64 = "0.22.1" ckb-sdk = "3.2.0" ckb-types = "0.116.1" ckb-jsonrpc-types = "0.116.1" diff --git a/src/types/generated/mod.rs b/src/types/generated/mod.rs index 8f93e5c..237a7cc 100644 --- a/src/types/generated/mod.rs +++ b/src/types/generated/mod.rs @@ -1,5 +1,6 @@ // Generated by Molecule 0.8.0 #![allow(dead_code)] +#![allow(clippy::all)] use molecule::prelude::*; #[derive(Clone)] diff --git a/src/types/object.rs b/src/types/object.rs index f3d60ba..4b77407 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -9,83 +9,7 @@ use jsonrpsee::types::ErrorCode; #[cfg(feature = "standalone_server")] use serde::Serialize; -<<<<<<< HEAD:src/types.rs -#[allow(clippy::enum_variant_names)] -#[derive(thiserror::Error, Debug)] -#[repr(i32)] -pub enum Error { - #[error("DNA bytes length not match the requirement in Cluster")] - DnaLengthNotMatch = 1001, - #[error("spore id length should equal to 32")] - SporeIdLengthInvalid, - #[error("natvie decoder not found")] - NativeDecoderNotFound, - #[error("spore id not exist on-chain")] - SporeIdNotFound, - #[error("uncompatible spore data")] - SporeDataUncompatible, - #[error("uncompatible spore data content_type")] - SporeDataContentTypeUncompatible, - #[error("unexpected DOB protocol version")] - DOBVersionUnexpected, - #[error("miss cluster id in spore data")] - ClusterIdNotSet, - #[error("cluster id not exist on-chain")] - ClusterIdNotFound, - #[error("uncompatible cluster data")] - ClusterDataUncompatible, - #[error("decoder id not exist on-chain")] - DecoderIdNotFound, - #[error("output of decoder should contain at least one line")] - DecoderOutputInvalid, - #[error("DNA string is not in hex format")] - HexedDNAParseError, - #[error("spore id string is not in hex format")] - HexedSporeIdParseError, - #[error("invalid decoder path to persist")] - DecoderBinaryPathInvalid, - #[error("encounter error while executing DNA decoding")] - DecoderExecutionError, - #[error("decoding program triggered an error")] - DecoderExecutionInternalError, - #[error("encounter error while searching live cells")] - FetchLiveCellsError, - #[error("encounter error while searching transaction by hash")] - FetchTransactionError, - #[error("not found specific output_cell in transaction")] - NoOutputCellInTransaction, - #[error("spore content cannot parse to DOB content")] - DOBContentUnexpected, - #[error("cluster description cannot parse to DOB metadata")] - DOBMetadataUnexpected, - #[error("DOB render cache folder not found")] - DOBRenderCacheNotFound, - #[error("cached DOB render result file has changed unexpectedly")] - DOBRenderCacheModified, - #[error("invalid deployed on-chain decoder code_hash")] - DecoderBinaryHashInvalid, - #[error("no binary found in cell for decoder")] - DecoderBinaryNotFoundInCell, - #[error("error ocurred while requesing json-rpc")] - JsonRpcRequestError, - #[error("error ocurred while requiring system timestamp")] - SystemTimeError, - #[error("BTC node responsed bad")] - FetchFromBtcNodeError, - #[error("BTC transaction format broken")] - InvalidBtcTransactionFormat, - #[error("Inscription format broken")] - InvalidInscriptionFormat, - #[error("Inscription content must be hex format")] - InvalidInscriptionContentHexFormat, - #[error("Inscription content must be filled")] - EmptyInscriptionContent, - #[error("fs header like 'btcfs://' and 'ckbfs://' are not contained")] - InvalidOnchainFsuriFormat, -} -======= use crate::types::Error; ->>>>>>> 6e23044 (feat: use molecule as parameter for syscall):src/types/object.rs pub enum Dob<'a> { V0(&'a DOBClusterFormatV0), diff --git a/src/vm.rs b/src/vm.rs index 12eed59..aa03812 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -4,6 +4,7 @@ use std::io::Cursor; use std::sync::{mpsc, Arc, Mutex}; use std::thread; +use base64::{engine::general_purpose::STANDARD, Engine}; use ckb_vm::cost_model::estimate_cycles; use ckb_vm::registers::{A0, A1, A2, A3, A7}; use ckb_vm::{Bytes, Memory, Register, SupportMachine, Syscalls}; @@ -128,9 +129,10 @@ impl Syscalls for ImageCombinationSyscall { let color_code = String::from_utf8_lossy(&color.raw_data()).to_string(); #[cfg(feature = "render_debug")] { - println!("COLOR => #{color_code}"); + println!("COLOR => {color_code}"); } - let rgb = hex::decode(color_code).map_err(|err| error!(err))?; + let rgb = hex::decode(color_code.trim_start_matches('#')) + .map_err(|err| error!(err))?; if rgb.len() != 3 { return Err(error!("invalid color code")); } @@ -141,10 +143,12 @@ impl Syscalls for ImageCombinationSyscall { generated::ItemUnion::RawImage(raw_image) => { #[cfg(feature = "render_debug")] { - println!("IMAGE => (bytes length: {})", raw_image.raw_data().len()); + println!("IMAGE => (base64 len: {})", raw_image.raw_data().len()); } - let image = - load_from_memory(&raw_image.raw_data()).map_err(|err| error!(err))?; + let image_bytes = STANDARD + .decode(raw_image.raw_data()) + .map_err(|err| error!(err))?; + let image = load_from_memory(&image_bytes).map_err(|err| error!(err))?; overlay_both_images(&mut combination, image); } generated::ItemUnion::URI(uri) => { @@ -172,10 +176,14 @@ impl Syscalls for ImageCombinationSyscall { .write_with_encoder(png) .map_err(|err| error!(err))?; if buffer_size > 0 { - buffer_size = buffer_size.min(output.len() as u64); + let mut base64_output = vec![]; + STANDARD + .encode_slice(output, &mut base64_output) + .map_err(|err| error!(err))?; + buffer_size = buffer_size.min(base64_output.len() as u64); machine .memory_mut() - .store_bytes(buffer_addr, &output[..buffer_size as usize])?; + .store_bytes(buffer_addr, &base64_output[..buffer_size as usize])?; } else { buffer_size = output.len() as u64; } From 9ac60c42b1959a5602b6b9786c40dcc099a618e5 Mon Sep 17 00:00:00 2001 From: liyukun Date: Mon, 10 Jun 2024 23:15:15 +0800 Subject: [PATCH 07/17] feat: complete the first available DOB/1 protocol version --- Cargo.lock | 24 ++++++++++++++++++ Cargo.toml | 1 + ...00000000000000000000000000000000000000.bin | Bin 27416 -> 98208 bytes src/client.rs | 17 ++++++++----- src/tests/dob1/decoder.rs | 6 ++--- src/types/error.rs | 2 ++ src/vm.rs | 8 +++--- 7 files changed, 45 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1be5de8..742e3a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,6 +900,7 @@ dependencies = [ "image", "jsonrpc-core", "jsonrpsee", + "lazy-regex", "molecule 0.8.0", "reqwest 0.12.4", "serde", @@ -1723,6 +1724,29 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "lazy-regex" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.57", +] + [[package]] name = "lazy_static" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index b79a0c6..c2781a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ ckb-vm = { version = "0.24", features = ["asm"] } thiserror = "1.0" serde_json = "1.0" hex = "0.4.3" +lazy-regex = "3.1.0" image = "0.25.1" reqwest = { version = "0.12.4", features = ["json"] } jsonrpc-core = "18.0" diff --git a/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin b/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin index 8f1a6a7f976d658d7b12b44f00325fb1651e94e5..ae39c4adc0cb94635642a2fdf28e6fdc63e69dc2 100755 GIT binary patch literal 98208 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 IJ= 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 delta 13279 zcmbtb4Ompg`k!-l&$7FMfQN7VSlJ+45fKzWu69@AtV%}aO>SA*u7@QtB~YN$bXVZ4 zqE^}RQnrmwJdaIjVua&L9nkag)z_d*N@0>Zu&+hYop8q}1bDWvq z`!Vml^UganXVH6&Tl+P$Dvn<~hC$@9%Ksv?b|zXqaE`0@-L$QiD1x>LGY7AV_a08g z)pD(s@~P#`aylTr`vj%GyOheQrHR7Gj^rASWhSt3Gw~26rXjGH(c)*BC|B3xR3_B^ z-<69M`5FH~&w}5_UFg5<_`j2%j_xqP^ZSr70-6!ZMry@_|)u4j*?<=LP^n6rH=WtpK=t=an76Pc)}?kyrE?v6#%X;Eep%_tX(=8YOWct&Q?&|%pbMMH*X4Yk20+p+m*2EP^{{WMpLz%^I0ioKaL<;+SnJ zo&$E6<`pk+j*^4qB@OMcm(t@FrLCGF%$@txT%Z+r&r3E3cg1v_m9VW^nF>q z79ZEAvl&`!G!!INX{m6uBGjO?!&D_&TxG~lXqu$Q^qo9{yVZpGEJ7yW zeDz6{@m8_UJc^%xfR?PHzEmm8C*i5a>8|7BDLG>TEe|VYi&Ey?Pw{%CRO#vY6mL?@ zdYJZ(0vd#PsNarTvm2Ba$G2&HV|@PZzd>~}i+ zV~-)a0qN;uOXlO$o~DS5A(>eaHN!`Ybj%P7okch`bnt(4vTqWe8=B%W&8R3hmCtmV zAV{6hn2P6_zyf$WL#PoZxrI=Kp0?Gvq0xlU?EySq;e)x7pR}n|?)Es^_Lz*PqVnmK z;ajCNEi|Q>Obd-^MyREyn-(oHElN!_Vtbe=5$#sACo0;Dp^Sw$;~8vv@Sv&4F?*g9 zO;Su(nI1DBlmJYVLhzoj{LD_nts`NLw4J2rT%w38$s4goyr5^cYo@{$qp&Sj+G(R6Akasqpnw?G&YbMbWJ;){zRGY$D|O#E3^lrX=02W|hLG4h`6FTntX=ZAlrWTy*T=t7^;7!qMwwBc>T12dAC!}10@pP+worjBV)TA{}r$( z8OKxmv*EBR`Vi>L!$B>c=v!z29)F>CLDzKYrrNFz}7QDHNgbUHh6V?K_`@; zFJq#r4|6!YoWq^-v^tWU0)6o>^AbqYl?F{uKZLrdh&}&?NS$s?h#Yz(#tKXb&7wS| zIZGJXyqSPE0Z#H385c46S-`deCZ|#bTLswmn=sd;KUqlXaw2(S+*zZJp?zx;Hh zF%9qnnEzf3ettm${`_gJ>mh)wgKmwYnSj{<11n&TdddTR7O=+vBZF24N`_De_``sc zI(bOh1lU7>8D)gZd;nM;U=I=OhNFz=DtThHI(bO>7+_k!h;GKZq>_%eoCBS1wAXJ3kTG-EM0t+4*iO_xqzgkWmH3^{8qhPVAk>n}` zB;yaaF8Uh4G6Bn@=D1aJUIDxiaCtc^F;2{U2k;`mCsKa98ZLhY{3XE2tdWNVdH}HP zfL$SRnbXsjp(Z_|=n4>DwB9-1Coq&Qbw80oZ7QL2@GF z91WOm9F%k^*eBoEM)7?v1O9xmR#yl-3FGnc#eI{PL3R6t>j8azQzOQ|4z%Myn?xeO zNM>d~AjUj=>RF4S3#wmEgglHdRPcrfIAdGxCoRZH1Ja;X1+8~M1iWXz`7}WkPXu~G zK3Rih?pW7sXgvfim{9p4qjdu|9)>N9+$j%ChkoS@8%K-azN^kYFS)IRB_)>@PCk>>}xh%RZ$ zJIS`H`lQq5(AUsLxGd1#q#d!4F!>OvORw)^+vI65A@qa0c^IWeZ>Lo5kA(WALJee5 zDz}wVlY8j({cT4SD$1l(ze+uyrq>U!9ahMu45j0@l#X?J{RG=kL_4;o^ zWDU0dOq@R9&Al(ckStmkH5$Z z!E4ueL-Ko%cnwD&wo2LZmcpFk4aMv%!4XUQ+PlnAJyJrNksK1;2Az1=ppAvQhsNqZ z8VkK(t^X*_!~S&khwg)7JrKk z;j&$ghlpLgw($_|!UG?Wd$dRRYUDnV7EP|{6BHjcDz@1%lPl#80qee0tn-H5%p2`b z0sAH^_BCT%QrFKVkIMU8pE6I!hSQtc4hQnDOukgmg&bRJq7Q2J)UcZOv~2aLD%2IX%+Fb{VL>v?^vd#OrLW9sAiNScE7Eoc$5M?y zR})#==Om3fm*D4Y(gdRN{>ypNmr|0tRQKI~;XN;9ivGkPcRKd6G-0D=h$gRX@@bwc zV=Wz&?*AYasdCnj2X#|4_)#h{*y#H6ULA6oqB}QpvMyh3?e0g)Jn>zYwp8TnkXHuh z_xU=>IC-wI5SOltOSgZ~;$$mRBN`yT!ITDaDrhaBQ8D*nyWvb^xk;AV;kpR4M4*9ct!D$VkCJ6@ zb}c8Y6DTL@B_eK;ua@VKW@FW6WQKR+5-8#2IlFeF8|v^9sR4o{49KNkaM>q`25w@~ zDa#r5e7xZEr?Oj^NSmTGlCLR7M`}`8&bCR3(dKYT+q$;qYAQ-&>6$R<)(M;qR*39$ z_atf<=Db>}#jY2_IGeAQMr5+U>%3a5^=d&P1Gz|=pH4)0E})eJUK`LV^J5eBTyD~j zmW%8!oBaA-H_Y;e@HLyYM{4xFZkT1)DP~24_EMJq6=DKs<8oEg0=5moBrD%fA}wO5@Z{M(d?MS1%8G-g{w15IA03C|bf74b-{?vchHJ*yGp zxU^XdAnCD;jpvNa5lQRVUMB|kr~DSpa~zxE30{u$F3DZq<3ybpC?8b&SMh*0jd)QK zQk026S@9ql%*XkL;V;ElH}X|1FBwYS~~H=Zz#`t{CSci6YQ)L)`nX!xc*o&8n#9`rD{)n$3B$ z&J;^ezP#+4CCh7;LqP$BfUD=0VJF`YkpZasVg%WND$CV~dN zyex6q5`nQTvC*zm*OSNXJzT@rNtcL55a+XNt2x2>p5zshO{pRF z7)IDr>Pjg8rEqHTp5laA7Z(&P+_f;~*^9Fa=1haVfh1tatd$L<^fbB*O%#zT8pz)5 z;XD@v#pRDsN^m^obv_U+Zv{N(pG$cK_ZJ^ip2m3;X>?;Qf^DE1ntO_(;3=vgxOm!b zB`_1zcs9jTp5*%|PjWuy+#1Dz2ZxiKDJLLrOuRsAC}x0Cv%{)^bVTV-wlT(VQ|XMT)y&RIHPPW8jlt^ zVQa~!+rJzE&8yXeOA-!$zpp6odP29QjT0=V*)HlDf4HTCm3KtUNf%QQb&mJlX=R=^ zOTZ{uuFyzLZuSL~ee<%V%^+(?jk9!su1=6L$!lT*UiJQ}jKB|EyCj6Rje%?<3H^m+ zZ!Fy4G*+i+fKv1KyODF6kK;)nd^$wtNbbc68&prMVA@qjo4#jqIUy-vX=6{*EkXJ_ z9{=H>2jwC8ziogLd;H&%l3(g?!CZ!h*Ii)fU;+#2%rzfdRk5tBcZtj`IS)qLs+NX2 z*7@~L&K9d7N7d?s_`ru|dhE3Rb4TZg21aQdc=6-p=mVf#XM20AXSudk0~2X5!9K?f!p-p4?z3vyUr(xHigj&(eKMl?by0l{j^e!dE|rAIK8Nc=@j2 zHStLgo5gvoI}FK;*t%JpByE@Sl*R0BHE$crxZBL{-gVPCQ@Fn3s0(Ew-QtlBs%sCO33Y^{43@tbjX1oqdHmz+m38tJWljF%h8omLY0lp$&AC@qW0wzj zuweJeWA^yB$s9}l&6wN9g8!9Z3XH>fdk6oy?xfHku7vbLNd4XVqH;Y{5{L9Y)g*1E znz;9G)#SA*GbrDt8b2HaGl)OhYt8d6Pl1CaoXh3FHys$(L^eYC^e+dN|5Hg&xAf+! zA$xr_xA3o31I_(!JbqtFX!B~7*}Doa*f*G`N>p5? z|7%JUUQ>PDyvLztC^wBGH;R>em%NnM!fjfko#sA z7yM{cdy3AkxL=v&_aQAzi!)VG;OXpwoe7ExOAw0N+F@|#EB#ZXjm>H{|}d1Uc9gg9!D z_kQAG^Joq=fO0G)KZ5fQ^cgUWq`lxmdApVk4`g$H^VLLprCf4PQ!g}VBCb2oUxdX$ zwYddGsB*)dI)g)um)ss><<#Suk{O8IMUqfad1jmCbUI{TbHr<`QIRw4haH&oIV@P( z0A=;gU@6^xl+-gC$9>u-L>|6Sj;DVX=L-C+cPP0Jk&e=r@dX0n!%JB{B;xJ{=nOxv zTO^97^b~WjvDu(_L`w0_=-V>#JSkV$gx;y{k1g9oiWF^VN%Msy~fJ z)qW0@^)rWmdUYfE8JW>;R2JLnJn;*!%g~9Xo3uEZ(;QmO6(#C+joTslS^pAV*L)f7 zPi*EITlCn~^ZQk<(b%Hp&sA6#iGHcy=so~fnN28N+AN6A?nIa)UW`Oz)9ZOBr!k{V zi1X?=%>Db?t)g46ixNd!>-msUHtYowH*irNl_N2@;o!ic>L&!vp4#l%?aR|w@LjQJ zxG@sNH}l4AEbbnM^6P4Y%zdGb=Ys3JO9b%+Bg)b}${hD-mg*3HcgPifTr84wd)BnB zJ#aO>F4Q6ItsWyGanU=>Yb)M6TP;W$&X%?EIV&D@=+6;<;8<-%=)WYv@|-zzCp#U# zb|@;M<~b{;T@*EC|Ef|9m$~WKf2fg}j-NcdI@9^7?{uhsYX{tV<$0GLwsgSm-N|0r zg3`Mzmk5fv0p!BX^!sZr5|7xBlO||zb-4}lyxZb)yv}v;ZD8ny-k-!(e=RprL5e~ z-l|MEX|SbBAE|?>BnDTx=~A^m2JVU<(oM2ZaQbaNyLhY5QuV4synn*m{?X^b`jB3u zYIoHlM|DxIMl)^=!av)PZ~3~jI6D??U6HwB+Glin)_gIgiTfd{Twl7jL|&7g`p+|0 zgxR*%?2_1qO_I+e*4YZ%<&QJB6kcc&PB~|r4sX&r) z*Pjd}ZO{=%ln5UZC6u%}m@jRBZt#__=7ls8M+wt$<B$4bYdJSiM{SSg;9VcMp zKgSAkhO@0xj#=`-Y~#!EWu7aRw)E4P9oZ0B>ermcwZ6_&y{}98wzJkJ$1~44F<$l0 zIF~z$oV_g`G8UfemdI&>`oy}Ld=3k(h$u_%%IplC6Hy%H)H~8UGEZm2v6u2Yi|FzI zgW(u#l;8j?n;-m>ugx6CX!UUo0df{_f5lgJho?)Cha1Q+q+n*GP2TIH(Y*!Ksloq= zrVuhu!JXg6hr9s>g}jk~Bfc9RLdL7NCFA_>ejs)1+0+-0K9NOv{#=OneBTeRIdOhC zq3Mf`kR1T)PJJntAq8#ZO9UU%8OiHAd%eAZ*L@y>>m~NWi?MPav6FV}`97MFne_)& zP~iissIcc1d@AtGK;j);<1x&Y4oXm>ym_ze108%Wh0}?O+M995$v?Y5F6QCY>aoL#-C_v{M(Jxcyl%*=7czZG}v4>(wTRulFsB%w$J&U&NTo3&Kl3&oH_$; zrLPdVGj9>rx6eu5Bk4fCrpSEF!{D-JKa)5} z?j!iRB>;EZHn(-o z@2VEN_hI&|IbN3ZA6F#bR;kS&;d{;|8h(gPfsY2pqId9&v#qoj|9XY@R9^2|Mawcu;r?=?hk(4VLLyv4 zY5s=ynjj0Grg>L-$JYU8j80z{v>Uw|}ljH=$lvD;Vlo{KpXTY$~SQ(cBI6O+f>x5KcR8S6~cMk78H z>81Pgx=z&0>YUz@e>$n{@JbD5KzLzyxF~RlukDSV^H%$NBfk4r26uPEw}s9y(N(F@ z97qG|X!H^X-(oahJjd%$xY+&~tFakSW%QucD!$`vBO1;{B5}o=F)qG#BK$1K*QTKa zZpV{5QGahHM;K7T$&2EBeKu5lxkd~# zB5}Tt(MVvT4qXwa_|cpVVw5jY4DW%&g>N!R6;sz}I703GRTMU%vJGPNo8j?wv$J&f zjV*xho2`jl2QK|4))g&rSc~+07`)wG;i9%tqsi^hX@nu1So*r2=c16=@S1jjAkKZg z7i8wTYC($zcy|d)a;pwjwG~MFo~~+`|HYaQt3g7O*Poj#iLC-dj@y%KNNqsw>*&k6 ztWdX=Gwv{Wt)!`zFI5 zeIFTeBmMl*9!%CyFACFU84DS9QA^>z;-tc?9xsp0d$~1nWb39C;2Ukyd~uPF%i<<5 z#0QuI9~^7cPL|+9sZhOZ1+#0^&!`jBPtMZ1#!H*Yw^%v;>?bI1uq3I1@E%uqQJ?yOOmBSb+TAqo zq22gIU3}!{S@um zTOW~z$cRGWEB9M7;5Uq<%Bso+dp^_KMYksI5wf$v*IXy$4teNk4DBSqsykv{AiZW~ zag=>~jipvxdlZ2ga%c9o(3$!|zJ3a-vs>!d7QQkws-VKJ8Yi(c`GwG4)e|=H^ON^;u>|$oH z@z_$T=GYVX#MGFERf`!@kHB>dHN3x=8KXJ&1Zwz|RN`i$hCjHNF@}4Uzp|Lruw*gQ G?|%VFgF@E; diff --git a/src/client.rs b/src/client.rs index 27f74ef..8f3bd46 100644 --- a/src/client.rs +++ b/src/client.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use ckb_jsonrpc_types::{CellWithStatus, JsonBytes, OutPoint, Uint32}; use ckb_sdk::rpc::ckb_indexer::{Cell, Order, Pagination, SearchKey}; use jsonrpc_core::futures::FutureExt; +use lazy_regex::regex_replace_all; use reqwest::{Client, Url}; use serde_json::Value; @@ -195,7 +196,7 @@ async fn parse_image_from_btcfs(url: &Url, index: usize) -> Result, Erro .ok_or(Error::InvalidBtcTransactionFormat)? .as_array() .ok_or(Error::InvalidBtcTransactionFormat)? - .get(index) + .get(0) .ok_or(Error::InvalidBtcTransactionFormat)?; let mut witness = vin .get("inner_witnessscript_asm") @@ -212,12 +213,10 @@ async fn parse_image_from_btcfs(url: &Url, index: usize) -> Result, Erro if !inscription.contains(header) { return Err(Error::InvalidInscriptionFormat); } - let hexed_image = inscription - .replace(header, "") - .replace(" OP_ENDIF", "") - .replace(" OP_PUSHDATA2 ", ""); + let base_removed = inscription.replace(header, ""); + let hexed = regex_replace_all!(r#"\s?OP\_\w+\s?"#, &base_removed, ""); let image = - hex::decode(hexed_image).map_err(|_| Error::InvalidInscriptionContentHexFormat)?; + hex::decode(hexed.as_bytes()).map_err(|_| Error::InvalidInscriptionContentHexFormat)?; images.push(image); witness = witness[end + "OP_ENDIF".len()..].to_owned(); } @@ -225,5 +224,9 @@ async fn parse_image_from_btcfs(url: &Url, index: usize) -> Result, Erro return Err(Error::EmptyInscriptionContent); } - Ok(images.remove(0)) + let image = images + .get(index) + .cloned() + .ok_or(Error::ExceededInscriptionIndex)?; + Ok(image) } diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs index 8dcf00d..130adf7 100644 --- a/src/tests/dob1/decoder.rs +++ b/src/tests/dob1/decoder.rs @@ -2,6 +2,7 @@ use std::io::Cursor; use ckb_types::h256; use image::codecs::png::{CompressionType, FilterType, PngEncoder}; +use serde_json::{json, Value}; use crate::client::ImageFetchClient; use crate::decoder::DOBDecoder; @@ -10,7 +11,6 @@ use crate::types::{ ClusterDescriptionField, DOBClusterFormat, DOBClusterFormatV0, DOBClusterFormatV1, DOBDecoderFormat, DecoderLocationType, }; -use serde_json::{json, Value}; fn generate_dob1_ingredients() -> (Value, ClusterDescriptionField) { let content = json!({ @@ -35,7 +35,7 @@ fn generate_dob1_ingredients() -> (Value, ClusterDescriptionField) { "0x0000000000000000000000000000000000000000000000000000000000000000" ), }, - pattern: Value::String("hello world".to_string()), + pattern: serde_json::from_str("[[\"0\",\"color\",\"Name\",\"options\",[[\"Alice\",\"#0000FF\"],[\"Bob\",\"#00FF00\"],[\"Ethan\",\"#FF0000\"],[[\"*\"],\"#FFFFFF\"]]],[\"0\",\"uri\",\"Age\",\"range\",[[[0,50],\"btcfs://b2f4560f17679d3e3fca66209ac425c660d28a252ef72444c3325c6eb0364393i0\"],[[51,100],\"btcfs://eb3910b3e32a5ed9460bd0d75168c01ba1b8f00cc0faf83e4d8b67b48ea79676i0\"],[[\"*\"],\"btcfs://11b6303eb7d887d7ade459ac27959754cd55f9f9e50345ced8e1e8f47f4581fai0\"]]],[\"1\",\"uri\",\"Score\",\"range\",[[[0,1000],\"btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei0\"],[[\"*\"],\"btcfs://e1484915b27e45b120239080fe5032580550ff9ff759eb26ee86bf8aaf90068bi0\"]]]]").unwrap(), }, }), }; @@ -46,7 +46,7 @@ fn generate_dob1_ingredients() -> (Value, ClusterDescriptionField) { async fn check_fetched_image() { let mut fetcher = ImageFetchClient::new("https://mempool.space/api/tx/", 100); let uris = vec![ - "btcfs://b2f4560f17679d3e3fca66209ac425c660d28a252ef72444c3325c6eb0364393i0".to_string(), + "btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei0".to_string(), ]; let images = fetcher.fetch_images(&uris).await.expect("fetch images"); let image_raw_bytes = images.first().expect("image"); diff --git a/src/types/error.rs b/src/types/error.rs index 46a86c7..134f79b 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -66,6 +66,8 @@ pub enum Error { InvalidInscriptionContentHexFormat, #[error("Inscription content must be filled")] EmptyInscriptionContent, + #[error("Inscription index flag exceeded")] + ExceededInscriptionIndex, #[error("fs header like 'btcfs://' and 'ckbfs://' are not contained")] InvalidOnchainFsuriFormat, } diff --git a/src/vm.rs b/src/vm.rs index aa03812..0920ff1 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -10,7 +10,6 @@ use ckb_vm::registers::{A0, A1, A2, A3, A7}; use ckb_vm::{Bytes, Memory, Register, SupportMachine, Syscalls}; use image::codecs::png::{CompressionType, FilterType, PngEncoder}; use image::{imageops, load_from_memory, DynamicImage, Pixel, Rgb, RgbaImage}; -use jsonrpsee::tracing; use molecule::prelude::Entity; use crate::client::ImageFetchClient; @@ -19,7 +18,10 @@ use crate::types::{generated, Error, Settings}; macro_rules! error { ($err: expr) => {{ let error = $err.to_string(); - tracing::error!("{error}"); + #[cfg(test)] + println!("[ERROR] {error}"); + #[cfg(not(test))] + jsonrpsee::tracing::error!("{error}"); ckb_vm::error::Error::Unexpected(error) }}; } @@ -176,7 +178,7 @@ impl Syscalls for ImageCombinationSyscall { .write_with_encoder(png) .map_err(|err| error!(err))?; if buffer_size > 0 { - let mut base64_output = vec![]; + let mut base64_output = vec![0u8; output.len() * 4 / 3 + 4]; STANDARD .encode_slice(output, &mut base64_output) .map_err(|err| error!(err))?; From d1b5b1fa54a8526c594fc336c362c8c5484460f8 Mon Sep 17 00:00:00 2001 From: liyukun Date: Tue, 11 Jun 2024 11:17:55 +0800 Subject: [PATCH 08/17] bug: fix syscall bug --- src/client.rs | 2 +- src/tests/dob1/decoder.rs | 21 +++++++++++++++++++-- src/vm.rs | 14 ++++++-------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8f3bd46..9c1f0c5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -196,7 +196,7 @@ async fn parse_image_from_btcfs(url: &Url, index: usize) -> Result, Erro .ok_or(Error::InvalidBtcTransactionFormat)? .as_array() .ok_or(Error::InvalidBtcTransactionFormat)? - .get(0) + .first() .ok_or(Error::InvalidBtcTransactionFormat)?; let mut witness = vin .get("inner_witnessscript_asm") diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs index 130adf7..8f9ef1f 100644 --- a/src/tests/dob1/decoder.rs +++ b/src/tests/dob1/decoder.rs @@ -35,7 +35,7 @@ fn generate_dob1_ingredients() -> (Value, ClusterDescriptionField) { "0x0000000000000000000000000000000000000000000000000000000000000000" ), }, - pattern: serde_json::from_str("[[\"0\",\"color\",\"Name\",\"options\",[[\"Alice\",\"#0000FF\"],[\"Bob\",\"#00FF00\"],[\"Ethan\",\"#FF0000\"],[[\"*\"],\"#FFFFFF\"]]],[\"0\",\"uri\",\"Age\",\"range\",[[[0,50],\"btcfs://b2f4560f17679d3e3fca66209ac425c660d28a252ef72444c3325c6eb0364393i0\"],[[51,100],\"btcfs://eb3910b3e32a5ed9460bd0d75168c01ba1b8f00cc0faf83e4d8b67b48ea79676i0\"],[[\"*\"],\"btcfs://11b6303eb7d887d7ade459ac27959754cd55f9f9e50345ced8e1e8f47f4581fai0\"]]],[\"1\",\"uri\",\"Score\",\"range\",[[[0,1000],\"btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei0\"],[[\"*\"],\"btcfs://e1484915b27e45b120239080fe5032580550ff9ff759eb26ee86bf8aaf90068bi0\"]]]]").unwrap(), + pattern: serde_json::from_str("[[\"0\",\"color\",\"Name\",\"options\",[[\"Alice\",\"#0000FF\"],[\"Bob\",\"#00FF00\"],[\"Ethan\",\"#FF0000\"],[[\"*\"],\"#FFFFFF\"]]],[\"0\",\"uri\",\"Age\",\"range\",[[[0,50],\"btcfs://b2f4560f17679d3e3fca66209ac425c660d28a252ef72444c3325c6eb0364393i0\"],[[51,100],\"btcfs://eb3910b3e32a5ed9460bd0d75168c01ba1b8f00cc0faf83e4d8b67b48ea79676i0\"],[[\"*\"],\"btcfs://11b6303eb7d887d7ade459ac27959754cd55f9f9e50345ced8e1e8f47f4581fai0\"]]],[\"0\",\"uri\",\"Score\",\"range\",[[[0,1000],\"btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei1\"],[[\"*\"],\"btcfs://e1484915b27e45b120239080fe5032580550ff9ff759eb26ee86bf8aaf90068bi1\"]]],[\"1\",\"uri\",\"Value\",\"range\",[[[0,100000],\"btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei0\"],[[\"*\"],\"btcfs://e1484915b27e45b120239080fe5032580550ff9ff759eb26ee86bf8aaf90068bi0\"]]]]").unwrap(), }, }), }; @@ -63,8 +63,25 @@ async fn test_dob1() { let settings = prepare_settings("text/plain"); let decoder = DOBDecoder::new(settings); let (content, metadata) = generate_dob1_ingredients(); - decoder + let _output = decoder .decode_dna(&content["dna"].as_str().unwrap(), metadata) .await .expect("decode dob/1"); + // use base64::{engine::general_purpose::STANDARD, Engine}; + // let dob1_output: Value = serde_json::from_str(&_output).unwrap(); + // let base64_image = dob1_output + // .get("images") + // .unwrap() + // .as_array() + // .unwrap() + // .first() + // .unwrap() + // .as_object() + // .unwrap() + // .get("content") + // .unwrap() + // .as_str() + // .unwrap(); + // let image = STANDARD.decode(base64_image).expect("decode base64 image"); + // std::fs::write("dob1.png", &image).expect("write image"); } diff --git a/src/vm.rs b/src/vm.rs index 0920ff1..9a198fa 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -177,17 +177,15 @@ impl Syscalls for ImageCombinationSyscall { combination .write_with_encoder(png) .map_err(|err| error!(err))?; + let base64_output = STANDARD.encode(output); if buffer_size > 0 { - let mut base64_output = vec![0u8; output.len() * 4 / 3 + 4]; - STANDARD - .encode_slice(output, &mut base64_output) - .map_err(|err| error!(err))?; buffer_size = buffer_size.min(base64_output.len() as u64); - machine - .memory_mut() - .store_bytes(buffer_addr, &base64_output[..buffer_size as usize])?; + machine.memory_mut().store_bytes( + buffer_addr, + &base64_output.as_bytes()[..buffer_size as usize], + )?; } else { - buffer_size = output.len() as u64; + buffer_size = base64_output.len() as u64; } machine .memory_mut() From d6bf6fa7cfb7d8b65c49c5b415f0e7654882f85b Mon Sep 17 00:00:00 2001 From: liyukun Date: Wed, 12 Jun 2024 10:19:24 +0800 Subject: [PATCH 09/17] chore: output format contains image type --- ...00000000000000000000000000000000000000.bin | Bin 98208 -> 98312 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin b/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin index ae39c4adc0cb94635642a2fdf28e6fdc63e69dc2..aaee957d018cde739d4cf7674a7f3a8ad9dd1971 100755 GIT binary patch delta 25167 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 delta 24902 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 F{|9Sw@Sp$y From cacdb1109d50ea29317eb299be58d30824260e97 Mon Sep 17 00:00:00 2001 From: liyukun Date: Mon, 17 Jun 2024 22:51:42 +0800 Subject: [PATCH 10/17] feat: add ipfs support --- settings.mainnet.toml | 2 +- settings.toml | 2 +- src/client.rs | 108 ++++++++++++++++++++++++++++---------- src/tests/dob1/decoder.rs | 39 +++++++++++--- src/tests/mod.rs | 18 ++++++- src/types/error.rs | 4 ++ src/types/object.rs | 4 +- 7 files changed, 137 insertions(+), 40 deletions(-) diff --git a/settings.mainnet.toml b/settings.mainnet.toml index f679cdd..864640e 100644 --- a/settings.mainnet.toml +++ b/settings.mainnet.toml @@ -7,7 +7,7 @@ protocol_versions = [ ckb_rpc = "https://mainnet.ckb.dev/" # connect to the image fetcher service -image_fetcher_url = "https://mempool.space/api/tx/" +image_fetcher_url = { btcfs = "https://mempool.space/api/tx/", ipfs = "https://ipfs.io/ipfs/" } # address that rpc server running at in case of standalone server mode rpc_server_address = "0.0.0.0:8090" diff --git a/settings.toml b/settings.toml index 11b6293..d7fa126 100644 --- a/settings.toml +++ b/settings.toml @@ -7,7 +7,7 @@ protocol_versions = [ ckb_rpc = "https://testnet.ckbapp.dev/" # connect to the image fetcher service -image_fetcher_url = "https://mempool.space/testnet/api/tx/" +image_fetcher_url = { btcfs = "https://mempool.space/testnet/api/tx/", ipfs = "https://ipfs.io/ipfs/" } # address that rpc server running at in case of standalone server mode rpc_server_address = "0.0.0.0:8090" diff --git a/src/client.rs b/src/client.rs index 9c1f0c5..a64dd43 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,6 @@ #![allow(clippy::assigning_clones)] -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::future::Future; use std::pin::Pin; use std::sync::atomic::{AtomicU64, Ordering}; @@ -119,14 +119,17 @@ impl RpcClient { } pub struct ImageFetchClient { - base_url: Url, + base_url: HashMap, images_cache: VecDeque<(Url, Vec)>, max_cache_size: usize, } impl ImageFetchClient { - pub fn new(base_url: &str, cache_size: usize) -> Self { - let base_url = Url::parse(base_url).expect("base url, e.g. \"http://127.0.0.1"); + pub fn new(base_url: &HashMap, cache_size: usize) -> Self { + let base_url = base_url + .iter() + .map(|(k, v)| (k.clone(), Url::parse(v).expect("url"))) + .collect::>(); Self { base_url, images_cache: VecDeque::new(), @@ -137,19 +140,53 @@ impl ImageFetchClient { pub async fn fetch_images(&mut self, images_uri: &[String]) -> Result>, Error> { let mut requests = vec![]; for uri in images_uri { - let (tx_hash, index) = parse_uri(uri)?; - let url = self.base_url.join(&tx_hash).expect("image url"); - let cached_image = self.images_cache.iter().find(|(v, _)| v == &url); - if let Some((_, image)) = cached_image { - requests.push(async { Ok((url, true, image.clone())) }.boxed()); - } else { - requests.push( - async move { - let image = parse_image_from_btcfs(&url, index).await?; - Ok((url, false, image)) + match uri.try_into()? { + URI::BTCFS(tx_hash, index) => { + let url = self + .base_url + .get("btcfs") + .ok_or(Error::FsuriNotFoundInConfig)? + .join(&tx_hash) + .expect("image url"); + let cached_image = self.images_cache.iter().find(|(v, _)| v == &url); + if let Some((_, image)) = cached_image { + requests.push(async { Ok((url, true, image.clone())) }.boxed()); + } else { + requests.push( + async move { + let image = parse_image_from_btcfs(&url, index).await?; + Ok((url, false, image)) + } + .boxed(), + ); } - .boxed(), - ); + } + URI::IPFS(cid) => { + let url = self + .base_url + .get("ipfs") + .ok_or(Error::FsuriNotFoundInConfig)? + .join(&cid) + .expect("image url"); + let cached_image = self.images_cache.iter().find(|(v, _)| v == &url); + if let Some((_, image)) = cached_image { + requests.push(async { Ok((url, true, image.clone())) }.boxed()); + } else { + requests.push( + async move { + let image = reqwest::get(url.clone()) + .await + .map_err(|_| Error::FetchFromIpfsError)? + .bytes() + .await + .map_err(|_| Error::FetchFromIpfsError)? + .to_vec(); + Ok((url, false, image)) + } + .boxed(), + ); + } + } } } let mut images = vec![]; @@ -168,19 +205,34 @@ impl ImageFetchClient { } } -fn parse_uri(uri: &str) -> Result<(String, usize), Error> { - let header = "xxxfs://".len(); - let body = uri.chars().skip(header).collect::(); - let parts: Vec<&str> = body.split('i').collect::>(); - if parts.len() != 2 { - return Err(Error::InvalidOnchainFsuriFormat); +#[allow(clippy::upper_case_acronyms)] +enum URI { + BTCFS(String, usize), + IPFS(String), +} + +impl TryFrom<&String> for URI { + type Error = Error; + + fn try_from(uri: &String) -> Result { + if uri.starts_with("btcfs://") { + let body = uri.chars().skip("btcfs://".len()).collect::(); + let parts: Vec<&str> = body.split('i').collect::>(); + if parts.len() != 2 { + return Err(Error::InvalidOnchainFsuriFormat); + } + let tx_hash = parts[0].to_string(); + let index = parts[1] + .parse() + .map_err(|_| Error::InvalidOnchainFsuriFormat)?; + Ok(URI::BTCFS(tx_hash, index)) + } else if uri.starts_with("ipfs://") { + let hash = uri.chars().skip("ipfs://".len()).collect::(); + Ok(URI::IPFS(hash)) + } else { + Err(Error::InvalidOnchainFsuriFormat) + } } - Ok(( - parts[0].to_string(), - parts[1] - .parse() - .map_err(|_| Error::InvalidOnchainFsuriFormat)?, - )) } async fn parse_image_from_btcfs(url: &Url, index: usize) -> Result, Error> { diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs index 8f9ef1f..9af7a1a 100644 --- a/src/tests/dob1/decoder.rs +++ b/src/tests/dob1/decoder.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::io::Cursor; use ckb_types::h256; @@ -6,6 +7,7 @@ use serde_json::{json, Value}; use crate::client::ImageFetchClient; use crate::decoder::DOBDecoder; +use crate::hashmap; use crate::tests::prepare_settings; use crate::types::{ ClusterDescriptionField, DOBClusterFormat, DOBClusterFormatV0, DOBClusterFormatV1, @@ -42,20 +44,43 @@ fn generate_dob1_ingredients() -> (Value, ClusterDescriptionField) { (content, metadata) } +macro_rules! png_decoder { + ($image:expr) => {{ + let rgba = image::load_from_memory(&$image).expect("load image"); + let mut inner_buffer = Vec::new(); + let buffer = Cursor::new(&mut inner_buffer); + let encoder = + PngEncoder::new_with_quality(buffer, CompressionType::Best, FilterType::NoFilter); + rgba.write_with_encoder(encoder).expect("write image"); + inner_buffer + }}; + () => {}; +} + #[tokio::test] async fn check_fetched_image() { - let mut fetcher = ImageFetchClient::new("https://mempool.space/api/tx/", 100); + let url = hashmap!(("btcfs", "https://mempool.space/api/tx/")); + let mut fetcher = ImageFetchClient::new(&url, 100); let uris = vec![ "btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei0".to_string(), ]; let images = fetcher.fetch_images(&uris).await.expect("fetch images"); let image_raw_bytes = images.first().expect("image"); - let rgba = image::load_from_memory(&image_raw_bytes).expect("load image"); - let mut inner_buffer = Vec::new(); - let buffer = Cursor::new(&mut inner_buffer); - let encoder = PngEncoder::new_with_quality(buffer, CompressionType::Best, FilterType::NoFilter); - rgba.write_with_encoder(encoder).expect("write image"); - println!("image size: {:?}", inner_buffer.len()); + let png = png_decoder!(image_raw_bytes); + println!("image size: {:?}", png.len()); + // std::fs::write("dob1.png", &png).expect("write image"); +} + +#[tokio::test] +async fn check_ipfs_image() { + let url = hashmap!(("ipfs", "https://ipfs.io/ipfs/")); + let mut fetcher = ImageFetchClient::new(&url, 100); + let uris = vec!["ipfs://QmWwrfhWxtkSD526cghpTMqHJ468iCy8sPYA5S1q4BTams".to_string()]; + let images = fetcher.fetch_images(&uris).await.expect("fetch images"); + let image_raw_bytes = images.first().expect("image"); + let png = png_decoder!(image_raw_bytes); + println!("image size: {:?}", png.len()); + // std::fs::write("dob1.png", &png).expect("write image"); } #[tokio::test] diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 1a818c1..06509cb 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use ckb_types::h256; use crate::types::{HashType, OnchainDecoderDeployment, ScriptId, Settings}; @@ -5,10 +7,24 @@ use crate::types::{HashType, OnchainDecoderDeployment, ScriptId, Settings}; mod dob0; mod dob1; +#[macro_export] +macro_rules! hashmap { + ($(($key:expr, $value:expr) $(,)?)+) => {{ + let mut map = HashMap::new(); + $( + map.insert($key.to_owned(), $value.to_owned()); + )+ + map + }}; +} + fn prepare_settings(version: &str) -> Settings { Settings { ckb_rpc: "https://testnet.ckbapp.dev/".to_string(), - image_fetcher_url: "https://mempool.space/api/tx/".to_string(), + image_fetcher_url: hashmap!( + ("btcfs", "https://mempool.space/api/tx/"), + ("ipfs", "https://ipfs.io/ipfs/") + ), protocol_versions: vec![version.to_string()], ckb_vm_runner: "ckb-vm-runner".to_string(), decoders_cache_directory: "cache/decoders".parse().unwrap(), diff --git a/src/types/error.rs b/src/types/error.rs index 134f79b..671c559 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -70,4 +70,8 @@ pub enum Error { ExceededInscriptionIndex, #[error("fs header like 'btcfs://' and 'ckbfs://' are not contained")] InvalidOnchainFsuriFormat, + #[error("fs header like 'btcfs://' and 'ckbfs://' are not configured in config file")] + FsuriNotFoundInConfig, + #[error("IPFS Gateway responsed bad")] + FetchFromIpfsError, } diff --git a/src/types/object.rs b/src/types/object.rs index 4b77407..1981589 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use ckb_types::{core::ScriptHashType, H256}; use serde::Deserialize; @@ -171,7 +171,7 @@ pub struct ScriptId { pub struct Settings { pub protocol_versions: Vec, pub ckb_rpc: String, - pub image_fetcher_url: String, + pub image_fetcher_url: HashMap, pub rpc_server_address: String, pub ckb_vm_runner: String, pub decoders_cache_directory: PathBuf, From e4dfaf9b842cc2d163e49e2233ec2a827597c72a Mon Sep 17 00:00:00 2001 From: liyukun Date: Tue, 18 Jun 2024 11:31:30 +0800 Subject: [PATCH 11/17] feat: keep output format in raw bytes --- ...00000000000000000000000000000000000000.bin | Bin 98312 -> 102040 bytes src/vm.rs | 12 +++++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin b/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin index aaee957d018cde739d4cf7674a7f3a8ad9dd1971..a06636c7ce134dee944a30ae3d4b4010f9daa0e9 100755 GIT binary patch delta 37722 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%0Dfv{#SvYZq$9lu>P1~Mo&vN9BNmH|VXS!_Y$=_## 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 z zN!eiX^#2_fu7wqWTZMP8MX3=~$lO`nw-%)|vM=v_^8Mg{bysv(2d^f_#UM6dTp_rA z(VdynoWr`a^>73*hdi2SGjQ=bba*MnB3`R>gf~c$MN^Y@N9uqQg(@vCY+JNVYV%H!TK_a#zQiN|Dy)zY86`_SpSA~JeJo=x-CyHS6LCFZGZYzGq+Y%Y2 zcJNqdW|(b?<~hFPo@tIerUTs$658B+tIHhIt~c=Qd=vd{!YW;>%B}2ZyC>(F$#S$HO3j5(WuWKBYq3#SG$h)WKa;G9k+w7(}k9 zR_i#_I-x3X6loosTBp%gO_wr}>gCTWg0(6Fu~$P-$RLg_;GZ%C1q@;<1OJplEcp!L zE(iaVLijZd!b=+2&}s$=mBJK6Xf`_G$0ZoTToqx-m;EZj90suze>9+)UY9mO3iPU$ zc8|3%xX4BFRHnWwgLjppg-hrRZf-WnE!;%p#AcuIDw)~M4P>uu8crH=*{x&}q2nt48}HqS zk}_T`TS@b&AyVLo**+r)rls`CzqGWS^aXV7O{xR@a7$7|8-`zTm|cQ?l4 zhOBjUm`{H*Az+YQH<@jSiTz`>Xe4SH$GulOi~K{!yzCDT??Erds^4_$i|8MZoqN%} z{r!7UDuOUU$A$DcIC39~ou;V2SFG%Y6$Acq6R~w4S}<*-3(@Z*?ev3t@&?`p@VF|k zjsmIfyALg#Qhbboi(660&!yAxjeW=%Esg`RsS&N10)@1#z;W$QCF15rIJthY S6(5DNs55F=KEARK-Ti-Rf>87T delta 34320 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 O7xL1RIm++a3 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 KbK*)bO8Ezk*^24_ diff --git a/src/vm.rs b/src/vm.rs index 9a198fa..284b079 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -177,15 +177,13 @@ impl Syscalls for ImageCombinationSyscall { combination .write_with_encoder(png) .map_err(|err| error!(err))?; - let base64_output = STANDARD.encode(output); if buffer_size > 0 { - buffer_size = buffer_size.min(base64_output.len() as u64); - machine.memory_mut().store_bytes( - buffer_addr, - &base64_output.as_bytes()[..buffer_size as usize], - )?; + buffer_size = buffer_size.min(output.len() as u64); + machine + .memory_mut() + .store_bytes(buffer_addr, &output[..buffer_size as usize])?; } else { - buffer_size = base64_output.len() as u64; + buffer_size = output.len() as u64; } machine .memory_mut() From 0c7b10243728628521e336188c96695808ab4c6e Mon Sep 17 00:00:00 2001 From: liyukun Date: Tue, 18 Jun 2024 15:29:45 +0800 Subject: [PATCH 12/17] chore: update dob/1 decoder --- ...00000000000000000000000000000000000000.bin | Bin 102040 -> 96880 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin b/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin index a06636c7ce134dee944a30ae3d4b4010f9daa0e9..abaa32ec8b5c9e61d4f27d023c96f82b0f9e8e42 100755 GIT binary patch delta 32963 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 delta 38137 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 pTgA*0G(ly)_gqneo?rof3je;gRaC7 Date: Wed, 19 Jun 2024 15:26:03 +0800 Subject: [PATCH 13/17] chore: verify dob/1 decoder with data in reality --- README.md | 10 ++++ ...c0b60eed2b6ce9f10bf7bf2ee76190c3a0071.bin} | Bin settings.toml | 12 +++++ src/tests/dob1/decoder.rs | 43 +++++++++++++++++- src/tests/mod.rs | 9 ++++ 5 files changed, 73 insertions(+), 1 deletion(-) rename cache/decoders/{code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin => code_hash_ac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071.bin} (100%) diff --git a/README.md b/README.md index 2e7fbec..f2d59f3 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,13 @@ refer to error definitions [here](https://github.com/sporeprotocol/dob-decoder-s | 1026 | DecoderBinaryNotFoundInCell | | 1027 | JsonRpcRequestError | | 1028 | SystemTimeError | +| 1029 | JsonRpcReques | +| 1030 | FetchFromBtcNodeError | +| 1031 | InvalidBtcTransactionFormat | +| 1032 | InvalidInscriptionFormat | +| 1033 | InvalidInscriptionContentHexFormat | +| 1034 | EmptyInscriptionContent | +| 1035 | ExceededInscriptionIndex | +| 1036 | InvalidOnchainFsuriFormat | +| 1037 | FsuriNotFoundInConfig | +| 1038 | FetchFromIpfsError | diff --git a/cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin b/cache/decoders/code_hash_ac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071.bin similarity index 100% rename from cache/decoders/code_hash_0000000000000000000000000000000000000000000000000000000000000000.bin rename to cache/decoders/code_hash_ac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071.bin diff --git a/settings.toml b/settings.toml index d7fa126..33aa9ea 100644 --- a/settings.toml +++ b/settings.toml @@ -56,6 +56,10 @@ hash_type = "data1" # associate `code_hash` with the corresponding onchain information about `tx_hash` and `out_index` # server will firstly search onchain decoders by `code_hash` in this configuration, if not found, cache will be used instead + +# +# DOB/0 +# [[onchain_decoder_deployment]] code_hash = "0xb82abd59ade361a014f0abb692f71b0feb880693c3ccb95b9137b73551d872ce" tx_hash = "0xb2497dc3e616055125ef8276be7ee21986d2cd4b2ce90992725386cabcb6ea7f" @@ -75,3 +79,11 @@ out_index = 0 code_hash = "0x13cac78ad8482202f18f9df4ea707611c35f994375fa03ae79121312dda9925c" tx_hash = "0x4a8a0d079f8438bed89e0ece1b14e67ab68e2aa7688a5f4917a59a185e0f8fd5" out_index = 0 + +# +# DOB/1 +# +[[onchain_decoder_deployment]] +code_hash = "0xac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071" +tx_hash = "0x1918a656f7c52ca5fbe7a903903c9bbc89d3e05525b4bb9f323fceb1a5bde51f" +out_index = 0 diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs index 9af7a1a..6ef091d 100644 --- a/src/tests/dob1/decoder.rs +++ b/src/tests/dob1/decoder.rs @@ -34,7 +34,7 @@ fn generate_dob1_ingredients() -> (Value, ClusterDescriptionField) { decoder: DOBDecoderFormat { location: DecoderLocationType::CodeHash, hash: h256!( - "0x0000000000000000000000000000000000000000000000000000000000000000" + "0xac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071" ), }, pattern: serde_json::from_str("[[\"0\",\"color\",\"Name\",\"options\",[[\"Alice\",\"#0000FF\"],[\"Bob\",\"#00FF00\"],[\"Ethan\",\"#FF0000\"],[[\"*\"],\"#FFFFFF\"]]],[\"0\",\"uri\",\"Age\",\"range\",[[[0,50],\"btcfs://b2f4560f17679d3e3fca66209ac425c660d28a252ef72444c3325c6eb0364393i0\"],[[51,100],\"btcfs://eb3910b3e32a5ed9460bd0d75168c01ba1b8f00cc0faf83e4d8b67b48ea79676i0\"],[[\"*\"],\"btcfs://11b6303eb7d887d7ade459ac27959754cd55f9f9e50345ced8e1e8f47f4581fai0\"]]],[\"0\",\"uri\",\"Score\",\"range\",[[[0,1000],\"btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei1\"],[[\"*\"],\"btcfs://e1484915b27e45b120239080fe5032580550ff9ff759eb26ee86bf8aaf90068bi1\"]]],[\"1\",\"uri\",\"Value\",\"range\",[[[0,100000],\"btcfs://11d6cc654f4c0759bfee520966937a4304db2b33880c88c2a6c649e30c7b9aaei0\"],[[\"*\"],\"btcfs://e1484915b27e45b120239080fe5032580550ff9ff759eb26ee86bf8aaf90068bi0\"]]]]").unwrap(), @@ -44,6 +44,15 @@ fn generate_dob1_ingredients() -> (Value, ClusterDescriptionField) { (content, metadata) } +#[test] +fn test_print_dob1_ingreidents() { + let (_, dob_metadata) = generate_dob1_ingredients(); + println!( + "cluster_description: {}", + serde_json::to_string(&dob_metadata).unwrap() + ); +} + macro_rules! png_decoder { ($image:expr) => {{ let rgba = image::load_from_memory(&$image).expect("load image"); @@ -110,3 +119,35 @@ async fn test_dob1() { // let image = STANDARD.decode(base64_image).expect("decode base64 image"); // std::fs::write("dob1.png", &image).expect("write image"); } + +#[tokio::test] +async fn test_fetch_dob1() { + let settings = prepare_settings("text/plain"); + let decoder = DOBDecoder::new(settings); + let dob1_spore_id = + hex::decode("2fa44c408dfb78f5f032f6cb30966f9122df7e906c38bcbbd1fe9751ad3b2083").unwrap(); + let (_, metadata) = decoder + .fetch_decode_ingredients(dob1_spore_id.try_into().unwrap()) + .await + .unwrap(); + let dob1_dna = "ac7b88aabbcc687474703a2f2f3132372e302e302e313a383039300000"; + let output = decoder.decode_dna(dob1_dna, metadata).await.unwrap(); + println!("dob1 output: {}", output); + // use base64::{engine::general_purpose::STANDARD, Engine}; + // let dob1_output: Value = serde_json::from_str(&output).unwrap(); + // let base64_image = dob1_output + // .get("images") + // .unwrap() + // .as_array() + // .unwrap() + // .first() + // .unwrap() + // .as_object() + // .unwrap() + // .get("content") + // .unwrap() + // .as_str() + // .unwrap(); + // let image = STANDARD.decode(base64_image).expect("decode base64 image"); + // std::fs::write("dob1.png", &image).expect("write image"); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 06509cb..ba07649 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -78,6 +78,15 @@ fn prepare_settings(version: &str) -> Settings { ), out_index: 0, }, + OnchainDecoderDeployment { + code_hash: h256!( + "0xac35b0e6178dc4a89fb85194b4ac0b60eed2b6ce9f10bf7bf2ee76190c3a0071" + ), + tx_hash: h256!( + "0x1918a656f7c52ca5fbe7a903903c9bbc89d3e05525b4bb9f323fceb1a5bde51f" + ), + out_index: 0, + }, ], ..Default::default() } From df412c5e9acb92ca4bfca1347c7a50e78a6b6301 Mon Sep 17 00:00:00 2001 From: liyukun Date: Wed, 19 Jun 2024 15:37:42 +0800 Subject: [PATCH 14/17] chore: use dna from return value directly --- src/tests/dob1/decoder.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs index 6ef091d..b5421b8 100644 --- a/src/tests/dob1/decoder.rs +++ b/src/tests/dob1/decoder.rs @@ -126,12 +126,11 @@ async fn test_fetch_dob1() { let decoder = DOBDecoder::new(settings); let dob1_spore_id = hex::decode("2fa44c408dfb78f5f032f6cb30966f9122df7e906c38bcbbd1fe9751ad3b2083").unwrap(); - let (_, metadata) = decoder + let ((_, dna), metadata) = decoder .fetch_decode_ingredients(dob1_spore_id.try_into().unwrap()) .await .unwrap(); - let dob1_dna = "ac7b88aabbcc687474703a2f2f3132372e302e302e313a383039300000"; - let output = decoder.decode_dna(dob1_dna, metadata).await.unwrap(); + let output = decoder.decode_dna(&dna, metadata).await.unwrap(); println!("dob1 output: {}", output); // use base64::{engine::general_purpose::STANDARD, Engine}; // let dob1_output: Value = serde_json::from_str(&output).unwrap(); From 2edbeec1020047ca9c190af8a285769479fe7e8a Mon Sep 17 00:00:00 2001 From: liyukun Date: Mon, 1 Jul 2024 18:49:21 +0800 Subject: [PATCH 15/17] chore: enable serialize feature for library case --- src/types/object.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/types/object.rs b/src/types/object.rs index 1981589..83e53f4 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -25,7 +25,8 @@ impl From for ErrorCode { // value on `description` field in Cluster data, adapting for DOB protocol in JSON format #[derive(Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub struct ClusterDescriptionField { pub description: String, pub dob: DOBClusterFormat, @@ -59,7 +60,8 @@ impl ClusterDescriptionField { // // note: if `ver` is empty, `dob_ver_0` must uniquely exist #[derive(Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub struct DOBClusterFormat { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -92,14 +94,16 @@ impl DOBClusterFormat { } #[derive(Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub struct DOBClusterFormatV0 { pub decoder: DOBDecoderFormat, pub pattern: Value, } #[derive(Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub struct DOBClusterFormatV1 { pub traits: DOBClusterFormatV0, pub images: DOBClusterFormatV0, @@ -107,7 +111,8 @@ pub struct DOBClusterFormatV1 { // restricted decoder locator type #[derive(Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub enum DecoderLocationType { #[serde(rename(serialize = "type_id", deserialize = "type_id"))] TypeId, @@ -117,7 +122,8 @@ pub enum DecoderLocationType { // decoder location information #[derive(Deserialize)] -#[cfg_attr(test, derive(serde::Serialize, PartialEq, Debug))] +#[cfg_attr(feature = "standalone_server", derive(Serialize))] +#[cfg_attr(test, derive(PartialEq, Debug))] pub struct DOBDecoderFormat { #[serde(rename(serialize = "type", deserialize = "type"))] pub location: DecoderLocationType, From 0ead7c859c6f3359fb9245deadf2f4bce303b69a Mon Sep 17 00:00:00 2001 From: liyukun Date: Tue, 2 Jul 2024 10:44:16 +0800 Subject: [PATCH 16/17] chore: solve conflicts --- src/decoder.rs | 341 --------------------------------------------- src/types/error.rs | 2 + 2 files changed, 2 insertions(+), 341 deletions(-) delete mode 100644 src/decoder.rs diff --git a/src/decoder.rs b/src/decoder.rs deleted file mode 100644 index e9a9a50..0000000 --- a/src/decoder.rs +++ /dev/null @@ -1,341 +0,0 @@ -use ckb_sdk::{constants::TYPE_ID_CODE_HASH, traits::CellQueryOptions}; -use ckb_types::{ - core::ScriptHashType, - packed::{OutPoint, Script}, - prelude::{Builder, Entity, Pack}, - H256, -}; -use serde_json::Value; -use spore_types::generated::spore::{ClusterData, SporeData}; - -use crate::{ - client::RpcClient, - types::{ClusterDescriptionField, DecoderLocationType, Error, ScriptId, Settings}, -}; - -type DecodeResult = Result; - -pub struct DOBDecoder { - rpc: RpcClient, - settings: Settings, -} - -impl DOBDecoder { - pub fn new(settings: Settings) -> Self { - Self { - rpc: RpcClient::new(&settings.ckb_rpc, &settings.ckb_rpc), - settings, - } - } - - pub fn protocol_versions(&self) -> Vec { - self.settings.protocol_versions.clone() - } - - pub fn setting(&self) -> &Settings { - &self.settings - } - - pub async fn fetch_decode_ingredients( - &self, - spore_id: [u8; 32], - ) -> DecodeResult<((Value, String), ClusterDescriptionField)> { - let (content, cluster_id) = self.fetch_dob_content(spore_id).await?; - let dob_metadata = self.fetch_dob_metadata(cluster_id).await?; - Ok((content, dob_metadata)) - } - - // decode DNA under target spore_id - pub async fn decode_dna( - &self, - dna: &str, - dob_metadata: ClusterDescriptionField, - ) -> DecodeResult { - let decoder_path = match dob_metadata.dob.decoder.location { - DecoderLocationType::CodeHash => { - let mut decoder_path = self.settings.decoders_cache_directory.clone(); - decoder_path.push(format!( - "code_hash_{}.bin", - hex::encode(&dob_metadata.dob.decoder.hash) - )); - if !decoder_path.exists() { - let onchain_decoder = - self.settings - .onchain_decoder_deployment - .iter() - .find_map(|deployment| { - if deployment.code_hash == dob_metadata.dob.decoder.hash { - Some(self.fetch_decoder_binary_directly( - deployment.tx_hash.clone(), - deployment.out_index, - )) - } else { - None - } - }); - let Some(decoder_binary) = onchain_decoder else { - return Err(Error::NativeDecoderNotFound); - }; - let decoder_file_content = decoder_binary.await?; - if ckb_hash::blake2b_256(&decoder_file_content) - != dob_metadata.dob.decoder.hash.0 - { - return Err(Error::DecoderBinaryHashInvalid); - } - std::fs::write(decoder_path.clone(), decoder_file_content) - .map_err(|_| Error::DecoderBinaryPathInvalid)?; - } - decoder_path - } - DecoderLocationType::TypeId => { - let mut decoder_path = self.settings.decoders_cache_directory.clone(); - decoder_path.push(format!( - "type_id_{}.bin", - hex::encode(&dob_metadata.dob.decoder.hash) - )); - if !decoder_path.exists() { - let decoder_binary = self - .fetch_decoder_binary(dob_metadata.dob.decoder.hash.into()) - .await?; - std::fs::write(decoder_path.clone(), decoder_binary) - .map_err(|_| Error::DecoderBinaryPathInvalid)?; - } - decoder_path - } - }; - let pattern = match &dob_metadata.dob.pattern { - Value::String(string) => string.to_owned(), - pattern => pattern.to_string(), - }; - let raw_render_result = { - let (exit_code, outputs) = crate::vm::execute_riscv_binary( - &decoder_path.to_string_lossy(), - vec![dna.to_owned().into(), pattern.into()], - ) - .map_err(|_| Error::DecoderExecutionError)?; - #[cfg(feature = "render_debug")] - { - println!("-------- DECODE RESULT ({exit_code}) ---------"); - outputs.iter().for_each(|output| println!("{output}")); - println!("-------- DECODE RESULT END ---------"); - } - if exit_code != 0 { - return Err(Error::DecoderExecutionInternalError); - } - outputs.first().ok_or(Error::DecoderOutputInvalid)?.clone() - }; - Ok(raw_render_result) - } - - // // invoke `ckb-vm-runner` in native machine and collect console output as result - // #[cfg(not(feature = "embeded_vm"))] - // fn execute_externally( - // &self, - // decoder_path: std::path::PathBuf, - // dna: &str, - // pattern: &str, - // ) -> DecodeResult { - // let output = std::process::Command::new(&self.settings.ckb_vm_runner) - // .arg(decoder_path) - // .arg(dna) - // .arg(pattern) - // .output() - // .map_err(|_| Error::DecoderExecutionError)?; - // let raw_render_result = { - // let console_output = String::from_utf8_lossy(&output.stdout) - // .to_string() - // .replace('\\', ""); - // let lines = console_output - // .split('\n') - // .map(|line| line.trim_matches('\"')) - // .collect::>(); - // #[cfg(feature = "render_debug")] - // { - // println!("-------- DECODE RESULT ---------"); - // lines.iter().for_each(|line| println!("{line}")); - // println!("-------- DECODE RESULT END ---------"); - // } - // lines - // .first() - // .ok_or(Error::DecoderOutputInvalid)? - // .to_string() - // }; - // Ok(raw_render_result) - // } - - // search on-chain spore cell and return its content field, which represents dob content - async fn fetch_dob_content( - &self, - spore_id: [u8; 32], - ) -> DecodeResult<((Value, String), [u8; 32])> { - let mut spore_cell = None; - for spore_search_option in - build_batch_search_options(spore_id, &self.settings.available_spores) - { - spore_cell = self - .rpc - .get_cells(spore_search_option.into(), 1, None) - .await - .map_err(|_| Error::FetchLiveCellsError)? - .objects - .first() - .cloned(); - if spore_cell.is_some() { - break; - } - } - let Some(spore_cell) = spore_cell else { - return Err(Error::SporeIdNotFound); - }; - let molecule_spore_data = - SporeData::from_compatible_slice(spore_cell.output_data.unwrap_or_default().as_bytes()) - .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() - && !self - .settings - .protocol_versions - .iter() - .any(|version| content_type.starts_with(version)) - { - return Err(Error::DOBVersionUnexpected); - } - let cluster_id = molecule_spore_data - .cluster_id() - .to_opt() - .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())) - } - - // search on-chain cluster cell and return its description field, which contains dob metadata - async fn fetch_dob_metadata( - &self, - cluster_id: [u8; 32], - ) -> DecodeResult { - let mut cluster_cell = None; - for cluster_search_option in - build_batch_search_options(cluster_id, &self.settings.available_clusters) - { - cluster_cell = self - .rpc - .get_cells(cluster_search_option.into(), 1, None) - .await - .map_err(|_| Error::FetchLiveCellsError)? - .objects - .first() - .cloned(); - if cluster_cell.is_some() { - break; - } - } - let Some(cluster_cell) = cluster_cell else { - return Err(Error::ClusterIdNotFound); - }; - let molecule_cluster_data = ClusterData::from_compatible_slice( - cluster_cell.output_data.unwrap_or_default().as_bytes(), - ) - .map_err(|_| Error::ClusterDataUncompatible)?; - let dob_metadata = serde_json::from_slice(&molecule_cluster_data.description().raw_data()) - .map_err(|_| Error::DOBMetadataUnexpected)?; - Ok(dob_metadata) - } - - // search on-chain decoder cell, deployed with type_id feature enabled - async fn fetch_decoder_binary(&self, decoder_id: [u8; 32]) -> DecodeResult> { - let decoder_search_option = build_type_id_search_option(decoder_id); - let decoder_cell = self - .rpc - .get_cells(decoder_search_option.into(), 1, None) - .await - .map_err(|_| Error::FetchLiveCellsError)? - .objects - .first() - .cloned() - .ok_or(Error::DecoderIdNotFound)?; - Ok(decoder_cell - .output_data - .unwrap_or_default() - .as_bytes() - .into()) - } - - // search on-chain decoder cell, directly by its tx_hash and out_index - async fn fetch_decoder_binary_directly( - &self, - tx_hash: H256, - out_index: u32, - ) -> DecodeResult> { - let decoder_cell = self - .rpc - .get_live_cell(&OutPoint::new(tx_hash.pack(), out_index).into(), true) - .await - .map_err(|_| Error::FetchTransactionError)?; - let decoder_binary = decoder_cell - .cell - .ok_or(Error::NoOutputCellInTransaction)? - .data - .ok_or(Error::DecoderBinaryNotFoundInCell)? - .content; - Ok(decoder_binary.as_bytes().to_vec()) - } -} - -fn build_type_id_search_option(type_id_args: [u8; 32]) -> CellQueryOptions { - let type_script = Script::new_builder() - .code_hash(TYPE_ID_CODE_HASH.0.pack()) - .hash_type(ScriptHashType::Type.into()) - .args(type_id_args.to_vec().pack()) - .build(); - CellQueryOptions::new_type(type_script) -} - -fn build_batch_search_options( - type_args: [u8; 32], - available_script_ids: &[ScriptId], -) -> Vec { - available_script_ids - .iter() - .map( - |ScriptId { - code_hash, - hash_type, - }| { - let hash_type: ScriptHashType = hash_type.into(); - let type_script = Script::new_builder() - .code_hash(code_hash.0.pack()) - .hash_type(hash_type.into()) - .args(type_args.to_vec().pack()) - .build(); - CellQueryOptions::new_type(type_script) - }, - ) - .collect() -} - -pub(crate) fn decode_spore_data(spore_data: &[u8]) -> Result<(Value, String), Error> { - if spore_data[0] == 0u8 { - let dna = hex::encode(&spore_data[1..]); - return Ok((serde_json::Value::String(dna.clone()), dna)); - } - - let value: Value = - serde_json::from_slice(spore_data).map_err(|_| Error::DOBContentUnexpected)?; - let dna = match &value { - serde_json::Value::String(_) => &value, - serde_json::Value::Array(array) => array.first().ok_or(Error::DOBContentUnexpected)?, - serde_json::Value::Object(object) => { - object.get("dna").ok_or(Error::DOBContentUnexpected)? - } - _ => return Err(Error::DOBContentUnexpected), - }; - let dna = match dna { - serde_json::Value::String(string) => string.to_owned(), - _ => return Err(Error::DOBContentUnexpected), - }; - - Ok((value, dna)) -} diff --git a/src/types/error.rs b/src/types/error.rs index 671c559..d239c58 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -56,6 +56,8 @@ pub enum Error { DecoderBinaryNotFoundInCell, #[error("error ocurred while requesing json-rpc")] JsonRpcRequestError, + #[error("system time calculation error")] + SystemTimeError, #[error("BTC node responsed bad")] FetchFromBtcNodeError, #[error("BTC transaction format broken")] From 107a50fa700890b11b5f13a4c4150e4ca1e85c17 Mon Sep 17 00:00:00 2001 From: liyukun Date: Tue, 2 Jul 2024 10:52:08 +0800 Subject: [PATCH 17/17] chore: take place with cuprush ipfs file --- src/tests/dob1/decoder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/dob1/decoder.rs b/src/tests/dob1/decoder.rs index b5421b8..eeb306f 100644 --- a/src/tests/dob1/decoder.rs +++ b/src/tests/dob1/decoder.rs @@ -84,7 +84,7 @@ async fn check_fetched_image() { async fn check_ipfs_image() { let url = hashmap!(("ipfs", "https://ipfs.io/ipfs/")); let mut fetcher = ImageFetchClient::new(&url, 100); - let uris = vec!["ipfs://QmWwrfhWxtkSD526cghpTMqHJ468iCy8sPYA5S1q4BTams".to_string()]; + let uris = vec!["ipfs://QmeQ6TfqzsjJCMtYmpbyZeMxiSzQGc6Aqg6NyJTeLYrrJr".to_string()]; let images = fetcher.fetch_images(&uris).await.expect("fetch images"); let image_raw_bytes = images.first().expect("image"); let png = png_decoder!(image_raw_bytes);