From 25ca7631c325704a49f61217532da14505e582c1 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 5 Dec 2024 13:54:39 +0400 Subject: [PATCH] feat(core)!: add L2 eviction proof transaction type --- Cargo.lock | 277 +++-- Cargo.toml | 3 +- applications/minotari_app_grpc/Cargo.toml | 1 + .../minotari_app_grpc/proto/base_node.proto | 36 +- .../proto/sidechain_types.proto | 146 ++- .../minotari_app_grpc/proto/types.proto | 4 +- .../minotari_app_grpc/proto/wallet.proto | 12 + .../minotari_app_grpc/src/conversions/mod.rs | 48 +- .../src/conversions/output_features.rs | 6 +- .../src/conversions/sidechain_feature.rs | 568 +++++++-- .../src/conversions/signature.rs | 30 +- .../src/conversions/validator_node_change.rs | 20 - .../validator_node_registration.rs | 28 - .../src/grpc/wallet_grpc_server.rs | 48 + .../src/grpc/base_node_grpc_server.rs | 86 +- base_layer/common_types/Cargo.toml | 2 +- base_layer/common_types/src/epoch.rs | 29 +- .../common_types/src/types/fixed_hash.rs | 1 + base_layer/core/Cargo.toml | 1 + .../comms_interface/comms_request.rs | 33 +- .../comms_interface/comms_response.rs | 27 +- .../comms_interface/inbound_handlers.rs | 116 +- .../comms_interface/local_interface.rs | 23 +- .../core/src/base_node/comms_interface/mod.rs | 7 +- base_layer/core/src/base_node/rpc/service.rs | 2 +- .../chain_storage/active_validator_node.rs | 36 +- base_layer/core/src/chain_storage/async_db.rs | 7 +- .../src/chain_storage/blockchain_backend.rs | 52 +- .../src/chain_storage/blockchain_database.rs | 79 +- .../chain_storage/lmdb_db/composite_key.rs | 179 ++- .../core/src/chain_storage/lmdb_db/cursors.rs | 149 ++- .../core/src/chain_storage/lmdb_db/helpers.rs | 2 +- .../core/src/chain_storage/lmdb_db/lmdb.rs | 2 +- .../core/src/chain_storage/lmdb_db/lmdb_db.rs | 511 ++++++-- .../lmdb_db/validator_node_store.rs | 1084 +++++++++++++---- base_layer/core/src/chain_storage/mod.rs | 4 +- .../tests/blockchain_database.rs | 58 +- .../core/src/chain_storage/tests/temp_db.rs | 8 +- .../core/src/consensus/consensus_constants.rs | 75 +- base_layer/core/src/covenants/test.rs | 8 +- .../core/src/proto/sidechain_feature.proto | 159 ++- .../core/src/proto/sidechain_feature.rs | 462 ++++++- base_layer/core/src/proto/transaction.rs | 6 +- base_layer/core/src/proto/types_impls.rs | 28 +- .../core/src/test_helpers/blockchain.rs | 87 +- .../src/transactions/key_manager/interface.rs | 2 +- .../src/transactions/key_manager/wrapper.rs | 4 +- .../transaction_components/mod.rs | 3 +- .../transaction_components/output_features.rs | 69 +- .../transaction_components/output_type.rs | 45 +- .../range_proof_type.rs | 21 + .../side_chain/confidential_output.rs | 13 +- .../transaction_components/side_chain/mod.rs | 10 +- .../side_chain/sidechain_feature.rs | 104 +- .../side_chain/template_registration.rs | 18 +- .../side_chain/validator_node_registration.rs | 38 +- .../side_chain/validator_node_signature.rs | 59 +- .../transaction_input.rs | 34 +- .../transaction_output.rs | 52 +- .../aggregate_body_chain_validator.rs | 51 +- .../aggregate_body_internal_validator.rs | 139 +-- .../block_body/block_body_full_validator.rs | 9 +- .../core/src/validation/block_body/test.rs | 14 +- base_layer/core/src/validation/error.rs | 38 +- base_layer/core/src/validation/helpers.rs | 83 +- .../transaction_chain_validator.rs | 5 +- base_layer/sidechain/Cargo.toml | 20 + base_layer/sidechain/src/command.rs | 42 + base_layer/sidechain/src/commit_proof.rs | 259 ++++ base_layer/sidechain/src/error.rs | 24 + base_layer/sidechain/src/eviction_proof.rs | 75 ++ base_layer/sidechain/src/lib.rs | 14 + base_layer/sidechain/src/shard_group.rs | 11 + base_layer/sidechain/src/validations.rs | 220 ++++ base_layer/sidechain/tests/eviction_proof.rs | 21 + .../tests/fixtures/eviction_proof1.json | 783 ++++++++++++ base_layer/sidechain/tests/support/mod.rs | 10 + base_layer/wallet/Cargo.toml | 1 + .../recovery/standard_outputs_recoverer.rs | 2 + .../storage/output_source.rs | 4 + .../wallet/src/transaction_service/error.rs | 2 + .../wallet/src/transaction_service/handle.rs | 53 +- .../wallet/src/transaction_service/service.rs | 159 ++- clippy.toml | 1 + hashing/Cargo.toml | 8 +- hashing/src/borsh_hasher.rs | 5 + hashing/src/domains.rs | 8 + hashing/src/layer2.rs | 54 + hashing/src/lib.rs | 3 + 89 files changed, 5581 insertions(+), 1559 deletions(-) delete mode 100644 applications/minotari_app_grpc/src/conversions/validator_node_change.rs delete mode 100644 applications/minotari_app_grpc/src/conversions/validator_node_registration.rs create mode 100644 base_layer/sidechain/Cargo.toml create mode 100644 base_layer/sidechain/src/command.rs create mode 100644 base_layer/sidechain/src/commit_proof.rs create mode 100644 base_layer/sidechain/src/error.rs create mode 100644 base_layer/sidechain/src/eviction_proof.rs create mode 100644 base_layer/sidechain/src/lib.rs create mode 100644 base_layer/sidechain/src/shard_group.rs create mode 100644 base_layer/sidechain/src/validations.rs create mode 100644 base_layer/sidechain/tests/eviction_proof.rs create mode 100644 base_layer/sidechain/tests/fixtures/eviction_proof1.json create mode 100644 base_layer/sidechain/tests/support/mod.rs create mode 100644 hashing/src/layer2.rs diff --git a/Cargo.lock b/Cargo.lock index ad29f1a83c..294a1b0e64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" @@ -186,7 +186,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -208,7 +208,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -219,7 +219,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -373,7 +373,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978e81a45367d2409ecd33369a45dda2e9a3ca516153ec194de1fbda4b9fb79d" dependencies = [ - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -539,9 +539,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" dependencies = [ "borsh-derive", "cfg_aliases", @@ -549,16 +549,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" dependencies = [ "once_cell", "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.79", - "syn_derive", + "syn 2.0.87", ] [[package]] @@ -923,7 +922,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1358,7 +1357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1436,7 +1435,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.79", + "syn 2.0.87", "synthez", ] @@ -1481,7 +1480,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1529,7 +1528,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1551,7 +1550,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1698,7 +1697,7 @@ dependencies = [ "console", "shell-words", "tempfile", - "thiserror", + "thiserror 1.0.69", "zeroize", ] @@ -1730,7 +1729,7 @@ dependencies = [ "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1750,7 +1749,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" dependencies = [ - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1837,7 +1836,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1955,7 +1954,17 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", ] [[package]] @@ -1994,6 +2003,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime 2.1.0", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2221,7 +2243,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -2333,9 +2355,9 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.79", + "syn 2.0.87", "textwrap 0.16.0", - "thiserror", + "thiserror 1.0.69", "typed-builder", ] @@ -2560,7 +2582,7 @@ dependencies = [ "once_cell", "radix_trie", "rand", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -2587,7 +2609,7 @@ dependencies = [ "ring", "rustls", "rustls-pemfile 2.2.0", - "thiserror", + "thiserror 1.0.69", "time", "tinyvec", "tokio", @@ -2612,7 +2634,7 @@ dependencies = [ "rand", "resolv-conf", "smallvec", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -3174,7 +3196,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3228,7 +3250,7 @@ dependencies = [ "ledger-transport 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", "log", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3369,9 +3391,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" dependencies = [ "serde", ] @@ -3404,7 +3426,7 @@ dependencies = [ "serde-value", "serde_json", "serde_yaml", - "thiserror", + "thiserror 1.0.69", "thread-id", "typemap-ors", "winapi", @@ -3579,8 +3601,9 @@ dependencies = [ "tari_features", "tari_max_size", "tari_script", + "tari_sidechain", "tari_utilities", - "thiserror", + "thiserror 1.0.69", "tokio", "tonic 0.12.3", "tonic-build", @@ -3604,7 +3627,7 @@ dependencies = [ "tari_comms", "tari_features", "tari_utilities", - "thiserror", + "thiserror 1.0.69", "tokio", "tonic 0.12.3", ] @@ -3630,7 +3653,7 @@ dependencies = [ "tari_p2p", "tari_shutdown", "tari_utilities", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -3679,7 +3702,7 @@ dependencies = [ "tari_script", "tari_shutdown", "tari_utilities", - "thiserror", + "thiserror 1.0.69", "tokio", "tonic 0.12.3", "tui", @@ -3716,7 +3739,7 @@ dependencies = [ "tari_crypto", "tari_script", "tari_utilities", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3754,7 +3777,7 @@ dependencies = [ "tari_key_manager", "tari_max_size", "tari_utilities", - "thiserror", + "thiserror 1.0.69", "tokio", "tonic 0.12.3", "tracing", @@ -3793,7 +3816,7 @@ dependencies = [ "tari_crypto", "tari_max_size", "tari_utilities", - "thiserror", + "thiserror 1.0.69", "tokio", "tonic 0.12.3", ] @@ -3813,7 +3836,7 @@ dependencies = [ "tari_crypto", "tari_features", "tari_utilities", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -3860,7 +3883,7 @@ dependencies = [ "tari_shutdown", "tari_storage", "tari_utilities", - "thiserror", + "thiserror 1.0.69", "tokio", "toml 0.5.11", "tonic 0.12.3", @@ -3916,10 +3939,11 @@ dependencies = [ "tari_script", "tari_service_framework", "tari_shutdown", + "tari_sidechain", "tari_test_utils", "tari_utilities", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tower", "zeroize", @@ -3961,7 +3985,7 @@ dependencies = [ "tari_test_utils", "tari_utilities", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "zeroize", ] @@ -3972,7 +3996,7 @@ version = "1.7.0-pre.3" dependencies = [ "minotari_app_grpc", "tari_common_types", - "thiserror", + "thiserror 1.0.69", "tokio", "tonic 0.12.3", ] @@ -4024,7 +4048,7 @@ dependencies = [ "hex-literal 0.4.1", "sealed", "serde", - "thiserror", + "thiserror 1.0.69", "tiny-keccak", ] @@ -4214,7 +4238,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4333,7 +4357,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4569,7 +4593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.69", "ucd-trie", ] @@ -4593,7 +4617,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4673,7 +4697,7 @@ dependencies = [ "sha3", "signature", "smallvec", - "thiserror", + "thiserror 1.0.69", "twofish", "x25519-dalek", "zeroize", @@ -4738,7 +4762,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4796,7 +4820,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4930,7 +4954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4960,7 +4984,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ - "thiserror", + "thiserror 1.0.69", "toml 0.5.11", ] @@ -5018,7 +5042,7 @@ dependencies = [ "memchr", "parking_lot 0.12.1", "protobuf", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5080,7 +5104,7 @@ dependencies = [ "prost 0.13.3", "prost-types 0.13.3", "regex", - "syn 2.0.79", + "syn 2.0.87", "tempfile", ] @@ -5107,7 +5131,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5250,7 +5274,7 @@ checksum = "9abb8f2aa3432700c2b64a67406ac0da4956d78991f50559509cecc2b6abf249" dependencies = [ "bitflags 1.3.2", "libc", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5311,7 +5335,7 @@ checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", "redox_syscall 0.2.16", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5711,7 +5735,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5803,7 +5827,7 @@ checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5825,7 +5849,7 @@ checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -6035,7 +6059,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -6265,27 +6289,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "syn_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "sync_wrapper" version = "0.1.2" @@ -6316,7 +6328,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d2c2202510a1e186e63e596d9318c91a8cbe85cd1a56a7be0c333e5f59ec8d" dependencies = [ - "syn 2.0.79", + "syn 2.0.87", "synthez-codegen", "synthez-core", ] @@ -6327,7 +6339,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f724aa6d44b7162f3158a57bccd871a77b39a4aef737e01bcdff41f4772c7746" dependencies = [ - "syn 2.0.79", + "syn 2.0.87", "synthez-core", ] @@ -6340,7 +6352,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -6425,7 +6437,7 @@ dependencies = [ "tari_service_framework", "tari_shutdown", "tari_storage", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6450,7 +6462,7 @@ dependencies = [ "tari_features", "tari_test_utils", "tempfile", - "thiserror", + "thiserror 1.0.69", "toml 0.5.11", ] @@ -6464,7 +6476,7 @@ dependencies = [ "serde", "tari_test_utils", "tari_utilities", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -6490,7 +6502,7 @@ dependencies = [ "tari_common", "tari_crypto", "tari_utilities", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6532,7 +6544,7 @@ dependencies = [ "tari_test_utils", "tari_utilities", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tokio-util 0.6.10", @@ -6580,7 +6592,7 @@ dependencies = [ "tari_test_utils", "tari_utilities", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tower", @@ -6630,7 +6642,7 @@ dependencies = [ "tari_test_utils", "tari_utilities", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tower", "uuid", @@ -6701,11 +6713,12 @@ dependencies = [ "tari_script", "tari_service_framework", "tari_shutdown", + "tari_sidechain", "tari_storage", "tari_test_utils", "tari_utilities", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "toml 0.5.11", "tracing", @@ -6792,7 +6805,7 @@ dependencies = [ "tari_shutdown", "tari_utilities", "tempfile", - "thiserror", + "thiserror 1.0.69", "time", "tokio", "tonic 0.12.3", @@ -6828,7 +6841,7 @@ dependencies = [ "tari_service_framework", "tari_utilities", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "zeroize", ] @@ -6854,7 +6867,7 @@ dependencies = [ "borsh", "serde", "tari_utilities", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6867,7 +6880,7 @@ dependencies = [ "once_cell", "prometheus", "reqwest", - "thiserror", + "thiserror 1.0.69", "tokio", "warp", ] @@ -6887,7 +6900,7 @@ dependencies = [ "serde_json", "tari_crypto", "tari_utilities", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6920,7 +6933,7 @@ dependencies = [ "tari_test_utils", "tari_utilities", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "toml 0.5.11", @@ -6943,7 +6956,7 @@ dependencies = [ "tari_crypto", "tari_max_size", "tari_utilities", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6957,7 +6970,7 @@ dependencies = [ "log", "tari_shutdown", "tari_test_utils", - "thiserror", + "thiserror 1.0.69", "tokio", "tower", "tower-service", @@ -6971,6 +6984,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tari_sidechain" +version = "1.7.0-pre.3" +dependencies = [ + "borsh", + "env_logger 0.11.5", + "log", + "serde", + "serde_json", + "tari_common_types", + "tari_crypto", + "tari_hashing", + "tari_utilities", + "thiserror 2.0.3", +] + [[package]] name = "tari_storage" version = "1.7.0-pre.3" @@ -6981,7 +7010,7 @@ dependencies = [ "rand", "serde", "tari_utilities", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -7081,22 +7110,42 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -7241,7 +7290,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -7421,7 +7470,7 @@ dependencies = [ "prost-build 0.13.3", "prost-types 0.13.3", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -7434,7 +7483,7 @@ dependencies = [ "hex-literal 0.3.4", "rand", "sha1 0.6.0", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -7490,7 +7539,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -7563,7 +7612,7 @@ checksum = "29a3151c41d0b13e3d011f98adc24434560ef06673a155a6c7f66b9879eecce2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -7835,7 +7884,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -7869,7 +7918,7 @@ checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8246,7 +8295,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -8266,7 +8315,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index faf499d373..5da5560363 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "base_layer/mmr", "base_layer/p2p", "base_layer/service_framework", + "base_layer/sidechain", "base_layer/wallet", "base_layer/wallet_ffi", "base_layer/tari_mining_helper_ffi", @@ -39,7 +40,7 @@ members = [ "applications/minotari_ledger_wallet/comms", "applications/minotari_ledger_wallet/common", "integration_tests", - "hashing" + "hashing", ] [workspace.dependencies] diff --git a/applications/minotari_app_grpc/Cargo.toml b/applications/minotari_app_grpc/Cargo.toml index 9ac896e91d..e97d00afcc 100644 --- a/applications/minotari_app_grpc/Cargo.toml +++ b/applications/minotari_app_grpc/Cargo.toml @@ -14,6 +14,7 @@ tari_core = { path = "../../base_layer/core" } tari_crypto = { version = "0.21.0" } tari_script = { path = "../../infrastructure/tari_script" } tari_max_size = { path = "../../infrastructure/max_size" } +tari_sidechain = { path = "../../base_layer/sidechain" } tari_utilities = { version = "0.8" } argon2 = { version = "0.4.1", features = ["std", "password-hash"] } diff --git a/applications/minotari_app_grpc/proto/base_node.proto b/applications/minotari_app_grpc/proto/base_node.proto index 07b2220391..9562963e65 100644 --- a/applications/minotari_app_grpc/proto/base_node.proto +++ b/applications/minotari_app_grpc/proto/base_node.proto @@ -46,7 +46,7 @@ service BaseNode { // Returns Block Fees rpc GetBlockFees (BlockGroupRequest) returns (BlockGroupResponse); // Get Version - rpc GetVersion(Empty) returns (StringValue); + rpc GetVersion(Empty) returns (BaseNodeGetVersionResponse); // Check for new updates rpc CheckForUpdates(Empty) returns (SoftwareUpdate); // Get coins in circulation @@ -233,9 +233,9 @@ message IntegerValue { uint64 value = 1; } -// A generic String value -message StringValue { - string value = 1; +message BaseNodeGetVersionResponse { + string version = 1; + uint32 network = 2; } /// GetBlockSize / GetBlockFees Request @@ -488,22 +488,25 @@ message GetActiveValidatorNodesResponse { } message GetValidatorNodeChangesRequest { - uint64 start_height = 1; - uint64 end_height = 2; - bytes sidechain_id = 3; + uint64 epoch = 1; + bytes sidechain_id = 2; } -enum ValidatorNodeChangeState { - ADD = 0; - REMOVE = 1; +message ValidatorNodeChange { + oneof change { + ValidatorNodeChangeAdd add = 1; + ValidatorNodeChangeRemove remove = 2; + } } -message ValidatorNodeChange { +message ValidatorNodeChangeAdd { + uint64 activation_epoch = 1; + ValidatorNodeRegistration registration = 2; + uint64 minimum_value_promise = 3; +} + +message ValidatorNodeChangeRemove { bytes public_key = 1; - ValidatorNodeChangeState state = 2; - uint64 start_height = 3; - ValidatorNodeRegistration registration = 4; - uint64 minimum_value_promise = 5; } message GetValidatorNodeChangesResponse { @@ -511,13 +514,12 @@ message GetValidatorNodeChangesResponse { } message GetShardKeyRequest { - uint64 height = 1; + uint64 epoch = 1; bytes public_key = 2; } message GetShardKeyResponse { bytes shard_key = 1; - bool found = 2; } message GetTemplateRegistrationsRequest { diff --git a/applications/minotari_app_grpc/proto/sidechain_types.proto b/applications/minotari_app_grpc/proto/sidechain_types.proto index b0c9728ecc..db9a0f28bb 100644 --- a/applications/minotari_app_grpc/proto/sidechain_types.proto +++ b/applications/minotari_app_grpc/proto/sidechain_types.proto @@ -26,50 +26,51 @@ package tari.rpc; import "types.proto"; message SideChainFeature { - oneof side_chain_feature { - ValidatorNodeRegistration validator_node_registration = 1; - TemplateRegistration template_registration = 2; - ConfidentialOutputData confidential_output = 3; - } + oneof feature { + ValidatorNodeRegistration validator_node_registration = 1; + TemplateRegistration template_registration = 2; + ConfidentialOutputData confidential_output = 3; + EvictionProof eviction_proof = 4; + } + SideChainId sidechain_id = 5; +} + +message SideChainId { + bytes public_key = 1; + Signature knowledge_proof = 2; } message ValidatorNodeRegistration { - bytes public_key = 1; - Signature signature = 2; - bytes claim_public_key = 3; - bytes sidechain_id = 4; - Signature sidechain_id_knowledge_proof = 5; + bytes public_key = 1; + Signature signature = 2; + bytes claim_public_key = 3; } message TemplateRegistration { - bytes author_public_key = 1; - Signature author_signature = 2; - string template_name = 3; - uint32 template_version = 4; - TemplateType template_type = 5; - BuildInfo build_info = 6; - bytes binary_sha = 7; - string binary_url = 8; - bytes sidechain_id = 9; - Signature sidechain_id_knowledge_proof = 10; + bytes author_public_key = 1; + Signature author_signature = 2; + string template_name = 3; + uint32 template_version = 4; + TemplateType template_type = 5; + BuildInfo build_info = 6; + bytes binary_sha = 7; + string binary_url = 8; } message ConfidentialOutputData { - bytes claim_public_key = 1; - bytes sidechain_id = 2; - Signature sidechain_id_knowledge_proof = 3; + bytes claim_public_key = 1; } message TemplateType { - oneof template_type { - WasmInfo wasm = 1; - FlowInfo flow = 2; - ManifestInfo manifest = 3; - } + oneof template_type { + WasmInfo wasm = 1; + FlowInfo flow = 2; + ManifestInfo manifest = 3; + } } message WasmInfo { - uint32 abi_version = 1; + uint32 abi_version = 1; } message FlowInfo { @@ -79,6 +80,89 @@ message ManifestInfo { } message BuildInfo { - string repo_url = 1; - bytes commit_hash = 2; + string repo_url = 1; + bytes commit_hash = 2; +} + +message EvictionProof { + CommitProof proof = 1; +} + +message CommitProof { + oneof version { + CommitProofV1 v1 = 1; + } +} + +message CommitProofV1 { + bytes command = 1; + SidechainBlockCommitProof commit_proof = 2; } + +message SidechainBlockCommitProof { + SidechainBlockHeader header = 1; + repeated CommitProofElement proof_elements = 2; +} + +message CommitProofElement { + oneof proof_element { + QuorumCertificate quorum_certificate = 1; + DummyChain dummy_chain = 2; + } +} + +message DummyChain { + repeated ChainLink chain_links = 1; +} + +message ChainLink { + bytes header_hash = 1; + bytes parent_id = 2; +} + +message SidechainBlockHeader { + uint32 network = 1; + bytes parent_id = 2; + bytes justify_id = 3; + uint64 height = 4; + uint64 epoch = 5; + ShardGroup shard_group = 6; + bytes proposed_by = 7; + uint64 total_leader_fee = 8; + bytes state_merkle_root = 9; + bytes command_merkle_root = 10; + bool is_dummy = 11; + bytes foreign_indexes_hash = 12; + Signature signature = 13; + uint64 timestamp = 14; + uint64 base_layer_block_height = 15; + bytes base_layer_block_hash = 16; + bytes extra_data_hash = 17; +} + +message ShardGroup { + uint32 start = 1; + uint32 end_inclusive = 2; +} + +message EvictAtom { + bytes public_key = 1; +} + +message QuorumCertificate { + bytes header_hash = 1; + bytes parent_id = 2; + repeated ValidatorSignature signatures = 3; + QuorumDecision decision = 4; +} + +enum QuorumDecision { + Accept = 0; + Reject = 1; +} + +message ValidatorSignature { + bytes public_key = 1; + Signature signature = 2; +} + diff --git a/applications/minotari_app_grpc/proto/types.proto b/applications/minotari_app_grpc/proto/types.proto index f44696fc30..2642c232fa 100644 --- a/applications/minotari_app_grpc/proto/types.proto +++ b/applications/minotari_app_grpc/proto/types.proto @@ -126,8 +126,8 @@ message ConsensusConstants { uint64 block_weight_kernels = 16; uint64 pre_mine_value = 17; uint64 max_script_byte_size = 18; - uint64 vn_registration_max_vns_initial_epoch = 19; - uint64 vn_registration_max_vns_per_epoch = 20; + uint32 vn_registration_max_vns_initial_epoch = 19; + uint32 vn_registration_max_vns_per_epoch = 20; uint64 effective_from_height = 21; Range valid_blockchain_version_range = 22; uint64 max_randomx_seed_height = 23; diff --git a/applications/minotari_app_grpc/proto/wallet.proto b/applications/minotari_app_grpc/proto/wallet.proto index ce4fb17360..d7a0c3c621 100644 --- a/applications/minotari_app_grpc/proto/wallet.proto +++ b/applications/minotari_app_grpc/proto/wallet.proto @@ -83,6 +83,7 @@ service Wallet { // Get calculated fees for a `CreateTemplateRegistrationRequest` rpc GetTemplateRegistrationFee(CreateTemplateRegistrationRequest) returns (GetTemplateRegistrationFeeResponse); + rpc SubmitValidatorEvictionProof(SubmitValidatorEvictionProofRequest) returns (SubmitValidatorEvictionProofResponse); } message GetVersionRequest {} @@ -366,3 +367,14 @@ message RegisterValidatorNodeResponse { bool is_success = 2; string failure_message = 3; } + +message SubmitValidatorEvictionProofRequest { + EvictionProof proof = 1; + uint64 fee_per_gram = 2; + string message = 3; + bytes sidechain_deployment_key = 4; +} + +message SubmitValidatorEvictionProofResponse { + uint64 tx_id = 1; +} diff --git a/applications/minotari_app_grpc/src/conversions/mod.rs b/applications/minotari_app_grpc/src/conversions/mod.rs index 7748035be5..e2b6225040 100644 --- a/applications/minotari_app_grpc/src/conversions/mod.rs +++ b/applications/minotari_app_grpc/src/conversions/mod.rs @@ -20,28 +20,26 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -pub mod aggregate_body; -pub mod base_node_state; -pub mod block; -pub mod block_header; -pub mod chain_metadata; -pub mod com_and_pub_signature; -pub mod commitment_signature; -pub mod consensus_constants; -pub mod historical_block; -pub mod new_block_template; -pub mod output_features; -pub mod peer; -pub mod proof_of_work; -pub mod sidechain_feature; -pub mod signature; -pub mod transaction; -pub mod transaction_input; -pub mod transaction_kernel; -pub mod transaction_output; -pub mod unblinded_output; -pub mod validator_node_change; -pub mod validator_node_registration; +mod aggregate_body; +mod base_node_state; +mod block; +mod block_header; +mod chain_metadata; +mod com_and_pub_signature; +mod commitment_signature; +mod consensus_constants; +mod historical_block; +mod new_block_template; +mod output_features; +mod peer; +mod proof_of_work; +mod sidechain_feature; +mod signature; +mod transaction; +mod transaction_input; +mod transaction_kernel; +mod transaction_output; +mod unblinded_output; use prost_types::Timestamp; @@ -61,12 +59,6 @@ impl From for grpc::IntegerValue { } } -impl From for grpc::StringValue { - fn from(value: String) -> Self { - Self { value } - } -} - impl From for grpc::HeightRequest { fn from(b: BlockGroupRequest) -> Self { Self { diff --git a/applications/minotari_app_grpc/src/conversions/output_features.rs b/applications/minotari_app_grpc/src/conversions/output_features.rs index e7927a1626..99991df387 100644 --- a/applications/minotari_app_grpc/src/conversions/output_features.rs +++ b/applications/minotari_app_grpc/src/conversions/output_features.rs @@ -38,11 +38,7 @@ impl TryFrom for OutputFeatures { type Error = String; fn try_from(features: grpc::OutputFeatures) -> Result { - let sidechain_feature = features - .sidechain_feature - .and_then(|f| f.side_chain_feature) - .map(SideChainFeature::try_from) - .transpose()?; + let sidechain_feature = features.sidechain_feature.map(SideChainFeature::try_from).transpose()?; let output_type = features .output_type diff --git a/applications/minotari_app_grpc/src/conversions/sidechain_feature.rs b/applications/minotari_app_grpc/src/conversions/sidechain_feature.rs index 1538782020..464cf5bb97 100644 --- a/applications/minotari_app_grpc/src/conversions/sidechain_feature.rs +++ b/applications/minotari_app_grpc/src/conversions/sidechain_feature.rs @@ -22,17 +22,43 @@ use std::convert::{TryFrom, TryInto}; -use tari_common_types::types::{PublicKey, Signature}; -use tari_core::transactions::transaction_components::{ - BuildInfo, - CodeTemplateRegistration, - ConfidentialOutputData, - SideChainFeature, - TemplateType, - ValidatorNodeRegistration, - ValidatorNodeSignature, +use prost::Message; +use tari_common_types::{ + epoch::VnEpoch, + types::{PublicKey, Signature}, +}; +use tari_core::{ + base_node::comms_interface::ValidatorNodeChange, + transactions::{ + tari_amount::MicroMinotari, + transaction_components::{ + BuildInfo, + CodeTemplateRegistration, + ConfidentialOutputData, + SideChainFeature, + SideChainFeatureData, + SideChainId, + TemplateType, + ValidatorNodeRegistration, + ValidatorNodeSignature, + }, + }, }; use tari_max_size::MaxSizeString; +use tari_sidechain::{ + ChainLink, + CommandCommitProof, + CommandCommitProofV1, + CommitProofElement, + EvictNodeAtom, + EvictionProof, + QuorumCertificate, + QuorumDecision, + ShardGroup, + SidechainBlockCommitProof, + SidechainBlockHeader, + ValidatorQcSignature, +}; use tari_utilities::ByteArray; use crate::tari_rpc as grpc; @@ -41,45 +67,90 @@ use crate::tari_rpc as grpc; impl From for grpc::SideChainFeature { fn from(value: SideChainFeature) -> Self { Self { - side_chain_feature: Some(value.into()), + feature: Some(value.data.into()), + sidechain_id: value.sidechain_id.as_ref().map(Into::into), } } } -impl From for grpc::side_chain_feature::SideChainFeature { - fn from(value: SideChainFeature) -> Self { +impl TryFrom for SideChainFeature { + type Error = String; + + fn try_from(value: grpc::SideChainFeature) -> Result { + Ok(Self { + data: value.feature.ok_or("Feature not provided")?.try_into()?, + sidechain_id: value.sidechain_id.map(TryInto::try_into).transpose()?, + }) + } +} + +impl From for grpc::side_chain_feature::Feature { + fn from(value: SideChainFeatureData) -> Self { match value { - SideChainFeature::ValidatorNodeRegistration(template_reg) => { - grpc::side_chain_feature::SideChainFeature::ValidatorNodeRegistration(template_reg.into()) + SideChainFeatureData::ValidatorNodeRegistration(template_reg) => { + grpc::side_chain_feature::Feature::ValidatorNodeRegistration(template_reg.into()) + }, + SideChainFeatureData::CodeTemplateRegistration(template_reg) => { + grpc::side_chain_feature::Feature::TemplateRegistration(template_reg.into()) }, - SideChainFeature::CodeTemplateRegistration(template_reg) => { - grpc::side_chain_feature::SideChainFeature::TemplateRegistration(template_reg.into()) + SideChainFeatureData::ConfidentialOutput(output_data) => { + grpc::side_chain_feature::Feature::ConfidentialOutput(output_data.into()) }, - SideChainFeature::ConfidentialOutput(output_data) => { - grpc::side_chain_feature::SideChainFeature::ConfidentialOutput(output_data.into()) + SideChainFeatureData::EvictionProof(proof) => { + grpc::side_chain_feature::Feature::EvictionProof(grpc::EvictionProof::from(&proof)) }, } } } -impl TryFrom for SideChainFeature { +impl TryFrom for SideChainFeatureData { type Error = String; - fn try_from(features: grpc::side_chain_feature::SideChainFeature) -> Result { + fn try_from(features: grpc::side_chain_feature::Feature) -> Result { match features { - grpc::side_chain_feature::SideChainFeature::ValidatorNodeRegistration(vn_reg) => { - Ok(SideChainFeature::ValidatorNodeRegistration(vn_reg.try_into()?)) + grpc::side_chain_feature::Feature::ValidatorNodeRegistration(vn_reg) => { + Ok(SideChainFeatureData::ValidatorNodeRegistration(vn_reg.try_into()?)) }, - grpc::side_chain_feature::SideChainFeature::TemplateRegistration(template_reg) => { - Ok(SideChainFeature::CodeTemplateRegistration(template_reg.try_into()?)) + grpc::side_chain_feature::Feature::TemplateRegistration(template_reg) => { + Ok(SideChainFeatureData::CodeTemplateRegistration(template_reg.try_into()?)) }, - grpc::side_chain_feature::SideChainFeature::ConfidentialOutput(output_data) => { - Ok(SideChainFeature::ConfidentialOutput(output_data.try_into()?)) + grpc::side_chain_feature::Feature::ConfidentialOutput(output_data) => { + Ok(SideChainFeatureData::ConfidentialOutput(output_data.try_into()?)) + }, + grpc::side_chain_feature::Feature::EvictionProof(proof) => { + Ok(SideChainFeatureData::EvictionProof(proof.try_into()?)) }, } } } +// -------------------------------- SideChainId -------------------------------- // + +impl TryFrom for SideChainId { + type Error = String; + + fn try_from(value: grpc::SideChainId) -> Result { + let public_key = + PublicKey::from_canonical_bytes(&value.public_key).map_err(|e| format!("sidechain_id: {}", e))?; + let knowledge_proof = value + .knowledge_proof + .ok_or("sidechain_id knowledge_proof not provided")?; + let knowledge_proof = + Signature::try_from(knowledge_proof).map_err(|e| format!("sidechain_id_knowledge_proof: {}", e))?; + + Ok(Self::new(public_key, knowledge_proof)) + } +} + +impl From<&SideChainId> for grpc::SideChainId { + fn from(value: &SideChainId) -> Self { + Self { + public_key: value.public_key().to_vec(), + knowledge_proof: Some(value.knowledge_proof().into()), + } + } +} + // -------------------------------- ValidatorNodeRegistration -------------------------------- // impl TryFrom for ValidatorNodeRegistration { type Error = String; @@ -90,16 +161,6 @@ impl TryFrom for ValidatorNodeRegistration { let claim_public_key = PublicKey::from_canonical_bytes(&value.claim_public_key) .map_err(|e| format!("Invalid claim public key: {}", e))?; - let sidechain_id = if value.sidechain_id.is_empty() { - None - } else { - Some(PublicKey::from_canonical_bytes(&value.sidechain_id).map_err(|e| format!("sidechain_id: {}", e))?) - }; - let sidechain_id_knowledge_proof = value - .sidechain_id_knowledge_proof - .map(|v| Signature::try_from(v).map_err(|e| format!("sidechain_id_knowledge_proof: {}", e))) - .transpose()?; - Ok(ValidatorNodeRegistration::new( ValidatorNodeSignature::new( public_key, @@ -109,21 +170,25 @@ impl TryFrom for ValidatorNodeRegistration { .ok_or("signature not provided")??, ), claim_public_key, - sidechain_id, - sidechain_id_knowledge_proof, )) } } +impl From<&ValidatorNodeRegistration> for crate::tari_rpc::ValidatorNodeRegistration { + fn from(registration: &ValidatorNodeRegistration) -> Self { + Self { + public_key: registration.public_key().to_vec(), + signature: Some(crate::tari_rpc::Signature { + public_nonce: registration.signature().get_public_nonce().to_vec(), + signature: registration.signature().get_signature().to_vec(), + }), + claim_public_key: registration.claim_public_key().to_vec(), + } + } +} impl From for grpc::ValidatorNodeRegistration { fn from(value: ValidatorNodeRegistration) -> Self { - Self { - public_key: value.public_key().to_vec(), - signature: Some(value.signature().into()), - claim_public_key: value.claim_public_key().to_vec(), - sidechain_id: value.sidechain_id().map(|v| v.to_vec()).unwrap_or_default(), - sidechain_id_knowledge_proof: value.sidechain_id_knowledge_proof().map(|v| v.into()), - } + Self::from(&value) } } @@ -132,15 +197,6 @@ impl TryFrom for CodeTemplateRegistration { type Error = String; fn try_from(value: grpc::TemplateRegistration) -> Result { - let sidechain_id = if value.sidechain_id.is_empty() { - None - } else { - Some(PublicKey::from_canonical_bytes(&value.sidechain_id).map_err(|e| format!("sidechain_id: {}", e))?) - }; - let sidechain_id_knowledge_proof = value - .sidechain_id_knowledge_proof - .map(|v| Signature::try_from(v).map_err(|e| format!("sidechain_id_knowledge_proof: {}", e))) - .transpose()?; Ok(Self { author_public_key: PublicKey::from_canonical_bytes(&value.author_public_key).map_err(|e| e.to_string())?, author_signature: value @@ -162,8 +218,6 @@ impl TryFrom for CodeTemplateRegistration { .ok_or("Build info not provided")??, binary_sha: value.binary_sha.try_into().map_err(|_| "Invalid commit sha")?, binary_url: MaxSizeString::try_from(value.binary_url).map_err(|e| e.to_string())?, - sidechain_id, - sidechain_id_knowledge_proof, }) } } @@ -179,8 +233,6 @@ impl From for grpc::TemplateRegistration { build_info: Some(value.build_info.into()), binary_sha: value.binary_sha.to_vec(), binary_url: value.binary_url.to_string(), - sidechain_id: value.sidechain_id.map(|v| v.to_vec()).unwrap_or_default(), - sidechain_id_knowledge_proof: value.sidechain_id_knowledge_proof.map(|v| v.into()), } } } @@ -190,19 +242,8 @@ impl TryFrom for ConfidentialOutputData { type Error = String; fn try_from(value: grpc::ConfidentialOutputData) -> Result { - let sidechain_id = if value.sidechain_id.is_empty() { - None - } else { - Some(PublicKey::from_canonical_bytes(&value.sidechain_id).map_err(|e| format!("sidechain_id: {}", e))?) - }; - let sidechain_id_knowledge_proof = value - .sidechain_id_knowledge_proof - .map(|v| Signature::try_from(v).map_err(|e| format!("sidechain_id_knowledge_proof: {}", e))) - .transpose()?; Ok(ConfidentialOutputData { claim_public_key: PublicKey::from_canonical_bytes(&value.claim_public_key).map_err(|e| e.to_string())?, - sidechain_id, - sidechain_id_knowledge_proof, }) } } @@ -211,8 +252,6 @@ impl From for grpc::ConfidentialOutputData { fn from(value: ConfidentialOutputData) -> Self { Self { claim_public_key: value.claim_public_key.to_vec(), - sidechain_id: value.sidechain_id.map(|v| v.to_vec()).unwrap_or_default(), - sidechain_id_knowledge_proof: value.sidechain_id_knowledge_proof.map(|v| v.into()), } } } @@ -272,3 +311,390 @@ impl From for grpc::BuildInfo { } } } + +// -------------------------------- EvictionProof -------------------------------- // + +impl TryFrom for EvictionProof { + type Error = String; + + fn try_from(value: grpc::EvictionProof) -> Result { + let proof = value.proof.ok_or("proof not provided")?.try_into()?; + Ok(EvictionProof::new(proof)) + } +} + +impl From<&EvictionProof> for grpc::EvictionProof { + fn from(value: &EvictionProof) -> Self { + Self { + proof: Some(value.proof().into()), + } + } +} + +// -------------------------------- Commit proof -------------------------------- // + +impl TryFrom for CommandCommitProof { + type Error = String; + + fn try_from(value: grpc::CommitProof) -> Result { + match value.version.ok_or("version not provided")? { + grpc::commit_proof::Version::V1(v1) => Ok(Self::V1(v1.try_into()?)), + } + } +} + +impl From<&CommandCommitProof> for grpc::CommitProof { + fn from(value: &CommandCommitProof) -> Self { + match value { + CommandCommitProof::V1(v1) => Self { + version: Some(grpc::commit_proof::Version::V1(v1.into())), + }, + } + } +} + +impl TryFrom for CommandCommitProofV1 { + type Error = String; + + fn try_from(value: grpc::CommitProofV1) -> Result { + let command = grpc::EvictAtom::decode(value.command.as_slice()).map_err(|e| e.to_string())?; + Ok(CommandCommitProofV1 { + command: command.try_into()?, + commit_proof: value.commit_proof.ok_or("commit_proof not provided")?.try_into()?, + }) + } +} + +impl From<&CommandCommitProofV1> for grpc::CommitProofV1 { + fn from(value: &CommandCommitProofV1) -> Self { + Self { + command: grpc::EvictAtom::from(value.command()).encode_to_vec(), + commit_proof: Some(value.commit_proof().into()), + } + } +} + +// -------------------------------- SidechainBlockCommitProof -------------------------------- // + +impl TryFrom for SidechainBlockCommitProof { + type Error = String; + + fn try_from(value: grpc::SidechainBlockCommitProof) -> Result { + Ok(Self { + header: value.header.ok_or("header not provided")?.try_into()?, + proof_elements: value + .proof_elements + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + }) + } +} + +impl From<&SidechainBlockCommitProof> for grpc::SidechainBlockCommitProof { + fn from(value: &SidechainBlockCommitProof) -> Self { + Self { + header: Some(value.header().into()), + proof_elements: value.proof_elements().iter().map(Into::into).collect(), + } + } +} + +// -------------------------------- SidechainBlockHeader -------------------------------- // + +impl TryFrom for SidechainBlockHeader { + type Error = String; + + fn try_from(value: grpc::SidechainBlockHeader) -> Result { + let network_byte = u8::try_from(value.network).map_err(|_| "Invalid network byte: overflows u8".to_string())?; + Ok(Self { + network: network_byte, + parent_id: value.parent_id.try_into().map_err(|_| "Invalid parent id")?, + justify_id: value.justify_id.try_into().map_err(|_| "Invalid justify id")?, + height: value.height, + epoch: value.epoch, + shard_group: value.shard_group.ok_or("missing shard_group")?.try_into()?, + proposed_by: PublicKey::from_canonical_bytes(&value.proposed_by) + .map_err(|_| "Invalid proposed_by public key")?, + total_leader_fee: value.total_leader_fee, + state_merkle_root: value + .state_merkle_root + .try_into() + .map_err(|_| "Invalid state merkle root")?, + command_merkle_root: value + .command_merkle_root + .try_into() + .map_err(|_| "Invalid command merkle root")?, + is_dummy: value.is_dummy, + foreign_indexes_hash: value + .foreign_indexes_hash + .try_into() + .map_err(|_| "Invalid foreign indexes hash")?, + signature: value + .signature + .ok_or("SidechainBlockHeader signature not provided")? + .try_into() + .map_err(|_| "Invalid signature")?, + timestamp: value.timestamp, + base_layer_block_height: value.base_layer_block_height, + base_layer_block_hash: value + .base_layer_block_hash + .try_into() + .map_err(|_| "Invalid base layer block hash")?, + extra_data_hash: value + .extra_data_hash + .try_into() + .map_err(|_| "Invalid extra data hash")?, + }) + } +} + +impl From<&SidechainBlockHeader> for grpc::SidechainBlockHeader { + fn from(value: &SidechainBlockHeader) -> Self { + Self { + network: u32::from(value.network), + parent_id: value.parent_id.to_vec(), + justify_id: value.justify_id.to_vec(), + height: value.height, + epoch: value.epoch, + shard_group: Some(value.shard_group.into()), + proposed_by: value.proposed_by.to_vec(), + total_leader_fee: value.total_leader_fee, + state_merkle_root: value.state_merkle_root.to_vec(), + command_merkle_root: value.command_merkle_root.to_vec(), + is_dummy: value.is_dummy, + foreign_indexes_hash: value.foreign_indexes_hash.to_vec(), + signature: Some(value.signature().into()), + timestamp: value.timestamp, + base_layer_block_height: value.base_layer_block_height, + base_layer_block_hash: value.base_layer_block_hash.to_vec(), + extra_data_hash: value.extra_data_hash.to_vec(), + } + } +} + +// -------------------------------- CommitProofElement -------------------------------- // + +impl TryFrom for CommitProofElement { + type Error = String; + + fn try_from(value: grpc::CommitProofElement) -> Result { + match value.proof_element.ok_or("proof element not provided")? { + grpc::commit_proof_element::ProofElement::QuorumCertificate(qc) => { + Ok(CommitProofElement::QuorumCertificate(qc.try_into()?)) + }, + grpc::commit_proof_element::ProofElement::DummyChain(chain) => Ok(CommitProofElement::DummyChain( + chain + .chain_links + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + )), + } + } +} + +impl From<&CommitProofElement> for grpc::CommitProofElement { + fn from(value: &CommitProofElement) -> Self { + match value { + CommitProofElement::QuorumCertificate(qc) => Self { + proof_element: Some(grpc::commit_proof_element::ProofElement::QuorumCertificate(qc.into())), + }, + CommitProofElement::DummyChain(chain) => Self { + proof_element: Some(grpc::commit_proof_element::ProofElement::DummyChain(grpc::DummyChain { + chain_links: chain.iter().map(Into::into).collect(), + })), + }, + } + } +} + +// -------------------------------- ChainLink -------------------------------- // + +impl TryFrom for ChainLink { + type Error = String; + + fn try_from(value: grpc::ChainLink) -> Result { + Ok(Self { + header_hash: value.header_hash.try_into().map_err(|_| "Invalid block id")?, + parent_id: value.parent_id.try_into().map_err(|_| "Invalid parent id")?, + }) + } +} + +impl From<&ChainLink> for grpc::ChainLink { + fn from(value: &ChainLink) -> Self { + Self { + header_hash: value.header_hash.to_vec(), + parent_id: value.parent_id.to_vec(), + } + } +} + +// -------------------------------- QuorumCertificate -------------------------------- // + +impl TryFrom for QuorumCertificate { + type Error = String; + + fn try_from(value: grpc::QuorumCertificate) -> Result { + Ok(Self { + header_hash: value.header_hash.try_into().map_err(|_| "Invalid block body hash")?, + parent_id: value.parent_id.try_into().map_err(|_| "Invalid parent id")?, + signatures: value + .signatures + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + decision: grpc::QuorumDecision::try_from(value.decision) + .map_err(|e| format!("Invalid QuorumDecision: {e}"))? + .into(), + }) + } +} + +impl From<&QuorumCertificate> for grpc::QuorumCertificate { + fn from(value: &QuorumCertificate) -> Self { + Self { + parent_id: value.parent_id.to_vec(), + header_hash: value.header_hash.to_vec(), + signatures: value.signatures.iter().map(Into::into).collect(), + decision: grpc::QuorumDecision::from(value.decision).into(), + } + } +} + +// -------------------------------- QuorumDecision -------------------------------- // + +impl From for QuorumDecision { + fn from(value: grpc::QuorumDecision) -> Self { + match value { + grpc::QuorumDecision::Accept => QuorumDecision::Accept, + grpc::QuorumDecision::Reject => QuorumDecision::Reject, + } + } +} + +impl From for grpc::QuorumDecision { + fn from(value: QuorumDecision) -> Self { + match value { + QuorumDecision::Accept => grpc::QuorumDecision::Accept, + QuorumDecision::Reject => grpc::QuorumDecision::Reject, + } + } +} + +// -------------------------------- ValidatorSignature -------------------------------- // + +impl TryFrom for ValidatorQcSignature { + type Error = String; + + fn try_from(value: grpc::ValidatorSignature) -> Result { + Ok(Self { + public_key: PublicKey::from_canonical_bytes(&value.public_key).map_err(|e| e.to_string())?, + signature: value.signature.ok_or("signature not provided")?.try_into()?, + }) + } +} + +impl From<&ValidatorQcSignature> for grpc::ValidatorSignature { + fn from(value: &ValidatorQcSignature) -> Self { + Self { + public_key: value.public_key().to_vec(), + signature: Some(value.signature().into()), + } + } +} + +// -------------------------------- EvictNodeAtom -------------------------------- // + +impl TryFrom for EvictNodeAtom { + type Error = String; + + fn try_from(value: grpc::EvictAtom) -> Result { + Ok(Self::new( + PublicKey::from_canonical_bytes(&value.public_key).map_err(|e| e.to_string())?, + )) + } +} + +impl From<&EvictNodeAtom> for grpc::EvictAtom { + fn from(value: &EvictNodeAtom) -> Self { + Self { + public_key: value.node_to_evict().to_vec(), + } + } +} + +// -------------------------------- ValidatorNodeChange -------------------------------- // + +impl TryFrom for ValidatorNodeChange { + type Error = String; + + fn try_from(value: grpc::ValidatorNodeChange) -> Result { + let change = value.change.ok_or("change not provided")?; + match change { + grpc::validator_node_change::Change::Add(add) => { + let activation_epoch = VnEpoch(add.activation_epoch); + let registration = add.registration.ok_or("registration not provided")?.try_into()?; + let minimum_value_promise = MicroMinotari(add.minimum_value_promise); + + Ok(ValidatorNodeChange::Add { + registration, + activation_epoch, + minimum_value_promise, + }) + }, + grpc::validator_node_change::Change::Remove(remove) => { + let public_key = PublicKey::from_canonical_bytes(&remove.public_key).map_err(|e| e.to_string())?; + Ok(ValidatorNodeChange::Remove { public_key }) + }, + } + } +} + +impl From<&ValidatorNodeChange> for grpc::ValidatorNodeChange { + fn from(node_change: &ValidatorNodeChange) -> Self { + match node_change { + ValidatorNodeChange::Add { + registration, + activation_epoch, + minimum_value_promise, + } => Self { + change: Some(grpc::validator_node_change::Change::Add(grpc::ValidatorNodeChangeAdd { + activation_epoch: activation_epoch.as_u64(), + registration: Some(registration.into()), + minimum_value_promise: (*minimum_value_promise).into(), + })), + }, + ValidatorNodeChange::Remove { public_key } => Self { + change: Some(grpc::validator_node_change::Change::Remove( + grpc::ValidatorNodeChangeRemove { + public_key: public_key.to_vec(), + }, + )), + }, + } + } +} + +// -------------------------------- ShardGroup -------------------------------- // + +impl TryFrom for ShardGroup { + type Error = String; + + fn try_from(value: grpc::ShardGroup) -> Result { + Ok(Self { + start: value.start, + end_inclusive: value.end_inclusive, + }) + } +} + +impl From for grpc::ShardGroup { + fn from(value: ShardGroup) -> Self { + Self { + start: value.start, + end_inclusive: value.end_inclusive, + } + } +} diff --git a/applications/minotari_app_grpc/src/conversions/signature.rs b/applications/minotari_app_grpc/src/conversions/signature.rs index c3e5e8c2c5..a0f0fa93ca 100644 --- a/applications/minotari_app_grpc/src/conversions/signature.rs +++ b/applications/minotari_app_grpc/src/conversions/signature.rs @@ -20,31 +20,37 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{borrow::Borrow, convert::TryFrom}; +use std::convert::TryFrom; -use tari_common_types::types::{PrivateKey, PublicKey, Signature}; +use tari_common_types::types::{PrivateKey, PublicKey}; +use tari_crypto::{hashing::DomainSeparation, signatures::SchnorrSignature}; use tari_utilities::ByteArray; use crate::tari_rpc as grpc; -impl TryFrom for Signature { +impl TryFrom for SchnorrSignature { type Error = String; fn try_from(sig: grpc::Signature) -> Result { - let public_nonce = - PublicKey::from_canonical_bytes(&sig.public_nonce).map_err(|_| "Could not get public nonce".to_string())?; - let signature = - PrivateKey::from_canonical_bytes(&sig.signature).map_err(|_| "Could not get signature".to_string())?; + let public_nonce = PublicKey::from_canonical_bytes(&sig.public_nonce).map_err(|e| e.to_string())?; + let signature = PrivateKey::from_canonical_bytes(&sig.signature).map_err(|e| e.to_string())?; Ok(Self::new(public_nonce, signature)) } } - -impl> From for grpc::Signature { - fn from(sig: T) -> Self { +impl From<&SchnorrSignature> for grpc::Signature { + fn from(sig: &SchnorrSignature) -> Self { + Self { + public_nonce: sig.get_public_nonce().to_vec(), + signature: sig.get_signature().to_vec(), + } + } +} +impl From> for grpc::Signature { + fn from(sig: SchnorrSignature) -> Self { Self { - public_nonce: sig.borrow().get_public_nonce().to_vec(), - signature: sig.borrow().get_signature().to_vec(), + public_nonce: sig.get_public_nonce().to_vec(), + signature: sig.get_signature().to_vec(), } } } diff --git a/applications/minotari_app_grpc/src/conversions/validator_node_change.rs b/applications/minotari_app_grpc/src/conversions/validator_node_change.rs deleted file mode 100644 index f083e5bc3f..0000000000 --- a/applications/minotari_app_grpc/src/conversions/validator_node_change.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2024 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use tari_core::base_node::comms_interface::{ValidatorNodeChange, ValidatorNodeChangeState}; -use tari_utilities::ByteArray; - -impl From<&ValidatorNodeChange> for crate::tari_rpc::ValidatorNodeChange { - fn from(node_change: &ValidatorNodeChange) -> Self { - Self { - public_key: node_change.public_key.to_vec(), - state: match node_change.state { - ValidatorNodeChangeState::ADD => crate::tari_rpc::ValidatorNodeChangeState::Add.into(), - ValidatorNodeChangeState::REMOVE => crate::tari_rpc::ValidatorNodeChangeState::Remove.into(), - }, - start_height: node_change.height, - registration: Some((&node_change.registration).into()), - minimum_value_promise: node_change.minimum_value_promise.into(), - } - } -} diff --git a/applications/minotari_app_grpc/src/conversions/validator_node_registration.rs b/applications/minotari_app_grpc/src/conversions/validator_node_registration.rs deleted file mode 100644 index 69c9e71dfa..0000000000 --- a/applications/minotari_app_grpc/src/conversions/validator_node_registration.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2024 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use tari_core::transactions::transaction_components::ValidatorNodeRegistration; -use tari_utilities::ByteArray; - -impl From<&ValidatorNodeRegistration> for crate::tari_rpc::ValidatorNodeRegistration { - fn from(registration: &ValidatorNodeRegistration) -> Self { - Self { - public_key: registration.public_key().to_vec(), - signature: Some(crate::tari_rpc::Signature { - public_nonce: registration.signature().get_public_nonce().to_vec(), - signature: registration.signature().get_signature().to_vec(), - }), - claim_public_key: registration.claim_public_key().to_vec(), - sidechain_id: match registration.sidechain_id() { - None => vec![], - Some(id) => id.to_vec(), - }, - sidechain_id_knowledge_proof: registration.sidechain_id_knowledge_proof().map(|signature| { - crate::tari_rpc::Signature { - public_nonce: signature.get_public_nonce().to_vec(), - signature: signature.get_signature().to_vec(), - } - }), - } - } -} diff --git a/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs index fcec4fca9d..86bf0f8e65 100644 --- a/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -71,6 +71,8 @@ use minotari_app_grpc::tari_rpc::{ SendShaAtomicSwapResponse, SetBaseNodeRequest, SetBaseNodeResponse, + SubmitValidatorEvictionProofRequest, + SubmitValidatorEvictionProofResponse, TransactionDirection, TransactionEvent, TransactionEventRequest, @@ -1132,6 +1134,52 @@ impl wallet_server::Wallet for WalletGrpcServer { Ok(Response::new(GetTemplateRegistrationFeeResponse { fee: fee.as_u64() })) } + + async fn submit_validator_eviction_proof( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let mut transaction_service = self.get_transaction_service(); + + let sidechain_key = Some(request.sidechain_deployment_key) + .filter(|k| !k.is_empty()) + .map(|k| PrivateKey::from_canonical_bytes(&k)) + .transpose() + .map_err(|_| Status::invalid_argument("sidechain_deployment_key is malformed"))?; + + let proof = request + .proof + .map(TryInto::try_into) + .ok_or_else(|| Status::invalid_argument("Proof is missing"))? + .map_err(|e| { + error!(target: LOG_TARGET, "Failed to convert proof: {}", e); + Status::invalid_argument(format!("Invalid proof: {e}")) + })?; + + let constants = self.get_consensus_constants().map_err(|e| { + error!(target: LOG_TARGET, "Failed to get consensus constants: {}", e); + Status::internal("failed to fetch consensus constants") + })?; + + let response = match transaction_service + .submit_validator_eviction_proof( + constants.validator_node_registration_min_deposit_amount(), + proof, + request.fee_per_gram.into(), + sidechain_key, + request.message, + ) + .await + { + Ok(tx) => SubmitValidatorEvictionProofResponse { tx_id: tx.as_u64() }, + Err(e) => { + error!(target: LOG_TARGET, "Transaction service error: {}", e); + return Err(Status::unknown(e.to_string())); + }, + }; + Ok(Response::new(response)) + } } async fn handle_completed_tx( diff --git a/applications/minotari_node/src/grpc/base_node_grpc_server.rs b/applications/minotari_node/src/grpc/base_node_grpc_server.rs index 2594d021c4..15661f6a88 100644 --- a/applications/minotari_node/src/grpc/base_node_grpc_server.rs +++ b/applications/minotari_node/src/grpc/base_node_grpc_server.rs @@ -35,6 +35,7 @@ use minotari_app_grpc::{ tari_rpc::{CalcType, GetValidatorNodeChangesRequest, GetValidatorNodeChangesResponse, Sorting}, }; use tari_common_types::{ + epoch::VnEpoch, key_branches::TransactionKeyManagerBranch, tari_address::TariAddress, types::{Commitment, FixedHash, PublicKey, Signature}, @@ -1944,9 +1945,16 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { .await } - async fn get_version(&self, _request: Request) -> Result, Status> { + async fn get_version( + &self, + _request: Request, + ) -> Result, Status> { self.check_method_enabled(GrpcMethod::GetVersion)?; - Ok(Response::new(env!("CARGO_PKG_VERSION").to_string().into())) + let resp = tari_rpc::BaseNodeGetVersionResponse { + version: env!("CARGO_PKG_VERSION").to_string(), + network: u32::from(self.network.as_network().as_byte()), + }; + Ok(Response::new(resp)) } async fn check_for_updates( @@ -2266,21 +2274,26 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { let public_key = PublicKey::from_canonical_bytes(&request.public_key) .map_err(|e| obscure_error_if_true(report_error_flag, Status::invalid_argument(e.to_string())))?; - let shard_key = handler.get_shard_key(request.height, public_key).await.map_err(|e| { - error!(target: LOG_TARGET, "Error {}", e); - obscure_error_if_true(report_error_flag, Status::internal(e.to_string())) - })?; - if let Some(shard_key) = shard_key { - Ok(Response::new(tari_rpc::GetShardKeyResponse { - shard_key: shard_key.to_vec(), - found: true, - })) - } else { - Ok(Response::new(tari_rpc::GetShardKeyResponse { - shard_key: vec![], - found: false, - })) + let epoch = request.epoch; + + let validator_node = handler + .get_validator_node(None, public_key) + .await + .map_err(|e| { + error!(target: LOG_TARGET, "Error {}", e); + obscure_error_if_true(report_error_flag, Status::internal(e.to_string())) + })? + .ok_or_else(|| Status::not_found("Validator node not found"))?; + + if validator_node.activation_epoch.as_u64() > epoch { + return Err(Status::not_found(format!( + "Validator node found but not active for epoch {epoch}" + ))); } + + Ok(Response::new(tari_rpc::GetShardKeyResponse { + shard_key: validator_node.shard_key.to_vec(), + })) } async fn get_active_validator_nodes( @@ -2438,12 +2451,7 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { .filter(|x| !x.is_empty()) .map(FixedHash::try_from) .transpose() - .map_err(|e| { - obscure_error_if_true( - report_error_flag, - Status::invalid_argument(format!("Invalid start_hash '{}'", e)), - ) - })?; + .map_err(|e| Status::invalid_argument(format!("Invalid start_hash '{}'", e)))?; let mut node_service = self.node_service.clone(); @@ -2452,14 +2460,12 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { .get_header_by_hash(hash) .await .map_err(|err| obscure_error_if_true(self.report_grpc_error, Status::internal(err.to_string())))? - .ok_or_else(|| obscure_error_if_true(report_error_flag, Status::not_found("Start hash not found")))?, + .ok_or_else(|| Status::not_found("Start hash not found"))?, None => node_service .get_header(0) .await .map_err(|err| obscure_error_if_true(self.report_grpc_error, Status::internal(err.to_string())))? - .ok_or_else(|| { - obscure_error_if_true(report_error_flag, Status::unavailable("Genesis block not available")) - })?, + .ok_or_else(|| Status::unavailable("Genesis block not available"))?, }; if request.count == 0 { @@ -2467,12 +2473,9 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { } let start_height = start_header.height(); - let end_height = start_height.checked_add(request.count - 1).ok_or_else(|| { - obscure_error_if_true( - report_error_flag, - Status::invalid_argument("Request start height + count overflows u64"), - ) - })?; + let end_height = start_height + .checked_add(request.count - 1) + .ok_or_else(|| Status::invalid_argument("Request start height + count overflows u64"))?; task::spawn(async move { let mut current_header = start_header; @@ -2500,7 +2503,7 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { let sidechain_outputs = utxos .into_iter() - .filter(|u| u.features.output_type.is_sidechain_type()) + .filter(|u| u.features.output_type.is_sidechain_type() || u.is_burned_to_sidechain()) .map(TryInto::try_into) .collect::, _>>(); @@ -2563,17 +2566,16 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { let mut handler = self.node_service.clone(); - let sidechain_id = if request.sidechain_id.is_empty() { - None - } else { - Some( - PublicKey::from_canonical_bytes(&request.sidechain_id) - .map_err(|e| Status::invalid_argument(format!("Invalid sidechain_id '{}'", e)))?, - ) - }; + let sidechain_id = Some(request.sidechain_id) + .filter(|id| !id.is_empty()) + .map(|id| { + PublicKey::from_canonical_bytes(&id) + .map_err(|e| Status::invalid_argument(format!("Invalid sidechain_id '{}'", e))) + }) + .transpose()?; let changes = handler - .get_validator_node_changes(request.start_height, request.end_height, sidechain_id) + .get_validator_node_changes(sidechain_id, VnEpoch(request.epoch)) .await .map_err(|error| { warn!(target: LOG_TARGET, "Base node service error: {}", error); diff --git a/base_layer/common_types/Cargo.toml b/base_layer/common_types/Cargo.toml index c7fa39724c..2c2443ad36 100644 --- a/base_layer/common_types/Cargo.toml +++ b/base_layer/common_types/Cargo.toml @@ -11,6 +11,7 @@ tari_crypto = { version = "0.21.0" } tari_utilities = { version = "0.8" } tari_common = { path = "../../common", version = "1.7.0-pre.3" } minotari_ledger_wallet_common = { path = "../../applications/minotari_ledger_wallet/common" } + chacha20poly1305 = "0.10.1" bitflags = { version = "2.4", features = ["serde"] } borsh = "1.5" @@ -33,5 +34,4 @@ default = [] [package.metadata.cargo-machete] ignored = [ "strum", - "strum_macros", ] # this is so we can run cargo machete without getting false positive about macro dependancies diff --git a/base_layer/common_types/src/epoch.rs b/base_layer/common_types/src/epoch.rs index dbe84d3058..5c2d8c9de9 100644 --- a/base_layer/common_types/src/epoch.rs +++ b/base_layer/common_types/src/epoch.rs @@ -21,15 +21,34 @@ // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH // DAMAGE. -use std::str::FromStr; +use std::{fmt::Display, str::FromStr}; +use borsh::{BorshDeserialize, BorshSerialize}; use newtype_ops::newtype_ops; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + Default, + BorshSerialize, + BorshDeserialize, +)] pub struct VnEpoch(pub u64); impl VnEpoch { + pub const fn zero() -> Self { + VnEpoch(0) + } + pub fn as_u64(&self) -> u64 { self.0 } @@ -54,3 +73,9 @@ impl FromStr for VnEpoch { Ok(VnEpoch(s.parse::().map_err(|e| e.to_string())?)) } } + +impl Display for VnEpoch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Epoch({})", self.0) + } +} diff --git a/base_layer/common_types/src/types/fixed_hash.rs b/base_layer/common_types/src/types/fixed_hash.rs index 253c6358a9..dbb4930ab3 100644 --- a/base_layer/common_types/src/types/fixed_hash.rs +++ b/base_layer/common_types/src/types/fixed_hash.rs @@ -52,6 +52,7 @@ pub struct FixedHashSizeError; BorshSerialize, BorshDeserialize, )] +#[serde(transparent)] pub struct FixedHash([u8; FixedHash::byte_size()]); impl FixedHash { diff --git a/base_layer/core/Cargo.toml b/base_layer/core/Cargo.toml index acfe58d00f..edf0541fa7 100644 --- a/base_layer/core/Cargo.toml +++ b/base_layer/core/Cargo.toml @@ -49,6 +49,7 @@ tari_key_manager = { path = "../key_manager", features = [ ], version = "1.7.0-pre.3" } tari_common_sqlite = { path = "../../common_sqlite" } tari_hashing = { path = "../../hashing" } +tari_sidechain = { path = "../sidechain" } async-trait = { version = "0.1.50" } bincode = "1.1.4" diff --git a/base_layer/core/src/base_node/comms_interface/comms_request.rs b/base_layer/core/src/base_node/comms_interface/comms_request.rs index b7250fc472..154a8dcb6c 100644 --- a/base_layer/core/src/base_node/comms_interface/comms_request.rs +++ b/base_layer/core/src/base_node/comms_interface/comms_request.rs @@ -26,7 +26,10 @@ use std::{ }; use serde::{Deserialize, Serialize}; -use tari_common_types::types::{BlockHash, Commitment, HashOutput, PrivateKey, PublicKey, Signature}; +use tari_common_types::{ + epoch::VnEpoch, + types::{BlockHash, Commitment, HashOutput, PrivateKey, PublicKey, Signature}, +}; use tari_utilities::hex::Hex; use crate::{blocks::NewBlockTemplate, chain_storage::MmrTree, proof_of_work::PowAlgorithm}; @@ -40,7 +43,7 @@ pub struct MmrStateRequest { } /// API Request enum -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum NodeCommsRequest { GetChainMetadata, FetchHeaders(RangeInclusive), @@ -66,12 +69,11 @@ pub enum NodeCommsRequest { validator_network: Option, }, FetchValidatorNodeChanges { - start_height: u64, - end_height: u64, + epoch: VnEpoch, sidechain_id: Option, }, - GetShardKey { - height: u64, + GetValidatorNode { + sidechain_id: Option, public_key: PublicKey, }, FetchTemplateRegistrations { @@ -83,7 +85,7 @@ pub enum NodeCommsRequest { }, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct GetNewBlockTemplateRequest { pub algo: PowAlgorithm, pub max_weight: u64, @@ -133,8 +135,11 @@ impl Display for NodeCommsRequest { .unwrap_or_else(|| "None".to_string()) ) }, - GetShardKey { height, public_key } => { - write!(f, "GetShardKey height ({}), public key ({:?})", height, public_key) + GetValidatorNode { + sidechain_id, + public_key, + } => { + write!(f, "GetShardKey ({:?}), public key ({:?})", sidechain_id, public_key) }, FetchTemplateRegistrations { start_height: start, @@ -145,15 +150,11 @@ impl Display for NodeCommsRequest { FetchUnspentUtxosInBlock { block_hash } => { write!(f, "FetchUnspentUtxosInBlock ({})", block_hash) }, - FetchValidatorNodeChanges { - start_height, - end_height, - sidechain_id, - } => { + FetchValidatorNodeChanges { epoch, sidechain_id } => { write!( f, - "FetchValidatorNodeChanges (Side chain ID:{:?}), Height range: {}-{}", - sidechain_id, start_height, end_height + "FetchValidatorNodeChanges (Side chain ID:{:?}), Epoch: {}", + sidechain_id, epoch ) }, } diff --git a/base_layer/core/src/base_node/comms_interface/comms_response.rs b/base_layer/core/src/base_node/comms_interface/comms_response.rs index 3d55bdfc81..f169937a2b 100644 --- a/base_layer/core/src/base_node/comms_interface/comms_response.rs +++ b/base_layer/core/src/base_node/comms_interface/comms_response.rs @@ -27,6 +27,7 @@ use std::{ use tari_common_types::{ chain_metadata::ChainMetadata, + epoch::VnEpoch, types::{HashOutput, PrivateKey, PublicKey}, }; @@ -63,7 +64,7 @@ pub enum NodeCommsResponse { FetchMempoolTransactionsByExcessSigsResponse(FetchMempoolTransactionsResponse), FetchValidatorNodesKeysResponse(Vec), FetchValidatorNodeChangesResponse(Vec), - GetShardKeyResponse(Option<[u8; 32]>), + GetValidatorNode(Option), FetchTemplateRegistrationsResponse(Vec), } @@ -100,7 +101,7 @@ impl Display for NodeCommsResponse { resp.not_found.len() ), FetchValidatorNodesKeysResponse(_) => write!(f, "FetchValidatorNodesKeysResponse"), - GetShardKeyResponse(_) => write!(f, "GetShardKeyResponse"), + GetValidatorNode(_) => write!(f, "GetShardKeyResponse"), FetchTemplateRegistrationsResponse(_) => write!(f, "FetchTemplateRegistrationsResponse"), FetchValidatorNodeChangesResponse(_) => write!(f, "FetchValidatorNodeChangesResponse"), } @@ -114,19 +115,15 @@ pub struct FetchMempoolTransactionsResponse { pub not_found: Vec, } -/// Represents a validator node state -#[derive(Debug, Clone)] -pub enum ValidatorNodeChangeState { - ADD, - REMOVE, -} - /// Represents a validator node state change #[derive(Debug, Clone)] -pub struct ValidatorNodeChange { - pub public_key: PublicKey, - pub state: ValidatorNodeChangeState, - pub registration: ValidatorNodeRegistration, - pub minimum_value_promise: MicroMinotari, - pub height: u64, +pub enum ValidatorNodeChange { + Add { + registration: ValidatorNodeRegistration, + activation_epoch: VnEpoch, + minimum_value_promise: MicroMinotari, + }, + Remove { + public_key: PublicKey, + }, } diff --git a/base_layer/core/src/base_node/comms_interface/inbound_handlers.rs b/base_layer/core/src/base_node/comms_interface/inbound_handlers.rs index c514cf8546..7b59565ba5 100644 --- a/base_layer/core/src/base_node/comms_interface/inbound_handlers.rs +++ b/base_layer/core/src/base_node/comms_interface/inbound_handlers.rs @@ -22,16 +22,11 @@ #[cfg(feature = "metrics")] use std::convert::{TryFrom, TryInto}; -use std::{ - cmp::max, - collections::{HashMap, HashSet}, - sync::Arc, - time::Instant, -}; +use std::{cmp::max, collections::HashSet, sync::Arc, time::Instant}; use log::*; use strum_macros::Display; -use tari_common_types::types::{BlockHash, FixedHash, HashOutput, PublicKey}; +use tari_common_types::types::{BlockHash, FixedHash, HashOutput}; use tari_comms::{connectivity::ConnectivityRequester, peer_manager::NodeId}; use tari_utilities::hex::Hex; use tokio::sync::RwLock; @@ -40,7 +35,7 @@ use tokio::sync::RwLock; use crate::base_node::metrics; use crate::{ base_node::comms_interface::{ - comms_response::{ValidatorNodeChange, ValidatorNodeChangeState}, + comms_response::ValidatorNodeChange, error::CommsInterfaceError, local_interface::BlockEventSender, FetchMempoolTransactionsResponse, @@ -428,9 +423,12 @@ where B: BlockchainBackend + 'static active_validator_nodes, )) }, - NodeCommsRequest::GetShardKey { height, public_key } => { - let shard_key = self.blockchain_db.get_shard_key(height, public_key).await?; - Ok(NodeCommsResponse::GetShardKeyResponse(shard_key)) + NodeCommsRequest::GetValidatorNode { + sidechain_id, + public_key, + } => { + let vn = self.blockchain_db.get_validator_node(sidechain_id, public_key).await?; + Ok(NodeCommsResponse::GetValidatorNode(vn)) }, NodeCommsRequest::FetchTemplateRegistrations { start_height, @@ -448,71 +446,30 @@ where B: BlockchainBackend + 'static let utxos = self.blockchain_db.fetch_outputs_in_block(block_hash).await?; Ok(NodeCommsResponse::TransactionOutputs(utxos)) }, - NodeCommsRequest::FetchValidatorNodeChanges { - start_height, - end_height, - sidechain_id, - } => { - let constants = self.consensus_manager.consensus_constants(start_height); - #[allow(clippy::mutable_key_type)] - let mut node_changes = HashMap::::new(); - let mut nodes = self + NodeCommsRequest::FetchValidatorNodeChanges { epoch, sidechain_id } => { + let added_validators = self .blockchain_db - .fetch_active_validator_nodes(start_height, sidechain_id.clone()) + .fetch_validators_activating_in_epoch(sidechain_id.clone(), epoch) .await?; - for height in start_height + 1..=end_height { - let current_nodes = self - .blockchain_db - .fetch_active_validator_nodes(height, sidechain_id.clone()) - .await?; - - // remove nodes - nodes.iter().for_each(|prev_node| { - let prev_exists_in_new_set = current_nodes - .iter() - .any(|current_node| prev_node.public_key == current_node.public_key); - if !prev_exists_in_new_set { - node_changes.insert(prev_node.public_key.clone(), ValidatorNodeChange { - public_key: prev_node.public_key.clone(), - state: ValidatorNodeChangeState::REMOVE, - registration: prev_node.original_registration.clone(), - minimum_value_promise: prev_node.minimum_value_promise, - height: constants.epoch_to_block_height(prev_node.start_epoch), - }); - } - }); - // add nodes - current_nodes.iter().for_each(|current_node| { - let new_exists_in_prev = nodes - .iter() - .any(|prev_node| current_node.public_key == prev_node.public_key); - if !new_exists_in_prev { - node_changes.insert(current_node.public_key.clone(), ValidatorNodeChange { - public_key: current_node.public_key.clone(), - state: ValidatorNodeChangeState::ADD, - registration: current_node.original_registration.clone(), - minimum_value_promise: current_node.minimum_value_promise, - height: constants.epoch_to_block_height(current_node.start_epoch), - }); - } - }); + let exit_validators = self + .blockchain_db + .fetch_validators_exiting_in_epoch(sidechain_id.clone(), epoch) + .await?; - nodes = current_nodes; - } + let mut node_changes = Vec::with_capacity(added_validators.len() + exit_validators.len()); - Ok(NodeCommsResponse::FetchValidatorNodeChangesResponse( - node_changes - .iter() - .map(|(pub_key, change)| ValidatorNodeChange { - public_key: pub_key.clone(), - state: change.state.clone(), - registration: change.registration.clone(), - minimum_value_promise: change.minimum_value_promise, - height: change.height, - }) - .collect(), - )) + node_changes.extend(added_validators.into_iter().map(|vn| ValidatorNodeChange::Add { + registration: vn.original_registration, + activation_epoch: vn.activation_epoch, + minimum_value_promise: vn.minimum_value_promise, + })); + + node_changes.extend(exit_validators.into_iter().map(|vn| ValidatorNodeChange::Remove { + public_key: vn.public_key, + })); + + Ok(NodeCommsResponse::FetchValidatorNodeChangesResponse(node_changes)) }, } } @@ -984,22 +941,7 @@ where B: BlockchainBackend + 'static details: format!("Output {} to be spent does not exist in db", input.output_hash()), })?; - let rp_hash = match output_mined_info.output.proof { - Some(proof) => proof.hash(), - None => FixedHash::zero(), - }; - input.add_output_data( - output_mined_info.output.version, - output_mined_info.output.features, - output_mined_info.output.commitment, - output_mined_info.output.script, - output_mined_info.output.sender_offset_public_key, - output_mined_info.output.covenant, - output_mined_info.output.encrypted_data, - output_mined_info.output.metadata_signature, - rp_hash, - output_mined_info.output.minimum_value_promise, - ); + input.add_output_data(output_mined_info.output); } debug!( target: LOG_TARGET, diff --git a/base_layer/core/src/base_node/comms_interface/local_interface.rs b/base_layer/core/src/base_node/comms_interface/local_interface.rs index 5f8af2d2a7..e32330823c 100644 --- a/base_layer/core/src/base_node/comms_interface/local_interface.rs +++ b/base_layer/core/src/base_node/comms_interface/local_interface.rs @@ -24,6 +24,7 @@ use std::{ops::RangeInclusive, sync::Arc}; use tari_common_types::{ chain_metadata::ChainMetadata, + epoch::VnEpoch, types::{BlockHash, Commitment, HashOutput, PublicKey, Signature}, }; use tari_service_framework::{reply_channel::SenderService, Service}; @@ -302,17 +303,12 @@ impl LocalNodeCommsInterface { pub async fn get_validator_node_changes( &mut self, - start_height: u64, - end_height: u64, sidechain_id: Option, + epoch: VnEpoch, ) -> Result, CommsInterfaceError> { match self .request_sender - .call(NodeCommsRequest::FetchValidatorNodeChanges { - start_height, - end_height, - sidechain_id, - }) + .call(NodeCommsRequest::FetchValidatorNodeChanges { epoch, sidechain_id }) .await?? { NodeCommsResponse::FetchValidatorNodeChangesResponse(validator_node_change) => Ok(validator_node_change), @@ -320,17 +316,20 @@ impl LocalNodeCommsInterface { } } - pub async fn get_shard_key( + pub async fn get_validator_node( &mut self, - height: u64, + sidechain_id: Option, public_key: PublicKey, - ) -> Result, CommsInterfaceError> { + ) -> Result, CommsInterfaceError> { match self .request_sender - .call(NodeCommsRequest::GetShardKey { height, public_key }) + .call(NodeCommsRequest::GetValidatorNode { + sidechain_id, + public_key, + }) .await?? { - NodeCommsResponse::GetShardKeyResponse(shard_key) => Ok(shard_key), + NodeCommsResponse::GetValidatorNode(vn) => Ok(vn), _ => Err(CommsInterfaceError::UnexpectedApiResponse), } } diff --git a/base_layer/core/src/base_node/comms_interface/mod.rs b/base_layer/core/src/base_node/comms_interface/mod.rs index 5804f072b0..a09f64a67f 100644 --- a/base_layer/core/src/base_node/comms_interface/mod.rs +++ b/base_layer/core/src/base_node/comms_interface/mod.rs @@ -24,12 +24,7 @@ mod comms_request; pub use comms_request::{GetNewBlockTemplateRequest, MmrStateRequest, NodeCommsRequest}; mod comms_response; -pub use comms_response::{ - FetchMempoolTransactionsResponse, - NodeCommsResponse, - ValidatorNodeChange, - ValidatorNodeChangeState, -}; +pub use comms_response::{FetchMempoolTransactionsResponse, NodeCommsResponse, ValidatorNodeChange}; mod error; pub use error::CommsInterfaceError; diff --git a/base_layer/core/src/base_node/rpc/service.rs b/base_layer/core/src/base_node/rpc/service.rs index 34b8a30d46..65c1bb041d 100644 --- a/base_layer/core/src/base_node/rpc/service.rs +++ b/base_layer/core/src/base_node/rpc/service.rs @@ -309,7 +309,7 @@ impl BaseNodeWalletService for BaseNodeWalletRpc let signature = Signature::try_from(sig).map_err(|_| RpcStatus::bad_request("Signature was invalid"))?; let response: TxQueryResponse = self.fetch_kernel(signature.clone()).await?; responses.push(TxQueryBatchResponse { - signature: Some(SignatureProto::from(signature)), + signature: Some(SignatureProto::from(&signature)), location: response.location, best_block_hash: response.best_block_hash, confirmations: response.confirmations, diff --git a/base_layer/core/src/chain_storage/active_validator_node.rs b/base_layer/core/src/chain_storage/active_validator_node.rs index 184d58a18a..1ca749008a 100644 --- a/base_layer/core/src/chain_storage/active_validator_node.rs +++ b/base_layer/core/src/chain_storage/active_validator_node.rs @@ -20,21 +20,47 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use std::cmp; + use serde::{Deserialize, Serialize}; use tari_common_types::{ epoch::VnEpoch, types::{Commitment, PublicKey}, }; -use crate::transactions::{tari_amount::MicroMinotari, transaction_components::ValidatorNodeRegistration}; +use crate::transactions::tari_amount::MicroMinotari; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ValidatorNodeEntry { pub shard_key: [u8; 32], - pub start_epoch: VnEpoch, + /// The epoch in which this validator node was (or will be) activated + pub activation_epoch: VnEpoch, + /// The epoch in which the validator registartion UTXO was submitted + pub registration_epoch: VnEpoch, pub public_key: PublicKey, pub commitment: Commitment, - pub sidechain_id: Option, - pub registration: ValidatorNodeRegistration, + pub sidechain_public_key: Option, pub minimum_value_promise: MicroMinotari, } + +impl Ord for ValidatorNodeEntry { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.sidechain_public_key + .cmp(&other.sidechain_public_key) + .then_with(|| self.shard_key.cmp(&other.shard_key)) + } +} + +impl PartialOrd for ValidatorNodeEntry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for ValidatorNodeEntry { + fn eq(&self, other: &Self) -> bool { + self.sidechain_public_key == other.sidechain_public_key && self.shard_key == other.shard_key + } +} + +impl Eq for ValidatorNodeEntry {} diff --git a/base_layer/core/src/chain_storage/async_db.rs b/base_layer/core/src/chain_storage/async_db.rs index 96721141ad..cdc275b291 100644 --- a/base_layer/core/src/chain_storage/async_db.rs +++ b/base_layer/core/src/chain_storage/async_db.rs @@ -31,6 +31,7 @@ use primitive_types::U256; use rand::{rngs::OsRng, RngCore}; use tari_common_types::{ chain_metadata::ChainMetadata, + epoch::VnEpoch, types::{BlockHash, Commitment, HashOutput, PublicKey, Signature}, }; use tari_utilities::epoch_time::EpochTime; @@ -284,7 +285,11 @@ impl AsyncBlockchainDb { make_async_fn!(fetch_active_validator_nodes(height: u64, validator_network: Option) -> Vec, "fetch_active_validator_nodes"); - make_async_fn!(get_shard_key(height:u64, public_key: PublicKey) -> Option<[u8;32]>, "get_shard_key"); + make_async_fn!(fetch_validators_activating_in_epoch(sidechain_pk: Option, epoch: VnEpoch) -> Vec, "fetch_validators_activating_in_epoch"); + + make_async_fn!(fetch_validators_exiting_in_epoch(sidechain_pk: Option, epoch: VnEpoch) -> Vec, "fetch_validators_exiting_in_epoch"); + + make_async_fn!(get_validator_node(sidechain_id: Option, public_key: PublicKey) -> Option, "get_validator_node"); make_async_fn!(fetch_template_registrations>(range: T) -> Vec, "fetch_template_registrations"); diff --git a/base_layer/core/src/chain_storage/blockchain_backend.rs b/base_layer/core/src/chain_storage/blockchain_backend.rs index 87542c49ec..4dfedba625 100644 --- a/base_layer/core/src/chain_storage/blockchain_backend.rs +++ b/base_layer/core/src/chain_storage/blockchain_backend.rs @@ -3,8 +3,10 @@ use tari_common_types::{ chain_metadata::ChainMetadata, + epoch::VnEpoch, types::{Commitment, HashOutput, PublicKey, Signature}, }; +use tari_sidechain::ShardGroup; use super::{TemplateRegistrationEntry, ValidatorNodeRegistrationInfo}; use crate::{ @@ -181,11 +183,55 @@ pub trait BlockchainBackend: Send + Sync { /// block body ordering. fn fetch_active_validator_nodes( &self, + sidechain_pk: Option<&PublicKey>, height: u64, - validator_network: Option, ) -> Result, ChainStorageError>; - /// Returns the shard key for the validator node if valid at the given height. - fn get_shard_key(&self, height: u64, public_key: PublicKey) -> Result, ChainStorageError>; + + fn fetch_validators_activating_in_epoch( + &self, + sidechain_pk: Option<&PublicKey>, + epoch: VnEpoch, + ) -> Result, ChainStorageError>; + + fn fetch_validators_exiting_in_epoch( + &self, + sidechain_pk: Option<&PublicKey>, + epoch: VnEpoch, + ) -> Result, ChainStorageError>; + /// Returns true if the validator node registration UTXO exists + fn validator_node_exists( + &self, + sidechain_pk: Option<&PublicKey>, + height: u64, + validator_node_pk: &PublicKey, + ) -> Result; + /// Returns true if the validator node is registered and currently active + fn validator_node_is_active( + &self, + sidechain_pk: Option<&PublicKey>, + end_epoch: VnEpoch, + validator_node_pk: &PublicKey, + ) -> Result; + + fn validator_node_is_active_for_shard_group( + &self, + sidechain_pk: Option<&PublicKey>, + epoch: VnEpoch, + validator_node_pk: &PublicKey, + shard_group: ShardGroup, + ) -> Result; + fn validator_nodes_count_for_shard_group( + &self, + sidechain_pk: Option<&PublicKey>, + end_epoch: VnEpoch, + shard_group: ShardGroup, + ) -> Result; + /// Returns the validator node for the given sidechain and public key if it exists + fn get_validator_node( + &self, + sidechain_pk: Option<&PublicKey>, + public_key: PublicKey, + ) -> Result, ChainStorageError>; /// Returns all template registrations within (inclusive) the given height range. fn fetch_template_registrations( &self, diff --git a/base_layer/core/src/chain_storage/blockchain_database.rs b/base_layer/core/src/chain_storage/blockchain_database.rs index ff467de6c4..9ef1e1d193 100644 --- a/base_layer/core/src/chain_storage/blockchain_database.rs +++ b/base_layer/core/src/chain_storage/blockchain_database.rs @@ -38,6 +38,7 @@ use primitive_types::U256; use serde::{Deserialize, Serialize}; use tari_common_types::{ chain_metadata::ChainMetadata, + epoch::VnEpoch, types::{BlockHash, Commitment, FixedHash, HashOutput, PublicKey, Signature}, }; use tari_hashing::TransactionHashDomain; @@ -945,9 +946,13 @@ where B: BlockchainBackend db.fetch_mmr_size(tree) } - pub fn get_shard_key(&self, height: u64, public_key: PublicKey) -> Result, ChainStorageError> { + pub fn get_validator_node( + &self, + sidechain_pk: Option, + public_key: PublicKey, + ) -> Result, ChainStorageError> { let db = self.db_read_access()?; - db.get_shard_key(height, public_key) + db.get_validator_node(sidechain_pk.as_ref(), public_key) } /// Tries to add a block to the longest chain. @@ -1297,10 +1302,28 @@ where B: BlockchainBackend pub fn fetch_active_validator_nodes( &self, height: u64, - validator_network: Option, + sidechain_pk: Option, + ) -> Result, ChainStorageError> { + let db = self.db_read_access()?; + db.fetch_active_validator_nodes(sidechain_pk.as_ref(), height) + } + + pub fn fetch_validators_activating_in_epoch( + &self, + sidechain_pk: Option, + epoch: VnEpoch, + ) -> Result, ChainStorageError> { + let db = self.db_read_access()?; + db.fetch_validators_activating_in_epoch(sidechain_pk.as_ref(), epoch) + } + + pub fn fetch_validators_exiting_in_epoch( + &self, + sidechain_pk: Option, + epoch: VnEpoch, ) -> Result, ChainStorageError> { let db = self.db_read_access()?; - db.fetch_active_validator_nodes(height, validator_network) + db.fetch_validators_exiting_in_epoch(sidechain_pk.as_ref(), epoch) } pub fn fetch_template_registrations>( @@ -1431,7 +1454,7 @@ pub fn calculate_mmr_roots( let validator_nodes = db.fetch_all_active_validator_nodes(block_height)?; (calculate_validator_node_mr(&validator_nodes)?, validator_nodes.len()) } else { - // MR is unchanged except for epoch boundary + // Active validator set never changes within epochs, so we can reuse the previous block VN MR let tip_header = fetch_header(db, block_height.saturating_sub(1))?; (tip_header.validator_node_mr, 0) }; @@ -1499,24 +1522,24 @@ pub fn calculate_validator_node_mr( } in validator_nodes { hash_map - .entry(sidechain_id.as_ref().map(|n| n.to_vec()).unwrap_or(vec![0u8; 32])) + .entry(sidechain_id.as_ref().map_or([0u8; 32].as_slice(), |n| n.as_bytes())) .or_insert_with(Vec::new) .push((pk, shard_key)); } let mut roots = HashMap::new(); - for (validator_network, set) in hash_map { + for (sidechain_pk, set) in hash_map { let mut sorted_set = set; sorted_set.sort_unstable_by(|(pk1, _), (pk2, _)| pk1.as_bytes().cmp(pk2.as_bytes())); let vn_bmt = ValidatorNodeBMT::create(sorted_set.iter().map(hash_node).collect()); - roots.insert(validator_network, vn_bmt.get_merkle_root()); + roots.insert(sidechain_pk, vn_bmt.get_merkle_root()); } - let mut keys = roots.keys().cloned().collect::>(); + let mut keys = roots.keys().copied().collect::>(); keys.sort(); let mut root_mt = SparseMerkleTree::::new(); for key in keys { - let node_key = NodeKey::try_from(key.as_slice())?; + let node_key = NodeKey::try_from(key)?; let value_hash = ValueHash::try_from(roots[&key].as_slice())?; root_mt.insert(node_key, value_hash)?; } @@ -1726,22 +1749,7 @@ fn fetch_block(db: &T, height: u64, compact: bool) -> Resu Err(e) => return Err(e), }; - let rp_hash = match utxo_mined_info.output.proof { - Some(proof) => proof.hash(), - None => FixedHash::zero(), - }; - compact_input.add_output_data( - utxo_mined_info.output.version, - utxo_mined_info.output.features, - utxo_mined_info.output.commitment, - utxo_mined_info.output.script, - utxo_mined_info.output.sender_offset_public_key, - utxo_mined_info.output.covenant, - utxo_mined_info.output.encrypted_data, - utxo_mined_info.output.metadata_signature, - rp_hash, - utxo_mined_info.output.minimum_value_promise, - ); + compact_input.add_output_data(utxo_mined_info.output); Ok(compact_input) }) .collect::, _>>()?; @@ -1791,16 +1799,23 @@ fn fetch_block_by_utxo_commitment( db: &T, commitment: &Commitment, ) -> Result, ChainStorageError> { - let output = db.fetch_unspent_output_hash_by_commitment(commitment)?; - match output { - Some(hash) => match db.fetch_output(&hash)? { - Some(mined_info) => fetch_block_by_hash(db, mined_info.header_hash, false), - None => Ok(None), - }, + match fetch_utxo_by_commitment(db, commitment)? { + Some(mined_info) => fetch_block_by_hash(db, mined_info.header_hash, false), None => Ok(None), } } +fn fetch_utxo_by_commitment( + db: &T, + commitment: &Commitment, +) -> Result, ChainStorageError> { + let output = db.fetch_unspent_output_hash_by_commitment(commitment)?; + output + .map(|hash| db.fetch_output(&hash)) + .transpose() + .map(|output| output.flatten()) +} + fn fetch_block_by_hash( db: &T, hash: BlockHash, diff --git a/base_layer/core/src/chain_storage/lmdb_db/composite_key.rs b/base_layer/core/src/chain_storage/lmdb_db/composite_key.rs index 6620166cfd..d4ac3a6f4d 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/composite_key.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/composite_key.rs @@ -21,24 +21,57 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use std::{ + convert::TryFrom, fmt::{Display, Formatter}, - ops::{Deref, DerefMut}, + ops::Deref, }; use lmdb_zero::traits::AsLmdbBytes; use tari_common_types::types::FixedHash; -use tari_utilities::hex::to_hex; use crate::chain_storage::ChainStorageError; +#[derive(Debug, Clone)] +enum SmallBytes { + Stack([u8; L]), + Heap(Box<[u8; L]>), +} + +impl SmallBytes { + pub fn as_slice(&self) -> &[u8] { + match self { + SmallBytes::Stack(b) => b.as_ref(), + SmallBytes::Heap(b) => b.as_ref(), + } + } + + pub fn as_mut_slice(&mut self) -> &mut [u8] { + match self { + SmallBytes::Stack(b) => b.as_mut(), + SmallBytes::Heap(b) => b.as_mut(), + } + } +} + +impl Deref for SmallBytes { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + match self { + SmallBytes::Stack(b) => b, + SmallBytes::Heap(b) => &**b, + } + } +} + #[derive(Debug, Clone)] pub(super) struct CompositeKey { - bytes: Box<[u8; L]>, + bytes: SmallBytes, len: usize, } impl CompositeKey { - pub fn new() -> Self { + pub(super) fn new() -> Self { Self { bytes: Self::new_buf(), len: 0, @@ -61,28 +94,48 @@ impl CompositeKey { if new_len > L { return false; } - self.bytes[self.len..new_len].copy_from_slice(b); + self.bytes.as_mut_slice()[self.len..new_len].copy_from_slice(b); self.len = new_len; true } pub fn as_bytes(&self) -> &[u8] { - &self.bytes[..self.len] + &self.bytes.as_slice()[..self.len] } - fn as_bytes_mut(&mut self) -> &mut [u8] { - &mut self.bytes[..self.len] + pub fn to_be_u64(&self, offset: usize) -> Result { + if offset + 8 > self.len { + return Err(ChainStorageError::CompositeKeyLengthExceeded); + } + let mut buf = [0u8; 8]; + buf.copy_from_slice(&self.bytes[offset..offset + 8]); + Ok(u64::from_be_bytes(buf)) } /// Returns a fixed 0-filled byte array. - fn new_buf() -> Box<[u8; L]> { - Box::new([0x0u8; L]) + fn new_buf() -> SmallBytes { + if L <= 64 { + return SmallBytes::Stack([0u8; L]); + } + SmallBytes::Heap(Box::new([0x0u8; L])) + } + + pub fn section_iter(&self, sections: [usize; SECTIONS]) -> SectionIter<'_, SECTIONS> { + SectionIter { + sections, + current: 0, + pointer: 0, + slice: self.as_bytes(), + } } } impl Display for CompositeKey { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", to_hex(self.as_bytes())) + for b in self.as_bytes() { + write!(f, "{:02x}", b)?; + } + Ok(()) } } @@ -94,12 +147,6 @@ impl Deref for CompositeKey { } } -impl DerefMut for CompositeKey { - fn deref_mut(&mut self) -> &mut Self::Target { - self.as_bytes_mut() - } -} - impl AsRef<[u8]> for CompositeKey { fn as_ref(&self) -> &[u8] { self.as_bytes() @@ -112,6 +159,20 @@ impl AsLmdbBytes for CompositeKey { } } +impl TryFrom<&[u8]> for CompositeKey { + type Error = ChainStorageError; + + fn try_from(value: &[u8]) -> Result { + if value.len() > L { + return Err(ChainStorageError::CompositeKeyLengthExceeded); + } + let mut key = Self::new(); + key.bytes.as_mut_slice()[..value.len()].copy_from_slice(value); + key.len = value.len(); + Ok(key) + } +} + #[derive(Debug, Clone)] pub(super) struct OutputKey(pub(super) CompositeKey<68>); @@ -139,3 +200,87 @@ impl InputKey { self.0 } } + +pub(super) struct SectionIter<'a, const SECTIONS: usize> { + sections: [usize; SECTIONS], + current: usize, + pointer: usize, + slice: &'a [u8], +} + +impl<'a, const SECTIONS: usize> SectionIter<'a, SECTIONS> { + /// Returns the next 8 bytes as a u64. + /// + /// # Panics + /// Panics if the next section is not 8 bytes long. + pub fn next_be_u64(&mut self) -> Option { + let bytes = self.next()?; + assert_eq!( + bytes.len(), + 8, + "Next section is not 8 bytes long. Section length: {}", + bytes.len() + ); + let mut buf = [0u8; 8]; + buf.copy_from_slice(bytes); + Some(u64::from_be_bytes(buf)) + } +} + +impl<'a, const SECTIONS: usize> Iterator for SectionIter<'a, SECTIONS> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + let cur_section = *self.sections.get(self.current)?; + + let lower = self.pointer; + let upper = self.pointer + cur_section; + self.current += 1; + self.pointer = upper; + + if upper > self.slice.len() { + return None; + } + + Some(&self.slice[lower..upper]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod section_iter { + use super::*; + + #[test] + fn it_returns_section_slices() { + let key = CompositeKey::<10>::try_from_parts(&[&[1, 3][..], &[6], &[7, 8, 9]]).unwrap(); + let mut iter = key.section_iter([2, 1, 3, 0]); + assert_eq!(iter.next(), Some(&[1, 3][..])); + assert_eq!(iter.next(), Some(&[6][..])); + assert_eq!(iter.next(), Some(&[7, 8, 9][..])); + assert_eq!(iter.next(), Some(&[][..])); + assert_eq!(iter.next(), None); + } + + #[test] + fn it_returns_none_if_sections_dont_exist() { + let key = CompositeKey::<10>::try_from_parts(&[&[1, 3][..]]).unwrap(); + let mut iter = key.section_iter([2, 1, 3]); + assert_eq!(iter.next(), Some(&[1, 3][..])); + assert_eq!(iter.next(), None); + assert_eq!(iter.next(), None); + assert_eq!(iter.next(), None); + } + + #[test] + fn it_returns_none_for_less_sections_than_len() { + let key = CompositeKey::<10>::try_from_parts(&[&[1, 3][..], &[1, 1, 1]]).unwrap(); + let mut iter = key.section_iter([3]); + assert_eq!(iter.next(), Some(&[1, 3, 1][..])); + assert_eq!(iter.next(), None); + assert_eq!(iter.next(), None); + } + } +} diff --git a/base_layer/core/src/chain_storage/lmdb_db/cursors.rs b/base_layer/core/src/chain_storage/lmdb_db/cursors.rs index 0718a97be2..4748a33660 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/cursors.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/cursors.rs @@ -96,32 +96,108 @@ where V: DeserializeOwned } } -pub struct LmdbReadCursor<'a, V> { +pub struct LmdbReadCursor<'a, K, V> { cursor: Cursor<'a, 'a>, - value_type: PhantomData, access: ConstAccessor<'a>, + seek_value: Option<(K, V)>, } -impl<'a, V: DeserializeOwned> LmdbReadCursor<'a, V> { +impl<'a, K: FromKeyBytes, V: DeserializeOwned> LmdbReadCursor<'a, K, V> { pub(super) fn new(cursor: Cursor<'a, 'a>, access: ConstAccessor<'a>) -> Self { Self { cursor, access, - value_type: PhantomData, + seek_value: None, } } + pub fn seek_first(&mut self) -> Result { + if let Some((k, v)) = convert_result_kv(self.cursor.first(&self.access))? { + self.seek_value = Some((k, v)); + return Ok(true); + } + Ok(false) + } + /// Returns the item at the cursor, progressing forwards until there are no more elements - pub fn next(&mut self) -> Result, ChainStorageError> { - convert_result(self.cursor.next(&self.access)) + pub fn next(&mut self) -> Result, ChainStorageError> { + if let Some(value) = self.seek_value.take() { + return Ok(Some(value)); + } + convert_result_kv(self.cursor.next(&self.access)) } - pub fn next_dup(&mut self) -> Result, ChainStorageError> { - convert_result(self.cursor.next_dup(&self.access)) + /// Returns the key of the item at the cursor, progressing forwards until there are no more elements + /// This is useful when the value is not needed, saving on deserialization costs + pub fn next_key(&mut self) -> Result, ChainStorageError> { + if let Some((key, _)) = self.seek_value.take() { + return Ok(Some(key)); + } + let result = self.cursor.next::<_, [u8]>(&self.access); + match result.to_opt()? { + Some((k, _)) => Ok(Some(K::from_key_bytes(k)?)), + None => Ok(None), + } + } + + /// Returns the item of the item at the cursor, progressing backwards until there are no more elements + #[allow(dead_code)] + pub fn prev(&mut self) -> Result, ChainStorageError> { + self.seek_value = None; + convert_result_kv(self.cursor.prev(&self.access)) } - pub fn seek_range(&mut self, key: &[u8]) -> Result, ChainStorageError> { - convert_result(self.cursor.seek_range_k(&self.access, key)) + /// Returns the key of the item at the cursor, progressing backwards until there are no more elements + /// This is useful when the value is not needed, saving on deserialization costs + #[allow(dead_code)] + pub fn prev_key(&mut self) -> Result, ChainStorageError> { + self.seek_value = None; + let result = self.cursor.prev::<_, [u8]>(&self.access); + match result.to_opt()? { + Some((k, _)) => Ok(Some(K::from_key_bytes(k)?)), + None => Ok(None), + } + } + + /// Returns the current key/value pair under this cursor. + /// + /// This corresponds to the `mdb_cursor_get` function with the + /// `MDB_CURRENT` operation. + pub fn current(&mut self) -> Result, ChainStorageError> { + if let Some(value) = self.seek_value.take() { + return Ok(Some(value)); + } + convert_result_kv(self.cursor.get_current(&self.access)) + } + + /// Return count of duplicates for current key. + /// + /// This call is only valid on `DUPSORT` databases. + pub fn count_dups(&mut self) -> Result { + let count = self.cursor.count()?; + Ok(count) + } + + /// Advances the cursor to the next value in the current key. + /// + /// This only makes sense on `DUPSORT` databases. This call returns None if + /// there are no more values in the current key. + pub fn next_dup(&mut self) -> Result, ChainStorageError> { + if let Some(value) = self.seek_value.take() { + return Ok(Some(value)); + } + convert_result_kv(self.cursor.next_dup(&self.access)) + } + + /// Positions the cursor at the first item whose key is greater than or equal to key. + /// If there is such a key, true is returned and calling [LmdbReadCursor::next] is guaranteed to return Some. + /// Conversely, if false is returned, calling [LmdbReadCursor::next] is guaranteed to return None. + pub fn seek_range(&mut self, key: &[u8]) -> Result { + if let Some((k, v)) = convert_result_kv(self.cursor.seek_range_k(&self.access, key))? { + self.seek_value = Some((k, v)); + return Ok(true); + } + Ok(false) } } @@ -147,10 +223,12 @@ impl FromKeyBytes for u64 { impl FromKeyBytes for CompositeKey { fn from_key_bytes(bytes: &[u8]) -> Result where Self: Sized { - if bytes.len() != SZ { - return Err(ChainStorageError::FromKeyBytesFailed( - "Invalid byte length for CompositeKey".to_string(), - )); + if bytes.len() > SZ { + return Err(ChainStorageError::FromKeyBytesFailed(format!( + "Invalid byte length for CompositeKey<{}> key. Byte len: {}", + SZ, + bytes.len() + ))); } let mut key = CompositeKey::::new(); key.push(bytes); @@ -158,7 +236,14 @@ impl FromKeyBytes for CompositeKey { } } -fn convert_result( +impl FromKeyBytes for Vec { + fn from_key_bytes(bytes: &[u8]) -> Result + where Self: Sized { + Ok(bytes.to_vec()) + } +} + +fn convert_result_kv( result: lmdb_zero::Result<(&[u8], &[u8])>, ) -> Result, ChainStorageError> { match result.to_opt()? { @@ -170,18 +255,19 @@ fn convert_result( #[cfg(test)] mod tests { + use super::*; use crate::chain_storage::{ lmdb_db::lmdb::{lmdb_get_prefix_cursor, lmdb_insert}, tests::temp_db::TempLmdbDatabase, }; #[test] - fn test_lmdb_get_prefix_cursor() { + fn test_lmdb_cursors() { let database = TempLmdbDatabase::new(); let db = database.default_db(); { let txn = database.write_transaction(); - lmdb_insert(&txn, db, &[0xffu8, 0, 0, 0], &1u64, "test").unwrap(); + lmdb_insert(&txn, db, &[0xfeu8, 0, 0, 0], &1u64, "test").unwrap(); lmdb_insert(&txn, db, &[0x2bu8, 0, 0, 1], &2u64, "test").unwrap(); lmdb_insert(&txn, db, &[0x2bu8, 0, 1, 1], &3u64, "test").unwrap(); lmdb_insert(&txn, db, &[0x2bu8, 1, 1, 0], &4u64, "test").unwrap(); @@ -190,6 +276,35 @@ mod tests { txn.commit().unwrap(); } + // Test LmdbReadCursor + { + let txn = database.read_transaction(); + let access = txn.access(); + let cursor = txn.cursor(db.clone()).unwrap(); + let mut cursor = LmdbReadCursor::<_, u64>::new(cursor, access); + assert!(cursor.seek_range(&[0x2bu8]).unwrap()); + let kv = cursor.next().unwrap().unwrap(); + assert_eq!(kv, (vec![0x2b, 0, 0, 1], 2)); + let kv = cursor.next().unwrap().unwrap(); + assert_eq!(kv, (vec![0x2b, 0, 1, 1], 3)); + let kv = cursor.next().unwrap().unwrap(); + assert_eq!(kv, (vec![0x2b, 1, 1, 0], 4)); + let kv = cursor.next().unwrap().unwrap(); + assert_eq!(kv, (vec![0x2b, 1, 1, 1], 5)); + let kv = cursor.next().unwrap().unwrap(); + assert_eq!(kv, (vec![0xfe, 0, 0, 0], 1)); + assert_eq!(cursor.next().unwrap(), None); + assert_eq!(cursor.next().unwrap(), None); + // Test seeking more than once on a cursor + cursor.seek_range(&[0x2b, 1, 1]).unwrap(); + let kv = cursor.next().unwrap().unwrap(); + assert_eq!(kv, (vec![0x2b, 1, 1, 0], 4)); + + assert!(!cursor.seek_range(&[0xffu8]).unwrap()); + assert_eq!(cursor.next().unwrap(), None); + } + + // Test prefix cursor { let txn = database.read_transaction(); let mut cursor = lmdb_get_prefix_cursor::(&txn, db, &[0x2b]).unwrap(); diff --git a/base_layer/core/src/chain_storage/lmdb_db/helpers.rs b/base_layer/core/src/chain_storage/lmdb_db/helpers.rs index 064b7be75b..5fe5fef56b 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/helpers.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/helpers.rs @@ -36,7 +36,7 @@ pub const LOG_TARGET: &str = "c::cs::lmdb_db::lmdb"; /// `size_hint` is given as an option as checking what the serialized would be is expensive /// for large data structures at ~30% overhead pub fn serialize(data: &T, size_hint: Option) -> Result, ChainStorageError> -where T: Serialize { +where T: Serialize + ?Sized { let start = Instant::now(); let mut buf = if let Some(size) = size_hint { Vec::with_capacity(size) diff --git a/base_layer/core/src/chain_storage/lmdb_db/lmdb.rs b/base_layer/core/src/chain_storage/lmdb_db/lmdb.rs index 2b104b7289..a88e0572b5 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/lmdb.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/lmdb.rs @@ -61,7 +61,7 @@ pub fn lmdb_insert( ) -> Result<(), ChainStorageError> where K: AsLmdbBytes + ?Sized + Debug, - V: Serialize + Debug, + V: Serialize + Debug + ?Sized, { let val_buf = serialize(val, None)?; match txn.access().put(db, key, &val_buf, put::NOOVERWRITE) { diff --git a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs index a51e9f203c..e99f267b82 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs @@ -43,6 +43,7 @@ use tari_common_types::{ types::{BlockHash, Commitment, FixedHash, HashOutput, PublicKey, Signature}, }; use tari_mmr::sparse_merkle_tree::{DeleteResult, NodeKey, ValueHash}; +use tari_sidechain::ShardGroup; use tari_storage::lmdb_store::{db, LMDBBuilder, LMDBConfig, LMDBStore, BYTES_PER_MB}; use tari_utilities::{ hex::{to_hex, Hex}, @@ -111,6 +112,8 @@ use crate::{ tari_amount::MicroMinotari, transaction_components::{ OutputType, + SideChainFeatureData, + SideChainId, SpentOutput, TransactionInput, TransactionKernel, @@ -150,13 +153,14 @@ const LMDB_DB_ORPHAN_PARENT_MAP_INDEX: &str = "orphan_parent_map_index"; const LMDB_DB_BAD_BLOCK_LIST: &str = "bad_blocks"; const LMDB_DB_REORGS: &str = "reorgs"; const LMDB_DB_VALIDATOR_NODES: &str = "validator_nodes"; -const LMDB_DB_VALIDATOR_NODES_MAPPING: &str = "validator_nodes_mapping"; +const LMDB_DB_VALIDATOR_NODES_ACTIVATION: &str = "validator_nodes_activation_queue"; +const LMDB_DB_VALIDATOR_NODES_EXIT: &str = "validator_nodes_exit"; const LMDB_DB_TEMPLATE_REGISTRATIONS: &str = "template_registrations"; /// HeaderHash(32), mmr_pos(8), hash(32) type KernelKey = CompositeKey<72>; /// Height(8), Hash(32) -type ValidatorNodeRegistrationKey = CompositeKey<40>; +type CodeTemplateRegistrationKey = CompositeKey<40>; pub fn create_lmdb_database>( path: P, @@ -199,7 +203,9 @@ pub fn create_lmdb_database>( .add_database(LMDB_DB_BAD_BLOCK_LIST, flags) .add_database(LMDB_DB_REORGS, flags | db::INTEGERKEY) .add_database(LMDB_DB_VALIDATOR_NODES, flags) - .add_database(LMDB_DB_VALIDATOR_NODES_MAPPING, flags) + .add_database(LMDB_DB_VALIDATOR_NODES_ACTIVATION, flags | db::DUPSORT | db::DUPFIXED) + // .add_database(LMDB_DB_VALIDATOR_NODES_MAPPING, flags) + .add_database(LMDB_DB_VALIDATOR_NODES_EXIT, flags) .add_database(LMDB_DB_TEMPLATE_REGISTRATIONS, flags | db::DUPSORT) .build() .map_err(|err| ChainStorageError::CriticalError(format!("Could not create LMDB store:{}", err)))?; @@ -257,11 +263,13 @@ pub struct LMDBDatabase { bad_blocks: DatabaseRef, /// Stores reorgs by epochtime and Reorg reorgs: DatabaseRef, - /// Maps -> ActiveValidatorNode + /// Maps -> ValidatorNodeEntry validator_nodes: DatabaseRef, - /// Maps -> VN Shard Key - validator_nodes_mapping: DatabaseRef, - /// Maps CodeTemplateRegistration -> TemplateRegistration + /// Maps -> \[PK\] + validator_nodes_activation_queue: DatabaseRef, + /// Maps -> ValidatorNodeEntry + validator_nodes_exit_queue: DatabaseRef, + /// Maps CodeTemplateRegistration -> TemplateRegistration template_registrations: DatabaseRef, _file_lock: Arc, consensus_manager: ConsensusManager, @@ -300,7 +308,8 @@ impl LMDBDatabase { bad_blocks: get_database(store, LMDB_DB_BAD_BLOCK_LIST)?, reorgs: get_database(store, LMDB_DB_REORGS)?, validator_nodes: get_database(store, LMDB_DB_VALIDATOR_NODES)?, - validator_nodes_mapping: get_database(store, LMDB_DB_VALIDATOR_NODES_MAPPING)?, + validator_nodes_activation_queue: get_database(store, LMDB_DB_VALIDATOR_NODES_ACTIVATION)?, + validator_nodes_exit_queue: get_database(store, LMDB_DB_VALIDATOR_NODES_EXIT)?, template_registrations: get_database(store, LMDB_DB_TEMPLATE_REGISTRATIONS)?, env, env_config: store.env_config(), @@ -497,7 +506,7 @@ impl LMDBDatabase { Ok(()) } - fn all_dbs(&self) -> [(&'static str, &DatabaseRef); 26] { + fn all_dbs(&self) -> [(&'static str, &DatabaseRef); 27] { [ (LMDB_DB_METADATA, &self.metadata_db), (LMDB_DB_HEADERS, &self.headers_db), @@ -529,7 +538,11 @@ impl LMDBDatabase { (LMDB_DB_BAD_BLOCK_LIST, &self.bad_blocks), (LMDB_DB_REORGS, &self.reorgs), (LMDB_DB_VALIDATOR_NODES, &self.validator_nodes), - (LMDB_DB_VALIDATOR_NODES_MAPPING, &self.validator_nodes_mapping), + ( + LMDB_DB_VALIDATOR_NODES_ACTIVATION, + &self.validator_nodes_activation_queue, + ), + (LMDB_DB_VALIDATOR_NODES_EXIT, &self.validator_nodes_exit_queue), (LMDB_DB_TEMPLATE_REGISTRATIONS, &self.template_registrations), ] } @@ -630,16 +643,14 @@ impl LMDBDatabase { fn input_with_output_data( &self, txn: &WriteTransaction<'_>, - input: TransactionInput, + mut input: TransactionInput, ) -> Result { let input_with_output_data = match input.spent_output { SpentOutput::OutputData { .. } => input, SpentOutput::OutputHash(output_hash) => match self.fetch_output_in_txn(txn, output_hash.as_slice()) { - Ok(Some(utxo_mined_info)) => TransactionInput { - version: input.version, - spent_output: SpentOutput::create_from_output(utxo_mined_info.output), - input_data: input.input_data, - script_signature: input.script_signature, + Ok(Some(utxo_mined_info)) => { + input.add_output_data(utxo_mined_info.output); + input }, Ok(None) => { error!( @@ -953,6 +964,8 @@ impl LMDBDatabase { let inputs = lmdb_delete_keys_starting_with::(txn, &self.inputs_db, block_hash)?; debug!(target: LOG_TARGET, "Deleted {} input(s)...", inputs.len()); + let constants = self.get_consensus_constants(height); + for utxo in &output_rows { trace!(target: LOG_TARGET, "Deleting UTXO `{}`", to_hex(utxo.hash.as_slice())); lmdb_delete( @@ -969,15 +982,31 @@ impl LMDBDatabase { continue; } - if let Some(vn_reg) = utxo - .output - .features - .sidechain_feature - .as_ref() - .and_then(|f| f.validator_node_registration()) - { - self.validator_node_store(txn) - .delete(height, vn_reg.public_key(), &utxo.output.commitment)?; + if let Some(sidechain_features) = utxo.output.features.sidechain_feature.as_ref() { + match &sidechain_features.data { + SideChainFeatureData::ValidatorNodeRegistration(vn_reg) => { + self.validator_node_store(txn) + .delete(sidechain_features.sidechain_public_key(), vn_reg.public_key())?; + }, + SideChainFeatureData::CodeTemplateRegistration(_) => { + let key = CodeTemplateRegistrationKey::try_from_parts(&[ + height.to_be_bytes().as_slice(), + output_hash.as_slice(), + ])?; + lmdb_delete(txn, &self.template_registrations, &key, "template_registrations")?; + }, + SideChainFeatureData::ConfidentialOutput(_) => { + // Nothing to do + }, + SideChainFeatureData::EvictionProof(evict) => { + let next_epoch = constants.block_height_to_epoch(height) + VnEpoch(1); + self.validator_node_store(txn).undo_exit( + sidechain_features.sidechain_public_key(), + next_epoch, + evict.node_to_evict(), + )?; + }, + } } // if an output was burned, it was never created as an unspent utxo @@ -1029,22 +1058,7 @@ impl LMDBDatabase { } })?; - let rp_hash = match utxo_mined_info.output.proof { - Some(proof) => proof.hash(), - None => FixedHash::zero(), - }; - input.add_output_data( - utxo_mined_info.output.version, - utxo_mined_info.output.features, - utxo_mined_info.output.commitment, - utxo_mined_info.output.script, - utxo_mined_info.output.sender_offset_public_key, - utxo_mined_info.output.covenant, - utxo_mined_info.output.encrypted_data, - utxo_mined_info.output.metadata_signature, - rp_hash, - utxo_mined_info.output.minimum_value_promise, - ); + input.add_output_data(utxo_mined_info.output); let smt_key = NodeKey::try_from(input.commitment()?.as_bytes())?; let smt_node = ValueHash::try_from(input.smt_hash(utxo_mined_info.mined_height).as_slice())?; if let Err(e) = output_smt.insert(smt_key, smt_node) { @@ -1282,30 +1296,8 @@ impl LMDBDatabase { } } - let output_hash = output.hash(); - if let Some(vn_reg) = output - .features - .sidechain_feature - .as_ref() - .and_then(|f| f.validator_node_registration()) - { - self.insert_validator_node(txn, header, &output.commitment, output.minimum_value_promise, vn_reg)?; - } - - if let Some(template_reg) = output - .features - .sidechain_feature - .as_ref() - .and_then(|f| f.code_template_registration()) - { - let record = TemplateRegistrationEntry { - registration_data: template_reg.clone(), - output_hash, - block_height: header.height, - block_hash, - }; - - self.insert_template_registration(txn, &record)?; + if output.features.output_type.is_sidechain_type() || output.is_burned_to_sidechain() { + self.handle_sidechain_utxo(txn, header, &output)?; } self.insert_output(txn, &block_hash, header.height, header.timestamp().as_u64(), &output)?; } @@ -1327,16 +1319,11 @@ impl LMDBDatabase { }; let features = input_with_output_data.features()?; - if let Some(vn_reg) = features - .sidechain_feature - .as_ref() - .and_then(|f| f.validator_node_registration()) - { - self.validator_node_store(txn).delete( - header.height, - vn_reg.public_key(), - input_with_output_data.commitment()?, - )?; + if let Some(sidechain_feature) = features.sidechain_feature.as_ref() { + if let Some(vn_reg) = sidechain_feature.validator_node_registration() { + self.validator_node_store(txn) + .delete(sidechain_feature.sidechain_public_key(), vn_reg.public_key())?; + } } trace!( target: LOG_TARGET, @@ -1366,7 +1353,68 @@ impl LMDBDatabase { &'a self, txn: &'a T, ) -> ValidatorNodeStore<'a, T> { - ValidatorNodeStore::new(txn, self.validator_nodes.clone(), self.validator_nodes_mapping.clone()) + ValidatorNodeStore::new( + txn, + self.validator_nodes.clone(), + self.validator_nodes_activation_queue.clone(), + self.validator_nodes_exit_queue.clone(), + ) + } + + fn handle_sidechain_utxo( + &self, + txn: &WriteTransaction<'_>, + header: &BlockHeader, + output: &TransactionOutput, + ) -> Result<(), ChainStorageError> { + let sidechain_feature = output.features.sidechain_feature.as_ref().ok_or_else(|| { + ChainStorageError::InvalidOperation( + "Output does not have a sidechain feature but is a sidechain type".to_string(), + ) + })?; + match &sidechain_feature.data { + SideChainFeatureData::ValidatorNodeRegistration(vn_reg) => { + self.insert_validator_node( + txn, + header, + &output.commitment, + output.minimum_value_promise, + sidechain_feature.sidechain_id(), + vn_reg, + )?; + }, + SideChainFeatureData::CodeTemplateRegistration(template_reg) => { + let output_hash = output.hash(); + let record = TemplateRegistrationEntry { + registration_data: template_reg.clone(), + output_hash, + block_height: header.height, + block_hash: header.hash(), + }; + + self.insert_template_registration(txn, &record)?; + }, + SideChainFeatureData::ConfidentialOutput(_) => { + // Nothing to do + }, + SideChainFeatureData::EvictionProof(proof) => { + let store = self.validator_node_store(txn); + let evict_node = proof.node_to_evict(); + let constants = self.get_consensus_constants(header.height); + let next_epoch = constants.block_height_to_epoch(header.height) + VnEpoch(1); + let sidechain_pk = sidechain_feature.sidechain_id().map(|id| id.public_key()); + info!( + target: LOG_TARGET, + "Evicting ValidatorNode in {}: public_key: {}, sidechain_public_key: {:?}", + next_epoch, + evict_node, + sidechain_pk.map(|pk| pk.to_hex()), + ); + store.exit(sidechain_pk, evict_node, next_epoch)?; + }, + } + + Ok(()) } fn insert_validator_node( @@ -1375,56 +1423,53 @@ impl LMDBDatabase { header: &BlockHeader, commitment: &Commitment, minimum_value_promise: MicroMinotari, + sidechain_id: Option<&SideChainId>, vn_reg: &ValidatorNodeRegistration, ) -> Result<(), ChainStorageError> { let store = self.validator_node_store(txn); let constants = self.get_consensus_constants(header.height); let current_epoch = constants.block_height_to_epoch(header.height); - // skip already added validator node - if store.is_vn_exists(current_epoch, vn_reg.public_key(), vn_reg.sidechain_id().cloned())? { - warn!(target: LOG_TARGET, "Validator node has been already registered: {:?}", vn_reg.public_key().to_string()); - return Ok(()); - } + let sidechain_pk = sidechain_id.map(|id| id.public_key()); - let prev_shard_key = store.get_shard_key( - 0, - current_epoch.as_u64() * constants.epoch_length(), - vn_reg.public_key(), - )?; let shard_key = vn_reg.derive_shard_key( - prev_shard_key, + None, current_epoch, constants.validator_node_registration_shuffle_interval(), &header.prev_hash, ); - let mut next_epoch = current_epoch + VnEpoch(1); - - // looking for next available epoch - let current_vn_count = store.get_vn_count_until_epoch(current_epoch, vn_reg.sidechain_id().cloned())?; - let mut vn_count = store.get_vn_count_in_epoch(next_epoch, vn_reg.sidechain_id().cloned())?; + let activation_epoch = store.get_next_activation_epoch( + sidechain_pk, + current_epoch, + constants.vn_registration_max_vns_initial_epoch() as usize, + constants.vn_registration_max_vns_per_epoch() as usize, + )?; - if (current_vn_count == 0 && vn_count >= constants.vn_registration_max_vns_initial_epoch()) || - (current_vn_count > 0 && vn_count >= constants.vn_registration_max_vns_per_epoch()) - { - while vn_count >= constants.vn_registration_max_vns_per_epoch() { - next_epoch += VnEpoch(1); - vn_count = store.get_vn_count_in_epoch(next_epoch, vn_reg.sidechain_id().cloned())?; - } - } + info!( + target: LOG_TARGET, + "Inserting ValidatorNode: public_key: {}, activation_epoch: {}, registration_epoch: {}, shard_key: {}, \ + commitment: {}, sidechain_public_key: {:?}, minimum_value_promise: {}", + vn_reg.public_key(), + activation_epoch, + current_epoch, + to_hex(&shard_key), + commitment.as_public_key(), + sidechain_pk.map(|pk| pk.to_hex()), + minimum_value_promise + ); let validator_node = ValidatorNodeEntry { shard_key, - start_epoch: next_epoch, + activation_epoch, + registration_epoch: current_epoch, public_key: vn_reg.public_key().clone(), commitment: commitment.clone(), - sidechain_id: vn_reg.sidechain_id().cloned(), - registration: vn_reg.clone(), + sidechain_public_key: sidechain_pk.cloned(), minimum_value_promise, }; - store.insert(header.height, &validator_node)?; + store.insert(&validator_node)?; Ok(()) } @@ -1666,7 +1711,7 @@ impl LMDBDatabase { txn: &WriteTransaction<'_>, template_registration: &TemplateRegistrationEntry, ) -> Result<(), ChainStorageError> { - let key = ValidatorNodeRegistrationKey::try_from_parts(&[ + let key = CodeTemplateRegistrationKey::try_from_parts(&[ template_registration.block_height.to_le_bytes().as_slice(), template_registration.output_hash.as_slice(), ])?; @@ -1736,6 +1781,28 @@ impl LMDBDatabase { fn get_consensus_constants(&self, height: u64) -> &ConsensusConstants { self.consensus_manager.consensus_constants(height) } + + fn fetch_utxo_by_commitment( + &self, + txn: &ConstTransaction<'_>, + commitment: &Commitment, + ) -> Result { + let output_hash = lmdb_get::<_, HashOutput>(txn, &self.utxo_commitment_index, commitment.as_bytes())? + .ok_or_else(|| ChainStorageError::ValueNotFound { + entity: "UTXO (in fetch_utxo_by_commitment)", + field: "commitment", + value: commitment.to_hex(), + })?; + let output = + self.fetch_output_in_txn(txn, output_hash.as_slice())? + .ok_or_else(|| ChainStorageError::ValueNotFound { + entity: "UTXO (in fetch_utxo_by_commitment)", + field: "hash", + value: output_hash.to_string(), + })?; + + Ok(output) + } } pub fn create_recovery_lmdb_database>(path: P) -> Result<(), ChainStorageError> { @@ -2500,35 +2567,237 @@ impl BlockchainBackend for LMDBDatabase { // Get the current epoch for the height let end_epoch = constants.block_height_to_epoch(height); - // Convert these back to height as validators regs are indexed by height - let end_height = end_epoch.as_u64() * constants.epoch_length(); - let nodes = vn_store.get_vn_set(0, end_height)?; + let vns = vn_store.get_entire_vn_set(end_epoch)?; + + let mut nodes = Vec::with_capacity(vns.len()); + for node in vns { + let output = self.fetch_utxo_by_commitment(&txn, &node.commitment)?; + let reg = output + .output + .features + .sidechain_feature + .as_ref() + .and_then(|f| f.validator_node_registration()) + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "fetch_all_active_validator_nodes", + details: "Output does not have a sidechain feature".to_string(), + })?; + nodes.push(ValidatorNodeRegistrationInfo { + public_key: node.public_key, + sidechain_id: node.sidechain_public_key, + shard_key: node.shard_key, + activation_epoch: node.activation_epoch, + original_registration: reg.clone(), + minimum_value_promise: output.output.minimum_value_promise, + }); + } + Ok(nodes) } fn fetch_active_validator_nodes( &self, + sidechain_pk: Option<&PublicKey>, height: u64, - validator_network: Option, ) -> Result, ChainStorageError> { - let nodes = self - .fetch_all_active_validator_nodes(height)? - .into_iter() - .filter(|vn| vn.sidechain_id == validator_network) - .collect(); + let txn = self.read_transaction()?; + let vn_store = self.validator_node_store(&txn); + let constants = self.consensus_manager.consensus_constants(height); + + // Get the current epoch for the height + let end_epoch = constants.block_height_to_epoch(height); + // TODO: custom limit + let vns = vn_store.get_vn_set(sidechain_pk, VnEpoch::zero(), end_epoch, 1_000_000)?; + + let mut nodes = Vec::with_capacity(vns.len()); + for node in vns { + let output = self.fetch_utxo_by_commitment(&txn, &node.commitment)?; + let reg = output + .output + .features + .sidechain_feature + .as_ref() + .and_then(|f| f.validator_node_registration()) + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "fetch_all_active_validator_nodes", + details: "Output does not have a sidechain feature".to_string(), + })?; + nodes.push(ValidatorNodeRegistrationInfo { + public_key: node.public_key, + sidechain_id: node.sidechain_public_key, + shard_key: node.shard_key, + activation_epoch: node.activation_epoch, + original_registration: reg.clone(), + minimum_value_promise: output.output.minimum_value_promise, + }); + } + Ok(nodes) } - fn get_shard_key(&self, height: u64, public_key: PublicKey) -> Result, ChainStorageError> { + fn fetch_validators_activating_in_epoch( + &self, + sidechain_pk: Option<&PublicKey>, + epoch: VnEpoch, + ) -> Result, ChainStorageError> { + let txn = self.read_transaction()?; + let vn_store = self.validator_node_store(&txn); + let vns = vn_store.get_activating_in_epoch(sidechain_pk, epoch)?; + let mut nodes = Vec::with_capacity(vns.len()); + for node in vns { + let output = self.fetch_utxo_by_commitment(&txn, &node.commitment)?; + let reg = output + .output + .features + .sidechain_feature + .as_ref() + .and_then(|f| f.validator_node_registration()) + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "fetch_validators_activating_in_epoch", + details: "Output does not have a sidechain feature".to_string(), + })?; + nodes.push(ValidatorNodeRegistrationInfo { + public_key: node.public_key, + sidechain_id: node.sidechain_public_key, + shard_key: node.shard_key, + activation_epoch: node.activation_epoch, + original_registration: reg.clone(), + minimum_value_promise: output.output.minimum_value_promise, + }); + } + Ok(nodes) + } + + fn fetch_validators_exiting_in_epoch( + &self, + sidechain_pk: Option<&PublicKey>, + epoch: VnEpoch, + ) -> Result, ChainStorageError> { + let txn = self.read_transaction()?; + let vn_store = self.validator_node_store(&txn); + let vns = vn_store.get_exiting_in_epoch(sidechain_pk, epoch)?; + let mut nodes = Vec::with_capacity(vns.len()); + for node in vns { + let output = self.fetch_utxo_by_commitment(&txn, &node.commitment)?; + let reg = output + .output + .features + .sidechain_feature + .as_ref() + .and_then(|f| f.validator_node_registration()) + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "fetch_validators_exiting_in_epoch", + details: "Output does not have a sidechain feature".to_string(), + })?; + nodes.push(ValidatorNodeRegistrationInfo { + public_key: node.public_key, + sidechain_id: node.sidechain_public_key, + shard_key: node.shard_key, + activation_epoch: node.activation_epoch, + original_registration: reg.clone(), + minimum_value_promise: output.output.minimum_value_promise, + }); + } + Ok(nodes) + } + + fn validator_node_exists( + &self, + sidechain_pk: Option<&PublicKey>, + height: u64, + validator_node_pk: &PublicKey, + ) -> Result { + let txn = self.read_transaction()?; + let vn_store = self.validator_node_store(&txn); + let constants = self.consensus_manager.consensus_constants(height); + + // Get the current epoch for the height + let end_epoch = constants.block_height_to_epoch(height); + let is_active = vn_store.vn_exists(sidechain_pk, validator_node_pk, end_epoch)?; + Ok(is_active) + } + + fn validator_node_is_active( + &self, + sidechain_pk: Option<&PublicKey>, + end_epoch: VnEpoch, + validator_node_pk: &PublicKey, + ) -> Result { + let txn = self.read_transaction()?; + let vn_store = self.validator_node_store(&txn); + + // Get the current epoch for the height + let is_active = vn_store.is_vn_active(sidechain_pk, validator_node_pk, end_epoch)?; + Ok(is_active) + } + + fn validator_node_is_active_for_shard_group( + &self, + sidechain_pk: Option<&PublicKey>, + end_epoch: VnEpoch, + validator_node_pk: &PublicKey, + _shard_group: ShardGroup, + ) -> Result { + // TODO: account for shard group + self.validator_node_is_active(sidechain_pk, end_epoch, validator_node_pk) + } + + fn validator_nodes_count_for_shard_group( + &self, + sidechain_pk: Option<&PublicKey>, + end_epoch: VnEpoch, + _shard_group: ShardGroup, + ) -> Result { + let txn = self.read_transaction()?; + let vn_store = self.validator_node_store(&txn); + vn_store.count_active_validators(sidechain_pk, end_epoch) + } + + fn get_validator_node( + &self, + sidechain_pk: Option<&PublicKey>, + public_key: PublicKey, + ) -> Result, ChainStorageError> { let txn = self.read_transaction()?; let store = self.validator_node_store(&txn); - let constants = self.get_consensus_constants(height); + let Some(vn) = store.get(sidechain_pk, &public_key)? else { + return Ok(None); + }; + + let hash = self + .fetch_unspent_output_hash_by_commitment(&vn.commitment)? + .ok_or_else(|| ChainStorageError::ValueNotFound { + entity: "UTXO (in fetch_unspent_output_hash_by_commitment)", + field: "commitment", + value: vn.commitment.to_hex(), + })?; + let output = self + .fetch_output(&hash)? + .ok_or_else(|| ChainStorageError::ValueNotFound { + entity: "UTXO (in fetch_unspent_output_hash_by_commitment)", + field: "hash", + value: hash.to_hex(), + })?; + + let reg = output + .output + .features + .sidechain_feature + .as_ref() + .and_then(|f| f.validator_node_registration()) + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "get_validator_node", + details: "Output does not have a sidechain feature".to_string(), + })?; - // Get the epoch height boundaries for our query - let current_epoch = constants.block_height_to_epoch(height); - let end_height = current_epoch.as_u64() * constants.epoch_length(); - let maybe_shard_id = store.get_shard_key(0, end_height, &public_key)?; - Ok(maybe_shard_id) + Ok(Some(ValidatorNodeRegistrationInfo { + public_key, + sidechain_id: sidechain_pk.cloned(), + shard_key: vn.shard_key, + activation_epoch: vn.activation_epoch, + original_registration: reg.clone(), + minimum_value_promise: vn.minimum_value_promise, + })) } fn fetch_template_registrations( diff --git a/base_layer/core/src/chain_storage/lmdb_db/validator_node_store.rs b/base_layer/core/src/chain_storage/lmdb_db/validator_node_store.rs index d82407e193..5d31c232d7 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/validator_node_store.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/validator_node_store.rs @@ -20,13 +20,12 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{collections::HashMap, ops::Deref}; +use std::{collections::BTreeSet, ops::Deref}; use lmdb_zero::{ConstTransaction, WriteTransaction}; -use tari_common_types::{ - epoch::VnEpoch, - types::{Commitment, PublicKey}, -}; +use log::debug; +use serde::de::DeserializeOwned; +use tari_common_types::{epoch::VnEpoch, types::PublicKey}; use tari_storage::lmdb_store::DatabaseRef; use tari_utilities::ByteArray; @@ -34,269 +33,693 @@ use crate::chain_storage::{ lmdb_db::{ composite_key::CompositeKey, cursors::{FromKeyBytes, LmdbReadCursor}, - lmdb::{lmdb_delete, lmdb_insert}, + lmdb::{lmdb_delete, lmdb_delete_key_value, lmdb_exists, lmdb_get, lmdb_insert, lmdb_insert_dup, lmdb_len}, }, ChainStorageError, ValidatorNodeEntry, - ValidatorNodeRegistrationInfo, }; -pub type ShardKey = [u8; 32]; -// -type ValidatorNodeStoreKey = CompositeKey<72>; -// -type ShardIdIndexKey = CompositeKey<72>; +const LOG_TARGET: &str = "c::cs::lmdb_db::validator_node_store"; + +const U64_SIZE: usize = size_of::(); +const PK_SIZE: usize = 32; + +/// +type ValidatorNodeStoreKey = CompositeKey<{ PK_SIZE + PK_SIZE + U64_SIZE }>; +/// +type ExitQueueKey = CompositeKey<{ PK_SIZE + U64_SIZE + PK_SIZE }>; +const EXIT_QUEUE_KEY_SECTIONS: [usize; 3] = [PK_SIZE, U64_SIZE, PK_SIZE]; +/// DUPSORT +type ActivationQueueKey = CompositeKey<{ PK_SIZE + U64_SIZE }>; +const ACTIVATION_QUEUE_KEY_SECTIONS: [usize; 2] = [PK_SIZE, U64_SIZE]; pub struct ValidatorNodeStore<'a, Txn> { txn: &'a Txn, db_validator_nodes: DatabaseRef, - db_validator_nodes_mapping: DatabaseRef, + db_validator_activation_queue: DatabaseRef, + db_validator_nodes_exit: DatabaseRef, } impl<'a, Txn: Deref>> ValidatorNodeStore<'a, Txn> { - pub fn new(txn: &'a Txn, db_height_to_vn: DatabaseRef, idx_public_key_to_shard: DatabaseRef) -> Self { + pub fn new( + txn: &'a Txn, + db_validator_nodes: DatabaseRef, + db_validator_activation_queue: DatabaseRef, + db_validator_nodes_exit: DatabaseRef, + ) -> Self { Self { txn, - db_validator_nodes: db_height_to_vn, - db_validator_nodes_mapping: idx_public_key_to_shard, + db_validator_nodes, + db_validator_activation_queue, + db_validator_nodes_exit, } } } impl ValidatorNodeStore<'_, WriteTransaction<'_>> { - pub fn insert(&self, height: u64, validator: &ValidatorNodeEntry) -> Result<(), ChainStorageError> { - let key = ValidatorNodeStoreKey::try_from_parts(&[ - height.to_be_bytes().as_slice(), - validator.public_key.as_bytes(), - validator.commitment.as_bytes(), - ]) - .expect("insert: Composite key length is incorrect"); - lmdb_insert(self.txn, &self.db_validator_nodes, &key, &validator, "Validator node")?; - - let key = ShardIdIndexKey::try_from_parts(&[ - validator.public_key.as_bytes(), - height.to_be_bytes().as_slice(), - validator.commitment.as_bytes(), - ]) - .expect("insert: Composite key length is incorrect"); + pub fn insert(&self, validator: &ValidatorNodeEntry) -> Result<(), ChainStorageError> { + let key = create_activation_key(validator.sidechain_public_key.as_ref(), validator.activation_epoch); + lmdb_insert_dup( + self.txn, + &self.db_validator_activation_queue, + &key, + &validator.public_key, + )?; + + let key = create_vn_key(validator.sidechain_public_key.as_ref(), &validator.public_key); + lmdb_insert(self.txn, &self.db_validator_nodes, &key, validator, "Validator node")?; + + Ok(()) + } + + pub fn delete(&self, sidechain_pk: Option<&PublicKey>, public_key: &PublicKey) -> Result<(), ChainStorageError> { + let key = create_vn_key(sidechain_pk, public_key); + let vn = lmdb_get::<_, ValidatorNodeEntry>(self.txn, &self.db_validator_nodes, &key)?.ok_or_else(|| { + ChainStorageError::ValueNotFound { + entity: "Validator node (delete)", + field: "public key", + value: public_key.to_string(), + } + })?; + lmdb_delete(self.txn, &self.db_validator_nodes, &key, "validator_nodes")?; + + let key = create_activation_key(sidechain_pk, vn.activation_epoch); + lmdb_delete_key_value(self.txn, &self.db_validator_activation_queue, &key, &vn.public_key)?; + + Ok(()) + } + + pub fn exit( + &self, + sidechain_pk: Option<&PublicKey>, + exit_node: &PublicKey, + exit_epoch: VnEpoch, + ) -> Result<(), ChainStorageError> { + let vn_key = create_vn_key(sidechain_pk, exit_node); + if !lmdb_exists(self.txn, &self.db_validator_nodes, &vn_key)? { + return Err(ChainStorageError::ValueNotFound { + entity: "Validator node (exit)", + field: "public key", + value: exit_node.to_string(), + }); + } + let vn = lmdb_get::<_, ValidatorNodeEntry>(self.txn, &self.db_validator_nodes, &vn_key)? + .expect("Found node key but not node in db_validator_nodes"); + + lmdb_delete(self.txn, &self.db_validator_nodes, &vn_key, "validator_nodes")?; + + let key = create_exit_queue_key(sidechain_pk, exit_epoch, exit_node); lmdb_insert( self.txn, - &self.db_validator_nodes_mapping, + &self.db_validator_nodes_exit, &key, - &validator.shard_key, - "Validator node", + &vn, + "validator_nodes_exit", )?; Ok(()) } - pub fn delete( + pub fn undo_exit( &self, - height: u64, - public_key: &PublicKey, - commitment: &Commitment, + sidechain_pk: Option<&PublicKey>, + exit_epoch: VnEpoch, + exit_node: &PublicKey, ) -> Result<(), ChainStorageError> { - let key = ValidatorNodeStoreKey::try_from_parts(&[ - height.to_be_bytes().as_slice(), - public_key.as_bytes(), - commitment.as_bytes(), - ]) - .expect("delete: Composite key length is incorrect"); - lmdb_delete(self.txn, &self.db_validator_nodes, &key, "validator_nodes")?; + let exit_key = create_exit_queue_key(sidechain_pk, exit_epoch, exit_node); + let vn = lmdb_get::<_, ValidatorNodeEntry>(self.txn, &self.db_validator_nodes_exit, &exit_key)?.ok_or_else( + || ChainStorageError::ValueNotFound { + entity: "Validator node (undo exit)", + field: "public key (undo exit)", + value: exit_node.to_string(), + }, + )?; - let key = ShardIdIndexKey::try_from_parts(&[ - public_key.as_bytes(), - height.to_be_bytes().as_slice(), - commitment.as_bytes(), - ]) - .expect("delete: Composite key length is incorrect"); lmdb_delete( self.txn, - &self.db_validator_nodes_mapping, - &key, - "validator_nodes_mapping", + &self.db_validator_nodes_exit, + &exit_key, + "validator_nodes_exit", )?; + + // Re-insert. Since we know the node was already activated in the past, we do not need to re-insert into the + // historical activation queue. + let key = create_vn_key(sidechain_pk, &vn.public_key); + lmdb_insert(self.txn, &self.db_validator_nodes, &key, &vn, "Validator node")?; + Ok(()) } } impl<'a, Txn: Deref>> ValidatorNodeStore<'a, Txn> { - fn db_read_cursor(&self) -> Result, ChainStorageError> { - let cursor = self.txn.cursor(self.db_validator_nodes.clone())?; - let access = self.txn.access(); - let cursor = LmdbReadCursor::new(cursor, access); - Ok(cursor) + fn validator_store_cursor( + &self, + ) -> Result, ChainStorageError> { + self.new_read_cursor(self.db_validator_nodes.clone()) + } + + fn activation_queue_read_cursor( + &self, + ) -> Result, ChainStorageError> { + self.new_read_cursor(self.db_validator_activation_queue.clone()) } - fn index_read_cursor(&self) -> Result, ChainStorageError> { - let cursor = self.txn.cursor(self.db_validator_nodes_mapping.clone())?; + fn exit_queue_read_cursor( + &self, + ) -> Result, ChainStorageError> { + self.new_read_cursor(self.db_validator_nodes_exit.clone()) + } + + fn new_read_cursor( + &self, + db: DatabaseRef, + ) -> Result, ChainStorageError> { + let cursor = self.txn.cursor(db)?; let access = self.txn.access(); let cursor = LmdbReadCursor::new(cursor, access); Ok(cursor) } - /// Checks if the given validator node (by it's public key and side chain ID) + /// Checks if the given validator node (by its public key and side chain ID) /// exists until a given `end_epoch`. - pub fn is_vn_exists( + pub fn vn_exists( &self, + sidechain_id: Option<&PublicKey>, + public_key: &PublicKey, end_epoch: VnEpoch, + ) -> Result { + let key = create_vn_key(sidechain_id, public_key); + let Some(vn) = lmdb_get::<_, ValidatorNodeEntry>(self.txn, &self.db_validator_nodes, &key)? else { + return Ok(false); + }; + + Ok(vn.registration_epoch <= end_epoch) + } + + /// Checks if the given validator node (by its public key and side chain ID) + /// exists until a given `end_epoch`. + pub fn is_vn_active( + &self, + sidechain_id: Option<&PublicKey>, public_key: &PublicKey, - sidechain_id: Option, + end_epoch: VnEpoch, ) -> Result { - let mut cursor = self.db_read_cursor()?; - while let Ok(Some((_, vn_entry))) = cursor.next::() { - if vn_entry.public_key == *public_key && - vn_entry.sidechain_id == sidechain_id && - vn_entry.start_epoch <= end_epoch - { - return Ok(true); + let key = create_vn_key(sidechain_id, public_key); + match lmdb_get::<_, ValidatorNodeEntry>(self.txn, &self.db_validator_nodes, &key)? { + Some(vn) => { + debug!(target: LOG_TARGET, "Found validator node in store: {} (activated: {}, end: {})", public_key, vn.activation_epoch, end_epoch); + Ok(vn.activation_epoch <= end_epoch) + }, + None => { + debug!(target: LOG_TARGET, "Validator node not found in store: {}, checking exit queue", public_key); + let key = create_exit_queue_prefix_key(sidechain_id, end_epoch); + let mut cursor = self.exit_queue_read_cursor()?; + cursor.seek_range(&key)?; + let sidechain_bytes = sid_as_slice(sidechain_id); + // TODO: This is O(n) where n is the number of validators in the exit queue for the sid at or after + // end_epoch. + while let Some(key) = cursor.next_key()? { + let mut sections = key.section_iter(EXIT_QUEUE_KEY_SECTIONS); + let sidechain_pk = sections + .next() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::is_vn_active", + details: "Malformed exit queue key".to_string(), + })?; + if sidechain_pk != sidechain_bytes { + break; + } + + let key_epoch = + sections + .next_be_u64() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::is_vn_active", + details: "Malformed exit queue key".to_string(), + })?; + + let pk = sections + .next() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::is_vn_active", + details: "Malformed exit queue key".to_string(), + })?; + if pk != public_key.as_bytes() { + continue; + } + if key_epoch <= end_epoch.as_u64() { + debug!(target: LOG_TARGET, "Found validator node in exit queue (exit applied): {} (exit: {}, end: {})", public_key, key_epoch, end_epoch); + return Ok(false); + } + + let (_, v) = cursor + .current()? + .expect("Cursor is not at a valid position in is_vn_active"); + + debug!(target: LOG_TARGET, "Found validator node in exit queue: {} (exit: {}, end: {})", public_key, key_epoch, end_epoch); + return Ok(v.activation_epoch <= end_epoch); + } + + debug!(target: LOG_TARGET, "Validator node not found in exit queue: {}", public_key); + Ok(false) + }, + } + } + + pub fn get_next_activation_epoch( + &self, + sidechain_id: Option<&PublicKey>, + current_epoch: VnEpoch, + initial_validators: usize, + validators_per_epoch: usize, + ) -> Result { + // Node activates earliest in the next epoch + let mut activation_epoch = current_epoch + VnEpoch(1); + // If there are less than the initial validators, we activate all new validators in the next epoch + let len = lmdb_len(self.txn, &self.db_validator_nodes)?; + if len < initial_validators { + return Ok(activation_epoch); + } + + let mut cursor = self.activation_queue_read_cursor()?; + loop { + let key = create_activation_key(sidechain_id, activation_epoch); + if !cursor.seek_range(&key)? { + break; + } + + // No activations in this epoch + if key.to_be_u64(PK_SIZE)? != activation_epoch.as_u64() { + break; + } + + // If there are less than the required number of validators in the queue for the epoch, we'll activate the + // next validator in this epoch + let num_queued = cursor.count_dups()?; + if num_queued < validators_per_epoch { + break; } + activation_epoch += VnEpoch(1); } - Ok(false) + Ok(activation_epoch) } - /// Returns validator nodes count until a given epoch. - pub fn get_vn_count_until_epoch( + pub fn count_active_validators( &self, - epoch: VnEpoch, - sidechain_id: Option, - ) -> Result { - let mut cursor = self.db_read_cursor()?; - let mut result = 0; - while let Ok(Some((_, value))) = cursor.next::() { - if value.sidechain_id == sidechain_id && value.start_epoch <= epoch { - result += 1; + sidechain_pk: Option<&PublicKey>, + end_epoch: VnEpoch, + ) -> Result { + let mut count = 0; + + { + let mut cursor = self.validator_store_cursor()?; + let sidechain_bytes = sid_as_slice(sidechain_pk); + if !cursor.seek_range(sidechain_bytes)? { + return Ok(0); } + + while let Some((key, vn)) = cursor.next()? { + if key[..PK_SIZE] != *sidechain_bytes { + // No further entries for this sidechain + break; + } + + if vn.activation_epoch > end_epoch { + break; + } + + count += 1; + } + } + + // We also need to search the exit queue, if the exit epoch is greater than the end epoch, the validator is + // still active + let mut cursor = self.exit_queue_read_cursor()?; + let prefix = create_exit_queue_prefix_key(sidechain_pk, end_epoch); + if !cursor.seek_range(&prefix)? { + return Ok(count); } + let sidechain_bytes = sid_as_slice(sidechain_pk); + while let Some((key, _)) = cursor.next()? { + let mut sections = key.section_iter(EXIT_QUEUE_KEY_SECTIONS); + let sid = sections + .next() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::count_active_validators", + details: "Malformed exit queue key".to_string(), + })?; - Ok(result) + if sid != sidechain_bytes { + // No further entries for this sidechain + break; + } + + let rec_epoch = sections + .next_be_u64() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::count_active_validators", + details: "Malformed exit queue key".to_string(), + })?; + if rec_epoch <= end_epoch.as_u64() { + // No further entries for this epoch + continue; + } + count += 1; + } + + Ok(count) } - /// Returns validator nodes count in a given epoch. - pub fn get_vn_count_in_epoch( - &self, - epoch: VnEpoch, - sidechain_id: Option, - ) -> Result { - let mut cursor = self.db_read_cursor()?; - let mut result = 0; - while let Ok(Some((_, value))) = cursor.next::() { - if value.sidechain_id == sidechain_id && value.start_epoch == epoch { - result += 1; + /// Returns a set of tuples ordered by epoch of registration. + /// This set contains no duplicates. If a duplicate registration is found, the last registration is included. + pub fn get_entire_vn_set(&self, end_epoch: VnEpoch) -> Result, ChainStorageError> { + // Nodes in db_validator_nodes are not ordered by epoch, meaning retreival until an end epoch will have to + // search the entire database incl > end_epoch. Instead, we first gather all public keys from the + // activation queue which is ordered by epoch. + let selected_nodes = { + let mut cursor = self.activation_queue_read_cursor()?; + cursor.seek_first()?; + + let mut selected_nodes = Vec::new(); + while let Some(key) = cursor.next_key()? { + let mut sections = key.section_iter(ACTIVATION_QUEUE_KEY_SECTIONS); + let sidechain_pk = sections + .next() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::get_entire_vn_set", + details: "Malformed activation queue key".to_string(), + })?; + + let activation_epoch = + sections + .next_be_u64() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::get_entire_vn_set", + details: "Malformed activation queue key".to_string(), + })?; + + if activation_epoch > end_epoch.as_u64() { + // Continue because we want to find the next sidechain ID - ideally we'd seek to it but that would + // require an ordered sidechain index + continue; + } + let (_, pk) = cursor + .current()? + .expect("Cursor is not at a valid position in get_entire_vn_set"); + let count = cursor.count_dups()?; + let mut pks = Vec::with_capacity(count); + // Collect all the public keys for the pair + pks.push(pk); + while let Some((_, pk)) = cursor.next_dup()? { + pks.push(pk) + } + + let mut sid = [0u8; 32]; + sid.copy_from_slice(sidechain_pk); + selected_nodes.push((sid, pks)); + } + selected_nodes + }; + + let mut nodes = BTreeSet::new(); + + for (sid, pks) in selected_nodes { + for pk in pks { + let key = create_vn_key_raw(&sid, pk.as_bytes()); + let maybe_vn = lmdb_get::<_, ValidatorNodeEntry>(self.txn, &self.db_validator_nodes, &key)?; + match maybe_vn { + Some(vn) => { + nodes.insert(vn); + }, + None => { + // Validator is queued for exit. Now we need to determine if the exit is before the end_epoch + // i.e an active validator at end_epoch + let mut cursor = self.exit_queue_read_cursor()?; + let prefix = create_exit_queue_prefix_key(Some(&sid), end_epoch); + if !cursor.seek_range(&prefix)? { + continue; + } + + while let Some(key) = cursor.next_key()? { + let mut sections = key.section_iter(EXIT_QUEUE_KEY_SECTIONS); + let key_sid = + sections + .next() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::get_entire_vn_set", + details: "Malformed exit queue key".to_string(), + })?; + + if key_sid != sid { + // No further entries for this sidechain + break; + } + + let key_epoch = + sections + .next_be_u64() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::get_entire_vn_set", + details: "Malformed exit queue key".to_string(), + })?; + + let key_pk = + sections + .next() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::get_entire_vn_set", + details: "Malformed exit queue key".to_string(), + })?; + + if pk.as_bytes() != key_pk { + continue; + } + + if key_epoch > end_epoch.as_u64() { + // We've found the exit record for the validator. It is active because the exit happens + // after end_epoch PANIC: the current key exists because + // we are iterating over the keys in the exit queue + let (_, v) = cursor + .current()? + .expect("Cursor is not at a valid position in get_entire_vn_set"); + nodes.insert(v); + } + break; + } + }, + }; } } - Ok(result) + Ok(nodes) } - /// Returns a set of tuples ordered by height of registration. + /// Returns a set of tuples ordered by epoch of registration. /// This set contains no duplicates. If a duplicate registration is found, the last registration is included. pub fn get_vn_set( &self, - start_height: u64, - end_height: u64, - ) -> Result, ChainStorageError> { - let mut cursor = self.db_read_cursor()?; - - let mut nodes = Vec::new(); - // Public key does not mutate once compressed and will always produce the same hash - #[allow(clippy::mutable_key_type)] - let mut dedup_map = HashMap::new(); - match cursor.seek_range::(&start_height.to_be_bytes())? { - Some((key, vn)) => { - let height = u64::from_key_bytes(&key[0..8])?; - if height > end_height { - return Ok(Vec::new()); - } - dedup_map.insert(vn.public_key.clone(), 0); - nodes.push(Some(ValidatorNodeRegistrationInfo { - public_key: vn.public_key, - sidechain_id: vn.sidechain_id, - shard_key: vn.shard_key, - start_epoch: vn.start_epoch, - original_registration: vn.registration.clone(), - minimum_value_promise: vn.minimum_value_promise, - })); - }, - None => return Ok(Vec::new()), + sidechain_pk: Option<&PublicKey>, + start_epoch: VnEpoch, + end_epoch: VnEpoch, + limit: usize, + ) -> Result, ChainStorageError> { + if end_epoch < start_epoch { + return Err(ChainStorageError::InvalidQuery(format!( + "get_vn_set: End epoch is less than start epoch: {} < {}", + end_epoch, start_epoch + ))); + } + + if limit == 0 { + return Ok(BTreeSet::new()); } - // Start from index 1 because we already have the first entry - let mut i = 1; - while let Some((key, vn)) = cursor.next_dup::()? { - let height = u64::from_key_bytes(&key[0..8])?; - if height > end_height { + let mut cursor = self.validator_store_cursor()?; + + let prefix = create_vn_store_prefix_key(sidechain_pk, start_epoch); + if !cursor.seek_range(&prefix)? { + return Ok(BTreeSet::new()); + } + + let sidechain_bytes = sid_as_slice(sidechain_pk); + let mut nodes = BTreeSet::new(); + while let Some((key, vn)) = cursor.next()? { + if key[..PK_SIZE] != *sidechain_bytes { + // No further entries for this sidechain break; } - if let Some(dup_idx) = dedup_map.insert(vn.public_key.clone(), i) { - // Remove duplicate registrations within the set without changing index order - let node_mut = nodes - .get_mut(dup_idx) - .expect("get_vn_set: internal dedeup map is not in sync with nodes"); - *node_mut = None; + + if vn.activation_epoch > end_epoch { + break; + } + + nodes.insert(vn); + if nodes.len() == limit { + break; } - nodes.push(Some(ValidatorNodeRegistrationInfo { - public_key: vn.public_key, - sidechain_id: vn.sidechain_id, - shard_key: vn.shard_key, - start_epoch: vn.start_epoch, - original_registration: vn.registration, - minimum_value_promise: vn.minimum_value_promise, - })); - i += 1; } - let mut vn_set = nodes.into_iter().flatten().collect::>(); - vn_set.sort_by(|vn_a, vn_b| { - vn_a.sidechain_id - .cmp(&vn_b.sidechain_id) - .then(vn_a.shard_key.cmp(&vn_b.shard_key)) - }); - Ok(vn_set) + Ok(nodes) } - pub fn get_shard_key( + pub fn get_activating_in_epoch( &self, - start_height: u64, - end_height: u64, - public_key: &PublicKey, - ) -> Result, ChainStorageError> { - let mut cursor = self.index_read_cursor()?; - let key = ShardIdIndexKey::try_from_parts(&[public_key.as_bytes(), &start_height.to_be_bytes()]) - .expect("fetch_shard_key: Composite key length is incorrect"); - - // Find the first entry at or above start_height - let mut shard_key = match cursor.seek_range::(key.as_bytes())? { - Some((key, s)) => { - if key[0..32] != *public_key.as_bytes() { - return Ok(None); + sidechain_pk: Option<&PublicKey>, + epoch: VnEpoch, + ) -> Result, ChainStorageError> { + let keys = { + let mut cursor = self.activation_queue_read_cursor()?; + let key = create_activation_key(sidechain_pk, epoch); + if !cursor.seek_range(&key)? { + return Ok(BTreeSet::new()); + } + + let num_keys = cursor.count_dups()?; + let mut keys = Vec::with_capacity(num_keys); + let sidechain_bytes = sid_as_slice(sidechain_pk); + while let Some((key, pk)) = cursor.next_dup()? { + if key[..PK_SIZE] != *sidechain_bytes { + // No further entries for this sidechain + break; } - let height = u64::from_key_bytes(&key[32..40])?; - if height > end_height { - return Ok(None); + if key.to_be_u64(PK_SIZE)? > epoch.as_u64() { + break; } - Some(s) - }, - None => return Ok(None), + + keys.push(create_vn_key(sidechain_pk, &pk)); + } + keys }; - // If there are any subsequent entries less than the end height, use that instead. - while let Some((key, s)) = cursor.next::()? { - if key[0..32] != *public_key.as_bytes() { + debug!(target: LOG_TARGET, "Found {} activating validators in epoch {}", keys.len(), epoch); + + let mut validators = BTreeSet::new(); + for key in keys { + let vn = lmdb_get::<_, ValidatorNodeEntry>(self.txn, &self.db_validator_nodes, &key)?.ok_or_else(|| { + ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::get_activating_in_epoch", + details: format!( + "Validator node in db_validator_activation_queue but not found in store for public key {}", + key + ), + } + })?; + + validators.insert(vn); + } + + Ok(validators) + } + + pub fn get_exiting_in_epoch( + &self, + sidechain_pk: Option<&PublicKey>, + epoch: VnEpoch, + ) -> Result, ChainStorageError> { + let mut cursor = self.exit_queue_read_cursor()?; + let prefix = create_exit_queue_prefix_key(sidechain_pk, epoch); + if !cursor.seek_range(&prefix)? { + return Ok(BTreeSet::new()); + } + + let sidechain_bytes = sid_as_slice(sidechain_pk); + let mut validators = BTreeSet::new(); + while let Some(key) = cursor.next_key()? { + debug!(target: LOG_TARGET, "exit queue key: {}", key); + let mut sections = key.section_iter(EXIT_QUEUE_KEY_SECTIONS); + let sid = sections + .next() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::get_exiting_in_epoch", + details: "Malformed exit queue key".to_string(), + })?; + + if sid != sidechain_bytes { + // No further entries for this sidechain break; } - let height = u64::from_key_bytes(&key[32..40])?; - if height > end_height { + + let rec_epoch = sections + .next_be_u64() + .ok_or_else(|| ChainStorageError::DataInconsistencyDetected { + function: "ValidatorNodeStore::get_exiting_in_epoch", + details: "Malformed exit queue key".to_string(), + })?; + if rec_epoch != epoch.as_u64() { + debug!(target: LOG_TARGET, "No further entries for this epoch {}, last rec epoch {}", epoch, rec_epoch); + // No further entries for this epoch break; } - shard_key = Some(s); + + let (_, vn) = cursor + .current()? + .expect("Cursor is not at a valid position in get_exiting_in_epoch"); + debug!(target: LOG_TARGET, "Found exiting validator in epoch {}: {}", rec_epoch, vn.public_key); + validators.insert(vn); } - Ok(shard_key) + + Ok(validators) + } + + pub fn get( + &self, + sidechain_pk: Option<&PublicKey>, + public_key: &PublicKey, + ) -> Result, ChainStorageError> { + let key = create_vn_key(sidechain_pk, public_key); + let vn = lmdb_get::<_, ValidatorNodeEntry>(self.txn, &self.db_validator_nodes, &key)?; + Ok(vn) } } +fn create_vn_key(sidechain_pk: Option<&PublicKey>, public_key: &PublicKey) -> ValidatorNodeStoreKey { + create_vn_key_raw(sid_as_slice(sidechain_pk), public_key.as_bytes()) +} +fn create_vn_key_raw(sidechain_pk: &[u8], public_key: &[u8]) -> ValidatorNodeStoreKey { + ValidatorNodeStoreKey::try_from_parts(&[sidechain_pk, public_key]) + .expect("create_key: Composite key length is incorrect") +} + +fn create_exit_queue_key(sidechain_pk: Option<&PublicKey>, epoch: VnEpoch, public_key: &PublicKey) -> ExitQueueKey { + ExitQueueKey::try_from_parts(&[ + sid_as_slice(sidechain_pk), + epoch.to_be_bytes().as_slice(), + public_key.as_bytes(), + ]) + .expect("create_key: Composite key length is incorrect") +} + +fn create_exit_queue_prefix_key(sidechain_pk: Option<&B>, epoch: VnEpoch) -> [u8; PK_SIZE + U64_SIZE] { + let mut buf = [0u8; PK_SIZE + U64_SIZE]; + if let Some(pk) = sidechain_pk { + buf[..PK_SIZE].copy_from_slice(pk.as_bytes()); + } + buf[PK_SIZE..].copy_from_slice(&epoch.to_be_bytes()); + buf +} + +fn create_activation_key(sidechain_pk: Option<&PublicKey>, epoch: VnEpoch) -> ActivationQueueKey { + ActivationQueueKey::try_from_parts(&[sid_as_slice(sidechain_pk), &epoch.to_be_bytes()]) + .expect("create_activation_key: Composite key length is incorrect") +} + +fn create_vn_store_prefix_key(sidechain_pk: Option<&PublicKey>, epoch: VnEpoch) -> [u8; PK_SIZE + U64_SIZE] { + let mut buf = [0u8; PK_SIZE + U64_SIZE]; + if let Some(pk) = sidechain_pk { + buf[..PK_SIZE].copy_from_slice(pk.as_bytes()); + } + buf[PK_SIZE..].copy_from_slice(&epoch.to_be_bytes()); + buf +} + +fn sid_as_slice(sidechain_pk: Option<&PublicKey>) -> &[u8] { + sidechain_pk.map_or([0u8; 32].as_slice(), |pk| pk.as_bytes()) +} + #[cfg(test)] mod tests { + use lmdb_zero::db; + use tari_common_types::types::Commitment; use tari_test_utils::unpack_enum; use super::*; @@ -305,44 +728,50 @@ mod tests { test_helpers::{make_hash, new_public_key}, }; - const DBS: &[&str] = &["validator_node_store", "validator_node_index"]; + const DBS: &[(&str, db::Flags)] = &[ + ("validator_node_store", db::CREATE), + ("validator_node_activation_queue", db::DUPSORT), + ("validator_node_exit_queue", db::CREATE), + ]; fn create_store<'a, Txn: Deref>>( db: &TempLmdbDatabase, txn: &'a Txn, ) -> ValidatorNodeStore<'a, Txn> { - let store_db = db.get_db(DBS[0]).clone(); - let index_db = db.get_db(DBS[1]).clone(); - ValidatorNodeStore::new(txn, store_db, index_db) + let store_db = db.get_db(DBS[0].0).clone(); + let activation_queue = db.get_db(DBS[1].0).clone(); + let exit_db = db.get_db(DBS[2].0).clone(); + ValidatorNodeStore::new(txn, store_db, activation_queue, exit_db) } fn insert_n_vns( store: &ValidatorNodeStore<'_, WriteTransaction<'_>>, - start_height: u64, + start_epoch: u64, + epoch_increment: u64, n: usize, - ) -> Vec { - let mut nodes = Vec::new(); + sidechain_id: Option<&PublicKey>, + ) -> Vec { + let mut nodes = Vec::with_capacity(n); for i in 0..n { let public_key = new_public_key(); let shard_key = make_hash(public_key.as_bytes()); - store - .insert(start_height + i as u64, &ValidatorNodeEntry { - public_key: public_key.clone(), - shard_key, - commitment: Commitment::from_public_key(&new_public_key()), - ..Default::default() - }) - .unwrap(); - nodes.push(ValidatorNodeRegistrationInfo { - public_key, - sidechain_id: None, + let start_epoch = VnEpoch(start_epoch + (i as u64 * epoch_increment)); + let entry = ValidatorNodeEntry { + public_key: public_key.clone(), shard_key, - start_epoch: Default::default(), - original_registration: Default::default(), - minimum_value_promise: Default::default(), - }); + commitment: Commitment::from_public_key(&new_public_key()), + activation_epoch: start_epoch, + sidechain_public_key: sidechain_id.cloned(), + ..Default::default() + }; + store.insert(&entry).unwrap(); + nodes.push(entry); } - nodes.sort_by(|a, b| a.sidechain_id.cmp(&b.sidechain_id).then(a.shard_key.cmp(&b.shard_key))); + nodes.sort_by(|a, b| { + a.sidechain_public_key + .cmp(&b.sidechain_public_key) + .then(a.shard_key.cmp(&b.shard_key)) + }); nodes } @@ -354,11 +783,12 @@ mod tests { let db = TempLmdbDatabase::with_dbs(DBS); let txn = db.write_transaction(); let store = create_store(&db, &txn); - let nodes = insert_n_vns(&store, 1, 3); - let set = store.get_vn_set(1, 3).unwrap(); - assert_eq!(set[0], nodes[0]); - assert_eq!(set[1], nodes[1]); - assert_eq!(set[2], nodes[2]); + let nodes = insert_n_vns(&store, 1, 0, 3, None); + let set = store.get_vn_set(None, VnEpoch(1), VnEpoch(3), 4).unwrap(); + for (i, node) in set.iter().enumerate() { + assert_eq!(*node, nodes[i]); + } + assert_eq!(set.len(), 3); } #[test] @@ -373,98 +803,224 @@ mod tests { commitment: Commitment::from_public_key(&new_public_key()), ..Default::default() }; - store.insert(1, &entry).unwrap(); - let err = store.insert(1, &entry).unwrap_err(); + store.insert(&entry).unwrap(); + let err = store.insert(&entry).unwrap_err(); unpack_enum!(ChainStorageError::KeyExists { .. } = err); } - } - - mod get_vn_set { - use super::*; #[test] - fn it_returns_a_deduped_set_of_validator_nodes() { + fn it_returns_key_exists_if_duplicate_inserted() { let db = TempLmdbDatabase::with_dbs(DBS); let txn = db.write_transaction(); let store = create_store(&db, &txn); - let nodes = insert_n_vns(&store, 1, 3); + let nodes = insert_n_vns(&store, 1, 0, 3, None); // Node 0 and 1 re-register at height 4 let s0 = make_hash(nodes[0].shard_key); - store - .insert(4, &ValidatorNodeEntry { + let err = store + .insert(&ValidatorNodeEntry { public_key: nodes[0].public_key.clone(), shard_key: s0, commitment: Commitment::from_public_key(&new_public_key()), ..Default::default() }) - .unwrap(); + .unwrap_err(); + assert!(matches!(err, ChainStorageError::KeyExists { .. })); + } + } - let s1 = make_hash(nodes[1].shard_key); - // The commitment is used last in the key and so changes the order they appear in the LMDB btree. - // We insert them in reverse order to demonstrate that insert order does not necessarily match the vn set - // order. - let mut ordered_commitments = vec![ - Commitment::from_public_key(&new_public_key()), - Commitment::from_public_key(&new_public_key()), - ]; - ordered_commitments.sort(); - store - .insert(5, &ValidatorNodeEntry { - public_key: nodes[1].public_key.clone(), - shard_key: make_hash(s1), - commitment: ordered_commitments[1].clone(), - ..Default::default() - }) - .unwrap(); - // This insert is counted as before the previous one because the commitment is "less" - store - .insert(5, &ValidatorNodeEntry { - public_key: nodes[1].public_key.clone(), - shard_key: s1, - commitment: ordered_commitments[0].clone(), - ..Default::default() - }) - .unwrap(); + mod get { + use super::*; - let set = store.get_vn_set(1, 5).unwrap(); - // s1 and s2 have replaced the previous shard keys, and are now ordered last since they come after node2 - assert_eq!(set.len(), 3); - assert_eq!(set.iter().filter(|s| s.public_key == nodes[1].public_key).count(), 1); + #[test] + fn it_returns_the_validator_node() { + let db = TempLmdbDatabase::with_dbs(DBS); + let txn = db.write_transaction(); + let store = create_store(&db, &txn); + let nodes = insert_n_vns(&store, 1, 0, 3, None); + + let s = store.get(None, &nodes[0].public_key).unwrap().unwrap(); + assert_eq!(s, nodes[0]); } } - mod get_shard_key { + mod activating_in_epoch { use super::*; #[test] - fn it_returns_latest_shard_key() { + fn it_returns_vns_activating_in_epoch() { let db = TempLmdbDatabase::with_dbs(DBS); let txn = db.write_transaction(); let store = create_store(&db, &txn); - let nodes = insert_n_vns(&store, 1, 3); - let new_shard_key = make_hash(nodes[0].shard_key); + let nodes = insert_n_vns(&store, 1, 0, 3, None); + let nodes2 = insert_n_vns(&store, 10, 0, 4, None); + let sid = new_public_key(); + let nodes3 = insert_n_vns(&store, 1, 0, 3, Some(&sid)); + + let epoch = VnEpoch(1); + let activating = store.get_activating_in_epoch(None, epoch).unwrap(); + assert_eq!(activating.len(), 3); + for (i, node) in activating.iter().enumerate() { + assert_eq!(*node, nodes[i]); + } + + for epoch in 2..10 { + let activating = store.get_activating_in_epoch(None, VnEpoch(epoch)).unwrap(); + assert_eq!(activating.len(), 0); + } + + let epoch = VnEpoch(10); + let activating = store.get_activating_in_epoch(None, epoch).unwrap(); + assert_eq!(activating.len(), 4); + for (i, node) in activating.iter().enumerate() { + assert_eq!(*node, nodes2[i]); + } + + let epoch = VnEpoch(1); + let activating = store.get_activating_in_epoch(Some(&sid), epoch).unwrap(); + assert_eq!(activating.len(), 3); + for (i, node) in activating.iter().enumerate() { + assert_eq!(*node, nodes3[i]); + } + } + } + + mod get_exiting_in_epoch { + use super::*; + + #[test] + fn it_returns_vns_exiting() { + let db = TempLmdbDatabase::with_dbs(DBS); + let txn = db.write_transaction(); + let store = create_store(&db, &txn); + let nodes = insert_n_vns(&store, 1, 0, 3, None); + let nodes2 = insert_n_vns(&store, 10, 0, 4, None); + let sid = new_public_key(); + let nodes3 = insert_n_vns(&store, 1, 0, 3, Some(&sid)); + + assert!(store.is_vn_active(None, &nodes[0].public_key, VnEpoch(1)).unwrap()); + assert!(!store.is_vn_active(None, &nodes2[0].public_key, VnEpoch(1)).unwrap()); + assert!(store.is_vn_active(None, &nodes2[0].public_key, VnEpoch(11)).unwrap()); + assert!(store + .is_vn_active(Some(&sid), &nodes3[0].public_key, VnEpoch(3)) + .unwrap()); + + // Exit some nodes + store.exit(None, &nodes[0].public_key, VnEpoch(11)).unwrap(); + store.exit(None, &nodes[1].public_key, VnEpoch(11)).unwrap(); + store.exit(None, &nodes2[0].public_key, VnEpoch(110)).unwrap(); + store.exit(None, &nodes2[1].public_key, VnEpoch(110)).unwrap(); + store.exit(Some(&sid), &nodes3[0].public_key, VnEpoch(11)).unwrap(); + store.exit(Some(&sid), &nodes3[1].public_key, VnEpoch(11)).unwrap(); + + assert!(store.is_vn_active(None, &nodes[0].public_key, VnEpoch(9)).unwrap()); + assert!(!store.is_vn_active(None, &nodes[0].public_key, VnEpoch(12)).unwrap()); + + let count = store.count_active_validators(None, VnEpoch(10)).unwrap(); + assert_eq!(count, 7); + let count = store.count_active_validators(None, VnEpoch(11)).unwrap(); + assert_eq!(count, 5); + let count = store.count_active_validators(Some(&sid), VnEpoch(10)).unwrap(); + assert_eq!(count, 3); + let count = store.count_active_validators(Some(&sid), VnEpoch(11)).unwrap(); + assert_eq!(count, 1); + let count = store.count_active_validators(None, VnEpoch(109)).unwrap(); + assert_eq!(count, 5); + let count = store.count_active_validators(None, VnEpoch(110)).unwrap(); + assert_eq!(count, 3); + let count = store.count_active_validators(None, VnEpoch(111)).unwrap(); + assert_eq!(count, 3); + + let exiting = store.get_exiting_in_epoch(None, VnEpoch(11)).unwrap(); + assert_eq!(exiting.len(), 2); + for (i, node) in exiting.iter().enumerate() { + assert_eq!(*node, nodes[i]); + } + assert!(store.is_vn_active(None, &nodes[0].public_key, VnEpoch(10)).unwrap()); + assert!(!store.is_vn_active(None, &nodes[0].public_key, VnEpoch(11)).unwrap()); + + let exiting = store.get_exiting_in_epoch(None, VnEpoch(110)).unwrap(); + assert_eq!(exiting.len(), 2); + for (i, node) in exiting.iter().enumerate() { + assert_eq!(*node, nodes2[i]); + } + assert!(store.is_vn_active(None, &nodes2[0].public_key, VnEpoch(109)).unwrap()); + assert!(!store.is_vn_active(None, &nodes2[0].public_key, VnEpoch(110)).unwrap()); + + let exiting = store.get_exiting_in_epoch(Some(&sid), VnEpoch(10)).unwrap(); + assert_eq!(exiting.len(), 0); + let exiting = store.get_exiting_in_epoch(Some(&sid), VnEpoch(11)).unwrap(); + assert_eq!(exiting.len(), 2); + for (i, node) in exiting.iter().enumerate() { + assert_eq!(*node, nodes3[i]); + } + assert!(store + .is_vn_active(Some(&sid), &nodes3[0].public_key, VnEpoch(10)) + .unwrap()); + assert!(!store + .is_vn_active(Some(&sid), &nodes3[0].public_key, VnEpoch(11)) + .unwrap()); + } + } + + mod get_entire_vn_set { + use super::*; + + #[test] + fn it_returns_all_active_validators_at_given_epoch() { + let db = TempLmdbDatabase::with_dbs(DBS); + let txn = db.write_transaction(); + let store = create_store(&db, &txn); + let nodes = insert_n_vns(&store, 1, 0, 3, None); + let nodes2 = insert_n_vns(&store, 10, 0, 4, None); + let sid = new_public_key(); + let nodes3 = insert_n_vns(&store, 1, 0, 3, Some(&sid)); + + // Exit some nodes + store.exit(None, &nodes[0].public_key, VnEpoch(11)).unwrap(); + store.exit(None, &nodes[1].public_key, VnEpoch(11)).unwrap(); + store.exit(None, &nodes2[0].public_key, VnEpoch(110)).unwrap(); + store.exit(None, &nodes2[1].public_key, VnEpoch(110)).unwrap(); + store.exit(Some(&sid), &nodes3[0].public_key, VnEpoch(11)).unwrap(); + store.exit(Some(&sid), &nodes3[1].public_key, VnEpoch(11)).unwrap(); + + let set = store.get_entire_vn_set(VnEpoch(10)).unwrap(); + assert_eq!(set.len(), 10); + let set = store.get_entire_vn_set(VnEpoch(11)).unwrap(); + assert_eq!(set.len(), 6); + let set = store.get_entire_vn_set(VnEpoch(110)).unwrap(); + assert_eq!(set.len(), 4); + + // re-register store - .insert(4, &ValidatorNodeEntry { - public_key: nodes[0].public_key.clone(), - shard_key: new_shard_key, + .insert(&ValidatorNodeEntry { + shard_key: nodes2[0].shard_key, + activation_epoch: VnEpoch(210), + registration_epoch: VnEpoch(210), + public_key: nodes2[0].public_key.clone(), commitment: Commitment::from_public_key(&new_public_key()), + sidechain_public_key: None, + minimum_value_promise: Default::default(), + }) + .unwrap(); - ..Default::default() + store + .insert(&ValidatorNodeEntry { + shard_key: nodes2[1].shard_key, + activation_epoch: VnEpoch(210), + registration_epoch: VnEpoch(210), + public_key: nodes2[1].public_key.clone(), + commitment: Commitment::from_public_key(&new_public_key()), + sidechain_public_key: None, + minimum_value_promise: Default::default(), }) .unwrap(); + // TODO: re-register with the same public key is not supported properly + // let set = store.get_entire_vn_set(VnEpoch(110)).unwrap(); + // assert_eq!(set.len(), 4); - // Height 0-3 has original shard key - let s = store.get_shard_key(0, 3, &nodes[0].public_key).unwrap().unwrap(); - assert_eq!(s, nodes[0].shard_key); - // Height 0-4 has shard key that was replaced at height 4 - let s = store.get_shard_key(0, 4, &nodes[0].public_key).unwrap().unwrap(); - assert_eq!(s, new_shard_key); - assert!(store.get_shard_key(5, 5, &nodes[0].public_key).unwrap().is_none()); - let s = store.get_shard_key(0, 3, &nodes[1].public_key).unwrap().unwrap(); - assert_eq!(s, nodes[1].shard_key); - let s = store.get_shard_key(0, 3, &nodes[2].public_key).unwrap().unwrap(); - assert_eq!(s, nodes[2].shard_key); + // let set = store.get_entire_vn_set(VnEpoch(210)).unwrap(); + // assert_eq!(set.len(), 6); } } } diff --git a/base_layer/core/src/chain_storage/mod.rs b/base_layer/core/src/chain_storage/mod.rs index 27020e3633..68d0dd4607 100644 --- a/base_layer/core/src/chain_storage/mod.rs +++ b/base_layer/core/src/chain_storage/mod.rs @@ -99,12 +99,12 @@ pub struct ChainTipData { pub total_accumulated_difficulty: U256, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct ValidatorNodeRegistrationInfo { pub public_key: PublicKey, pub sidechain_id: Option, pub shard_key: [u8; 32], - pub start_epoch: VnEpoch, + pub activation_epoch: VnEpoch, pub original_registration: ValidatorNodeRegistration, pub minimum_value_promise: MicroMinotari, } diff --git a/base_layer/core/src/chain_storage/tests/blockchain_database.rs b/base_layer/core/src/chain_storage/tests/blockchain_database.rs index 0e1de44bb1..40d82e29c2 100644 --- a/base_layer/core/src/chain_storage/tests/blockchain_database.rs +++ b/base_layer/core/src/chain_storage/tests/blockchain_database.rs @@ -586,14 +586,13 @@ mod validator_node_merkle_root { use std::convert::TryFrom; use rand::rngs::OsRng; - use tari_comms::types::Signature; + use tari_common_types::epoch::VnEpoch; use tari_crypto::{keys::PublicKey as PublicKeyTrait, ristretto::RistrettoPublicKey}; use tari_mmr::sparse_merkle_tree::SparseMerkleTree; - use tari_utilities::ByteArray; use super::*; use crate::{ - chain_storage::{calculate_validator_node_mr, ValidatorNodeRegistrationInfo}, + chain_storage::calculate_validator_node_mr, transactions::{ key_manager::create_memory_db_key_manager, transaction_components::{OutputFeatures, ValidatorNodeSignature}, @@ -617,14 +616,8 @@ mod validator_node_merkle_root { let (blocks, outputs) = add_many_chained_blocks(1, &db, &key_manager).await; let (sk, public_key) = RistrettoPublicKey::random_keypair(&mut OsRng); - let signature = ValidatorNodeSignature::sign(&sk, &public_key, &[]); - let features = OutputFeatures::for_validator_node_registration( - public_key.clone(), - signature.signature().clone(), - public_key.clone(), - None, - None, - ); + let signature = ValidatorNodeSignature::sign(&sk, None, &public_key, VnEpoch::zero()); + let features = OutputFeatures::for_validator_node_registration(signature, public_key.clone(), None); let (tx, _outputs) = schema_to_transaction( &[txn_schema!( from: vec![outputs[0].clone()], @@ -649,20 +642,8 @@ mod validator_node_merkle_root { let consts = db.consensus_constants().unwrap(); let (_, _) = add_many_chained_blocks(usize::try_from(consts.epoch_length()).unwrap(), &db, &key_manager).await; - let shard_key = db - .get_shard_key(consts.epoch_length(), public_key.clone()) - .unwrap() - .unwrap(); - - let merkle_root = calculate_validator_node_mr(&[ValidatorNodeRegistrationInfo { - public_key, - sidechain_id: None, - shard_key, - start_epoch: Default::default(), - original_registration: Default::default(), - minimum_value_promise: Default::default(), - }]) - .unwrap(); + let vn = db.get_validator_node(None, public_key.clone()).unwrap().unwrap(); + let merkle_root = calculate_validator_node_mr(&[vn]).unwrap(); let tip = db.fetch_tip_header().unwrap(); assert_eq!(tip.header().validator_node_mr, merkle_root); @@ -675,16 +656,10 @@ mod validator_node_merkle_root { let (blocks, outputs) = add_many_chained_blocks(1, &db, &key_manager).await; let (sk, public_key) = RistrettoPublicKey::random_keypair(&mut OsRng); - let signature = ValidatorNodeSignature::sign(&sk, &public_key, &[]); let (sidechain_private, sidechain_public) = RistrettoPublicKey::random_keypair(&mut OsRng); - let sidechain_signature = Signature::sign(&sidechain_private, public_key.as_bytes(), &mut OsRng).unwrap(); - let features = OutputFeatures::for_validator_node_registration( - public_key.clone(), - signature.signature().clone(), - public_key.clone(), - Some(sidechain_public.clone()), - Some(sidechain_signature), - ); + let signature = ValidatorNodeSignature::sign(&sk, Some(&sidechain_public), &public_key, VnEpoch::zero()); + let features = + OutputFeatures::for_validator_node_registration(signature, public_key.clone(), Some(&sidechain_private)); let (tx, _outputs) = schema_to_transaction( &[txn_schema!( from: vec![outputs[0].clone()], @@ -709,20 +684,11 @@ mod validator_node_merkle_root { let consts = db.consensus_constants().unwrap(); let (_, _) = add_many_chained_blocks(usize::try_from(consts.epoch_length()).unwrap(), &db, &key_manager).await; - let shard_key = db - .get_shard_key(consts.epoch_length(), public_key.clone()) + let vn = db + .get_validator_node(Some(sidechain_public.clone()), public_key.clone()) .unwrap() .unwrap(); - - let merkle_root = calculate_validator_node_mr(&[ValidatorNodeRegistrationInfo { - public_key, - sidechain_id: Some(sidechain_public), - shard_key, - start_epoch: Default::default(), - original_registration: Default::default(), - minimum_value_promise: Default::default(), - }]) - .unwrap(); + let merkle_root = calculate_validator_node_mr(&[vn]).unwrap(); let tip = db.fetch_tip_header().unwrap(); assert_eq!(tip.header().validator_node_mr, merkle_root); diff --git a/base_layer/core/src/chain_storage/tests/temp_db.rs b/base_layer/core/src/chain_storage/tests/temp_db.rs index 786fc1cd9c..4bfc0504a4 100644 --- a/base_layer/core/src/chain_storage/tests/temp_db.rs +++ b/base_layer/core/src/chain_storage/tests/temp_db.rs @@ -37,7 +37,7 @@ impl TempLmdbDatabase { Self::with_dbs(&[]) } - pub fn with_dbs(dbs: &[&'static str]) -> Self { + pub fn with_dbs(dbs: &[(&'static str, db::Flags)]) -> Self { let temp_path = create_temporary_data_path(); let mut builder = LMDBBuilder::new() @@ -46,8 +46,8 @@ impl TempLmdbDatabase { .set_max_number_of_databases(1) .add_database("__default", db::CREATE); - for db in dbs { - builder = builder.add_database(db, db::CREATE) + for (db, flags) in dbs { + builder = builder.add_database(db, db::CREATE | *flags) } let lmdb_store = builder.build().unwrap(); @@ -58,7 +58,7 @@ impl TempLmdbDatabase { default_db: Some(default_db.db()), dbs: dbs .iter() - .map(|db| (*db, lmdb_store.get_handle(db).unwrap().db())) + .map(|(db, _)| (*db, lmdb_store.get_handle(db).unwrap().db())) .collect(), } } diff --git a/base_layer/core/src/consensus/consensus_constants.rs b/base_layer/core/src/consensus/consensus_constants.rs index baad3bf93e..07803ed79f 100644 --- a/base_layer/core/src/consensus/consensus_constants.rs +++ b/base_layer/core/src/consensus/consensus_constants.rs @@ -108,7 +108,7 @@ pub struct ConsensusConstants { /// An allowlist of output types permitted_output_types: &'static [OutputType], /// The allowlist of range proof types - permitted_range_proof_types: [(OutputType, &'static [RangeProofType]); 5], + permitted_range_proof_types: &'static [(OutputType, &'static [RangeProofType])], /// Coinbase outputs are allowed to have metadata, but it has the following length limit coinbase_output_features_extra_max_length: u32, /// Maximum number of token elements permitted in covenants @@ -123,9 +123,9 @@ pub struct ConsensusConstants { vn_registration_shuffle_interval: VnEpoch, /// Maximum number of validator nodes activated initially /// (in the first epoch when we do not have any vns yet). - vn_registration_max_vns_initial_epoch: u64, + vn_registration_max_vns_initial_epoch: u32, /// Maximum number of validator nodes activated in an epoch. - vn_registration_max_vns_per_epoch: u64, + vn_registration_max_vns_per_epoch: u32, } #[derive(Debug, Clone)] @@ -336,7 +336,7 @@ impl ConsensusConstants { } /// Returns the permitted range proof types - pub fn permitted_range_proof_types(&self) -> [(OutputType, &[RangeProofType]); 5] { + pub fn permitted_range_proof_types(&self) -> &'static [(OutputType, &'static [RangeProofType])] { self.permitted_range_proof_types } @@ -364,14 +364,14 @@ impl ConsensusConstants { /// Returns the block height of the start of the given epoch pub fn epoch_to_block_height(&self, epoch: VnEpoch) -> u64 { - epoch.as_u64() * self.vn_epoch_length + epoch.as_u64().saturating_mul(self.vn_epoch_length) } - pub fn vn_registration_max_vns_initial_epoch(&self) -> u64 { + pub fn vn_registration_max_vns_initial_epoch(&self) -> u32 { self.vn_registration_max_vns_initial_epoch } - pub fn vn_registration_max_vns_per_epoch(&self) -> u64 { + pub fn vn_registration_max_vns_per_epoch(&self) -> u32 { self.vn_registration_max_vns_per_epoch } @@ -725,8 +725,8 @@ impl ConsensusConstants { &[OutputType::Coinbase, OutputType::Standard, OutputType::Burn] } - const fn current_permitted_range_proof_types() -> [(OutputType, &'static [RangeProofType]); 5] { - [ + const fn current_permitted_range_proof_types() -> &'static [(OutputType, &'static [RangeProofType])] { + &[ (OutputType::Standard, &[RangeProofType::BulletProofPlus]), (OutputType::Coinbase, &[ RangeProofType::BulletProofPlus, @@ -737,17 +737,22 @@ impl ConsensusConstants { RangeProofType::BulletProofPlus, ]), (OutputType::CodeTemplateRegistration, &[RangeProofType::BulletProofPlus]), + (OutputType::SidechainCheckpoint, &[RangeProofType::BulletProofPlus]), + (OutputType::SidechainProof, &[RangeProofType::BulletProofPlus]), ] } - const fn all_range_proof_types() -> [(OutputType, &'static [RangeProofType]); 5] { - [ + const fn all_range_proof_types() -> &'static [(OutputType, &'static [RangeProofType])] { + const RP_TYPES: &[(OutputType, &[RangeProofType])] = &[ (OutputType::Standard, RangeProofType::all()), (OutputType::Coinbase, RangeProofType::all()), (OutputType::Burn, RangeProofType::all()), (OutputType::ValidatorNodeRegistration, RangeProofType::all()), (OutputType::CodeTemplateRegistration, RangeProofType::all()), - ] + (OutputType::SidechainCheckpoint, RangeProofType::all()), + (OutputType::SidechainProof, RangeProofType::all()), + ]; + RP_TYPES } } @@ -894,7 +899,7 @@ impl ConsensusConstantsBuilder { pub fn with_permitted_range_proof_types( mut self, - permitted_range_proof_types: [(OutputType, &'static [RangeProofType]); 5], + permitted_range_proof_types: &'static [(OutputType, &'static [RangeProofType])], ) -> Self { self.consensus.permitted_range_proof_types = permitted_range_proof_types; self @@ -1081,47 +1086,13 @@ mod test { } } - // This function is to ensure all OutputType variants are assessed in the tests - fn cycle_output_type_enum(output_type: OutputType) -> OutputType { - match output_type { - OutputType::Standard => OutputType::Coinbase, - OutputType::Coinbase => OutputType::Burn, - OutputType::Burn => OutputType::ValidatorNodeRegistration, - OutputType::ValidatorNodeRegistration => OutputType::CodeTemplateRegistration, - OutputType::CodeTemplateRegistration => OutputType::Standard, - } - } - - // This function is to ensure all RangeProofType variants are assessed in the tests - fn cycle_range_proof_type_enum(range_proof_type: RangeProofType) -> RangeProofType { - match range_proof_type { - RangeProofType::BulletProofPlus => RangeProofType::RevealedValue, - RangeProofType::RevealedValue => RangeProofType::BulletProofPlus, - } - } - #[test] fn range_proof_types_coverage() { - let mut output_type_enums = vec![OutputType::Standard]; - loop { - let next_variant = cycle_output_type_enum(*output_type_enums.last().unwrap()); - if output_type_enums.contains(&next_variant) { - break; - } - output_type_enums.push(next_variant); - } - - let mut range_proof_type_enums = vec![RangeProofType::BulletProofPlus]; - loop { - let next_variant = cycle_range_proof_type_enum(*range_proof_type_enums.last().unwrap()); - if range_proof_type_enums.contains(&next_variant) { - break; - } - range_proof_type_enums.push(next_variant); - } + let output_type_variants = OutputType::all(); + let range_proof_type_variants = RangeProofType::all(); let permitted_range_proof_types = ConsensusConstants::current_permitted_range_proof_types().to_vec(); - for item in &output_type_enums { + for item in output_type_variants { let entries = permitted_range_proof_types .iter() .filter(|&&x| x.0 == *item) @@ -1131,13 +1102,13 @@ mod test { } let permitted_range_proof_types = ConsensusConstants::all_range_proof_types().to_vec(); - for output_type in &output_type_enums { + for output_type in output_type_variants { let entries = permitted_range_proof_types .iter() .filter(|&&x| x.0 == *output_type) .collect::>(); assert_eq!(entries.len(), 1); - for range_proof_type in &range_proof_type_enums { + for range_proof_type in range_proof_type_variants { assert!(entries[0].1.iter().any(|&x| x == *range_proof_type)); } } diff --git a/base_layer/core/src/covenants/test.rs b/base_layer/core/src/covenants/test.rs index 14f8309d95..a6151f7a6c 100644 --- a/base_layer/core/src/covenants/test.rs +++ b/base_layer/core/src/covenants/test.rs @@ -31,6 +31,7 @@ use crate::{ BuildInfo, CodeTemplateRegistration, SideChainFeature, + SideChainFeatureData, TemplateType, TransactionInput, TransactionOutput, @@ -76,8 +77,9 @@ pub fn make_sample_sidechain_feature() -> SideChainFeature { }, binary_sha: Default::default(), binary_url: "https://github.com/tari-project/tari.git".try_into().unwrap(), - sidechain_id: None, - sidechain_id_knowledge_proof: None, }; - SideChainFeature::CodeTemplateRegistration(template_reg) + SideChainFeature { + data: SideChainFeatureData::CodeTemplateRegistration(template_reg), + sidechain_id: None, + } } diff --git a/base_layer/core/src/proto/sidechain_feature.proto b/base_layer/core/src/proto/sidechain_feature.proto index 49484de997..1971578d1c 100644 --- a/base_layer/core/src/proto/sidechain_feature.proto +++ b/base_layer/core/src/proto/sidechain_feature.proto @@ -8,60 +8,143 @@ import "types.proto"; package tari.types; message SideChainFeature { - oneof side_chain_feature { - ValidatorNodeRegistration validator_node_registration = 1; - TemplateRegistration template_registration = 2; - ConfidentialOutputData confidential_output = 3; - } + oneof side_chain_feature { + ValidatorNodeRegistration validator_node_registration = 1; + TemplateRegistration template_registration = 2; + ConfidentialOutputData confidential_output = 3; + EvictionProof eviction_proof = 4; + } + SidechainId sidechain_id = 5; +} + +message SidechainId { + bytes public_key = 1; + Signature knowledge_proof = 2; } message ValidatorNodeRegistration { - bytes public_key = 1; - Signature signature = 2; - bytes claim_public_key = 3; - bytes sidechain_id = 4; - Signature sidechain_id_knowledge_proof = 5; + bytes public_key = 1; + Signature signature = 2; + bytes claim_public_key = 3; } message TemplateRegistration { - bytes author_public_key = 1; - Signature author_signature = 2; - string template_name = 3; - uint32 template_version = 4; - TemplateType template_type = 5; - BuildInfo build_info = 6; - bytes binary_sha = 7; - string binary_url = 8; - bytes sidechain_id = 9; - Signature sidechain_id_knowledge_proof = 10; + bytes author_public_key = 1; + Signature author_signature = 2; + string template_name = 3; + uint32 template_version = 4; + TemplateType template_type = 5; + BuildInfo build_info = 6; + bytes binary_sha = 7; + string binary_url = 8; } message ConfidentialOutputData { - bytes claim_public_key = 1; - bytes sidechain_id = 2; - Signature sidechain_id_knowledge_proof = 3; + bytes claim_public_key = 1; } message TemplateType { - oneof template_type { - WasmInfo wasm = 1; - FlowInfo flow = 2; - ManifestInfo manifest =3; - } + oneof template_type { + WasmInfo wasm = 1; + FlowInfo flow = 2; + ManifestInfo manifest = 3; + } +} +message WasmInfo { + uint32 abi_version = 1; +} + +message FlowInfo { + +} + +message ManifestInfo { + +} + +message BuildInfo { + string repo_url = 1; + bytes commit_hash = 2; +} + +message EvictionProof { + CommitProof proof = 1; +} + +message CommitProof { + oneof version { + CommitProofV1 v1 = 1; + } } - message WasmInfo { - uint32 abi_version = 1; - } - message FlowInfo { +message CommitProofV1 { + bytes command = 1; + SidechainBlockCommitProof commit_proof = 2; +} + +message SidechainBlockCommitProof { + SidechainBlockHeader header = 1; + repeated CommitProofElement proof_elements = 2; +} + +message CommitProofElement { + oneof proof_element { + QuorumCertificate quorum_certificate = 1; + DummyChain dummy_chain = 2; + } +} - } +message DummyChain { + repeated ChainLink chain_links = 1; +} - message ManifestInfo { +message ChainLink { + bytes header_hash = 1; + bytes parent_id = 2; +} - } +message SidechainBlockHeader { + uint32 network = 1; + bytes parent_id = 2; + bytes justify_id = 3; + uint64 height = 4; + uint64 epoch = 5; + ShardGroup shard_group = 6; + bytes proposed_by = 7; + uint64 total_leader_fee = 8; + bytes state_merkle_root = 9; + bytes command_merkle_root = 10; + bool is_dummy = 11; + bytes foreign_indexes_hash = 12; + Signature signature = 13; + uint64 timestamp = 14; + uint64 base_layer_block_height = 15; + bytes base_layer_block_hash = 16; + bytes extra_data_hash = 17; +} - message BuildInfo { - string repo_url = 1; - bytes commit_hash = 2; +message EvictAtom { + bytes public_key = 1; } + +message QuorumCertificate { + bytes header_hash = 1; + bytes parent_id = 2; + repeated ValidatorSignature signatures = 3; + QuorumDecision decision = 4; +} + +enum QuorumDecision { + Accept = 0; + Reject = 1; +} + +message ValidatorSignature { + bytes public_key = 1; + Signature signature = 2; +} + +message ShardGroup { + uint32 start = 1; + uint32 end_inclusive = 2; +} \ No newline at end of file diff --git a/base_layer/core/src/proto/sidechain_feature.rs b/base_layer/core/src/proto/sidechain_feature.rs index 3126010b11..7a790f1652 100644 --- a/base_layer/core/src/proto/sidechain_feature.rs +++ b/base_layer/core/src/proto/sidechain_feature.rs @@ -24,8 +24,24 @@ use std::convert::{TryFrom, TryInto}; +use prost::Message; +use tari_common::configuration::Network; use tari_common_types::types::{PublicKey, Signature}; use tari_max_size::MaxSizeString; +use tari_sidechain::{ + ChainLink, + CommandCommitProof, + CommandCommitProofV1, + CommitProofElement, + EvictNodeAtom, + EvictionProof, + QuorumCertificate, + QuorumDecision, + ShardGroup, + SidechainBlockCommitProof, + SidechainBlockHeader, + ValidatorQcSignature, +}; use tari_utilities::ByteArray; use crate::{ @@ -35,6 +51,8 @@ use crate::{ CodeTemplateRegistration, ConfidentialOutputData, SideChainFeature, + SideChainFeatureData, + SideChainId, TemplateType, ValidatorNodeRegistration, ValidatorNodeSignature, @@ -45,40 +63,61 @@ use crate::{ impl From for proto::types::SideChainFeature { fn from(value: SideChainFeature) -> Self { Self { - side_chain_feature: Some(value.into()), + side_chain_feature: Some(value.data.into()), + sidechain_id: value.sidechain_id.as_ref().map(Into::into), } } } -impl From for proto::types::side_chain_feature::SideChainFeature { - fn from(value: SideChainFeature) -> Self { +impl TryFrom for SideChainFeature { + type Error = String; + + fn try_from(features: proto::types::SideChainFeature) -> Result { + Ok(Self { + data: features + .side_chain_feature + .map(TryInto::try_into) + .ok_or("sidec_hain_feature not provided")??, + sidechain_id: features.sidechain_id.map(TryInto::try_into).transpose()?, + }) + } +} + +impl From for proto::types::side_chain_feature::SideChainFeature { + fn from(value: SideChainFeatureData) -> Self { match value { - SideChainFeature::ValidatorNodeRegistration(template_reg) => { + SideChainFeatureData::ValidatorNodeRegistration(template_reg) => { proto::types::side_chain_feature::SideChainFeature::ValidatorNodeRegistration(template_reg.into()) }, - SideChainFeature::CodeTemplateRegistration(template_reg) => { + SideChainFeatureData::CodeTemplateRegistration(template_reg) => { proto::types::side_chain_feature::SideChainFeature::TemplateRegistration(template_reg.into()) }, - SideChainFeature::ConfidentialOutput(output_data) => { + SideChainFeatureData::ConfidentialOutput(output_data) => { proto::types::side_chain_feature::SideChainFeature::ConfidentialOutput(output_data.into()) }, + SideChainFeatureData::EvictionProof(ref proof) => { + proto::types::side_chain_feature::SideChainFeature::EvictionProof(proof.into()) + }, } } } -impl TryFrom for SideChainFeature { +impl TryFrom for SideChainFeatureData { type Error = String; fn try_from(features: proto::types::side_chain_feature::SideChainFeature) -> Result { match features { proto::types::side_chain_feature::SideChainFeature::ValidatorNodeRegistration(vn_reg) => { - Ok(SideChainFeature::ValidatorNodeRegistration(vn_reg.try_into()?)) + Ok(SideChainFeatureData::ValidatorNodeRegistration(vn_reg.try_into()?)) }, proto::types::side_chain_feature::SideChainFeature::TemplateRegistration(template_reg) => { - Ok(SideChainFeature::CodeTemplateRegistration(template_reg.try_into()?)) + Ok(SideChainFeatureData::CodeTemplateRegistration(template_reg.try_into()?)) }, proto::types::side_chain_feature::SideChainFeature::ConfidentialOutput(output_data) => { - Ok(SideChainFeature::ConfidentialOutput(output_data.try_into()?)) + Ok(SideChainFeatureData::ConfidentialOutput(output_data.try_into()?)) + }, + proto::types::side_chain_feature::SideChainFeature::EvictionProof(proof) => { + Ok(SideChainFeatureData::EvictionProof(proof.try_into()?)) }, } } @@ -94,16 +133,6 @@ impl TryFrom for ValidatorNodeRegistrat let claim_public_key = PublicKey::from_canonical_bytes(&value.claim_public_key).map_err(|e| format!("claim_public_key: {}", e))?; - let sidechain_id = if value.sidechain_id.is_empty() { - None - } else { - Some(PublicKey::from_canonical_bytes(&value.sidechain_id).map_err(|e| format!("sidechain_id: {}", e))?) - }; - let sidechain_id_knowledge_proof = value - .sidechain_id_knowledge_proof - .map(|v| Signature::try_from(v).map_err(|e| format!("sidechain_id_knowledge_proof: {}", e))) - .transpose()?; - Ok(Self::new( ValidatorNodeSignature::new( public_key, @@ -113,8 +142,6 @@ impl TryFrom for ValidatorNodeRegistrat .ok_or("signature not provided")??, ), claim_public_key, - sidechain_id, - sidechain_id_knowledge_proof, )) } } @@ -125,8 +152,6 @@ impl From for proto::types::ValidatorNodeRegistration public_key: value.public_key().to_vec(), signature: Some(value.signature().into()), claim_public_key: value.claim_public_key().to_vec(), - sidechain_id: value.sidechain_id().map(|v| v.to_vec()).unwrap_or_default(), - sidechain_id_knowledge_proof: value.sidechain_id_knowledge_proof().map(|v| v.into()), } } } @@ -136,15 +161,6 @@ impl TryFrom for CodeTemplateRegistration { type Error = String; fn try_from(value: proto::types::TemplateRegistration) -> Result { - let sidechain_id = if value.sidechain_id.is_empty() { - None - } else { - Some(PublicKey::from_canonical_bytes(&value.sidechain_id).map_err(|e| format!("sidechain_id: {}", e))?) - }; - let sidechain_id_knowledge_proof = value - .sidechain_id_knowledge_proof - .map(|v| Signature::try_from(v).map_err(|e| format!("sidechain_id_knowledge_proof: {}", e))) - .transpose()?; Ok(Self { author_public_key: PublicKey::from_canonical_bytes(&value.author_public_key).map_err(|e| e.to_string())?, author_signature: value @@ -166,8 +182,6 @@ impl TryFrom for CodeTemplateRegistration { .ok_or("Build info not provided")??, binary_sha: value.binary_sha.try_into().map_err(|_| "Invalid commit sha")?, binary_url: MaxSizeString::try_from(value.binary_url).map_err(|e| e.to_string())?, - sidechain_id, - sidechain_id_knowledge_proof, }) } } @@ -183,8 +197,6 @@ impl From for proto::types::TemplateRegistration { build_info: Some(value.build_info.into()), binary_sha: value.binary_sha.to_vec(), binary_url: value.binary_url.to_string(), - sidechain_id: value.sidechain_id.map(|v| v.to_vec()).unwrap_or_default(), - sidechain_id_knowledge_proof: value.sidechain_id_knowledge_proof.map(|v| v.into()), } } } @@ -194,19 +206,8 @@ impl TryFrom for ConfidentialOutputData { type Error = String; fn try_from(value: proto::types::ConfidentialOutputData) -> Result { - let sidechain_id = if value.sidechain_id.is_empty() { - None - } else { - Some(PublicKey::from_canonical_bytes(&value.sidechain_id).map_err(|e| format!("sidechain_id: {}", e))?) - }; - let sidechain_id_knowledge_proof = value - .sidechain_id_knowledge_proof - .map(|v| Signature::try_from(v).map_err(|e| format!("sidechain_id_knowledge_proof: {}", e))) - .transpose()?; Ok(ConfidentialOutputData { claim_public_key: PublicKey::from_canonical_bytes(&value.claim_public_key).map_err(|e| e.to_string())?, - sidechain_id, - sidechain_id_knowledge_proof, }) } } @@ -215,8 +216,6 @@ impl From for proto::types::ConfidentialOutputData { fn from(value: ConfidentialOutputData) -> Self { Self { claim_public_key: value.claim_public_key.to_vec(), - sidechain_id: value.sidechain_id.map(|v| v.to_vec()).unwrap_or_default(), - sidechain_id_knowledge_proof: value.sidechain_id_knowledge_proof.map(|v| v.into()), } } } @@ -282,3 +281,366 @@ impl From for proto::types::BuildInfo { } } } + +// -------------------------------- SidechainId -------------------------------- // + +impl TryFrom for SideChainId { + type Error = String; + + fn try_from(value: proto::types::SidechainId) -> Result { + let public_key = PublicKey::from_canonical_bytes(&value.public_key).map_err(|e| e.to_string())?; + let knowledge_proof = value + .knowledge_proof + .map(Signature::try_from) + .ok_or("knowledge_proof not provided")??; + Ok(Self::new(public_key, knowledge_proof)) + } +} + +impl From<&SideChainId> for proto::types::SidechainId { + fn from(value: &SideChainId) -> Self { + Self { + public_key: value.public_key().to_vec(), + knowledge_proof: Some(value.knowledge_proof().into()), + } + } +} + +// -------------------------------- EvictionProof -------------------------------- // + +impl TryFrom for EvictionProof { + type Error = String; + + fn try_from(value: proto::types::EvictionProof) -> Result { + let proof = value.proof.ok_or("proof not provided")?.try_into()?; + Ok(EvictionProof::new(proof)) + } +} + +impl From<&EvictionProof> for proto::types::EvictionProof { + fn from(value: &EvictionProof) -> Self { + Self { + proof: Some(value.proof().into()), + } + } +} + +// -------------------------------- Commit proof -------------------------------- // + +impl TryFrom for CommandCommitProof { + type Error = String; + + fn try_from(value: proto::types::CommitProof) -> Result { + match value.version.ok_or("version not provided")? { + proto::types::commit_proof::Version::V1(v1) => Ok(Self::V1(v1.try_into()?)), + } + } +} + +impl From<&CommandCommitProof> for proto::types::CommitProof { + fn from(value: &CommandCommitProof) -> Self { + match value { + CommandCommitProof::V1(v1) => Self { + version: Some(proto::types::commit_proof::Version::V1(v1.into())), + }, + } + } +} + +impl TryFrom for CommandCommitProofV1 { + type Error = String; + + fn try_from(value: proto::types::CommitProofV1) -> Result { + let command = proto::types::EvictAtom::decode(value.command.as_slice()).map_err(|e| e.to_string())?; + Ok(CommandCommitProofV1 { + command: command.try_into()?, + commit_proof: value.commit_proof.ok_or("commit_proof not provided")?.try_into()?, + }) + } +} + +impl From<&CommandCommitProofV1> for proto::types::CommitProofV1 { + fn from(value: &CommandCommitProofV1) -> Self { + Self { + command: proto::types::EvictAtom::from(value.command()).encode_to_vec(), + commit_proof: Some(value.commit_proof().into()), + } + } +} + +// -------------------------------- SidechainBlockCommitProof -------------------------------- // + +impl TryFrom for SidechainBlockCommitProof { + type Error = String; + + fn try_from(value: proto::types::SidechainBlockCommitProof) -> Result { + Ok(Self { + header: value.header.ok_or("header not provided")?.try_into()?, + proof_elements: value + .proof_elements + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + }) + } +} + +impl From<&SidechainBlockCommitProof> for proto::types::SidechainBlockCommitProof { + fn from(value: &SidechainBlockCommitProof) -> Self { + Self { + header: Some(value.header().into()), + proof_elements: value.proof_elements().iter().map(Into::into).collect(), + } + } +} + +// -------------------------------- SidechainBlockHeader -------------------------------- // + +impl TryFrom for SidechainBlockHeader { + type Error = String; + + fn try_from(value: proto::types::SidechainBlockHeader) -> Result { + let network_byte = u8::try_from(value.network).map_err(|_| "Invalid network byte: overflows u8".to_string())?; + Network::try_from(network_byte).map_err(|err| format!("Invalid network byte: {}", err))?; + Ok(Self { + network: network_byte, + parent_id: value.parent_id.try_into().map_err(|_| "Invalid parent id")?, + justify_id: value.justify_id.try_into().map_err(|_| "Invalid justify id")?, + height: value.height, + epoch: value.epoch, + shard_group: value.shard_group.ok_or("missing shard_group")?.try_into()?, + proposed_by: PublicKey::from_canonical_bytes(&value.proposed_by) + .map_err(|_| "Invalid proposed_by public key")?, + total_leader_fee: value.total_leader_fee, + state_merkle_root: value + .state_merkle_root + .try_into() + .map_err(|_| "Invalid state merkle root")?, + command_merkle_root: value + .command_merkle_root + .try_into() + .map_err(|_| "Invalid command merkle root")?, + is_dummy: value.is_dummy, + foreign_indexes_hash: value + .foreign_indexes_hash + .try_into() + .map_err(|_| "Invalid foreign indexes hash")?, + signature: value + .signature + .ok_or("SidechainBlockHeader signature not provided")? + .try_into() + .map_err(|_| "Invalid signature")?, + timestamp: value.timestamp, + base_layer_block_height: value.base_layer_block_height, + base_layer_block_hash: value + .base_layer_block_hash + .try_into() + .map_err(|_| "Invalid base layer block hash")?, + extra_data_hash: value + .extra_data_hash + .try_into() + .map_err(|_| "Invalid extra data hash")?, + }) + } +} + +impl From<&SidechainBlockHeader> for proto::types::SidechainBlockHeader { + fn from(value: &SidechainBlockHeader) -> Self { + Self { + network: u32::from(value.network), + parent_id: value.parent_id.to_vec(), + justify_id: value.justify_id.to_vec(), + height: value.height, + epoch: value.epoch, + shard_group: Some(value.shard_group.into()), + proposed_by: value.proposed_by.to_vec(), + total_leader_fee: value.total_leader_fee, + state_merkle_root: value.state_merkle_root.to_vec(), + command_merkle_root: value.command_merkle_root.to_vec(), + is_dummy: value.is_dummy, + foreign_indexes_hash: value.foreign_indexes_hash.to_vec(), + signature: Some(value.signature().into()), + timestamp: value.timestamp, + base_layer_block_height: value.base_layer_block_height, + base_layer_block_hash: value.base_layer_block_hash.to_vec(), + extra_data_hash: value.extra_data_hash.to_vec(), + } + } +} + +// -------------------------------- CommitProofElement -------------------------------- // + +impl TryFrom for CommitProofElement { + type Error = String; + + fn try_from(value: proto::types::CommitProofElement) -> Result { + match value.proof_element.ok_or("proof element not provided")? { + proto::types::commit_proof_element::ProofElement::QuorumCertificate(qc) => { + Ok(CommitProofElement::QuorumCertificate(qc.try_into()?)) + }, + proto::types::commit_proof_element::ProofElement::DummyChain(chain) => Ok(CommitProofElement::DummyChain( + chain + .chain_links + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + )), + } + } +} + +impl From<&CommitProofElement> for proto::types::CommitProofElement { + fn from(value: &CommitProofElement) -> Self { + match value { + CommitProofElement::QuorumCertificate(qc) => Self { + proof_element: Some(proto::types::commit_proof_element::ProofElement::QuorumCertificate( + qc.into(), + )), + }, + CommitProofElement::DummyChain(chain) => Self { + proof_element: Some(proto::types::commit_proof_element::ProofElement::DummyChain( + proto::types::DummyChain { + chain_links: chain.iter().map(Into::into).collect(), + }, + )), + }, + } + } +} + +// -------------------------------- ChainLink -------------------------------- // + +impl TryFrom for ChainLink { + type Error = String; + + fn try_from(value: proto::types::ChainLink) -> Result { + Ok(Self { + header_hash: value.header_hash.try_into().map_err(|_| "Invalid block id")?, + parent_id: value.parent_id.try_into().map_err(|_| "Invalid parent id")?, + }) + } +} + +impl From<&ChainLink> for proto::types::ChainLink { + fn from(value: &ChainLink) -> Self { + Self { + header_hash: value.header_hash.to_vec(), + parent_id: value.parent_id.to_vec(), + } + } +} + +// -------------------------------- QuorumCertificate -------------------------------- // + +impl TryFrom for QuorumCertificate { + type Error = String; + + fn try_from(value: proto::types::QuorumCertificate) -> Result { + Ok(Self { + header_hash: value.header_hash.try_into().map_err(|_| "Invalid block body hash")?, + parent_id: value.parent_id.try_into().map_err(|_| "Invalid parent id")?, + signatures: value + .signatures + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + decision: proto::types::QuorumDecision::try_from(value.decision) + .map_err(|e| format!("Invalid QuorumDecision: {e}"))? + .into(), + }) + } +} + +impl From<&QuorumCertificate> for proto::types::QuorumCertificate { + fn from(value: &QuorumCertificate) -> Self { + Self { + parent_id: value.parent_id.to_vec(), + header_hash: value.header_hash.to_vec(), + signatures: value.signatures.iter().map(Into::into).collect(), + decision: proto::types::QuorumDecision::from(value.decision).into(), + } + } +} + +// -------------------------------- QuorumDecision -------------------------------- // + +impl From for QuorumDecision { + fn from(value: proto::types::QuorumDecision) -> Self { + match value { + proto::types::QuorumDecision::Accept => QuorumDecision::Accept, + proto::types::QuorumDecision::Reject => QuorumDecision::Reject, + } + } +} + +impl From for proto::types::QuorumDecision { + fn from(value: QuorumDecision) -> Self { + match value { + QuorumDecision::Accept => proto::types::QuorumDecision::Accept, + QuorumDecision::Reject => proto::types::QuorumDecision::Reject, + } + } +} + +// -------------------------------- ValidatorSignature -------------------------------- // + +impl TryFrom for ValidatorQcSignature { + type Error = String; + + fn try_from(value: proto::types::ValidatorSignature) -> Result { + Ok(Self { + public_key: PublicKey::from_canonical_bytes(&value.public_key).map_err(|e| e.to_string())?, + signature: value.signature.ok_or("signature not provided")?.try_into()?, + }) + } +} + +impl From<&ValidatorQcSignature> for proto::types::ValidatorSignature { + fn from(value: &ValidatorQcSignature) -> Self { + Self { + public_key: value.public_key().to_vec(), + signature: Some(value.signature().into()), + } + } +} + +// -------------------------------- EvictNodeAtom -------------------------------- // + +impl TryFrom for EvictNodeAtom { + type Error = String; + + fn try_from(value: proto::types::EvictAtom) -> Result { + Ok(Self::new( + PublicKey::from_canonical_bytes(&value.public_key).map_err(|e| e.to_string())?, + )) + } +} + +impl From<&EvictNodeAtom> for proto::types::EvictAtom { + fn from(value: &EvictNodeAtom) -> Self { + Self { + public_key: value.node_to_evict().to_vec(), + } + } +} +// -------------------------------- ShardGroup -------------------------------- // + +impl TryFrom for ShardGroup { + type Error = String; + + fn try_from(value: proto::types::ShardGroup) -> Result { + Ok(Self { + start: value.start, + end_inclusive: value.end_inclusive, + }) + } +} + +impl From for proto::types::ShardGroup { + fn from(value: ShardGroup) -> Self { + Self { + start: value.start, + end_inclusive: value.end_inclusive, + } + } +} diff --git a/base_layer/core/src/proto/transaction.rs b/base_layer/core/src/proto/transaction.rs index 683a05f511..749e585cf1 100644 --- a/base_layer/core/src/proto/transaction.rs +++ b/base_layer/core/src/proto/transaction.rs @@ -340,11 +340,7 @@ impl TryFrom for OutputFeatures { type Error = String; fn try_from(features: proto::types::OutputFeatures) -> Result { - let sidechain_feature = features - .sidechain_feature - .and_then(|features| features.side_chain_feature) - .map(SideChainFeature::try_from) - .transpose()?; + let sidechain_feature = features.sidechain_feature.map(SideChainFeature::try_from).transpose()?; let output_type = features .output_type diff --git a/base_layer/core/src/proto/types_impls.rs b/base_layer/core/src/proto/types_impls.rs index 6cf22b993c..4f12f33e17 100644 --- a/base_layer/core/src/proto/types_impls.rs +++ b/base_layer/core/src/proto/types_impls.rs @@ -20,12 +20,10 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{ - borrow::Borrow, - convert::{TryFrom, TryInto}, -}; +use std::convert::{TryFrom, TryInto}; -use tari_common_types::types::{ComAndPubSignature, Commitment, HashOutput, PrivateKey, PublicKey, Signature}; +use tari_common_types::types::{ComAndPubSignature, Commitment, HashOutput, PrivateKey, PublicKey}; +use tari_crypto::{hashing::DomainSeparation, signatures::SchnorrSignature}; use tari_utilities::{ByteArray, ByteArrayError}; use super::types as proto; @@ -49,7 +47,8 @@ impl From for proto::Commitment { } //---------------------------------- Signature --------------------------------------------// -impl TryFrom for Signature { + +impl TryFrom for SchnorrSignature { type Error = String; fn try_from(sig: proto::Signature) -> Result { @@ -59,12 +58,19 @@ impl TryFrom for Signature { Ok(Self::new(public_nonce, signature)) } } - -impl> From for proto::Signature { - fn from(sig: T) -> Self { +impl From<&SchnorrSignature> for proto::Signature { + fn from(sig: &SchnorrSignature) -> Self { + Self { + public_nonce: sig.get_public_nonce().to_vec(), + signature: sig.get_signature().to_vec(), + } + } +} +impl From> for proto::Signature { + fn from(sig: SchnorrSignature) -> Self { Self { - public_nonce: sig.borrow().get_public_nonce().to_vec(), - signature: sig.borrow().get_signature().to_vec(), + public_nonce: sig.get_public_nonce().to_vec(), + signature: sig.get_signature().to_vec(), } } } diff --git a/base_layer/core/src/test_helpers/blockchain.rs b/base_layer/core/src/test_helpers/blockchain.rs index 28b312ebda..c165981d9f 100644 --- a/base_layer/core/src/test_helpers/blockchain.rs +++ b/base_layer/core/src/test_helpers/blockchain.rs @@ -31,10 +31,12 @@ use std::{ use tari_common::configuration::Network; use tari_common_types::{ chain_metadata::ChainMetadata, + epoch::VnEpoch, tari_address::TariAddress, types::{Commitment, FixedHash, HashOutput, PublicKey, Signature}, }; use tari_mmr::sparse_merkle_tree::{NodeKey, ValueHash}; +use tari_sidechain::ShardGroup; use tari_storage::lmdb_store::LMDBConfig; use tari_test_utils::paths::create_temporary_data_path; use tari_utilities::ByteArray; @@ -415,17 +417,94 @@ impl BlockchainBackend for TempDatabase { fn fetch_active_validator_nodes( &self, + sidechain_pk: Option<&PublicKey>, height: u64, - validator_network: Option, ) -> Result, ChainStorageError> { self.db .as_ref() .unwrap() - .fetch_active_validator_nodes(height, validator_network) + .fetch_active_validator_nodes(sidechain_pk, height) } - fn get_shard_key(&self, height: u64, public_key: PublicKey) -> Result, ChainStorageError> { - self.db.as_ref().unwrap().get_shard_key(height, public_key) + fn fetch_validators_activating_in_epoch( + &self, + sidechain_pk: Option<&PublicKey>, + epoch: VnEpoch, + ) -> Result, ChainStorageError> { + self.db + .as_ref() + .unwrap() + .fetch_validators_activating_in_epoch(sidechain_pk, epoch) + } + + fn fetch_validators_exiting_in_epoch( + &self, + sidechain_pk: Option<&PublicKey>, + epoch: VnEpoch, + ) -> Result, ChainStorageError> { + self.db + .as_ref() + .unwrap() + .fetch_validators_exiting_in_epoch(sidechain_pk, epoch) + } + + fn validator_node_exists( + &self, + sidechain_pk: Option<&PublicKey>, + height: u64, + validator_node_pk: &PublicKey, + ) -> Result { + self.db + .as_ref() + .unwrap() + .validator_node_exists(sidechain_pk, height, validator_node_pk) + } + + fn validator_node_is_active( + &self, + sidechain_pk: Option<&PublicKey>, + end_epoch: VnEpoch, + validator_node_pk: &PublicKey, + ) -> Result { + self.db + .as_ref() + .unwrap() + .validator_node_is_active(sidechain_pk, end_epoch, validator_node_pk) + } + + fn validator_node_is_active_for_shard_group( + &self, + sidechain_pk: Option<&PublicKey>, + end_epoch: VnEpoch, + validator_node_pk: &PublicKey, + shard_group: ShardGroup, + ) -> Result { + self.db.as_ref().unwrap().validator_node_is_active_for_shard_group( + sidechain_pk, + end_epoch, + validator_node_pk, + shard_group, + ) + } + + fn validator_nodes_count_for_shard_group( + &self, + sidechain_pk: Option<&PublicKey>, + end_epoch: VnEpoch, + shard_group: ShardGroup, + ) -> Result { + self.db + .as_ref() + .unwrap() + .validator_nodes_count_for_shard_group(sidechain_pk, end_epoch, shard_group) + } + + fn get_validator_node( + &self, + sidechain_pk: Option<&PublicKey>, + public_key: PublicKey, + ) -> Result, ChainStorageError> { + self.db.as_ref().unwrap().get_validator_node(sidechain_pk, public_key) } fn fetch_template_registrations( diff --git a/base_layer/core/src/transactions/key_manager/interface.rs b/base_layer/core/src/transactions/key_manager/interface.rs index 44a19cc555..45c21da0e5 100644 --- a/base_layer/core/src/transactions/key_manager/interface.rs +++ b/base_layer/core/src/transactions/key_manager/interface.rs @@ -241,7 +241,7 @@ pub trait TransactionKeyManagerInterface: KeyManagerInterface { &self, private_key_id: &TariKeyId, nonce: &TariKeyId, - challenge: &[u8; 64], + message: &[u8; 64], ) -> Result; async fn get_receiver_partial_metadata_signature( diff --git a/base_layer/core/src/transactions/key_manager/wrapper.rs b/base_layer/core/src/transactions/key_manager/wrapper.rs index 77385031e5..e00a546c3f 100644 --- a/base_layer/core/src/transactions/key_manager/wrapper.rs +++ b/base_layer/core/src/transactions/key_manager/wrapper.rs @@ -506,12 +506,12 @@ where TBackend: KeyManagerBackend + 'static &self, private_key_id: &TariKeyId, nonce: &TariKeyId, - challenge: &[u8; 64], + message: &[u8; 64], ) -> Result { self.transaction_key_manager_inner .read() .await - .sign_with_nonce_and_challenge(private_key_id, nonce, challenge) + .sign_with_nonce_and_challenge(private_key_id, nonce, message) .await } diff --git a/base_layer/core/src/transactions/transaction_components/mod.rs b/base_layer/core/src/transactions/transaction_components/mod.rs index 3d31a0fe36..1428da7310 100644 --- a/base_layer/core/src/transactions/transaction_components/mod.rs +++ b/base_layer/core/src/transactions/transaction_components/mod.rs @@ -62,8 +62,7 @@ pub use output_features::CoinBaseExtra; mod output_features_version; mod output_type; mod range_proof_type; -mod side_chain; - +pub mod side_chain; mod transaction; mod transaction_builder; mod transaction_input; diff --git a/base_layer/core/src/transactions/transaction_components/output_features.rs b/base_layer/core/src/transactions/transaction_components/output_features.rs index 4a7becef4f..013cb0528c 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -28,10 +28,11 @@ use std::{ use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -use tari_common_types::types::{PublicKey, Signature}; +use tari_common_types::types::{PrivateKey, PublicKey}; use tari_max_size::MaxSizeBytes; +use tari_sidechain::EvictionProof; -use super::OutputFeaturesVersion; +use super::{OutputFeaturesVersion, SideChainFeatureData, SideChainId}; use crate::transactions::transaction_components::{ range_proof_type::RangeProofType, side_chain::SideChainFeature, @@ -46,7 +47,7 @@ use crate::transactions::transaction_components::{ pub type CoinBaseExtra = MaxSizeBytes<256>; /// Options for UTXO's -#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, BorshSerialize, BorshDeserialize)] pub struct OutputFeatures { pub version: OutputFeaturesVersion, /// Flags are the feature flags that differentiate between outputs, eg Coinbase all of which has different rules @@ -129,46 +130,68 @@ impl OutputFeatures { /// creates output features for a burned output with confidential output data pub fn create_burn_confidential_output( claim_public_key: PublicKey, - sidechain_id: Option, - sidechain_id_knowledge_proof: Option, + sidechain_deployment_key: Option<&PrivateKey>, ) -> OutputFeatures { + let output_data = ConfidentialOutputData { claim_public_key }; + let sidechain_id = sidechain_deployment_key.map(|k| SideChainId::sign(k, output_data.sidechain_id_message())); + OutputFeatures { output_type: OutputType::Burn, - sidechain_feature: Some(SideChainFeature::ConfidentialOutput(ConfidentialOutputData { - claim_public_key, + sidechain_feature: Some(SideChainFeature { + data: SideChainFeatureData::ConfidentialOutput(output_data), sidechain_id, - sidechain_id_knowledge_proof, - })), + }), ..Default::default() } } /// Creates template registration output features - pub fn for_template_registration(template_registration: CodeTemplateRegistration) -> OutputFeatures { + pub fn for_template_registration( + template_registration: CodeTemplateRegistration, + sidechain_deployment_key: Option<&PrivateKey>, + ) -> OutputFeatures { + let sidechain_id = + sidechain_deployment_key.map(|k| SideChainId::sign(k, template_registration.sidechain_id_message())); + OutputFeatures { output_type: OutputType::CodeTemplateRegistration, - sidechain_feature: Some(SideChainFeature::CodeTemplateRegistration(template_registration)), + sidechain_feature: Some(SideChainFeature { + data: SideChainFeatureData::CodeTemplateRegistration(template_registration), + sidechain_id, + }), ..Default::default() } } pub fn for_validator_node_registration( - public_key: PublicKey, - signature: Signature, + signature: ValidatorNodeSignature, claim_public_key: PublicKey, - sidechain_id: Option, - sidechain_id_knowledge_proof: Option, + sidechain_deployment_key: Option<&PrivateKey>, ) -> OutputFeatures { + let vn_reg = ValidatorNodeRegistration::new(signature, claim_public_key); + let sidechain_id = sidechain_deployment_key.map(|k| SideChainId::sign(k, vn_reg.sidechain_id_message())); OutputFeatures { output_type: OutputType::ValidatorNodeRegistration, - sidechain_feature: Some(SideChainFeature::ValidatorNodeRegistration( - ValidatorNodeRegistration::new( - ValidatorNodeSignature::new(public_key, signature), - claim_public_key, - sidechain_id, - sidechain_id_knowledge_proof, - ), - )), + sidechain_feature: Some(SideChainFeature { + data: SideChainFeatureData::ValidatorNodeRegistration(vn_reg), + sidechain_id, + }), + ..Default::default() + } + } + + pub fn for_validator_node_eviction( + eviction_proof: EvictionProof, + sidechain_deployment_key: Option<&PrivateKey>, + ) -> OutputFeatures { + let sidechain_id = + sidechain_deployment_key.map(|k| SideChainId::sign(k, eviction_proof.sidechain_id_message())); + OutputFeatures { + output_type: OutputType::SidechainProof, + sidechain_feature: Some(SideChainFeature { + data: SideChainFeatureData::EvictionProof(eviction_proof), + sidechain_id, + }), ..Default::default() } } diff --git a/base_layer/core/src/transactions/transaction_components/output_type.rs b/base_layer/core/src/transactions/transaction_components/output_type.rs index 4e7115b14a..6758883e2d 100644 --- a/base_layer/core/src/transactions/transaction_components/output_type.rs +++ b/base_layer/core/src/transactions/transaction_components/output_type.rs @@ -46,16 +46,20 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; #[repr(u8)] #[borsh(use_discriminant = true)] pub enum OutputType { - /// An standard output. + /// A standard output. Standard = 0, /// Output is a coinbase output, must not be spent until maturity. Coinbase = 1, /// Output is a burned output and can not be spent ever. Burn = 2, - /// Output defines a validator node registration + /// Output containing a validator node registration ValidatorNodeRegistration = 3, - /// Output defines a new re-usable code template. + /// Output containing a new re-usable code template. CodeTemplateRegistration = 4, + /// Output containing a sidechain checkpoint + SidechainCheckpoint = 5, + /// Output containing a sidechain proof. + SidechainProof = 6, } impl OutputType { @@ -77,13 +81,18 @@ impl OutputType { OutputType::Burn, OutputType::ValidatorNodeRegistration, OutputType::CodeTemplateRegistration, + OutputType::SidechainCheckpoint, + OutputType::SidechainProof, ] } pub fn is_sidechain_type(&self) -> bool { matches!( self, - OutputType::ValidatorNodeRegistration | OutputType::CodeTemplateRegistration | OutputType::Burn + OutputType::ValidatorNodeRegistration | + OutputType::CodeTemplateRegistration | + OutputType::SidechainCheckpoint | + OutputType::SidechainProof ) } @@ -108,7 +117,31 @@ impl Display for OutputType { #[cfg(test)] mod tests { use super::*; + #[test] + fn it_contains_all_enum_variants() { + let mut variant_bits = 0u8; + + fn check_duplicate(variant_bits: u8, mask: u8) { + if variant_bits & mask != 0 { + panic!("Duplicate variant"); + } + } + for variant in OutputType::all() { + let mask = 1 << *variant as u8; + check_duplicate(variant_bits, mask); + match variant { + OutputType::Standard => variant_bits |= mask, + OutputType::Coinbase => variant_bits |= mask, + OutputType::Burn => variant_bits |= mask, + OutputType::ValidatorNodeRegistration => variant_bits |= mask, + OutputType::CodeTemplateRegistration => variant_bits |= mask, + OutputType::SidechainCheckpoint => variant_bits |= mask, + OutputType::SidechainProof => variant_bits |= mask, + } + } + assert_eq!(variant_bits, 0b1111111); + } #[test] fn it_converts_from_byte_to_output_type() { assert_eq!(OutputType::from_byte(0), Some(OutputType::Standard)); @@ -116,7 +149,9 @@ mod tests { assert_eq!(OutputType::from_byte(2), Some(OutputType::Burn)); assert_eq!(OutputType::from_byte(3), Some(OutputType::ValidatorNodeRegistration)); assert_eq!(OutputType::from_byte(4), Some(OutputType::CodeTemplateRegistration)); - for i in 5..=255 { + assert_eq!(OutputType::from_byte(5), Some(OutputType::SidechainCheckpoint)); + assert_eq!(OutputType::from_byte(6), Some(OutputType::SidechainProof)); + for i in 7..=255 { assert_eq!(OutputType::from_byte(i), None); } } diff --git a/base_layer/core/src/transactions/transaction_components/range_proof_type.rs b/base_layer/core/src/transactions/transaction_components/range_proof_type.rs index 50fb8b1195..4be9658f58 100644 --- a/base_layer/core/src/transactions/transaction_components/range_proof_type.rs +++ b/base_layer/core/src/transactions/transaction_components/range_proof_type.rs @@ -93,6 +93,27 @@ impl FromStr for RangeProofType { mod tests { use super::*; + #[test] + fn it_contains_all_enum_variants() { + let mut variant_bits = 0u8; + + fn check_duplicate(variant_bits: u8, mask: u8) { + if variant_bits & mask != 0 { + panic!("Duplicate variant"); + } + } + + for variant in RangeProofType::all() { + let mask = 1 << *variant as u8; + check_duplicate(variant_bits, mask); + match variant { + RangeProofType::BulletProofPlus => variant_bits |= mask, + RangeProofType::RevealedValue => variant_bits |= mask, + } + } + assert_eq!(variant_bits, 0b11); + } + #[test] fn it_converts_from_byte_to_output_type() { assert_eq!(RangeProofType::default(), RangeProofType::BulletProofPlus); diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/confidential_output.rs b/base_layer/core/src/transactions/transaction_components/side_chain/confidential_output.rs index 2cc5cba3f6..f74bc16795 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/confidential_output.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/confidential_output.rs @@ -22,11 +22,16 @@ use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -use tari_common_types::types::{PublicKey, Signature}; +use tari_common_types::types::PublicKey; +use tari_utilities::ByteArray; -#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, BorshSerialize, BorshDeserialize)] pub struct ConfidentialOutputData { pub claim_public_key: PublicKey, - pub sidechain_id: Option, - pub sidechain_id_knowledge_proof: Option, +} + +impl ConfidentialOutputData { + pub fn sidechain_id_message(&self) -> &[u8] { + self.claim_public_key.as_bytes() + } } diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs b/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs index 6ccb03ea8a..e4ff939b2a 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. mod sidechain_feature; -pub use sidechain_feature::SideChainFeature; +pub use sidechain_feature::*; mod confidential_output; mod template_registration; @@ -29,12 +29,12 @@ mod validator_node_registration; mod validator_node_signature; use blake2::Blake2b; -pub use confidential_output::ConfidentialOutputData; +pub use confidential_output::*; use digest::consts::U32; use tari_crypto::{hash_domain, hashing::DomainSeparatedHasher}; -pub use template_registration::{BuildInfo, CodeTemplateRegistration, TemplateType}; -pub use validator_node_registration::ValidatorNodeRegistration; -pub use validator_node_signature::{ValidatorNodeHashDomain, ValidatorNodeSignature}; +pub use template_registration::*; +pub use validator_node_registration::*; +pub use validator_node_signature::*; hash_domain!( ContractAcceptanceHashDomain, diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_feature.rs b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_feature.rs index 6d41c7d1ff..527aa8c975 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_feature.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_feature.rs @@ -21,7 +21,11 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use borsh::{BorshDeserialize, BorshSerialize}; +use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; +use tari_common_types::types::{PrivateKey, PublicKey, Signature}; +use tari_crypto::{keys::PublicKey as _, ristretto::RistrettoSchnorr}; +use tari_sidechain::EvictionProof; use crate::transactions::transaction_components::{ side_chain::confidential_output::ConfidentialOutputData, @@ -29,32 +33,110 @@ use crate::transactions::transaction_components::{ ValidatorNodeRegistration, }; -#[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq, BorshSerialize, BorshDeserialize)] -pub enum SideChainFeature { - ValidatorNodeRegistration(ValidatorNodeRegistration), - CodeTemplateRegistration(CodeTemplateRegistration), - ConfidentialOutput(ConfidentialOutputData), +// NOTE: tari_mining_helper_ffi makes use of borsh encoding (not serde/bincode), therefore we need to +// implement BorshDeserialize on all types + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct SideChainFeature { + pub data: SideChainFeatureData, + pub sidechain_id: Option, } impl SideChainFeature { + pub fn is_sidechain_id_valid(&self) -> bool { + let Some(sidechain_id) = self.sidechain_id.as_ref() else { + return true; + }; + + match &self.data { + SideChainFeatureData::ValidatorNodeRegistration(reg) => sidechain_id.is_valid(reg.sidechain_id_message()), + SideChainFeatureData::CodeTemplateRegistration(reg) => sidechain_id.is_valid(reg.sidechain_id_message()), + SideChainFeatureData::ConfidentialOutput(output) => sidechain_id.is_valid(output.sidechain_id_message()), + SideChainFeatureData::EvictionProof(proof) => sidechain_id.is_valid(proof.sidechain_id_message()), + } + } + + pub fn data(&self) -> &SideChainFeatureData { + &self.data + } + + pub fn sidechain_id(&self) -> Option<&SideChainId> { + self.sidechain_id.as_ref() + } + + pub fn sidechain_public_key(&self) -> Option<&PublicKey> { + self.sidechain_id.as_ref().map(|id| id.public_key()) + } + pub fn code_template_registration(&self) -> Option<&CodeTemplateRegistration> { - match self { - Self::CodeTemplateRegistration(v) => Some(v), + match &self.data { + SideChainFeatureData::CodeTemplateRegistration(v) => Some(v), _ => None, } } pub fn validator_node_registration(&self) -> Option<&ValidatorNodeRegistration> { - match self { - Self::ValidatorNodeRegistration(v) => Some(v), + match &self.data { + SideChainFeatureData::ValidatorNodeRegistration(v) => Some(v), + _ => None, + } + } + + pub fn eviction_proof(&self) -> Option<&EvictionProof> { + match &self.data { + SideChainFeatureData::EvictionProof(v) => Some(v), _ => None, } } pub fn confidential_output_data(&self) -> Option<&ConfidentialOutputData> { - match self { - Self::ConfidentialOutput(v) => Some(v), + match &self.data { + SideChainFeatureData::ConfidentialOutput(v) => Some(v), _ => None, } } } + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub enum SideChainFeatureData { + ValidatorNodeRegistration(ValidatorNodeRegistration), + CodeTemplateRegistration(CodeTemplateRegistration), + ConfidentialOutput(ConfidentialOutputData), + EvictionProof(EvictionProof), +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct SideChainId { + public_key: PublicKey, + knowledge_proof: Signature, +} + +impl SideChainId { + pub fn new(public_key: PublicKey, knowledge_proof: Signature) -> Self { + Self { + public_key, + knowledge_proof, + } + } + + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + pub fn knowledge_proof(&self) -> &Signature { + &self.knowledge_proof + } + + pub fn sign>(private_key: &PrivateKey, message: T) -> Self { + let public_key = PublicKey::from_secret_key(private_key); + Self { + public_key, + knowledge_proof: RistrettoSchnorr::sign(private_key, message, &mut OsRng) + .expect("RistrettoSchnorr::sign is completely infallible"), + } + } + + pub fn is_valid>(&self, message: T) -> bool { + self.knowledge_proof.verify(&self.public_key, message) + } +} diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/template_registration.rs b/base_layer/core/src/transactions/transaction_components/side_chain/template_registration.rs index e574f79cb4..66965aaeff 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/template_registration.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/template_registration.rs @@ -27,11 +27,10 @@ use serde::{Deserialize, Serialize}; use tari_common_types::types::{FixedHash, PublicKey, Signature}; use tari_hashing::TransactionHashDomain; use tari_max_size::{MaxSizeBytes, MaxSizeString}; -use tari_utilities::ByteArray; use crate::consensus::DomainSeparatedConsensusHasher; -#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] pub struct CodeTemplateRegistration { pub author_public_key: PublicKey, pub author_signature: Signature, @@ -41,20 +40,27 @@ pub struct CodeTemplateRegistration { pub build_info: BuildInfo, pub binary_sha: FixedHash, pub binary_url: MaxSizeString<255>, - pub sidechain_id: Option, - pub sidechain_id_knowledge_proof: Option, } impl CodeTemplateRegistration { - pub fn create_challenge(&self, public_nonce: &PublicKey) -> [u8; 64] { + /// Creates a signature message used to prove knowledge of the author secret key + pub fn create_signature_message(&self, public_nonce: &PublicKey) -> [u8; 64] { DomainSeparatedConsensusHasher::>::new("template_registration") .chain(&self.author_public_key) .chain(public_nonce) + .chain(&self.template_name) + .chain(&self.template_version) + .chain(&self.template_type) + .chain(&self.build_info) .chain(&self.binary_sha) - .chain(&self.sidechain_id.as_ref().map(|n| n.to_vec()).unwrap_or(vec![0u8; 32])) + .chain(&self.binary_url) .finalize() .into() } + + pub fn sidechain_id_message(&self) -> [u8; 64] { + self.create_signature_message(self.author_signature.get_public_nonce()) + } } // -------------------------------- TemplateType -------------------------------- // diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/validator_node_registration.rs b/base_layer/core/src/transactions/transaction_components/side_chain/validator_node_registration.rs index d6eb49722f..53e5a2576e 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/validator_node_registration.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/validator_node_registration.rs @@ -34,31 +34,23 @@ use tari_utilities::ByteArray; use crate::{consensus::DomainSeparatedConsensusHasher, transactions::transaction_components::ValidatorNodeSignature}; -#[derive(Default, Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] pub struct ValidatorNodeRegistration { signature: ValidatorNodeSignature, claim_public_key: PublicKey, - sidechain_id: Option, - sidechain_id_knowledge_proof: Option, } impl ValidatorNodeRegistration { - pub fn new( - signature: ValidatorNodeSignature, - claim_public_key: PublicKey, - sidechain_id: Option, - sidechain_id_knowledge_proof: Option, - ) -> Self { + pub fn new(signature: ValidatorNodeSignature, claim_public_key: PublicKey) -> Self { Self { signature, claim_public_key, - sidechain_id, - sidechain_id_knowledge_proof, } } - pub fn is_valid_signature_for(&self, msg: &[u8]) -> bool { - self.signature.is_valid_signature_for(&self.claim_public_key, msg) + pub fn is_valid_signature_for(&self, sidechain_pk: Option<&PublicKey>, epoch: VnEpoch) -> bool { + self.signature + .is_valid_signature_for(sidechain_pk, &self.claim_public_key, epoch) } pub fn derive_shard_key( @@ -92,12 +84,8 @@ impl ValidatorNodeRegistration { self.signature.signature() } - pub fn sidechain_id(&self) -> Option<&PublicKey> { - self.sidechain_id.as_ref() - } - - pub fn sidechain_id_knowledge_proof(&self) -> Option<&Signature> { - self.sidechain_id_knowledge_proof.as_ref() + pub fn sidechain_id_message(&self) -> &[u8] { + self.public_key().as_bytes() } } @@ -130,10 +118,8 @@ mod test { let claim_public_key = PublicKey::from_secret_key(&sk); ValidatorNodeRegistration::new( - ValidatorNodeSignature::sign(&sk, &claim_public_key, b"valid"), + ValidatorNodeSignature::sign(&sk, None, &claim_public_key, VnEpoch(1)), claim_public_key, - None, - None, ) } @@ -143,13 +129,13 @@ mod test { #[test] fn it_returns_true_for_valid_signature() { let reg = create_instance(); - assert!(reg.is_valid_signature_for(b"valid")); + assert!(reg.is_valid_signature_for(None, VnEpoch(1))); } #[test] fn it_returns_false_for_invalid_challenge() { let reg = create_instance(); - assert!(!reg.is_valid_signature_for(b"there's wally")); + assert!(!reg.is_valid_signature_for(None, VnEpoch(2))); } #[test] @@ -158,10 +144,8 @@ mod test { reg = ValidatorNodeRegistration::new( ValidatorNodeSignature::new(reg.public_key().clone(), Signature::default()), Default::default(), - None, - None, ); - assert!(!reg.is_valid_signature_for(b"valid")); + assert!(!reg.is_valid_signature_for(None, VnEpoch(1))); } } diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/validator_node_signature.rs b/base_layer/core/src/transactions/transaction_components/side_chain/validator_node_signature.rs index a9ba9cd48c..9c08f6bffd 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/validator_node_signature.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/validator_node_signature.rs @@ -20,22 +20,17 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use blake2::Blake2b; use borsh::{BorshDeserialize, BorshSerialize}; -use digest::consts::U64; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; -use tari_common_types::types::{PrivateKey, PublicKey, Signature}; -use tari_crypto::{hash_domain, hashing::DomainSeparatedHasher, keys::PublicKey as PublicKeyT}; -use tari_utilities::ByteArray; +use tari_common_types::{ + epoch::VnEpoch, + types::{PrivateKey, PublicKey, Signature}, +}; +use tari_crypto::keys::PublicKey as PublicKeyT; +use tari_hashing::layer2::validator_registration_hasher; -hash_domain!( - ValidatorNodeHashDomain, - "com.tari.base_layer.core.transactions.side_chain.validator_node", - 0 -); - -#[derive(Default, Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] pub struct ValidatorNodeSignature { public_key: PublicKey, signature: Signature, @@ -46,35 +41,49 @@ impl ValidatorNodeSignature { Self { public_key, signature } } - pub fn sign(private_key: &PrivateKey, claim_public_key: &PublicKey, msg: &[u8]) -> Self { + pub fn sign( + private_key: &PrivateKey, + sidechain_pk: Option<&PublicKey>, + claim_public_key: &PublicKey, + epoch: VnEpoch, + ) -> Self { let (secret_nonce, public_nonce) = PublicKey::random_keypair(&mut OsRng); let public_key = PublicKey::from_secret_key(private_key); - let challenge = Self::construct_challenge(&public_key, &public_nonce, claim_public_key, msg); + let challenge = + Self::construct_signature_message(&public_key, &public_nonce, sidechain_pk, claim_public_key, epoch); let signature = Signature::sign_raw_uniform(private_key, secret_nonce, &challenge) .expect("Sign cannot fail with 64-byte challenge and a RistrettoPublicKey"); Self { public_key, signature } } - fn construct_challenge( + fn construct_signature_message( public_key: &PublicKey, public_nonce: &PublicKey, + sidechain_pk: Option<&PublicKey>, claim_public_key: &PublicKey, - msg: &[u8], + epoch: VnEpoch, ) -> [u8; 64] { - let hasher = DomainSeparatedHasher::, ValidatorNodeHashDomain>::new_with_label("registration") - .chain(public_key.as_bytes()) - .chain(public_nonce.as_bytes()) - .chain(claim_public_key.as_bytes()) - .chain(msg); - digest::Digest::finalize(hasher).into() + validator_registration_hasher() + .chain(public_key) + .chain(public_nonce) + .chain(&sidechain_pk) + .chain(claim_public_key) + .chain(&epoch) + .finalize_into_array() } - pub fn is_valid_signature_for(&self, claim_public_key: &PublicKey, msg: &[u8]) -> bool { - let challenge = Self::construct_challenge( + pub fn is_valid_signature_for( + &self, + sidechain_pk: Option<&PublicKey>, + claim_public_key: &PublicKey, + epoch: VnEpoch, + ) -> bool { + let challenge = Self::construct_signature_message( &self.public_key, self.signature.get_public_nonce(), + sidechain_pk, claim_public_key, - msg, + epoch, ); self.signature.verify_raw_uniform(&self.public_key, &challenge) } diff --git a/base_layer/core/src/transactions/transaction_components/transaction_input.rs b/base_layer/core/src/transactions/transaction_components/transaction_input.rs index d659e4bfe4..09559c7a28 100644 --- a/base_layer/core/src/transactions/transaction_components/transaction_input.rs +++ b/base_layer/core/src/transactions/transaction_components/transaction_input.rs @@ -141,30 +141,18 @@ impl TransactionInput { } /// Populate the spent output data fields - pub fn add_output_data( - &mut self, - version: TransactionOutputVersion, - features: OutputFeatures, - commitment: Commitment, - script: TariScript, - sender_offset_public_key: PublicKey, - covenant: Covenant, - encrypted_data: EncryptedData, - metadata_signature: ComAndPubSignature, - rangeproof_hash: FixedHash, - minimum_value_promise: MicroMinotari, - ) { + pub fn add_output_data(&mut self, output: TransactionOutput) { self.spent_output = SpentOutput::OutputData { - version, - features, - commitment, - script, - sender_offset_public_key, - covenant, - encrypted_data, - metadata_signature, - rangeproof_hash, - minimum_value_promise, + version: output.version, + features: output.features, + commitment: output.commitment, + script: output.script, + sender_offset_public_key: output.sender_offset_public_key, + covenant: output.covenant, + encrypted_data: output.encrypted_data, + metadata_signature: output.metadata_signature, + rangeproof_hash: output.proof.map(|p| p.hash()).unwrap_or_else(FixedHash::zero), + minimum_value_promise: output.minimum_value_promise, }; } diff --git a/base_layer/core/src/transactions/transaction_components/transaction_output.rs b/base_layer/core/src/transactions/transaction_components/transaction_output.rs index c8854a16e3..ceb05f5baa 100644 --- a/base_layer/core/src/transactions/transaction_components/transaction_output.rs +++ b/base_layer/core/src/transactions/transaction_components/transaction_output.rs @@ -33,15 +33,18 @@ use borsh::{BorshDeserialize, BorshSerialize}; use digest::consts::{U32, U64}; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; -use tari_common_types::types::{ - ComAndPubSignature, - Commitment, - CommitmentFactory, - FixedHash, - PrivateKey, - PublicKey, - RangeProof, - RangeProofService, +use tari_common_types::{ + epoch::VnEpoch, + types::{ + ComAndPubSignature, + Commitment, + CommitmentFactory, + FixedHash, + PrivateKey, + PublicKey, + RangeProof, + RangeProofService, + }, }; use tari_crypto::{ commitment::HomomorphicCommitmentFactory, @@ -340,17 +343,22 @@ impl TransactionOutput { } pub fn verify_validator_node_signature(&self) -> Result<(), TransactionError> { - if let Some(validator_node_reg) = self - .features - .sidechain_feature - .as_ref() - .and_then(|f| f.validator_node_registration()) - { - if !validator_node_reg.is_valid_signature_for(&[]) { - return Err(TransactionError::InvalidSignatureError( - "Validator node signature is not valid!".to_string(), - )); - } + let Some(sidechain_features) = self.features.sidechain_feature.as_ref() else { + return Ok(()); + }; + + let Some(validator_node_reg) = sidechain_features.validator_node_registration() else { + return Ok(()); + }; + + if !validator_node_reg.is_valid_signature_for( + sidechain_features.sidechain_id.as_ref().map(|id| id.public_key()), + // TODO: use actual epoch + VnEpoch::zero(), + ) { + return Err(TransactionError::InvalidSignatureError( + "Validator node signature is not valid!".to_string(), + )); } Ok(()) } @@ -382,6 +390,10 @@ impl TransactionOutput { matches!(self.features.output_type, OutputType::Burn) } + pub fn is_burned_to_sidechain(&self) -> bool { + self.is_burned() && self.features.sidechain_feature.is_some() + } + /// Convenience function that calculates the challenge for the metadata commitment signature pub fn build_metadata_signature_challenge( version: &TransactionOutputVersion, diff --git a/base_layer/core/src/validation/aggregate_body/aggregate_body_chain_validator.rs b/base_layer/core/src/validation/aggregate_body/aggregate_body_chain_validator.rs index 840f3f0994..53f33cafeb 100644 --- a/base_layer/core/src/validation/aggregate_body/aggregate_body_chain_validator.rs +++ b/base_layer/core/src/validation/aggregate_body/aggregate_body_chain_validator.rs @@ -23,10 +23,10 @@ use std::collections::HashSet; use log::warn; -use tari_common_types::types::FixedHash; use tari_utilities::hex::Hex; use crate::{ + blocks::BlockHeader, chain_storage::BlockchainBackend, consensus::{ConsensusConstants, ConsensusManager}, transactions::{ @@ -35,10 +35,12 @@ use crate::{ }, validation::{ helpers::{ + check_eviction_proof, check_input_is_utxo, check_not_duplicate_txo, check_tari_encrypted_data_byte_size, check_tari_script_byte_size, + check_validator_node_registration, }, ValidationError, }, @@ -60,13 +62,13 @@ impl AggregateBodyChainLinkedValidator { pub fn validate( &self, body: &AggregateBody, - height: u64, + header: &BlockHeader, db: &B, ) -> Result { - let constants = self.consensus_manager.consensus_constants(height); + let constants = self.consensus_manager.consensus_constants(header.height); self.validate_consensus(body, db)?; - let body = self.validate_input_and_maturity(body, db, constants, height)?; + let body = self.validate_body(body, db, constants, header)?; Ok(body) } @@ -76,12 +78,12 @@ impl AggregateBodyChainLinkedValidator { Ok(()) } - fn validate_input_and_maturity( + fn validate_body( &self, body: &AggregateBody, db: &B, constants: &ConsensusConstants, - height: u64, + header: &BlockHeader, ) -> Result { // inputs may be "slim", only containing references to outputs // so we need to resolve those references, creating a new body in the process @@ -89,12 +91,12 @@ impl AggregateBodyChainLinkedValidator { // UNCHECKED: sorting has been checked by the AggregateBodyInternalConsistencyValidator let body = AggregateBody::new_sorted_unchecked(inputs, body.outputs().to_vec(), body.kernels().to_vec()); - validate_input_maturity(&body, height)?; + validate_input_maturity(&body, header.height)?; check_inputs_are_utxos(db, &body)?; - check_outputs(db, constants, &body)?; + check_outputs(db, constants, &body, header.height)?; verify_no_duplicated_inputs_outputs(&body)?; check_total_burned(&body)?; - verify_timelocks(&body, height)?; + verify_timelocks(&body, header.height)?; Ok(body) } @@ -107,18 +109,23 @@ fn validate_input_not_pruned( let mut inputs: Vec = body.inputs().clone(); for input in &mut inputs { if input.is_compact() { - let output = match db.fetch_output(&input.output_hash()) { + let input_output_hash = input.output_hash(); + // TODO: we clone the block body 3 times in validation and the inputs 1 more time here. We also discard + // the hydrated block in all cases expect block sync. This is unnecessarily slow and wasteful. + // SIMPLE REFACTOR: populate/hydrate the block inputs (which is owned, no cloning necessary) before + // performing validation. If hydration fails with UnknownInput, the block is invalid. + let output = match db.fetch_output(&input_output_hash) { Ok(val) => match val { Some(output_mined_info) => output_mined_info.output, None => { - let input_output_hash = input.output_hash(); + // Input is found in this block if let Some(found) = body.outputs().iter().find(|o| o.hash() == input_output_hash) { found.clone() } else { warn!( target: LOG_TARGET, "Input not found in database or block, commitment: {}, hash: {}", - input.commitment()?.to_hex(), input_output_hash.to_hex() + input.commitment()?.as_public_key(), input_output_hash, ); return Err(ValidationError::UnknownInput); } @@ -127,22 +134,7 @@ fn validate_input_not_pruned( Err(e) => return Err(ValidationError::from(e)), }; - let rp_hash = match output.proof { - Some(proof) => proof.hash(), - None => FixedHash::zero(), - }; - input.add_output_data( - output.version, - output.features, - output.commitment, - output.script, - output.sender_offset_public_key, - output.covenant, - output.encrypted_data, - output.metadata_signature, - rp_hash, - output.minimum_value_promise, - ); + input.add_output_data(output); } } @@ -225,6 +217,7 @@ pub fn check_outputs( db: &B, constants: &ConsensusConstants, body: &AggregateBody, + height: u64, ) -> Result<(), ValidationError> { let max_script_size = constants.max_script_byte_size(); let max_encrypted_data_size = constants.max_extra_encrypted_data_byte_size(); @@ -232,6 +225,8 @@ pub fn check_outputs( check_tari_script_byte_size(&output.script, max_script_size)?; check_tari_encrypted_data_byte_size(&output.encrypted_data, max_encrypted_data_size)?; check_not_duplicate_txo(db, output)?; + check_validator_node_registration(db, output, height)?; + check_eviction_proof(db, output, constants)?; } Ok(()) } diff --git a/base_layer/core/src/validation/aggregate_body/aggregate_body_internal_validator.rs b/base_layer/core/src/validation/aggregate_body/aggregate_body_internal_validator.rs index df097bcb92..ec215e575a 100644 --- a/base_layer/core/src/validation/aggregate_body/aggregate_body_internal_validator.rs +++ b/base_layer/core/src/validation/aggregate_body/aggregate_body_internal_validator.rs @@ -23,14 +23,17 @@ use std::{collections::HashSet, convert::TryInto}; use log::{trace, warn}; -use tari_common_types::types::{Commitment, CommitmentFactory, HashOutput, PrivateKey, PublicKey, RangeProofService}; +use tari_common_types::{ + epoch::VnEpoch, + types::{Commitment, CommitmentFactory, HashOutput, PrivateKey, PublicKey, RangeProofService}, +}; use tari_crypto::{ commitment::HomomorphicCommitmentFactory, keys::PublicKey as PublicKeyTrait, ristretto::pedersen::PedersenCommitment, }; use tari_script::ScriptContext; -use tari_utilities::{hex::Hex, ByteArray}; +use tari_utilities::hex::Hex; use crate::{ consensus::{ConsensusConstants, ConsensusManager}, @@ -40,6 +43,7 @@ use crate::{ transaction_components::{ transaction_output::batch_verify_range_proofs, KernelSum, + SideChainFeature, TransactionError, TransactionInput, TransactionKernel, @@ -117,15 +121,13 @@ impl AggregateBodyInternalConsistencyValidator { check_encrypted_data_byte_size(output, constants.max_extra_encrypted_data_byte_size())?; check_covenant_length(&output.covenant, constants.max_covenant_length())?; check_permitted_range_proof_types(constants, output)?; - check_validator_node_registration_utxo(constants, output)?; - check_template_registration_utxo(output)?; - check_confidential_output_utxo(output)?; + check_sidechain_features(constants, output)?; } check_weight(body, height, constants)?; check_sorting_and_duplicates(body)?; - // Check that the inputs are are allowed to be spent + // Check that inputs are allowed to be spent check_maturity(height, body.inputs())?; check_kernel_lock_height(height, body.kernels())?; @@ -147,58 +149,16 @@ impl AggregateBodyInternalConsistencyValidator { } } -fn check_confidential_output_utxo(output: &TransactionOutput) -> Result<(), ValidationError> { - if let Some(conf_output) = output.features.confidential_output_data() { - if conf_output.sidechain_id.is_some() || conf_output.sidechain_id_knowledge_proof.is_some() { - // If one of these is set, both must be set - if conf_output.sidechain_id.is_none() || conf_output.sidechain_id_knowledge_proof.is_none() { - return Err(ValidationError::ConfidentialOutputSidechainNotSet); - } - // If set, the signature must be valid - let sig_pub_key = conf_output.sidechain_id.as_ref().unwrap(); - if !conf_output - .sidechain_id_knowledge_proof - .as_ref() - .unwrap() - .verify(sig_pub_key, conf_output.claim_public_key.to_vec()) - { - return Err(ValidationError::ConfidentialOutputSidechainIdKnowledgeProofNotValid); - } - } - } - Ok(()) -} - -fn check_template_registration_utxo(output: &TransactionOutput) -> Result<(), ValidationError> { - if let Some(temp) = output.features.code_template_registration() { - let challenge = temp.create_challenge(temp.author_signature.get_public_nonce()); +fn check_template_registration_utxo(sidechain_feature: &SideChainFeature) -> Result<(), ValidationError> { + if let Some(template_reg) = sidechain_feature.code_template_registration() { + let message = template_reg.create_signature_message(template_reg.author_signature.get_public_nonce()); - if !temp + if !template_reg .author_signature - .verify_raw_uniform(&temp.author_public_key, &challenge) + .verify_raw_uniform(&template_reg.author_public_key, &message) { return Err(ValidationError::TemplateAuthorSignatureNotValid); } - - if temp.sidechain_id.is_some() || temp.sidechain_id_knowledge_proof.is_some() { - // If one of these is set, both must be set - if temp.sidechain_id.is_none() || temp.sidechain_id_knowledge_proof.is_none() { - return Err(ValidationError::TemplateRegistrationSidechainNotSet); - } - - // If set, the signature must be valid - let sig_pub_key = temp.sidechain_id.as_ref().unwrap(); - // TODO: I've used the author pub key here. The author signature includes the network - // as part of it's challenge. Should there be other fields in here as well? - if !temp - .sidechain_id_knowledge_proof - .as_ref() - .unwrap() - .verify(sig_pub_key, temp.author_public_key.to_vec()) - { - return Err(ValidationError::TemplateInvalidSidechainIdKnowledgeProof); - } - } } Ok(()) } @@ -449,45 +409,54 @@ fn check_total_burned(body: &AggregateBody) -> Result<(), ValidationError> { Ok(()) } +fn check_sidechain_features(constants: &ConsensusConstants, output: &TransactionOutput) -> Result<(), ValidationError> { + let Some(sidechain_feature) = output.features.sidechain_feature.as_ref() else { + return Ok(()); + }; + check_sidechain_id_proof_of_knowledge(sidechain_feature)?; + check_validator_node_registration_utxo(constants, output, sidechain_feature)?; + check_template_registration_utxo(sidechain_feature)?; + + Ok(()) +} +fn check_sidechain_id_proof_of_knowledge(sidechain_feature: &SideChainFeature) -> Result<(), ValidationError> { + if !sidechain_feature.is_sidechain_id_valid() { + return Err(ValidationError::ValidatorNodeInvalidSidechainIdKnowledgeProof); + } + + Ok(()) +} fn check_validator_node_registration_utxo( consensus_constants: &ConsensusConstants, utxo: &TransactionOutput, + sidechain_feature: &SideChainFeature, ) -> Result<(), ValidationError> { - if let Some(reg) = utxo.features.validator_node_registration() { - if utxo.minimum_value_promise < consensus_constants.validator_node_registration_min_deposit_amount() { - return Err(ValidationError::ValidatorNodeRegistrationMinDepositAmount { - min: consensus_constants.validator_node_registration_min_deposit_amount(), - actual: utxo.minimum_value_promise, - }); - } - if utxo.features.maturity < consensus_constants.validator_node_registration_min_lock_height() { - return Err(ValidationError::ValidatorNodeRegistrationMinLockHeight { - min: consensus_constants.validator_node_registration_min_lock_height(), - actual: utxo.features.maturity, - }); - } + let Some(reg) = sidechain_feature.validator_node_registration() else { + return Ok(()); + }; - // TODO: This should be the claim public key that is being signed - if !reg.is_valid_signature_for(&[]) { - return Err(ValidationError::InvalidValidatorNodeSignature); - } + if utxo.minimum_value_promise < consensus_constants.validator_node_registration_min_deposit_amount() { + return Err(ValidationError::ValidatorNodeRegistrationMinDepositAmount { + min: consensus_constants.validator_node_registration_min_deposit_amount(), + actual: utxo.minimum_value_promise, + }); + } + if utxo.features.maturity < consensus_constants.validator_node_registration_min_lock_height() { + return Err(ValidationError::ValidatorNodeRegistrationMinLockHeight { + min: consensus_constants.validator_node_registration_min_lock_height(), + actual: utxo.features.maturity, + }); + } - if reg.sidechain_id().is_some() || reg.sidechain_id_knowledge_proof().is_some() { - // If one of these is set, both must be set - if reg.sidechain_id().is_none() || reg.sidechain_id_knowledge_proof().is_none() { - return Err(ValidationError::ValidatorNodeRegistrationSidechainNotSet); - } - // If set, the signature must be valid - let sig_pub_key = reg.sidechain_id().unwrap(); - if !reg - .sidechain_id_knowledge_proof() - .unwrap() - .verify(sig_pub_key, reg.public_key().to_vec()) - { - return Err(ValidationError::ValidatorNodeInvalidSidechainIdKnowledgeProof); - } - } + // TODO: some additional "single use data" e.g. epoch should be used to prevent replay + // (assuming that we disallow the same validator node to register multiple times). + if !reg.is_valid_signature_for( + sidechain_feature.sidechain_id.as_ref().map(|id| id.public_key()), + VnEpoch::zero(), + ) { + return Err(ValidationError::InvalidValidatorNodeSignature); } + Ok(()) } diff --git a/base_layer/core/src/validation/block_body/block_body_full_validator.rs b/base_layer/core/src/validation/block_body/block_body_full_validator.rs index 5e02ced3e9..adc65a465e 100644 --- a/base_layer/core/src/validation/block_body/block_body_full_validator.rs +++ b/base_layer/core/src/validation/block_body/block_body_full_validator.rs @@ -75,11 +75,12 @@ impl BlockBodyFullValidator { } // validate the block body against the current db - let body = &block.body; - let height = block.header.height; // the inputs may be only references to outputs, that's why the validator returns a new body and we need a new - // block - let body = self.aggregate_body_chain_validator.validate(body, height, backend)?; + // block (TODO: refactor to avoid cloning the block so many times (here and elsewhere) by hydrating the inputs + // before calling the validator) + let body = self + .aggregate_body_chain_validator + .validate(&block.body, &block.header, backend)?; let block = Block::new(block.header.clone(), body); // validate the internal consistency of the block body diff --git a/base_layer/core/src/validation/block_body/test.rs b/base_layer/core/src/validation/block_body/test.rs index fede6a9984..fa4171b125 100644 --- a/base_layer/core/src/validation/block_body/test.rs +++ b/base_layer/core/src/validation/block_body/test.rs @@ -618,7 +618,7 @@ mod orphan_validator { let rules = ConsensusManager::builder(Network::LocalNet) .add_consensus_constants( ConsensusConstantsBuilder::new(Network::LocalNet) - .with_permitted_range_proof_types([ + .with_permitted_range_proof_types(&[ (OutputType::Standard, &[RangeProofType::RevealedValue]), (OutputType::Coinbase, &[RangeProofType::RevealedValue]), (OutputType::Burn, &[RangeProofType::RevealedValue]), @@ -652,7 +652,7 @@ mod orphan_validator { let rules = ConsensusManager::builder(Network::LocalNet) .add_consensus_constants( ConsensusConstantsBuilder::new(Network::LocalNet) - .with_permitted_range_proof_types([ + .with_permitted_range_proof_types(&[ (OutputType::Standard, &[RangeProofType::BulletProofPlus]), (OutputType::Coinbase, &[RangeProofType::BulletProofPlus]), (OutputType::Burn, &[RangeProofType::BulletProofPlus]), @@ -686,13 +686,9 @@ mod orphan_validator { let rules = ConsensusManager::builder(Network::LocalNet) .add_consensus_constants( ConsensusConstantsBuilder::new(Network::LocalNet) - .with_permitted_range_proof_types([ - (OutputType::CodeTemplateRegistration, &[RangeProofType::BulletProofPlus]), - (OutputType::CodeTemplateRegistration, &[RangeProofType::BulletProofPlus]), - (OutputType::CodeTemplateRegistration, &[RangeProofType::BulletProofPlus]), - (OutputType::CodeTemplateRegistration, &[RangeProofType::BulletProofPlus]), - (OutputType::CodeTemplateRegistration, &[RangeProofType::BulletProofPlus]), - ]) + .with_permitted_range_proof_types(&[(OutputType::CodeTemplateRegistration, &[ + RangeProofType::BulletProofPlus, + ])]) .with_coinbase_lockheight(0) .build(), ) diff --git a/base_layer/core/src/validation/error.rs b/base_layer/core/src/validation/error.rs index 9a734ee20f..a535dddf28 100644 --- a/base_layer/core/src/validation/error.rs +++ b/base_layer/core/src/validation/error.rs @@ -20,7 +20,11 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use tari_common_types::types::HashOutput; +use tari_common_types::{ + epoch::VnEpoch, + types::{HashOutput, PublicKey}, +}; +use tari_sidechain::SidechainProofValidationError; use thiserror::Error; use crate::{ @@ -123,29 +127,14 @@ pub enum ValidationError { ValidatorNodeRegistrationMinDepositAmount { min: MicroMinotari, actual: MicroMinotari }, #[error("Validator registration has invalid maturity {actual}, must be at least {min}")] ValidatorNodeRegistrationMinLockHeight { min: u64, actual: u64 }, - #[error( - "Sidechain not set for template registration. If sidechain_id is set, then sidechain_id_knowledge_proof must \ - also be set" - )] - TemplateRegistrationSidechainNotSet, #[error("Sidechain ID knowledge proof not valid for template registration")] TemplateInvalidSidechainIdKnowledgeProof, #[error("Author signature not valid for template registration")] TemplateAuthorSignatureNotValid, - #[error( - "Sidechain not set for confidential output. If sidechain_id is set, then sidechain_id_knowledge_proof must \ - also be set" - )] - ConfidentialOutputSidechainNotSet, #[error("Sidechain ID knowledge proof not valid for confidential output")] ConfidentialOutputSidechainIdKnowledgeProofNotValid, #[error("Validator node registration signature failed verification")] InvalidValidatorNodeSignature, - #[error( - "Sidechain not set for validator node registration. If sidechain_id is set, then sidechain_id_knowledge_proof \ - must also be set" - )] - ValidatorNodeRegistrationSidechainNotSet, #[error("Sidechain ID knowledge proof not valid for validator node registration")] ValidatorNodeInvalidSidechainIdKnowledgeProof, #[error( @@ -157,6 +146,16 @@ pub enum ValidationError { DifficultyError(#[from] DifficultyError), #[error("Covenant too large. Max size: {max_size}, Actual size: {actual_size}")] CovenantTooLarge { max_size: usize, actual_size: usize }, + #[error("Sidechain proof invalid: `{0}`")] + SidechainProofInvalid(#[from] SidechainProofValidationError), + #[error("Sidechain eviction proof submitted for unregistered validator {validator_pk}")] + SidechainEvictionProofValidatorNotFound { validator_pk: PublicKey }, + #[error( + "Sidechain eviction proof invalid: given epoch {epoch} is greater than the epoch at tip height {tip_height}" + )] + SidechainEvictionProofInvalidEpoch { epoch: VnEpoch, tip_height: u64 }, + #[error("Validator node already registered: {public_key}")] + ValidatorNodeAlreadyRegistered { public_key: PublicKey }, } // ChainStorageError has a ValidationError variant, so to prevent a cyclic dependency we use a string representation in @@ -205,15 +204,16 @@ impl ValidationError { err @ ValidationError::ValidatorNodeRegistrationMinDepositAmount { .. } | err @ ValidationError::ValidatorNodeRegistrationMinLockHeight { .. } | err @ ValidationError::InvalidValidatorNodeSignature | - err @ ValidationError::ValidatorNodeRegistrationSidechainNotSet | err @ ValidationError::ValidatorNodeInvalidSidechainIdKnowledgeProof | - err @ ValidationError::TemplateRegistrationSidechainNotSet | err @ ValidationError::TemplateInvalidSidechainIdKnowledgeProof | err @ ValidationError::TemplateAuthorSignatureNotValid | - err @ ValidationError::ConfidentialOutputSidechainNotSet | err @ ValidationError::ConfidentialOutputSidechainIdKnowledgeProofNotValid | err @ ValidationError::DifficultyError(_) | err @ ValidationError::CoinbaseExceedsMaxLimit | + err @ ValidationError::SidechainEvictionProofValidatorNotFound { .. } | + err @ ValidationError::SidechainProofInvalid(_) | + err @ ValidationError::SidechainEvictionProofInvalidEpoch { .. } | + err @ ValidationError::ValidatorNodeAlreadyRegistered { .. } | err @ ValidationError::CovenantTooLarge { .. } => Some(BanReason { reason: err.to_string(), ban_duration: BanPeriod::Long, diff --git a/base_layer/core/src/validation/helpers.rs b/base_layer/core/src/validation/helpers.rs index 160706defe..04e93b409a 100644 --- a/base_layer/core/src/validation/helpers.rs +++ b/base_layer/core/src/validation/helpers.rs @@ -23,9 +23,10 @@ use std::convert::TryFrom; use log::*; -use tari_common_types::types::FixedHash; +use tari_common_types::types::{FixedHash, PublicKey}; use tari_crypto::tari_utilities::{epoch_time::EpochTime, hex::Hex}; use tari_script::TariScript; +use tari_sidechain::SidechainProofValidationError; use crate::{ blocks::{BlockHeader, BlockHeaderValidationError, BlockValidationError}, @@ -262,6 +263,86 @@ pub fn check_not_duplicate_txo( Ok(()) } +/// This function checks the validity of the validator node registration if appliable +pub fn check_validator_node_registration( + db: &B, + output: &TransactionOutput, + height: u64, +) -> Result<(), ValidationError> { + let Some(sidechain_features) = output.features.sidechain_feature.as_ref() else { + return Ok(()); + }; + let Some(vn_reg) = sidechain_features.validator_node_registration() else { + return Ok(()); + }; + + if db.validator_node_exists(sidechain_features.sidechain_public_key(), height, vn_reg.public_key())? { + return Err(ValidationError::ValidatorNodeAlreadyRegistered { + public_key: vn_reg.public_key().clone(), + }); + } + + Ok(()) +} + +/// This function checks the validity of the eviction proof if appliable +pub fn check_eviction_proof( + db: &B, + output: &TransactionOutput, + constants: &ConsensusConstants, +) -> Result<(), ValidationError> { + let Some(sidechain_features) = output.features.sidechain_feature.as_ref() else { + return Ok(()); + }; + let Some(eviction_proof) = sidechain_features.eviction_proof() else { + return Ok(()); + }; + + let epoch = eviction_proof.epoch(); + let shard_group = eviction_proof.shard_group(); + + let chain_metadata = db.fetch_chain_metadata()?; + let tip_height = chain_metadata.best_block_height(); + let tip_epoch = constants.block_height_to_epoch(tip_height); + if epoch > tip_epoch { + return Err(ValidationError::SidechainEvictionProofInvalidEpoch { + epoch, + tip_height: chain_metadata.best_block_height(), + }); + } + + let validator_pk = eviction_proof.node_to_evict(); + + // Only allow a single exit or evict on an active validator + if !db.validator_node_is_active_for_shard_group( + sidechain_features.sidechain_public_key(), + tip_epoch, + validator_pk, + shard_group, + )? { + return Err(ValidationError::SidechainEvictionProofValidatorNotFound { + validator_pk: validator_pk.clone(), + }); + } + + let committee_size = + db.validator_nodes_count_for_shard_group(sidechain_features.sidechain_public_key(), tip_epoch, shard_group)?; + let quorum_threshold = committee_size - (committee_size - 1) / 3; + + let sidechain_pk = sidechain_features.sidechain_public_key(); + + let check_vn = |public_key: &PublicKey| { + let is_active = db + .validator_node_is_active_for_shard_group(sidechain_pk, tip_epoch, public_key, shard_group) + .map_err(SidechainProofValidationError::internal_error)?; + + Ok(is_active) + }; + + eviction_proof.validate(quorum_threshold, &check_vn)?; + + Ok(()) +} pub fn check_mmr_roots(header: &BlockHeader, mmr_roots: &MmrRoots) -> Result<(), ValidationError> { if header.kernel_mr != mmr_roots.kernel_mr { diff --git a/base_layer/core/src/validation/transaction/transaction_chain_validator.rs b/base_layer/core/src/validation/transaction/transaction_chain_validator.rs index 7879cd50f5..a9c33d0b36 100644 --- a/base_layer/core/src/validation/transaction/transaction_chain_validator.rs +++ b/base_layer/core/src/validation/transaction/transaction_chain_validator.rs @@ -62,8 +62,9 @@ impl TransactionValidator for TransactionChainLinkedValida { let db = self.db.db_read_access()?; - let tip_height = db.fetch_chain_metadata()?.best_block_height(); - self.aggregate_body_validator.validate(&tx.body, tip_height, &*db)?; + let tip_header = db.fetch_tip_header()?; + self.aggregate_body_validator + .validate(&tx.body, tip_header.header(), &*db)?; }; Ok(()) diff --git a/base_layer/sidechain/Cargo.toml b/base_layer/sidechain/Cargo.toml new file mode 100644 index 0000000000..00a7516e60 --- /dev/null +++ b/base_layer/sidechain/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tari_sidechain" +version.workspace = true +edition.workspace = true +authors.workspace = true + +[dependencies] +tari_hashing = { workspace = true } +tari_crypto = { version = "0.21.0", features = ["borsh"] } +tari_utilities = "0.8" +tari_common_types = { workspace = true } + +log = "0.4.22" +thiserror = "2.0" +borsh = "1.5" +serde = { version = "1.0", features = ["derive"] } + +[dev-dependencies] +serde_json = "1.0.108" +env_logger = "0.11.5" \ No newline at end of file diff --git a/base_layer/sidechain/src/command.rs b/base_layer/sidechain/src/command.rs new file mode 100644 index 0000000000..5761f52385 --- /dev/null +++ b/base_layer/sidechain/src/command.rs @@ -0,0 +1,42 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use tari_common_types::types::FixedHash; +use tari_hashing::layer2::command_hasher; + +use crate::eviction_proof::EvictNodeAtom; + +pub trait ToCommand { + fn to_command(&self) -> Command; +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub enum Command { + LocalOnly, + Prepare, + LocalPrepare, + AllPrepare, + SomePrepare, + LocalAccept, + AllAccept, + SomeAccept, + ForeignProposal, + MintConfidentialOutput, + EvictNode(EvictNodeAtom), + EndEpoch, +} + +impl Command { + pub fn evict_node(&self) -> Option<&EvictNodeAtom> { + match self { + Self::EvictNode(evict_node_atom) => Some(evict_node_atom), + _ => None, + } + } + + pub fn hash(&self) -> FixedHash { + command_hasher().chain(self).finalize().into() + } +} diff --git a/base_layer/sidechain/src/commit_proof.rs b/base_layer/sidechain/src/commit_proof.rs new file mode 100644 index 0000000000..9ea4c90fae --- /dev/null +++ b/base_layer/sidechain/src/commit_proof.rs @@ -0,0 +1,259 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use tari_common_types::{ + epoch::VnEpoch, + types::{FixedHash, PrivateKey, PublicKey}, +}; +use tari_crypto::signatures::SchnorrSignature; +use tari_hashing::{ + layer2::{block_hasher, vote_signature_hasher}, + ValidatorNodeHashDomain, +}; + +use super::error::SidechainProofValidationError; +use crate::{ + command::{Command, ToCommand}, + shard_group::ShardGroup, + validations::{check_command_inclusion_proof, check_proof_elements}, +}; + +pub type ValidatorBlockSignature = SchnorrSignature; +pub type CheckVnFunc<'a> = dyn Fn(&PublicKey) -> Result + 'a; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub enum CommandCommitProof { + V1(CommandCommitProofV1), +} + +impl CommandCommitProof { + pub fn new(command: C, commit_proof: SidechainBlockCommitProof) -> Self { + Self::V1(CommandCommitProofV1 { command, commit_proof }) + } + + pub fn command(&self) -> &C { + match self { + CommandCommitProof::V1(v1) => &v1.command, + } + } + + pub fn header(&self) -> &SidechainBlockHeader { + match self { + CommandCommitProof::V1(v1) => &v1.commit_proof.header, + } + } + + pub fn epoch(&self) -> VnEpoch { + match self { + CommandCommitProof::V1(v1) => VnEpoch(v1.commit_proof.header().epoch), + } + } + + pub fn shard_group(&self) -> ShardGroup { + match self { + CommandCommitProof::V1(v1) => v1.commit_proof.header().shard_group, + } + } + + pub fn validate_committed( + &self, + quorum_threshold: usize, + check_vn: &CheckVnFunc<'_>, + ) -> Result<(), SidechainProofValidationError> { + #[allow(clippy::single_match)] + match self { + CommandCommitProof::V1(v1) => v1.validate_committed(quorum_threshold, check_vn), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct CommandCommitProofV1 { + // TODO: Implement MerkleProof + // command_merkle_proof: MerkleProof, + pub command: C, + pub commit_proof: SidechainBlockCommitProof, +} + +impl CommandCommitProofV1 { + pub fn command(&self) -> &C { + &self.command + } + + pub fn commit_proof(&self) -> &SidechainBlockCommitProof { + &self.commit_proof + } + + pub fn validate_committed( + &self, + quorum_threshold: usize, + check_vn: &CheckVnFunc<'_>, + ) -> Result<(), SidechainProofValidationError> { + let command = self.command.to_command(); + self.commit_proof + .validate_committed(&command, quorum_threshold, check_vn) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct SidechainBlockCommitProof { + pub header: SidechainBlockHeader, + pub proof_elements: Vec, +} + +impl SidechainBlockCommitProof { + pub fn validate_committed( + &self, + command: &Command, + quorum_threshold: usize, + check_vn: &CheckVnFunc<'_>, + ) -> Result<(), SidechainProofValidationError> { + check_command_inclusion_proof(&self.header, command)?; + check_proof_elements( + &self.header, + &self.proof_elements, + check_vn, + QuorumDecision::Accept, + quorum_threshold, + )?; + + Ok(()) + } + + pub fn proof_elements(&self) -> &[CommitProofElement] { + &self.proof_elements + } + + pub fn header(&self) -> &SidechainBlockHeader { + &self.header + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub enum CommitProofElement { + QuorumCertificate(QuorumCertificate), + DummyChain(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct ChainLink { + pub header_hash: FixedHash, + pub parent_id: FixedHash, +} + +impl ChainLink { + pub fn calc_block_id(&self) -> FixedHash { + block_hasher() + .chain(&self.parent_id) + .chain(&self.header_hash) + .finalize() + .into() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct SidechainBlockHeader { + pub network: u8, + pub parent_id: FixedHash, + pub justify_id: FixedHash, + pub height: u64, + pub epoch: u64, + pub shard_group: ShardGroup, + pub proposed_by: PublicKey, + pub total_leader_fee: u64, + pub state_merkle_root: FixedHash, + pub command_merkle_root: FixedHash, + /// If the block is a dummy block. + pub is_dummy: bool, + pub foreign_indexes_hash: FixedHash, + /// Signature of block by the proposer. + pub signature: ValidatorBlockSignature, + pub timestamp: u64, + pub base_layer_block_height: u64, + pub base_layer_block_hash: FixedHash, + pub extra_data_hash: FixedHash, +} + +impl SidechainBlockHeader { + pub fn calculate_hash(&self) -> FixedHash { + block_hasher() + .chain(&self.network) + .chain(&self.justify_id) + .chain(&self.height) + .chain(&self.total_leader_fee) + .chain(&self.epoch) + .chain(&self.shard_group) + .chain(&self.proposed_by) + .chain(&self.state_merkle_root) + .chain(&self.is_dummy) + .chain(&self.command_merkle_root) + .chain(&self.foreign_indexes_hash) + .chain(&self.timestamp) + .chain(&self.base_layer_block_height) + .chain(&self.base_layer_block_hash) + .chain(&self.extra_data_hash) + .finalize() + .into() + } + + pub fn calculate_block_id(&self) -> FixedHash { + let header_hash = self.calculate_hash(); + block_hasher() + .chain(&self.parent_id) + .chain(&header_hash) + .finalize() + .into() + } + + pub fn signature(&self) -> &ValidatorBlockSignature { + &self.signature + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct QuorumCertificate { + pub header_hash: FixedHash, + pub parent_id: FixedHash, + pub signatures: Vec, + pub decision: QuorumDecision, +} + +impl QuorumCertificate { + pub fn calculate_justified_block(&self) -> FixedHash { + block_hasher() + .chain(&self.parent_id) + .chain(&self.header_hash) + .finalize() + .into() + } +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub enum QuorumDecision { + Accept, + Reject, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct ValidatorQcSignature { + pub public_key: PublicKey, + pub signature: ValidatorBlockSignature, +} + +impl ValidatorQcSignature { + #[must_use] + pub fn verify(&self, block_id: &FixedHash, decision: QuorumDecision) -> bool { + let message = vote_signature_hasher().chain(block_id).chain(&decision).finalize(); + self.signature.verify(&self.public_key, message) + } + + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + pub fn signature(&self) -> &ValidatorBlockSignature { + &self.signature + } +} diff --git a/base_layer/sidechain/src/error.rs b/base_layer/sidechain/src/error.rs new file mode 100644 index 0000000000..65777d32f7 --- /dev/null +++ b/base_layer/sidechain/src/error.rs @@ -0,0 +1,24 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +#[derive(Debug, thiserror::Error)] +pub enum SidechainProofValidationError { + #[error("Unexpected command: {details}")] + UnexpectedCommand { details: String }, + #[error("Invalid proof: {details}")] + InvalidProof { details: String }, + #[error("Internal error: {details}")] + InternalError { details: String }, +} + +impl SidechainProofValidationError { + pub fn is_internal_error(&self) -> bool { + matches!(self, Self::InternalError { .. }) + } + + pub fn internal_error(details: T) -> Self { + Self::InternalError { + details: details.to_string(), + } + } +} diff --git a/base_layer/sidechain/src/eviction_proof.rs b/base_layer/sidechain/src/eviction_proof.rs new file mode 100644 index 0000000000..ccf7d0b760 --- /dev/null +++ b/base_layer/sidechain/src/eviction_proof.rs @@ -0,0 +1,75 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use tari_common_types::{epoch::VnEpoch, types::PublicKey}; +use tari_utilities::ByteArray; + +use super::error::SidechainProofValidationError; +use crate::{ + command::{Command, ToCommand}, + commit_proof::CommandCommitProof, + shard_group::ShardGroup, + CheckVnFunc, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct EvictionProof { + proof: CommandCommitProof, +} + +impl EvictionProof { + pub fn new(proof: CommandCommitProof) -> Self { + Self { proof } + } + + pub fn proof(&self) -> &CommandCommitProof { + &self.proof + } + + pub fn epoch(&self) -> VnEpoch { + self.proof.epoch() + } + + pub fn shard_group(&self) -> ShardGroup { + self.proof.shard_group() + } + + pub fn node_to_evict(&self) -> &PublicKey { + self.proof.command().node_to_evict() + } + + pub fn validate( + &self, + quorum_threshold: usize, + check_vn: &CheckVnFunc<'_>, + ) -> Result<(), SidechainProofValidationError> { + self.proof.validate_committed(quorum_threshold, check_vn) + } + + pub fn sidechain_id_message(&self) -> &[u8] { + self.node_to_evict().as_bytes() + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct EvictNodeAtom { + public_key: PublicKey, +} + +impl EvictNodeAtom { + pub fn new(public_key: PublicKey) -> Self { + Self { public_key } + } + + pub fn node_to_evict(&self) -> &PublicKey { + &self.public_key + } +} + +impl ToCommand for EvictNodeAtom { + fn to_command(&self) -> Command { + Command::EvictNode(self.clone()) + } +} diff --git a/base_layer/sidechain/src/lib.rs b/base_layer/sidechain/src/lib.rs new file mode 100644 index 0000000000..cf58654534 --- /dev/null +++ b/base_layer/sidechain/src/lib.rs @@ -0,0 +1,14 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause +mod command; +mod commit_proof; +mod error; +mod eviction_proof; +mod shard_group; +mod validations; + +pub use command::*; +pub use commit_proof::*; +pub use error::*; +pub use eviction_proof::*; +pub use shard_group::*; diff --git a/base_layer/sidechain/src/shard_group.rs b/base_layer/sidechain/src/shard_group.rs new file mode 100644 index 0000000000..895162ecd6 --- /dev/null +++ b/base_layer/sidechain/src/shard_group.rs @@ -0,0 +1,11 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)] +pub struct ShardGroup { + pub start: u32, + pub end_inclusive: u32, +} diff --git a/base_layer/sidechain/src/validations.rs b/base_layer/sidechain/src/validations.rs new file mode 100644 index 0000000000..dcd1a54c61 --- /dev/null +++ b/base_layer/sidechain/src/validations.rs @@ -0,0 +1,220 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::{collections::HashSet, hash::Hash}; + +use log::debug; +use tari_common_types::types::FixedHash; + +use crate::{ + CheckVnFunc, + Command, + CommitProofElement, + QuorumCertificate, + QuorumDecision, + SidechainBlockHeader, + SidechainProofValidationError, +}; + +const LOG_TARGET: &str = "c::sidechain::validations"; +pub fn check_command_inclusion_proof( + _header: &SidechainBlockHeader, + // inclusion_proof: &InclusionProof, + _command: &Command, +) -> Result<(), SidechainProofValidationError> { + // TODO: Implement + Ok(()) +} + +pub fn check_proof_elements( + header: &SidechainBlockHeader, + proof_elements: &[CommitProofElement], + check_vn: &CheckVnFunc<'_>, + expected_decision: QuorumDecision, + quorum_threshold: usize, +) -> Result<(), SidechainProofValidationError> { + check_proof_elements_num_qcs(proof_elements, 3)?; + + let mut last_parent = None::<&FixedHash>; + let mut last_justify = None; + let mut proven_3_chain = 0usize; + for elem in proof_elements { + match elem { + CommitProofElement::QuorumCertificate(qc) => { + let justifies = qc.calculate_justified_block(); + debug!(target: LOG_TARGET, "Validating quorum certificate that justifies: {justifies}"); + validate_qc(qc, quorum_threshold, check_vn, expected_decision)?; + debug!(target: LOG_TARGET, "Quorum certificate OK"); + + if let Some(last_parent) = last_parent { + if *last_parent != justifies { + return Err(SidechainProofValidationError::InvalidProof { + details: format!( + "Parent block ID {last_parent} does not match the parent block ID {justifies} in the \ + quorum certificate" + ), + }); + } + } + + if proven_3_chain < 3 { + proven_3_chain += 1; + debug!(target: LOG_TARGET, "3-chain rule: {} of 3 proven", proven_3_chain); + } + + debug!(target: LOG_TARGET, "Setting last parent to {}", qc.parent_id); + last_parent = Some(&qc.parent_id); + last_justify = Some(justifies); + }, + CommitProofElement::DummyChain(chain) => { + if proven_3_chain != 3 { + return Err(SidechainProofValidationError::InvalidProof { + details: format!( + "A 3-chain must be proven before a dummy chain. A chain of length {proven_3_chain} was \ + proven" + ), + }); + } + + let Some(parent) = last_parent else { + return Err(SidechainProofValidationError::InvalidProof { + details: "Dummy chain must be preceded by a quorum certificate".to_string(), + }); + }; + if chain.is_empty() { + return Err(SidechainProofValidationError::InvalidProof { + details: "Dummy chain must contain at least one element".to_string(), + }); + } + let mut expected = *parent; + for (i, link) in chain.iter().enumerate() { + let block_id = link.calc_block_id(); + debug!(target: LOG_TARGET, "Validating dummy chain link {i}: link {block_id} ?= last parent {expected}"); + if block_id != expected { + return Err(SidechainProofValidationError::InvalidProof { + details: format!( + "Block ID in dummy chain (index :{i}) does not match the parent block ID in the \ + quorum certificate. Expected {expected}, but got {block_id}", + ), + }); + } + expected = block_id; + last_parent = Some(&link.parent_id); + } + }, + } + } + + let Some(last_justify) = last_justify else { + // Not reachable because we check length of proof elements above + return Err(SidechainProofValidationError::InvalidProof { + details: "BUG: Proof must contain at least one quorum certificate".to_string(), + }); + }; + + let header_block_id = header.calculate_block_id(); + if last_justify != header_block_id { + return Err(SidechainProofValidationError::InvalidProof { + details: format!( + "Last parent block ID does not match the block ID in the header. Expected {}, but got {}", + header_block_id, last_justify, + ), + }); + } + + Ok(()) +} +pub fn check_proof_elements_num_qcs( + proof_elems: &[CommitProofElement], + expected_len: usize, +) -> Result<(), SidechainProofValidationError> { + const MAX_PROOF_ELEMS: usize = 20; + if proof_elems.len() > MAX_PROOF_ELEMS { + return Err(SidechainProofValidationError::InvalidProof { + details: format!( + "Commit Proof contained too many proof elements. Expected at most {} but got {}", + MAX_PROOF_ELEMS, + proof_elems.len() + ), + }); + } + let num_qcs = proof_elems + .iter() + .filter(|elem| matches!(elem, CommitProofElement::QuorumCertificate(_))) + .count(); + if num_qcs < expected_len { + return Err(SidechainProofValidationError::InvalidProof { + details: format!( + "Expected at least {} proof elements, but got {}", + expected_len, + proof_elems.len() + ), + }); + } + Ok(()) +} + +fn validate_qc( + qc: &QuorumCertificate, + quorum_threshold: usize, + check_vn: &CheckVnFunc<'_>, + quorum_decision: QuorumDecision, +) -> Result<(), SidechainProofValidationError> { + if qc.signatures.len() < quorum_threshold { + return Err(SidechainProofValidationError::InvalidProof { + details: format!( + "Quorum certificate must contain at least {} signatures but contained {}", + quorum_threshold, + qc.signatures.len() + ), + }); + } + + if qc.decision != quorum_decision { + return Err(SidechainProofValidationError::InvalidProof { + details: format!( + "Quorum certificate decision must be {:?} but was {:?}", + quorum_decision, qc.decision + ), + }); + } + + if has_duplicates(qc.signatures.iter().map(|s| &s.public_key)) { + return Err(SidechainProofValidationError::InvalidProof { + details: "Quorum certificate contains more than one signature from a single validator".to_string(), + }); + } + + let block_id = qc.calculate_justified_block(); + for sig in &qc.signatures { + if !check_vn(&sig.public_key)? { + return Err(SidechainProofValidationError::InvalidProof { + details: format!( + "QC was signed by public key {} that is not in the active validator set", + sig.public_key + ), + }); + } + + if !sig.verify(&block_id, quorum_decision) { + return Err(SidechainProofValidationError::InvalidProof { + details: "Invalid signature".to_string(), + }); + } + } + Ok(()) +} + +fn has_duplicates(iter: I) -> bool +where + I: IntoIterator + ExactSizeIterator, + T: Eq + Hash, +{ + let mut set = HashSet::with_capacity(iter.len()); + for item in iter { + if !set.insert(item) { + return true; + } + } + false +} diff --git a/base_layer/sidechain/tests/eviction_proof.rs b/base_layer/sidechain/tests/eviction_proof.rs new file mode 100644 index 0000000000..aec3c9112f --- /dev/null +++ b/base_layer/sidechain/tests/eviction_proof.rs @@ -0,0 +1,21 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use tari_sidechain::EvictionProof; + +mod support; + +mod validate { + use super::*; + #[test] + fn it_validates_a_valid_proof() { + let proof = support::load_fixture::("eviction_proof1.json"); + proof.validate(5, &|_| Ok(true)).unwrap(); + } + + #[test] + fn it_rejects_if_qc_signs_for_unknown_validator() { + let proof = support::load_fixture::("eviction_proof1.json"); + proof.validate(5, &|_| Ok(false)).unwrap_err(); + } +} diff --git a/base_layer/sidechain/tests/fixtures/eviction_proof1.json b/base_layer/sidechain/tests/fixtures/eviction_proof1.json new file mode 100644 index 0000000000..643c0578b5 --- /dev/null +++ b/base_layer/sidechain/tests/fixtures/eviction_proof1.json @@ -0,0 +1,783 @@ +{ + "proof": { + "V1": { + "command": { + "public_key": "fac00ae089a2a49052fb232926ff88a0be1e3587ff0ed0620b55d83b53f05c31" + }, + "commit_proof": { + "header": { + "network": 16, + "parent_id": [ + 96, + 228, + 82, + 83, + 151, + 80, + 215, + 217, + 198, + 40, + 240, + 183, + 41, + 245, + 164, + 255, + 182, + 163, + 244, + 52, + 59, + 231, + 52, + 178, + 48, + 207, + 81, + 59, + 178, + 29, + 187, + 83 + ], + "justify_id": [ + 91, + 128, + 146, + 205, + 104, + 181, + 223, + 51, + 178, + 122, + 235, + 40, + 48, + 162, + 169, + 169, + 81, + 249, + 148, + 83, + 107, + 154, + 34, + 128, + 59, + 181, + 103, + 224, + 134, + 64, + 231, + 198 + ], + "height": 66, + "epoch": 5, + "shard_group": { + "start": 0, + "end_inclusive": 255 + }, + "proposed_by": "5e13c16840aa8d2e7e68390d0eb1b45c86bc363db0419f7ec5daa534e63bdb35", + "total_leader_fee": 0, + "state_merkle_root": [ + 247, + 115, + 121, + 67, + 152, + 239, + 4, + 213, + 134, + 205, + 188, + 237, + 238, + 135, + 251, + 0, + 201, + 163, + 1, + 210, + 90, + 179, + 146, + 172, + 69, + 197, + 43, + 40, + 1, + 120, + 253, + 141 + ], + "command_merkle_root": [ + 231, + 9, + 182, + 13, + 242, + 171, + 86, + 197, + 164, + 182, + 2, + 28, + 201, + 247, + 92, + 220, + 138, + 31, + 216, + 74, + 157, + 216, + 74, + 61, + 1, + 17, + 255, + 134, + 70, + 63, + 96, + 59 + ], + "is_dummy": false, + "foreign_indexes_hash": [ + 174, + 121, + 133, + 214, + 40, + 14, + 126, + 117, + 54, + 252, + 169, + 249, + 194, + 31, + 199, + 120, + 166, + 181, + 169, + 146, + 51, + 231, + 237, + 70, + 239, + 20, + 42, + 140, + 41, + 185, + 53, + 216 + ], + "signature": { + "public_nonce": "2846178a36d464460afc23c230cd5b5ed537fc214126e0ed4a2c972580068828", + "signature": "16f4be46fb4c5736e6b5be0771bf5e108c254a8cf28b2c407b089f466ec73305" + }, + "timestamp": 1732873628, + "base_layer_block_height": 50, + "base_layer_block_hash": [ + 25, + 244, + 181, + 192, + 179, + 56, + 148, + 2, + 152, + 84, + 182, + 233, + 186, + 146, + 40, + 18, + 183, + 41, + 1, + 114, + 211, + 244, + 142, + 109, + 211, + 160, + 8, + 20, + 68, + 17, + 58, + 112 + ], + "extra_data_hash": [ + 66, + 246, + 214, + 27, + 72, + 255, + 194, + 20, + 119, + 132, + 193, + 254, + 40, + 230, + 29, + 4, + 225, + 212, + 138, + 236, + 119, + 52, + 154, + 201, + 87, + 221, + 191, + 252, + 80, + 62, + 160, + 78 + ] + }, + "proof_elements": [ + { + "QuorumCertificate": { + "header_hash": [ + 59, + 45, + 92, + 232, + 206, + 67, + 243, + 83, + 87, + 88, + 195, + 137, + 97, + 155, + 15, + 46, + 111, + 115, + 178, + 57, + 63, + 179, + 65, + 207, + 243, + 142, + 109, + 183, + 101, + 112, + 63, + 132 + ], + "parent_id": [ + 46, + 59, + 154, + 240, + 242, + 27, + 12, + 218, + 188, + 193, + 149, + 250, + 46, + 3, + 209, + 188, + 221, + 165, + 255, + 167, + 55, + 222, + 181, + 33, + 120, + 233, + 155, + 227, + 132, + 47, + 103, + 204 + ], + "signatures": [ + { + "public_key": "048c457bddf93adc0cc7c8a934437cacb77998c2469bb2028b83d4e1d2ea344e", + "signature": { + "public_nonce": "92f95d184b356ebf53996a97f7414cf6fddbe0f93548e9fd09ca46128b2c455b", + "signature": "c46430a66fea2c5f522784388f276de188f5cc20962cda588c451266155a0003" + } + }, + { + "public_key": "c8939413cbd8df92f65d7f9fd8316a55ca5e333e22a07f8041831a43eb20357b", + "signature": { + "public_nonce": "aed0fb7173ec03b7874dc853dc5727c3ff69ef181d087f29c9c24d565e053122", + "signature": "531e945fd8e5350afb6aa7aff4e4250f292f7a8c6ec43d7f68651ea670c7500a" + } + }, + { + "public_key": "5e13c16840aa8d2e7e68390d0eb1b45c86bc363db0419f7ec5daa534e63bdb35", + "signature": { + "public_nonce": "440ead93562e4ed40026e6b396ff75929c6efce857c7809ba0f4d03b5e65b310", + "signature": "8f2ad409e14bbcc9f2e0f8a02bc5553fe6a90b0b1edc55a602ec83a9caaa9a04" + } + }, + { + "public_key": "d6915bb1c499c289cdc78a0170e9649c1bc3e3534fa864cd3a6953f05a067e28", + "signature": { + "public_nonce": "962b8d4d5122d601a93c39eb362c7291f309bb0f4c0328d128f10e3a5b432760", + "signature": "134ce6e41d80c08a3a0c46620347840cd8d3aaf8c155f12a1a381364ae1db102" + } + }, + { + "public_key": "f02cc332c74ac694bcfd1740896bae20d7d7d2676f1a5ae8ad9bce21f8156444", + "signature": { + "public_nonce": "68ac5ef39b800905457203cdcc4a7ee49d1d063e581d9cb2faa933a8492d7b01", + "signature": "73bcc58c6d805a89e2f549f9d49e1a252535811e0fe14cc85e5628d50bb3bc0d" + } + } + ], + "decision": "Accept" + } + }, + { + "QuorumCertificate": { + "header_hash": [ + 168, + 203, + 174, + 132, + 184, + 148, + 156, + 199, + 215, + 122, + 166, + 147, + 172, + 31, + 85, + 4, + 120, + 79, + 137, + 21, + 180, + 102, + 172, + 164, + 108, + 219, + 176, + 30, + 71, + 37, + 151, + 3 + ], + "parent_id": [ + 220, + 227, + 27, + 34, + 2, + 234, + 157, + 93, + 126, + 214, + 246, + 192, + 45, + 94, + 92, + 125, + 133, + 86, + 242, + 18, + 229, + 29, + 252, + 45, + 253, + 105, + 189, + 20, + 140, + 75, + 129, + 190 + ], + "signatures": [ + { + "public_key": "c8939413cbd8df92f65d7f9fd8316a55ca5e333e22a07f8041831a43eb20357b", + "signature": { + "public_nonce": "dcb68d7077e63c81351e1bb218569b2aae4cf1c83e0819655b8a7a638d874275", + "signature": "e4dd8f1801a3edb253db8cca1ae9a078ea1608cf6736affca0f78e8e2af9f50f" + } + }, + { + "public_key": "f02cc332c74ac694bcfd1740896bae20d7d7d2676f1a5ae8ad9bce21f8156444", + "signature": { + "public_nonce": "7a0872d98cc357cea456f73409a41cf673091562fb4b242f3002ccb9f8848809", + "signature": "ecce8a90d832570d79368616e21573a770317c248dc4845277358a8ed6405f09" + } + }, + { + "public_key": "d6915bb1c499c289cdc78a0170e9649c1bc3e3534fa864cd3a6953f05a067e28", + "signature": { + "public_nonce": "7c2935a67fef7e74882d65371813b3c29fd2946cdc91ddafa63c3eef178cb068", + "signature": "2de2415ab1a525433fc85d52f9a6e72ed9e25c45daa80788e18b346fe6cb7809" + } + }, + { + "public_key": "048c457bddf93adc0cc7c8a934437cacb77998c2469bb2028b83d4e1d2ea344e", + "signature": { + "public_nonce": "d0cc4d537a8e959723c09eb007e0bf20dae7200a0a23651bf016201af2379041", + "signature": "a82b7718c4c69e107c6108f417bf1c63ecdf9e0b695ec0057430f5ab23559c0f" + } + }, + { + "public_key": "5e13c16840aa8d2e7e68390d0eb1b45c86bc363db0419f7ec5daa534e63bdb35", + "signature": { + "public_nonce": "8a4a05b935782b030e3ff62d3dff65012975efb2905cdf32dcd580736b6ca90a", + "signature": "e6706afca2d32744649a48e42a9ed3a546f041658787daf3fdd803d818d45901" + } + } + ], + "decision": "Accept" + } + }, + { + "QuorumCertificate": { + "header_hash": [ + 213, + 35, + 77, + 106, + 93, + 104, + 244, + 67, + 16, + 249, + 225, + 56, + 43, + 188, + 234, + 113, + 11, + 24, + 184, + 246, + 180, + 165, + 65, + 11, + 168, + 180, + 205, + 255, + 183, + 106, + 140, + 130 + ], + "parent_id": [ + 180, + 177, + 9, + 52, + 80, + 37, + 145, + 162, + 90, + 228, + 242, + 92, + 234, + 180, + 144, + 11, + 112, + 34, + 7, + 204, + 205, + 36, + 254, + 17, + 76, + 85, + 136, + 45, + 8, + 45, + 67, + 0 + ], + "signatures": [ + { + "public_key": "f02cc332c74ac694bcfd1740896bae20d7d7d2676f1a5ae8ad9bce21f8156444", + "signature": { + "public_nonce": "883594bafc96448a2d0310a1fd9d589b8c1c993519d6a1c7d0c6462e8344b72e", + "signature": "f3127bada1c68b5753f1526730f36af22f1583becf02a58d8449ac38d98ca10e" + } + }, + { + "public_key": "d6915bb1c499c289cdc78a0170e9649c1bc3e3534fa864cd3a6953f05a067e28", + "signature": { + "public_nonce": "32689ac8b65657ece2c8d1db283cc02973c43bbf8ec9dc7f2f7802b0f14b8563", + "signature": "7c0b32c65caa4d179f952a966045f421c57c296d431b97bfb9a1361998e7d800" + } + }, + { + "public_key": "c8939413cbd8df92f65d7f9fd8316a55ca5e333e22a07f8041831a43eb20357b", + "signature": { + "public_nonce": "f2cd062dc4e609fb2cd5617373bdb20c39141032ac8db8bd31f7083914090f44", + "signature": "1bedb418c2e240c6fcf19abcd8ad49939179c4e171b90c663c1b6f8589290d06" + } + }, + { + "public_key": "5e13c16840aa8d2e7e68390d0eb1b45c86bc363db0419f7ec5daa534e63bdb35", + "signature": { + "public_nonce": "221129b8c982f962c3833a36c4c22d702b1b00e6dbe0c69b49c518f8a410f51e", + "signature": "ad00607845984a7d3274da793f5734fd7688a0d956f0924c17030a209a15bb07" + } + }, + { + "public_key": "048c457bddf93adc0cc7c8a934437cacb77998c2469bb2028b83d4e1d2ea344e", + "signature": { + "public_nonce": "2ab12eb04fb61cadfd044180e69bdcba813d7cf035ba0f1f95b3bf5abe0fd772", + "signature": "7c583c1d3e3e833f28bcf058d48fc12612c3081ae0f682f2662e03f971784809" + } + } + ], + "decision": "Accept" + } + }, + { + "DummyChain": [ + { + "header_hash": [ + 99, + 35, + 174, + 85, + 19, + 157, + 52, + 19, + 231, + 204, + 206, + 5, + 162, + 231, + 3, + 155, + 13, + 254, + 182, + 69, + 62, + 19, + 249, + 84, + 145, + 62, + 43, + 2, + 76, + 143, + 188, + 205 + ], + "parent_id": [ + 28, + 219, + 229, + 193, + 168, + 148, + 188, + 194, + 84, + 180, + 124, + 240, + 23, + 212, + 209, + 118, + 8, + 131, + 155, + 112, + 72, + 209, + 192, + 33, + 98, + 188, + 205, + 57, + 231, + 99, + 82, + 136 + ] + } + ] + }, + { + "QuorumCertificate": { + "header_hash": [ + 89, + 9, + 169, + 18, + 180, + 180, + 85, + 135, + 190, + 17, + 222, + 5, + 3, + 221, + 248, + 191, + 93, + 17, + 21, + 8, + 227, + 226, + 122, + 81, + 77, + 32, + 200, + 198, + 119, + 68, + 205, + 115 + ], + "parent_id": [ + 96, + 228, + 82, + 83, + 151, + 80, + 215, + 217, + 198, + 40, + 240, + 183, + 41, + 245, + 164, + 255, + 182, + 163, + 244, + 52, + 59, + 231, + 52, + 178, + 48, + 207, + 81, + 59, + 178, + 29, + 187, + 83 + ], + "signatures": [ + { + "public_key": "5e13c16840aa8d2e7e68390d0eb1b45c86bc363db0419f7ec5daa534e63bdb35", + "signature": { + "public_nonce": "208b4efeadf518e5e921989a02209e14243f5f637d18718635ea947e38b0a238", + "signature": "462ce418e386eab7898a37960c9adf6289d11715ab9aef9697bef6be4b05360a" + } + }, + { + "public_key": "d6915bb1c499c289cdc78a0170e9649c1bc3e3534fa864cd3a6953f05a067e28", + "signature": { + "public_nonce": "168ff11d1f2ead1cae80ad90ca9f105db9c0c55eb4dfa155027eed4b30b4d935", + "signature": "352bfbbc3619a58a9a2f724e562846982685611acdf101815b45cce89e86360b" + } + }, + { + "public_key": "f02cc332c74ac694bcfd1740896bae20d7d7d2676f1a5ae8ad9bce21f8156444", + "signature": { + "public_nonce": "64f6e95799ae20e0de85524a8c3f1008dfc8a82fba62adea39719a1f9e459817", + "signature": "a70d4acc353ccdd0f85db582ca93cbc16320afe743eb275b06a5e413ae7ded0b" + } + }, + { + "public_key": "048c457bddf93adc0cc7c8a934437cacb77998c2469bb2028b83d4e1d2ea344e", + "signature": { + "public_nonce": "dac310cc7f0bc35dd977a4a52f7005944319cbb69711464755a3f1a6bc309b42", + "signature": "ba1445202413130d36f430fef71bd097ed14c83289fd014ed08c93617700a604" + } + }, + { + "public_key": "c8939413cbd8df92f65d7f9fd8316a55ca5e333e22a07f8041831a43eb20357b", + "signature": { + "public_nonce": "f2934e4f807c6f78f33375c34d2b8eb5c711ab3cbb277c3b95892a80855e0234", + "signature": "31a3ba1fe68df47dc4a1fd8ebcd6d1d49e62656579ba57df4f0a0e17a0f32004" + } + } + ], + "decision": "Accept" + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/base_layer/sidechain/tests/support/mod.rs b/base_layer/sidechain/tests/support/mod.rs new file mode 100644 index 0000000000..d3bb297e6e --- /dev/null +++ b/base_layer/sidechain/tests/support/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use serde::de::DeserializeOwned; + +pub fn load_fixture(name: &str) -> T { + let path = format!("tests/fixtures/{name}"); + let file = std::fs::File::open(path).unwrap(); + serde_json::from_reader(file).unwrap() +} diff --git a/base_layer/wallet/Cargo.toml b/base_layer/wallet/Cargo.toml index c4616ea4a6..82516f35fd 100644 --- a/base_layer/wallet/Cargo.toml +++ b/base_layer/wallet/Cargo.toml @@ -29,6 +29,7 @@ tari_p2p = { path = "../p2p", features = [ tari_script = { path = "../../infrastructure/tari_script", version = "1.7.0-pre.3" } tari_service_framework = { path = "../service_framework", version = "1.7.0-pre.3" } tari_shutdown = { path = "../../infrastructure/shutdown", version = "1.7.0-pre.3" } +tari_sidechain = { path = "../../base_layer/sidechain", version = "1.7.0-pre.3" } tari_utilities = { version = "0.8" } # Uncomment for tokio tracing via tokio-console (needs "tracing" features) diff --git a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs index 5d111f5cda..8ab5315cb7 100644 --- a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs +++ b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs @@ -194,6 +194,8 @@ where OutputType::Burn => OutputSource::Burn, OutputType::ValidatorNodeRegistration => OutputSource::ValidatorNodeRegistration, OutputType::CodeTemplateRegistration => OutputSource::CodeTemplateRegistration, + OutputType::SidechainCheckpoint => OutputSource::SidechainCheckpoint, + OutputType::SidechainProof => OutputSource::SidechainProof, } } diff --git a/base_layer/wallet/src/output_manager_service/storage/output_source.rs b/base_layer/wallet/src/output_manager_service/storage/output_source.rs index 19d178f0df..a738be063c 100644 --- a/base_layer/wallet/src/output_manager_service/storage/output_source.rs +++ b/base_layer/wallet/src/output_manager_service/storage/output_source.rs @@ -39,6 +39,8 @@ pub enum OutputSource { Burn, ValidatorNodeRegistration, CodeTemplateRegistration, + SidechainCheckpoint, + SidechainProof, } impl TryFrom for OutputSource { @@ -56,6 +58,8 @@ impl TryFrom for OutputSource { 7 => OutputSource::Burn, 8 => OutputSource::ValidatorNodeRegistration, 9 => OutputSource::CodeTemplateRegistration, + 10 => OutputSource::SidechainCheckpoint, + 11 => OutputSource::SidechainProof, _ => { return Err(OutputManagerStorageError::ConversionError { reason: "Was expecting value between 0 and 7 for OutputSource".to_string(), diff --git a/base_layer/wallet/src/transaction_service/error.rs b/base_layer/wallet/src/transaction_service/error.rs index f883862da1..becbef7a77 100644 --- a/base_layer/wallet/src/transaction_service/error.rs +++ b/base_layer/wallet/src/transaction_service/error.rs @@ -204,6 +204,8 @@ pub enum TransactionServiceError { NotSupported(String), #[error("Tari script error: {0}")] ScriptError(#[from] ScriptError), + #[error("Invalid validator node signature")] + InvalidValidatorNodeSignature, } impl From for TransactionServiceError { diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 0044c85d38..f753cb3ee6 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -55,6 +55,7 @@ use tari_crypto::ristretto::pedersen::PedersenCommitment; use tari_max_size::MaxSizeString; use tari_script::CheckSigSchnorrSignature; use tari_service_framework::reply_channel::SenderService; +use tari_sidechain::EvictionProof; use tari_utilities::hex::Hex; use tokio::sync::broadcast; use tower::Service; @@ -150,6 +151,13 @@ pub enum TransactionServiceRequest { fee_per_gram: MicroMinotari, sidechain_deployment_key: Option, }, + SubmitValidatorEvictionProof { + amount: MicroMinotari, + proof: EvictionProof, + fee_per_gram: MicroMinotari, + message: String, + sidechain_deployment_key: Option, + }, GetCodeTemplateFee { template_name: MaxSizeString<32>, template_version: u16, @@ -389,12 +397,28 @@ impl fmt::Display for TransactionServiceRequest { Self::GetFeePerGramStatsPerBlock { count } => { write!(f, "GetFeePerGramEstimatesPerBlock(count: {})", count,) }, - TransactionServiceRequest::RegisterCodeTemplate { template_name, .. } => { + Self::RegisterCodeTemplate { template_name, .. } => { write!(f, "RegisterCodeTemplate: {}", template_name) }, TransactionServiceRequest::GetCodeTemplateFee { template_name, .. } => { write!(f, "GetCodeTemplateFee: {}", template_name) }, + Self::SubmitValidatorEvictionProof { + amount, + proof, + fee_per_gram, + message, + .. + } => { + write!( + f, + "SubmitValidatorEvictionProof (amount: {}, evicts: {}, fee_per_gram: {}, message: {})", + amount, + proof.node_to_evict(), + fee_per_gram, + message, + ) + }, } } } @@ -447,6 +471,9 @@ pub enum TransactionServiceResponse { CodeTemplateRegistrationFeeResponse { fee: MicroMinotari, }, + ValidatorEvictionProofSent { + tx_id: TxId, + }, } #[derive(Clone, Debug, Hash, PartialEq, Eq, Default)] @@ -760,6 +787,30 @@ impl TransactionServiceHandle { } } + pub async fn submit_validator_eviction_proof( + &mut self, + amount: MicroMinotari, + proof: EvictionProof, + fee_per_gram: MicroMinotari, + sidechain_deployment_key: Option, + message: String, + ) -> Result { + match self + .handle + .call(TransactionServiceRequest::SubmitValidatorEvictionProof { + amount, + proof, + fee_per_gram, + message, + sidechain_deployment_key, + }) + .await?? + { + TransactionServiceResponse::TransactionSent(tx_id) => Ok(tx_id), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + } + } + pub async fn send_one_sided_transaction( &mut self, destination: TariAddress, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 0b2d5c8996..9c9c98d8bc 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -36,6 +36,7 @@ use sha2::Sha256; use tari_common::configuration::Network; use tari_common_types::{ burnt_proof::BurntProof, + epoch::VnEpoch, key_branches::TransactionKeyManagerBranch, tari_address::{TariAddress, TariAddressFeatures}, transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, @@ -62,6 +63,7 @@ use tari_core::{ TemplateType, Transaction, TransactionOutput, + ValidatorNodeSignature, WalletOutputBuilder, }, transaction_protocol::{ @@ -77,7 +79,6 @@ use tari_core::{ use tari_crypto::{ keys::{PublicKey as PKtrait, SecretKey}, ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey}, - signatures::SchnorrSignature, tari_utilities::ByteArray, }; use tari_key_manager::key_manager_service::KeyId; @@ -86,6 +87,7 @@ use tari_p2p::domain_message::DomainMessage; use tari_script::{push_pubkey_script, script, CheckSigSchnorrSignature, ExecutionStack, ScriptContext, TariScript}; use tari_service_framework::{reply_channel, reply_channel::Receiver}; use tari_shutdown::ShutdownSignal; +use tari_sidechain::EvictionProof; use tokio::{ sync::{mpsc, mpsc::Sender, oneshot, Mutex}, task::JoinHandle, @@ -836,6 +838,28 @@ where template_address, }) }, + TransactionServiceRequest::SubmitValidatorEvictionProof { + amount, + proof, + fee_per_gram, + message, + sidechain_deployment_key, + } => { + let rp = reply_channel.take().expect("Cannot be missing"); + self.submit_validator_eviction_proof( + amount, + proof, + sidechain_deployment_key, + UtxoSelectionCriteria::default(), + fee_per_gram, + message, + send_transaction_join_handles, + transaction_broadcast_join_handles, + rp, + ) + .await?; + return Ok(()); + }, TransactionServiceRequest::SendShaAtomicSwapTransaction( destination, amount, @@ -2192,20 +2216,11 @@ where "A sidechain deployment key was provided without a claim public key".to_string(), )); } - let (sidechain_id, sidechain_id_knowledge_proof) = match sidechain_deployment_key { - Some(key) => { - let sidechain_id = PublicKey::from_secret_key(&key); - let sidechain_id_knowledge_proof = - SchnorrSignature::sign(&key, claim_public_key.as_ref().unwrap().as_bytes(), &mut OsRng) - .map_err(|e| TransactionServiceError::InvalidBurnTransaction(format!("Error: {:?}", e)))?; - (Some(sidechain_id), Some(sidechain_id_knowledge_proof)) - }, - None => (None, None), - }; + let output_features = claim_public_key .as_ref() .cloned() - .map(|c| OutputFeatures::create_burn_confidential_output(c, sidechain_id, sidechain_id_knowledge_proof)) + .map(|c| OutputFeatures::create_burn_confidential_output(c, sidechain_deployment_key.as_ref())) .unwrap_or_else(OutputFeatures::create_burn_output); // Prepare sender part of the transaction @@ -2416,7 +2431,7 @@ where })) } - pub async fn register_validator_node( + async fn register_validator_node( &mut self, amount: MicroMinotari, validator_node_public_key: CommsPublicKey, @@ -2434,22 +2449,16 @@ where >, reply_channel: oneshot::Sender>, ) -> Result<(), TransactionServiceError> { - let (sidechain_id, sidechain_id_knowledge_proof) = match sidechain_deployment_key { - Some(k) => { - let sidechain_id = PublicKey::from_secret_key(&k); - let sidechain_id_knowledge_proof = - SchnorrSignature::sign(&k, validator_node_public_key.to_vec(), &mut OsRng) - .map_err(|e| TransactionServiceError::SidechainSigningError(e.to_string()))?; - (Some(sidechain_id), Some(sidechain_id_knowledge_proof)) - }, - None => (None, None), - }; + let signature = ValidatorNodeSignature::new(validator_node_public_key, validator_node_signature); + let sidechain_pk = sidechain_deployment_key.as_ref().map(PublicKey::from_secret_key); + if !signature.is_valid_signature_for(sidechain_pk.as_ref(), &validator_node_claim_public_key, VnEpoch(0)) { + return Err(TransactionServiceError::InvalidValidatorNodeSignature); + } + let output_features = OutputFeatures::for_validator_node_registration( - validator_node_public_key, - validator_node_signature, + signature, validator_node_claim_public_key, - sidechain_id, - sidechain_id_knowledge_proof, + sidechain_deployment_key.as_ref(), ); self.send_transaction( self.resources.interactive_tari_address.clone(), @@ -2479,10 +2488,15 @@ where sidechain_deployment_key: Option, selection_criteria: UtxoSelectionCriteria, ) -> Result { + let author_key_id = self + .resources + .transaction_key_manager_service + .get_static_key(TransactionKeyManagerBranch::CodeTemplateAuthor.get_branch_key()) + .await?; let author_key = self .resources .transaction_key_manager_service - .get_next_key(TransactionKeyManagerBranch::CodeTemplateAuthor.get_branch_key()) + .get_public_key_at_key_id(&author_key_id) .await?; let (nonce_secret, nonce_pub) = RistrettoPublicKey::random_keypair(&mut OsRng); let nonce_id = self @@ -2490,18 +2504,8 @@ where .transaction_key_manager_service .import_key(nonce_secret) .await?; - let (sidechain_id, sidechain_id_knowledge_proof) = match sidechain_deployment_key { - Some(k) => ( - Some(PublicKey::from_secret_key(&k)), - Some( - SchnorrSignature::sign(&k, author_key.pub_key.as_bytes(), &mut OsRng) - .map_err(|e| TransactionServiceError::SidechainSigningError(e.to_string()))?, - ), - ), - None => (None, None), - }; let mut template_registration = CodeTemplateRegistration { - author_public_key: author_key.pub_key.clone(), + author_public_key: author_key.clone(), author_signature: Signature::default(), template_name: template_name .try_into() @@ -2513,19 +2517,20 @@ where build_info, binary_sha, binary_url, - sidechain_id, - sidechain_id_knowledge_proof, }; - let challenge = template_registration.create_challenge(&nonce_pub); + let signature_message = template_registration.create_signature_message(&nonce_pub); + let author_sig = self .resources .transaction_key_manager_service - .sign_with_nonce_and_challenge(&author_key.key_id, &nonce_id, &challenge) + .sign_with_nonce_and_challenge(&author_key_id, &nonce_id, &signature_message) .await .map_err(|e| TransactionServiceError::SidechainSigningError(e.to_string()))?; template_registration.author_signature = author_sig; - let output_features = OutputFeatures::for_template_registration(template_registration); + + let output_features = + OutputFeatures::for_template_registration(template_registration, sidechain_deployment_key.as_ref()); let fee = self .resources @@ -2536,7 +2541,41 @@ where Ok(fee) } - pub async fn register_code_template( + async fn submit_validator_eviction_proof( + &mut self, + amount: MicroMinotari, + eviction_proof: EvictionProof, + sidechain_deployment_key: Option, + selection_criteria: UtxoSelectionCriteria, + fee_per_gram: MicroMinotari, + message: String, + join_handles: &mut FuturesUnordered< + JoinHandle>>, + >, + transaction_broadcast_join_handles: &mut FuturesUnordered< + JoinHandle>>, + >, + reply_channel: oneshot::Sender>, + ) -> Result<(), TransactionServiceError> { + let output_features = + OutputFeatures::for_validator_node_eviction(eviction_proof, sidechain_deployment_key.as_ref()); + self.send_transaction( + self.resources.interactive_tari_address.clone(), + amount, + selection_criteria, + output_features, + fee_per_gram, + message, + TransactionMetadata::default(), + join_handles, + transaction_broadcast_join_handles, + reply_channel, + ) + .await?; + Ok(()) + } + + async fn register_code_template( &mut self, fee_per_gram: MicroMinotari, template_name: String, @@ -2553,10 +2592,15 @@ where JoinHandle>>, >, ) -> Result<(TxId, FixedHash), TransactionServiceError> { + let author_key_id = self + .resources + .transaction_key_manager_service + .get_static_key(TransactionKeyManagerBranch::CodeTemplateAuthor.get_branch_key()) + .await?; let author_key = self .resources .transaction_key_manager_service - .get_next_key(TransactionKeyManagerBranch::CodeTemplateAuthor.get_branch_key()) + .get_public_key_at_key_id(&author_key_id) .await?; let (nonce_secret, nonce_pub) = RistrettoPublicKey::random_keypair(&mut OsRng); let nonce_id = self @@ -2564,19 +2608,8 @@ where .transaction_key_manager_service .import_key(nonce_secret) .await?; - let (sidechain_id, sidechain_id_knowledge_proof) = match sidechain_deployment_key { - Some(k) => ( - Some(PublicKey::from_secret_key(&k)), - Some( - SchnorrSignature::sign(&k, author_key.pub_key.as_bytes(), &mut OsRng) - .map_err(|e| TransactionServiceError::SidechainSigningError(e.to_string()))?, - ), - ), - None => (None, None), - }; - let mut template_registration = CodeTemplateRegistration { - author_public_key: author_key.pub_key.clone(), + author_public_key: author_key.clone(), author_signature: Signature::default(), template_name: template_name .try_into() @@ -2588,21 +2621,21 @@ where build_info, binary_sha, binary_url, - sidechain_id, - sidechain_id_knowledge_proof, }; - let challenge = template_registration.create_challenge(&nonce_pub); + let signature_message = template_registration.create_signature_message(&nonce_pub); + let author_sig = self .resources .transaction_key_manager_service - .sign_with_nonce_and_challenge(&author_key.key_id, &nonce_id, &challenge) + .sign_with_nonce_and_challenge(&author_key_id, &nonce_id, &signature_message) .await .map_err(|e| TransactionServiceError::SidechainSigningError(e.to_string()))?; template_registration.author_signature = author_sig; - let output_features = OutputFeatures::for_template_registration(template_registration); + let output_features = + OutputFeatures::for_template_registration(template_registration, sidechain_deployment_key.as_ref()); let tx_id = TxId::new_random(); let (fee, transaction) = self .resources diff --git a/clippy.toml b/clippy.toml index 8fcbb43341..f717bf989c 100644 --- a/clippy.toml +++ b/clippy.toml @@ -2,3 +2,4 @@ cognitive-complexity-threshold = 15 too-many-arguments-threshold = 12 # Set from 200 to size of a RistrettoPublicKey enum-variant-size-threshold = 216 +ignore-interior-mutability = ["bytes::Bytes", "tari_crypto::ristretto::RistrettoPublicKey"] \ No newline at end of file diff --git a/hashing/Cargo.toml b/hashing/Cargo.toml index a55fbc3984..a6ddd48fc4 100644 --- a/hashing/Cargo.toml +++ b/hashing/Cargo.toml @@ -12,11 +12,9 @@ license = "BSD-3-Clause" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tari_crypto = { version = "0.21.0", default-features = false, features = [ - "borsh", -] } +tari_crypto = { version = "0.21.0", default-features = false, features = ["borsh"] } + borsh = { version = "1.5", default-features = false } digest = { version = "0.10", default-features = false } +blake2 = { version = "0.10", default-features = false } -[dev-dependencies] -blake2 = "0.10" diff --git a/hashing/src/borsh_hasher.rs b/hashing/src/borsh_hasher.rs index b494149e36..2e7dace311 100644 --- a/hashing/src/borsh_hasher.rs +++ b/hashing/src/borsh_hasher.rs @@ -53,6 +53,11 @@ impl DomainSeparatedBorshHasher self.writer.0.finalize() } + pub fn finalize_into_array(self) -> [u8; I] + where [u8; I]: From> { + self.writer.0.finalize().into() + } + /// Update the hasher using the Borsh encoding of the input, which is assumed to be canonical. pub fn update_consensus_encode(&mut self, data: &T) { BorshSerialize::serialize(data, &mut self.writer) diff --git a/hashing/src/domains.rs b/hashing/src/domains.rs index f91795c27a..933c066e04 100644 --- a/hashing/src/domains.rs +++ b/hashing/src/domains.rs @@ -14,11 +14,13 @@ hash_domain!( "com.tari.base_layer.core.transactions.secure_nonce_kdf", 0 ); + hash_domain!( ValidatorNodeMerkleHashDomain, "com.tari.base_layer.core.validator_node_mmr", 1 ); + hash_domain!( WalletOutputEncryptionKeysDomain, "com.tari.base_layer.wallet.output_encryption_keys", @@ -36,3 +38,9 @@ hash_domain!( "com.tari.base_layer.core.transactions.key_manager", 1 ); + +hash_domain!( + ValidatorNodeHashDomain, + "com.tari.base_layer.core.transactions.side_chain.validator_node", + 0 +); diff --git a/hashing/src/layer2.rs b/hashing/src/layer2.rs new file mode 100644 index 0000000000..72d1b537d2 --- /dev/null +++ b/hashing/src/layer2.rs @@ -0,0 +1,54 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use blake2::{digest::consts::U32, Blake2b}; +use digest::consts::U64; +use tari_crypto::{ + hash_domain, + hashing::{DomainSeparatedHasher, DomainSeparation}, +}; + +use crate::{domains::ValidatorNodeMerkleHashDomain, DomainSeparatedBorshHasher, ValidatorNodeHashDomain}; + +hash_domain!(TariDanConsensusHashDomain, "com.tari.consensus", 0); + +pub type TariDomainHasher = DomainSeparatedBorshHasher>; +pub type TariConsensusHasher = TariDomainHasher; + +pub fn tari_hasher64(label: &'static str) -> TariDomainHasher { + TariDomainHasher::::new_with_label(label) +} + +fn tari_consensus_hasher(label: &'static str) -> TariConsensusHasher { + TariConsensusHasher::new_with_label(label) +} + +pub fn validator_registration_hasher() -> TariDomainHasher { + tari_hasher64("registration") +} + +pub fn block_hasher() -> TariConsensusHasher { + tari_consensus_hasher("Block") +} + +pub fn command_hasher() -> TariConsensusHasher { + tari_consensus_hasher("Command") +} + +pub fn quorum_certificate_hasher() -> TariConsensusHasher { + tari_consensus_hasher("QuorumCertificate") +} + +pub fn vote_signature_hasher() -> TariConsensusHasher { + tari_consensus_hasher("VoteSignature") +} + +pub fn extra_data_hasher() -> TariConsensusHasher { + tari_consensus_hasher("ExtraData") +} + +pub fn foreign_indexes_hasher() -> TariConsensusHasher { + tari_consensus_hasher("ForeignIndexes") +} + +pub type ValidatorNodeBmtHasherBlake2b = DomainSeparatedHasher, ValidatorNodeMerkleHashDomain>; diff --git a/hashing/src/lib.rs b/hashing/src/lib.rs index ce560d90b4..07de0f23df 100644 --- a/hashing/src/lib.rs +++ b/hashing/src/lib.rs @@ -27,4 +27,7 @@ mod domains; pub use domains::*; mod borsh_hasher; + pub use borsh_hasher::*; + +pub mod layer2;