diff --git a/.gitignore b/.gitignore index 0199232fe6..53625dde02 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ web-root **/.idea/ /rust-toolchain /.vscode/ +.zed **/db-* /testing/integration/testdata/dags_for_json_tests/goref-mainnet /testing/integration/testdata/dags_for_json_tests/goref-1.6M-tx-10K-blocks diff --git a/Cargo.lock b/Cargo.lock index efdd871231..efe872557d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -57,7 +57,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if 1.0.0", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -324,7 +324,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -335,13 +335,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -499,7 +499,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -576,7 +576,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", "syn_derive", ] @@ -827,9 +827,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -837,9 +837,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -849,21 +849,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" @@ -963,7 +963,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.19", + "clap 4.5.51", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1060,7 +1060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -1128,7 +1128,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -1137,8 +1137,18 @@ version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.10", + "darling_macro 0.20.10", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -1152,7 +1162,21 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.79", + "syn 2.0.110", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.110", ] [[package]] @@ -1161,9 +1185,20 @@ version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core", + "darling_core 0.20.10", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -1195,7 +1230,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -1234,10 +1269,10 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -1247,7 +1282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" dependencies = [ "derive_builder_core", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -1260,7 +1295,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -1388,7 +1423,7 @@ checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c" dependencies = [ "num-traits", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -1465,7 +1500,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -1610,7 +1645,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -1667,6 +1702,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gimli" version = "0.31.1" @@ -2079,7 +2126,7 @@ dependencies = [ "http 0.2.12", "hyper 0.14.30", "log", - "rand", + "rand 0.8.5", "tokio", "url", "xmltree", @@ -2096,7 +2143,7 @@ dependencies = [ "delegate-display", "fancy_constructor", "js-sys", - "uuid 1.10.0", + "uuid 1.18.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2274,7 +2321,7 @@ dependencies = [ "local-ip-address", "log", "parking_lot", - "rand", + "rand 0.8.5", "rocksdb", "rv", "serde", @@ -2296,15 +2343,15 @@ dependencies = [ "borsh", "bs58", "faster-hex", - "getrandom", + "getrandom 0.2.15", "hmac", "js-sys", "kaspa-consensus-core", "kaspa-utils", "once_cell", "pbkdf2", - "rand", - "rand_core", + "rand 0.8.5", + "rand_core 0.6.4", "ripemd", "secp256k1", "serde", @@ -2376,7 +2423,7 @@ dependencies = [ "kaspa-utils", "log", "parking_lot", - "rand", + "rand 0.8.5", "tokio", ] @@ -2412,7 +2459,7 @@ dependencies = [ "log", "once_cell", "parking_lot", - "rand", + "rand 0.8.5", "rand_distr", "rayon", "rocksdb", @@ -2441,7 +2488,7 @@ dependencies = [ "kaspa-txscript", "kaspa-utils", "kaspa-wasm-core", - "rand", + "rand 0.8.5", "secp256k1", "serde", "serde-wasm-bindgen", @@ -2465,7 +2512,7 @@ dependencies = [ "criterion", "faster-hex", "futures-util", - "getrandom", + "getrandom 0.2.15", "itertools 0.13.0", "js-sys", "kaspa-addresses", @@ -2476,7 +2523,7 @@ dependencies = [ "kaspa-muhash", "kaspa-txscript-errors", "kaspa-utils", - "rand", + "rand 0.8.5", "secp256k1", "serde", "serde-wasm-bindgen", @@ -2524,7 +2571,7 @@ dependencies = [ "kaspa-hashes", "kaspa-txscript", "kaspa-utils", - "rand", + "rand 0.8.5", "secp256k1", "serde", "serde-wasm-bindgen", @@ -2550,7 +2597,7 @@ dependencies = [ "kaspa-utils", "log", "parking_lot", - "rand", + "rand 0.8.5", "tokio", ] @@ -2609,7 +2656,7 @@ dependencies = [ "num-traits", "num_cpus", "parking_lot", - "rand", + "rand 0.8.5", "rocksdb", "serde", "smallvec", @@ -2639,7 +2686,7 @@ dependencies = [ "parking_lot", "paste", "prost", - "rand", + "rand 0.8.5", "regex", "rustls", "thiserror", @@ -2669,7 +2716,7 @@ dependencies = [ "log", "paste", "prost", - "rand", + "rand 0.8.5", "regex", "thiserror", "tokio", @@ -2706,14 +2753,14 @@ dependencies = [ "parking_lot", "paste", "prost", - "rand", + "rand 0.8.5", "rustls", "thiserror", "tokio", "tokio-stream", "tonic", "triggered", - "uuid 1.10.0", + "uuid 1.18.1", ] [[package]] @@ -2729,7 +2776,7 @@ dependencies = [ "kaspa-utils", "keccak", "once_cell", - "rand", + "rand 0.8.5", "serde", "sha2", "sha3", @@ -2778,7 +2825,7 @@ dependencies = [ "log", "parking_lot", "paste", - "rand", + "rand 0.8.5", "thiserror", "tokio", "triggered", @@ -2795,7 +2842,7 @@ dependencies = [ "kaspa-utils", "malachite-base", "malachite-nz", - "rand_chacha", + "rand_chacha 0.3.1", "serde", "serde-wasm-bindgen", "thiserror", @@ -2846,7 +2893,7 @@ dependencies = [ "kaspa-utils", "log", "parking_lot", - "rand", + "rand 0.8.5", "secp256k1", "serde", "smallvec", @@ -2870,8 +2917,8 @@ dependencies = [ "criterion", "kaspa-hashes", "kaspa-math", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rayon", "serde", ] @@ -2901,7 +2948,7 @@ dependencies = [ "log", "parking_lot", "paste", - "rand", + "rand 0.8.5", "serde", "thiserror", "tokio", @@ -2937,11 +2984,11 @@ dependencies = [ "kaspa-utils-tower", "log", "parking_lot", - "rand", + "rand 0.8.5", "thiserror", "tokio", "tokio-stream", - "uuid 1.10.0", + "uuid 1.18.1", ] [[package]] @@ -2964,7 +3011,7 @@ dependencies = [ "log", "parking_lot", "prost", - "rand", + "rand 0.8.5", "seqlock", "serde", "thiserror", @@ -2972,7 +3019,7 @@ dependencies = [ "tokio-stream", "tonic", "tonic-build", - "uuid 1.10.0", + "uuid 1.18.1", ] [[package]] @@ -3049,13 +3096,14 @@ dependencies = [ "kaspa-utils", "log", "paste", - "rand", + "rand 0.8.5", "serde", "serde-wasm-bindgen", "serde_json", + "serde_nested_with", "smallvec", "thiserror", - "uuid 1.10.0", + "uuid 1.18.1", "wasm-bindgen", "workflow-core", "workflow-serializer", @@ -3112,7 +3160,7 @@ dependencies = [ "async-trait", "bincode", "chrono", - "clap 4.5.19", + "clap 4.5.51", "criterion", "crossbeam-channel", "dhat", @@ -3150,7 +3198,7 @@ dependencies = [ "kaspad", "log", "parking_lot", - "rand", + "rand 0.8.5", "rand_distr", "rayon", "rocksdb", @@ -3184,7 +3232,7 @@ dependencies = [ "kaspa-wasm-core", "log", "parking_lot", - "rand", + "rand 0.8.5", "secp256k1", "serde", "serde-wasm-bindgen", @@ -3226,7 +3274,7 @@ dependencies = [ "num_cpus", "once_cell", "parking_lot", - "rand", + "rand 0.8.5", "rlimit", "serde", "serde_json", @@ -3236,7 +3284,7 @@ dependencies = [ "thiserror", "tokio", "triggered", - "uuid 1.10.0", + "uuid 1.18.1", "wasm-bindgen", ] @@ -3271,7 +3319,7 @@ dependencies = [ "kaspa-utils", "log", "parking_lot", - "rand", + "rand 0.8.5", "rocksdb", "serde", "thiserror", @@ -3356,7 +3404,7 @@ dependencies = [ "md-5", "pad", "pbkdf2", - "rand", + "rand 0.8.5", "regex", "ripemd", "secp256k1", @@ -3401,7 +3449,7 @@ dependencies = [ "kaspa-txscript-errors", "kaspa-utils", "kaspa-wasm-core", - "rand", + "rand 0.8.5", "ripemd", "secp256k1", "serde", @@ -3516,7 +3564,7 @@ dependencies = [ "kaspa-rpc-core", "kaspa-rpc-macros", "paste", - "rand", + "rand 0.8.5", "regex", "rustls", "serde", @@ -3555,7 +3603,7 @@ name = "kaspa-wrpc-proxy" version = "1.0.1" dependencies = [ "async-trait", - "clap 4.5.19", + "clap 4.5.51", "kaspa-consensus-core", "kaspa-grpc-client", "kaspa-rpc-core", @@ -3607,6 +3655,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "kaspa-wrpc-vcc-v2" +version = "1.0.1" +dependencies = [ + "futures", + "kaspa-rpc-core", + "kaspa-wrpc-client", + "tokio", +] + [[package]] name = "kaspa-wrpc-wasm" version = "1.0.1" @@ -3643,7 +3701,7 @@ version = "1.0.1" dependencies = [ "async-channel 2.3.1", "cfg-if 1.0.0", - "clap 4.5.19", + "clap 4.5.51", "dhat", "dirs", "futures-util", @@ -3675,7 +3733,7 @@ dependencies = [ "kaspa-wrpc-server", "log", "num_cpus", - "rand", + "rand 0.8.5", "rayon", "rocksdb", "serde", @@ -3869,7 +3927,7 @@ dependencies = [ "log-mdc", "once_cell", "parking_lot", - "rand", + "rand 0.8.5", "serde", "serde-value", "serde_json", @@ -3938,7 +3996,7 @@ dependencies = [ "cfg-if 1.0.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -3949,7 +4007,7 @@ checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -3962,7 +4020,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -4123,7 +4181,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] @@ -4439,7 +4497,7 @@ checksum = "70df726c43c645ef1dde24c7ae14692036ebe5457c92c5f0ec4cfceb99634ff6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -4449,7 +4507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -4491,7 +4549,7 @@ dependencies = [ "order-stat", "peroxide-ad", "puruspe", - "rand", + "rand 0.8.5", "rand_distr", ] @@ -4532,7 +4590,7 @@ checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -4618,7 +4676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -4653,11 +4711,33 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -4689,7 +4769,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.79", + "syn 2.0.110", "tempfile", ] @@ -4703,7 +4783,7 @@ dependencies = [ "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -4746,7 +4826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" dependencies = [ "bytes", - "rand", + "rand 0.8.5", "ring", "rustc-hash 2.1.1", "rustls", @@ -4771,13 +4851,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -4785,8 +4871,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -4796,7 +4892,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -4805,7 +4911,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -4815,7 +4930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -4859,7 +4974,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror", ] @@ -4946,7 +5061,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if 1.0.0", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -4986,7 +5101,7 @@ name = "rothschild" version = "1.0.1" dependencies = [ "async-channel 2.3.1", - "clap 4.5.19", + "clap 4.5.51", "criterion", "faster-hex", "itertools 0.13.0", @@ -5000,7 +5115,7 @@ dependencies = [ "kaspa-utils", "log", "parking_lot", - "rand", + "rand 0.8.5", "rayon", "secp256k1", "tokio", @@ -5104,7 +5219,7 @@ dependencies = [ "num", "num-traits", "peroxide", - "rand", + "rand 0.8.5", "rand_distr", "special", ] @@ -5145,7 +5260,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "rand", + "rand 0.8.5", "secp256k1-sys", "serde", ] @@ -5221,7 +5336,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -5236,6 +5351,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_nested_with" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc84538493ef215370434907a7dca8117778d16ac1acd0482ce88a0f5cf19707" +dependencies = [ + "darling 0.21.3", + "proc-macro-error2", + "quote", + "syn 2.0.110", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -5244,7 +5371,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -5292,10 +5419,10 @@ version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -5395,7 +5522,7 @@ version = "1.0.1" dependencies = [ "async-channel 2.3.1", "cfg-if 1.0.0", - "clap 4.5.19", + "clap 4.5.51", "dhat", "futures", "futures-util", @@ -5412,7 +5539,7 @@ dependencies = [ "kaspa-utils", "log", "num_cpus", - "rand", + "rand 0.8.5", "rand_distr", "rayon", "secp256k1", @@ -5528,9 +5655,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -5546,7 +5673,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -5658,7 +5785,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -5769,7 +5896,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -5902,7 +6029,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -5916,7 +6043,7 @@ dependencies = [ "indexmap 1.9.3", "pin-project", "pin-project-lite", - "rand", + "rand 0.8.5", "slab", "tokio", "tokio-util", @@ -5986,7 +6113,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -6022,7 +6149,7 @@ dependencies = [ "http 1.1.0", "httparse", "log", - "rand", + "rand 0.8.5", "rustls", "rustls-pki-types", "sha1", @@ -6144,17 +6271,18 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] name = "uuid" -version = "1.10.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom", - "rand", + "getrandom 0.3.4", + "js-sys", + "rand 0.9.2", "serde", "wasm-bindgen", ] @@ -6223,6 +6351,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -6247,7 +6384,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", "wasm-bindgen-shared", ] @@ -6282,7 +6419,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6317,7 +6454,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -6421,7 +6558,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -6432,7 +6569,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] @@ -6631,6 +6768,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "workflow-chrome" version = "0.18.0" @@ -6661,10 +6804,10 @@ dependencies = [ "dirs", "faster-hex", "futures", - "getrandom", + "getrandom 0.2.15", "instant", "js-sys", - "rand", + "rand 0.8.5", "rlimit", "serde", "serde-wasm-bindgen", @@ -6792,7 +6935,7 @@ dependencies = [ "futures", "js-sys", "nw-sys", - "rand", + "rand 0.8.5", "serde", "serde-wasm-bindgen", "thiserror", @@ -6842,9 +6985,9 @@ dependencies = [ "downcast-rs", "futures", "futures-util", - "getrandom", + "getrandom 0.2.15", "manual_future", - "rand", + "rand 0.8.5", "serde", "serde_json", "thiserror", @@ -7087,7 +7230,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.110", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 53ecd8c8e9..cdc3845544 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ members = [ "rpc/wrpc/wasm", "rpc/wrpc/examples/subscriber", "rpc/wrpc/examples/simple_client", + "rpc/wrpc/examples/vcc_v2", "mining", "mining/errors", "protocol/p2p", @@ -145,7 +146,7 @@ argon2 = "0.5.2" async-channel = "2.0.0" async-std = { version = "1.12.0", features = ['attributes'] } async-stream = "0.3.5" -async-trait = "0.1.74" +async-trait = "0.1.88" base64 = "0.22.1" bincode = { version = "1.3.3", default-features = false } bitflags = "2.9.4" @@ -157,7 +158,7 @@ cc = "1.0.83" cfb-mode = "0.8.2" cfg-if = "1.0.0" chacha20poly1305 = "0.10.1" -clap = { version = "4.4.7", features = ["derive", "string", "cargo"] } +clap = { version = "4.5.35", features = ["derive", "string", "cargo"] } convert_case = "0.6.0" criterion = { version = "0.5.1", default-features = false } crossbeam-channel = "0.5.8" @@ -213,7 +214,7 @@ num-traits = "0.2.17" once_cell = "1.18.0" pad = "0.1.6" parking_lot = "0.12.1" -paste = "1.0.14" +paste = "1.0.15" pbkdf2 = "0.12.2" portable-atomic = { version = "1.5.1", features = ["float"] } prost = "0.13.2" @@ -236,6 +237,7 @@ separator = "0.4.1" seqlock = "0.2.0" serde = { version = "1.0.190", features = ["derive", "rc"] } serde_bytes = "0.11.12" +serde_nested_with = "0.2.5" # helper, can be removed when https://github.com/serde-rs/serde/issues/723 is reseolved serde_json = "1.0.107" serde_repr = "0.1.18" serde-value = "0.7.0" @@ -257,7 +259,7 @@ toml = "0.8.8" tonic = { version = "0.12.3", features = ["tls-webpki-roots", "gzip", "transport"] } tonic-build = { version = "0.12.3", features = ["prost"] } triggered = "0.1.2" -uuid = { version = "1.5.0", features = ["v4", "fast-rng", "serde"] } +uuid = { version = "1.16.0", features = ["v4", "fast-rng", "serde", "js"] } wasm-bindgen = { version = "0.2.100", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.43" wasm-bindgen-test = "0.3.50" @@ -339,4 +341,4 @@ strip = false [workspace.lints.clippy] empty_docs = "allow" -uninlined_format_args = "allow" \ No newline at end of file +uninlined_format_args = "allow" diff --git a/cli/src/modules/rpc.rs b/cli/src/modules/rpc.rs index 75bc50f421..6273e27cee 100644 --- a/cli/src/modules/rpc.rs +++ b/cli/src/modules/rpc.rs @@ -290,6 +290,30 @@ impl Rpc { self.println(&ctx, result); } + RpcApiOps::GetVirtualChainFromBlockV2 => { + if argv.is_empty() { + return Err(Error::custom("Missing startHash argument")); + }; + let start_hash = RpcHash::from_hex(argv.remove(0).as_str())?; + + argv.reverse(); // reverse so we can pop from the end + + let verbosity_level_i32 = argv.pop().and_then(|arg| arg.parse::().ok()).unwrap_or_default(); + let verbosity_level = RpcDataVerbosityLevel::try_from(verbosity_level_i32)?; + + let result = rpc + .get_virtual_chain_from_block_v2_call( + None, + GetVirtualChainFromBlockV2Request { + start_hash, + data_verbosity_level: Some(verbosity_level), + min_confirmation_count: None, + }, + ) + .await; + + self.println(&ctx, result); + } _ => { tprintln!(ctx, "rpc method exists but is not supported by the cli: '{op_str}'\r\n"); return Ok(()); diff --git a/components/consensusmanager/src/session.rs b/components/consensusmanager/src/session.rs index 6c91df8a79..58f85308af 100644 --- a/components/consensusmanager/src/session.rs +++ b/components/consensusmanager/src/session.rs @@ -3,7 +3,7 @@ //! We use newtypes in order to simplify changing the underlying lock in the future use kaspa_consensus_core::{ - acceptance_data::AcceptanceData, + acceptance_data::{AcceptanceData, MergesetBlockAcceptanceData}, api::{BlockCount, BlockValidationFutures, ConsensusApi, ConsensusStats, DynConsensus}, block::Block, blockstatus::BlockStatus, @@ -13,8 +13,7 @@ use kaspa_consensus_core::{ mass::{ContextualMasses, NonContextualMasses}, pruning::{PruningPointProof, PruningPointTrustedData, PruningPointsList}, trusted::{ExternalGhostdagData, TrustedBlock}, - tx::{MutableTransaction, SignableTransaction, Transaction, TransactionOutpoint, UtxoEntry}, - utxo::utxo_inquirer::UtxoInquirerError, + tx::{MutableTransaction, Transaction, TransactionId, TransactionOutpoint, TransactionQueryResult, TransactionType, UtxoEntry}, BlockHashSet, BlueWorkType, ChainPath, Hash, }; use kaspa_utils::sync::rwlock::*; @@ -262,7 +261,7 @@ impl ConsensusSessionOwned { self.clone().spawn_blocking(move |c| c.get_current_block_color(hash)).await } - /// retention period root refers to the earliest block from which the current node has full header & block data + /// retention period root refers to the earliest block from which the current node has full header & block data pub async fn async_get_retention_period_root(&self) -> Hash { self.clone().spawn_blocking(|c| c.get_retention_period_root()).await } @@ -316,12 +315,27 @@ impl ConsensusSessionOwned { self.clone().spawn_blocking(|c| c.get_chain_block_samples()).await } - pub async fn async_get_populated_transaction( + pub async fn async_get_transactions_by_accepting_daa_score( &self, - txid: Hash, - accepting_block_daa_score: u64, - ) -> Result { - self.clone().spawn_blocking(move |c| c.get_populated_transaction(txid, accepting_block_daa_score)).await + accepting_daa_score: u64, + tx_ids: Option>, + tx_type: TransactionType, + ) -> ConsensusResult { + self.clone().spawn_blocking(move |c| c.get_transactions_by_accepting_daa_score(accepting_daa_score, tx_ids, tx_type)).await + } + + pub async fn async_get_transactions_by_block_acceptance_data( + &self, + accepting_block: Hash, + block_acceptance_data: MergesetBlockAcceptanceData, + tx_ids: Option>, + tx_type: TransactionType, + ) -> ConsensusResult { + self.clone() + .spawn_blocking(move |c| { + c.get_transactions_by_block_acceptance_data(accepting_block, block_acceptance_data, tx_ids, tx_type) + }) + .await } /// Returns the antipast of block `hash` from the POV of `context`, i.e. `antipast(hash) ∩ past(context)`. diff --git a/consensus/core/src/acceptance_data.rs b/consensus/core/src/acceptance_data.rs index 2ab2355839..95fcaa20ea 100644 --- a/consensus/core/src/acceptance_data.rs +++ b/consensus/core/src/acceptance_data.rs @@ -3,9 +3,11 @@ use serde::{Deserialize, Serialize}; use crate::tx::TransactionId; +/// Holds a mergeset acceptance data, a list of all its merged block with their accepted transactions pub type AcceptanceData = Vec; #[derive(Debug, Clone, Serialize, Deserialize)] +/// Holds a merged block with its accepted transactions pub struct MergesetBlockAcceptanceData { pub block_hash: Hash, pub accepted_transactions: Vec, diff --git a/consensus/core/src/api/mod.rs b/consensus/core/src/api/mod.rs index f85bcdb9c2..e8d55d8166 100644 --- a/consensus/core/src/api/mod.rs +++ b/consensus/core/src/api/mod.rs @@ -3,7 +3,7 @@ use kaspa_muhash::MuHash; use std::sync::Arc; use crate::{ - acceptance_data::AcceptanceData, + acceptance_data::{AcceptanceData, MergesetBlockAcceptanceData}, api::args::{TransactionValidationArgs, TransactionValidationBatchArgs}, block::{Block, BlockTemplate, TemplateBuildMode, TemplateTransactionSelector, VirtualStateApproxId}, blockstatus::BlockStatus, @@ -20,8 +20,10 @@ use crate::{ mass::{ContextualMasses, NonContextualMasses}, pruning::{PruningPointProof, PruningPointTrustedData, PruningPointsList, PruningProofMetadata}, trusted::{ExternalGhostdagData, TrustedBlock}, - tx::{MutableTransaction, SignableTransaction, Transaction, TransactionOutpoint, UtxoEntry}, - utxo::utxo_inquirer::UtxoInquirerError, + tx::{ + MutableTransaction, Transaction, TransactionId, TransactionIndexType, TransactionOutpoint, TransactionQueryResult, + TransactionType, UtxoEntry, + }, BlockHashSet, BlueWorkType, ChainPath, }; use kaspa_hashes::Hash; @@ -151,7 +153,7 @@ pub trait ConsensusApi: Send + Sync { unimplemented!() } - /// retention period root refers to the earliest block from which the current node has full header & block data + /// retention period root refers to the earliest block from which the current node has full header & block data fn get_retention_period_root(&self) -> Hash { unimplemented!() } @@ -162,7 +164,7 @@ pub trait ConsensusApi: Send + Sync { /// Gets the virtual chain paths from `low` to the `sink` hash, or until `chain_path_added_limit` is reached /// - /// Note: + /// Note: /// 1) `chain_path_added_limit` will populate removed fully, and then the added chain path, up to `chain_path_added_limit` amount of hashes. /// 1.1) use `None to impose no limit with optimized backward chain iteration, for better performance in cases where batching is not required. fn get_virtual_chain_from_block(&self, low: Hash, chain_path_added_limit: Option) -> ConsensusResult { @@ -175,7 +177,32 @@ pub trait ConsensusApi: Send + Sync { /// Returns the fully populated transaction with the given txid which was accepted at the provided accepting_block_daa_score. /// The argument `accepting_block_daa_score` is expected to be the DAA score of the accepting chain block of `txid`. - fn get_populated_transaction(&self, txid: Hash, accepting_block_daa_score: u64) -> Result { + /// Note: If the transaction vec is None, the function returns all accepted transactions. + fn get_transactions_by_accepting_daa_score( + &self, + accepting_daa_score: u64, + tx_ids: Option>, + tx_type: TransactionType, + ) -> ConsensusResult { + unimplemented!() + } + + fn get_transactions_by_block_acceptance_data( + &self, + accepting_block: Hash, + block_acceptance_data: MergesetBlockAcceptanceData, + tx_ids: Option>, + tx_type: TransactionType, + ) -> ConsensusResult { + unimplemented!() + } + + fn get_transactions_by_accepting_block( + &self, + accepting_block: Hash, + tx_ids: Option>, + tx_type: TransactionType, + ) -> ConsensusResult { unimplemented!() } @@ -284,6 +311,10 @@ pub trait ConsensusApi: Send + Sync { unimplemented!() } + fn get_block_transactions(&self, hash: Hash, indices: Option>) -> ConsensusResult> { + unimplemented!() + } + fn get_block_body(&self, hash: Hash) -> ConsensusResult>> { unimplemented!() } diff --git a/consensus/core/src/errors/consensus.rs b/consensus/core/src/errors/consensus.rs index 58c5ed35e9..1a2c8a3c76 100644 --- a/consensus/core/src/errors/consensus.rs +++ b/consensus/core/src/errors/consensus.rs @@ -1,6 +1,8 @@ use kaspa_hashes::Hash; use thiserror::Error; +use crate::{tx::TransactionIndexType, utxo::utxo_inquirer::UtxoInquirerError}; + use super::{difficulty::DifficultyError, sync::SyncManagerError, traversal::TraversalError}; #[derive(Error, Debug, Clone)] @@ -11,6 +13,12 @@ pub enum ConsensusError { #[error("cannot find header {0}")] HeaderNotFound(Hash), + #[error("trying to query {0} txs in block {1}, but the block only holds {2} txs")] + TransactionQueryTooLarge(usize, Hash, usize), + + #[error("index {0} out of max {1} in block {2} is out of bounds")] + TransactionIndexOutOfBounds(TransactionIndexType, usize, Hash), + #[error("block {0} is invalid")] InvalidBlock(Hash), @@ -35,6 +43,9 @@ pub enum ConsensusError { #[error("{0}")] General(&'static str), + #[error("utxo inquirer error: {0}")] + UtxoInquirerError(#[from] UtxoInquirerError), + #[error("{0}")] GeneralOwned(String), } diff --git a/consensus/core/src/tx.rs b/consensus/core/src/tx.rs index 969528b01c..64f8286fe9 100644 --- a/consensus/core/src/tx.rs +++ b/consensus/core/src/tx.rs @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering::SeqCst; +use std::sync::Arc; use std::{ fmt::Display, ops::Range, @@ -538,6 +539,18 @@ impl MutableTransaction { /// and can also be modified internally and signed etc. pub type SignableTransaction = MutableTransaction; +#[derive(Debug, Clone)] +pub enum TransactionType { + Transaction, + SignableTransaction, +} + +#[derive(Debug, Clone)] +pub enum TransactionQueryResult { + Transaction(Arc>), + SignableTransaction(Arc>), +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/core/src/utxo/utxo_inquirer.rs b/consensus/core/src/utxo/utxo_inquirer.rs index 3aa1000295..8442306c95 100644 --- a/consensus/core/src/utxo/utxo_inquirer.rs +++ b/consensus/core/src/utxo/utxo_inquirer.rs @@ -1,6 +1,8 @@ use kaspa_hashes::Hash; use thiserror::Error; +use crate::tx::{TransactionId, TransactionOutpoint}; + #[derive(Error, Debug, Clone)] pub enum UtxoInquirerError { #[error("Transaction is already pruned")] @@ -11,6 +13,8 @@ pub enum UtxoInquirerError { NoTxAtScore, #[error("Transaction was found but not standard")] NonStandard, + #[error("no transaction specified")] + TransactionNotFound, #[error("Did not find compact header for block hash {0} ")] MissingCompactHeaderForBlockHash(Hash), #[error("Did not find containing_acceptance for tx {0} ")] @@ -19,8 +23,6 @@ pub enum UtxoInquirerError { MissingBlockFromBlockTxStore(Hash), #[error("Did not find index {0} in transactions of block {1}")] MissingTransactionIndexOfBlock(usize, Hash), - #[error("Expected {0} to match {1} when checking block_transaction_store using array index of transaction")] - UnexpectedTransactionMismatch(Hash, Hash), #[error("Did not find a utxo diff for chain block {0} ")] MissingUtxoDiffForChainBlock(Hash), #[error("Transaction {0} acceptance data must also be in the same block in this case")] @@ -33,6 +35,16 @@ pub enum UtxoInquirerError { MissingHashAtIndex(u64), #[error("Did not find acceptance data for chain block {0}")] MissingAcceptanceDataForChainBlock(Hash), + #[error("Did not find accepting block for {0}")] + MissingAcceptingBlock(Hash), + #[error("Did not find utxo entry for outpoint {0}")] + MissingUtxoEntryForOutpoint(TransactionOutpoint), + #[error("Did not find queried transactions in acceptance data: {0:?}")] + MissingQueriedTransactions(Vec), + #[error("unexpected store error")] + StoreError, #[error("Utxo entry is not filled")] UnfilledUtxoEntry, } + +pub type UtxoInquirerResult = std::result::Result; diff --git a/consensus/src/consensus/mod.rs b/consensus/src/consensus/mod.rs index 33b18cd692..94ac56f0d5 100644 --- a/consensus/src/consensus/mod.rs +++ b/consensus/src/consensus/mod.rs @@ -44,7 +44,7 @@ use crate::{ }, }; use kaspa_consensus_core::{ - acceptance_data::AcceptanceData, + acceptance_data::{AcceptanceData, MergesetBlockAcceptanceData}, api::{ args::{TransactionValidationArgs, TransactionValidationBatchArgs}, stats::BlockCount, @@ -70,8 +70,10 @@ use kaspa_consensus_core::{ network::NetworkType, pruning::{PruningPointProof, PruningPointTrustedData, PruningPointsList, PruningProofMetadata}, trusted::{ExternalGhostdagData, TrustedBlock}, - tx::{MutableTransaction, SignableTransaction, Transaction, TransactionOutpoint, UtxoEntry}, - utxo::utxo_inquirer::UtxoInquirerError, + tx::{ + MutableTransaction, Transaction, TransactionId, TransactionIndexType, TransactionOutpoint, TransactionQueryResult, + TransactionType, UtxoEntry, + }, BlockHashSet, BlueWorkType, ChainPath, HashMapCustomHasher, }; use kaspa_consensus_notify::root::ConsensusNotificationRoot; @@ -87,12 +89,14 @@ use kaspa_database::prelude::StoreResultExtensions; use kaspa_hashes::Hash; use kaspa_muhash::MuHash; use kaspa_txscript::caches::TxScriptCacheCounters; +use kaspa_utils::arc::ArcExtensions; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rocksdb::WriteBatch; use std::{ - cmp::{self, Reverse}, - collections::{BinaryHeap, VecDeque}, + cmp, + cmp::Reverse, + collections::{BinaryHeap, HashSet, VecDeque}, future::Future, iter::once, ops::Deref, @@ -741,7 +745,7 @@ impl ConsensusApi for Consensus { /// Estimates the number of blocks and headers stored in the node database. /// /// This is an estimation based on the DAA score difference between the node's `retention root` and `virtual`'s DAA score, - /// as such, it does not include non-daa blocks, and does not include headers stored as part of the pruning proof. + /// as such, it does not include non-daa blocks, and does not include headers stored as part of the pruning proof. fn estimate_block_count(&self) -> BlockCount { // PRUNE SAFETY: retention root is always a current or past pruning point which its header is kept permanently let retention_period_root_score = self.headers_store.get_daa_score(self.get_retention_period_root()).unwrap(); @@ -845,11 +849,133 @@ impl ConsensusApi for Consensus { sample_headers } + fn get_transactions_by_accepting_daa_score( + &self, + accepting_daa_score: u64, + tx_ids: Option>, + tx_type: TransactionType, + ) -> ConsensusResult { + // We need consistency between the acceptance store and the block transaction store, + let _guard = self.pruning_lock.blocking_read(); + let accepting_block = self + .virtual_processor + .find_accepting_chain_block_hash_at_daa_score(accepting_daa_score, self.get_retention_period_root())?; + self.get_transactions_by_accepting_block(accepting_block, tx_ids, tx_type) + } - fn get_populated_transaction(&self, txid: Hash, accepting_block_daa_score: u64) -> Result { - // We need consistency between the pruning_point_store, utxo_diffs_store, block_transactions_store, selected chain and headers store reads + fn get_transactions_by_block_acceptance_data( + &self, + accepting_block: Hash, + block_acceptance_data: MergesetBlockAcceptanceData, + tx_ids: Option>, + tx_type: TransactionType, + ) -> ConsensusResult { + // Need consistency between the acceptance store and the block transaction store. let _guard = self.pruning_lock.blocking_read(); - self.virtual_processor.get_populated_transaction(txid, accepting_block_daa_score, self.get_retention_period_root()) + + match tx_type { + TransactionType::Transaction => { + if let Some(tx_ids) = tx_ids { + let mut tx_ids_filter = HashSet::with_capacity(tx_ids.len()); + tx_ids_filter.extend(tx_ids); + + Ok(TransactionQueryResult::Transaction(Arc::new( + self.get_block_transactions( + block_acceptance_data.block_hash, + Some( + block_acceptance_data + .accepted_transactions + .into_iter() + .filter_map(|atx| { + if tx_ids_filter.contains(&atx.transaction_id) { + Some(atx.index_within_block) + } else { + None + } + }) + .collect(), + ), + )?, + ))) + } else { + Ok(TransactionQueryResult::Transaction(Arc::new(self.get_block_transactions( + block_acceptance_data.block_hash, + Some(block_acceptance_data.accepted_transactions.iter().map(|atx| atx.index_within_block).collect()), + )?))) + } + } + TransactionType::SignableTransaction => Ok(TransactionQueryResult::SignableTransaction(Arc::new( + self.virtual_processor.get_populated_transactions_by_block_acceptance_data( + tx_ids, + block_acceptance_data, + accepting_block, + )?, + ))), + } + } + + fn get_transactions_by_accepting_block( + &self, + accepting_block: Hash, + tx_ids: Option>, + tx_type: TransactionType, + ) -> ConsensusResult { + // We need consistency between the acceptance store and the block transaction store, + let _guard = self.pruning_lock.blocking_read(); + + match tx_type { + TransactionType::Transaction => { + let accepting_block_mergeset_acceptance_data_iter = self + .acceptance_data_store + .get(accepting_block) + .map_err(|_| ConsensusError::MissingData(accepting_block))? + .unwrap_or_clone() + .into_iter(); + + if let Some(tx_ids) = tx_ids { + let mut tx_ids_filter = HashSet::with_capacity(tx_ids.len()); + tx_ids_filter.extend(tx_ids); + + Ok(TransactionQueryResult::Transaction(Arc::new( + accepting_block_mergeset_acceptance_data_iter + .flat_map(|mbad| { + self.get_block_transactions( + mbad.block_hash, + Some( + mbad.accepted_transactions + .into_iter() + .filter_map(|atx| { + if tx_ids_filter.contains(&atx.transaction_id) { + Some(atx.index_within_block) + } else { + None + } + }) + .collect(), + ), + ) + }) + .flatten() + .collect::>(), + ))) + } else { + Ok(TransactionQueryResult::Transaction(Arc::new( + accepting_block_mergeset_acceptance_data_iter + .flat_map(|mbad| { + self.get_block_transactions( + mbad.block_hash, + Some(mbad.accepted_transactions.iter().map(|atx| atx.index_within_block).collect()), + ) + }) + .flatten() + .collect::>(), + ))) + } + } + TransactionType::SignableTransaction => Ok(TransactionQueryResult::SignableTransaction(Arc::new( + self.virtual_processor.get_populated_transactions_by_accepting_block(tx_ids, accepting_block)?, + ))), + } } fn get_virtual_parents(&self) -> BlockHashSet { @@ -1049,6 +1175,33 @@ impl ConsensusApi for Consensus { }) } + fn get_block_transactions(&self, hash: Hash, indices: Option>) -> ConsensusResult> { + let transactions = self.block_transactions_store.get(hash).unwrap_option().ok_or(ConsensusError::BlockNotFound(hash))?; + let tx_len = transactions.len(); + + if let Some(indices) = indices { + if tx_len < indices.len() { + return Err(ConsensusError::TransactionQueryTooLarge(indices.len(), hash, transactions.len())); + } + + let res = transactions + .unwrap_or_clone() + .into_iter() + .enumerate() + .filter(|(index, _tx)| indices.contains(&(*index as TransactionIndexType))) + .map(|(_, tx)| tx) + .collect::>(); + + if res.len() != indices.len() { + Err(ConsensusError::TransactionIndexOutOfBounds(*indices.iter().max().unwrap(), tx_len, hash)) + } else { + Ok(res) + } + } else { + Ok(transactions.unwrap_or_clone()) + } + } + fn get_block_body(&self, hash: Hash) -> ConsensusResult>> { if match self.statuses_store.read().get(hash).unwrap_option() { Some(status) => !status.has_block_body(), diff --git a/consensus/src/pipeline/virtual_processor/utxo_inquirer.rs b/consensus/src/pipeline/virtual_processor/utxo_inquirer.rs index 6ab6a463cb..3e53d8dd95 100644 --- a/consensus/src/pipeline/virtual_processor/utxo_inquirer.rs +++ b/consensus/src/pipeline/virtual_processor/utxo_inquirer.rs @@ -1,80 +1,268 @@ -use std::{cmp, sync::Arc}; +use std::{cmp, collections::HashSet, sync::Arc}; use kaspa_consensus_core::{ - acceptance_data::AcceptanceData, - tx::{SignableTransaction, Transaction, UtxoEntry}, - utxo::{utxo_diff::ImmutableUtxoDiff, utxo_inquirer::UtxoInquirerError}, + acceptance_data::{AcceptanceData, MergesetBlockAcceptanceData}, + tx::{SignableTransaction, Transaction, TransactionId, TransactionIndexType, TransactionOutpoint, UtxoEntry}, + utxo::{ + utxo_diff::ImmutableUtxoDiff, + utxo_inquirer::{UtxoInquirerError, UtxoInquirerResult}, + }, }; -use kaspa_core::{trace, warn}; +use kaspa_core::trace; use kaspa_hashes::Hash; -use crate::model::stores::{ - acceptance_data::AcceptanceDataStoreReader, block_transactions::BlockTransactionsStoreReader, headers::HeaderStoreReader, - selected_chain::SelectedChainStoreReader, utxo_diffs::UtxoDiffsStoreReader, +use crate::model::{ + services::reachability::ReachabilityService, + stores::{ + acceptance_data::AcceptanceDataStoreReader, block_transactions::BlockTransactionsStoreReader, headers::HeaderStoreReader, + selected_chain::SelectedChainStoreReader, utxo_diffs::UtxoDiffsStoreReader, utxo_set::UtxoSetStoreReader, + virtual_state::VirtualStateStoreReader, + }, }; use super::VirtualStateProcessor; +pub struct MergesetAcceptanceMetaData { + pub accepting_block_hash: Hash, + pub acceptance_data: Arc, + pub accepting_daa_score: u64, + pub mergeset_idx: usize, +} + impl VirtualStateProcessor { - /// Returns the fully populated transaction with the given txid which was accepted at the provided accepting_block_daa_score. - /// The argument `accepting_block_daa_score` is expected to be the DAA score of the accepting chain block of `txid`. - /// - /// *Assumed to be called under the pruning read lock.* - pub fn get_populated_transaction( + pub fn find_accepting_data( &self, - txid: Hash, - accepting_block_daa_score: u64, + block_hash: Hash, + retention_period_root_hash: Hash, + sink_hash: Hash, + ) -> UtxoInquirerResult> { + // accepting block hash, daa score, acceptance data + // check if block is an ancestor of the sink block, i.e. we expect it to be accepted + if self.reachability_service.is_dag_ancestor_of(block_hash, sink_hash) { + // find the first "possible" accepting chain block + let ancestor = self.find_accepting_chain_block_hash_at_daa_score( + self.headers_store + .get_daa_score(block_hash) + .map_err(|_| UtxoInquirerError::MissingCompactHeaderForBlockHash(block_hash))?, + retention_period_root_hash, + )?; + // iterate forward from the ancestor to the sink block, looking for the accepting block + for candidate in self.reachability_service.forward_chain_iterator(ancestor, sink_hash, true) { + let acceptance_data = self + .acceptance_data_store + .get(candidate) + .map_err(|_| UtxoInquirerError::MissingAcceptanceDataForChainBlock(candidate))?; + for (i, mbad) in acceptance_data.iter().enumerate() { + if mbad.block_hash == block_hash { + return Ok(Some(MergesetAcceptanceMetaData { + accepting_block_hash: candidate, + acceptance_data, + accepting_daa_score: self + .headers_store + .get_daa_score(candidate) + .map_err(|_| UtxoInquirerError::MissingCompactHeaderForBlockHash(candidate))?, + mergeset_idx: i, + })); + } + } + } + } + Ok(None) + } + + pub fn populate_block_transactions( + &self, + block_hash: Hash, + txs: Vec, retention_period_root_hash: Hash, - ) -> Result { - let retention_period_root_daa_score = self + ) -> UtxoInquirerResult> { + let virual_state_read = self.virtual_stores.read(); + let sink_hash = virual_state_read.state.get().expect("expected virtual state").ghostdag_data.selected_parent; + let utxo_store = &virual_state_read.utxo_set; + + let mut signable_transactions = Vec::with_capacity(txs.len()); + + if let Some(mergeset_meta_data) = self.find_accepting_data(block_hash, retention_period_root_hash, sink_hash)? { + // We have a mergeset acceptance, so we most factor in the acceptance data to populate the transactions + let utxo_diff = self + .utxo_diffs_store + .get(mergeset_meta_data.accepting_block_hash) + .map_err(|_| UtxoInquirerError::MissingUtxoDiffForChainBlock(mergeset_meta_data.accepting_block_hash))?; + for tx in txs.into_iter() { + let mut entries = Vec::with_capacity(tx.inputs.len()); + for input in tx.inputs.iter() { + if let Some(utxo) = utxo_diff.removed().get(&input.previous_outpoint) { + // first check: if it was accepted, i.e. removed in the diff + entries.push(utxo.clone()); + } else if let Some(utxo) = utxo_store.get(&input.previous_outpoint).ok().map(|arc| (*arc).clone()) { + // secound check: if it was not accepted, it may be in the utxo set + entries.push(utxo); + } else { + // third check: if it was not accepted and not in the utxo set, it may have been created and spent in a parallel block. + entries.push(self.resolve_missing_outpoint( + &input.previous_outpoint, + &mergeset_meta_data.acceptance_data, + mergeset_meta_data.accepting_daa_score, + )?); + } + } + signable_transactions.push(SignableTransaction::with_entries(tx, entries)); + } + } else { + // We don't have a mergeset acceptance, so we use the utxo set solely to populate the transactions. + // we do not expect to find the outpoints anywhere else. + for tx in txs.into_iter() { + let mut entries = Vec::with_capacity(tx.inputs.len()); + for input in tx.inputs.iter() { + match utxo_store.get(&input.previous_outpoint) { + Ok(utxo) => entries.push((*utxo).clone()), + Err(_) => return Err(UtxoInquirerError::MissingUtxoEntryForOutpoint(input.previous_outpoint)), + } + } + signable_transactions.push(SignableTransaction::with_entries(tx, entries)); + } + } + + Ok(signable_transactions) + } + + fn resolve_missing_outpoint( + &self, + outpoint: &TransactionOutpoint, + acceptance_data: &AcceptanceData, + accepting_block_daa_score: u64, + ) -> UtxoInquirerResult { + // This handles this rare scenario: + // - UTXO0 is spent by TX1 and creates UTXO1 + // - UTXO1 is spent by TX2 and creates UTXO2 + // - A chain block happens to accept both of these + // In this case, removed_diff wouldn't contain the outpoint of the created-and-immediately-spent UTXO + // so we use the transaction (which also has acceptance data in this block) and look at its outputs + let other_tx = &self.find_txs_from_acceptance_data(Some(vec![outpoint.transaction_id]), acceptance_data)?[0]; + let output = &other_tx.outputs[outpoint.index as usize]; + let utxo_entry = + UtxoEntry::new(output.value, output.script_public_key.clone(), accepting_block_daa_score, other_tx.is_coinbase()); + Ok(utxo_entry) + } + + pub fn get_populated_transactions_by_block_acceptance_data( + &self, + tx_ids: Option>, + block_acceptance_data: MergesetBlockAcceptanceData, + accepting_block: Hash, + ) -> UtxoInquirerResult> { + let accepting_daa_score = self .headers_store - .get_daa_score(retention_period_root_hash) - .map_err(|_| UtxoInquirerError::MissingCompactHeaderForBlockHash(retention_period_root_hash))?; + .get_daa_score(accepting_block) + .map_err(|_| UtxoInquirerError::MissingCompactHeaderForBlockHash(accepting_block))?; + + let utxo_diff = self + .utxo_diffs_store + .get(accepting_block) + .map_err(|_| UtxoInquirerError::MissingUtxoDiffForChainBlock(accepting_block))?; + + let acceptance_data_for_this_block = vec![block_acceptance_data]; + + let txs = self.find_txs_from_acceptance_data(tx_ids, &acceptance_data_for_this_block)?; - if accepting_block_daa_score < retention_period_root_daa_score { - // Early exit if target daa score is lower than that of pruning point's daa score: - return Err(UtxoInquirerError::AlreadyPruned); + let mut populated_txs = Vec::::with_capacity(txs.len()); + + for tx in txs.iter() { + let mut entries = Vec::with_capacity(tx.inputs.len()); + for input in tx.inputs.iter() { + let filled_utxo = if let Some(utxo) = utxo_diff.removed().get(&input.previous_outpoint).cloned() { + Some(utxo) + } else if let Some(utxo) = populated_txs.iter().map(|ptx| &ptx.tx).chain(txs.iter()).find_map(|tx| { + if tx.id() == input.previous_outpoint.transaction_id { + let output = &tx.outputs[input.previous_outpoint.index as usize]; + Some(UtxoEntry::new(output.value, output.script_public_key.clone(), accepting_daa_score, tx.is_coinbase())) + } else { + None + } + }) { + Some(utxo) + } else { + Some(self.resolve_missing_outpoint( + &input.previous_outpoint, + &acceptance_data_for_this_block, + accepting_daa_score, + )?) + }; + + entries.push(filled_utxo.ok_or(UtxoInquirerError::MissingUtxoEntryForOutpoint(input.previous_outpoint))?); + } + populated_txs.push(SignableTransaction::with_entries(tx.clone(), entries)); } - let (matching_chain_block_hash, acceptance_data) = - self.find_accepting_chain_block_hash_at_daa_score(accepting_block_daa_score, retention_period_root_hash)?; + Ok(populated_txs) + } + pub fn get_populated_transactions_by_accepting_block( + &self, + tx_ids: Option>, + accepting_block: Hash, + ) -> UtxoInquirerResult> { + let acceptance_data = self + .acceptance_data_store + .get(accepting_block) + .map_err(|_| UtxoInquirerError::MissingAcceptanceDataForChainBlock(accepting_block))?; + + let accepting_daa_score = self + .headers_store + .get_daa_score(accepting_block) + .map_err(|_| UtxoInquirerError::MissingCompactHeaderForBlockHash(accepting_block))?; // Expected to never fail, since we found the acceptance data and therefore there must be matching diff let utxo_diff = self .utxo_diffs_store - .get(matching_chain_block_hash) - .map_err(|_| UtxoInquirerError::MissingUtxoDiffForChainBlock(matching_chain_block_hash))?; - - let tx = self.find_tx_from_acceptance_data(txid, &acceptance_data)?; - - let mut populated_tx = SignableTransaction::new(tx); - - let removed_diffs = utxo_diff.removed(); - - populated_tx.tx.inputs.iter().enumerate().for_each(|(index, input)| { - let filled_utxo = if let Some(utxo_entry) = removed_diffs.get(&input.previous_outpoint) { - Some(utxo_entry.clone().to_owned()) - } else { - // This handles this rare scenario: - // - UTXO0 is spent by TX1 and creates UTXO1 - // - UTXO1 is spent by TX2 and creates UTXO2 - // - A chain block happens to accept both of these - // In this case, removed_diff wouldn't contain the outpoint of the created-and-immediately-spent UTXO - // so we use the transaction (which also has acceptance data in this block) and look at its outputs - let other_txid = input.previous_outpoint.transaction_id; - let other_tx = self.find_tx_from_acceptance_data(other_txid, &acceptance_data).unwrap(); - let output = &other_tx.outputs[input.previous_outpoint.index as usize]; - let utxo_entry = - UtxoEntry::new(output.value, output.script_public_key.clone(), accepting_block_daa_score, other_tx.is_coinbase()); - Some(utxo_entry) - }; - - populated_tx.entries[index] = filled_utxo; - }); - - Ok(populated_tx) + .get(accepting_block) + .map_err(|_| UtxoInquirerError::MissingUtxoDiffForChainBlock(accepting_block))?; + + let txs = self.find_txs_from_acceptance_data(tx_ids, &acceptance_data)?; + + let mut populated_txs = Vec::::with_capacity(txs.len()); + + for tx in txs.iter() { + let mut entries = Vec::with_capacity(tx.inputs.len()); + for input in tx.inputs.iter() { + let filled_utxo = if let Some(utxo) = utxo_diff.removed().get(&input.previous_outpoint).cloned() { + Some(utxo) + } else if let Some(utxo) = populated_txs.iter().map(|ptx| &ptx.tx).chain(txs.iter()).find_map(|tx| { + if tx.id() == input.previous_outpoint.transaction_id { + let output = &tx.outputs[input.previous_outpoint.index as usize]; + Some(UtxoEntry::new(output.value, output.script_public_key.clone(), accepting_daa_score, tx.is_coinbase())) + } else { + None + } + }) { + Some(utxo) + } else { + Some(self.resolve_missing_outpoint(&input.previous_outpoint, &acceptance_data, accepting_daa_score)?) + }; + + entries.push(filled_utxo.ok_or(UtxoInquirerError::MissingUtxoEntryForOutpoint(input.previous_outpoint))?); + } + populated_txs.push(SignableTransaction::with_entries(tx.clone(), entries)); + } + + Ok(populated_txs) } + /// Returns the fully populated transactions with the given tx ids which were accepted at the provided accepting_block_daa_score. + /// The argument `accepting_block_daa_score` is expected to be the DAA score of the accepting chain block of `tx ids`. + /// + /// *Assumed to be called under the pruning read lock.* + /// + pub fn get_populated_transactions_by_accepting_daa_score( + &self, + tx_ids: Option>, + accepting_block_daa_score: u64, + retention_period_root_hash: Hash, + ) -> UtxoInquirerResult> { + let matching_chain_block_hash = + self.find_accepting_chain_block_hash_at_daa_score(accepting_block_daa_score, retention_period_root_hash)?; + + self.get_populated_transactions_by_accepting_block(tx_ids, matching_chain_block_hash) + } /// Find the accepting chain block hash at the given DAA score by binary searching /// through selected chain store using indexes. /// This method assumes that local caller have acquired the pruning read lock to guarantee @@ -82,11 +270,11 @@ impl VirtualStateProcessor { /// other stores outside). If no such lock is acquired, this method tries to find /// the accepting chain block hash on a best effort basis (may fail if parts of the data /// are pruned between two sequential calls) - fn find_accepting_chain_block_hash_at_daa_score( + pub fn find_accepting_chain_block_hash_at_daa_score( &self, target_daa_score: u64, retention_period_root_hash: Hash, - ) -> Result<(Hash, Arc), UtxoInquirerError> { + ) -> UtxoInquirerResult { let sc_read = self.selected_chain_store.read(); let retention_period_root_index = sc_read @@ -138,50 +326,158 @@ impl VirtualStateProcessor { } }; - let acceptance_data = self - .acceptance_data_store - .get(matching_chain_block_hash) - .map_err(|_| UtxoInquirerError::MissingAcceptanceDataForChainBlock(matching_chain_block_hash))?; - - Ok((matching_chain_block_hash, acceptance_data)) + Ok(matching_chain_block_hash) } /// Finds a transaction's containing block hash and index within block through /// the accepting block acceptance data - fn find_containing_block_and_index_from_acceptance_data( + fn find_containing_blocks_and_indices_from_acceptance_data( &self, - txid: Hash, + tx_ids: &[TransactionId], acceptance_data: &AcceptanceData, - ) -> Option<(Hash, usize)> { - acceptance_data.iter().find_map(|mbad| { - let tx_arr_index = - mbad.accepted_transactions.iter().find_map(|tx| (tx.transaction_id == txid).then_some(tx.index_within_block as usize)); - tx_arr_index.map(|index| (mbad.block_hash, index)) - }) + ) -> Vec<(Hash, Vec)> { + let tx_set = tx_ids.iter().collect::>(); + let mut collected = 0usize; + + let mut result = Vec::with_capacity(acceptance_data.len()); + + 'outer: for mbad in acceptance_data.iter() { + for atx in mbad.accepted_transactions.iter() { + let mut indices = Vec::new(); + if tx_set.contains(&atx.transaction_id) { + indices.push(atx.index_within_block); + collected += 1; + if collected == tx_ids.len() { + result.push((mbad.block_hash, indices)); + break 'outer; + } + } + if !indices.is_empty() { + result.push((mbad.block_hash, indices)); + } + } + } + + result } /// Finds a transaction through the accepting block acceptance data (and using indexed info therein for /// finding the tx in the block transactions store) - fn find_tx_from_acceptance_data(&self, txid: Hash, acceptance_data: &AcceptanceData) -> Result { - let (containing_block, index) = self - .find_containing_block_and_index_from_acceptance_data(txid, acceptance_data) - .ok_or(UtxoInquirerError::MissingContainingAcceptanceForTx(txid))?; - - let tx = self - .block_transactions_store - .get(containing_block) - .map_err(|_| UtxoInquirerError::MissingBlockFromBlockTxStore(containing_block)) - .and_then(|block_txs| { - block_txs.get(index).cloned().ok_or(UtxoInquirerError::MissingTransactionIndexOfBlock(index, containing_block)) - })?; + fn find_txs_from_acceptance_data( + &self, + tx_ids: Option>, // specifying `None` returns all transactions in the acceptance data + acceptance_data: &AcceptanceData, + ) -> UtxoInquirerResult> { + if let Some(mut tx_ids) = tx_ids { + match tx_ids.len() { + // empty vec should never happen + 0 => panic!("tx_ids should not be empty"), + // if we are dealing with a single tx, we optimize for this. + 1 => { + let tx_id = tx_ids.pop().unwrap(); - if tx.id() != txid { - // Should never happen, but do a sanity check. This would mean something went wrong with storing block transactions. - // Sanity check is necessary to guarantee that this function will never give back a wrong address (err on the side of not found) - warn!("Expected {} to match {} when checking block_transaction_store using array index of transaction", tx.id(), txid); - return Err(UtxoInquirerError::UnexpectedTransactionMismatch(tx.id(), txid)); - } + //sanity check + assert!(tx_ids.is_empty()); + + let (containing_block, index) = acceptance_data + .iter() + .find_map(|mbad| { + let tx_arr_index = mbad + .accepted_transactions + .iter() + .find_map(|tx| (tx.transaction_id == tx_id).then_some(tx.index_within_block as usize)); + tx_arr_index.map(|index| (mbad.block_hash, index)) + }) + .ok_or_else(|| UtxoInquirerError::MissingQueriedTransactions(vec![tx_id]))?; + + let tx = self + .block_transactions_store + .get(containing_block) + .map_err(|_| UtxoInquirerError::MissingBlockFromBlockTxStore(containing_block)) + .and_then(|block_txs| { + block_txs + .get(index) + .cloned() + .ok_or(UtxoInquirerError::MissingTransactionIndexOfBlock(index, containing_block)) + })?; + + Ok(vec![tx]) + } + // else we work, and optimize with sets, and iterate by block hash, as to minimize block transaction store queries. + _ => { + panic!("we should not be here (yet)") + /* - Ok(tx) + let mut txs = HashMap::::new(); + for (containing_block, indices) in + self.find_containing_blocks_and_indices_from_acceptance_data(&tx_ids, acceptance_data) + { + let mut indice_iter = indices.iter(); + let mut target_index = (*indice_iter.next().unwrap()) as usize; + let cut_off_index = (*indices.last().unwrap()) as usize; + + txs.extend( + self.block_transactions_store + .get(containing_block) + .map_err(|_| UtxoInquirerError::MissingBlockFromBlockTxStore(containing_block))? + .unwrap_or_clone() + .into_iter() + .enumerate() + .take_while(|(i, _)| *i <= cut_off_index) + .filter_map(|(i, tx)| { + if i == target_index { + target_index = (*indice_iter.next().unwrap()) as usize; + Some((tx.id(), tx)) + } else { + None + } + }), + ); + } + + /* + if txs.len() < tx_ids.len() { + // The query includes txs which are not in the acceptance data, we constitute this as an error. + return Err(UtxoInquirerError::MissingQueriedTransactions( + tx_ids.iter().filter(|tx_id| !txs.contains_key(*tx_id)).copied().collect::>(), + )); + }; + */ + + return Ok(tx_ids.iter().map(|tx_id| txs.remove(tx_id).expect("expected queried tx id")).collect::>()) + */ + } + } + } else { + // if tx_ids is None, we return all transactions in the acceptance data + let mut all_txs = Vec::new(); + + for mbad in acceptance_data.iter() { + let mut index_iter = mbad.accepted_transactions.iter().map(|tx| tx.index_within_block as usize); + + if let Some(mut next_target_index) = index_iter.next() { + all_txs.extend( + self.block_transactions_store + .get(mbad.block_hash) + .map_err(|_| UtxoInquirerError::MissingBlockFromBlockTxStore(mbad.block_hash))? + .iter() + .enumerate() + .filter_map(|(i, tx)| { + if i == next_target_index { + next_target_index = index_iter.next().unwrap_or(usize::MAX); + Some(tx.clone()) + } else { + None + } + }) + .take(mbad.accepted_transactions.len()), + ) + } else { + continue; + }; + } + + Ok(all_txs) + } } } diff --git a/crypto/txscript/src/script_class.rs b/crypto/txscript/src/script_class.rs index ad61f30d89..85d96cab98 100644 --- a/crypto/txscript/src/script_class.rs +++ b/crypto/txscript/src/script_class.rs @@ -16,7 +16,7 @@ pub enum Error { } /// Standard classes of script payment in the blockDAG -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[borsh(use_discriminant = true)] #[repr(u8)] pub enum ScriptClass { diff --git a/rothschild/src/main.rs b/rothschild/src/main.rs index 2834b3fe27..9c68a71d03 100644 --- a/rothschild/src/main.rs +++ b/rothschild/src/main.rs @@ -236,7 +236,7 @@ async fn main() { None | Some(_) => TESTNET_PARAMS.coinbase_maturity().after(), }; info!( - "Node block-DAG info: \n\tNetwork: {}, \n\tBlock count: {}, \n\tHeader count: {}, \n\tDifficulty: {}, + "Node block-DAG info: \n\tNetwork: {}, \n\tBlock count: {}, \n\tHeader count: {}, \n\tDifficulty: {}, \tMedian time: {}, \n\tDAA score: {}, \n\tPruning point: {}, \n\tTips: {}, \n\t{} virtual parents: ...{}, \n\tCoinbase maturity: {}", info.network, info.block_count, diff --git a/rpc/core/Cargo.toml b/rpc/core/Cargo.toml index f2e9f72f9e..190fb09476 100644 --- a/rpc/core/Cargo.toml +++ b/rpc/core/Cargo.toml @@ -45,6 +45,7 @@ paste.workspace = true rand.workspace = true serde-wasm-bindgen.workspace = true serde.workspace = true +serde_nested_with.workspace = true smallvec.workspace = true thiserror.workspace = true uuid.workspace = true diff --git a/rpc/core/src/api/ops.rs b/rpc/core/src/api/ops.rs index 4541ddc56d..1ad1344185 100644 --- a/rpc/core/src/api/ops.rs +++ b/rpc/core/src/api/ops.rs @@ -138,6 +138,8 @@ pub enum RpcApiOps { GetCurrentBlockColor = 149, /// Get UTXO Return Addresses GetUtxoReturnAddress = 150, + /// Get Virtual Chain from Block V2 + GetVirtualChainFromBlockV2 = 151, } impl RpcApiOps { diff --git a/rpc/core/src/api/rpc.rs b/rpc/core/src/api/rpc.rs index f40f90e84d..01ff570cd4 100644 --- a/rpc/core/src/api/rpc.rs +++ b/rpc/core/src/api/rpc.rs @@ -482,6 +482,24 @@ pub trait RpcApi: Sync + Send + AnySync { request: GetCurrentBlockColorRequest, ) -> RpcResult; + async fn get_virtual_chain_from_block_v2( + &self, + start_hash: RpcHash, + data_verbosity_level: Option, + min_confirmation_count: Option, + ) -> RpcResult { + self.get_virtual_chain_from_block_v2_call( + None, + GetVirtualChainFromBlockV2Request::new(start_hash, data_verbosity_level, min_confirmation_count), + ) + .await + } + async fn get_virtual_chain_from_block_v2_call( + &self, + connection: Option<&DynRpcConnection>, + request: GetVirtualChainFromBlockV2Request, + ) -> RpcResult; + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/core/src/convert/block.rs b/rpc/core/src/convert/block.rs index bfdf6802e8..4ac84038b9 100644 --- a/rpc/core/src/convert/block.rs +++ b/rpc/core/src/convert/block.rs @@ -2,7 +2,7 @@ use std::sync::Arc; -use crate::{RpcBlock, RpcError, RpcRawBlock, RpcResult, RpcTransaction}; +use crate::{RpcBlock, RpcError, RpcOptionalBlock, RpcOptionalTransaction, RpcRawBlock, RpcResult, RpcTransaction}; use kaspa_consensus_core::block::{Block, MutableBlock}; // ---------------------------------------------------------------------------- @@ -81,3 +81,49 @@ impl TryFrom for Block { }) } } + +// ---------------------------------------------------------------------------- +// consensus_core to optional rpc_core +// ---------------------------------------------------------------------------- + +impl From<&Block> for RpcOptionalBlock { + fn from(item: &Block) -> Self { + Self { + header: Some(item.header.as_ref().into()), + transactions: item.transactions.iter().map(RpcOptionalTransaction::from).collect(), + // TODO: Implement a populating process inspired from kaspad\app\rpc\rpccontext\verbosedata.go + verbose_data: None, + } + } +} + +impl From<&MutableBlock> for RpcOptionalBlock { + fn from(item: &MutableBlock) -> Self { + Self { + header: Some(item.header.as_ref().into()), + transactions: item.transactions.iter().map(RpcOptionalTransaction::from).collect(), + verbose_data: None, + } + } +} + +// ---------------------------------------------------------------------------- +// optional rpc_core to consensus_core +// ---------------------------------------------------------------------------- + +impl TryFrom for Block { + type Error = RpcError; + fn try_from(item: RpcOptionalBlock) -> RpcResult { + Ok(Self { + header: Arc::new( + (item.header.ok_or(RpcError::MissingRpcFieldError("RpcBlock".to_string(), "header".to_string()))?).try_into()?, + ), + transactions: Arc::new( + item.transactions + .into_iter() + .map(kaspa_consensus_core::tx::Transaction::try_from) + .collect::>>()?, + ), + }) + } +} diff --git a/rpc/core/src/convert/mod.rs b/rpc/core/src/convert/mod.rs index bc5c0e64b9..63c2595164 100644 --- a/rpc/core/src/convert/mod.rs +++ b/rpc/core/src/convert/mod.rs @@ -7,3 +7,4 @@ pub mod notification; pub mod scope; pub mod tx; pub mod utxo; +pub mod verbosity; diff --git a/rpc/core/src/convert/tx.rs b/rpc/core/src/convert/tx.rs index 5485f99df7..63a749bf42 100644 --- a/rpc/core/src/convert/tx.rs +++ b/rpc/core/src/convert/tx.rs @@ -1,6 +1,9 @@ //! Conversion of Transaction related types -use crate::{RpcError, RpcResult, RpcTransaction, RpcTransactionInput, RpcTransactionOutput}; +use crate::{ + RpcError, RpcOptionalTransaction, RpcOptionalTransactionInput, RpcOptionalTransactionOutput, RpcResult, RpcTransaction, + RpcTransactionInput, RpcTransactionOutput, +}; use kaspa_consensus_core::tx::{Transaction, TransactionInput, TransactionOutput}; // ---------------------------------------------------------------------------- @@ -81,3 +84,94 @@ impl TryFrom for TransactionInput { Ok(Self::new(item.previous_outpoint.into(), item.signature_script, item.sequence, item.sig_op_count)) } } + +// ---------------------------------------------------------------------------- +// consensus_core to optional rpc_core +// ---------------------------------------------------------------------------- + +impl From<&Transaction> for RpcOptionalTransaction { + fn from(item: &Transaction) -> Self { + Self { + version: Some(item.version), + inputs: item.inputs.iter().map(RpcOptionalTransactionInput::from).collect(), + outputs: item.outputs.iter().map(RpcOptionalTransactionOutput::from).collect(), + lock_time: Some(item.lock_time), + subnetwork_id: Some(item.subnetwork_id.clone()), + gas: Some(item.gas), + payload: Some(item.payload.clone()), + mass: Some(item.mass()), + verbose_data: None, + } + } +} + +impl From<&TransactionOutput> for RpcOptionalTransactionOutput { + fn from(item: &TransactionOutput) -> Self { + Self { value: Some(item.value), script_public_key: Some(item.script_public_key.clone()), verbose_data: None } + } +} + +impl From<&TransactionInput> for RpcOptionalTransactionInput { + fn from(item: &TransactionInput) -> Self { + Self { + previous_outpoint: Some(item.previous_outpoint.into()), + signature_script: Some(item.signature_script.clone()), + sequence: Some(item.sequence), + sig_op_count: Some(item.sig_op_count), + verbose_data: None, + } + } +} + +// ---------------------------------------------------------------------------- +// optional rpc_core to consensus_core +// ---------------------------------------------------------------------------- + +impl TryFrom for Transaction { + type Error = RpcError; + fn try_from(item: RpcOptionalTransaction) -> RpcResult { + let transaction = Transaction::new( + item.version.ok_or(RpcError::MissingRpcFieldError("RpcTransaction".to_owned(), "version".to_owned()))?, + item.inputs + .into_iter() + .map(kaspa_consensus_core::tx::TransactionInput::try_from) + .collect::>>()?, + item.outputs + .into_iter() + .map(kaspa_consensus_core::tx::TransactionOutput::try_from) + .collect::>>()?, + item.lock_time.ok_or(RpcError::MissingRpcFieldError("RpcTransaction".to_owned(), "lock_time".to_owned()))?, + item.subnetwork_id.ok_or(RpcError::MissingRpcFieldError("RpcTransaction".to_owned(), "subnetwork_id".to_owned()))?, + item.gas.ok_or(RpcError::MissingRpcFieldError("RpcTransaction".to_owned(), "gas".to_owned()))?, + item.payload.ok_or(RpcError::MissingRpcFieldError("RpcTransaction".to_owned(), "payload".to_owned()))?, + ); + transaction.set_mass(item.mass.ok_or(RpcError::MissingRpcFieldError("RpcTransaction".to_owned(), "mass".to_owned()))?); + Ok(transaction) + } +} + +impl TryFrom for TransactionOutput { + type Error = RpcError; + fn try_from(item: RpcOptionalTransactionOutput) -> RpcResult { + Ok(Self::new( + item.value.ok_or(RpcError::MissingRpcFieldError("RpcTransactionOutput".to_owned(), "value".to_owned()))?, + item.script_public_key + .ok_or(RpcError::MissingRpcFieldError("RpcTransactionOutput".to_owned(), "script_public_key".to_owned()))?, + )) + } +} + +impl TryFrom for TransactionInput { + type Error = RpcError; + fn try_from(item: RpcOptionalTransactionInput) -> RpcResult { + Ok(Self::new( + item.previous_outpoint + .ok_or(RpcError::MissingRpcFieldError("RpcTransactionInput".to_owned(), "previous_outpoint".to_owned()))? + .try_into()?, + item.signature_script + .ok_or(RpcError::MissingRpcFieldError("RpcTransactionInput".to_owned(), "signature_script".to_owned()))?, + item.sequence.ok_or(RpcError::MissingRpcFieldError("RpcTransactionInput".to_owned(), "sequence".to_owned()))?, + item.sig_op_count.ok_or(RpcError::MissingRpcFieldError("RpcTransactionInput".to_owned(), "sig_op_count".to_owned()))?, + )) + } +} diff --git a/rpc/core/src/convert/verbosity.rs b/rpc/core/src/convert/verbosity.rs new file mode 100644 index 0000000000..b63fa901c4 --- /dev/null +++ b/rpc/core/src/convert/verbosity.rs @@ -0,0 +1,192 @@ +use crate::{ + RpcAcceptanceDataVerbosity, RpcDataVerbosityLevel, RpcError, RpcHeaderVerbosity, RpcMergesetBlockAcceptanceDataVerbosity, + RpcTransactionInputVerboseDataVerbosity, RpcTransactionInputVerbosity, RpcTransactionOutputVerboseDataVerbosity, + RpcTransactionOutputVerbosity, RpcTransactionVerboseDataVerbosity, RpcTransactionVerbosity, RpcUtxoEntryVerboseDataVerbosity, + RpcUtxoEntryVerbosity, +}; + +macro_rules! impl_verbosity_from { + ( + for $target:ty, from $level:ty { + $( $field:ident : $handler:tt ),* $(,)? + } + ) => { + impl ::core::convert::From<$level> for $target { + fn from(level: $level) -> Self { + Self { + $( + $field: impl_verbosity_from!(@eval level, $level, $handler), + )* + } + } + } + }; + + // (|level| expr) -> Some(expr_result) + (@eval $lev:ident, $levty:ty, (| $L:ident | $e:expr)) => { + ::core::option::Option::Some((|$L: $levty| { $e })($lev)) + }; + + // (Level) -> Some(bool) + (@eval $lev:ident, $levty:ty, ($min_level:expr)) => { + ::core::option::Option::Some($lev.is_at_least($min_level)) + }; + + // (none) -> None + (@eval $lev:ident, $levty:ty, (none)) => { + ::core::option::Option::None + }; +} + +impl RpcDataVerbosityLevel { + /// Check if this verbosity level is at least the specified level + pub const fn is_at_least(&self, other: Self) -> bool { + *self as u8 >= other as u8 + } +} + +impl From for i32 { + fn from(v: RpcDataVerbosityLevel) -> Self { + v as i32 + } +} + +impl TryFrom for RpcDataVerbosityLevel { + type Error = RpcError; + fn try_from(v: i32) -> Result { + match v { + 0 => Ok(Self::None), + 1 => Ok(Self::Low), + 2 => Ok(Self::High), + 3 => Ok(Self::Full), + _ => Err(RpcError::NotImplemented), + } + } +} + +impl_verbosity_from! { + for RpcHeaderVerbosity, from RpcDataVerbosityLevel { + include_hash: (RpcDataVerbosityLevel::None), + include_version: (RpcDataVerbosityLevel::Low), + include_timestamp: (RpcDataVerbosityLevel::Low), + include_bits: (RpcDataVerbosityLevel::Low), + include_nonce: (RpcDataVerbosityLevel::Low), + include_daa_score: (RpcDataVerbosityLevel::Low), + include_blue_work: (RpcDataVerbosityLevel::Low), + include_blue_score: (RpcDataVerbosityLevel::Low), + include_parents_by_level: (RpcDataVerbosityLevel::High), + include_hash_merkle_root: (RpcDataVerbosityLevel::High), + include_accepted_id_merkle_root: (RpcDataVerbosityLevel::High), + include_utxo_commitment: (RpcDataVerbosityLevel::Full), + include_pruning_point: (RpcDataVerbosityLevel::Full), + } +} + +impl_verbosity_from! { + for RpcUtxoEntryVerboseDataVerbosity, from RpcDataVerbosityLevel { + include_script_public_key_type: (RpcDataVerbosityLevel::Low), + include_script_public_key_address: (RpcDataVerbosityLevel::Low), + } +} + +impl_verbosity_from! { + for RpcUtxoEntryVerbosity, from RpcDataVerbosityLevel { + include_amount: (RpcDataVerbosityLevel::High), + include_script_public_key: (RpcDataVerbosityLevel::High), + include_block_daa_score: (RpcDataVerbosityLevel::Full), + include_is_coinbase: (RpcDataVerbosityLevel::High), + verbose_data_verbosity: (|level| { + RpcUtxoEntryVerboseDataVerbosity::from(level) + }), + } +} + +impl_verbosity_from! { + for RpcTransactionInputVerbosity, from RpcDataVerbosityLevel { + include_signature_script: (RpcDataVerbosityLevel::Low), + include_sequence: (RpcDataVerbosityLevel::High), + include_sig_op_count: (RpcDataVerbosityLevel::High), + include_previous_outpoint: (RpcDataVerbosityLevel::High), + verbose_data_verbosity: (|level| { + RpcTransactionInputVerboseDataVerbosity::from(level) + }), + + } +} + +impl_verbosity_from! { + for RpcTransactionInputVerboseDataVerbosity, from RpcDataVerbosityLevel { + utxo_entry_verbosity: (|level| { + RpcUtxoEntryVerbosity::from(level) + }), + } +} + +impl_verbosity_from! { + for RpcTransactionOutputVerbosity, from RpcDataVerbosityLevel { + include_amount: (RpcDataVerbosityLevel::Low), + include_script_public_key: (RpcDataVerbosityLevel::Low), + verbose_data_verbosity: (|level| { + RpcTransactionOutputVerboseDataVerbosity::from(level) + }), + } +} + +impl_verbosity_from! { + for RpcTransactionOutputVerboseDataVerbosity, from RpcDataVerbosityLevel { + include_script_public_key_type: (RpcDataVerbosityLevel::Low), + include_script_public_key_address: (RpcDataVerbosityLevel::Low), + } +} + +impl_verbosity_from! { + for RpcTransactionVerbosity, from RpcDataVerbosityLevel { + include_payload: (RpcDataVerbosityLevel::High), + include_mass: (RpcDataVerbosityLevel::High), + include_version: (RpcDataVerbosityLevel::Full), + include_lock_time: (RpcDataVerbosityLevel::Full), + include_subnetwork_id: (RpcDataVerbosityLevel::Full), + include_gas: (RpcDataVerbosityLevel::Full), + input_verbosity: (|level| { + RpcTransactionInputVerbosity::from(level) + }), + output_verbosity: (|level| { + RpcTransactionOutputVerbosity::from(level) + }), + verbose_data_verbosity: (|level| { + RpcTransactionVerboseDataVerbosity::from(level) + }), + } +} + +impl_verbosity_from! { + for RpcTransactionVerboseDataVerbosity, from RpcDataVerbosityLevel { + include_transaction_id: (RpcDataVerbosityLevel::Low), + include_compute_mass: (RpcDataVerbosityLevel::High), + include_block_hash: (RpcDataVerbosityLevel::Low), + include_block_time: (RpcDataVerbosityLevel::Low), + include_hash: (RpcDataVerbosityLevel::Low), + } +} + +impl_verbosity_from! { + for RpcAcceptanceDataVerbosity, from RpcDataVerbosityLevel { + accepting_chain_header_verbosity: (|level| { + RpcHeaderVerbosity::from(level) + }), + mergeset_block_acceptance_data_verbosity: (|level| { + RpcMergesetBlockAcceptanceDataVerbosity::from(level) + }), + } +} + +impl_verbosity_from! { + for RpcMergesetBlockAcceptanceDataVerbosity, from RpcDataVerbosityLevel { + merged_header_verbosity: (|level| { + RpcHeaderVerbosity::from(level) + }), + accepted_transactions_verbosity: (|level| { + RpcTransactionVerbosity::from(level) + }), + } +} diff --git a/rpc/core/src/error.rs b/rpc/core/src/error.rs index a51de937e2..42d7842e3f 100644 --- a/rpc/core/src/error.rs +++ b/rpc/core/src/error.rs @@ -141,6 +141,12 @@ pub enum RpcError { #[error("utxo return address could not be found -> {0}")] UtxoReturnAddressNotFound(UtxoInquirerError), + #[error("header hashes to {0}, provided is {1}")] + BadBlockHash(RpcHash, RpcHash), + + #[error("consensus converter required {0} - but was not found")] + ConsensusConverterNotFound(String), + #[error("consensus is currently in a transitional ibd state")] ConsensusInTransitionalIbdState, diff --git a/rpc/core/src/model/acceptance_data.rs b/rpc/core/src/model/acceptance_data.rs new file mode 100644 index 0000000000..4ff8e343fa --- /dev/null +++ b/rpc/core/src/model/acceptance_data.rs @@ -0,0 +1,81 @@ +use kaspa_hashes::Hash; +use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; + +use crate::RpcHash; + +use super::{RpcOptionalHeader, RpcOptionalTransaction}; + +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RpcAcceptanceData { + /// struct properties are optionally returned depending on the verbosity level + pub accepting_chain_header: Option, + /// struct properties are optionally returned depending on the verbosity level + pub mergeset_block_acceptance_data: Vec, +} + +impl RpcAcceptanceData { + pub fn new( + accepting_chain_header: Option, + mergeset_block_acceptance_data: Vec, + ) -> Self { + Self { accepting_chain_header, mergeset_block_acceptance_data } + } +} + +impl Serializer for RpcAcceptanceData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.accepting_chain_header, writer)?; + serialize!(Vec, &self.mergeset_block_acceptance_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcAcceptanceData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader); + let accepting_chain_header = load!(Option, reader)?; + let mergeset_block_acceptance_data = deserialize!(Vec, reader)?; + + Ok(Self { accepting_chain_header, mergeset_block_acceptance_data }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcMergesetBlockAcceptanceData { + pub merged_block_hash: RpcHash, + pub accepted_transactions: Vec, +} + +impl RpcMergesetBlockAcceptanceData { + #[inline(always)] + pub fn new(merged_block_hash: RpcHash, accepted_transactions: Vec) -> Self { + Self { merged_block_hash, accepted_transactions } + } +} + +impl Serializer for RpcMergesetBlockAcceptanceData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + + store!(Hash, &self.merged_block_hash, writer)?; + serialize!(Vec, &self.accepted_transactions, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcMergesetBlockAcceptanceData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader); + + let merged_block_hash = load!(RpcHash, reader)?; + let accepted_transactions = deserialize!(Vec, reader)?; + + Ok(Self { merged_block_hash, accepted_transactions }) + } +} diff --git a/rpc/core/src/model/block.rs b/rpc/core/src/model/block.rs index 3f4870dc69..f8ed1d2666 100644 --- a/rpc/core/src/model/block.rs +++ b/rpc/core/src/model/block.rs @@ -132,7 +132,7 @@ cfg_if::cfg_if! { const TS_BLOCK: &'static str = r#" /** * Interface defining the structure of a block. - * + * * @category Consensus */ export interface IBlock { @@ -143,7 +143,7 @@ cfg_if::cfg_if! { /** * Interface defining the structure of a block verbose data. - * + * * @category Node RPC */ export interface IBlockVerboseData { @@ -161,11 +161,11 @@ cfg_if::cfg_if! { /** * Interface defining the structure of a raw block. - * + * * Raw block is a structure used by GetBlockTemplate and SubmitBlock RPCs * and differs from `IBlock` in that it does not include verbose data and carries * `IRawHeader` that does not include a cached block hash. - * + * * @category Consensus */ export interface IRawBlock { diff --git a/rpc/core/src/model/message.rs b/rpc/core/src/model/message.rs index fa1f12cbe9..7a0a4c23b1 100644 --- a/rpc/core/src/model/message.rs +++ b/rpc/core/src/model/message.rs @@ -2733,6 +2733,77 @@ impl Deserializer for GetUtxoReturnAddressResponse { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetVirtualChainFromBlockV2Request { + pub start_hash: RpcHash, + pub data_verbosity_level: Option, + pub min_confirmation_count: Option, +} + +impl GetVirtualChainFromBlockV2Request { + pub fn new(start_hash: RpcHash, data_verbosity_level: Option, min_confirmation_count: Option) -> Self { + Self { start_hash, data_verbosity_level, min_confirmation_count } + } +} + +impl Serializer for GetVirtualChainFromBlockV2Request { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(RpcHash, &self.start_hash, writer)?; + serialize!(Option, &self.data_verbosity_level, writer)?; + store!(Option, &self.min_confirmation_count, writer)?; + + Ok(()) + } +} + +impl Deserializer for GetVirtualChainFromBlockV2Request { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let start_hash = load!(RpcHash, reader)?; + let data_verbosity_level = deserialize!(Option, reader)?; + let min_confirmation_count = load!(Option, reader)?; + + Ok(Self { start_hash, data_verbosity_level, min_confirmation_count }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetVirtualChainFromBlockV2Response { + /// always present, no matter the verbosity level + pub removed_chain_block_hashes: Arc>, + /// always present, no matter the verbosity level + pub added_chain_block_hashes: Arc>, + /// struct properties are optionally returned depending on the verbosity level + pub chain_block_accepted_transactions: Arc>, +} + +impl Serializer for GetVirtualChainFromBlockV2Response { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Vec, &self.removed_chain_block_hashes, writer)?; + store!(Vec, &self.added_chain_block_hashes, writer)?; + serialize!(Vec, &self.chain_block_accepted_transactions, writer)?; + Ok(()) + } +} + +impl Deserializer for GetVirtualChainFromBlockV2Response { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + let removed_chain_block_hashes = load!(Vec, reader)?; + let added_chain_block_hashes = load!(Vec, reader)?; + let chain_block_accepted_transactions = deserialize!(Vec, reader)?; + Ok(Self { + removed_chain_block_hashes: removed_chain_block_hashes.into(), + added_chain_block_hashes: added_chain_block_hashes.into(), + chain_block_accepted_transactions: chain_block_accepted_transactions.into(), + }) + } +} + // ---------------------------------------------------------------------------- // Subscriptions & notifications // ---------------------------------------------------------------------------- diff --git a/rpc/core/src/model/mod.rs b/rpc/core/src/model/mod.rs index a7c2556249..34f08c85b1 100644 --- a/rpc/core/src/model/mod.rs +++ b/rpc/core/src/model/mod.rs @@ -1,6 +1,7 @@ //! This module contains RPC-specific data structures //! used in RPC methods. +pub mod acceptance_data; pub mod address; pub mod block; pub mod blue_work; @@ -11,12 +12,15 @@ pub mod hex_cnv; pub mod mempool; pub mod message; pub mod network; +pub mod optional; pub mod peer; pub mod script_class; pub mod subnets; mod tests; pub mod tx; +pub mod verbosity; +pub use acceptance_data::*; pub use address::*; pub use block::*; pub use blue_work::*; @@ -27,6 +31,8 @@ pub use hex_cnv::*; pub use mempool::*; pub use message::*; pub use network::*; +pub use optional::*; pub use peer::*; pub use subnets::*; pub use tx::*; +pub use verbosity::*; diff --git a/rpc/core/src/model/optional/block.rs b/rpc/core/src/model/optional/block.rs new file mode 100644 index 0000000000..b9b2214f29 --- /dev/null +++ b/rpc/core/src/model/optional/block.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; + +use crate::{RpcBlockVerboseData, RpcOptionalHeader, RpcOptionalTransaction}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalBlock { + pub header: Option, + pub transactions: Vec, + pub verbose_data: Option, +} + +impl Serializer for RpcOptionalBlock { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(Option, &self.header, writer)?; + serialize!(Vec, &self.transactions, writer)?; + serialize!(Option, &self.verbose_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcOptionalBlock { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + match _version { + 1 => { + let header = Some(deserialize!(RpcOptionalHeader, reader)?); + let transactions = deserialize!(Vec, reader)?; + let verbose_data = deserialize!(Option, reader)?; + Ok(Self { header, transactions, verbose_data }) + } + _ => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Unsupported version: {}", _version))), + } + } +} diff --git a/rpc/core/src/model/optional/header.rs b/rpc/core/src/model/optional/header.rs new file mode 100644 index 0000000000..5d137f3d07 --- /dev/null +++ b/rpc/core/src/model/optional/header.rs @@ -0,0 +1,232 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use kaspa_consensus_core::{header::Header, BlueWorkType}; +use kaspa_hashes::Hash; +use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; + +use crate::{RpcError, RpcResult}; + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalHeader { + /// Level: None - Cached hash + pub hash: Option, + /// Level: Low + pub version: Option, + /// Level: High + pub parents_by_level: Vec>, + /// Level: High + pub hash_merkle_root: Option, + /// Level: High + pub accepted_id_merkle_root: Option, + /// Level: Full + pub utxo_commitment: Option, + /// Level: Low - Timestamp is in milliseconds + pub timestamp: Option, + /// Level: Low + pub bits: Option, + /// Level: Low + pub nonce: Option, + /// Level: Low + pub daa_score: Option, + /// Level: Low + pub blue_work: Option, + /// Level: Low + pub blue_score: Option, + /// Level: Full + pub pruning_point: Option, +} + +impl RpcOptionalHeader { + pub fn is_empty(&self) -> bool { + self.hash.is_none() + && self.version.is_none() + && self.parents_by_level.is_empty() + && self.hash_merkle_root.is_none() + && self.accepted_id_merkle_root.is_none() + && self.utxo_commitment.is_none() + && self.timestamp.is_none() + && self.bits.is_none() + && self.nonce.is_none() + && self.daa_score.is_none() + && self.blue_work.is_none() + && self.blue_score.is_none() + && self.pruning_point.is_none() + } + pub fn direct_parents(&self) -> &[Hash] { + if self.parents_by_level.is_empty() { + &[] + } else { + &self.parents_by_level[0] + } + } +} + +impl AsRef for RpcOptionalHeader { + fn as_ref(&self) -> &RpcOptionalHeader { + self + } +} + +impl From
for RpcOptionalHeader { + fn from(header: Header) -> Self { + Self { + hash: Some(header.hash), + version: Some(header.version), + parents_by_level: header.parents_by_level.into(), + hash_merkle_root: Some(header.hash_merkle_root), + accepted_id_merkle_root: Some(header.accepted_id_merkle_root), + utxo_commitment: Some(header.utxo_commitment), + timestamp: Some(header.timestamp), + bits: Some(header.bits), + nonce: Some(header.nonce), + daa_score: Some(header.daa_score), + blue_work: Some(header.blue_work), + blue_score: Some(header.blue_score), + pruning_point: Some(header.pruning_point), + } + } +} + +impl From<&Header> for RpcOptionalHeader { + fn from(header: &Header) -> Self { + Self { + hash: Some(header.hash), + version: Some(header.version), + parents_by_level: header.parents_by_level.clone().into(), + hash_merkle_root: Some(header.hash_merkle_root), + accepted_id_merkle_root: Some(header.accepted_id_merkle_root), + utxo_commitment: Some(header.utxo_commitment), + timestamp: Some(header.timestamp), + bits: Some(header.bits), + nonce: Some(header.nonce), + daa_score: Some(header.daa_score), + blue_work: Some(header.blue_work), + blue_score: Some(header.blue_score), + pruning_point: Some(header.pruning_point), + } + } +} + +impl TryFrom for Header { + type Error = RpcError; + + fn try_from(header: RpcOptionalHeader) -> RpcResult { + Ok(Self { + hash: header.hash.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "hash".to_owned()))?, + version: header.version.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "version".to_owned()))?, + parents_by_level: header.parents_by_level.try_into()?, + hash_merkle_root: header + .hash_merkle_root + .ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "hash_merkle_root".to_owned()))?, + accepted_id_merkle_root: header + .accepted_id_merkle_root + .ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "accepted_id_merkle_root".to_owned()))?, + utxo_commitment: header + .utxo_commitment + .ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "utxo_commitment".to_owned()))?, + timestamp: header.timestamp.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "timestamp".to_owned()))?, + bits: header.bits.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "bits".to_owned()))?, + nonce: header.nonce.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "nonce".to_owned()))?, + daa_score: header.daa_score.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "daa_score".to_owned()))?, + blue_work: header.blue_work.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "blue_work".to_owned()))?, + blue_score: header.blue_score.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "blue_score".to_owned()))?, + pruning_point: header + .pruning_point + .ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "pruning_point".to_owned()))?, + }) + } +} + +impl TryFrom<&RpcOptionalHeader> for Header { + type Error = RpcError; + + fn try_from(header: &RpcOptionalHeader) -> RpcResult { + Ok(Self { + hash: header.hash.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "hash".to_owned()))?, + version: header.version.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "version".to_owned()))?, + parents_by_level: header.parents_by_level.clone().try_into()?, + hash_merkle_root: header + .hash_merkle_root + .ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "hash_merkle_root".to_owned()))?, + accepted_id_merkle_root: header + .accepted_id_merkle_root + .ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "accepted_id_merkle_root".to_owned()))?, + utxo_commitment: header + .utxo_commitment + .ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "utxo_commitment".to_owned()))?, + timestamp: header.timestamp.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "timestamp".to_owned()))?, + bits: header.bits.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "bits".to_owned()))?, + nonce: header.nonce.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "nonce".to_owned()))?, + daa_score: header.daa_score.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "daa_score".to_owned()))?, + blue_work: header.blue_work.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "blue_work".to_owned()))?, + blue_score: header.blue_score.ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "blue_score".to_owned()))?, + pruning_point: header + .pruning_point + .ok_or(RpcError::MissingRpcFieldError("RpcHeader".to_owned(), "pruning_point".to_owned()))?, + }) + } +} + +impl Serializer for RpcOptionalHeader { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + + store!(Option, &self.hash, writer)?; + store!(Option, &self.version, writer)?; + store!(Vec>, &self.parents_by_level, writer)?; + store!(Option, &self.hash_merkle_root, writer)?; + store!(Option, &self.accepted_id_merkle_root, writer)?; + store!(Option, &self.utxo_commitment, writer)?; + store!(Option, &self.timestamp, writer)?; + store!(Option, &self.bits, writer)?; + store!(Option, &self.nonce, writer)?; + store!(Option, &self.daa_score, writer)?; + store!(Option, &self.blue_work, writer)?; + store!(Option, &self.blue_score, writer)?; + store!(Option, &self.pruning_point, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcOptionalHeader { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + + match _version { + 1 => { + let hash = load!(Option, reader)?; + let version = load!(Option, reader)?; + let parents_by_level = load!(Vec>, reader)?; + let hash_merkle_root = load!(Option, reader)?; + let accepted_id_merkle_root = load!(Option, reader)?; + let utxo_commitment = load!(Option, reader)?; + let timestamp = load!(Option, reader)?; + let bits = load!(Option, reader)?; + let nonce = load!(Option, reader)?; + let daa_score = load!(Option, reader)?; + let blue_work = load!(Option, reader)?; + let blue_score = load!(Option, reader)?; + let pruning_point = load!(Option, reader)?; + + Ok(Self { + hash, + version, + parents_by_level, + hash_merkle_root, + accepted_id_merkle_root, + utxo_commitment, + timestamp, + bits, + nonce, + daa_score, + blue_work, + blue_score, + pruning_point, + }) + } + _ => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Unsupported version: {}", _version))), + } + } +} diff --git a/rpc/core/src/model/optional/mod.rs b/rpc/core/src/model/optional/mod.rs new file mode 100644 index 0000000000..1949785b53 --- /dev/null +++ b/rpc/core/src/model/optional/mod.rs @@ -0,0 +1,7 @@ +pub mod block; +pub mod header; +pub mod tx; + +pub use block::*; +pub use header::*; +pub use tx::*; diff --git a/rpc/core/src/model/optional/tx.rs b/rpc/core/src/model/optional/tx.rs new file mode 100644 index 0000000000..8654175251 --- /dev/null +++ b/rpc/core/src/model/optional/tx.rs @@ -0,0 +1,594 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use kaspa_addresses::Address; +use kaspa_consensus_core::tx::{ + ScriptPublicKey, TransactionId, TransactionIndexType, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry, +}; +use kaspa_utils::{hex::ToHex, serde_bytes_fixed_ref}; +use serde::{Deserialize, Serialize}; +use serde_nested_with::serde_nested; +use workflow_serializer::prelude::*; + +use crate::{ + prelude::{RpcHash, RpcScriptClass, RpcSubnetworkId}, + RpcError, RpcResult, RpcScriptPublicKey, RpcTransactionId, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalUtxoEntry { + /// Level: High + pub amount: Option, + /// Level: High + pub script_public_key: Option, + /// Level: Full + pub block_daa_score: Option, + /// Level: High + pub is_coinbase: Option, + pub verbose_data: Option, +} + +impl RpcOptionalUtxoEntry { + pub fn is_empty(&self) -> bool { + self.amount.is_none() + && self.script_public_key.is_none() + && self.block_daa_score.is_none() + && self.is_coinbase.is_none() + && (self.verbose_data.is_none() || self.verbose_data.as_ref().is_some_and(|x| x.is_empty())) + } + + pub fn new( + amount: Option, + script_public_key: Option, + block_daa_score: Option, + is_coinbase: Option, + verbose_data: Option, + ) -> Self { + Self { amount, script_public_key, block_daa_score, is_coinbase, verbose_data } + } +} + +impl From for RpcOptionalUtxoEntry { + fn from(entry: UtxoEntry) -> Self { + Self { + amount: Some(entry.amount), + script_public_key: Some(entry.script_public_key), + block_daa_score: Some(entry.block_daa_score), + is_coinbase: Some(entry.is_coinbase), + verbose_data: None, + } + } +} + +impl TryFrom for UtxoEntry { + type Error = RpcError; + + fn try_from(entry: RpcOptionalUtxoEntry) -> RpcResult { + Ok(Self { + amount: entry.amount.ok_or(RpcError::MissingRpcFieldError("RpcUtxoEntry".to_string(), "amount".to_string()))?, + script_public_key: entry + .script_public_key + .ok_or(RpcError::MissingRpcFieldError("RpcUtxoEntry".to_string(), "script_public_key".to_string()))?, + block_daa_score: entry + .block_daa_score + .ok_or(RpcError::MissingRpcFieldError("RpcUtxoEntry".to_string(), "block_daa_score".to_string()))?, + is_coinbase: entry + .is_coinbase + .ok_or(RpcError::MissingRpcFieldError("RpcUtxoEntry".to_string(), "is_coinbase".to_string()))?, + }) + } +} + +impl Serializer for RpcOptionalUtxoEntry { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.amount, writer)?; + store!(Option, &self.script_public_key, writer)?; + store!(Option, &self.block_daa_score, writer)?; + store!(Option, &self.is_coinbase, writer)?; + serialize!(Option, &self.verbose_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcOptionalUtxoEntry { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + + match _version { + 1 => { + let amount = load!(Option, reader)?; + let script_public_key = load!(Option, reader)?; + let block_daa_score = load!(Option, reader)?; + let is_coinbase = load!(Option, reader)?; + let verbose_data = deserialize!(Option, reader)?; + + Ok(Self { amount, script_public_key, block_daa_score, is_coinbase, verbose_data }) + } + _ => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Unsupported version: {}", _version))), + } + } +} + +#[derive(Eq, Hash, PartialEq, Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalUtxoEntryVerboseData { + /// Level: Low + pub script_public_key_type: Option, + /// Level: Low + pub script_public_key_address: Option
, +} + +impl RpcOptionalUtxoEntryVerboseData { + pub fn is_empty(&self) -> bool { + self.script_public_key_type.is_none() && self.script_public_key_address.is_none() + } + + pub fn new(script_public_key_type: Option, script_public_key_address: Option
) -> Self { + Self { script_public_key_type, script_public_key_address } + } +} + +impl Serializer for RpcOptionalUtxoEntryVerboseData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.script_public_key_type, writer)?; + store!(Option
, &self.script_public_key_address, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcOptionalUtxoEntryVerboseData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let script_public_key_type = load!(Option, reader)?; + let script_public_key_address = load!(Option
, reader)?; + + Ok(Self { script_public_key_type, script_public_key_address }) + } +} + +/// Represents a Kaspa transaction outpoint +#[derive(Eq, Hash, PartialEq, Debug, Copy, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde_nested] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalTransactionOutpoint { + #[serde_nested(sub = "TransactionId", serde(with = "serde_bytes_fixed_ref"))] + pub transaction_id: Option, + pub index: Option, +} + +impl From for RpcOptionalTransactionOutpoint { + fn from(outpoint: TransactionOutpoint) -> Self { + Self { transaction_id: Some(outpoint.transaction_id), index: Some(outpoint.index) } + } +} + +impl TryFrom for TransactionOutpoint { + type Error = RpcError; + + fn try_from(outpoint: RpcOptionalTransactionOutpoint) -> RpcResult { + Ok(Self { + transaction_id: outpoint + .transaction_id + .ok_or(RpcError::MissingRpcFieldError("RpcTransactionOutpoint".to_string(), "transaction_id".to_string()))?, + index: outpoint.index.ok_or(RpcError::MissingRpcFieldError("RpcTransactionOutpoint".to_string(), "index".to_string()))?, + }) + } +} + +impl From for RpcOptionalTransactionOutpoint { + fn from(outpoint: kaspa_consensus_client::TransactionOutpoint) -> Self { + TransactionOutpoint::from(outpoint).into() + } +} + +impl TryFrom for kaspa_consensus_client::TransactionOutpoint { + type Error = RpcError; + + fn try_from(outpoint: RpcOptionalTransactionOutpoint) -> RpcResult { + Ok(TransactionOutpoint::try_from(outpoint)?.into()) + } +} + +impl Serializer for RpcOptionalTransactionOutpoint { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.transaction_id, writer)?; + store!(Option, &self.index, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcOptionalTransactionOutpoint { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + match _version { + 1 => { + let transaction_id = load!(Option, reader)?; + let index = load!(Option, reader)?; + Ok(Self { transaction_id, index }) + } + _ => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Unsupported version: {}", _version))), + } + } +} + +/// Represents a Kaspa transaction input +#[derive(Clone, Serialize, Deserialize)] +#[serde_nested] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalTransactionInput { + /// Level: High + pub previous_outpoint: Option, + #[serde_nested(sub = "Vec", serde(with = "hex::serde"))] + /// Level: Low + pub signature_script: Option>, + /// Level: High + pub sequence: Option, + /// Level: High + pub sig_op_count: Option, + pub verbose_data: Option, +} + +impl std::fmt::Debug for RpcOptionalTransactionInput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RpcTransactionInput") + .field("previous_outpoint", &self.previous_outpoint) + .field("signature_script", &self.signature_script.as_ref().map(|v| v.to_hex())) + .field("sequence", &self.sequence) + .field("sig_op_count", &self.sig_op_count) + .field("verbose_data", &self.verbose_data) + .finish() + } +} + +impl From for RpcOptionalTransactionInput { + fn from(input: TransactionInput) -> Self { + Self { + previous_outpoint: Some(input.previous_outpoint.into()), + signature_script: Some(input.signature_script), + sequence: Some(input.sequence), + sig_op_count: Some(input.sig_op_count), + verbose_data: None, + } + } +} + +impl RpcOptionalTransactionInput { + /// Note: verbose data will not be automatically populated when converting from TransactionInput to RpcTransactionInput + pub fn from_transaction_inputs(other: Vec) -> Vec { + other.into_iter().map(Self::from).collect() + } + + pub fn is_empty(&self) -> bool { + self.previous_outpoint.is_none() + && self.signature_script.is_none() + && self.sequence.is_none() + && self.sig_op_count.is_none() + && (self.verbose_data.is_none() || self.verbose_data.as_ref().is_some_and(|x| x.is_empty())) + } +} + +impl Serializer for RpcOptionalTransactionInput { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + serialize!(Option, &self.previous_outpoint, writer)?; + store!(Option>, &self.signature_script, writer)?; + store!(Option, &self.sequence, writer)?; + store!(Option, &self.sig_op_count, writer)?; + serialize!(Option, &self.verbose_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcOptionalTransactionInput { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + + Ok(match _version { + 1 => { + let previous_outpoint = deserialize!(Option, reader)?; + let signature_script = load!(Option>, reader)?; + let sequence = load!(Option, reader)?; + let sig_op_count = load!(Option, reader)?; + let verbose_data = deserialize!(Option, reader)?; + + Self { previous_outpoint, signature_script, sequence, sig_op_count, verbose_data } + } + _ => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Unsupported version: {}", _version))), + }) + } +} + +/// Represent Kaspa transaction input verbose data +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalTransactionInputVerboseData { + pub utxo_entry: Option, +} + +impl RpcOptionalTransactionInputVerboseData { + pub fn is_empty(&self) -> bool { + self.utxo_entry.is_none() || self.utxo_entry.as_ref().is_some_and(|x| x.is_empty()) + } +} + +impl Serializer for RpcOptionalTransactionInputVerboseData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + serialize!(Option, &self.utxo_entry, writer)?; + Ok(()) + } +} + +impl Deserializer for RpcOptionalTransactionInputVerboseData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let utxo_entry = deserialize!(Option, reader)?; + Ok(Self { utxo_entry }) + } +} + +/// Represents a Kaspad transaction output +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalTransactionOutput { + /// Level - Low + pub value: Option, + /// Level - Low + pub script_public_key: Option, + pub verbose_data: Option, +} + +impl RpcOptionalTransactionOutput { + pub fn is_empty(&self) -> bool { + self.value.is_none() + && self.script_public_key.is_none() + && (self.verbose_data.is_none() || self.verbose_data.as_ref().is_some_and(|x| x.is_empty())) + } + + pub fn from_transaction_outputs(other: Vec) -> Vec { + other.into_iter().map(Self::from).collect() + } +} + +impl From for RpcOptionalTransactionOutput { + fn from(output: TransactionOutput) -> Self { + Self { value: Some(output.value), script_public_key: Some(output.script_public_key), verbose_data: None } + } +} + +impl Serializer for RpcOptionalTransactionOutput { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.value, writer)?; + store!(Option, &self.script_public_key, writer)?; + serialize!(Option, &self.verbose_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcOptionalTransactionOutput { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + Ok(match _version { + 1 => { + let value = load!(Option, reader)?; + let script_public_key = load!(Option, reader)?; + let verbose_data = deserialize!(Option, reader)?; + + Self { value, script_public_key, verbose_data } + } + _ => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Unsupported version: {}", _version))), + }) + } +} + +/// Represent Kaspa transaction output verbose data +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalTransactionOutputVerboseData { + /// Level: Low + pub script_public_key_type: Option, + /// Level: Low + pub script_public_key_address: Option
, +} + +impl RpcOptionalTransactionOutputVerboseData { + pub fn is_empty(&self) -> bool { + self.script_public_key_type.is_none() && self.script_public_key_address.is_none() + } +} + +impl Serializer for RpcOptionalTransactionOutputVerboseData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.script_public_key_type, writer)?; + store!(Option
, &self.script_public_key_address, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcOptionalTransactionOutputVerboseData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + Ok(match _version { + 1 => { + let script_public_key_type = load!(Option, reader)?; + let script_public_key_address = load!(Option
, reader)?; + Self { script_public_key_type, script_public_key_address } + } + _ => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Unsupported version: {}", _version))), + }) + } +} + +/// Represents a Kaspa transaction +#[derive(Clone, Serialize, Deserialize)] +#[serde_nested] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalTransaction { + /// Level: Full + pub version: Option, + pub inputs: Vec, + pub outputs: Vec, + /// Level: Full + pub lock_time: Option, + /// Level: Full + pub subnetwork_id: Option, + /// Level: Full + pub gas: Option, + #[serde_nested(sub = "Vec", serde(with = "hex::serde"))] + /// Level: High + pub payload: Option>, + /// Level: High + pub mass: Option, + pub verbose_data: Option, +} + +impl RpcOptionalTransaction { + pub fn is_empty(&self) -> bool { + self.version.is_none() + && (self.inputs.is_empty() || self.inputs.iter().all(|input| input.is_empty())) + && (self.outputs.is_empty() || self.outputs.iter().all(|output| output.is_empty())) + && self.lock_time.is_none() + && self.subnetwork_id.is_none() + && self.gas.is_none() + && self.payload.is_none() + && self.mass.is_none() + && (self.verbose_data.is_none() || self.verbose_data.as_ref().is_some_and(|x| x.is_empty())) + } +} + +impl std::fmt::Debug for RpcOptionalTransaction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RpcTransaction") + .field("version", &self.version) + .field("lock_time", &self.lock_time) + .field("subnetwork_id", &self.subnetwork_id) + .field("gas", &self.gas) + .field("payload", &self.payload.as_ref().map(|v|v.to_hex())) + .field("mass", &self.mass) + .field("inputs", &self.inputs) // Inputs and outputs are placed purposely at the end for better debug visibility + .field("outputs", &self.outputs) + .field("verbose_data", &self.verbose_data) + .finish() + } +} + +impl Serializer for RpcOptionalTransaction { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + store!(Option, &self.version, writer)?; + serialize!(Vec, &self.inputs, writer)?; + serialize!(Vec, &self.outputs, writer)?; + store!(Option, &self.lock_time, writer)?; + store!(Option, &self.subnetwork_id, writer)?; + store!(Option, &self.gas, writer)?; + store!(Option>, &self.payload, writer)?; + store!(Option, &self.mass, writer)?; + serialize!(Option, &self.verbose_data, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcOptionalTransaction { + fn deserialize(reader: &mut R) -> std::io::Result { + let _struct_version = load!(u16, reader)?; + Ok(match _struct_version { + 1 => { + let version = load!(Option, reader)?; + let inputs = deserialize!(Vec, reader)?; + let outputs = deserialize!(Vec, reader)?; + let lock_time = load!(Option, reader)?; + let subnetwork_id = load!(Option, reader)?; + let gas = load!(Option, reader)?; + let payload = load!(Option>, reader)?; + let mass = load!(Option, reader)?; + let verbose_data = deserialize!(Option, reader)?; + + Self { version, inputs, outputs, lock_time, subnetwork_id, gas, payload, mass, verbose_data } + } + _ => { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Unsupported version: {}", _struct_version))) + } + }) + } +} + +/// Represent Kaspa transaction verbose data +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde_nested] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalTransactionVerboseData { + #[serde_nested(sub = "RpcTransactionId", serde(with = "serde_bytes_fixed_ref"))] + /// Level: Low + pub transaction_id: Option, + #[serde_nested(sub = "RpcHash", serde(with = "serde_bytes_fixed_ref"))] + /// Level: Low + pub hash: Option, + /// Level: High + pub compute_mass: Option, + #[serde_nested(sub = "RpcHash", serde(with = "serde_bytes_fixed_ref"))] + /// Level: Low + pub block_hash: Option, + /// Level: Low + pub block_time: Option, +} + +impl RpcOptionalTransactionVerboseData { + pub fn is_empty(&self) -> bool { + self.transaction_id.is_none() + && self.hash.is_none() + && self.compute_mass.is_none() + && self.block_hash.is_none() + && self.block_time.is_none() + } +} + +impl Serializer for RpcOptionalTransactionVerboseData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.transaction_id, writer)?; + store!(Option, &self.hash, writer)?; + store!(Option, &self.compute_mass, writer)?; + store!(Option, &self.block_hash, writer)?; + store!(Option, &self.block_time, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcOptionalTransactionVerboseData { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + Ok(match _version { + 1 => { + let transaction_id = load!(Option, reader)?; + let hash = load!(Option, reader)?; + let compute_mass = load!(Option, reader)?; + let block_hash = load!(Option, reader)?; + let block_time = load!(Option, reader)?; + + Self { transaction_id, hash, compute_mass, block_hash, block_time } + } + _ => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Unsupported version: {}", _version))), + }) + } +} + +/// Represents accepted transaction ids +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcOptionalAcceptedTransactionIds { + #[serde(with = "serde_bytes_fixed_ref")] + pub accepting_block_hash: RpcHash, + pub accepted_transaction_ids: Vec, +} diff --git a/rpc/core/src/model/tests.rs b/rpc/core/src/model/tests.rs index 16a8e78628..035acbb273 100644 --- a/rpc/core/src/model/tests.rs +++ b/rpc/core/src/model/tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod mockery { - use crate::{model::*, RpcScriptClass}; + use crate::model::*; use kaspa_addresses::{Prefix, Version}; use kaspa_consensus_core::api::BlockCount; use kaspa_consensus_core::network::NetworkType; @@ -11,6 +11,7 @@ mod mockery { use kaspa_math::Uint192; use kaspa_notify::subscription::Command; use kaspa_rpc_macros::test_wrpc_serializer as test; + use kaspa_txscript::script_class::ScriptClass; use kaspa_utils::networking::{ContextualNetAddress, IpAddress, NetAddress}; use rand::Rng; use std::net::{IpAddr, Ipv4Addr}; @@ -55,7 +56,8 @@ mod mockery { where T: Mock, { - Mock::mock() + // forward to the type's Mock implementation + T::mock() } // this function tests serialization and deserialization of a type @@ -203,9 +205,9 @@ mod mockery { } } - impl Mock for RpcTransactionInputVerboseData { + impl Mock for RpcOptionalTransactionInputVerboseData { fn mock() -> Self { - RpcTransactionInputVerboseData {} + RpcOptionalTransactionInputVerboseData { utxo_entry: mock() } } } @@ -221,9 +223,33 @@ mod mockery { } } + impl Mock for RpcOptionalTransactionInput { + fn mock() -> Self { + RpcOptionalTransactionInput { + previous_outpoint: mock(), + signature_script: Some(Hash::mock().as_bytes().to_vec()), + sequence: mock(), + sig_op_count: mock(), + verbose_data: mock(), + } + } + } + + impl Mock for RpcOptionalTransactionOutpoint { + fn mock() -> Self { + RpcOptionalTransactionOutpoint { transaction_id: mock(), index: mock() } + } + } + impl Mock for RpcTransactionOutputVerboseData { fn mock() -> Self { - RpcTransactionOutputVerboseData { script_public_key_type: RpcScriptClass::PubKey, script_public_key_address: mock() } + RpcTransactionOutputVerboseData { script_public_key_type: mock(), script_public_key_address: mock() } + } + } + + impl Mock for RpcOptionalTransactionOutputVerboseData { + fn mock() -> Self { + RpcOptionalTransactionOutputVerboseData { script_public_key_type: mock(), script_public_key_address: mock() } } } @@ -233,6 +259,12 @@ mod mockery { } } + impl Mock for RpcOptionalTransactionOutput { + fn mock() -> Self { + RpcOptionalTransactionOutput { value: mock(), script_public_key: mock(), verbose_data: mock() } + } + } + impl Mock for RpcTransactionVerboseData { fn mock() -> Self { RpcTransactionVerboseData { @@ -245,6 +277,103 @@ mod mockery { } } + impl Mock for RpcOptionalTransactionVerboseData { + fn mock() -> Self { + RpcOptionalTransactionVerboseData { + transaction_id: mock(), + hash: mock(), + compute_mass: mock(), + block_hash: mock(), + block_time: mock(), + } + } + } + + impl Mock for RpcUtxoEntryVerbosity { + fn mock() -> Self { + RpcUtxoEntryVerbosity { + include_amount: mock(), + include_script_public_key: mock(), + include_block_daa_score: mock(), + include_is_coinbase: mock(), + verbose_data_verbosity: mock(), + } + } + } + + impl Mock for RpcUtxoEntryVerboseDataVerbosity { + fn mock() -> Self { + RpcUtxoEntryVerboseDataVerbosity { include_script_public_key_type: mock(), include_script_public_key_address: mock() } + } + } + + impl Mock for RpcTransactionInputVerboseDataVerbosity { + fn mock() -> Self { + RpcTransactionInputVerboseDataVerbosity { utxo_entry_verbosity: mock() } + } + } + + impl Mock for RpcTransactionInputVerboseData { + fn mock() -> Self { + RpcTransactionInputVerboseData {} + } + } + + impl Mock for RpcTransactionInputVerbosity { + fn mock() -> Self { + RpcTransactionInputVerbosity { + include_previous_outpoint: mock(), + include_signature_script: mock(), + include_sequence: mock(), + include_sig_op_count: mock(), + verbose_data_verbosity: mock(), + } + } + } + + impl Mock for RpcTransactionOutputVerbosity { + fn mock() -> Self { + RpcTransactionOutputVerbosity { include_amount: mock(), include_script_public_key: mock(), verbose_data_verbosity: mock() } + } + } + + impl Mock for RpcTransactionOutputVerboseDataVerbosity { + fn mock() -> Self { + RpcTransactionOutputVerboseDataVerbosity { + include_script_public_key_type: mock(), + include_script_public_key_address: mock(), + } + } + } + + impl Mock for RpcTransactionVerboseDataVerbosity { + fn mock() -> Self { + RpcTransactionVerboseDataVerbosity { + include_transaction_id: mock(), + include_hash: mock(), + include_compute_mass: mock(), + include_block_hash: mock(), + include_block_time: mock(), + } + } + } + + impl Mock for RpcTransactionVerbosity { + fn mock() -> Self { + RpcTransactionVerbosity { + include_version: mock(), + input_verbosity: mock(), + output_verbosity: mock(), + include_lock_time: mock(), + include_subnetwork_id: mock(), + include_gas: mock(), + include_payload: mock(), + include_mass: mock(), + verbose_data_verbosity: mock(), + } + } + } + impl Mock for RpcTransaction { fn mock() -> Self { RpcTransaction { @@ -261,6 +390,54 @@ mod mockery { } } + impl Mock for RpcOptionalTransaction { + fn mock() -> Self { + RpcOptionalTransaction { + version: mock(), + inputs: mock(), + outputs: mock(), + lock_time: mock(), + subnetwork_id: mock(), + gas: mock(), + payload: Some(Hash::mock().as_bytes().to_vec()), + mass: mock(), + verbose_data: mock(), + } + } + } + + impl Mock for RpcMergesetBlockAcceptanceData { + fn mock() -> Self { + RpcMergesetBlockAcceptanceData { merged_block_hash: mock(), accepted_transactions: mock() } + } + } + + impl Mock for RpcOptionalHeader { + fn mock() -> Self { + RpcOptionalHeader { + version: mock(), + timestamp: mock(), + bits: mock(), + nonce: mock(), + hash_merkle_root: mock(), + accepted_id_merkle_root: mock(), + utxo_commitment: mock(), + hash: mock(), + parents_by_level: vec![mock()], + daa_score: mock(), + blue_score: mock(), + blue_work: mock(), + pruning_point: mock(), + } + } + } + + impl Mock for RpcAcceptanceData { + fn mock() -> Self { + RpcAcceptanceData { accepting_chain_header: mock(), mergeset_block_acceptance_data: mock() } + } + } + impl Mock for RpcNodeId { fn mock() -> Self { RpcNodeId::new(Uuid::new_v4()) @@ -329,7 +506,36 @@ mod mockery { impl Mock for RpcUtxoEntry { fn mock() -> Self { - RpcUtxoEntry { amount: mock(), script_public_key: mock(), block_daa_score: mock(), is_coinbase: true } + RpcUtxoEntry { amount: mock(), script_public_key: mock(), block_daa_score: mock(), is_coinbase: mock() } + } + } + + impl Mock for RpcOptionalUtxoEntry { + fn mock() -> Self { + RpcOptionalUtxoEntry { + amount: mock(), + script_public_key: mock(), + block_daa_score: mock(), + is_coinbase: mock(), + verbose_data: mock(), + } + } + } + + impl Mock for RpcOptionalUtxoEntryVerboseData { + fn mock() -> Self { + RpcOptionalUtxoEntryVerboseData { script_public_key_type: mock(), script_public_key_address: mock() } + } + } + + impl Mock for ScriptClass { + fn mock() -> Self { + match rand::thread_rng().gen::() % 4 { + 0 => ScriptClass::NonStandard, + 1 => ScriptClass::PubKey, + 2 => ScriptClass::PubKeyECDSA, + _ => ScriptClass::ScriptHash, // 3 + } } } @@ -1068,6 +1274,32 @@ mod mockery { test!(GetDaaScoreTimestampEstimateResponse); + impl Mock for GetVirtualChainFromBlockV2Request { + fn mock() -> Self { + GetVirtualChainFromBlockV2Request { start_hash: mock(), data_verbosity_level: None, min_confirmation_count: mock() } + } + } + + test!(GetVirtualChainFromBlockV2Request); + + impl Mock for RpcChainBlockAcceptedTransactions { + fn mock() -> Self { + RpcChainBlockAcceptedTransactions { chain_block_header: mock(), accepted_transactions: mock() } + } + } + + impl Mock for GetVirtualChainFromBlockV2Response { + fn mock() -> Self { + GetVirtualChainFromBlockV2Response { + removed_chain_block_hashes: mock(), + added_chain_block_hashes: mock(), + chain_block_accepted_transactions: mock(), + } + } + } + + test!(GetVirtualChainFromBlockV2Response); + impl Mock for NotifyBlockAddedRequest { fn mock() -> Self { NotifyBlockAddedRequest { command: Command::Start } diff --git a/rpc/core/src/model/tx.rs b/rpc/core/src/model/tx.rs index 0c17e26f53..fdcf407d89 100644 --- a/rpc/core/src/model/tx.rs +++ b/rpc/core/src/model/tx.rs @@ -8,7 +8,10 @@ use kaspa_utils::{hex::ToHex, serde_bytes_fixed_ref}; use serde::{Deserialize, Serialize}; use workflow_serializer::prelude::*; -use crate::prelude::{RpcHash, RpcScriptClass, RpcSubnetworkId}; +use crate::{ + prelude::{RpcHash, RpcScriptClass, RpcSubnetworkId}, + RpcOptionalHeader, RpcOptionalTransaction, +}; /// Represents the ID of a Kaspa transaction pub type RpcTransactionId = TransactionId; @@ -313,7 +316,7 @@ impl std::fmt::Debug for RpcTransaction { .field("gas", &self.gas) .field("payload", &self.payload.to_hex()) .field("mass", &self.mass) - .field("inputs", &self.inputs) // Inputs and outputs are placed purposely at the end for better debug visibility + .field("inputs", &self.inputs) // Inputs and outputs are placed purposely at the end for better debug visibility .field("outputs", &self.outputs) .field("verbose_data", &self.verbose_data) .finish() @@ -398,3 +401,31 @@ pub struct RpcAcceptedTransactionIds { pub accepting_block_hash: RpcHash, pub accepted_transaction_ids: Vec, } + +/// Represents accepted transaction ids +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcChainBlockAcceptedTransactions { + pub chain_block_header: RpcOptionalHeader, + pub accepted_transactions: Vec, +} + +impl Serializer for RpcChainBlockAcceptedTransactions { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + serialize!(RpcOptionalHeader, &self.chain_block_header, writer)?; + serialize!(Vec, &self.accepted_transactions, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcChainBlockAcceptedTransactions { + fn deserialize(reader: &mut R) -> std::io::Result { + let _struct_version = load!(u16, reader)?; + let chain_block_header = deserialize!(RpcOptionalHeader, reader)?; + let accepted_transactions = deserialize!(Vec, reader)?; + + Ok(Self { chain_block_header, accepted_transactions }) + } +} diff --git a/rpc/core/src/model/verbosity.rs b/rpc/core/src/model/verbosity.rs new file mode 100644 index 0000000000..6d322d44f4 --- /dev/null +++ b/rpc/core/src/model/verbosity.rs @@ -0,0 +1,614 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use workflow_serializer::prelude::*; + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Copy)] +#[borsh(use_discriminant = true)] +#[repr(i32)] +pub enum RpcDataVerbosityLevel { + None = 0, + Low = 1, + High = 2, + Full = 3, +} + +impl Serializer for RpcDataVerbosityLevel { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + + let val: i32 = *self as i32; + writer.write_all(&val.to_le_bytes())?; + + Ok(()) + } +} + +impl Deserializer for RpcDataVerbosityLevel { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + + let mut buf = [0u8; 4]; + reader.read_exact(&mut buf)?; + let val = i32::from_le_bytes(buf); + RpcDataVerbosityLevel::try_from(val) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid RpcDataVerbosityLevel")) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcHeaderVerbosity { + /// Cached hash + pub include_hash: Option, + pub include_version: Option, + pub include_parents_by_level: Option, + pub include_hash_merkle_root: Option, + pub include_accepted_id_merkle_root: Option, + pub include_utxo_commitment: Option, + /// Timestamp is in milliseconds + pub include_timestamp: Option, + pub include_bits: Option, + pub include_nonce: Option, + pub include_daa_score: Option, + pub include_blue_work: Option, + pub include_blue_score: Option, + pub include_pruning_point: Option, +} + +impl Serializer for RpcHeaderVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u16, &1, writer)?; + + store!(Option, &self.include_hash, writer)?; + store!(Option, &self.include_version, writer)?; + store!(Option, &self.include_parents_by_level, writer)?; + store!(Option, &self.include_hash_merkle_root, writer)?; + store!(Option, &self.include_accepted_id_merkle_root, writer)?; + store!(Option, &self.include_utxo_commitment, writer)?; + store!(Option, &self.include_timestamp, writer)?; + store!(Option, &self.include_bits, writer)?; + store!(Option, &self.include_nonce, writer)?; + store!(Option, &self.include_daa_score, writer)?; + store!(Option, &self.include_blue_work, writer)?; + store!(Option, &self.include_blue_score, writer)?; + store!(Option, &self.include_pruning_point, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcHeaderVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u16, reader)?; + + let include_hash = load!(Option, reader)?; + let include_version = load!(Option, reader)?; + let include_parents_by_level = load!(Option, reader)?; + let include_hash_merkle_root = load!(Option, reader)?; + let include_accepted_id_merkle_root = load!(Option, reader)?; + let include_utxo_commitment = load!(Option, reader)?; + let include_timestamp = load!(Option, reader)?; + let include_bits = load!(Option, reader)?; + let include_nonce = load!(Option, reader)?; + let include_daa_score = load!(Option, reader)?; + let include_blue_work = load!(Option, reader)?; + let include_blue_score = load!(Option, reader)?; + let include_pruning_point = load!(Option, reader)?; + + Ok(Self { + include_hash, + include_version, + include_parents_by_level, + include_hash_merkle_root, + include_accepted_id_merkle_root, + include_utxo_commitment, + include_timestamp, + include_bits, + include_nonce, + include_daa_score, + include_blue_work, + include_blue_score, + include_pruning_point, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcUtxoEntryVerboseDataVerbosity { + pub include_script_public_key_type: Option, + pub include_script_public_key_address: Option, +} + +impl RpcUtxoEntryVerboseDataVerbosity { + pub fn new(include_script_public_key_type: Option, include_script_public_key_address: Option) -> Self { + Self { include_script_public_key_type, include_script_public_key_address } + } +} + +impl Serializer for RpcUtxoEntryVerboseDataVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.include_script_public_key_type, writer)?; + store!(Option, &self.include_script_public_key_address, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcUtxoEntryVerboseDataVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let include_script_public_key_type = load!(Option, reader)?; + let include_script_public_key_address = load!(Option, reader)?; + + Ok(Self { include_script_public_key_type, include_script_public_key_address }) + } +} + +// RpcUtxoEntryVerbosity +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcUtxoEntryVerbosity { + pub include_amount: Option, + pub include_script_public_key: Option, + pub include_block_daa_score: Option, + pub include_is_coinbase: Option, + pub verbose_data_verbosity: Option, +} + +impl RpcUtxoEntryVerbosity { + pub fn new( + include_amount: Option, + include_script_public_key: Option, + include_block_daa_score: Option, + include_is_coinbase: Option, + verbose_data_verbosity: Option, + ) -> Self { + Self { include_amount, include_script_public_key, include_block_daa_score, include_is_coinbase, verbose_data_verbosity } + } +} + +impl Serializer for RpcUtxoEntryVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.include_amount, writer)?; + store!(Option, &self.include_script_public_key, writer)?; + store!(Option, &self.include_block_daa_score, writer)?; + store!(Option, &self.include_is_coinbase, writer)?; + serialize!(Option, &self.verbose_data_verbosity, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcUtxoEntryVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + + let include_amount = load!(Option, reader)?; + let include_script_public_key = load!(Option, reader)?; + let include_block_daa_score = load!(Option, reader)?; + let include_is_coinbase = load!(Option, reader)?; + let verbose_data_verbosity = deserialize!(Option, reader)?; + + Ok(Self { include_amount, include_script_public_key, include_block_daa_score, include_is_coinbase, verbose_data_verbosity }) + } +} + +// RpcTransactionInputVerbosity +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcTransactionInputVerbosity { + pub include_previous_outpoint: Option, + pub include_signature_script: Option, + pub include_sequence: Option, + pub include_sig_op_count: Option, + pub verbose_data_verbosity: Option, +} + +impl RpcTransactionInputVerbosity { + pub fn new( + include_previous_outpoint: Option, + include_signature_script: Option, + include_sequence: Option, + include_sig_op_count: Option, + verbose_data_verbosity: Option, + ) -> Self { + Self { include_previous_outpoint, include_signature_script, include_sequence, include_sig_op_count, verbose_data_verbosity } + } +} + +impl Serializer for RpcTransactionInputVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.include_previous_outpoint, writer)?; + store!(Option, &self.include_signature_script, writer)?; + store!(Option, &self.include_sequence, writer)?; + store!(Option, &self.include_sig_op_count, writer)?; + serialize!(Option, &self.verbose_data_verbosity, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionInputVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + + let include_previous_outpoint = load!(Option, reader)?; + let include_signature_script = load!(Option, reader)?; + let include_sequence = load!(Option, reader)?; + let include_sig_op_count = load!(Option, reader)?; + let verbose_data_verbosity = deserialize!(Option, reader)?; + + Ok(Self { + include_previous_outpoint, + include_signature_script, + include_sequence, + include_sig_op_count, + verbose_data_verbosity, + }) + } +} + +// RpcTransactionInputVerboseDataVerbosity +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcTransactionInputVerboseDataVerbosity { + pub utxo_entry_verbosity: Option, +} + +impl RpcTransactionInputVerboseDataVerbosity { + pub fn new(utxo_entry_verbosity: Option) -> Self { + Self { utxo_entry_verbosity } + } +} + +impl Serializer for RpcTransactionInputVerboseDataVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + serialize!(Option, &self.utxo_entry_verbosity, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionInputVerboseDataVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let utxo_entry_verbosity = deserialize!(Option, reader)?; + + Ok(Self { utxo_entry_verbosity }) + } +} + +// RpcTransactionOutputVerbosity + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcTransactionOutputVerbosity { + pub include_amount: Option, + pub include_script_public_key: Option, + pub verbose_data_verbosity: Option, +} + +impl RpcTransactionOutputVerbosity { + pub fn new( + include_amount: Option, + include_script_public_key: Option, + verbose_data_verbosity: Option, + ) -> Self { + Self { include_amount, include_script_public_key, verbose_data_verbosity } + } +} + +impl Serializer for RpcTransactionOutputVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.include_amount, writer)?; + store!(Option, &self.include_script_public_key, writer)?; + serialize!(Option, &self.verbose_data_verbosity, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionOutputVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + + let include_amount = load!(Option, reader)?; + let include_script_public_key = load!(Option, reader)?; + let verbose_data_verbosity = deserialize!(Option, reader)?; + + Ok(Self { include_amount, include_script_public_key, verbose_data_verbosity }) + } +} + +// RpcTransactionOutputVerboseDataVerbosity +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcTransactionOutputVerboseDataVerbosity { + pub include_script_public_key_type: Option, + pub include_script_public_key_address: Option, +} + +impl RpcTransactionOutputVerboseDataVerbosity { + pub fn new(include_script_public_key_type: Option, include_script_public_key_address: Option) -> Self { + Self { include_script_public_key_type, include_script_public_key_address } + } +} + +impl Serializer for RpcTransactionOutputVerboseDataVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.include_script_public_key_type, writer)?; + store!(Option, &self.include_script_public_key_address, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionOutputVerboseDataVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + + let include_script_public_key_type = load!(Option, reader)?; + let include_script_public_key_address = load!(Option, reader)?; + + Ok(Self { include_script_public_key_type, include_script_public_key_address }) + } +} + +// RpcTransactionVerbosity +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcTransactionVerbosity { + pub include_version: Option, + pub input_verbosity: Option, + pub output_verbosity: Option, + pub include_lock_time: Option, + pub include_subnetwork_id: Option, + pub include_gas: Option, + pub include_payload: Option, + pub include_mass: Option, + pub verbose_data_verbosity: Option, +} + +impl RpcTransactionVerbosity { + pub fn new( + include_version: Option, + input_verbosity: Option, + output_verbosity: Option, + include_lock_time: Option, + include_subnetwork_id: Option, + include_gas: Option, + include_payload: Option, + include_mass: Option, + verbose_data_verbosity: Option, + ) -> Self { + Self { + include_version, + input_verbosity, + output_verbosity, + include_lock_time, + include_subnetwork_id, + include_gas, + include_payload, + include_mass, + verbose_data_verbosity, + } + } + + pub fn requires_populated_transaction(&self) -> bool { + self.input_verbosity + .as_ref() + .is_some_and(|active| active.verbose_data_verbosity.as_ref().is_some_and(|active| active.utxo_entry_verbosity.is_some())) + } + + pub fn requires_block_hash(&self) -> bool { + self.verbose_data_verbosity.as_ref().is_some_and(|active| active.include_block_hash.is_some()) + } + + pub fn requires_block_time(&self) -> bool { + self.verbose_data_verbosity.as_ref().is_some_and(|active| active.include_block_time.is_some()) + } +} + +impl Serializer for RpcTransactionVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.include_version, writer)?; + serialize!(Option, &self.input_verbosity, writer)?; + serialize!(Option, &self.output_verbosity, writer)?; + store!(Option, &self.include_lock_time, writer)?; + store!(Option, &self.include_subnetwork_id, writer)?; + store!(Option, &self.include_gas, writer)?; + store!(Option, &self.include_payload, writer)?; + store!(Option, &self.include_mass, writer)?; + serialize!(Option, &self.verbose_data_verbosity, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + + let include_version = load!(Option, reader)?; + let input_verbosity = deserialize!(Option, reader)?; + let output_verbosity = deserialize!(Option, reader)?; + let include_lock_time = load!(Option, reader)?; + let include_subnetwork_id = load!(Option, reader)?; + let include_gas = load!(Option, reader)?; + let include_payload = load!(Option, reader)?; + let include_mass = load!(Option, reader)?; + let verbose_data_verbosity = deserialize!(Option, reader)?; + + Ok(Self { + include_version, + input_verbosity, + output_verbosity, + include_lock_time, + include_subnetwork_id, + include_gas, + include_payload, + include_mass, + verbose_data_verbosity, + }) + } +} + +// RpcTransactionVerboseDataVerbosity +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcTransactionVerboseDataVerbosity { + pub include_transaction_id: Option, + pub include_hash: Option, + pub include_compute_mass: Option, + pub include_block_hash: Option, + pub include_block_time: Option, +} + +impl RpcTransactionVerboseDataVerbosity { + pub fn new( + include_transaction_id: Option, + include_hash: Option, + include_compute_mass: Option, + include_block_hash: Option, + include_block_time: Option, + ) -> Self { + Self { include_transaction_id, include_hash, include_compute_mass, include_block_hash, include_block_time } + } +} + +impl Serializer for RpcTransactionVerboseDataVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.include_transaction_id, writer)?; + store!(Option, &self.include_hash, writer)?; + store!(Option, &self.include_compute_mass, writer)?; + store!(Option, &self.include_block_hash, writer)?; + store!(Option, &self.include_block_time, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcTransactionVerboseDataVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + + let include_transaction_id = load!(Option, reader)?; + let include_hash = load!(Option, reader)?; + let include_compute_mass = load!(Option, reader)?; + let include_block_hash = load!(Option, reader)?; + let include_block_time = load!(Option, reader)?; + + Ok(Self { include_transaction_id, include_hash, include_compute_mass, include_block_hash, include_block_time }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcAcceptanceDataVerbosity { + pub accepting_chain_header_verbosity: Option, + pub mergeset_block_acceptance_data_verbosity: Option, +} + +impl RpcAcceptanceDataVerbosity { + pub fn new( + accepting_chain_header_verbosity: Option, + mergeset_block_acceptance_data_verbosity: Option, + ) -> Self { + Self { accepting_chain_header_verbosity, mergeset_block_acceptance_data_verbosity } + } + + pub fn requires_merged_header(&self, default: bool) -> bool { + self.mergeset_block_acceptance_data_verbosity.as_ref().map_or(default, |active| active.requires_merged_header()) + } + + pub fn requeires_accepted_header(&self, default: bool) -> bool { + self.mergeset_block_acceptance_data_verbosity.as_ref().map_or(default, |active| active.requires_merged_block_hash()) + } + + pub fn requires_accepted_transactions(&self, default: bool) -> bool { + self.mergeset_block_acceptance_data_verbosity + .as_ref() + .map_or(default, |active| active.accepted_transactions_verbosity.is_some()) + } +} + +impl Serializer for RpcAcceptanceDataVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + store!(Option, &self.accepting_chain_header_verbosity, writer)?; + serialize!(Option, &self.mergeset_block_acceptance_data_verbosity, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcAcceptanceDataVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader); + let accepting_chain_header_verbosity = load!(Option, reader)?; + let mergeset_block_acceptance_data_verbosity = deserialize!(Option, reader)?; + + Ok(Self { accepting_chain_header_verbosity, mergeset_block_acceptance_data_verbosity }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RpcMergesetBlockAcceptanceDataVerbosity { + pub merged_header_verbosity: Option, + pub accepted_transactions_verbosity: Option, +} + +impl RpcMergesetBlockAcceptanceDataVerbosity { + pub fn requires_merged_header(&self) -> bool { + self.merged_header_verbosity.is_some() + || self.accepted_transactions_verbosity.as_ref().is_some_and(|active| { + active.verbose_data_verbosity.as_ref().is_some_and(|active| active.include_block_hash.unwrap_or(false)) + }) + } + + pub fn requires_merged_block_hash(&self) -> bool { + self.merged_header_verbosity.as_ref().is_some_and(|active| active.include_hash.unwrap_or(false)) + || self.accepted_transactions_verbosity.as_ref().is_some_and(|active| { + active.verbose_data_verbosity.as_ref().is_some_and(|active| active.include_block_hash.unwrap_or(false)) + }) + } +} + +impl RpcMergesetBlockAcceptanceDataVerbosity { + pub fn new( + merged_header_verbosity: Option, + accepted_transactions_verbosity: Option, + ) -> Self { + Self { merged_header_verbosity, accepted_transactions_verbosity } + } +} + +impl Serializer for RpcMergesetBlockAcceptanceDataVerbosity { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + store!(u8, &1, writer)?; + serialize!(Option, &self.merged_header_verbosity, writer)?; + serialize!(Option, &self.accepted_transactions_verbosity, writer)?; + + Ok(()) + } +} + +impl Deserializer for RpcMergesetBlockAcceptanceDataVerbosity { + fn deserialize(reader: &mut R) -> std::io::Result { + let _version = load!(u8, reader)?; + let merged_header_verbosity = deserialize!(Option, reader)?; + let accepted_transactions_verbosity = deserialize!(Option, reader)?; + + Ok(Self { merged_header_verbosity, accepted_transactions_verbosity }) + } +} diff --git a/rpc/core/src/wasm/convert.rs b/rpc/core/src/wasm/convert.rs index 319f74bf0c..49e62aeadd 100644 --- a/rpc/core/src/wasm/convert.rs +++ b/rpc/core/src/wasm/convert.rs @@ -76,5 +76,54 @@ cfg_if::cfg_if! { } } } + + impl From for RpcOptionalTransactionInput { + fn from(tx_input: TransactionInput) -> Self { + let inner = tx_input.inner(); + RpcOptionalTransactionInput { + previous_outpoint: Some(inner.previous_outpoint.clone().into()), + signature_script: Some(inner.signature_script.clone().unwrap_or_default()), + sequence: Some(inner.sequence), + sig_op_count: Some(inner.sig_op_count), + verbose_data: None, + } + } + } + + impl From for RpcOptionalTransactionOutput { + fn from(output: TransactionOutput) -> Self { + let inner = output.inner(); + RpcOptionalTransactionOutput { value: Some(inner.value), script_public_key: Some(inner.script_public_key.clone()), verbose_data: None } + } + } + + impl From for RpcOptionalTransaction { + fn from(tx: Transaction) -> Self { + RpcOptionalTransaction::from(&tx) + } + } + + impl From<&Transaction> for RpcOptionalTransaction { + + fn from(tx: &Transaction) -> Self { + let inner = tx.inner(); + let inputs: Vec = + inner.inputs.clone().into_iter().map(|input| input.into()).collect::>(); + let outputs: Vec = + inner.outputs.clone().into_iter().map(|output| output.into()).collect::>(); + + RpcOptionalTransaction { + version: Some(inner.version), + inputs, + outputs, + lock_time: Some(inner.lock_time), + subnetwork_id: Some(inner.subnetwork_id.clone()), + gas: Some(inner.gas), + payload: Some(inner.payload.clone()), + mass: Some(inner.mass), + verbose_data: None, + } + } + } } } diff --git a/rpc/core/src/wasm/message.rs b/rpc/core/src/wasm/message.rs index 53ba49fe29..9f1c5095d3 100644 --- a/rpc/core/src/wasm/message.rs +++ b/rpc/core/src/wasm/message.rs @@ -44,6 +44,30 @@ const TS_ACCEPTED_TRANSACTION_IDS: &'static str = r#" } "#; +#[wasm_bindgen(typescript_custom_section)] +const TS_ADDED_ACCEPTANCE_DATA: &'static str = r#" + /** + * Accepted Acceptance Data + * + * @category Node RPC + */ + export interface IChainBlockAddedTransactions { + chainBlockHeader: Header; + acceptedTransactions: Transaction[]; + } +"#; + +// DataVerbosityLevel +#[wasm_bindgen(typescript_custom_section)] +const TS_DATA_VERBOSITY_LEVEL: &'static str = r#" + /** + * Data Verbosity level + * + * @category Node RPC + */ + export type DataVerbosityLevel = "None" | "Low" | "High" | "Full"; +"#; + // --- declare! { @@ -1263,6 +1287,49 @@ try_from! ( args: GetVirtualChainFromBlockResponse, IGetVirtualChainFromBlockRes Ok(to_value(&args)?.into()) }); +declare! { + IGetVirtualChainFromBlockV2Request, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetVirtualChainFromBlockV2Request { + startHash : HexString; + dataVerbosityLevel?: DataVerbosityLevel; + /** + * If passed, this request will only return blocks that have at least minConfirmationCount number of confirmations. Confirmation is counted through the distance from virtual chain tip. + * If not passed, it will be interpreted as 0. + */ + minConfirmationCount?: number; + } + "#, +} + +try_from! ( args: IGetVirtualChainFromBlockV2Request, GetVirtualChainFromBlockV2Request, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetVirtualChainFromBlockV2Response, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetVirtualChainFromBlockV2Response { + removedChainBlockHashes : HexString[]; + addedChainBlockHashes : HexString[]; + chainBlockAcceptedTransactions : IChainBlockAddedTransactions[]; + } + "#, +} + +try_from! ( args: GetVirtualChainFromBlockV2Response, IGetVirtualChainFromBlockV2Response, { + Ok(to_value(&args)?.into()) +}); // --- declare! { diff --git a/rpc/grpc/client/src/lib.rs b/rpc/grpc/client/src/lib.rs index faa2a3522a..5d45d9f5b3 100644 --- a/rpc/grpc/client/src/lib.rs +++ b/rpc/grpc/client/src/lib.rs @@ -277,6 +277,7 @@ impl RpcApi for GrpcClient { route!(get_fee_estimate_experimental_call, GetFeeEstimateExperimental); route!(get_current_block_color_call, GetCurrentBlockColor); route!(get_utxo_return_address_call, GetUtxoReturnAddress); + route!(get_virtual_chain_from_block_v2_call, GetVirtualChainFromBlockV2); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/grpc/core/proto/messages.proto b/rpc/grpc/core/proto/messages.proto index ccb2798b67..af0a91614d 100644 --- a/rpc/grpc/core/proto/messages.proto +++ b/rpc/grpc/core/proto/messages.proto @@ -65,7 +65,8 @@ message KaspadRequest { GetFeeEstimateRequestMessage getFeeEstimateRequest = 1106; GetFeeEstimateExperimentalRequestMessage getFeeEstimateExperimentalRequest = 1108; GetCurrentBlockColorRequestMessage getCurrentBlockColorRequest = 1110; - GetUtxoReturnAddressRequestMessage GetUtxoReturnAddressRequest = 1112; + GetUtxoReturnAddressRequestMessage getUtxoReturnAddressRequest = 1112; + GetVirtualChainFromBlockV2RequestMessage getVirtualChainFromBlockV2Request = 1114; } } @@ -131,7 +132,8 @@ message KaspadResponse { GetFeeEstimateResponseMessage getFeeEstimateResponse = 1107; GetFeeEstimateExperimentalResponseMessage getFeeEstimateExperimentalResponse = 1109; GetCurrentBlockColorResponseMessage getCurrentBlockColorResponse = 1111; - GetUtxoReturnAddressResponseMessage GetUtxoReturnAddressResponse = 1113; + GetUtxoReturnAddressResponseMessage getUtxoReturnAddressResponse = 1113; + GetVirtualChainFromBlockV2ResponseMessage getVirtualChainFromBlockV2Response = 1115; } } diff --git a/rpc/grpc/core/proto/rpc.proto b/rpc/grpc/core/proto/rpc.proto index 50e0c57964..e5a21f23d3 100644 --- a/rpc/grpc/core/proto/rpc.proto +++ b/rpc/grpc/core/proto/rpc.proto @@ -33,8 +33,9 @@ message RpcBlockHeader { uint64 nonce = 8; uint64 daaScore = 9; string blueWork = 10; - string pruningPoint = 14; uint64 blueScore = 13; + string pruningPoint = 14; + string hash = 15; } message RpcBlockLevelParents { @@ -66,6 +67,14 @@ message RpcTransaction { uint64 mass = 10; } +message RpcTransactionVerboseData{ + string transactionId = 1; + string hash = 2; + uint64 computeMass = 4; + string blockHash = 12; + uint64 blockTime = 14; +} + message RpcTransactionInput { RpcOutpoint previousOutpoint = 1; string signatureScript = 2; @@ -74,6 +83,23 @@ message RpcTransactionInput { RpcTransactionInputVerboseData verboseData = 4; } +message RpcTransactionInputVerboseData { + RpcUtxoEntry utxoEntry = 1; // this field is new, if this is null, the utxo cannot be found, or is coinbase. +} + +message RpcUtxoEntry { + uint64 amount = 1; + RpcScriptPublicKey scriptPublicKey = 2; + uint64 blockDaaScore = 3; + bool isCoinbase = 4; + RpcUtxoEntryVerboseData verboseData = 5; +} + +message RpcUtxoEntryVerboseData { + string scriptPublicKeyType = 5; + string scriptPublicKeyAddress = 6; +} + message RpcScriptPublicKey { uint32 version = 1; string scriptPublicKey = 2; @@ -90,29 +116,16 @@ message RpcOutpoint { uint32 index = 2; } -message RpcUtxoEntry { - uint64 amount = 1; - RpcScriptPublicKey scriptPublicKey = 2; - uint64 blockDaaScore = 3; - bool isCoinbase = 4; -} - -message RpcTransactionVerboseData{ - string transactionId = 1; - string hash = 2; - uint64 computeMass = 4; - string blockHash = 12; - uint64 blockTime = 14; -} - -message RpcTransactionInputVerboseData{ -} - message RpcTransactionOutputVerboseData{ string scriptPublicKeyType = 5; string scriptPublicKeyAddress = 6; } +message RpcChainBlockAcceptedTransactions { + RpcBlockHeader chainBlockHeader = 1; + repeated RpcTransaction acceptedTransactions = 2; +} + enum RpcNotifyCommand { NOTIFY_START = 0; NOTIFY_STOP = 1; @@ -557,7 +570,7 @@ message GetUtxosByAddressesResponseMessage { } // GetBalanceByAddressRequest returns the total balance in unspent transactions towards a given address -// +// // This call is only available when this kaspad was started with `--utxoindex` message GetBalanceByAddressRequestMessage { string address = 1; @@ -792,7 +805,7 @@ message ConnectionMetrics { uint32 jsonLiveConnections = 41; uint64 jsonConnectionAttempts = 42; uint64 jsonHandshakeFailures = 43; - + uint32 activePeers = 51; } @@ -815,7 +828,7 @@ message ConsensusMetrics{ uint64 txsCounts = 5; uint64 chainBlockCounts = 6; uint64 massCounts = 7; - + uint64 blockCount = 11; uint64 headerCount = 12; uint64 mempoolSize = 13; @@ -917,7 +930,7 @@ message RpcFeerateBucket { } // Data required for making fee estimates. -// +// // Feerate values represent fee/mass of a transaction in `sompi/gram` units. // Given a feerate value recommendation, calculate the required fee by // taking the transaction mass and multiplying it by feerate: `fee = feerate * mass(tx)` @@ -966,22 +979,73 @@ message GetFeeEstimateExperimentalResponseMessage { RPCError error = 1000; } -message GetCurrentBlockColorRequestMessage { - string hash = 1; +message GetUtxoReturnAddressRequestMessage { + string txid = 1; + uint64 accepting_block_daa_score = 2; } -message GetCurrentBlockColorResponseMessage { - bool blue = 1; +message GetUtxoReturnAddressResponseMessage { + string return_address = 1; + RPCError error = 1000; +} + +// ### messages for vspcn v2 ### + +enum RpcDataVerbosityLevel { + NONE = 0; + LOW = 1; + HIGH = 2; + FULL = 3; +} + +// new base messages for vspcn v2 +message RpcAcceptanceData { + optional RpcBlockHeader acceptingChainHeader = 1; + repeated RpcMergesetBlockAcceptanceData mergesetBlockAcceptanceData = 20; +} + +message RpcMergesetBlockAcceptanceData { + string mergedBlockHash = 2; + repeated RpcTransaction acceptedTransactions = 3; +} + +/// GetVirtualChainFromBlockV2RequestMessage requests the virtual selected +/// parent chain from some startHash to this kaspad's current virtual +/// Note: +/// this call batches the response to: +/// a. the network's `mergeset size limit * 10` amount of added chain blocks, if `RpcAcceptanceDataVerbosity is nullified` +/// b. or `mergeset size limit * 10` amount of merged blocks, if `RpcAcceptanceDataVerbosity is not nullified` +/// TODO: perhaps find new limit based on expected size of the response, via the `RpcAcceptanceDataVerbosity` switches. +/// c. it does not batch the removed chain blocks, only the added ones. +message GetVirtualChainFromBlockV2RequestMessage{ + string startHash = 1; + + /// Note: where fields are switched to false, default values of the type will be used to fill the assosicated response fields. + /// it is therefore the client's responsibility to ensure to ignore the fields in the response. + optional RpcDataVerbosityLevel dataVerbosityLevel = 2; + optional uint64 minConfirmationCount = 3; +} + +message GetVirtualChainFromBlockV2ResponseMessage{ + // The chain blocks that were removed, in high-to-low order + repeated string removedChainBlockHashes = 1; + + // The chain blocks that were added, in low-to-high order + repeated string addedChainBlockHashes = 2; + + // The mergeset data accepted by each block in addedChainBlockHashes. + // Will be filled dependent on the supplied RpcAcceptanceDataVerbosity. + repeated RpcChainBlockAcceptedTransactions chainBlockAcceptedTransactions = 3; RPCError error = 1000; } -message GetUtxoReturnAddressRequestMessage { - string txid = 1; - uint64 accepting_block_daa_score = 2; +message GetCurrentBlockColorRequestMessage { + string hash = 1; } -message GetUtxoReturnAddressResponseMessage { - string return_address = 1; +message GetCurrentBlockColorResponseMessage { + bool blue = 1; + RPCError error = 1000; } diff --git a/rpc/grpc/core/src/convert/acceptance_data.rs b/rpc/grpc/core/src/convert/acceptance_data.rs new file mode 100644 index 0000000000..25828d84e8 --- /dev/null +++ b/rpc/grpc/core/src/convert/acceptance_data.rs @@ -0,0 +1,70 @@ +use crate::protowire::{self}; +use crate::{from, try_from}; +use kaspa_rpc_core::{RpcError, RpcHash, RpcMergesetBlockAcceptanceData}; +use std::str::FromStr; + +// ---------------------------------------------------------------------------- +// rpc_core to protowire +// ---------------------------------------------------------------------------- + +from!(item: &kaspa_rpc_core::RpcAcceptanceData, protowire::RpcAcceptanceData, { + Self { + accepting_chain_header: item.accepting_chain_header.as_ref().map(protowire::RpcBlockHeader::from), + mergeset_block_acceptance_data: item + .mergeset_block_acceptance_data + .iter() + .map(protowire::RpcMergesetBlockAcceptanceData::from) + .collect(), + } +}); + +from!(item: &kaspa_rpc_core::RpcMergesetBlockAcceptanceData, protowire::RpcMergesetBlockAcceptanceData, { + Self { + merged_block_hash: item.merged_block_hash.to_string(), + accepted_transactions: item.accepted_transactions.iter().map(protowire::RpcTransaction::from).collect(), + } +}); + +from!(item: &kaspa_rpc_core::RpcDataVerbosityLevel, protowire::RpcDataVerbosityLevel, { + match item { + kaspa_rpc_core::RpcDataVerbosityLevel::None => protowire::RpcDataVerbosityLevel::None, + kaspa_rpc_core::RpcDataVerbosityLevel::Low => protowire::RpcDataVerbosityLevel::Low, + kaspa_rpc_core::RpcDataVerbosityLevel::High => protowire::RpcDataVerbosityLevel::High, + kaspa_rpc_core::RpcDataVerbosityLevel::Full => protowire::RpcDataVerbosityLevel::Full, + } +}); + +// ---------------------------------------------------------------------------- +// protowire to rpc_core +// ---------------------------------------------------------------------------- + +try_from!(item: &protowire::RpcAcceptanceData, kaspa_rpc_core::RpcAcceptanceData, { + Self { + accepting_chain_header: item + .accepting_chain_header + .as_ref() + .map(kaspa_rpc_core::RpcOptionalHeader::try_from) + .transpose()?, + mergeset_block_acceptance_data: item + .mergeset_block_acceptance_data + .iter() + .map(RpcMergesetBlockAcceptanceData::try_from) + .collect::>()?, + } +}); + +try_from!(item: &protowire::RpcMergesetBlockAcceptanceData, kaspa_rpc_core::RpcMergesetBlockAcceptanceData, { + Self { + merged_block_hash: RpcHash::from_str(&item.merged_block_hash)?, + accepted_transactions: item.accepted_transactions.iter().map(kaspa_rpc_core::RpcOptionalTransaction::try_from).collect::>()?, + } +}); + +try_from!(item: &protowire::RpcDataVerbosityLevel, kaspa_rpc_core::RpcDataVerbosityLevel, { + match item { + protowire::RpcDataVerbosityLevel::None => kaspa_rpc_core::RpcDataVerbosityLevel::None, + protowire::RpcDataVerbosityLevel::Low => kaspa_rpc_core::RpcDataVerbosityLevel::Low, + protowire::RpcDataVerbosityLevel::High => kaspa_rpc_core::RpcDataVerbosityLevel::High, + protowire::RpcDataVerbosityLevel::Full => kaspa_rpc_core::RpcDataVerbosityLevel::Full + } +}); diff --git a/rpc/grpc/core/src/convert/block.rs b/rpc/grpc/core/src/convert/block.rs index 6ab9e37fa0..94db02b429 100644 --- a/rpc/grpc/core/src/convert/block.rs +++ b/rpc/grpc/core/src/convert/block.rs @@ -7,6 +7,14 @@ use std::str::FromStr; // rpc_core to protowire // ---------------------------------------------------------------------------- +from!(item: &kaspa_rpc_core::RpcOptionalBlock, protowire::RpcBlock, { + Self { + header: item.header.as_ref().map(protowire::RpcBlockHeader::from), + transactions: item.transactions.iter().map(protowire::RpcTransaction::from).collect(), + verbose_data: item.verbose_data.as_ref().map(|x| x.into()), + } +}); + from!(item: &kaspa_rpc_core::RpcBlock, protowire::RpcBlock, { Self { header: Some(protowire::RpcBlockHeader::from(&item.header)), @@ -42,19 +50,19 @@ from!(item: &kaspa_rpc_core::RpcBlockVerboseData, protowire::RpcBlockVerboseData // protowire to rpc_core // ---------------------------------------------------------------------------- -try_from!(item: &protowire::RpcBlock, kaspa_rpc_core::RpcBlock, { +try_from!(item: &protowire::RpcBlock, kaspa_rpc_core::RpcOptionalBlock, { Self { header: item .header .as_ref() - .ok_or_else(|| RpcError::MissingRpcFieldError("RpcBlock".to_string(), "header".to_string()))? - .try_into()?, - transactions: item.transactions.iter().map(kaspa_rpc_core::RpcTransaction::try_from).collect::, _>>()?, + .map(kaspa_rpc_core::RpcOptionalHeader::try_from) + .transpose()?, + transactions: item.transactions.iter().map(kaspa_rpc_core::RpcOptionalTransaction::try_from).collect::, _>>()?, verbose_data: item.verbose_data.as_ref().map(kaspa_rpc_core::RpcBlockVerboseData::try_from).transpose()?, } }); -try_from!(item: &protowire::RpcBlock, kaspa_rpc_core::RpcRawBlock, { +try_from!(item: &protowire::RpcBlock, kaspa_rpc_core::RpcBlock, { Self { header: item .header @@ -62,6 +70,14 @@ try_from!(item: &protowire::RpcBlock, kaspa_rpc_core::RpcRawBlock, { .ok_or_else(|| RpcError::MissingRpcFieldError("RpcBlock".to_string(), "header".to_string()))? .try_into()?, transactions: item.transactions.iter().map(kaspa_rpc_core::RpcTransaction::try_from).collect::, _>>()?, + verbose_data: item.verbose_data.as_ref().map(kaspa_rpc_core::RpcBlockVerboseData::try_from).transpose()?, + } +}); + +try_from!(item: &protowire::RpcBlock, kaspa_rpc_core::RpcRawBlock, { + Self { + header: kaspa_rpc_core::RpcRawHeader::try_from(item.header.as_ref().ok_or(RpcError::MissingRpcFieldError("RpcBlock".to_string(), "header".to_string()))?)?, + transactions: item.transactions.iter().map(kaspa_rpc_core::RpcTransaction::try_from).collect::, _>>()?, } }); diff --git a/rpc/grpc/core/src/convert/header.rs b/rpc/grpc/core/src/convert/header.rs index ff03d85bec..f3fcd76447 100644 --- a/rpc/grpc/core/src/convert/header.rs +++ b/rpc/grpc/core/src/convert/header.rs @@ -22,11 +22,35 @@ from!(item: &kaspa_rpc_core::RpcHeader, protowire::RpcBlockHeader, { blue_work: item.blue_work.to_rpc_hex(), blue_score: item.blue_score, pruning_point: item.pruning_point.to_string(), + hash: Default::default(), + } +}); + +from!(item: &kaspa_rpc_core::RpcOptionalHeader, protowire::RpcBlockHeader, { + Self { + hash: item.hash.map(|x| x.to_string()).unwrap_or_default(), + version: item.version.map(|x| x.into()).unwrap_or_default(), + parents: item + .parents_by_level + .iter() + .map(|level| level.as_slice().into()) + .collect(), + hash_merkle_root: item.hash_merkle_root.map(|x| x.to_string()).unwrap_or_default(), + accepted_id_merkle_root: item.accepted_id_merkle_root.map(|x| x.to_string()).unwrap_or_default(), + utxo_commitment: item.utxo_commitment.map(|x| x.to_string()).unwrap_or_default(), + timestamp: item.timestamp.map(|x| x.try_into().expect("timestamp is always convertible to i64")).unwrap_or_default(), + bits: item.bits.unwrap_or_default(), + nonce: item.nonce.unwrap_or_default(), + daa_score: item.daa_score.unwrap_or_default(), + blue_work: item.blue_work.map(|x| x.to_rpc_hex()).unwrap_or_default(), + blue_score: item.blue_score.unwrap_or_default(), + pruning_point: item.pruning_point.map(|x| x.to_string()).unwrap_or_default(), } }); from!(item: &kaspa_rpc_core::RpcRawHeader, protowire::RpcBlockHeader, { Self { + hash: Default::default(), // We don't include the hash for the raw header version: item.version.into(), parents: item.parents_by_level.iter().map(|x| x.as_slice().into()).collect(), hash_merkle_root: item.hash_merkle_root.to_string(), @@ -48,6 +72,23 @@ from!(item: &[RpcHash], protowire::RpcBlockLevelParents, { Self { parent_hashes: // protowire to rpc_core // ---------------------------------------------------------------------------- +try_from!(item: &protowire::RpcBlockHeader, kaspa_rpc_core::RpcRawHeader, { + Self { + version: item.version.try_into()?, + parents_by_level: item.parents.iter().map(Vec::::try_from).collect::>>>()?, + hash_merkle_root: RpcHash::from_str(&item.hash_merkle_root)?, + accepted_id_merkle_root: RpcHash::from_str(&item.accepted_id_merkle_root)?, + utxo_commitment: RpcHash::from_str(&item.utxo_commitment)?, + timestamp: item.timestamp.try_into()?, + bits: item.bits, + nonce: item.nonce, + daa_score: item.daa_score, + blue_work: kaspa_rpc_core::RpcBlueWorkType::from_rpc_hex(&item.blue_work)?, + blue_score: item.blue_score, + pruning_point: RpcHash::from_str(&item.pruning_point)?, + } +}); + try_from!(item: &protowire::RpcBlockHeader, kaspa_rpc_core::RpcHeader, { // We re-hash the block to remain as most trustless as possible let header = Header::new_finalized( @@ -65,25 +106,30 @@ try_from!(item: &protowire::RpcBlockHeader, kaspa_rpc_core::RpcHeader, { RpcHash::from_str(&item.pruning_point)?, ); - header.into() -}); + kaspa_rpc_core::RpcHeader::from(header) +} +); -try_from!(item: &protowire::RpcBlockHeader, kaspa_rpc_core::RpcRawHeader, { - Self { - version: item.version.try_into()?, - parents_by_level: item.parents.iter().map(Vec::::try_from).collect::>>>()?, - hash_merkle_root: RpcHash::from_str(&item.hash_merkle_root)?, - accepted_id_merkle_root: RpcHash::from_str(&item.accepted_id_merkle_root)?, - utxo_commitment: RpcHash::from_str(&item.utxo_commitment)?, - timestamp: item.timestamp.try_into()?, - bits: item.bits, - nonce: item.nonce, - daa_score: item.daa_score, - blue_work: kaspa_rpc_core::RpcBlueWorkType::from_rpc_hex(&item.blue_work)?, - blue_score: item.blue_score, - pruning_point: RpcHash::from_str(&item.pruning_point)?, - } -}); +try_from!(item: &protowire::RpcBlockHeader, kaspa_rpc_core::RpcOptionalHeader, { + // We re-hash the block to remain as most trustless as possible + let header = Header::new_finalized( + item.version.try_into()?, + item.parents.iter().map(Vec::::try_from).collect::>>>()?.try_into()?, + RpcHash::from_str(&item.hash_merkle_root)?, + RpcHash::from_str(&item.accepted_id_merkle_root)?, + RpcHash::from_str(&item.utxo_commitment)?, + item.timestamp.try_into()?, + item.bits, + item.nonce, + item.daa_score, + kaspa_rpc_core::RpcBlueWorkType::from_rpc_hex(&item.blue_work)?, + item.blue_score, + RpcHash::from_str(&item.pruning_point)?, + ); + + kaspa_rpc_core::RpcOptionalHeader::from(header) +} +); try_from!(item: &protowire::RpcBlockLevelParents, Vec, { item.parent_hashes.iter().map(|x| RpcHash::from_str(x)).collect::, _>>()? diff --git a/rpc/grpc/core/src/convert/kaspad.rs b/rpc/grpc/core/src/convert/kaspad.rs index 7243fd401a..3d1d51a980 100644 --- a/rpc/grpc/core/src/convert/kaspad.rs +++ b/rpc/grpc/core/src/convert/kaspad.rs @@ -64,6 +64,7 @@ pub mod kaspad_request_convert { impl_into_kaspad_request!(GetFeeEstimateExperimental); impl_into_kaspad_request!(GetCurrentBlockColor); impl_into_kaspad_request!(GetUtxoReturnAddress); + impl_into_kaspad_request!(GetVirtualChainFromBlockV2); impl_into_kaspad_request!(NotifyBlockAdded); impl_into_kaspad_request!(NotifyNewBlockTemplate); @@ -202,6 +203,7 @@ pub mod kaspad_response_convert { impl_into_kaspad_response!(GetFeeEstimateExperimental); impl_into_kaspad_response!(GetCurrentBlockColor); impl_into_kaspad_response!(GetUtxoReturnAddress); + impl_into_kaspad_response!(GetVirtualChainFromBlockV2); impl_into_kaspad_notify_response!(NotifyBlockAdded); impl_into_kaspad_notify_response!(NotifyNewBlockTemplate); diff --git a/rpc/grpc/core/src/convert/message.rs b/rpc/grpc/core/src/convert/message.rs index 254710b5ff..2c039758c1 100644 --- a/rpc/grpc/core/src/convert/message.rs +++ b/rpc/grpc/core/src/convert/message.rs @@ -24,11 +24,11 @@ use kaspa_consensus_core::{network::NetworkId, Hash}; use kaspa_core::debug; use kaspa_notify::subscription::Command; use kaspa_rpc_core::{ - RpcContextualPeerAddress, RpcError, RpcExtraData, RpcHash, RpcIpAddress, RpcNetworkType, RpcPeerAddress, RpcResult, - SubmitBlockRejectReason, SubmitBlockReport, + RpcContextualPeerAddress, RpcDataVerbosityLevel, RpcError, RpcExtraData, RpcHash, RpcIpAddress, RpcNetworkType, RpcPeerAddress, + RpcResult, SubmitBlockRejectReason, SubmitBlockReport, }; use kaspa_utils::hex::*; -use std::str::FromStr; +use std::{str::FromStr, sync::Arc}; macro_rules! from { // Response capture @@ -264,8 +264,6 @@ from!(item: RpcResult<&kaspa_rpc_core::GetSubnetworkResponse>, protowire::GetSub Self { gas_limit: item.gas_limit, error: None } }); -// ~~~ - from!(item: &kaspa_rpc_core::GetVirtualChainFromBlockRequest, protowire::GetVirtualChainFromBlockRequestMessage, { Self { start_hash: item.start_hash.to_string(), include_accepted_transaction_ids: item.include_accepted_transaction_ids, min_confirmation_count: item.min_confirmation_count } }); @@ -518,6 +516,23 @@ from!(item: RpcResult<&kaspa_rpc_core::GetSyncStatusResponse>, protowire::GetSyn } }); +from!(item: &kaspa_rpc_core::GetVirtualChainFromBlockV2Request, protowire::GetVirtualChainFromBlockV2RequestMessage, { + Self { + start_hash: item.start_hash.to_string(), + data_verbosity_level: item.data_verbosity_level.map(|v| v as i32), + min_confirmation_count: item.min_confirmation_count + } +}); + +from!(item: RpcResult<&kaspa_rpc_core::GetVirtualChainFromBlockV2Response>, protowire::GetVirtualChainFromBlockV2ResponseMessage, { + Self { + removed_chain_block_hashes: item.removed_chain_block_hashes.iter().map(|x| x.to_string()).collect(), + added_chain_block_hashes: item.added_chain_block_hashes.iter().map(|x| x.to_string()).collect(), + chain_block_accepted_transactions: item.chain_block_accepted_transactions.iter().map(|x| x.into()).collect(), + error: None, + } +}); + from!(item: &kaspa_rpc_core::NotifyUtxosChangedRequest, protowire::NotifyUtxosChangedRequestMessage, { Self { addresses: item.addresses.iter().map(|x| x.into()).collect(), command: item.command.into() } }); @@ -771,6 +786,21 @@ try_from!(item: &protowire::GetVirtualChainFromBlockResponseMessage, RpcResult, { + Self { + removed_chain_block_hashes: Arc::new(item.removed_chain_block_hashes.iter().map(|x| RpcHash::from_str(x)).collect::, _>>()?), + added_chain_block_hashes: Arc::new(item.added_chain_block_hashes.iter().map(|x| RpcHash::from_str(x)).collect::, _>>()?), + chain_block_accepted_transactions: Arc::new(item.chain_block_accepted_transactions.iter().map(|x| x.try_into()).collect::, _>>()?), + } +}); + try_from!(item: &protowire::GetBlocksRequestMessage, kaspa_rpc_core::GetBlocksRequest, { Self { low_hash: if item.low_hash.is_empty() { None } else { Some(RpcHash::from_str(&item.low_hash)?) }, diff --git a/rpc/grpc/core/src/convert/mod.rs b/rpc/grpc/core/src/convert/mod.rs index d4948f57dc..d5d42711f8 100644 --- a/rpc/grpc/core/src/convert/mod.rs +++ b/rpc/grpc/core/src/convert/mod.rs @@ -1,3 +1,4 @@ +pub mod acceptance_data; pub mod address; pub mod block; pub mod error; diff --git a/rpc/grpc/core/src/convert/notification.rs b/rpc/grpc/core/src/convert/notification.rs index 2f2273af1c..28b42d4c3d 100644 --- a/rpc/grpc/core/src/convert/notification.rs +++ b/rpc/grpc/core/src/convert/notification.rs @@ -33,7 +33,7 @@ from!(item: &kaspa_rpc_core::Notification, Payload, { Notification::VirtualDaaScoreChanged(ref notification) => Payload::VirtualDaaScoreChangedNotification(notification.into()), Notification::PruningPointUtxoSetOverride(ref notification) => { Payload::PruningPointUtxoSetOverrideNotification(notification.into()) - } + }, } }); diff --git a/rpc/grpc/core/src/convert/tx.rs b/rpc/grpc/core/src/convert/tx.rs index 7a75a0255e..32f2034b09 100644 --- a/rpc/grpc/core/src/convert/tx.rs +++ b/rpc/grpc/core/src/convert/tx.rs @@ -1,6 +1,8 @@ -use crate::protowire; +use crate::protowire::{self}; use crate::{from, try_from}; -use kaspa_rpc_core::{FromRpcHex, RpcError, RpcHash, RpcResult, RpcScriptVec, ToRpcHex}; +use kaspa_rpc_core::{ + FromRpcHex, RpcAddress, RpcError, RpcHash, RpcOptionalHeader, RpcResult, RpcScriptClass, RpcScriptVec, ToRpcHex, +}; use std::str::FromStr; // ---------------------------------------------------------------------------- @@ -21,6 +23,20 @@ from!(item: &kaspa_rpc_core::RpcTransaction, protowire::RpcTransaction, { } }); +from!(item: &kaspa_rpc_core::RpcOptionalTransaction, protowire::RpcTransaction, { + Self { + version: item.version.unwrap_or_default().into(), + inputs: item.inputs.iter().map(protowire::RpcTransactionInput::from).collect(), + outputs: item.outputs.iter().map(protowire::RpcTransactionOutput::from).collect(), + lock_time: item.lock_time.unwrap_or_default(), + subnetwork_id: item.subnetwork_id.as_ref().map(|x| x.to_string()).unwrap_or_default(), + gas: item.gas.unwrap_or_default(), + payload: item.payload.as_ref().map(|x| x.to_rpc_hex()).unwrap_or_default(), + mass: item.mass.unwrap_or_default(), + verbose_data: item.verbose_data.as_ref().map(|x| x.into()), + } +}); + from!(item: &kaspa_rpc_core::RpcTransactionInput, protowire::RpcTransactionInput, { Self { previous_outpoint: Some((&item.previous_outpoint).into()), @@ -31,6 +47,16 @@ from!(item: &kaspa_rpc_core::RpcTransactionInput, protowire::RpcTransactionInput } }); +from!(item: &kaspa_rpc_core::RpcOptionalTransactionInput, protowire::RpcTransactionInput, { + Self { + previous_outpoint: item.previous_outpoint.as_ref().map(protowire::RpcOutpoint::from), + signature_script: item.signature_script.as_ref().map(|x| x.to_rpc_hex()).unwrap_or_default(), + sequence: item.sequence.unwrap_or_default(), + sig_op_count: item.sig_op_count.map(|x| x.into()).unwrap_or_default(), + verbose_data: item.verbose_data.as_ref().map(protowire::RpcTransactionInputVerboseData::from), + } +}); + from!(item: &kaspa_rpc_core::RpcTransactionOutput, protowire::RpcTransactionOutput, { Self { amount: item.value, @@ -39,16 +65,53 @@ from!(item: &kaspa_rpc_core::RpcTransactionOutput, protowire::RpcTransactionOutp } }); +from!(item: &kaspa_rpc_core::RpcOptionalTransactionOutput, protowire::RpcTransactionOutput, { + Self { + amount: item.value.unwrap_or_default(), + script_public_key: item.script_public_key.as_ref().map(|x| x.into()), + verbose_data: item.verbose_data.as_ref().map(|x| x.into()), + } +}); + from!(item: &kaspa_rpc_core::RpcTransactionOutpoint, protowire::RpcOutpoint, { Self { transaction_id: item.transaction_id.to_string(), index: item.index } }); +from!(item: &kaspa_rpc_core::RpcOptionalTransactionOutpoint, protowire::RpcOutpoint, { + Self { transaction_id: item.transaction_id.as_ref().map(|x| x.to_string()).unwrap_or_default(), index: item.index.unwrap_or_default() } +}); + from!(item: &kaspa_rpc_core::RpcUtxoEntry, protowire::RpcUtxoEntry, { Self { amount: item.amount, script_public_key: Some((&item.script_public_key).into()), block_daa_score: item.block_daa_score, is_coinbase: item.is_coinbase, + verbose_data: None, + } +}); + +from!(item: &kaspa_rpc_core::RpcOptionalUtxoEntry, protowire::RpcUtxoEntry, { + Self { + amount: item.amount.unwrap_or_default(), + script_public_key: item.script_public_key.as_ref().map(|x| x.into()), + block_daa_score: item.block_daa_score.unwrap_or_default(), + is_coinbase: item.is_coinbase.unwrap_or_default(), + verbose_data: item.verbose_data.as_ref().map(|x| x.into()), + } +}); + +from!(item: &kaspa_rpc_core::RpcOptionalUtxoEntryVerboseData, protowire::RpcUtxoEntryVerboseData, { + Self { + script_public_key_type: item.script_public_key_type.as_ref().map(|x| x.to_string()).unwrap_or_default(), + script_public_key_address: item.script_public_key_address.as_ref().map(|x| x.to_string()).unwrap_or_default(), + } +}); + +from!(item: &kaspa_rpc_core::RpcChainBlockAcceptedTransactions, protowire::RpcChainBlockAcceptedTransactions, { + Self { + chain_block_header: Some((&item.chain_block_header).into()), + accepted_transactions: item.accepted_transactions.iter().map(protowire::RpcTransaction::from).collect(), } }); @@ -56,6 +119,16 @@ from!(item: &kaspa_rpc_core::RpcScriptPublicKey, protowire::RpcScriptPublicKey, Self { version: item.version().into(), script_public_key: item.script().to_rpc_hex() } }); +from!(item: &kaspa_rpc_core::RpcOptionalTransactionVerboseData, protowire::RpcTransactionVerboseData, { + Self { + transaction_id: item.transaction_id.map(|v| v.to_string()).unwrap_or_default(), + hash: item.hash.map(|v| v.to_string()).unwrap_or_default(), + compute_mass: item.compute_mass.unwrap_or_default(), + block_hash: item.block_hash.map(|v| v.to_string()).unwrap_or_default(), + block_time: item.block_time.unwrap_or_default(), + } +}); + from!(item: &kaspa_rpc_core::RpcTransactionVerboseData, protowire::RpcTransactionVerboseData, { Self { transaction_id: item.transaction_id.to_string(), @@ -66,7 +139,17 @@ from!(item: &kaspa_rpc_core::RpcTransactionVerboseData, protowire::RpcTransactio } }); -from!(&kaspa_rpc_core::RpcTransactionInputVerboseData, protowire::RpcTransactionInputVerboseData); +from!(item: &kaspa_rpc_core::RpcOptionalTransactionInputVerboseData, protowire::RpcTransactionInputVerboseData, { + Self { + utxo_entry: item.utxo_entry.as_ref().map(|x| x.into()), + } +}); + +from!(_item: &kaspa_rpc_core::RpcTransactionInputVerboseData, protowire::RpcTransactionInputVerboseData, { + Self { + utxo_entry: None, + } +}); from!(item: &kaspa_rpc_core::RpcTransactionOutputVerboseData, protowire::RpcTransactionOutputVerboseData, { Self { @@ -75,6 +158,13 @@ from!(item: &kaspa_rpc_core::RpcTransactionOutputVerboseData, protowire::RpcTran } }); +from!(item: &kaspa_rpc_core::RpcOptionalTransactionOutputVerboseData, protowire::RpcTransactionOutputVerboseData, { +Self { + script_public_key_type: item.script_public_key_type.as_ref().map(|x| x.to_string()).unwrap_or_default(), + script_public_key_address: item.script_public_key_address.as_ref().map(|x| x.to_string()).unwrap_or_default(), + } +}); + from!(item: &kaspa_rpc_core::RpcAcceptedTransactionIds, protowire::RpcAcceptedTransactionIds, { Self { accepting_block_hash: item.accepting_block_hash.to_string(), @@ -116,6 +206,44 @@ try_from!(item: &protowire::RpcTransaction, kaspa_rpc_core::RpcTransaction, { } }); +try_from!(item: &protowire::RpcTransaction, kaspa_rpc_core::RpcOptionalTransaction, { + Self { + version: Some(item.version.try_into()?), + inputs: item + .inputs + .iter() + .map(kaspa_rpc_core::RpcOptionalTransactionInput::try_from) + .collect::>>()?, + outputs: item + .outputs + .iter() + .map(kaspa_rpc_core::RpcOptionalTransactionOutput::try_from) + .collect::>>()?, + lock_time: Some(item.lock_time), + subnetwork_id: Some(kaspa_rpc_core::RpcSubnetworkId::from_str(&item + .subnetwork_id)?), + gas: Some(item.gas), + payload: Some(Vec::from_rpc_hex(&item.payload)?), + mass: Some(item.mass), + verbose_data: item.verbose_data.as_ref().map(kaspa_rpc_core::RpcOptionalTransactionVerboseData::try_from).transpose()?, + } +}); + +try_from!(item: &protowire::RpcTransactionInput, kaspa_rpc_core::RpcOptionalTransactionInput, { + Self { + previous_outpoint: item + .previous_outpoint + .as_ref() + .map(kaspa_rpc_core::RpcOptionalTransactionOutpoint::try_from) + .transpose()?, + signature_script: Some(Vec::from_rpc_hex(&item + .signature_script)?), + sequence: Some(item.sequence), + sig_op_count: Some(item.sig_op_count.try_into()?), + verbose_data: item.verbose_data.as_ref().map(kaspa_rpc_core::RpcOptionalTransactionInputVerboseData::try_from).transpose()?, + } +}); + try_from!(item: &protowire::RpcTransactionInput, kaspa_rpc_core::RpcTransactionInput, { Self { previous_outpoint: item @@ -142,6 +270,25 @@ try_from!(item: &protowire::RpcTransactionOutput, kaspa_rpc_core::RpcTransaction } }); +try_from!(item: &protowire::RpcTransactionOutput, kaspa_rpc_core::RpcOptionalTransactionOutput, { + Self { + value: Some(item.amount), + script_public_key: item + .script_public_key + .as_ref() + .map(kaspa_rpc_core::RpcScriptPublicKey::try_from) + .transpose()?, + verbose_data: item.verbose_data.as_ref().map(kaspa_rpc_core::RpcOptionalTransactionOutputVerboseData::try_from).transpose()?, + } +}); + +try_from!(item: &protowire::RpcOutpoint, kaspa_rpc_core::RpcOptionalTransactionOutpoint, { + Self { + transaction_id: Some(RpcHash::from_str(&item.transaction_id)?), + index: Some(item.index), + } +}); + try_from!(item: &protowire::RpcOutpoint, kaspa_rpc_core::RpcTransactionOutpoint, { Self { transaction_id: RpcHash::from_str(&item.transaction_id)?, index: item.index } }); @@ -159,6 +306,27 @@ try_from!(item: &protowire::RpcUtxoEntry, kaspa_rpc_core::RpcUtxoEntry, { } }); +try_from!(item: &protowire::RpcUtxoEntry, kaspa_rpc_core::RpcOptionalUtxoEntry, { + Self { + amount: Some(item.amount), + script_public_key: item + .script_public_key + .as_ref() + .map(|x| x.try_into()) + .transpose()?, + block_daa_score: Some(item.block_daa_score), + is_coinbase: Some(item.is_coinbase), + verbose_data: item.verbose_data.as_ref().map(kaspa_rpc_core::RpcOptionalUtxoEntryVerboseData::try_from).transpose()?, + } +}); + +try_from!(item: &protowire::RpcUtxoEntryVerboseData, kaspa_rpc_core::RpcOptionalUtxoEntryVerboseData, { + Self { + script_public_key_type: Some(RpcScriptClass::from_str(&item.script_public_key_type)?), + script_public_key_address: Some(RpcAddress::try_from(item.script_public_key_address.as_ref())?), + } +}); + try_from!(item: &protowire::RpcScriptPublicKey, kaspa_rpc_core::RpcScriptPublicKey, { Self::new(u16::try_from(item.version)?, RpcScriptVec::from_rpc_hex(item.script_public_key.as_str())?) }); @@ -173,8 +341,35 @@ try_from!(item: &protowire::RpcTransactionVerboseData, kaspa_rpc_core::RpcTransa } }); +try_from!(item: &protowire::RpcTransactionVerboseData, kaspa_rpc_core::RpcOptionalTransactionVerboseData, { + Self { + transaction_id: Some(RpcHash::from_str(item.transaction_id.as_ref())?), + hash: Some(RpcHash::from_str(item.hash.as_ref())?), + compute_mass: Some(item.compute_mass), + block_hash: if item.block_hash.is_empty() { + None + } else { + Some(RpcHash::from_str(item.block_hash.as_ref())?) + }, + block_time: Some(item.block_time), + } +}); + try_from!(&protowire::RpcTransactionInputVerboseData, kaspa_rpc_core::RpcTransactionInputVerboseData); +try_from!(item: &protowire::RpcTransactionInputVerboseData, kaspa_rpc_core::RpcOptionalTransactionInputVerboseData, { + Self { + utxo_entry: item.utxo_entry.as_ref().map(kaspa_rpc_core::RpcOptionalUtxoEntry::try_from).transpose()?, + } +}); + +try_from!(item: &protowire::RpcTransactionOutputVerboseData, kaspa_rpc_core::RpcOptionalTransactionOutputVerboseData, { + Self { + script_public_key_type: Some(RpcScriptClass::from_str(item.script_public_key_type.as_ref())?), + script_public_key_address: Some(RpcAddress::try_from(item.script_public_key_address.as_ref())?), + } +}); + try_from!(item: &protowire::RpcTransactionOutputVerboseData, kaspa_rpc_core::RpcTransactionOutputVerboseData, { Self { script_public_key_type: item.script_public_key_type.as_str().try_into()?, @@ -189,6 +384,13 @@ try_from!(item: &protowire::RpcAcceptedTransactionIds, kaspa_rpc_core::RpcAccept } }); +try_from!(item: &protowire::RpcChainBlockAcceptedTransactions, kaspa_rpc_core::RpcChainBlockAcceptedTransactions, { + Self { + chain_block_header: RpcOptionalHeader::try_from(item.chain_block_header.as_ref().ok_or_else(|| RpcError::MissingRpcFieldError("RpcChainBlockAcceptedTransactions".to_string(), "chain_block_header".to_string()))?)?, + accepted_transactions: item.accepted_transactions.iter().map(kaspa_rpc_core::RpcOptionalTransaction::try_from).collect::>()?, + } +}); + try_from!(item: &protowire::RpcUtxosByAddressesEntry, kaspa_rpc_core::RpcUtxosByAddressesEntry, { let address = if item.address.is_empty() { None } else { Some(item.address.as_str().try_into()?) }; Self { diff --git a/rpc/grpc/core/src/ops.rs b/rpc/grpc/core/src/ops.rs index 223774c74c..923d9511a1 100644 --- a/rpc/grpc/core/src/ops.rs +++ b/rpc/grpc/core/src/ops.rs @@ -88,6 +88,7 @@ pub enum KaspadPayloadOps { GetFeeEstimateExperimental, GetCurrentBlockColor, GetUtxoReturnAddress, + GetVirtualChainFromBlockV2, // Subscription commands for starting/stopping notifications NotifyBlockAdded, diff --git a/rpc/grpc/server/src/request_handler/factory.rs b/rpc/grpc/server/src/request_handler/factory.rs index 9fec86e476..002b6bc603 100644 --- a/rpc/grpc/server/src/request_handler/factory.rs +++ b/rpc/grpc/server/src/request_handler/factory.rs @@ -82,6 +82,7 @@ impl Factory { GetFeeEstimateExperimental, GetCurrentBlockColor, GetUtxoReturnAddress, + GetVirtualChainFromBlockV2, NotifyBlockAdded, NotifyNewBlockTemplate, NotifyFinalityConflict, diff --git a/rpc/grpc/server/src/tests/rpc_core_mock.rs b/rpc/grpc/server/src/tests/rpc_core_mock.rs index ada801850e..27d443e477 100644 --- a/rpc/grpc/server/src/tests/rpc_core_mock.rs +++ b/rpc/grpc/server/src/tests/rpc_core_mock.rs @@ -370,6 +370,14 @@ impl RpcApi for RpcCoreMock { Err(RpcError::NotImplemented) } + async fn get_virtual_chain_from_block_v2_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetVirtualChainFromBlockV2Request, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/macros/src/handler.rs b/rpc/macros/src/handler.rs index 165c4e0ebc..98522a6dea 100644 --- a/rpc/macros/src/handler.rs +++ b/rpc/macros/src/handler.rs @@ -36,9 +36,13 @@ impl Handler { _ => (handler.to_token_stream().to_string(), vec![]), }; //let name = handler.to_token_stream().to_string(); - let fn_call = Ident::new(&format!("{}_call", name.to_case(Case::Snake)), Span::call_site()); - let fn_with_suffix = fn_suffix.map(|suffix| Ident::new(&format!("{}_{suffix}", name.to_case(Case::Snake)), Span::call_site())); - let fn_no_suffix = Ident::new(&name.to_case(Case::Snake), Span::call_site()); + + // replace _v_2 with _v2 + let snake_case_name = name.to_case(Case::Snake).replacen("_v_", "_v", 1); + + let fn_call = Ident::new(&format!("{}_call", snake_case_name.clone()), Span::call_site()); + let fn_with_suffix = fn_suffix.map(|suffix| Ident::new(&format!("{}_{suffix}", snake_case_name.clone()), Span::call_site())); + let fn_no_suffix = Ident::new(&snake_case_name, Span::call_site()); let fn_camel = Ident::new(&name.to_case(Case::Camel), Span::call_site()); let request_type = Ident::new(&format!("{name}Request"), Span::call_site()); let response_type = Ident::new(&format!("{name}Response"), Span::call_site()); diff --git a/rpc/service/src/converter/consensus.rs b/rpc/service/src/converter/consensus.rs index 262e25aef8..76b4618ce9 100644 --- a/rpc/service/src/converter/consensus.rs +++ b/rpc/service/src/converter/consensus.rs @@ -1,26 +1,36 @@ use async_trait::async_trait; -use kaspa_addresses::Address; +use kaspa_addresses::{Address, AddressError}; use kaspa_consensus_core::{ + acceptance_data::{AcceptanceData, MergesetBlockAcceptanceData}, block::Block, config::Config, hashing::tx::hash, header::Header, - tx::{MutableTransaction, Transaction, TransactionId, TransactionInput, TransactionOutput}, + tx::{ + MutableTransaction, SignableTransaction, Transaction, TransactionId, TransactionInput, TransactionOutput, + TransactionQueryResult, TransactionType, UtxoEntry, + }, ChainPath, }; use kaspa_consensus_notify::notification::{self as consensus_notify, Notification as ConsensusNotification}; use kaspa_consensusmanager::{ConsensusManager, ConsensusProxy}; +use kaspa_hashes::Hash; use kaspa_math::Uint256; use kaspa_mining::model::{owner_txs::OwnerTransactions, TransactionIdSet}; use kaspa_notify::converter::Converter; use kaspa_rpc_core::{ - BlockAddedNotification, Notification, RpcAcceptedTransactionIds, RpcBlock, RpcBlockVerboseData, RpcHash, RpcMempoolEntry, - RpcMempoolEntryByAddress, RpcResult, RpcTransaction, RpcTransactionInput, RpcTransactionOutput, RpcTransactionOutputVerboseData, - RpcTransactionVerboseData, + BlockAddedNotification, Notification, RpcAcceptanceData, RpcAcceptanceDataVerbosity, RpcAcceptedTransactionIds, RpcBlock, + RpcBlockVerboseData, RpcError, RpcHash, RpcHeaderVerbosity, RpcMempoolEntry, RpcMempoolEntryByAddress, + RpcMergesetBlockAcceptanceData, RpcMergesetBlockAcceptanceDataVerbosity, RpcOptionalHeader, RpcOptionalTransaction, + RpcOptionalTransactionInput, RpcOptionalTransactionInputVerboseData, RpcOptionalTransactionOutput, + RpcOptionalTransactionOutputVerboseData, RpcOptionalTransactionVerboseData, RpcOptionalUtxoEntry, RpcOptionalUtxoEntryVerboseData, + RpcResult, RpcTransaction, RpcTransactionInput, RpcTransactionInputVerboseDataVerbosity, RpcTransactionInputVerbosity, + RpcTransactionOutput, RpcTransactionOutputVerboseData, RpcTransactionOutputVerboseDataVerbosity, RpcTransactionOutputVerbosity, + RpcTransactionVerboseData, RpcTransactionVerboseDataVerbosity, RpcTransactionVerbosity, RpcUtxoEntryVerboseDataVerbosity, + RpcUtxoEntryVerbosity, }; use kaspa_txscript::{extract_script_pub_key_address, script_class::ScriptClass}; use std::{collections::HashMap, fmt::Debug, sync::Arc}; - /// Conversion of consensus_core to rpc_core structures pub struct ConsensusConverter { consensus_manager: Arc, @@ -178,6 +188,479 @@ impl ConsensusConverter { }) .collect()) } + + fn adapt_header_to_header_with_verbosity( + &self, + verbosity: &RpcHeaderVerbosity, + header: &Arc
, + ) -> RpcResult { + Ok(RpcOptionalHeader { + hash: if verbosity.include_hash.unwrap_or(false) { Some(header.hash) } else { Default::default() }, + version: if verbosity.include_version.unwrap_or(false) { Some(header.version) } else { Default::default() }, + parents_by_level: if verbosity.include_parents_by_level.unwrap_or(false) { + header.parents_by_level.to_owned().into() + } else { + Default::default() + }, + hash_merkle_root: if verbosity.include_hash_merkle_root.unwrap_or(false) { + Some(header.hash_merkle_root) + } else { + Default::default() + }, + accepted_id_merkle_root: if verbosity.include_accepted_id_merkle_root.unwrap_or(false) { + Some(header.accepted_id_merkle_root) + } else { + Default::default() + }, + utxo_commitment: if verbosity.include_utxo_commitment.unwrap_or(false) { + Some(header.utxo_commitment) + } else { + Default::default() + }, + timestamp: if verbosity.include_timestamp.unwrap_or(false) { Some(header.timestamp) } else { Default::default() }, + bits: if verbosity.include_bits.unwrap_or(false) { Some(header.bits) } else { Default::default() }, + nonce: if verbosity.include_nonce.unwrap_or(false) { Some(header.nonce) } else { Default::default() }, + daa_score: if verbosity.include_daa_score.unwrap_or(false) { Some(header.daa_score) } else { Default::default() }, + blue_work: if verbosity.include_blue_work.unwrap_or(false) { Some(header.blue_work) } else { Default::default() }, + blue_score: if verbosity.include_blue_score.unwrap_or(false) { Some(header.blue_score) } else { Default::default() }, + pruning_point: if verbosity.include_pruning_point.unwrap_or(false) { + Some(header.pruning_point) + } else { + Default::default() + }, + }) + } + + fn convert_utxo_entry_with_verbosity( + &self, + utxo: UtxoEntry, + verbosity: &RpcUtxoEntryVerbosity, + ) -> RpcResult { + Ok(RpcOptionalUtxoEntry { + amount: if verbosity.include_amount.unwrap_or(false) { Some(utxo.amount) } else { Default::default() }, + script_public_key: if verbosity.include_script_public_key.unwrap_or(false) { + Some(utxo.script_public_key.clone()) + } else { + Default::default() + }, + block_daa_score: if verbosity.include_block_daa_score.unwrap_or(false) { + Some(utxo.block_daa_score) + } else { + Default::default() + }, + is_coinbase: if verbosity.include_is_coinbase.unwrap_or(false) { Some(utxo.is_coinbase) } else { Default::default() }, + verbose_data: if let Some(utxo_entry_verbosity) = verbosity.verbose_data_verbosity.as_ref() { + Some(self.get_utxo_verbose_data_with_verbosity(&utxo, utxo_entry_verbosity)?) + } else { + Default::default() + }, + }) + } + + fn get_utxo_verbose_data_with_verbosity( + &self, + utxo: &UtxoEntry, + verbosity: &RpcUtxoEntryVerboseDataVerbosity, + ) -> RpcResult { + Ok(RpcOptionalUtxoEntryVerboseData { + script_public_key_type: if verbosity.include_script_public_key_type.unwrap_or(false) { + Some(ScriptClass::from_script(&utxo.script_public_key)) + } else { + Default::default() + }, + script_public_key_address: if verbosity.include_script_public_key_address.unwrap_or(false) { + Some( + extract_script_pub_key_address(&utxo.script_public_key, self.config.prefix()) + .map_err(|_| AddressError::InvalidAddress)?, + ) + } else { + Default::default() + }, + }) + } + + fn get_input_verbose_data_with_verbosity( + &self, + utxo: Option, + verbosity: &RpcTransactionInputVerboseDataVerbosity, + ) -> RpcResult { + Ok(RpcOptionalTransactionInputVerboseData { + utxo_entry: if let Some(utxo_entry_verbosity) = verbosity.utxo_entry_verbosity.as_ref() { + if let Some(utxo) = utxo { + Some(self.convert_utxo_entry_with_verbosity(utxo, utxo_entry_verbosity)?) + } else { + return Err(RpcError::ConsensusConverterNotFound("UtxoEntry".to_string())); + } + } else { + Default::default() + }, + }) + } + + fn get_transaction_verbose_data_with_verbosity( + &self, + transaction: &Transaction, + block_hash: Hash, + block_time: u64, + compute_mass: u64, + verbosity: &RpcTransactionVerboseDataVerbosity, + ) -> RpcResult { + Ok(RpcOptionalTransactionVerboseData { + transaction_id: if verbosity.include_transaction_id.unwrap_or(false) { + Some(transaction.id()) + } else { + Default::default() + }, + hash: if verbosity.include_hash.unwrap_or(false) { Some(hash(transaction)) } else { Default::default() }, + compute_mass: if verbosity.include_compute_mass.unwrap_or(false) { Some(compute_mass) } else { Default::default() }, + block_hash: if verbosity.include_block_hash.unwrap_or(false) { Some(block_hash) } else { Default::default() }, + block_time: if verbosity.include_block_time.unwrap_or(false) { Some(block_time) } else { Default::default() }, + }) + } + + fn get_transaction_output_verbose_data_with_verbosity( + &self, + output: &TransactionOutput, + verbosity: &RpcTransactionOutputVerboseDataVerbosity, + ) -> RpcResult { + Ok(RpcOptionalTransactionOutputVerboseData { + script_public_key_type: if verbosity.include_script_public_key_type.unwrap_or(false) { + Some(ScriptClass::from_script(&output.script_public_key)) + } else { + Default::default() + }, + script_public_key_address: if verbosity.include_script_public_key_address.unwrap_or(false) { + Some( + extract_script_pub_key_address(&output.script_public_key, self.config.prefix()) + .map_err(|_| AddressError::InvalidAddress)?, + ) + } else { + None + }, + }) + } + + fn convert_transaction_output_with_verbosity( + &self, + output: &TransactionOutput, + verbosity: &RpcTransactionOutputVerbosity, + ) -> RpcResult { + Ok(RpcOptionalTransactionOutput { + value: if verbosity.include_amount.unwrap_or(false) { Some(output.value) } else { Default::default() }, + script_public_key: if verbosity.include_script_public_key.unwrap_or(false) { + Some(output.script_public_key.clone()) + } else { + Default::default() + }, + verbose_data: if let Some(output_verbose_data_verbosity) = verbosity.verbose_data_verbosity.as_ref() { + Some(self.get_transaction_output_verbose_data_with_verbosity(output, output_verbose_data_verbosity)?) + } else { + Default::default() + }, + }) + } + + pub fn get_transaction_input_with_verbosity( + &self, + input: &TransactionInput, + utxo: Option, + verbosity: &RpcTransactionInputVerbosity, + ) -> RpcResult { + Ok(RpcOptionalTransactionInput { + previous_outpoint: if verbosity.include_previous_outpoint.unwrap_or(false) { + Some(input.previous_outpoint.into()) + } else { + Default::default() + }, + signature_script: if verbosity.include_signature_script.unwrap_or(false) { + Some(input.signature_script.clone()) + } else { + Default::default() + }, + sequence: if verbosity.include_sequence.unwrap_or(false) { Some(input.sequence) } else { Default::default() }, + sig_op_count: if verbosity.include_sig_op_count.unwrap_or(false) { Some(input.sig_op_count) } else { Default::default() }, + verbose_data: if let Some(input_verbose_data_verbosity) = verbosity.verbose_data_verbosity.as_ref() { + Some(self.get_input_verbose_data_with_verbosity(utxo, input_verbose_data_verbosity)?) + } else { + Default::default() + }, + }) + } + + pub async fn convert_transaction_with_verbosity( + &self, + consensus: &ConsensusProxy, + transaction: &Transaction, + block_hash: Option, + block_time: u64, + verbosity: &RpcTransactionVerbosity, + ) -> RpcResult { + Ok(RpcOptionalTransaction { + version: if verbosity.include_version.unwrap_or(false) { Some(transaction.version) } else { Default::default() }, + inputs: if let Some(ref input_verbosity) = verbosity.input_verbosity { + transaction + .inputs + .iter() + .map(|x| self.get_transaction_input_with_verbosity(x, None, input_verbosity)) + .collect::, _>>()? + } else { + Default::default() + }, + outputs: if let Some(ref output_verbosity) = verbosity.output_verbosity { + transaction + .outputs + .iter() + .map(|x| self.convert_transaction_output_with_verbosity(x, output_verbosity)) + .collect::, _>>()? + } else { + Default::default() + }, + lock_time: if verbosity.include_lock_time.unwrap_or(false) { Some(transaction.lock_time) } else { Default::default() }, + subnetwork_id: if verbosity.include_subnetwork_id.unwrap_or(false) { + Some(transaction.subnetwork_id.clone()) + } else { + Default::default() + }, + gas: if verbosity.include_gas.unwrap_or(false) { Some(transaction.gas) } else { Default::default() }, + payload: if verbosity.include_payload.unwrap_or(false) { Some(transaction.payload.clone()) } else { Default::default() }, + mass: if verbosity.include_mass.unwrap_or(false) { Some(transaction.mass()) } else { Default::default() }, + verbose_data: if let Some(verbose_data_verbosity) = verbosity.verbose_data_verbosity.as_ref() { + Some(self.get_transaction_verbose_data_with_verbosity( + transaction, + block_hash.unwrap(), + block_time, + consensus.calculate_transaction_non_contextual_masses(transaction).compute_mass, + verbose_data_verbosity, + )?) + } else { + Default::default() + }, + }) + } + + pub async fn convert_signable_transaction_with_verbosity( + &self, + consensus: &ConsensusProxy, + transaction: &SignableTransaction, + block_hash: Option, + block_time: u64, + verbosity: &RpcTransactionVerbosity, + ) -> RpcResult { + Ok(RpcOptionalTransaction { + version: if verbosity.include_version.unwrap_or(false) { Some(transaction.tx.version) } else { Default::default() }, + inputs: if let Some(input_verbosity) = verbosity.input_verbosity.as_ref() { + transaction + .tx + .inputs + .iter() + .enumerate() + .map(|(i, x)| self.get_transaction_input_with_verbosity(x, transaction.entries[i].clone(), input_verbosity)) + .collect::, _>>()? + } else { + Default::default() + }, + outputs: if let Some(output_verbosity) = verbosity.output_verbosity.as_ref() { + transaction + .tx + .outputs + .iter() + .map(|x| self.convert_transaction_output_with_verbosity(x, output_verbosity)) + .collect::, _>>()? + } else { + Default::default() + }, + lock_time: if verbosity.include_lock_time.unwrap_or(false) { Some(transaction.tx.lock_time) } else { Default::default() }, + subnetwork_id: Some(transaction.tx.subnetwork_id.clone()), + gas: Some(transaction.tx.gas), + payload: Some(transaction.tx.payload.clone()), + mass: Some(transaction.tx.mass()), + verbose_data: if let Some(verbose_data_verbosity) = verbosity.verbose_data_verbosity.as_ref() { + Some( + self.get_transaction_verbose_data_with_verbosity( + &transaction.tx, + block_hash.unwrap(), + block_time, + transaction + .calculated_non_contextual_masses + .unwrap_or(consensus.calculate_transaction_non_contextual_masses(transaction.tx.as_ref())) + .compute_mass, + verbose_data_verbosity, + )?, + ) + } else { + Default::default() + }, + }) + } + + pub async fn get_accepted_transactions_with_verbosity( + &self, + consensus: &ConsensusProxy, + tx_ids: Option>, + accepting_block: Hash, + merged_block_data: &MergesetBlockAcceptanceData, + verbosity: &RpcTransactionVerbosity, + ) -> RpcResult> { + let merged_block_timestamp = consensus.async_get_header(merged_block_data.block_hash).await?.timestamp; + + let txs = consensus + .async_get_transactions_by_block_acceptance_data( + accepting_block, + merged_block_data.clone(), + tx_ids, + if verbosity.requires_populated_transaction() { + TransactionType::SignableTransaction + } else { + TransactionType::Transaction + }, + ) + .await?; + + Ok(match txs { + TransactionQueryResult::Transaction(txs) => { + let mut converted = Vec::with_capacity(txs.len()); + + for tx in txs.iter() { + converted.push({ + let rpc_tx = self + .convert_transaction_with_verbosity( + consensus, + tx, + Some(merged_block_data.block_hash), + merged_block_timestamp, + verbosity, + ) + .await?; + + if rpc_tx.is_empty() { + continue; + }; + + rpc_tx + }); + } + + converted + } + TransactionQueryResult::SignableTransaction(txs) => { + let mut converted = Vec::with_capacity(txs.len()); + + for tx in txs.iter() { + converted.push({ + let rpc_tx = self + .convert_signable_transaction_with_verbosity( + consensus, + tx, + Some(merged_block_data.block_hash), + merged_block_timestamp, + verbosity, + ) + .await?; + + if rpc_tx.is_empty() { + continue; + }; + + rpc_tx + }); + } + + converted + } + }) + } + + async fn get_mergeset_blocks_data_with_verbosity( + &self, + consensus: &ConsensusProxy, + accepting_block: Hash, + mergeset_blocks_acceptance_data: &Arc, + verbosity: &RpcMergesetBlockAcceptanceDataVerbosity, + ) -> RpcResult> { + let mut rpc_mergeset_blocks_acceptance_data: Vec = + Vec::with_capacity(mergeset_blocks_acceptance_data.len()); + + for merged_block_acceptance_data in mergeset_blocks_acceptance_data.iter() { + let accepted_txs = if let Some(accepted_transaction_verbosity) = verbosity.accepted_transactions_verbosity.as_ref() { + self.get_accepted_transactions_with_verbosity( + consensus, + None, + accepting_block, + merged_block_acceptance_data, + accepted_transaction_verbosity, + ) + .await? + } else { + Vec::new() + }; + + rpc_mergeset_blocks_acceptance_data.push(RpcMergesetBlockAcceptanceData { + merged_block_hash: merged_block_acceptance_data.block_hash, + accepted_transactions: accepted_txs, + }); + } + + Ok(rpc_mergeset_blocks_acceptance_data) + } + + pub async fn get_acceptance_data_with_verbosity( + &self, + consensus: &ConsensusProxy, + verbosity: &RpcAcceptanceDataVerbosity, + chain_path: &ChainPath, + merged_blocks_limit: Option, + ) -> RpcResult> { + if verbosity.accepting_chain_header_verbosity.is_none() && verbosity.mergeset_block_acceptance_data_verbosity.is_none() { + // early exit condition + return Ok(Vec::new()); + } + + let chain_block_mergeset_acceptance_data_vec = + consensus.async_get_blocks_acceptance_data(chain_path.added.clone(), merged_blocks_limit).await.unwrap(); + let mut rpc_acceptance_data = Vec::::with_capacity(chain_block_mergeset_acceptance_data_vec.len()); + + // for each chain block + for (accepting_chain_hash, chain_block_mergeset_acceptance_data) in + chain_path.added.iter().zip(chain_block_mergeset_acceptance_data_vec.iter()) + { + // accepting chain block header is always needed to populate transactions + let accepting_chain_header = consensus.async_get_header(*accepting_chain_hash).await?; + + // adapt it to fit target verbosity in response + let accepting_chain_header_with_verbosity = if let Some(verbosity) = verbosity.accepting_chain_header_verbosity.as_ref() { + let header = self.adapt_header_to_header_with_verbosity(verbosity, &accepting_chain_header)?; + if header.is_empty() { + Default::default() + } else { + Some(header) + } + } else { + Default::default() + }; + + if let Some(mergeset_block_acceptance_data_verbosity) = verbosity.mergeset_block_acceptance_data_verbosity.as_ref() { + let rpc_mergeset_blocks_acceptance_data_with_verbosity = self + .get_mergeset_blocks_data_with_verbosity( + consensus, + *accepting_chain_hash, + chain_block_mergeset_acceptance_data, + mergeset_block_acceptance_data_verbosity, + ) + .await?; + + rpc_acceptance_data.push(RpcAcceptanceData { + accepting_chain_header: accepting_chain_header_with_verbosity, + mergeset_block_acceptance_data: rpc_mergeset_blocks_acceptance_data_with_verbosity, + }); + } else { + rpc_acceptance_data.push(RpcAcceptanceData { + accepting_chain_header: accepting_chain_header_with_verbosity, + mergeset_block_acceptance_data: Default::default(), + }); + }; + } + Ok(rpc_acceptance_data) + } } #[async_trait] diff --git a/rpc/service/src/service.rs b/rpc/service/src/service.rs index 2c4ae5fc64..7735298999 100644 --- a/rpc/service/src/service.rs +++ b/rpc/service/src/service.rs @@ -7,6 +7,7 @@ use async_trait::async_trait; use kaspa_consensus_core::api::counters::ProcessingCounters; use kaspa_consensus_core::daa_score_timestamp::DaaScoreTimestamp; use kaspa_consensus_core::errors::block::RuleError; +use kaspa_consensus_core::tx::{TransactionQueryResult, TransactionType}; use kaspa_consensus_core::utxo::utxo_inquirer::UtxoInquirerError; use kaspa_consensus_core::{ block::Block, @@ -337,7 +338,7 @@ impl RpcApi for RpcCoreService { Ok(_) => Ok(SubmitBlockResponse { report: SubmitBlockReport::Success }), Err(ProtocolError::RuleError(RuleError::BadMerkleRoot(h1, h2))) => { warn!( - "The RPC submitted block {} triggered a {} error: {}. + "The RPC submitted block {} triggered a {} error: {}. NOTE: This error usually indicates an RPC conversion error between the node and the miner. This is likely to reflect using a NON-SUPPORTED miner.", hash, stringify!(RuleError::BadMerkleRoot), @@ -874,23 +875,34 @@ NOTE: This error usually indicates an RPC conversion error between the node and return Err(RpcError::ConsensusInTransitionalIbdState); } - match session.async_get_populated_transaction(request.txid, request.accepting_block_daa_score).await { - Ok(tx) => { - if tx.tx.inputs.is_empty() || tx.entries.is_empty() { - return Err(RpcError::UtxoReturnAddressNotFound(UtxoInquirerError::TxFromCoinbase)); + match session + .async_get_transactions_by_accepting_daa_score( + request.accepting_block_daa_score, + Some(vec![request.txid]), + TransactionType::SignableTransaction, + ) + .await? + { + TransactionQueryResult::SignableTransaction(txs) => { + if txs.is_empty() { + return Err(RpcError::ConsensusError(UtxoInquirerError::TransactionNotFound.into())); + }; + + if txs[0].tx.inputs.is_empty() || txs[0].entries.is_empty() { + return Err(RpcError::ConsensusError(UtxoInquirerError::TxFromCoinbase.into())); } - if let Some(utxo_entry) = &tx.entries[0] { + if let Some(utxo_entry) = &txs[0].entries[0] { if let Ok(address) = extract_script_pub_key_address(&utxo_entry.script_public_key, self.config.prefix()) { Ok(GetUtxoReturnAddressResponse { return_address: address }) } else { - Err(RpcError::UtxoReturnAddressNotFound(UtxoInquirerError::NonStandard)) + Err(RpcError::ConsensusError(UtxoInquirerError::NonStandard.into())) } } else { - Err(RpcError::UtxoReturnAddressNotFound(UtxoInquirerError::UnfilledUtxoEntry)) + Err(RpcError::ConsensusError(UtxoInquirerError::UnfilledUtxoEntry.into())) } } - Err(error) => return Err(RpcError::UtxoReturnAddressNotFound(error)), + TransactionQueryResult::Transaction(_) => Err(RpcError::ConsensusError(UtxoInquirerError::TransactionNotFound.into())), } } @@ -1222,6 +1234,63 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetSyncStatusResponse { is_synced }) } + async fn get_virtual_chain_from_block_v2_call( + &self, + _connection: Option<&DynRpcConnection>, + request: GetVirtualChainFromBlockV2Request, + ) -> RpcResult { + let session = self.consensus_manager.consensus().session().await; + let data_verbosity_level = request.data_verbosity_level; + let verbosity: RpcAcceptanceDataVerbosity = data_verbosity_level.map(RpcAcceptanceDataVerbosity::from).unwrap_or_default(); + let batch_size = (self.config.mergeset_size_limit().upper_bound() * 10) as usize; + + let mut chain_path = session.async_get_virtual_chain_from_block(request.start_hash, Some(batch_size)).await?; + + if let Some(min_confirmation_count) = request.min_confirmation_count { + if min_confirmation_count > 0 { + let sink_blue_score = session.async_get_sink_blue_score().await; + + while !chain_path.added.is_empty() { + let vc_last_accepted_block_hash = chain_path.added.last().unwrap(); + let vc_last_accepted_block = session.async_get_block(*vc_last_accepted_block_hash).await?; + + let distance = sink_blue_score.saturating_sub(vc_last_accepted_block.header.blue_score); + + if distance > min_confirmation_count { + break; + } + + chain_path.added.pop(); + } + } + } + + let chain_blocks_acceptance_data = + self.consensus_converter.get_acceptance_data_with_verbosity(&session, &verbosity, &chain_path, Some(batch_size)).await?; + + let chain_block_accepted_transactions: Vec<_> = chain_blocks_acceptance_data + .iter() + .map(|chain_block_acceptance_data| { + let chain_block_header = chain_block_acceptance_data.accepting_chain_header.as_ref().unwrap().clone(); + // Flatten all accepted transactions from mergeset blocks into a single vec + let accepted_transactions: Vec = chain_block_acceptance_data + .mergeset_block_acceptance_data + .iter() + .flat_map(|msb_acceptance_data| msb_acceptance_data.accepted_transactions.clone()) + .collect(); + + RpcChainBlockAcceptedTransactions { chain_block_header, accepted_transactions } + }) + .collect(); + chain_path.added.truncate(chain_blocks_acceptance_data.len()); + + Ok(GetVirtualChainFromBlockV2Response { + removed_chain_block_hashes: chain_path.removed.into(), + added_chain_block_hashes: chain_path.added.into(), + chain_block_accepted_transactions: chain_block_accepted_transactions.into(), + }) + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/rpc/wrpc/client/src/client.rs b/rpc/wrpc/client/src/client.rs index f22bcf6255..497b156ae3 100644 --- a/rpc/wrpc/client/src/client.rs +++ b/rpc/wrpc/client/src/client.rs @@ -647,6 +647,7 @@ impl RpcApi for KaspaRpcClient { GetUtxoReturnAddress, GetUtxosByAddresses, GetVirtualChainFromBlock, + GetVirtualChainFromBlockV2, ResolveFinalityConflict, Shutdown, SubmitBlock, diff --git a/rpc/wrpc/examples/simple_client/src/main.rs b/rpc/wrpc/examples/simple_client/src/main.rs index 0d63a24c27..6092647cda 100644 --- a/rpc/wrpc/examples/simple_client/src/main.rs +++ b/rpc/wrpc/examples/simple_client/src/main.rs @@ -3,8 +3,7 @@ use kaspa_rpc_core::{api::rpc::RpcApi, GetBlockDagInfoResponse, GetServerInfoResponse}; use kaspa_wrpc_client::{ client::{ConnectOptions, ConnectStrategy}, - prelude::NetworkId, - prelude::NetworkType, + prelude::{NetworkId, NetworkType}, result::Result, KaspaRpcClient, Resolver, WrpcEncoding, }; diff --git a/rpc/wrpc/examples/vcc_v2/Cargo.toml b/rpc/wrpc/examples/vcc_v2/Cargo.toml new file mode 100644 index 0000000000..07cf38bf9b --- /dev/null +++ b/rpc/wrpc/examples/vcc_v2/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "kaspa-wrpc-vcc-v2" +description = "Kaspa VCCV2 example" +publish = false +rust-version.workspace = true +version.workspace = true +edition.workspace = true +authors.workspace = true +include.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +futures.workspace = true +kaspa-rpc-core.workspace = true +kaspa-wrpc-client.workspace = true +tokio.workspace = true + + +[lints] +workspace = true diff --git a/rpc/wrpc/examples/vcc_v2/src/main.rs b/rpc/wrpc/examples/vcc_v2/src/main.rs new file mode 100644 index 0000000000..ded225a7b6 --- /dev/null +++ b/rpc/wrpc/examples/vcc_v2/src/main.rs @@ -0,0 +1,111 @@ +// Example of VCCv2 endpoint + +use kaspa_rpc_core::{api::rpc::RpcApi, RpcDataVerbosityLevel, RpcHash}; +use kaspa_wrpc_client::{ + client::{ConnectOptions, ConnectStrategy}, + prelude::NetworkId, + prelude::NetworkType, + result::Result, + KaspaRpcClient, WrpcEncoding, +}; +use std::str::FromStr; +use std::time::Duration; +use std::{ + collections::{HashMap, HashSet}, + process::ExitCode, +}; + +#[tokio::main] +async fn main() -> ExitCode { + match get_vcc_v2().await { + Ok(_) => { + println!("Well done! You successfully completed your first client connection to Kaspa node!"); + ExitCode::SUCCESS + } + Err(error) => { + println!("An error occurred: {error}"); + ExitCode::FAILURE + } + } +} + +async fn get_vcc_v2() -> Result<()> { + let encoding = WrpcEncoding::Borsh; + + let url = Some("ws://127.0.0.1:17110"); + let resolver = None; + + let network_type = NetworkType::Mainnet; + let selected_network = Some(NetworkId::new(network_type)); + + // Advanced options + let subscription_context = None; + + // Create new wRPC client with parameters defined above + let client = KaspaRpcClient::new(encoding, url, resolver, selected_network, subscription_context)?; + + // Advanced connection options + let timeout = 5_000; + let options = ConnectOptions { + block_async_connect: true, + connect_timeout: Some(Duration::from_millis(timeout)), + strategy: ConnectStrategy::Fallback, + ..Default::default() + }; + + // Connect to selected Kaspa node + client.connect(Some(options)).await?; + + let response = client + .get_virtual_chain_from_block_v2( + RpcHash::from_str("d274ca180b7ba85f512e4a59020c7a302a4eb9ba35331a3fd6e86c18bd3bac8c").unwrap(), + Some(RpcDataVerbosityLevel::Low), + None, + ) + .await?; + + let mut global_seen_tx = HashSet::::with_capacity(30_000); + let mut tx_occurrence_counts = HashMap::::new(); + let mut total_duplicates = 0; + let mut total_intra_mergeset_duplicates = 0; + + response.chain_block_accepted_transactions.iter().for_each(|acd| { + let mut mergeset_seen_tx = HashSet::::new(); + let mut current_mergeset_intra_duplicates = 0; + let mergeset_block_hash = acd.chain_block_header.as_ref().hash.unwrap(); + + acd.accepted_transactions.iter().for_each(|tx| { + let id = tx.verbose_data.as_ref().unwrap().transaction_id.unwrap(); + + *tx_occurrence_counts.entry(id).or_insert(0) += 1; + + if !global_seen_tx.insert(id) { + total_duplicates += 1; + } + if !mergeset_seen_tx.insert(id) { + current_mergeset_intra_duplicates += 1; + } + }); + + println!("mergeset of {} has {} intra-duplicates", mergeset_block_hash, current_mergeset_intra_duplicates); + total_intra_mergeset_duplicates += current_mergeset_intra_duplicates; + }); + + let total_cross_mergeset_duplicates = total_duplicates - total_intra_mergeset_duplicates; + println!("total tx: {}", global_seen_tx.len()); + println!("total duplicated tx instances: {}", total_duplicates); + println!("total intra-mergeset duplicated tx instances: {}", total_intra_mergeset_duplicates); + println!("total cross-mergeset duplicated tx instances: {}", total_cross_mergeset_duplicates); + + println!("\nTransaction occurrence counts (duplicates only):"); + for (tx_id, count) in tx_occurrence_counts.iter() { + if *count > 1 { + println!(" Tx ID: {}, Occurrences: {}", tx_id, count); + } + } + // Disconnect client from Kaspa node + client.disconnect().await?; + + // Return function result + Ok(()) +} diff --git a/rpc/wrpc/server/src/router.rs b/rpc/wrpc/server/src/router.rs index b4c74a3374..7dc3d6b9bd 100644 --- a/rpc/wrpc/server/src/router.rs +++ b/rpc/wrpc/server/src/router.rs @@ -69,6 +69,7 @@ impl Router { GetSystemInfo, GetUtxosByAddresses, GetVirtualChainFromBlock, + GetVirtualChainFromBlockV2, ResolveFinalityConflict, Shutdown, SubmitBlock, diff --git a/rpc/wrpc/wasm/src/client.rs b/rpc/wrpc/wasm/src/client.rs index ff42a69028..cead974c21 100644 --- a/rpc/wrpc/wasm/src/client.rs +++ b/rpc/wrpc/wasm/src/client.rs @@ -37,7 +37,7 @@ declare! { r#" /** * RPC client configuration options - * + * * @category Node RPC */ export interface IRpcConfig { @@ -1056,6 +1056,10 @@ build_wrpc_wasm_bindgen_interface!( /// Returned information: None. Unban, /// Get UTXO Return Addresses. - GetUtxoReturnAddress + GetUtxoReturnAddress, + /// Retrieves the virtual chain corresponding to a specified block hash. + /// Returned information: Virtual chain information. (Version 2) + /// May be used to get fully populated transactions + GetVirtualChainFromBlockV2 ] ); diff --git a/simpa/src/main.rs b/simpa/src/main.rs index 27ad2279b8..aa5be9c4ec 100644 --- a/simpa/src/main.rs +++ b/simpa/src/main.rs @@ -17,7 +17,7 @@ use kaspa_consensus::{ }; use kaspa_consensus_core::{ api::ConsensusApi, block::Block, blockstatus::BlockStatus, config::bps::calculate_ghostdag_k, errors::block::BlockProcessResult, - mining_rules::MiningRules, BlockHashSet, BlockLevel, HashMapCustomHasher, + mining_rules::MiningRules, tx::TransactionType, BlockHashSet, BlockLevel, HashMapCustomHasher, }; use kaspa_consensus_notify::root::ConsensusNotificationRoot; use kaspa_core::{ @@ -296,7 +296,13 @@ fn main_impl(mut args: Args) { let block = consensus.get_block(hash).unwrap(); cbad.accepted_transactions.iter().for_each(|ate| { assert!( - consensus.get_populated_transaction(ate.transaction_id, block.header.daa_score).is_ok(), + consensus + .get_transactions_by_accepting_daa_score( + block.header.daa_score, + Some(vec![ate.transaction_id]), + TransactionType::SignableTransaction + ) + .is_ok(), "Expected to find find tx {} at accepted daa {} via get_populated_transaction", ate.transaction_id, block.header.daa_score diff --git a/testing/integration/src/consensus_integration_tests.rs b/testing/integration/src/consensus_integration_tests.rs index 50bc05a91c..5900329547 100644 --- a/testing/integration/src/consensus_integration_tests.rs +++ b/testing/integration/src/consensus_integration_tests.rs @@ -673,7 +673,7 @@ async fn mergeset_size_limit_test() { #[allow(non_snake_case)] #[derive(Deserialize, Debug)] struct RPCBlock { - Header: RPCBlockHeader, + Header: RpcHeader, Transactions: Vec, VerboseData: RPCBlockVerboseData, } @@ -722,7 +722,7 @@ struct RPCOutpoint { #[allow(non_snake_case)] #[derive(Deserialize, Debug)] -struct RPCBlockHeader { +struct RpcHeader { Version: u16, Parents: Vec, HashMerkleRoot: String, @@ -986,7 +986,7 @@ async fn json_test(file_path: &str, concurrency: bool) { let proof_lines = gzip_file_lines(&main_path.join("proof.json.gz")); let proof = proof_lines .map(|line| { - let rpc_headers: Vec = serde_json::from_str(&line).unwrap(); + let rpc_headers: Vec = serde_json::from_str(&line).unwrap(); rpc_headers.iter().map(|rh| Arc::new(rpc_header_to_header(rh))).collect_vec() }) .collect_vec(); @@ -1127,7 +1127,7 @@ fn submit_body_chunk( futures } -fn rpc_header_to_header(rpc_header: &RPCBlockHeader) -> Header { +fn rpc_header_to_header(rpc_header: &RpcHeader) -> Header { Header::new_finalized( rpc_header.Version, rpc_header diff --git a/testing/integration/src/rpc_tests.rs b/testing/integration/src/rpc_tests.rs index 39bfcae35c..67a898a1fc 100644 --- a/testing/integration/src/rpc_tests.rs +++ b/testing/integration/src/rpc_tests.rs @@ -689,6 +689,25 @@ async fn sanity_test() { }) } + KaspadPayloadOps::GetVirtualChainFromBlockV2 => { + let rpc_client = client.clone(); + tst!(op, { + let response = rpc_client + .get_virtual_chain_from_block_v2_call( + None, + GetVirtualChainFromBlockV2Request { + start_hash: SIMNET_GENESIS.hash, + data_verbosity_level: None, + min_confirmation_count: None, + }, + ) + .await + .unwrap(); + assert!(response.added_chain_block_hashes.is_empty()); + assert!(response.removed_chain_block_hashes.is_empty()); + }) + } + KaspadPayloadOps::NotifyBlockAdded => { let rpc_client = client.clone(); let id = listener_id; diff --git a/wallet/core/src/tests/rpc_core_mock.rs b/wallet/core/src/tests/rpc_core_mock.rs index 529fffffe8..2382929cac 100644 --- a/wallet/core/src/tests/rpc_core_mock.rs +++ b/wallet/core/src/tests/rpc_core_mock.rs @@ -387,6 +387,14 @@ impl RpcApi for RpcCoreMock { Err(RpcError::NotImplemented) } + async fn get_virtual_chain_from_block_v2_call( + &self, + _connection: Option<&DynRpcConnection>, + _request: GetVirtualChainFromBlockV2Request, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Notification API diff --git a/wasm/examples/nodejs/javascript/general/get-vritual-chain-v2.js b/wasm/examples/nodejs/javascript/general/get-vritual-chain-v2.js new file mode 100644 index 0000000000..3153e35f34 --- /dev/null +++ b/wasm/examples/nodejs/javascript/general/get-vritual-chain-v2.js @@ -0,0 +1,69 @@ +// @ts-ignore +globalThis.WebSocket = require("websocket").w3cwebsocket; // W3C WebSocket module shim + +const kaspa = require("../../../../nodejs/kaspa"); +const { RpcClient, Encoding } = kaspa; + +kaspa.initConsolePanicHook(); + +const delay = (ms) => new Promise((res) => setTimeout(res, ms)); + +(async () => { + const rpc = new RpcClient({ + url: "127.0.0.1", + encoding: Encoding.Borsh, + // resolver: new Resolver(), + networkId: "mainnet", + }); + console.log(`Resolving RPC endpoint...`); + await rpc.connect(); + console.log(`Connecting to ${rpc.url}`); + + console.log("Getting known block hash from node..."); + + const info = await rpc.getBlockDagInfo(); + console.info("BlockDagInfo:", info); + + // Start from node sink / pruning point + let lowHash = info.sink; + console.info("Starting lowHash (sink):", lowHash); + + await delay(2000); + + // Main loop - runs forever every 10 seconds + while (true) { + try { + const date = new Date(); + const vspc = await rpc.getVirtualChainFromBlockV2({ + startHash: lowHash, + minConfirmationCount: 10, + dataVerbosityLevel: "None", + }); + console.info("VSPC Info:", vspc); + + for (const hash of vspc.removedChainBlockHashes) { + console.info("Removed block hash:", hash); + } + + for (const hash of vspc.addedChainBlockHashes) { + console.info("Added block hash:", hash); + lowHash = hash; + } + + for (const cbat of vspc.chainBlockAcceptedTransactions) { + // Do something with the chain block header + console.info(cbat.chainBlockHeader); + // Do something with the accepted transactions + console.info(cbat.acceptedTransactions); + } + + console.info("Time span:", Date.now() - date.getTime(), "ms"); + } catch (innerErr) { + console.error("Error in loop iteration:", innerErr); + // keep running despite errors + } + + // wait 10 seconds before next iteration + await delay(10000); + } +})();