From 9058467be1246e4693cb0098a62b18d01b498700 Mon Sep 17 00:00:00 2001 From: Peilun Li Date: Wed, 3 Jan 2024 18:24:52 +0800 Subject: [PATCH] Initial commit --- .github/actions-rs/grcov.yml | 3 + .github/workflows/cc.yml | 53 + .github/workflows/rust.yml | 100 + .github/workflows/tests.yml | 50 + .gitignore | 7 + .gitmodules | 6 + Cargo.lock | 8082 +++++++++++++++++ Cargo.toml | 32 + LICENSE.lighthouse | 201 + NOTICE | 6 + codecov.yml | 8 + common/append_merkle/Cargo.toml | 14 + common/append_merkle/src/lib.rs | 641 ++ common/append_merkle/src/merkle_tree.rs | 131 + common/append_merkle/src/proof.rs | 167 + common/append_merkle/src/sha3.rs | 23 + common/channel/Cargo.toml | 7 + common/channel/src/channel.rs | 112 + common/channel/src/error.rs | 18 + common/channel/src/lib.rs | 5 + common/channel/src/test_util.rs | 55 + common/contract-interface/Cargo.toml | 14 + common/contract-interface/build.rs | 30 + common/contract-interface/src/lib.rs | 13 + common/directory/Cargo.toml | 9 + common/directory/src/lib.rs | 12 + common/hashset_delay/Cargo.toml | 12 + common/hashset_delay/src/hashset_delay.rs | 197 + common/hashset_delay/src/lib.rs | 12 + common/lighthouse_metrics/Cargo.toml | 11 + common/lighthouse_metrics/src/lib.rs | 359 + common/merkle_light/Cargo.toml | 36 + common/merkle_light/README.md | 113 + common/merkle_light/benches/crypto_sha512.rs | 170 + .../merkle_light/benches/hash512/hash512.rs | 58 + common/merkle_light/benches/hash512/mod.rs | 3 + common/merkle_light/benches/ring_sha512.rs | 168 + common/merkle_light/benches/ringx/init.rs | 26 + common/merkle_light/benches/ringx/mod.rs | 567 ++ common/merkle_light/clippy.toml | 3 + common/merkle_light/examples/no_std.rs | 4 + common/merkle_light/src/hash.rs | 135 + common/merkle_light/src/hash_impl.rs | 188 + common/merkle_light/src/lib.rs | 178 + common/merkle_light/src/merkle.rs | 305 + common/merkle_light/src/proof.rs | 80 + common/merkle_light/src/test_cmh.rs | 76 + common/merkle_light/src/test_item.rs | 39 + common/merkle_tree/Cargo.toml | 13 + common/merkle_tree/src/lib.rs | 88 + common/spec/Cargo.toml | 6 + common/spec/src/lib.rs | 18 + common/task_executor/Cargo.toml | 13 + common/task_executor/src/lib.rs | 369 + common/task_executor/src/metrics.rs | 36 + common/task_executor/src/test_utils.rs | 51 + common/unused_port/Cargo.toml | 8 + common/unused_port/src/lib.rs | 55 + common/zgs_seal/Cargo.toml | 9 + common/zgs_seal/src/lib.rs | 62 + common/zgs_version/Cargo.toml | 14 + common/zgs_version/src/lib.rs | 47 + doc/install.md | 70 + doc/run.md | 91 + node/.gitignore | 1 + node/Cargo.toml | 42 + node/build.rs | 13 + node/chunk_pool/Cargo.toml | 15 + node/chunk_pool/src/handler.rs | 93 + node/chunk_pool/src/lib.rs | 38 + node/chunk_pool/src/mem_pool/chunk_cache.rs | 158 + .../src/mem_pool/chunk_pool_inner.rs | 332 + .../src/mem_pool/chunk_write_control.rs | 200 + node/chunk_pool/src/mem_pool/mod.rs | 15 + node/file_location_cache/Cargo.toml | 13 + .../src/file_location_cache.rs | 532 ++ node/file_location_cache/src/lib.rs | 20 + node/file_location_cache/src/test_util.rs | 47 + node/log_entry_sync/Cargo.toml | 24 + node/log_entry_sync/src/lib.rs | 10 + node/log_entry_sync/src/rpc_proxy/eth.rs | 30 + node/log_entry_sync/src/rpc_proxy/mod.rs | 38 + .../log_entry_sync/src/sync_manager/config.rs | 65 + .../src/sync_manager/data_cache.rs | 58 + .../src/sync_manager/log_entry_fetcher.rs | 356 + .../src/sync_manager/log_query.rs | 183 + node/log_entry_sync/src/sync_manager/mod.rs | 319 + node/miner/Cargo.toml | 22 + node/miner/src/config.rs | 53 + node/miner/src/lib.rs | 19 + node/miner/src/loader.rs | 20 + node/miner/src/mine.rs | 173 + node/miner/src/pora.rs | 165 + node/miner/src/sealer.rs | 213 + node/miner/src/service.rs | 62 + node/miner/src/submitter.rs | 128 + node/miner/src/watcher.rs | 142 + node/network/Cargo.toml | 55 + node/network/src/behaviour/gossip_cache.rs | 183 + .../behaviour/gossipsub_scoring_parameters.rs | 354 + node/network/src/behaviour/mod.rs | 989 ++ node/network/src/config.rs | 351 + node/network/src/discovery/enr.rs | 152 + node/network/src/discovery/enr_ext.rs | 314 + node/network/src/discovery/mod.rs | 632 ++ node/network/src/lib.rs | 155 + node/network/src/metrics.rs | 162 + node/network/src/nat.rs | 231 + node/network/src/peer_manager/config.rs | 46 + node/network/src/peer_manager/mod.rs | 1329 +++ .../src/peer_manager/network_behaviour.rs | 251 + node/network/src/peer_manager/peerdb.rs | 1930 ++++ .../network/src/peer_manager/peerdb/client.rs | 102 + .../src/peer_manager/peerdb/peer_info.rs | 424 + node/network/src/peer_manager/peerdb/score.rs | 425 + .../src/peer_manager/peerdb/sync_status.rs | 82 + node/network/src/rpc/codec/base.rs | 262 + node/network/src/rpc/codec/mod.rs | 60 + node/network/src/rpc/codec/ssz_snappy.rs | 585 ++ node/network/src/rpc/handler.rs | 983 ++ node/network/src/rpc/methods.rs | 367 + node/network/src/rpc/mod.rs | 260 + node/network/src/rpc/outbound.rs | 178 + node/network/src/rpc/protocol.rs | 542 ++ node/network/src/rpc/rate_limiter.rs | 388 + node/network/src/service.rs | 508 ++ node/network/src/types/error.rs | 5 + node/network/src/types/globals.rs | 100 + node/network/src/types/mod.rs | 10 + node/network/src/types/pubsub.rs | 307 + node/network/src/types/topics.rs | 177 + node/network/tests/common/behaviour.rs | 394 + node/network/tests/common/mod.rs | 175 + node/network/tests/common/swarm.rs | 99 + node/network/tests/gossipsub_tests.rs | 171 + node/network/tests/pm_tests.rs | 197 + node/network/tests/rpc_tests.rs | 393 + node/router/Cargo.toml | 24 + node/router/src/lib.rs | 32 + node/router/src/libp2p_event_handler.rs | 786 ++ node/router/src/peer_manager.rs | 221 + node/router/src/service.rs | 347 + node/rpc/Cargo.toml | 29 + node/rpc/src/admin/api.rs | 23 + node/rpc/src/admin/impl.rs | 101 + node/rpc/src/admin/mod.rs | 5 + node/rpc/src/config.rs | 9 + node/rpc/src/error.rs | 30 + node/rpc/src/lib.rs | 82 + node/rpc/src/miner/api.rs | 14 + node/rpc/src/miner/impl.rs | 49 + node/rpc/src/miner/mod.rs | 6 + node/rpc/src/types.rs | 226 + node/rpc/src/zgs/api.rs | 34 + node/rpc/src/zgs/impl.rs | 238 + node/rpc/src/zgs/mod.rs | 5 + node/shared_types/Cargo.toml | 20 + node/shared_types/src/lib.rs | 348 + node/shared_types/src/proof.rs | 220 + node/src/cli/mod.rs | 7 + node/src/client/builder.rs | 273 + node/src/client/environment.rs | 253 + node/src/client/mod.rs | 34 + node/src/config/config_macro.rs | 86 + node/src/config/convert.rs | 158 + node/src/config/mod.rs | 100 + node/src/log.rs | 61 + node/src/main.rs | 101 + node/storage-async/Cargo.toml | 12 + node/storage-async/src/lib.rs | 91 + node/storage/Cargo.toml | 41 + node/storage/benches/benchmark.rs | 184 + node/storage/src/config.rs | 6 + node/storage/src/error.rs | 42 + node/storage/src/lib.rs | 32 + node/storage/src/log_store/config.rs | 85 + node/storage/src/log_store/flow_store.rs | 539 ++ .../src/log_store/load_chunk/bitmap.rs | 175 + .../src/log_store/load_chunk/chunk_data.rs | 497 + node/storage/src/log_store/load_chunk/mod.rs | 425 + node/storage/src/log_store/load_chunk/seal.rs | 303 + .../storage/src/log_store/load_chunk/serde.rs | 83 + node/storage/src/log_store/log_manager.rs | 1077 +++ node/storage/src/log_store/mod.rs | 227 + node/storage/src/log_store/tests.rs | 207 + node/storage/src/log_store/tx_store.rs | 276 + node/sync/Cargo.toml | 30 + node/sync/src/auto_sync/manager.rs | 667 ++ node/sync/src/auto_sync/mod.rs | 5 + node/sync/src/auto_sync/sync_store.rs | 264 + node/sync/src/auto_sync/tx_store.rs | 242 + node/sync/src/context.rs | 46 + node/sync/src/controllers/mod.rs | 16 + node/sync/src/controllers/peers.rs | 306 + node/sync/src/controllers/serial.rs | 1510 +++ node/sync/src/lib.rs | 42 + node/sync/src/service.rs | 1576 ++++ node/sync/src/test_util.rs | 150 + requirements.txt | 9 + run/config.toml | 32 + run/log_config | 1 + rust-toolchain | 1 + tests/cache_test.py | 26 + tests/ccov.sh | 40 + tests/cli_submission_test.py | 109 + tests/config/MockToken.json | 286 + tests/config/bsc.toml | 101 + tests/config/genesis.json | 32 + tests/config/node_config.py | 50 + tests/crash_test.py | 152 + tests/dep_pip3.sh | 18 + tests/example_test.py | 24 + tests/fuzz_test.py | 119 + tests/mine_test.py | 68 + tests/parallel_submission_test.py | 61 + tests/partial_submission_test.py | 26 + tests/random_test.py | 84 + tests/recovery_test.py | 57 + tests/revert_test.py | 78 + tests/rpc_test.py | 103 + tests/same_root_test.py | 73 + tests/submission_test.py | 104 + tests/sync_test.py | 58 + tests/test_all.py | 169 + tests/test_framework/blockchain_node.py | 326 + tests/test_framework/bsc_node.py | 164 + tests/test_framework/conflux_node.py | 335 + tests/test_framework/contract_proxy.py | 66 + tests/test_framework/test_framework.py | 461 + tests/test_framework/zgs_node.py | 110 + tests/utility/merkle_tree.py | 238 + tests/utility/signature_utils.py | 615 ++ tests/utility/simple_rpc_proxy.py | 31 + tests/utility/submission.py | 228 + tests/utility/utils.py | 130 + version-meld/discv5/.cargo-ok | 1 + version-meld/discv5/.cargo_vcs_info.json | 6 + version-meld/discv5/Cargo.lock | 2102 +++++ version-meld/discv5/Cargo.toml | 139 + version-meld/discv5/Cargo.toml.orig | 53 + version-meld/discv5/LICENSE | 201 + version-meld/discv5/README.md | 75 + .../discv5/examples/custom_executor.rs | 82 + version-meld/discv5/examples/find_nodes.rs | 167 + version-meld/discv5/examples/request_enr.rs | 65 + version-meld/discv5/examples/simple_server.rs | 109 + version-meld/discv5/src/config.rs | 339 + version-meld/discv5/src/discv5.rs | 592 ++ version-meld/discv5/src/discv5/test.rs | 482 + version-meld/discv5/src/error.rs | 146 + version-meld/discv5/src/executor.rs | 42 + .../discv5/src/handler/crypto/ecdh.rs | 23 + version-meld/discv5/src/handler/crypto/mod.rs | 405 + .../discv5/src/handler/hashmap_delay.rs | 170 + version-meld/discv5/src/handler/mod.rs | 1143 +++ version-meld/discv5/src/handler/session.rs | 253 + version-meld/discv5/src/handler/tests.rs | 219 + version-meld/discv5/src/kbucket.rs | 1027 +++ version-meld/discv5/src/kbucket/bucket.rs | 1439 +++ version-meld/discv5/src/kbucket/entry.rs | 198 + version-meld/discv5/src/kbucket/filter.rs | 96 + version-meld/discv5/src/kbucket/key.rs | 168 + version-meld/discv5/src/lib.rs | 135 + version-meld/discv5/src/lru_time_cache.rs | 253 + version-meld/discv5/src/metrics.rs | 72 + version-meld/discv5/src/node_info.rs | 190 + version-meld/discv5/src/packet/mod.rs | 899 ++ version-meld/discv5/src/permit_ban.rs | 27 + version-meld/discv5/src/query_pool.rs | 293 + version-meld/discv5/src/query_pool/peers.rs | 68 + .../discv5/src/query_pool/peers/closest.rs | 707 ++ .../discv5/src/query_pool/peers/predicate.rs | 438 + version-meld/discv5/src/rpc.rs | 1018 +++ version-meld/discv5/src/service.rs | 1434 +++ .../discv5/src/service/hashset_delay.rs | 131 + version-meld/discv5/src/service/ip_vote.rs | 113 + version-meld/discv5/src/service/query_info.rs | 134 + version-meld/discv5/src/service/test.rs | 173 + .../discv5/src/socket/filter/cache.rs | 113 + .../discv5/src/socket/filter/config.rs | 18 + version-meld/discv5/src/socket/filter/mod.rs | 223 + .../discv5/src/socket/filter/rate_limiter.rs | 414 + version-meld/discv5/src/socket/mod.rs | 95 + version-meld/discv5/src/socket/recv.rs | 160 + version-meld/discv5/src/socket/send.rs | 74 + version-meld/enr/.cargo-ok | 1 + version-meld/enr/.cargo_vcs_info.json | 6 + version-meld/enr/Cargo.toml | 101 + version-meld/enr/Cargo.toml.orig | 45 + version-meld/enr/LICENSE | 21 + version-meld/enr/README.md | 170 + version-meld/enr/src/builder.rs | 187 + version-meld/enr/src/error.rs | 33 + version-meld/enr/src/keys/combined.rs | 184 + version-meld/enr/src/keys/ed25519.rs | 75 + version-meld/enr/src/keys/k256_key.rs | 106 + version-meld/enr/src/keys/mod.rs | 108 + version-meld/enr/src/keys/rust_secp256k1.rs | 73 + version-meld/enr/src/lib.rs | 1268 +++ version-meld/enr/src/node_id.rs | 77 + version-meld/eth2_ssz/.cargo-ok | 1 + version-meld/eth2_ssz/.cargo_vcs_info.json | 5 + version-meld/eth2_ssz/Cargo.lock | 473 + version-meld/eth2_ssz/Cargo.toml | 31 + version-meld/eth2_ssz/Cargo.toml.orig | 20 + version-meld/eth2_ssz/README.md | 3 + version-meld/eth2_ssz/examples/large_list.rs | 15 + .../examples/large_list_of_structs.rs | 31 + .../eth2_ssz/examples/struct_definition.rs | 73 + version-meld/eth2_ssz/src/decode.rs | 372 + version-meld/eth2_ssz/src/decode/impls.rs | 697 ++ version-meld/eth2_ssz/src/encode.rs | 196 + version-meld/eth2_ssz/src/encode/impls.rs | 504 + version-meld/eth2_ssz/src/legacy.rs | 265 + version-meld/eth2_ssz/src/lib.rs | 71 + version-meld/eth2_ssz/src/union_selector.rs | 29 + version-meld/eth2_ssz/tests/tests.rs | 466 + zerog-storage-client | 1 + zerog-storage-contracts | 1 + 319 files changed, 71712 insertions(+) create mode 100644 .github/actions-rs/grcov.yml create mode 100644 .github/workflows/cc.yml create mode 100644 .github/workflows/rust.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE.lighthouse create mode 100644 NOTICE create mode 100644 codecov.yml create mode 100644 common/append_merkle/Cargo.toml create mode 100644 common/append_merkle/src/lib.rs create mode 100644 common/append_merkle/src/merkle_tree.rs create mode 100644 common/append_merkle/src/proof.rs create mode 100644 common/append_merkle/src/sha3.rs create mode 100644 common/channel/Cargo.toml create mode 100644 common/channel/src/channel.rs create mode 100644 common/channel/src/error.rs create mode 100644 common/channel/src/lib.rs create mode 100644 common/channel/src/test_util.rs create mode 100644 common/contract-interface/Cargo.toml create mode 100644 common/contract-interface/build.rs create mode 100644 common/contract-interface/src/lib.rs create mode 100644 common/directory/Cargo.toml create mode 100644 common/directory/src/lib.rs create mode 100644 common/hashset_delay/Cargo.toml create mode 100644 common/hashset_delay/src/hashset_delay.rs create mode 100644 common/hashset_delay/src/lib.rs create mode 100644 common/lighthouse_metrics/Cargo.toml create mode 100644 common/lighthouse_metrics/src/lib.rs create mode 100644 common/merkle_light/Cargo.toml create mode 100644 common/merkle_light/README.md create mode 100644 common/merkle_light/benches/crypto_sha512.rs create mode 100644 common/merkle_light/benches/hash512/hash512.rs create mode 100644 common/merkle_light/benches/hash512/mod.rs create mode 100644 common/merkle_light/benches/ring_sha512.rs create mode 100644 common/merkle_light/benches/ringx/init.rs create mode 100644 common/merkle_light/benches/ringx/mod.rs create mode 100644 common/merkle_light/clippy.toml create mode 100644 common/merkle_light/examples/no_std.rs create mode 100644 common/merkle_light/src/hash.rs create mode 100644 common/merkle_light/src/hash_impl.rs create mode 100644 common/merkle_light/src/lib.rs create mode 100644 common/merkle_light/src/merkle.rs create mode 100644 common/merkle_light/src/proof.rs create mode 100644 common/merkle_light/src/test_cmh.rs create mode 100644 common/merkle_light/src/test_item.rs create mode 100644 common/merkle_tree/Cargo.toml create mode 100644 common/merkle_tree/src/lib.rs create mode 100644 common/spec/Cargo.toml create mode 100644 common/spec/src/lib.rs create mode 100644 common/task_executor/Cargo.toml create mode 100644 common/task_executor/src/lib.rs create mode 100644 common/task_executor/src/metrics.rs create mode 100644 common/task_executor/src/test_utils.rs create mode 100644 common/unused_port/Cargo.toml create mode 100644 common/unused_port/src/lib.rs create mode 100644 common/zgs_seal/Cargo.toml create mode 100644 common/zgs_seal/src/lib.rs create mode 100644 common/zgs_version/Cargo.toml create mode 100644 common/zgs_version/src/lib.rs create mode 100644 doc/install.md create mode 100644 doc/run.md create mode 100644 node/.gitignore create mode 100644 node/Cargo.toml create mode 100644 node/build.rs create mode 100644 node/chunk_pool/Cargo.toml create mode 100644 node/chunk_pool/src/handler.rs create mode 100644 node/chunk_pool/src/lib.rs create mode 100644 node/chunk_pool/src/mem_pool/chunk_cache.rs create mode 100644 node/chunk_pool/src/mem_pool/chunk_pool_inner.rs create mode 100644 node/chunk_pool/src/mem_pool/chunk_write_control.rs create mode 100644 node/chunk_pool/src/mem_pool/mod.rs create mode 100644 node/file_location_cache/Cargo.toml create mode 100644 node/file_location_cache/src/file_location_cache.rs create mode 100644 node/file_location_cache/src/lib.rs create mode 100644 node/file_location_cache/src/test_util.rs create mode 100644 node/log_entry_sync/Cargo.toml create mode 100644 node/log_entry_sync/src/lib.rs create mode 100644 node/log_entry_sync/src/rpc_proxy/eth.rs create mode 100644 node/log_entry_sync/src/rpc_proxy/mod.rs create mode 100644 node/log_entry_sync/src/sync_manager/config.rs create mode 100644 node/log_entry_sync/src/sync_manager/data_cache.rs create mode 100644 node/log_entry_sync/src/sync_manager/log_entry_fetcher.rs create mode 100644 node/log_entry_sync/src/sync_manager/log_query.rs create mode 100644 node/log_entry_sync/src/sync_manager/mod.rs create mode 100644 node/miner/Cargo.toml create mode 100644 node/miner/src/config.rs create mode 100644 node/miner/src/lib.rs create mode 100644 node/miner/src/loader.rs create mode 100644 node/miner/src/mine.rs create mode 100644 node/miner/src/pora.rs create mode 100644 node/miner/src/sealer.rs create mode 100644 node/miner/src/service.rs create mode 100644 node/miner/src/submitter.rs create mode 100644 node/miner/src/watcher.rs create mode 100644 node/network/Cargo.toml create mode 100644 node/network/src/behaviour/gossip_cache.rs create mode 100644 node/network/src/behaviour/gossipsub_scoring_parameters.rs create mode 100644 node/network/src/behaviour/mod.rs create mode 100644 node/network/src/config.rs create mode 100644 node/network/src/discovery/enr.rs create mode 100644 node/network/src/discovery/enr_ext.rs create mode 100644 node/network/src/discovery/mod.rs create mode 100644 node/network/src/lib.rs create mode 100644 node/network/src/metrics.rs create mode 100644 node/network/src/nat.rs create mode 100644 node/network/src/peer_manager/config.rs create mode 100644 node/network/src/peer_manager/mod.rs create mode 100644 node/network/src/peer_manager/network_behaviour.rs create mode 100644 node/network/src/peer_manager/peerdb.rs create mode 100644 node/network/src/peer_manager/peerdb/client.rs create mode 100644 node/network/src/peer_manager/peerdb/peer_info.rs create mode 100644 node/network/src/peer_manager/peerdb/score.rs create mode 100644 node/network/src/peer_manager/peerdb/sync_status.rs create mode 100644 node/network/src/rpc/codec/base.rs create mode 100644 node/network/src/rpc/codec/mod.rs create mode 100644 node/network/src/rpc/codec/ssz_snappy.rs create mode 100644 node/network/src/rpc/handler.rs create mode 100644 node/network/src/rpc/methods.rs create mode 100644 node/network/src/rpc/mod.rs create mode 100644 node/network/src/rpc/outbound.rs create mode 100644 node/network/src/rpc/protocol.rs create mode 100644 node/network/src/rpc/rate_limiter.rs create mode 100644 node/network/src/service.rs create mode 100644 node/network/src/types/error.rs create mode 100644 node/network/src/types/globals.rs create mode 100644 node/network/src/types/mod.rs create mode 100644 node/network/src/types/pubsub.rs create mode 100644 node/network/src/types/topics.rs create mode 100644 node/network/tests/common/behaviour.rs create mode 100644 node/network/tests/common/mod.rs create mode 100644 node/network/tests/common/swarm.rs create mode 100644 node/network/tests/gossipsub_tests.rs create mode 100644 node/network/tests/pm_tests.rs create mode 100644 node/network/tests/rpc_tests.rs create mode 100644 node/router/Cargo.toml create mode 100644 node/router/src/lib.rs create mode 100644 node/router/src/libp2p_event_handler.rs create mode 100644 node/router/src/peer_manager.rs create mode 100644 node/router/src/service.rs create mode 100644 node/rpc/Cargo.toml create mode 100644 node/rpc/src/admin/api.rs create mode 100644 node/rpc/src/admin/impl.rs create mode 100644 node/rpc/src/admin/mod.rs create mode 100644 node/rpc/src/config.rs create mode 100644 node/rpc/src/error.rs create mode 100644 node/rpc/src/lib.rs create mode 100644 node/rpc/src/miner/api.rs create mode 100644 node/rpc/src/miner/impl.rs create mode 100644 node/rpc/src/miner/mod.rs create mode 100644 node/rpc/src/types.rs create mode 100644 node/rpc/src/zgs/api.rs create mode 100644 node/rpc/src/zgs/impl.rs create mode 100644 node/rpc/src/zgs/mod.rs create mode 100644 node/shared_types/Cargo.toml create mode 100644 node/shared_types/src/lib.rs create mode 100644 node/shared_types/src/proof.rs create mode 100644 node/src/cli/mod.rs create mode 100644 node/src/client/builder.rs create mode 100644 node/src/client/environment.rs create mode 100644 node/src/client/mod.rs create mode 100644 node/src/config/config_macro.rs create mode 100644 node/src/config/convert.rs create mode 100644 node/src/config/mod.rs create mode 100644 node/src/log.rs create mode 100644 node/src/main.rs create mode 100644 node/storage-async/Cargo.toml create mode 100644 node/storage-async/src/lib.rs create mode 100644 node/storage/Cargo.toml create mode 100644 node/storage/benches/benchmark.rs create mode 100644 node/storage/src/config.rs create mode 100644 node/storage/src/error.rs create mode 100644 node/storage/src/lib.rs create mode 100644 node/storage/src/log_store/config.rs create mode 100644 node/storage/src/log_store/flow_store.rs create mode 100644 node/storage/src/log_store/load_chunk/bitmap.rs create mode 100644 node/storage/src/log_store/load_chunk/chunk_data.rs create mode 100644 node/storage/src/log_store/load_chunk/mod.rs create mode 100644 node/storage/src/log_store/load_chunk/seal.rs create mode 100644 node/storage/src/log_store/load_chunk/serde.rs create mode 100644 node/storage/src/log_store/log_manager.rs create mode 100644 node/storage/src/log_store/mod.rs create mode 100644 node/storage/src/log_store/tests.rs create mode 100644 node/storage/src/log_store/tx_store.rs create mode 100644 node/sync/Cargo.toml create mode 100644 node/sync/src/auto_sync/manager.rs create mode 100644 node/sync/src/auto_sync/mod.rs create mode 100644 node/sync/src/auto_sync/sync_store.rs create mode 100644 node/sync/src/auto_sync/tx_store.rs create mode 100644 node/sync/src/context.rs create mode 100644 node/sync/src/controllers/mod.rs create mode 100644 node/sync/src/controllers/peers.rs create mode 100644 node/sync/src/controllers/serial.rs create mode 100644 node/sync/src/lib.rs create mode 100644 node/sync/src/service.rs create mode 100644 node/sync/src/test_util.rs create mode 100644 requirements.txt create mode 100644 run/config.toml create mode 100644 run/log_config create mode 100644 rust-toolchain create mode 100755 tests/cache_test.py create mode 100755 tests/ccov.sh create mode 100755 tests/cli_submission_test.py create mode 100644 tests/config/MockToken.json create mode 100644 tests/config/bsc.toml create mode 100644 tests/config/genesis.json create mode 100644 tests/config/node_config.py create mode 100644 tests/crash_test.py create mode 100755 tests/dep_pip3.sh create mode 100755 tests/example_test.py create mode 100644 tests/fuzz_test.py create mode 100755 tests/mine_test.py create mode 100644 tests/parallel_submission_test.py create mode 100755 tests/partial_submission_test.py create mode 100755 tests/random_test.py create mode 100755 tests/recovery_test.py create mode 100755 tests/revert_test.py create mode 100755 tests/rpc_test.py create mode 100755 tests/same_root_test.py create mode 100755 tests/submission_test.py create mode 100755 tests/sync_test.py create mode 100755 tests/test_all.py create mode 100644 tests/test_framework/blockchain_node.py create mode 100644 tests/test_framework/bsc_node.py create mode 100644 tests/test_framework/conflux_node.py create mode 100644 tests/test_framework/contract_proxy.py create mode 100644 tests/test_framework/test_framework.py create mode 100644 tests/test_framework/zgs_node.py create mode 100644 tests/utility/merkle_tree.py create mode 100644 tests/utility/signature_utils.py create mode 100644 tests/utility/simple_rpc_proxy.py create mode 100644 tests/utility/submission.py create mode 100644 tests/utility/utils.py create mode 100644 version-meld/discv5/.cargo-ok create mode 100644 version-meld/discv5/.cargo_vcs_info.json create mode 100644 version-meld/discv5/Cargo.lock create mode 100644 version-meld/discv5/Cargo.toml create mode 100644 version-meld/discv5/Cargo.toml.orig create mode 100644 version-meld/discv5/LICENSE create mode 100644 version-meld/discv5/README.md create mode 100644 version-meld/discv5/examples/custom_executor.rs create mode 100644 version-meld/discv5/examples/find_nodes.rs create mode 100644 version-meld/discv5/examples/request_enr.rs create mode 100644 version-meld/discv5/examples/simple_server.rs create mode 100644 version-meld/discv5/src/config.rs create mode 100644 version-meld/discv5/src/discv5.rs create mode 100644 version-meld/discv5/src/discv5/test.rs create mode 100644 version-meld/discv5/src/error.rs create mode 100644 version-meld/discv5/src/executor.rs create mode 100644 version-meld/discv5/src/handler/crypto/ecdh.rs create mode 100644 version-meld/discv5/src/handler/crypto/mod.rs create mode 100644 version-meld/discv5/src/handler/hashmap_delay.rs create mode 100644 version-meld/discv5/src/handler/mod.rs create mode 100644 version-meld/discv5/src/handler/session.rs create mode 100644 version-meld/discv5/src/handler/tests.rs create mode 100644 version-meld/discv5/src/kbucket.rs create mode 100644 version-meld/discv5/src/kbucket/bucket.rs create mode 100644 version-meld/discv5/src/kbucket/entry.rs create mode 100644 version-meld/discv5/src/kbucket/filter.rs create mode 100644 version-meld/discv5/src/kbucket/key.rs create mode 100644 version-meld/discv5/src/lib.rs create mode 100644 version-meld/discv5/src/lru_time_cache.rs create mode 100644 version-meld/discv5/src/metrics.rs create mode 100644 version-meld/discv5/src/node_info.rs create mode 100644 version-meld/discv5/src/packet/mod.rs create mode 100644 version-meld/discv5/src/permit_ban.rs create mode 100644 version-meld/discv5/src/query_pool.rs create mode 100644 version-meld/discv5/src/query_pool/peers.rs create mode 100644 version-meld/discv5/src/query_pool/peers/closest.rs create mode 100644 version-meld/discv5/src/query_pool/peers/predicate.rs create mode 100644 version-meld/discv5/src/rpc.rs create mode 100644 version-meld/discv5/src/service.rs create mode 100644 version-meld/discv5/src/service/hashset_delay.rs create mode 100644 version-meld/discv5/src/service/ip_vote.rs create mode 100644 version-meld/discv5/src/service/query_info.rs create mode 100644 version-meld/discv5/src/service/test.rs create mode 100644 version-meld/discv5/src/socket/filter/cache.rs create mode 100644 version-meld/discv5/src/socket/filter/config.rs create mode 100644 version-meld/discv5/src/socket/filter/mod.rs create mode 100644 version-meld/discv5/src/socket/filter/rate_limiter.rs create mode 100644 version-meld/discv5/src/socket/mod.rs create mode 100644 version-meld/discv5/src/socket/recv.rs create mode 100644 version-meld/discv5/src/socket/send.rs create mode 100644 version-meld/enr/.cargo-ok create mode 100644 version-meld/enr/.cargo_vcs_info.json create mode 100644 version-meld/enr/Cargo.toml create mode 100644 version-meld/enr/Cargo.toml.orig create mode 100644 version-meld/enr/LICENSE create mode 100644 version-meld/enr/README.md create mode 100644 version-meld/enr/src/builder.rs create mode 100644 version-meld/enr/src/error.rs create mode 100644 version-meld/enr/src/keys/combined.rs create mode 100644 version-meld/enr/src/keys/ed25519.rs create mode 100644 version-meld/enr/src/keys/k256_key.rs create mode 100644 version-meld/enr/src/keys/mod.rs create mode 100644 version-meld/enr/src/keys/rust_secp256k1.rs create mode 100644 version-meld/enr/src/lib.rs create mode 100644 version-meld/enr/src/node_id.rs create mode 100644 version-meld/eth2_ssz/.cargo-ok create mode 100644 version-meld/eth2_ssz/.cargo_vcs_info.json create mode 100644 version-meld/eth2_ssz/Cargo.lock create mode 100644 version-meld/eth2_ssz/Cargo.toml create mode 100644 version-meld/eth2_ssz/Cargo.toml.orig create mode 100644 version-meld/eth2_ssz/README.md create mode 100644 version-meld/eth2_ssz/examples/large_list.rs create mode 100644 version-meld/eth2_ssz/examples/large_list_of_structs.rs create mode 100644 version-meld/eth2_ssz/examples/struct_definition.rs create mode 100644 version-meld/eth2_ssz/src/decode.rs create mode 100644 version-meld/eth2_ssz/src/decode/impls.rs create mode 100644 version-meld/eth2_ssz/src/encode.rs create mode 100644 version-meld/eth2_ssz/src/encode/impls.rs create mode 100644 version-meld/eth2_ssz/src/legacy.rs create mode 100644 version-meld/eth2_ssz/src/lib.rs create mode 100644 version-meld/eth2_ssz/src/union_selector.rs create mode 100644 version-meld/eth2_ssz/tests/tests.rs create mode 160000 zerog-storage-client create mode 160000 zerog-storage-contracts diff --git a/.github/actions-rs/grcov.yml b/.github/actions-rs/grcov.yml new file mode 100644 index 0000000..5cfdff8 --- /dev/null +++ b/.github/actions-rs/grcov.yml @@ -0,0 +1,3 @@ +branch: false +ignore-not-existing: true +llvm: true \ No newline at end of file diff --git a/.github/workflows/cc.yml b/.github/workflows/cc.yml new file mode 100644 index 0000000..f824d0e --- /dev/null +++ b/.github/workflows/cc.yml @@ -0,0 +1,53 @@ +name: code-coverage + +on: + push: + branches: [ "main"] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + unittest-cov: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + + - name: Install 1.71.0 toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: 1.71.0 + override: true + + - name: Run unittest + run: cargo test --all-features --no-fail-fast + env: + CARGO_INCREMENTAL: '0' + RUSTC_BOOTSTRAP: '1' + RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests' + RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests' + + - id: coverage + uses: actions-rs/grcov@v0.1 + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ${{ steps.coverage.outputs.report }} + fail_ci_if_error: true \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..792c2c1 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,100 @@ +# Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md +# +# While our "example" application has the platform-specific code, +# for simplicity we are compiling and testing everything on the Ubuntu environment only. +# For multi-OS testing see the `cross.yml` workflow. + +name: rust + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: check + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Run cargo check + uses: actions-rs/cargo@v1 + with: + command: check + + test: + name: test + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + args: --release + + lints: + name: lints + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..7dc76f1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,50 @@ +name: functional-test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build --release --verbose + + - name: Set up Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: '3.9' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run tests + run: | + cd tests + python test_all.py \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2e98aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/target +# src/config.rs +/.idea +tests/**/__pycache__ +tests/tmp/** +.vscode/*.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f8df220 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "zerog-storage-contracts"] + path = zerog-storage-contracts + url = git@github.com:zero-gravity-labs/zerog-storage-contracts.git +[submodule "zerog-storage-client"] + path = zerog-storage-client + url = git@github.com:zero-gravity-labs/zerog-storage-client.git diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..072c7ba --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,8082 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures 0.2.9", + "ctr 0.8.0", + "opaque-debug", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.9", +] + +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes 0.7.5", + "cipher 0.3.0", + "ctr 0.8.0", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.10", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anyhow" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +dependencies = [ + "backtrace", +] + +[[package]] +name = "append_merkle" +version = "0.1.0" +dependencies = [ + "anyhow", + "eth2_ssz", + "eth2_ssz_derive", + "ethereum-types 0.14.1", + "lazy_static", + "serde", + "tiny-keccak", + "tracing", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "asn1_der" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand 1.9.0", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix 0.37.23", + "slab", + "socket2 0.4.9", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-process" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 0.37.23", + "signal-hook", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite 0.2.12", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-std-resolver" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2f8a4a203be3325981310ab243a28e6e4ea55b6519bffce05d41ab60e09ad8" +dependencies = [ + "async-std", + "async-trait", + "futures-io", + "futures-util", + "pin-utils", + "socket2 0.4.9", + "trust-dns-resolver", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "asynchronous-codec" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite 0.2.12", +] + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + +[[package]] +name = "attohttpc" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247" +dependencies = [ + "http", + "log", + "url", + "wildmatch", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "auto_impl" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dabbe35f96fb9507f7330793dc490461b2962659ac5d427181e451a623751d1" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +dependencies = [ + "serde", +] + +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.28", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "bitmaps" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703642b98a00b3b90513279a8ede3fcfa479c126c5fb46e78f3051522f021403" + +[[package]] +name = "bitvec" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c" +dependencies = [ + "either", + "radium 0.3.0", +] + +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty 1.1.0", + "radium 0.6.2", + "tap", + "wyz 0.2.0", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty 2.0.0", + "radium 0.7.0", + "tap", + "wyz 0.5.1", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand 1.9.0", + "futures-lite", + "log", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +dependencies = [ + "sha2 0.9.9", +] + +[[package]] +name = "bstr" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfa25e60aea747ec7e1124f238816749faa93759c6ff5b31f1ccdda137f4479" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures 0.2.9", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" +dependencies = [ + "aead", + "chacha20", + "cipher 0.3.0", + "poly1305", + "zeroize", +] + +[[package]] +name = "channel" +version = "0.1.0" +dependencies = [ + "tokio", +] + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "chunk_pool" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-lock", + "hashlink 0.8.3", + "log_entry_sync", + "network", + "shared_types", + "storage-async", + "tokio", + "tracing", +] + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "coins-bip32" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30a84aab436fcb256a2ab3c80663d8aec686e6bae12827bb05fef3e1e439c9f" +dependencies = [ + "bincode", + "bs58", + "coins-core", + "digest 0.10.7", + "getrandom 0.2.10", + "hmac 0.12.1", + "k256 0.13.1", + "lazy_static", + "serde", + "sha2 0.10.7", + "thiserror", +] + +[[package]] +name = "coins-bip39" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f4d04ee18e58356accd644896aeb2094ddeafb6a713e056cef0c0a8e468c15" +dependencies = [ + "bitvec 0.17.4", + "coins-bip32", + "getrandom 0.2.10", + "hmac 0.12.1", + "once_cell", + "pbkdf2 0.12.2", + "rand 0.8.5", + "sha2 0.10.7", + "thiserror", +] + +[[package]] +name = "coins-core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b949a1c63fb7eb591eb7ba438746326aedf0ae843e51ec92ba6bec5bb382c4f" +dependencies = [ + "base64 0.21.2", + "bech32", + "bs58", + "digest 0.10.7", + "generic-array", + "hex", + "ripemd", + "serde", + "serde_derive", + "sha2 0.10.7", + "sha3", + "thiserror", +] + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml 0.5.11", + "yaml-rust", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "contract-interface" +version = "0.1.0" +dependencies = [ + "ethers", + "serde_json", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher 0.3.0", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "ctrlc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e" +dependencies = [ + "nix 0.26.2", + "windows-sys 0.48.0", +] + +[[package]] +name = "cuckoofilter" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b810a8449931679f64cd7eef1bbd0fa315801b6d5d9cdc1ace2804d6529eee18" +dependencies = [ + "byteorder", + "fnv", + "rand 0.7.3", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +dependencies = [ + "cfg-if", + "fiat-crypto", + "packed_simd_2", + "platforms", + "subtle", + "zeroize", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "directory" +version = "0.1.0" + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "discv5" +version = "0.1.0-beta.13" +dependencies = [ + "aes 0.7.5", + "aes-gcm", + "arrayvec", + "digest 0.10.7", + "enr 0.6.2", + "fnv", + "futures", + "hashlink 0.7.0", + "hex", + "hkdf", + "lazy_static", + "libp2p-core 0.30.2", + "lru", + "parking_lot 0.11.2", + "rand 0.8.5", + "rlp", + "sha2 0.10.7", + "smallvec", + "tokio", + "tokio-stream", + "tokio-util 0.6.10", + "tracing", + "tracing-subscriber", + "uint", + "zeroize", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "dns-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d33be9473d06f75f58220f71f7a9317aca647dc061dbd3c361b0bef505fbea" +dependencies = [ + "byteorder", + "quick-error", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "duration-str" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f037c488d179e21c87ef5fa9c331e8e62f5dddfa84618b41bb197da03edff1" +dependencies = [ + "chrono", + "nom", + "rust_decimal", + "serde", + "thiserror", + "time 0.3.25", +] + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +dependencies = [ + "der 0.7.8", + "digest 0.10.7", + "elliptic-curve 0.13.5", + "rfc6979 0.4.0", + "signature 2.1.0", + "spki 0.7.2", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.0", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest 0.10.7", + "ff 0.12.1", + "generic-array", + "group 0.12.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.2", + "digest 0.10.7", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", + "subtle", + "zeroize", +] + +[[package]] +name = "ena" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" +dependencies = [ + "log", +] + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enr" +version = "0.6.2" +dependencies = [ + "base64 0.13.1", + "bs58", + "bytes", + "ed25519-dalek", + "hex", + "k256 0.11.6", + "log", + "rand 0.8.5", + "rlp", + "serde", + "sha3", + "zeroize", +] + +[[package]] +name = "enr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf56acd72bb22d2824e66ae8e9e5ada4d0de17a69c7fd35569dde2ada8ec9116" +dependencies = [ + "base64 0.13.1", + "bytes", + "hex", + "k256 0.13.1", + "log", + "rand 0.8.5", + "rlp", + "serde", + "sha3", + "zeroize", +] + +[[package]] +name = "enum-as-inner" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + +[[package]] +name = "eth-keystore" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" +dependencies = [ + "aes 0.8.3", + "ctr 0.9.2", + "digest 0.10.7", + "hex", + "hmac 0.12.1", + "pbkdf2 0.11.0", + "rand 0.8.5", + "scrypt", + "serde", + "serde_json", + "sha2 0.10.7", + "sha3", + "thiserror", + "uuid", +] + +[[package]] +name = "eth2_hashing" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b67737df7e3769e823d9d583eb5d60bcc4b2ef97ca674d1964ef287a02f8517" +dependencies = [ + "cpufeatures 0.1.5", + "lazy_static", + "ring", + "sha2 0.9.9", +] + +[[package]] +name = "eth2_serde_utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "477fffc25490dfc866288273f96344c6879676a1337187fc39245cd422e10825" +dependencies = [ + "hex", + "serde", + "serde_derive", +] + +[[package]] +name = "eth2_ssz" +version = "0.4.0" +dependencies = [ + "ethereum-types 0.14.1", + "smallvec", +] + +[[package]] +name = "eth2_ssz_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635b86d2c941bb71e7419a571e1763d65c93e51a1bafc400352e3bef6ff59fc9" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "eth2_ssz_types" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9423ac7fb37037f828a32b724cdfa65ea62290055811731402a90fb8a5bcbb1" +dependencies = [ + "eth2_serde_utils", + "eth2_ssz", + "serde", + "serde_derive", + "tree_hash", + "typenum", +] + +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types 0.14.1", + "hex", + "once_cell", + "regex", + "serde", + "serde_json", + "sha3", + "thiserror", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" +dependencies = [ + "crunchy", + "fixed-hash 0.7.0", + "impl-rlp", + "impl-serde 0.3.2", + "tiny-keccak", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy", + "fixed-hash 0.8.0", + "impl-codec 0.6.0", + "impl-rlp", + "impl-serde 0.4.0", + "scale-info", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" +dependencies = [ + "ethbloom 0.11.1", + "fixed-hash 0.7.0", + "impl-rlp", + "impl-serde 0.3.2", + "primitive-types 0.10.1", + "uint", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom 0.13.0", + "fixed-hash 0.8.0", + "impl-codec 0.6.0", + "impl-rlp", + "impl-serde 0.4.0", + "primitive-types 0.12.1", + "scale-info", + "uint", +] + +[[package]] +name = "ethers" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b4026b97da8281276744741fac7eb385da905f6093c583331fa2953fdd4253" +dependencies = [ + "ethers-addressbook", + "ethers-contract", + "ethers-core", + "ethers-etherscan", + "ethers-middleware", + "ethers-providers", + "ethers-signers", + "ethers-solc", +] + +[[package]] +name = "ethers-addressbook" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcb6ffefc230d8c42874c51b28dc11dbb8de50b27a8fdf92648439d6baa68dc" +dependencies = [ + "ethers-core", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "ethers-contract" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4719a44c3d37ab07c6dea99ab174068d8c35e441b60b6c20ce4e48357273e8" +dependencies = [ + "ethers-contract-abigen", + "ethers-contract-derive", + "ethers-core", + "ethers-providers", + "ethers-signers", + "futures-util", + "hex", + "once_cell", + "pin-project 1.1.3", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "ethers-contract-abigen" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155ea1b84d169d231317ed86e307af6f2bed6b40dd17e5e94bc84da21cadb21c" +dependencies = [ + "Inflector", + "dunce", + "ethers-core", + "ethers-etherscan", + "eyre", + "hex", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "reqwest", + "serde", + "serde_json", + "syn 2.0.28", + "toml 0.7.6", + "walkdir", +] + +[[package]] +name = "ethers-contract-derive" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8567ff196c4a37c1a8c90ec73bda0ad2062e191e4f0a6dc4d943e2ec4830fc88" +dependencies = [ + "Inflector", + "ethers-contract-abigen", + "ethers-core", + "hex", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.28", +] + +[[package]] +name = "ethers-core" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60ca2514feb98918a0a31de7e1983c29f2267ebf61b2dc5d4294f91e5b866623" +dependencies = [ + "arrayvec", + "bytes", + "cargo_metadata", + "chrono", + "elliptic-curve 0.13.5", + "ethabi", + "generic-array", + "hex", + "k256 0.13.1", + "num_enum", + "once_cell", + "open-fastrlp", + "rand 0.8.5", + "rlp", + "serde", + "serde_json", + "strum 0.25.0", + "syn 2.0.28", + "tempfile", + "thiserror", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "ethers-etherscan" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22b3a8269d3df0ed6364bc05b4735b95f4bf830ce3aef87d5e760fb0e93e5b91" +dependencies = [ + "ethers-core", + "reqwest", + "semver", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "ethers-middleware" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c339aad74ae5c451d27e0e49c7a3c7d22620b119b4f9291d7aa21f72d7f366" +dependencies = [ + "async-trait", + "auto_impl", + "ethers-contract", + "ethers-core", + "ethers-etherscan", + "ethers-providers", + "ethers-signers", + "futures-channel", + "futures-locks", + "futures-util", + "instant", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-futures", + "url", +] + +[[package]] +name = "ethers-providers" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b411b119f1cf0efb69e2190883dee731251882bb21270f893ee9513b3a697c48" +dependencies = [ + "async-trait", + "auto_impl", + "base64 0.21.2", + "bytes", + "enr 0.8.1", + "ethers-core", + "futures-channel", + "futures-core", + "futures-timer", + "futures-util", + "hashers", + "hex", + "http", + "instant", + "once_cell", + "pin-project 1.1.3", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-futures", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "ws_stream_wasm", +] + +[[package]] +name = "ethers-signers" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4864d387456a9c09a1157fa10e1528b29d90f1d859443acf06a1b23365fb518c" +dependencies = [ + "async-trait", + "coins-bip32", + "coins-bip39", + "elliptic-curve 0.13.5", + "eth-keystore", + "ethers-core", + "hex", + "rand 0.8.5", + "sha2 0.10.7", + "thiserror", + "tracing", +] + +[[package]] +name = "ethers-solc" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6c2b9625a2c639d46625f88acc2092a3cb35786c37f7c2128b3ca20f639b3c" +dependencies = [ + "cfg-if", + "dunce", + "ethers-core", + "glob", + "hex", + "home", + "md-5", + "num_cpus", + "once_cell", + "path-slash", + "rayon", + "regex", + "semver", + "serde", + "serde_json", + "solang-parser", + "svm-rs", + "thiserror", + "tiny-keccak", + "tokio", + "tracing", + "walkdir", + "yansi", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "exit-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e43f2f1833d64e33f15592464d6fdd70f349dda7b1a53088eb83cd94014008c5" +dependencies = [ + "futures", +] + +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + +[[package]] +name = "file_location_cache" +version = "0.1.0" +dependencies = [ + "hashlink 0.8.3", + "network", + "parking_lot 0.12.1", + "priority-queue", + "rand 0.8.5", + "shared_types", + "tracing", +] + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite 0.2.12", + "waker-fn", +] + +[[package]] +name = "futures-locks" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06" +dependencies = [ + "futures-channel", + "futures-task", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "futures-rustls" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2411eed028cdf8c8034eaf21f9915f956b6c3abec4d4c7949ee67f0721127bd" +dependencies = [ + "futures-io", + "rustls 0.20.8", + "webpki", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.12", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "git-version" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899" +dependencies = [ + "git-version-macro", + "proc-macro-hack", +] + +[[package]] +name = "git-version-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "gloo-net" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "js-sys", + "pin-project 1.1.3", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util 0.7.8", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashers" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bca93b15ea5a746f220e56587f71e73c6165eab783df9e26590069953e3c30" +dependencies = [ + "fxhash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", +] + +[[package]] +name = "hashset_delay" +version = "0.2.0" +dependencies = [ + "futures", + "tokio", + "tokio-util 0.6.10", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite 0.2.12", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite 0.2.12", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper", + "log", + "rustls 0.20.8", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.23.4", + "webpki-roots 0.22.6", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls 0.21.6", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows 0.48.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if-addrs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc0fa01ffc752e9dbc72818cdb072cd028b86be5e09dd04c5a643704fe101a9" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "if-addrs" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfc4a06638d2fd0dda83b01126fefd38ef9f04f54d2fc717a938df68b83a68d" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "if-watch" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "015a7df1eb6dda30df37f34b63ada9b7b352984b0e84de2a20ed526345000791" +dependencies = [ + "async-io", + "core-foundation", + "fnv", + "futures", + "if-addrs 0.7.0", + "ipnet", + "log", + "rtnetlink", + "system-configuration", + "windows 0.34.0", +] + +[[package]] +name = "igd" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556b5a75cd4adb7c4ea21c64af1c48cefb2ce7d43dc4352c720a1fe47c21f355" +dependencies = [ + "attohttpc", + "log", + "rand 0.8.5", + "url", + "xmltree", +] + +[[package]] +name = "impl-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +dependencies = [ + "parity-scale-codec 2.3.1", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec 3.6.4", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.2", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.3", + "widestring", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.2", + "rustix 0.38.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonrpsee" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11e017217fcd18da0a25296d3693153dd19c8a6aadab330b3595285d075385d1" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-http-server", + "jsonrpsee-proc-macros", + "jsonrpsee-types", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", + "jsonrpsee-ws-server", + "tracing", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce395539a14d3ad4ec1256fde105abd36a2da25d578a291cabe98f45adfdb111" +dependencies = [ + "anyhow", + "futures-channel", + "futures-timer", + "futures-util", + "gloo-net", + "http", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project 1.1.3", + "rustls-native-certs", + "soketto", + "thiserror", + "tokio", + "tokio-rustls 0.23.4", + "tokio-util 0.7.8", + "tracing", + "webpki-roots 0.22.6", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16efcd4477de857d4a2195a45769b2fe9ebb54f3ef5a4221d3b014a4fe33ec0b" +dependencies = [ + "anyhow", + "arrayvec", + "async-lock", + "async-trait", + "beef", + "futures-channel", + "futures-timer", + "futures-util", + "globset", + "hyper", + "jsonrpsee-types", + "lazy_static", + "parking_lot 0.12.1", + "rand 0.8.5", + "rustc-hash", + "serde", + "serde_json", + "soketto", + "thiserror", + "tokio", + "tracing", + "unicase", + "wasm-bindgen-futures", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc1d8c0e4f455c47df21f8a29f4bbbcb75eb71bfee919b92e92502b48358392" +dependencies = [ + "async-trait", + "hyper", + "hyper-rustls 0.23.2", + "jsonrpsee-core", + "jsonrpsee-types", + "rustc-hash", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-http-server" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd69efeb3ce2cba767f126872f4eeb4624038a29098e75d77608b2b4345ad03" +dependencies = [ + "futures-channel", + "futures-util", + "hyper", + "jsonrpsee-core", + "jsonrpsee-types", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874cf3f6a027cebf36cae767feca9aa2e8a8f799880e49eb5540819fcbd8eada" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcf76cd316f5d3ad48138085af1f45e2c58c98e02f0779783dbb034d43f7c86" +dependencies = [ + "anyhow", + "beef", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1fcb257e9b60de22ec3c151106ad1e089ab1c0691d25e3282f1cc7a0c7ba651" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee043cb5dd0d51d3eb93432e998d5bae797691a7b10ec4a325e036bcdb48c48a" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", +] + +[[package]] +name = "jsonrpsee-ws-server" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd2e4d266774a671f8def3794255b28eddd09b18d76e0b913fa439f34588c0a" +dependencies = [ + "futures-channel", + "futures-util", + "jsonrpsee-core", + "jsonrpsee-types", + "serde_json", + "soketto", + "tokio", + "tokio-stream", + "tokio-util 0.7.8", + "tracing", +] + +[[package]] +name = "k256" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +dependencies = [ + "cfg-if", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.7", +] + +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa 0.16.8", + "elliptic-curve 0.13.5", + "once_cell", + "sha2 0.10.7", + "signature 2.1.0", +] + +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures 0.2.9", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "kvdb" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d770dcb02bf6835887c3a979b5107a04ff4bbde97a5f0928d27404a155add9" +dependencies = [ + "smallvec", +] + +[[package]] +name = "kvdb-memorydb" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7a85fe66f9ff9cd74e169fdd2c94c6e1e74c412c99a73b4df3200b5d3760b2" +dependencies = [ + "kvdb", + "parking_lot 0.12.1", +] + +[[package]] +name = "kvdb-rocksdb" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b644c70b92285f66bfc2032922a79000ea30af7bc2ab31902992a5dcb9b434f6" +dependencies = [ + "kvdb", + "num_cpus", + "parking_lot 0.12.1", + "regex", + "rocksdb", + "smallvec", +] + +[[package]] +name = "lalrpop" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4081d44f4611b66c6dd725e6de3169f9f63905421e8626fcb86b6a898998b8" +dependencies = [ + "ascii-canvas", + "bit-set", + "diff", + "ena", + "is-terminal", + "itertools", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax 0.7.4", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + +[[package]] +name = "libp2p" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41726ee8f662563fafba2d2d484b14037cc8ecb8c953fbfc8439d4ce3a0a9029" +dependencies = [ + "bytes", + "futures", + "futures-timer", + "getrandom 0.2.10", + "instant", + "lazy_static", + "libp2p-autonat", + "libp2p-core 0.33.0", + "libp2p-deflate", + "libp2p-dns", + "libp2p-floodsub", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-kad", + "libp2p-mdns", + "libp2p-metrics", + "libp2p-mplex", + "libp2p-noise", + "libp2p-ping", + "libp2p-plaintext", + "libp2p-pnet", + "libp2p-relay", + "libp2p-rendezvous", + "libp2p-request-response", + "libp2p-swarm", + "libp2p-swarm-derive", + "libp2p-tcp", + "libp2p-uds", + "libp2p-wasm-ext", + "libp2p-websocket", + "libp2p-yamux", + "multiaddr 0.14.0", + "parking_lot 0.12.1", + "pin-project 1.1.3", + "rand 0.7.3", + "smallvec", +] + +[[package]] +name = "libp2p-autonat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d45945fd2f96c4b133c23d5c28a8b7fc8d7138e6dd8d5a8cd492dd384f888e3" +dependencies = [ + "async-trait", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "libp2p-request-response", + "libp2p-swarm", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.8.5", +] + +[[package]] +name = "libp2p-core" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86aad7d54df283db817becded03e611137698a6509d4237a96881976a162340c" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "lazy_static", + "libsecp256k1", + "log", + "multiaddr 0.13.0", + "multihash 0.14.0", + "multistream-select 0.10.4", + "parking_lot 0.11.2", + "pin-project 1.1.3", + "prost 0.9.0", + "prost-build 0.9.0", + "rand 0.8.5", + "ring", + "rw-stream-sink 0.2.1", + "sha2 0.9.9", + "smallvec", + "thiserror", + "unsigned-varint", + "void", + "zeroize", +] + +[[package]] +name = "libp2p-core" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b02602099fb75cb2d16f9ea860a320d6eb82ce41e95ab680912c454805cd5" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "lazy_static", + "log", + "multiaddr 0.14.0", + "multihash 0.16.3", + "multistream-select 0.11.0", + "parking_lot 0.12.1", + "pin-project 1.1.3", + "prost 0.9.0", + "prost-build 0.9.0", + "rand 0.8.5", + "ring", + "rw-stream-sink 0.2.1", + "sha2 0.10.7", + "smallvec", + "thiserror", + "unsigned-varint", + "void", + "zeroize", +] + +[[package]] +name = "libp2p-core" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d46fca305dee6757022e2f5a4f6c023315084d0ed7441c3ab244e76666d979" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "lazy_static", + "libsecp256k1", + "log", + "multiaddr 0.14.0", + "multihash 0.16.3", + "multistream-select 0.11.0", + "parking_lot 0.12.1", + "pin-project 1.1.3", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.8.5", + "ring", + "rw-stream-sink 0.3.0", + "sha2 0.10.7", + "smallvec", + "thiserror", + "unsigned-varint", + "void", + "zeroize", +] + +[[package]] +name = "libp2p-deflate" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86adefc55ea4ed8201149f052fb441210727481dff1fb0b8318460206a79f5fb" +dependencies = [ + "flate2", + "futures", + "libp2p-core 0.33.0", +] + +[[package]] +name = "libp2p-dns" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb462ec3a51fab457b4b44ac295e8b0a4b04dc175127e615cf996b1f0f1a268" +dependencies = [ + "async-std-resolver", + "futures", + "libp2p-core 0.33.0", + "log", + "parking_lot 0.12.1", + "smallvec", + "trust-dns-resolver", +] + +[[package]] +name = "libp2p-floodsub" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a505d0c6f851cbf2919535150198e530825def8bd3757477f13dc3a57f46cbcc" +dependencies = [ + "cuckoofilter", + "fnv", + "futures", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.7.3", + "smallvec", +] + +[[package]] +name = "libp2p-gossipsub" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e064ba4d7832e01c738626c6b274ae100baba05f5ffcc7b265c2a3ed398108" +dependencies = [ + "asynchronous-codec", + "base64 0.13.1", + "byteorder", + "bytes", + "fnv", + "futures", + "hex_fmt", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "prometheus-client", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.7.3", + "regex", + "sha2 0.10.7", + "smallvec", + "unsigned-varint", + "wasm-timer", +] + +[[package]] +name = "libp2p-identify" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84b53490442d086db1fa5375670c9666e79143dccadef3f7c74a4346899a984" +dependencies = [ + "asynchronous-codec", + "futures", + "futures-timer", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "lru", + "prost 0.10.4", + "prost-build 0.10.4", + "prost-codec", + "smallvec", + "thiserror", + "void", +] + +[[package]] +name = "libp2p-kad" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6b5d4de90fcd35feb65ea6223fd78f3b747a64ca4b65e0813fbe66a27d56aa" +dependencies = [ + "arrayvec", + "asynchronous-codec", + "bytes", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.7.3", + "sha2 0.10.7", + "smallvec", + "thiserror", + "uint", + "unsigned-varint", + "void", +] + +[[package]] +name = "libp2p-mdns" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4783f8cf00c7b6c1ff0f1870b4fcf50b042b45533d2e13b6fb464caf447a6951" +dependencies = [ + "async-io", + "data-encoding", + "dns-parser", + "futures", + "if-watch", + "lazy_static", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "rand 0.8.5", + "smallvec", + "socket2 0.4.9", + "void", +] + +[[package]] +name = "libp2p-metrics" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "564a7e5284d7d9b3140fdfc3cb6567bc32555e86a21de5604c2ec85da05cf384" +dependencies = [ + "libp2p-core 0.33.0", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-kad", + "libp2p-ping", + "libp2p-relay", + "libp2p-swarm", + "prometheus-client", +] + +[[package]] +name = "libp2p-mplex" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ff9c893f2367631a711301d703c47432af898c9bb8253bea0e2c051a13f7640" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures", + "libp2p-core 0.33.0", + "log", + "nohash-hasher", + "parking_lot 0.12.1", + "rand 0.7.3", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "libp2p-noise" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2cee1dad1c83325bbd182a8e94555778699cec8a9da00086efb7522c4c15ad" +dependencies = [ + "bytes", + "curve25519-dalek 3.2.0", + "futures", + "lazy_static", + "libp2p-core 0.33.0", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.8.5", + "sha2 0.10.7", + "snow", + "static_assertions", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "libp2p-ping" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41516c82fe8dd148ec925eead0c5ec08a0628f7913597e93e126e4dfb4e0787" +dependencies = [ + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "rand 0.7.3", + "void", +] + +[[package]] +name = "libp2p-plaintext" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db007e737adc5d28b2e03223b0210164928ad742591127130796a72aa8eaf54f" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures", + "libp2p-core 0.33.0", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "unsigned-varint", + "void", +] + +[[package]] +name = "libp2p-pnet" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6468f382568da936b4fa1cff273ce59b1debf873ff5f4ca412c3b91d0b37442c" +dependencies = [ + "futures", + "log", + "pin-project 1.1.3", + "rand 0.8.5", + "salsa20", + "sha3", +] + +[[package]] +name = "libp2p-relay" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624ead3406f64437a0d4567c31bd128a9a0b8226d5f16c074038f5d0fc32f650" +dependencies = [ + "asynchronous-codec", + "bytes", + "either", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "pin-project 1.1.3", + "prost 0.10.4", + "prost-build 0.10.4", + "prost-codec", + "rand 0.8.5", + "smallvec", + "static_assertions", + "thiserror", + "void", +] + +[[package]] +name = "libp2p-rendezvous" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59967ea2db2c7560f641aa58ac05982d42131863fcd3dd6dcf0dd1daf81c60c" +dependencies = [ + "asynchronous-codec", + "bimap", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "prost 0.10.4", + "prost-build 0.10.4", + "rand 0.8.5", + "sha2 0.10.7", + "thiserror", + "unsigned-varint", + "void", +] + +[[package]] +name = "libp2p-request-response" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02e0acb725e5a757d77c96b95298fd73a7394fe82ba7b8bbeea510719cbe441" +dependencies = [ + "async-trait", + "bytes", + "futures", + "instant", + "libp2p-core 0.33.0", + "libp2p-swarm", + "log", + "rand 0.7.3", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "libp2p-swarm" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4bb21c5abadbf00360c734f16bf87f1712ed4f23cd46148f625d2ddb867346" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.33.0", + "log", + "pin-project 1.1.3", + "rand 0.7.3", + "smallvec", + "thiserror", + "void", +] + +[[package]] +name = "libp2p-swarm-derive" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f693c8c68213034d472cbb93a379c63f4f307d97c06f1c41e4985de481687a5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "libp2p-tcp" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4933e38ef21b50698aefc87799c24f2a365c9d3f6cf50471f3f6a0bc410892" +dependencies = [ + "async-io", + "futures", + "futures-timer", + "if-addrs 0.7.0", + "if-watch", + "ipnet", + "libc", + "libp2p-core 0.33.0", + "log", + "socket2 0.4.9", + "tokio", +] + +[[package]] +name = "libp2p-uds" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24bdab114f7f2701757d6541266e1131b429bbae382008f207f2114ee4222dcb" +dependencies = [ + "async-std", + "futures", + "libp2p-core 0.32.1", + "log", +] + +[[package]] +name = "libp2p-wasm-ext" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f066f2b8b1a1d64793f05da2256e6842ecd0293d6735ca2e9bda89831a1bdc06" +dependencies = [ + "futures", + "js-sys", + "libp2p-core 0.33.0", + "parity-send-wrapper", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "libp2p-websocket" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39d398fbb29f432c4128fabdaac2ed155c3bcaf1b9bd40eeeb10a471eefacbf5" +dependencies = [ + "either", + "futures", + "futures-rustls", + "libp2p-core 0.33.0", + "log", + "parking_lot 0.12.1", + "quicksink", + "rw-stream-sink 0.3.0", + "soketto", + "url", + "webpki-roots 0.22.6", +] + +[[package]] +name = "libp2p-yamux" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe653639ad74877c759720febb0cbcbf4caa221adde4eed2d3126ce5c6f381f" +dependencies = [ + "futures", + "libp2p-core 0.33.0", + "parking_lot 0.12.1", + "thiserror", + "yamux", +] + +[[package]] +name = "librocksdb-sys" +version = "0.11.0+8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "tikv-jemalloc-sys", +] + +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64 0.13.1", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libz-sys" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lighthouse_metrics" +version = "0.2.0" +dependencies = [ + "lazy_static", + "prometheus", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "log_entry_sync" +version = "0.1.0" +dependencies = [ + "anyhow", + "append_merkle", + "async-trait", + "contract-interface", + "ethereum-types 0.14.1", + "ethers", + "futures", + "futures-core", + "futures-util", + "jsonrpsee", + "serde_json", + "shared_types", + "storage", + "task_executor", + "thiserror", + "tokio", +] + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "merkle_light" +version = "0.4.0" +dependencies = [ + "rayon", +] + +[[package]] +name = "merkle_tree" +version = "0.1.0" +dependencies = [ + "hex", + "merkle_light", + "tiny-keccak", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miner" +version = "0.1.0" +dependencies = [ + "async-trait", + "blake2", + "contract-interface", + "ethereum-types 0.14.1", + "ethers", + "lazy_static", + "network", + "rand 0.8.5", + "shared_types", + "storage", + "task_executor", + "tiny-keccak", + "tokio", + "tracing", + "zgs_seal", + "zgs_spec", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "multiaddr" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ee4ea82141951ac6379f964f71b20876d43712bea8faf6dd1a375e08a46499" +dependencies = [ + "arrayref", + "bs58", + "byteorder", + "data-encoding", + "multihash 0.14.0", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + +[[package]] +name = "multiaddr" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c580bfdd8803cce319b047d239559a22f809094aaea4ac13902a1fdcfcd4261" +dependencies = [ + "arrayref", + "bs58", + "byteorder", + "data-encoding", + "multihash 0.16.3", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + +[[package]] +name = "multihash" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "752a61cd890ff691b4411423d23816d5866dd5621e4d1c5687a53b94b5a979d8" +dependencies = [ + "digest 0.9.0", + "generic-array", + "multihash-derive 0.7.2", + "sha2 0.9.9", + "unsigned-varint", +] + +[[package]] +name = "multihash" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c346cf9999c631f002d8f977c4eaeaa0e6386f16007202308d0b3757522c2cc" +dependencies = [ + "core2", + "digest 0.10.7", + "multihash-derive 0.8.1", + "sha2 0.10.7", + "unsigned-varint", +] + +[[package]] +name = "multihash-derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424f6e86263cd5294cbd7f1e95746b95aca0e0d66bff31e5a40d6baa87b4aa99" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "multihash-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6d4752e6230d8ef7adf7bd5d8c4b1f6561c1014c5ba9a37445ccefe18aa1db" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "multistream-select" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56a336acba8bc87c8876f6425407dbbe6c417bf478b22015f8fb0994ef3bc0ab" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project 1.1.3", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "multistream-select" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "363a84be6453a70e63513660f4894ef815daf88e3356bffcda9ca27d810ce83b" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project 1.1.3", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "netlink-packet-core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345b8ab5bd4e71a2986663e88c56856699d060e78e152e6e9d7966fcd5491297" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9ea4302b9759a7a88242299225ea3688e63c85ea136371bb6cf94fd674efaab" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror", +] + +[[package]] +name = "netlink-proto" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65b4b14489ab424703c092062176d52ba55485a89c076b4f9db05092b7223aa6" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror", + "tokio", +] + +[[package]] +name = "netlink-sys" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" +dependencies = [ + "async-io", + "bytes", + "futures", + "libc", + "log", +] + +[[package]] +name = "network" +version = "0.2.0" +dependencies = [ + "directory", + "dirs", + "discv5", + "error-chain", + "eth2_ssz", + "eth2_ssz_derive", + "eth2_ssz_types", + "ethereum-types 0.14.1", + "exit-future", + "fnv", + "futures", + "hashset_delay", + "hex", + "if-addrs 0.10.1", + "igd", + "lazy_static", + "libp2p", + "lighthouse_metrics", + "lru", + "parking_lot 0.12.1", + "prometheus-client", + "rand 0.8.5", + "regex", + "serde", + "serde_derive", + "sha2 0.10.7", + "shared_types", + "slog", + "smallvec", + "snap", + "strum 0.24.1", + "task_executor", + "tempfile", + "tiny-keccak", + "tokio", + "tokio-io-timeout", + "tokio-util 0.6.10", + "tracing", + "tracing-test", + "unsigned-varint", + "unused_port", + "void", + "zgs_version", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "static_assertions", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.2", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "open-fastrlp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", + "ethereum-types 0.14.1", + "open-fastrlp-derive", +] + +[[package]] +name = "open-fastrlp-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003b2be5c6c53c1cfeb0a238b8a1c3915cd410feb684457a36c10038f764bb1c" +dependencies = [ + "bytes", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "os_str_bytes" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owning_ref" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "packed_simd_2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" +dependencies = [ + "cfg-if", + "libm", +] + +[[package]] +name = "parity-scale-codec" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" +dependencies = [ + "arrayvec", + "bitvec 0.20.4", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive 2.3.1", + "serde", +] + +[[package]] +name = "parity-scale-codec" +version = "3.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8e946cc0cc711189c0b0249fb8b599cbeeab9784d83c415719368bb8d4ac64" +dependencies = [ + "arrayvec", + "bitvec 1.0.1", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive 3.6.4", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a296c3079b5fefbc499e1de58dc26c09b1b9a5952d26694ee89f04a43ebbb3e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parity-send-wrapper" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9777aa91b8ad9dd5aaa04a9b6bcb02c7f1deb952fca5a66034d5e63afc5c6f" + +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets 0.48.2", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", + "password-hash", + "sha2 0.10.7", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pest" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666d00490d4ac815001da55838c500eafb0320019bbaa44444137c48b443a853" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ca01446f50dbda87c1786af8770d535423fa8a53aec03b8f4e3d7eb10e0929" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "pest_meta" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.7", +] + +[[package]] +name = "petgraph" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +dependencies = [ + "fixedbitset", + "indexmap 1.9.3", +] + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a" +dependencies = [ + "pin-project-internal 0.4.30", +] + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal 1.1.3", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + +[[package]] +name = "pin-project-lite" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.8", + "spki 0.7.2", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "platforms" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite 0.2.12", + "windows-sys 0.48.0", +] + +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures 0.2.9", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.9", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +dependencies = [ + "proc-macro2", + "syn 2.0.28", +] + +[[package]] +name = "primitive-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" +dependencies = [ + "fixed-hash 0.7.0", + "impl-codec 0.5.1", + "impl-rlp", + "impl-serde 0.3.2", + "uint", +] + +[[package]] +name = "primitive-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +dependencies = [ + "fixed-hash 0.8.0", + "impl-codec 0.6.0", + "impl-rlp", + "impl-serde 0.4.0", + "scale-info", + "uint", +] + +[[package]] +name = "priority-queue" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff39edfcaec0d64e8d0da38564fad195d2d51b680940295fcc307366e101e61" +dependencies = [ + "autocfg", + "indexmap 1.9.3", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +dependencies = [ + "thiserror", + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot 0.12.1", + "protobuf", + "thiserror", +] + +[[package]] +name = "prometheus-client" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1abe0255c04d15f571427a2d1e00099016506cf3297b53853acd2b7eb87825" +dependencies = [ + "dtoa", + "itoa", + "owning_ref", + "prometheus-client-derive-text-encode", +] + +[[package]] +name = "prometheus-client-derive-text-encode" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e12d01b9d66ad9eb4529c57666b6263fc1993cb30261d83ead658fdd932652" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes", + "prost-derive 0.9.0", +] + +[[package]] +name = "prost" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" +dependencies = [ + "bytes", + "prost-derive 0.10.1", +] + +[[package]] +name = "prost-build" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" +dependencies = [ + "bytes", + "heck 0.3.3", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost 0.9.0", + "prost-types 0.9.0", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-build" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae5a4388762d5815a9fc0dea33c56b021cdc8dde0c55e0c9ca57197254b0cab" +dependencies = [ + "bytes", + "cfg-if", + "cmake", + "heck 0.4.1", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost 0.10.4", + "prost-types 0.10.1", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-codec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af1e92c33b4813cc79fda3f2dbf56af5169709be0202df730e9ebc3e4cd007" +dependencies = [ + "asynchronous-codec", + "bytes", + "prost 0.10.4", + "thiserror", + "unsigned-varint", +] + +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" +dependencies = [ + "bytes", + "prost 0.9.0", +] + +[[package]] +name = "prost-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" +dependencies = [ + "bytes", + "prost 0.10.4", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quicksink" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77de3c815e5a160b1539c6592796801df2043ae35e123b46d73380cfa57af858" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project-lite 0.1.12", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" + +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.10", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.10", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.3.6", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls 0.24.1", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite 0.2.12", + "rustls 0.21.6", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.22.6", + "winreg 0.10.1", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac 0.12.1", + "zeroize", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rlp-derive", + "rustc-hex", +] + +[[package]] +name = "rlp-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rocksdb" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + +[[package]] +name = "router" +version = "0.1.0" +dependencies = [ + "channel", + "chrono", + "error-chain", + "file_location_cache", + "futures", + "lazy_static", + "miner", + "network", + "rand 0.8.5", + "shared_types", + "storage", + "storage-async", + "sync", + "task_executor", + "tokio", + "tracing", +] + +[[package]] +name = "rpc" +version = "0.1.0" +dependencies = [ + "append_merkle", + "base64 0.13.1", + "chunk_pool", + "futures", + "futures-channel", + "jsonrpsee", + "merkle_light", + "merkle_tree", + "miner", + "network", + "serde", + "serde_json", + "shared_types", + "storage", + "storage-async", + "sync", + "task_executor", + "tokio", + "tracing", +] + +[[package]] +name = "rtnetlink" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322c53fd76a18698f1c27381d58091de3a043d356aa5bd0d510608b565f469a0" +dependencies = [ + "async-global-executor", + "futures", + "log", + "netlink-packet-route", + "netlink-proto", + "nix 0.24.3", + "thiserror", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust_decimal" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a2ab0025103a60ecaaf3abf24db1db240a4e1c15837090d2c32f625ac98abea" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.5", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.3", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "rw-stream-sink" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da5fcb054c46f5a5dff833b129285a93d3f0179531735e6c866e8cc307d2020" +dependencies = [ + "futures", + "pin-project 0.4.30", + "static_assertions", +] + +[[package]] +name = "rw-stream-sink" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26338f5e09bb721b85b135ea05af7767c90b52f6de4f087d4f4a3a9d64e7dc04" +dependencies = [ + "futures", + "pin-project 1.1.3", + "static_assertions", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scale-info" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0a159d0c45c12b20c5a844feb1fe4bea86e28f17b92a5f0c42193634d3782" +dependencies = [ + "cfg-if", + "derive_more", + "parity-scale-codec 3.6.4", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912e55f6d20e0e80d63733872b40e1227c0bce1e1ab81ba67d696339bfd7fd29" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" +dependencies = [ + "hmac 0.12.1", + "pbkdf2 0.11.0", + "salsa20", + "sha2 0.10.7", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.8", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +dependencies = [ + "serde", +] + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "serde_json" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.9", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.9", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.9", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.9", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_types" +version = "0.1.0" +dependencies = [ + "anyhow", + "append_merkle", + "chrono", + "eth2_ssz", + "eth2_ssz_derive", + "eth2_ssz_types", + "ethereum-types 0.14.1", + "merkle_light", + "merkle_tree", + "serde", + "tiny-keccak", + "tracing", + "typenum", + "zgs_spec", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "snap" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9f0ab6ef7eb7353d9119c170a436d1bf248eea575ac42d19d12f4e34130831" + +[[package]] +name = "snow" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek 4.0.0-rc.1", + "rand_core 0.6.4", + "ring", + "rustc_version", + "sha2 0.10.7", + "subtle", +] + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "soketto" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1c5305e39e09653383c2c7244f2f78b3bcae37cf50c64cb4789c9f5096ec2" +dependencies = [ + "base64 0.13.1", + "bytes", + "flate2", + "futures", + "httparse", + "log", + "rand 0.8.5", + "sha-1", +] + +[[package]] +name = "solang-parser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c792fe9fae2a2f716846f214ca10d5a1e21133e0bf36cef34bcc4a852467b21" +dependencies = [ + "itertools", + "lalrpop", + "lalrpop-util", + "phf", + "thiserror", + "unicode-xid", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der 0.7.8", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "storage" +version = "0.1.0" +dependencies = [ + "anyhow", + "append_merkle", + "bitmaps", + "criterion", + "eth2_ssz", + "eth2_ssz_derive", + "ethereum-types 0.14.1", + "hex", + "hex-literal", + "itertools", + "kvdb", + "kvdb-memorydb", + "kvdb-rocksdb", + "merkle_light", + "merkle_tree", + "rand 0.8.5", + "rayon", + "shared_types", + "static_assertions", + "tempdir", + "tiny-keccak", + "tracing", + "typenum", + "zgs_seal", + "zgs_spec", +] + +[[package]] +name = "storage-async" +version = "0.1.0" +dependencies = [ + "anyhow", + "shared_types", + "storage", + "task_executor", + "tokio", + "tracing", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot 0.12.1", + "phf_shared 0.10.0", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.2", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.28", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "svm-rs" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a04fc4f5cd35c700153b233f5575ccb3237e0f941fa5049d9e98254d10bf2fe" +dependencies = [ + "fs2", + "hex", + "home", + "once_cell", + "reqwest", + "semver", + "serde", + "serde_json", + "sha2 0.10.7", + "thiserror", + "url", + "zip", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync" +version = "0.1.0" +dependencies = [ + "anyhow", + "append_merkle", + "channel", + "duration-str", + "eth2_ssz", + "file_location_cache", + "libp2p", + "log_entry_sync", + "merkle_light", + "network", + "rand 0.8.5", + "serde", + "shared_types", + "storage", + "storage-async", + "task_executor", + "tokio", + "tracing", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target_info" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" + +[[package]] +name = "task_executor" +version = "0.1.0" +dependencies = [ + "exit-future", + "futures", + "lazy_static", + "lighthouse_metrics", + "tokio", + "tracing", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "tempfile" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" +dependencies = [ + "cfg-if", + "fastrand 2.0.0", + "redox_syscall 0.3.5", + "rustix 0.38.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9207952ae1a003f42d3d5e892dac3c6ba42aa6ac0c79a6a91a2b5cb4253e75c" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1728216d3244de4f14f14f8c15c79be1a7c67867d28d69b719690e2a19fb445" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tikv-jemalloc-sys" +version = "0.5.4+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9402443cb8fd499b6f327e40565234ff34dbda27460c5b47db0db77443dd85d1" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +dependencies = [ + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40de3a2ba249dcb097e01be5e67a5ff53cf250397715a071a81543e8a832a920" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite 0.2.12", + "signal-hook-registry", + "socket2 0.5.3", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite 0.2.12", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.8", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.6", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite 0.2.12", + "tokio", + "tokio-util 0.7.8", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c" +dependencies = [ + "futures-util", + "log", + "rustls 0.21.6", + "tokio", + "tokio-rustls 0.24.1", + "tungstenite", + "webpki-roots 0.23.1", +] + +[[package]] +name = "tokio-util" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "log", + "pin-project-lite 0.2.12", + "slab", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite 0.2.12", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite 0.2.12", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +dependencies = [ + "crossbeam-channel", + "time 0.3.25", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project 1.1.3", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a2c0ff408fe918a94c428a3f2ad04e4afd5c95bbc08fcf868eff750c15728a4" +dependencies = [ + "lazy_static", + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bc1c4f8e2e73a977812ab339d503e6feeb92700f6d07a6de4d321522d5c08" +dependencies = [ + "lazy_static", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tree_hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9c8a86fad3169a65aad2265d3c6a8bc119d0b771046af3c1b2fb0e9b12182b" +dependencies = [ + "eth2_hashing", + "ethereum-types 0.12.1", + "smallvec", +] + +[[package]] +name = "trust-dns-proto" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "log", + "rand 0.8.5", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot 0.12.1", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tungstenite" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.21.6", + "sha1", + "thiserror", + "url", + "utf-8", + "webpki", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "unsigned-varint" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86a8dc7f45e4c1b0d30e43038c38f274e77af056aa5f74b93c2cf9eb3c1c836" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures-io", + "futures-util", + "tokio-util 0.6.10", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "unused_port" +version = "0.1.0" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna 0.4.0", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.10", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" +dependencies = [ + "rustls-webpki 0.100.1", +] + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + +[[package]] +name = "wildmatch" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45296b64204227616fdbf2614cefa4c236b98ee64dfaaaa435207ed99fe7829f" +dependencies = [ + "windows_aarch64_msvc 0.34.0", + "windows_i686_gnu 0.34.0", + "windows_i686_msvc 0.34.0", + "windows_x86_64_gnu 0.34.0", + "windows_x86_64_msvc 0.34.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" +dependencies = [ + "windows_aarch64_gnullvm 0.48.2", + "windows_aarch64_msvc 0.48.2", + "windows_i686_gnu 0.48.2", + "windows_i686_msvc 0.48.2", + "windows_x86_64_gnu 0.48.2", + "windows_x86_64_gnullvm 0.48.2", + "windows_x86_64_msvc 0.48.2", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" + +[[package]] +name = "winnow" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e461589e194280efaa97236b73623445efa195aa633fd7004f39805707a9d53" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "ws_stream_wasm" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper 0.6.0", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +dependencies = [ + "curve25519-dalek 3.2.0", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "xml-rs" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yamux" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d9ba232399af1783a58d8eb26f6b5006fbefe2dc9ef36bd283324792d03ea5" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot 0.12.1", + "rand 0.8.5", + "static_assertions", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "zgs_node" +version = "0.1.0" +dependencies = [ + "anyhow", + "chunk_pool", + "clap", + "config", + "ctrlc", + "duration-str", + "error-chain", + "ethereum-types 0.14.1", + "exit-future", + "file_location_cache", + "futures", + "itertools", + "libp2p", + "log_entry_sync", + "miner", + "network", + "router", + "rpc", + "serde", + "shared_types", + "storage", + "storage-async", + "sync", + "task_executor", + "tokio", + "tokio-stream", + "toml 0.5.11", + "tracing", + "tracing-appender", + "tracing-subscriber", + "zgs_version", +] + +[[package]] +name = "zgs_seal" +version = "0.1.0" +dependencies = [ + "ethereum-types 0.14.1", + "tiny-keccak", + "zgs_spec", +] + +[[package]] +name = "zgs_spec" +version = "0.1.0" + +[[package]] +name = "zgs_version" +version = "0.1.0" +dependencies = [ + "git-version", + "regex", + "target_info", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes 0.8.3", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac 0.12.1", + "pbkdf2 0.11.0", + "sha1", + "time 0.3.25", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9d15452 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[workspace] + +members = [ + "common/channel", + "common/directory", + "common/hashset_delay", + "common/lighthouse_metrics", + "common/merkle_tree", + "common/task_executor", + "common/zgs_version", + "common/unused_port", + "common/append_merkle", + + "node", + "node/chunk_pool", + "node/file_location_cache", + "node/log_entry_sync", + "node/miner", + "node/network", + "node/router", + "node/rpc", + "node/shared_types", + "node/storage", + "node/storage-async", + "node/sync", +] +resolver = "2" + +[patch.crates-io] +discv5 = { path = "version-meld/discv5" } +eth2_ssz = { path = "version-meld/eth2_ssz" } +enr = { path = "version-meld/enr" } \ No newline at end of file diff --git a/LICENSE.lighthouse b/LICENSE.lighthouse new file mode 100644 index 0000000..98016b5 --- /dev/null +++ b/LICENSE.lighthouse @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Sigma Prime Pty Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..15ed7b8 --- /dev/null +++ b/NOTICE @@ -0,0 +1,6 @@ +ZeroGStorage +Copyright 2023 ZeroGStorage + +The Initial Developer of some parts of the framework, which are copied from, derived from, or +inspired by Lighthouse, is Sigma Prime Pty Ltd (https://sigmaprime.io). +Copyright 2018 - 2022 Sigma Prime Pty Ltd. \ No newline at end of file diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..a793f59 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true \ No newline at end of file diff --git a/common/append_merkle/Cargo.toml b/common/append_merkle/Cargo.toml new file mode 100644 index 0000000..37c0b3d --- /dev/null +++ b/common/append_merkle/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "append_merkle" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +ethereum-types = "0.14" +tiny-keccak = { version = "2.0.2", features = ["keccak"] } +eth2_ssz = "0.4.0" +eth2_ssz_derive = "0.3.0" +serde = { version = "1.0.137", features = ["derive"] } +lazy_static = "1.4.0" +tracing = "0.1.36" \ No newline at end of file diff --git a/common/append_merkle/src/lib.rs b/common/append_merkle/src/lib.rs new file mode 100644 index 0000000..72743fb --- /dev/null +++ b/common/append_merkle/src/lib.rs @@ -0,0 +1,641 @@ +mod merkle_tree; +mod proof; +mod sha3; + +use anyhow::{anyhow, bail, Result}; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::fmt::Debug; +use std::marker::PhantomData; +use tracing::warn; + +pub use crate::merkle_tree::{Algorithm, HashElement, MerkleTreeInitialData, MerkleTreeRead}; +pub use proof::{Proof, RangeProof}; +pub use sha3::Sha3Algorithm; + +pub struct AppendMerkleTree> { + /// Keep all the nodes in the latest version. `layers[0]` is the layer of leaves. + layers: Vec>, + /// Keep the delta nodes that can be used to construct a history tree. + /// The key is the root node of that version. + delta_nodes_map: HashMap>, + root_to_tx_seq_map: HashMap, + + /// For `last_chunk_merkle` after the first chunk, this is set to `Some(10)` so that + /// `revert_to` can reset the state correctly when needed. + min_depth: Option, + /// Used to compute the correct padding hash. + /// 0 for `pora_chunk_merkle` and 10 for not-first `last_chunk_merkle`. + leaf_height: usize, + _a: PhantomData, +} + +impl> AppendMerkleTree { + pub fn new(leaves: Vec, leaf_height: usize, start_tx_seq: Option) -> Self { + let mut merkle = Self { + layers: vec![leaves], + delta_nodes_map: HashMap::new(), + root_to_tx_seq_map: HashMap::new(), + min_depth: None, + leaf_height, + _a: Default::default(), + }; + if merkle.leaves() == 0 { + if let Some(seq) = start_tx_seq { + merkle.delta_nodes_map.insert( + seq, + DeltaNodes { + right_most_nodes: vec![], + }, + ); + } + return merkle; + } + // Reconstruct the whole tree. + merkle.recompute(0, 0, None); + // Commit the first version in memory. + // TODO(zz): Check when the roots become available. + merkle.commit(start_tx_seq); + merkle + } + + pub fn new_with_subtrees( + initial_data: MerkleTreeInitialData, + leaf_height: usize, + start_tx_seq: Option, + ) -> Result { + let mut merkle = Self { + layers: vec![vec![]], + delta_nodes_map: HashMap::new(), + root_to_tx_seq_map: HashMap::new(), + min_depth: None, + leaf_height, + _a: Default::default(), + }; + if initial_data.subtree_list.is_empty() { + if let Some(seq) = start_tx_seq { + merkle.delta_nodes_map.insert( + seq, + DeltaNodes { + right_most_nodes: vec![], + }, + ); + } + return Ok(merkle); + } + merkle.append_subtree_list(initial_data.subtree_list)?; + merkle.commit(start_tx_seq); + for (index, h) in initial_data.known_leaves { + merkle.fill_leaf(index, h); + } + Ok(merkle) + } + + /// This is only used for the last chunk, so `leaf_height` is always 0 so far. + pub fn new_with_depth(leaves: Vec, depth: usize, start_tx_seq: Option) -> Self { + if leaves.is_empty() { + // Create an empty merkle tree with `depth`. + let mut merkle = Self { + layers: vec![vec![]; depth], + delta_nodes_map: HashMap::new(), + root_to_tx_seq_map: HashMap::new(), + min_depth: Some(depth), + leaf_height: 0, + _a: Default::default(), + }; + if let Some(seq) = start_tx_seq { + merkle.delta_nodes_map.insert( + seq, + DeltaNodes { + right_most_nodes: vec![], + }, + ); + } + merkle + } else { + let mut layers = vec![vec![]; depth]; + layers[0] = leaves; + let mut merkle = Self { + layers, + delta_nodes_map: HashMap::new(), + root_to_tx_seq_map: HashMap::new(), + min_depth: Some(depth), + leaf_height: 0, + _a: Default::default(), + }; + // Reconstruct the whole tree. + merkle.recompute(0, 0, None); + // Commit the first version in memory. + merkle.commit(start_tx_seq); + merkle + } + } + + /// Return the new merkle root. + pub fn append(&mut self, new_leaf: E) { + self.layers[0].push(new_leaf); + self.recompute_after_append_leaves(self.leaves() - 1); + } + + pub fn append_list(&mut self, mut leaf_list: Vec) { + let start_index = self.leaves(); + self.layers[0].append(&mut leaf_list); + self.recompute_after_append_leaves(start_index); + } + + /// Append a leaf list by providing their intermediate node hash. + /// The appended subtree must be aligned. And it's up to the caller to + /// append the padding nodes for alignment. + /// Other nodes in the subtree will be set to `null` nodes. + /// TODO: Optimize to avoid storing the `null` nodes? + pub fn append_subtree(&mut self, subtree_depth: usize, subtree_root: E) -> Result<()> { + let start_index = self.leaves(); + self.append_subtree_inner(subtree_depth, subtree_root)?; + self.recompute_after_append_subtree(start_index, subtree_depth - 1); + Ok(()) + } + + pub fn append_subtree_list(&mut self, subtree_list: Vec<(usize, E)>) -> Result<()> { + for (subtree_depth, subtree_root) in subtree_list { + let start_index = self.leaves(); + self.append_subtree_inner(subtree_depth, subtree_root)?; + self.recompute_after_append_subtree(start_index, subtree_depth - 1); + } + Ok(()) + } + + /// Change the value of the last leaf and return the new merkle root. + /// This is needed if our merkle-tree in memory only keeps intermediate nodes instead of real leaves. + pub fn update_last(&mut self, updated_leaf: E) { + if self.layers[0].is_empty() { + // Special case for the first data. + self.layers[0].push(updated_leaf); + } else { + *self.layers[0].last_mut().unwrap() = updated_leaf; + } + self.recompute_after_append_leaves(self.leaves() - 1); + } + + /// Fill an unknown `null` leaf with its real value. + /// Panics if the leaf changes the merkle root or the index is out of range. + /// TODO: Batch computing intermediate nodes. + pub fn fill_leaf(&mut self, index: usize, leaf: E) { + if self.layers[0][index] == E::null() { + self.layers[0][index] = leaf; + self.recompute_after_fill_leaves(index, index + 1); + } else if self.layers[0][index] != leaf { + panic!("Fill with invalid leaf") + } + } + + pub fn gen_range_proof(&self, start_index: usize, end_index: usize) -> Result> { + if end_index <= start_index { + bail!( + "invalid proof range: start={} end={}", + start_index, + end_index + ); + } + // TODO(zz): Optimize range proof. + let left_proof = self.gen_proof(start_index)?; + let right_proof = self.gen_proof(end_index - 1)?; + Ok(RangeProof { + left_proof, + right_proof, + }) + } + + pub fn check_root(&self, root: &E) -> bool { + self.root_to_tx_seq_map.contains_key(root) + } + + pub fn leaf_at(&self, position: usize) -> Result> { + if position >= self.leaves() { + bail!("Out of bound: position={} end={}", position, self.leaves()); + } + if self.layers[0][position] != E::null() { + Ok(Some(self.layers[0][position].clone())) + } else { + // The leaf hash is unknown. + Ok(None) + } + } + + /// Return a list of subtrees that can be used to rebuild the tree. + pub fn get_subtrees(&self) -> Vec<(usize, E)> { + let mut next_index = 0; + let mut subtree_list: Vec<(usize, E)> = Vec::new(); + while next_index < self.leaves() { + let root_tuple = self.first_known_root_at(next_index); + let subtree_size = 1 << (root_tuple.0 - 1); + let root_start_index = next_index / subtree_size * subtree_size; + + // Previous subtrees are included within the new subtree. + // Pop them out and replace with the new one. + if root_start_index < next_index { + while let Some(last) = subtree_list.pop() { + next_index -= 1 << (last.0 - 1); + if next_index == root_start_index { + break; + } + } + } + next_index += subtree_size; + subtree_list.push(root_tuple); + } + subtree_list + } +} + +impl> AppendMerkleTree { + pub fn commit(&mut self, tx_seq: Option) { + if let Some(tx_seq) = tx_seq { + if self.leaves() == 0 { + // The state is empty, so we just save the root as `null`. + // Note that this root should not be used. + self.delta_nodes_map.insert( + tx_seq, + DeltaNodes { + right_most_nodes: vec![], + }, + ); + return; + } + let mut right_most_nodes = Vec::new(); + for layer in &self.layers { + right_most_nodes.push((layer.len() - 1, layer.last().unwrap().clone())); + } + let root = self.root().clone(); + assert_eq!(root, right_most_nodes.last().unwrap().1); + self.delta_nodes_map + .insert(tx_seq, DeltaNodes::new(right_most_nodes)); + self.root_to_tx_seq_map.insert(root, tx_seq); + } + } + + fn before_extend_layer(&mut self, height: usize) { + if height == self.layers.len() { + self.layers.push(Vec::new()); + } + } + + fn recompute_after_append_leaves(&mut self, start_index: usize) { + self.recompute(start_index, 0, None) + } + + fn recompute_after_append_subtree(&mut self, start_index: usize, height: usize) { + self.recompute(start_index, height, None) + } + + fn recompute_after_fill_leaves(&mut self, start_index: usize, end_index: usize) { + self.recompute(start_index, 0, Some(end_index)) + } + + /// Given a range of changed leaf nodes and recompute the tree. + /// Since this tree is append-only, we always compute to the end. + fn recompute( + &mut self, + mut start_index: usize, + mut height: usize, + mut maybe_end_index: Option, + ) { + start_index >>= height; + maybe_end_index = maybe_end_index.map(|end| end >> height); + // Loop until we compute the new root and reach `tree_depth`. + while self.layers[height].len() > 1 || height < self.layers.len() - 1 { + let next_layer_start_index = start_index >> 1; + if start_index % 2 == 1 { + start_index -= 1; + } + + let mut end_index = maybe_end_index.unwrap_or(self.layers[height].len()); + if end_index % 2 == 1 && end_index != self.layers[height].len() { + end_index += 1; + } + let mut i = 0; + let mut iter = self.layers[height][start_index..end_index].chunks_exact(2); + // We cannot modify the parent layer while iterating the child layer, + // so just keep the changes and update them later. + let mut parent_update = Vec::new(); + while let Some([left, right]) = iter.next() { + // If either left or right is null (unknown), we cannot compute the parent hash. + // Note that if we are recompute a range of an existing tree, + // we do not need to keep these possibly null parent. This is only saved + // for the case of constructing a new tree from the leaves. + let parent = if *left == E::null() || *right == E::null() { + E::null() + } else { + A::parent(left, right) + }; + parent_update.push((next_layer_start_index + i, parent)); + i += 1; + } + if let [r] = iter.remainder() { + // Same as above. + let parent = if *r == E::null() { + E::null() + } else { + A::parent_single(r, height + self.leaf_height) + }; + parent_update.push((next_layer_start_index + i, parent)); + } + if !parent_update.is_empty() { + self.before_extend_layer(height + 1); + } + // `parent_update` is in increasing order by `parent_index`, so + // we can just overwrite `last_changed_parent_index` with new values. + let mut last_changed_parent_index = None; + for (parent_index, parent) in parent_update { + match parent_index.cmp(&self.layers[height + 1].len()) { + Ordering::Less => { + // We do not overwrite with null. + if parent != E::null() { + if self.layers[height + 1][parent_index] == E::null() + // The last node in a layer can be updated. + || (self.layers[height + 1][parent_index] != parent + && parent_index == self.layers[height + 1].len() - 1) + { + self.layers[height + 1][parent_index] = parent; + last_changed_parent_index = Some(parent_index); + } else if self.layers[height + 1][parent_index] != parent { + // Recompute changes a node in the middle. This should be impossible + // if the inputs are valid. + panic!("Invalid append merkle tree! height={} index={} expected={:?} get={:?}", + height + 1, parent_index, self.layers[height + 1][parent_index], parent); + } + } + } + Ordering::Equal => { + self.layers[height + 1].push(parent); + last_changed_parent_index = Some(parent_index); + } + Ordering::Greater => { + unreachable!("depth={}, parent_index={}", height, parent_index); + } + } + } + if last_changed_parent_index.is_none() { + break; + } + maybe_end_index = last_changed_parent_index.map(|i| i + 1); + height += 1; + start_index = next_layer_start_index; + } + } + + fn append_subtree_inner(&mut self, subtree_depth: usize, subtree_root: E) -> Result<()> { + if subtree_depth == 0 { + bail!("Subtree depth should not be zero!"); + } + if self.leaves() % (1 << (subtree_depth - 1)) != 0 { + warn!( + "The current leaves count is not aligned with the merged subtree, \ + this is only possible during recovery, leaves={}", + self.leaves() + ); + } + for height in 0..(subtree_depth - 1) { + self.before_extend_layer(height); + let subtree_layer_size = 1 << (subtree_depth - 1 - height); + self.layers[height].append(&mut vec![E::null(); subtree_layer_size]); + } + self.before_extend_layer(subtree_depth - 1); + self.layers[subtree_depth - 1].push(subtree_root); + Ok(()) + } + + #[cfg(test)] + pub fn validate(&self, proof: &Proof, leaf: &E, position: usize) -> Result { + proof.validate::(leaf, position)?; + Ok(self.root_to_tx_seq_map.contains_key(&proof.root())) + } + + pub fn revert_to(&mut self, tx_seq: u64) -> Result<()> { + if self.layers[0].is_empty() { + // Any previous state of an empty tree is always empty. + return Ok(()); + } + let delta_nodes = self + .delta_nodes_map + .get(&tx_seq) + .ok_or_else(|| anyhow!("tx_seq unavailable, root={:?}", tx_seq))?; + // Dropping the upper layers that are not in the old merkle tree. + self.layers.truncate(delta_nodes.right_most_nodes.len()); + for (height, (last_index, right_most_node)) in + delta_nodes.right_most_nodes.iter().enumerate() + { + self.layers[height].truncate(*last_index + 1); + self.layers[height][*last_index] = right_most_node.clone(); + } + self.clear_after(tx_seq); + Ok(()) + } + + pub fn at_root_version(&self, root_hash: &E) -> Result> { + let tx_seq = self + .root_to_tx_seq_map + .get(root_hash) + .ok_or_else(|| anyhow!("old root unavailable, root={:?}", root_hash))?; + let delta_nodes = self + .delta_nodes_map + .get(tx_seq) + .ok_or_else(|| anyhow!("tx_seq unavailable, tx_seq={:?}", tx_seq))?; + if delta_nodes.height() == 0 { + bail!("empty tree"); + } + Ok(HistoryTree { + layers: &self.layers, + delta_nodes, + leaf_height: self.leaf_height, + }) + } + + pub fn reset(&mut self) { + self.layers = match self.min_depth { + None => vec![vec![]], + Some(depth) => vec![vec![]; depth], + }; + } + + fn clear_after(&mut self, tx_seq: u64) { + let mut tx_seq = tx_seq + 1; + while self.delta_nodes_map.contains_key(&tx_seq) { + if let Some(nodes) = self.delta_nodes_map.remove(&tx_seq) { + if nodes.height() != 0 { + self.root_to_tx_seq_map.remove(nodes.root()); + } + } + tx_seq += 1; + } + } + + /// Return the height and the root hash of the first available node from the leaf to the root. + /// The caller should ensure that `index` is within range. + fn first_known_root_at(&self, index: usize) -> (usize, E) { + let mut height = 0; + let mut index_in_layer = index; + while height < self.layers.len() { + let node = self.node(height, index_in_layer); + if !node.is_null() { + return (height + 1, node.clone()); + } + height += 1; + index_in_layer /= 2; + } + unreachable!("root is always available") + } +} + +#[derive(Clone, Debug)] +struct DeltaNodes { + /// The right most nodes in a layer and its position. + right_most_nodes: Vec<(usize, E)>, +} + +impl DeltaNodes { + fn new(right_most_nodes: Vec<(usize, E)>) -> Self { + Self { right_most_nodes } + } + + fn get(&self, height: usize, position: usize) -> Result> { + if height >= self.right_most_nodes.len() || position > self.right_most_nodes[height].0 { + Err(anyhow!("position out of tree range")) + } else if position == self.right_most_nodes[height].0 { + Ok(Some(&self.right_most_nodes[height].1)) + } else { + Ok(None) + } + } + + fn layer_len(&self, height: usize) -> usize { + self.right_most_nodes[height].0 + 1 + } + + fn height(&self) -> usize { + self.right_most_nodes.len() + } + + fn root(&self) -> &E { + &self.right_most_nodes.last().unwrap().1 + } +} + +pub struct HistoryTree<'m, E: HashElement> { + /// A reference to the global tree nodes. + layers: &'m Vec>, + /// The delta nodes that are difference from `layers`. + /// This could be a reference, we just take ownership for convenience. + delta_nodes: &'m DeltaNodes, + + leaf_height: usize, +} + +impl> MerkleTreeRead for AppendMerkleTree { + type E = E; + + fn node(&self, layer: usize, index: usize) -> &Self::E { + &self.layers[layer][index] + } + + fn height(&self) -> usize { + self.layers.len() + } + + fn layer_len(&self, layer_height: usize) -> usize { + self.layers[layer_height].len() + } + + fn padding_node(&self, height: usize) -> Self::E { + E::end_pad(height + self.leaf_height) + } +} + +impl<'a, E: HashElement> MerkleTreeRead for HistoryTree<'a, E> { + type E = E; + fn node(&self, layer: usize, index: usize) -> &Self::E { + match self.delta_nodes.get(layer, index).expect("range checked") { + Some(node) => node, + None => &self.layers[layer][index], + } + } + + fn height(&self) -> usize { + self.delta_nodes.height() + } + + fn layer_len(&self, layer_height: usize) -> usize { + self.delta_nodes.layer_len(layer_height) + } + + fn padding_node(&self, height: usize) -> Self::E { + E::end_pad(height + self.leaf_height) + } +} + +#[macro_export] +macro_rules! ensure_eq { + ($given:expr, $expected:expr) => { + ensure!( + $given == $expected, + format!( + "equal check fails! {}:{}: {}={:?}, {}={:?}", + file!(), + line!(), + stringify!($given), + $given, + stringify!($expected), + $expected, + ) + ); + }; +} + +#[cfg(test)] +mod tests { + use crate::merkle_tree::MerkleTreeRead; + use crate::sha3::Sha3Algorithm; + use crate::AppendMerkleTree; + use ethereum_types::H256; + + #[test] + fn test_proof() { + let n = [1, 2, 6, 1025]; + for entry_len in n { + let mut data = Vec::new(); + for _ in 0..entry_len { + data.push(H256::random()); + } + let mut merkle = + AppendMerkleTree::::new(vec![H256::zero()], 0, None); + merkle.append_list(data.clone()); + merkle.commit(Some(0)); + verify(&data, &merkle); + + data.push(H256::random()); + merkle.append(*data.last().unwrap()); + merkle.commit(Some(1)); + verify(&data, &merkle); + + for _ in 0..6 { + data.push(H256::random()); + } + merkle.append_list(data[data.len() - 6..].to_vec()); + merkle.commit(Some(2)); + verify(&data, &merkle); + } + } + + fn verify(data: &Vec, merkle: &AppendMerkleTree) { + for i in 0..data.len() { + let proof = merkle.gen_proof(i + 1).unwrap(); + let r = merkle.validate(&proof, &data[i], i + 1); + assert!(matches!(r, Ok(true)), "{:?}", r); + } + for i in (0..data.len()).step_by(6) { + let end = std::cmp::min(i + 3, data.len()); + let range_proof = merkle.gen_range_proof(i + 1, end + 1).unwrap(); + let r = range_proof.validate::(&data[i..end], i + 1); + assert!(r.is_ok(), "{:?}", r); + } + } +} diff --git a/common/append_merkle/src/merkle_tree.rs b/common/append_merkle/src/merkle_tree.rs new file mode 100644 index 0000000..5c822fb --- /dev/null +++ b/common/append_merkle/src/merkle_tree.rs @@ -0,0 +1,131 @@ +use crate::sha3::Sha3Algorithm; +use crate::Proof; +use anyhow::{bail, Result}; +use ethereum_types::H256; +use lazy_static::lazy_static; +use ssz::{Decode, Encode}; +use std::fmt::Debug; +use std::hash::Hash; +use tracing::trace; + +pub trait HashElement: + Clone + Debug + Eq + Hash + AsRef<[u8]> + AsMut<[u8]> + Decode + Encode + Send + Sync +{ + fn end_pad(height: usize) -> Self; + fn null() -> Self; + fn is_null(&self) -> bool { + self == &Self::null() + } +} + +impl HashElement for H256 { + fn end_pad(height: usize) -> Self { + ZERO_HASHES[height] + } + + fn null() -> Self { + H256::repeat_byte(1) + } +} + +lazy_static! { + static ref ZERO_HASHES: [H256; 64] = { + let leaf_zero_hash: H256 = Sha3Algorithm::leaf(&[0u8; 256]); + let mut list = [H256::zero(); 64]; + list[0] = leaf_zero_hash; + for i in 1..list.len() { + list[i] = Sha3Algorithm::parent(&list[i - 1], &list[i - 1]); + } + list + }; +} + +pub trait Algorithm { + fn parent(left: &E, right: &E) -> E; + fn parent_single(r: &E, height: usize) -> E { + Self::parent(r, &E::end_pad(height)) + } + fn leaf(data: &[u8]) -> E; +} + +pub trait MerkleTreeRead { + type E: HashElement; + fn node(&self, layer: usize, index: usize) -> &Self::E; + fn height(&self) -> usize; + fn layer_len(&self, layer_height: usize) -> usize; + fn padding_node(&self, height: usize) -> Self::E; + + fn leaves(&self) -> usize { + self.layer_len(0) + } + + fn root(&self) -> &Self::E { + self.node(self.height() - 1, 0) + } + + fn gen_proof(&self, leaf_index: usize) -> Result> { + if leaf_index >= self.leaves() { + bail!( + "leaf index out of bound: leaf_index={} total_leaves={}", + leaf_index, + self.leaves() + ); + } + if self.node(0, leaf_index) == &Self::E::null() { + bail!("Not ready to generate proof for leaf_index={}", leaf_index); + } + if self.height() == 1 { + return Ok(Proof::new( + vec![self.root().clone(), self.root().clone()], + vec![], + )); + } + let mut lemma: Vec = Vec::with_capacity(self.height()); // path + root + let mut path: Vec = Vec::with_capacity(self.height() - 2); // path - 1 + let mut index_in_layer = leaf_index; + lemma.push(self.node(0, leaf_index).clone()); + for height in 0..(self.height() - 1) { + trace!( + "gen_proof: height={} index={} hash={:?}", + height, + index_in_layer, + self.node(height, index_in_layer) + ); + if index_in_layer % 2 == 0 { + path.push(true); + if index_in_layer + 1 == self.layer_len(height) { + // TODO: This can be skipped if the tree size is available in validation. + lemma.push(self.padding_node(height)); + } else { + lemma.push(self.node(height, index_in_layer + 1).clone()); + } + } else { + path.push(false); + lemma.push(self.node(height, index_in_layer - 1).clone()); + } + index_in_layer >>= 1; + } + lemma.push(self.root().clone()); + Ok(Proof::new(lemma, path)) + } +} + +/// This includes the data to reconstruct an `AppendMerkleTree` root where some nodes +/// are `null`. Other intermediate nodes will be computed based on these known nodes. +pub struct MerkleTreeInitialData { + /// A list of `(subtree_depth, root)`. + /// The subtrees are continuous so we can compute the tree root with these subtree roots. + pub subtree_list: Vec<(usize, E)>, + /// A list of `(index, leaf_hash)`. + /// These leaves are in some large subtrees of `subtree_list`. 1-node subtrees are also leaves, + /// but they will not be duplicated in `known_leaves`. + pub known_leaves: Vec<(usize, E)>, +} + +impl MerkleTreeInitialData { + pub fn leaves(&self) -> usize { + self.subtree_list.iter().fold(0, |acc, (subtree_depth, _)| { + acc + (1 << (subtree_depth - 1)) + }) + } +} diff --git a/common/append_merkle/src/proof.rs b/common/append_merkle/src/proof.rs new file mode 100644 index 0000000..3c08f5f --- /dev/null +++ b/common/append_merkle/src/proof.rs @@ -0,0 +1,167 @@ +use crate::{ensure_eq, Algorithm, HashElement}; +use anyhow::{bail, ensure, Result}; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode as DeriveDecode, Encode as DeriveEncode}; + +#[derive(Clone, Debug, Eq, PartialEq, DeriveEncode, DeriveDecode, Deserialize, Serialize)] +pub struct Proof { + lemma: Vec, + path: Vec, +} + +impl Proof { + /// Creates new MT inclusion proof + pub fn new(hash: Vec, path: Vec) -> Proof { + assert_eq!(hash.len() - 2, path.len()); + Proof { lemma: hash, path } + } + + pub fn new_empty() -> Proof { + Proof { + lemma: vec![], + path: vec![], + } + } + + /// Return proof target leaf + pub fn item(&self) -> T { + self.lemma.first().unwrap().clone() + } + + /// Return tree root + pub fn root(&self) -> T { + self.lemma.last().unwrap().clone() + } + + /// Verifies MT inclusion proof + fn validate_integrity>(&self) -> bool { + let size = self.lemma.len(); + + if size < 2 { + return false; + } + let mut h = self.item(); + + for i in 1..size - 1 { + h = if self.path[i - 1] { + A::parent(&h, &self.lemma[i]) + } else { + A::parent(&self.lemma[i], &h) + }; + } + + h == self.root() + } + + pub fn validate>(&self, item: &T, position: usize) -> Result<()> { + if !self.validate_integrity::() { + bail!("Invalid proof"); + } + if *item != self.item() { + bail!("Proof item unmatch"); + } + if position != self.position() { + bail!("Proof position unmatch"); + } + Ok(()) + } + + /// Returns the path of this proof. + pub fn path(&self) -> &[bool] { + &self.path + } + + /// Returns the lemma of this proof. + pub fn lemma(&self) -> &[T] { + &self.lemma + } + + pub fn position(&self) -> usize { + let mut pos = 0; + for (i, is_left) in self.path.iter().enumerate() { + if !is_left { + pos += 1 << i; + } + } + pos + } +} + +#[derive(Clone, Debug, Eq, PartialEq, DeriveEncode, DeriveDecode, Deserialize, Serialize)] +pub struct RangeProof { + pub left_proof: Proof, + pub right_proof: Proof, +} + +impl RangeProof { + pub fn new_empty() -> Self { + Self { + left_proof: Proof::new_empty(), + right_proof: Proof::new_empty(), + } + } + + fn validate_integrity>(&self) -> bool { + self.left_proof.validate_integrity::() + && self.right_proof.validate_integrity::() + && self.left_proof.root() == self.right_proof.root() + && self.left_proof.path().len() == self.right_proof.path().len() + } + + pub fn root(&self) -> E { + self.left_proof.root() + } + + pub fn validate>( + &self, + range_leaves: &[E], + start_position: usize, + ) -> Result<()> { + if !self.validate_integrity::() { + bail!("Invalid range proof"); + } + if range_leaves.is_empty() { + bail!("Empty range"); + } + let end_position = start_position + range_leaves.len() - 1; + ensure_eq!(self.left_proof.item(), range_leaves[0]); + ensure_eq!( + self.right_proof.item(), + *range_leaves.last().expect("not empty") + ); + ensure_eq!(self.left_proof.position(), start_position); + ensure_eq!(self.right_proof.position(), end_position); + let tree_depth = self.left_proof.path().len() + 1; + // TODO: We can avoid copying the first layer. + let mut children_layer = range_leaves.to_vec(); + for height in 0..(tree_depth - 1) { + let mut parent_layer = Vec::new(); + let start_index = if !self.left_proof.path()[height] { + // If the left-most node is the right child, its sibling is not within the data range and should be retrieved from the proof. + let parent = A::parent(&self.left_proof.lemma()[height + 1], &children_layer[0]); + parent_layer.push(parent); + 1 + } else { + // The left-most node is the left child, its sibling is just the next child. + 0 + }; + let mut iter = children_layer[start_index..].chunks_exact(2); + while let Some([left, right]) = iter.next() { + parent_layer.push(A::parent(left, right)) + } + if let [right_most] = iter.remainder() { + if self.right_proof.path()[height] { + parent_layer.push(A::parent(right_most, &self.right_proof.lemma()[height + 1])); + } else { + bail!("Unexpected error"); + } + } + children_layer = parent_layer; + } + assert_eq!(children_layer.len(), 1); + let computed_root = children_layer.pop().unwrap(); + ensure_eq!(computed_root, self.root()); + + Ok(()) + } +} diff --git a/common/append_merkle/src/sha3.rs b/common/append_merkle/src/sha3.rs new file mode 100644 index 0000000..f7d983c --- /dev/null +++ b/common/append_merkle/src/sha3.rs @@ -0,0 +1,23 @@ +use crate::{Algorithm, HashElement}; +use tiny_keccak::{Hasher, Keccak}; + +pub struct Sha3Algorithm {} + +impl Algorithm for Sha3Algorithm { + fn parent(left: &E, right: &E) -> E { + let mut h = Keccak::v256(); + let mut e = E::null(); + h.update(left.as_ref()); + h.update(right.as_ref()); + h.finalize(e.as_mut()); + e + } + + fn leaf(data: &[u8]) -> E { + let mut h = Keccak::v256(); + let mut e = E::null(); + h.update(data.as_ref()); + h.finalize(e.as_mut()); + e + } +} diff --git a/common/channel/Cargo.toml b/common/channel/Cargo.toml new file mode 100644 index 0000000..cbdc369 --- /dev/null +++ b/common/channel/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "channel" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.19.2", features = ["sync", "time"] } diff --git a/common/channel/src/channel.rs b/common/channel/src/channel.rs new file mode 100644 index 0000000..2f5206a --- /dev/null +++ b/common/channel/src/channel.rs @@ -0,0 +1,112 @@ +use crate::error::Error; +use std::time::Duration; +use tokio::sync::mpsc::error::TryRecvError; +use tokio::sync::{mpsc, oneshot}; +use tokio::time::timeout; + +const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(3); + +pub type ResponseSender = oneshot::Sender; + +#[derive(Debug)] +pub enum Message { + Notification(N), + Request(Req, ResponseSender), +} + +pub struct Channel { + _phantom: std::marker::PhantomData<(N, Req, Res)>, +} + +impl Channel { + pub fn unbounded() -> (Sender, Receiver) { + let (sender, receiver) = mpsc::unbounded_channel(); + (Sender { chan: sender }, Receiver { chan: receiver }) + } +} + +pub struct Sender { + chan: mpsc::UnboundedSender>, +} + +impl Clone for Sender { + fn clone(&self) -> Self { + Sender { + chan: self.chan.clone(), + } + } +} + +impl Sender { + pub fn notify(&self, msg: N) -> Result<(), Error> { + self.chan + .send(Message::Notification(msg)) + .map_err(|e| Error::SendError(e)) + } + + pub async fn request(&self, request: Req) -> Result> { + let (sender, receiver) = oneshot::channel(); + + self.chan + .send(Message::Request(request, sender)) + .map_err(|e| Error::SendError(e))?; + + timeout(DEFAULT_REQUEST_TIMEOUT, receiver) + .await + .map_err(|_| Error::TimeoutError)? + .map_err(|e| Error::RecvError(e)) + } +} + +pub struct Receiver { + chan: mpsc::UnboundedReceiver>, +} + +impl Receiver { + pub async fn recv(&mut self) -> Option> { + self.chan.recv().await + } + + pub fn try_recv(&mut self) -> Result, TryRecvError> { + self.chan.try_recv() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug)] + enum Notification {} + + #[derive(Debug)] + enum Request { + GetNumber, + } + + #[derive(Debug, PartialEq, Eq)] + enum Response { + GetNumber(u32), + } + + #[tokio::test] + async fn request_response() { + let (tx, mut rx) = Channel::::unbounded(); + + let task1 = async move { + match rx.recv().await.expect("not dropped") { + Message::Notification(_) => {} + Message::Request(Request::GetNumber, sender) => { + sender.send(Response::GetNumber(42)).expect("not dropped"); + } + } + }; + + let task2 = async move { + let result = tx.request(Request::GetNumber).await.expect("not dropped"); + assert_eq!(result, Response::GetNumber(42)); + }; + + tokio::join!(task1, task2); + } +} diff --git a/common/channel/src/error.rs b/common/channel/src/error.rs new file mode 100644 index 0000000..2a76572 --- /dev/null +++ b/common/channel/src/error.rs @@ -0,0 +1,18 @@ +use crate::Message; +use std::fmt::{Debug, Display, Formatter}; +use tokio::sync::{mpsc, oneshot}; + +#[derive(Debug)] +pub enum Error { + SendError(mpsc::error::SendError>), + RecvError(oneshot::error::RecvError), + TimeoutError, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "ChannelError: {:?}", self) + } +} + +impl std::error::Error for Error {} diff --git a/common/channel/src/lib.rs b/common/channel/src/lib.rs new file mode 100644 index 0000000..f78f284 --- /dev/null +++ b/common/channel/src/lib.rs @@ -0,0 +1,5 @@ +mod channel; +pub mod error; +pub mod test_util; + +pub use crate::channel::{Channel, Message, Receiver, ResponseSender, Sender}; diff --git a/common/channel/src/test_util.rs b/common/channel/src/test_util.rs new file mode 100644 index 0000000..c796c0c --- /dev/null +++ b/common/channel/src/test_util.rs @@ -0,0 +1,55 @@ +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, + time::Duration, +}; + +use tokio::time::timeout; + +use crate::{Message, Receiver}; + +pub struct TestReceiver { + recv: Receiver, +} + +impl From> for TestReceiver { + fn from(recv: Receiver) -> Self { + Self { recv } + } +} + +impl Deref for TestReceiver { + type Target = Receiver; + + fn deref(&self) -> &Self::Target { + &self.recv + } +} + +impl DerefMut for TestReceiver { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.recv + } +} + +impl TestReceiver { + pub async fn expect_response(&mut self, response: Res) { + let request = timeout(Duration::from_secs(3), self.recv()) + .await + .expect("Timeout to receive request") + .expect("Channel closed"); + + match request { + Message::Notification(..) => panic!("Unexpected message type"), + Message::Request(_, resp_sender) => { + resp_sender.send(response).expect("Channel closed"); + } + } + } + + pub async fn expect_responses(&mut self, responses: Vec) { + for resp in responses { + self.expect_response(resp).await; + } + } +} diff --git a/common/contract-interface/Cargo.toml b/common/contract-interface/Cargo.toml new file mode 100644 index 0000000..f9a1564 --- /dev/null +++ b/common/contract-interface/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "contract-interface" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ethers = "^2" +serde_json = "1.0.82" + +[features] +compile-contracts = [] \ No newline at end of file diff --git a/common/contract-interface/build.rs b/common/contract-interface/build.rs new file mode 100644 index 0000000..0d405ff --- /dev/null +++ b/common/contract-interface/build.rs @@ -0,0 +1,30 @@ +use std::process::Command; + +const INSTALL_ERROR_MESSAGE: &str = + "Install dependencies for contract fail, try to run `yarn` in folder 'zerog-storage-contracts'"; +const COMPILE_ERROR_MESSAGE: &str = + "Compile solidity contracts fail, try to run `yarn compile` in folder 'zerog-storage-contracts'"; + +fn main() { + if cfg!(feature = "compile-contracts") { + println!("cargo:rerun-if-changed=../../zerog-storage-contracts/contracts/"); + println!("cargo:rerun-if-changed=../../zerog-storage-contracts/hardhat.config.ts"); + + let output = Command::new("yarn") + .arg("--cwd") + .arg("../../zerog-storage-contracts") + .status() + .expect(INSTALL_ERROR_MESSAGE); + assert!(output.success(), "{}", INSTALL_ERROR_MESSAGE); + + let output = Command::new("yarn") + .arg("--cwd") + .arg("../../zerog-storage-contracts") + .arg("compile") + .status() + .expect(COMPILE_ERROR_MESSAGE); + assert!(output.success(), "{}", COMPILE_ERROR_MESSAGE); + } else { + println!("cargo:rerun-if-changed=../../zerog-storage-contracts/artifacts/"); + } +} diff --git a/common/contract-interface/src/lib.rs b/common/contract-interface/src/lib.rs new file mode 100644 index 0000000..8750816 --- /dev/null +++ b/common/contract-interface/src/lib.rs @@ -0,0 +1,13 @@ +use ethers::prelude::abigen; + +// run `cargo doc -p contract-interface --open` to read struct definition + +abigen!( + ZgsFlow, + "../../zerog-storage-contracts/artifacts/contracts/dataFlow/Flow.sol/Flow.json" +); + +abigen!( + PoraMine, + "../../zerog-storage-contracts/artifacts/contracts/test/PoraMineTest.sol/PoraMineTest.json" +); diff --git a/common/directory/Cargo.toml b/common/directory/Cargo.toml new file mode 100644 index 0000000..3f95c1d --- /dev/null +++ b/common/directory/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "directory" +version = "0.1.0" +authors = ["pawan "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] \ No newline at end of file diff --git a/common/directory/src/lib.rs b/common/directory/src/lib.rs new file mode 100644 index 0000000..41acc3c --- /dev/null +++ b/common/directory/src/lib.rs @@ -0,0 +1,12 @@ +/// Names for the default directories. +pub const DEFAULT_ROOT_DIR: &str = ".lighthouse"; +pub const DEFAULT_BEACON_NODE_DIR: &str = "beacon"; +pub const DEFAULT_NETWORK_DIR: &str = "network"; +pub const DEFAULT_VALIDATOR_DIR: &str = "validators"; +pub const DEFAULT_SECRET_DIR: &str = "secrets"; +pub const DEFAULT_WALLET_DIR: &str = "wallets"; + +pub const DEFAULT_HARDCODED_NETWORK: &str = "mainnet"; + +/// Base directory name for unnamed testnets passed through the --testnet-dir flag +pub const CUSTOM_TESTNET_DIR: &str = "custom"; diff --git a/common/hashset_delay/Cargo.toml b/common/hashset_delay/Cargo.toml new file mode 100644 index 0000000..dcc2d7c --- /dev/null +++ b/common/hashset_delay/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "hashset_delay" +version = "0.2.0" +authors = ["Sigma Prime "] +edition = "2021" + +[dependencies] +futures = "0.3.21" +tokio-util = { version = "0.6.10", features = ["time"] } + +[dev-dependencies] +tokio = { version = "1.19.2", features = ["time", "rt-multi-thread", "macros"] } diff --git a/common/hashset_delay/src/hashset_delay.rs b/common/hashset_delay/src/hashset_delay.rs new file mode 100644 index 0000000..052d71f --- /dev/null +++ b/common/hashset_delay/src/hashset_delay.rs @@ -0,0 +1,197 @@ +//NOTE: This is just a specific case of a HashMapDelay. +// The code has been copied to make unique `insert` and `insert_at` functions. + +/// The default delay for entries, in seconds. This is only used when `insert()` is used to add +/// entries. +const DEFAULT_DELAY: u64 = 30; + +use futures::prelude::*; +use std::{ + collections::HashMap, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; +use tokio_util::time::delay_queue::{self, DelayQueue}; + +pub struct HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// The given entries. + entries: HashMap, + /// A queue holding the timeouts of each entry. + expirations: DelayQueue, + /// The default expiration timeout of an entry. + default_entry_timeout: Duration, +} + +/// A wrapping around entries that adds the link to the entry's expiration, via a `delay_queue` key. +struct MapEntry { + /// The expiration key for the entry. + key: delay_queue::Key, + /// The actual entry. + value: Instant, +} + +impl Default for HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + fn default() -> Self { + HashSetDelay::new(Duration::from_secs(DEFAULT_DELAY)) + } +} + +impl HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// Creates a new instance of `HashSetDelay`. + pub fn new(default_entry_timeout: Duration) -> Self { + HashSetDelay { + entries: HashMap::new(), + expirations: DelayQueue::new(), + default_entry_timeout, + } + } + + /// Insert an entry into the mapping. Entries will expire after the `default_entry_timeout`. + pub fn insert(&mut self, key: K) { + self.insert_at(key, self.default_entry_timeout); + } + + /// Inserts an entry that will expire at a given instant. If the entry already exists, the + /// timeout is updated. + pub fn insert_at(&mut self, key: K, entry_duration: Duration) { + if self.contains(&key) { + // update the timeout + self.update_timeout(&key, entry_duration); + } else { + let delay_key = self.expirations.insert(key.clone(), entry_duration); + let entry = MapEntry { + key: delay_key, + value: Instant::now() + entry_duration, + }; + self.entries.insert(key, entry); + } + } + + /// Gets a reference to an entry if it exists. + /// + /// Returns None if the entry does not exist. + pub fn get(&self, key: &K) -> Option<&Instant> { + self.entries.get(key).map(|entry| &entry.value) + } + + /// Returns true if the key exists, false otherwise. + pub fn contains(&self, key: &K) -> bool { + self.entries.contains_key(key) + } + + /// Returns the length of the mapping. + pub fn len(&self) -> usize { + self.entries.len() + } + + /// Checks if the mapping is empty. + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + /// Updates the timeout for a given key. Returns true if the key existed, false otherwise. + /// + /// Panics if the duration is too far in the future. + pub fn update_timeout(&mut self, key: &K, timeout: Duration) -> bool { + if let Some(entry) = self.entries.get(key) { + self.expirations.reset(&entry.key, timeout); + true + } else { + false + } + } + + /// Removes a key from the map returning the value associated with the key that was in the map. + /// + /// Return false if the key was not in the map. + pub fn remove(&mut self, key: &K) -> bool { + if let Some(entry) = self.entries.remove(key) { + self.expirations.remove(&entry.key); + return true; + } + false + } + + /// Retains only the elements specified by the predicate. + /// + /// In other words, remove all pairs `(k, v)` such that `f(&k,&mut v)` returns false. + pub fn retain bool>(&mut self, mut f: F) { + let expiration = &mut self.expirations; + self.entries.retain(|key, entry| { + let result = f(key); + if !result { + expiration.remove(&entry.key); + } + result + }) + } + + /// Removes all entries from the map. + pub fn clear(&mut self) { + self.entries.clear(); + self.expirations.clear(); + } + + /// Returns a vector of referencing all keys in the map. + pub fn keys(&self) -> impl Iterator { + self.entries.keys() + } +} + +impl Stream for HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + match self.expirations.poll_expired(cx) { + Poll::Ready(Some(Ok(key))) => match self.entries.remove(key.get_ref()) { + Some(_) => Poll::Ready(Some(Ok(key.into_inner()))), + None => Poll::Ready(Some(Err("Value no longer exists in expirations".into()))), + }, + Poll::Ready(Some(Err(e))) => { + Poll::Ready(Some(Err(format!("delay queue error: {:?}", e)))) + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +#[cfg(test)] + +mod tests { + use super::*; + + #[tokio::test] + async fn should_not_panic() { + let key = 2u8; + + let mut map = HashSetDelay::default(); + + map.insert(key); + map.update_timeout(&key, Duration::from_secs(100)); + + let fut = |cx: &mut Context| { + let _ = map.poll_next_unpin(cx); + let _ = map.poll_next_unpin(cx); + Poll::Ready(()) + }; + + future::poll_fn(fut).await; + + map.insert(key); + map.update_timeout(&key, Duration::from_secs(100)); + } +} diff --git a/common/hashset_delay/src/lib.rs b/common/hashset_delay/src/lib.rs new file mode 100644 index 0000000..175ad72 --- /dev/null +++ b/common/hashset_delay/src/lib.rs @@ -0,0 +1,12 @@ +//! This crate provides a single type (its counter-part HashMapDelay has been removed as it +//! currently is not in use in lighthouse): +//! - `HashSetDelay` +//! +//! # HashSetDelay +//! +//! This is similar to a `HashMapDelay` except the mapping maps to the expiry time. This +//! allows users to add objects and check their expiry deadlines before the `Stream` +//! consumes them. + +mod hashset_delay; +pub use crate::hashset_delay::HashSetDelay; diff --git a/common/lighthouse_metrics/Cargo.toml b/common/lighthouse_metrics/Cargo.toml new file mode 100644 index 0000000..9e1e88c --- /dev/null +++ b/common/lighthouse_metrics/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "lighthouse_metrics" +version = "0.2.0" +authors = ["Paul Hauner "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4.0" +prometheus = "0.13.1" diff --git a/common/lighthouse_metrics/src/lib.rs b/common/lighthouse_metrics/src/lib.rs new file mode 100644 index 0000000..98973de --- /dev/null +++ b/common/lighthouse_metrics/src/lib.rs @@ -0,0 +1,359 @@ +#![allow(clippy::needless_doctest_main)] +//! A wrapper around the `prometheus` crate that provides a global, `lazy_static` metrics registry +//! and functions to add and use the following components (more info at +//! [Prometheus docs](https://prometheus.io/docs/concepts/metric_types/)): +//! +//! - `Histogram`: used with `start_timer(..)` and `stop_timer(..)` to record durations (e.g., +//! block processing time). +//! - `IncCounter`: used to represent an ideally ever-growing, never-shrinking integer (e.g., +//! number of block processing requests). +//! - `IntGauge`: used to represent an varying integer (e.g., number of attestations per block). +//! +//! ## Important +//! +//! Metrics will fail if two items have the same `name`. All metrics must have a unique `name`. +//! Because we use a global registry there is no namespace per crate, it's one big global space. +//! +//! See the [Prometheus naming best practices](https://prometheus.io/docs/practices/naming/) when +//! choosing metric names. +//! +//! ## Example +//! +//! ```rust +//! #[macro_use] +//! extern crate lazy_static; +//! use lighthouse_metrics::*; +//! +//! // These metrics are "magically" linked to the global registry defined in `lighthouse_metrics`. +//! lazy_static! { +//! pub static ref RUN_COUNT: Result = try_create_int_counter( +//! "runs_total", +//! "Total number of runs" +//! ); +//! pub static ref CURRENT_VALUE: Result = try_create_int_gauge( +//! "current_value", +//! "The current value" +//! ); +//! pub static ref RUN_TIME: Result = +//! try_create_histogram("run_seconds", "Time taken (measured to high precision)"); +//! } +//! +//! +//! fn main() { +//! for i in 0..100 { +//! inc_counter(&RUN_COUNT); +//! let timer = start_timer(&RUN_TIME); +//! +//! for j in 0..10 { +//! set_gauge(&CURRENT_VALUE, j); +//! println!("Howdy partner"); +//! } +//! +//! stop_timer(timer); +//! } +//! } +//! ``` + +use prometheus::{HistogramOpts, Opts}; +use std::time::Duration; + +use prometheus::core::{Atomic, GenericGauge, GenericGaugeVec}; +pub use prometheus::{ + proto::{Metric, MetricFamily, MetricType}, + Encoder, Gauge, GaugeVec, Histogram, HistogramTimer, HistogramVec, IntCounter, IntCounterVec, + IntGauge, IntGaugeVec, Result, TextEncoder, +}; + +/// Collect all the metrics for reporting. +pub fn gather() -> Vec { + prometheus::gather() +} + +/// Attempts to create an `IntCounter`, returning `Err` if the registry does not accept the counter +/// (potentially due to naming conflict). +pub fn try_create_int_counter(name: &str, help: &str) -> Result { + let opts = Opts::new(name, help); + let counter = IntCounter::with_opts(opts)?; + prometheus::register(Box::new(counter.clone()))?; + Ok(counter) +} + +/// Attempts to create an `IntGauge`, returning `Err` if the registry does not accept the counter +/// (potentially due to naming conflict). +pub fn try_create_int_gauge(name: &str, help: &str) -> Result { + let opts = Opts::new(name, help); + let gauge = IntGauge::with_opts(opts)?; + prometheus::register(Box::new(gauge.clone()))?; + Ok(gauge) +} + +/// Attempts to create a `Gauge`, returning `Err` if the registry does not accept the counter +/// (potentially due to naming conflict). +pub fn try_create_float_gauge(name: &str, help: &str) -> Result { + let opts = Opts::new(name, help); + let gauge = Gauge::with_opts(opts)?; + prometheus::register(Box::new(gauge.clone()))?; + Ok(gauge) +} + +/// Attempts to create a `Histogram`, returning `Err` if the registry does not accept the counter +/// (potentially due to naming conflict). +pub fn try_create_histogram(name: &str, help: &str) -> Result { + let opts = HistogramOpts::new(name, help); + let histogram = Histogram::with_opts(opts)?; + prometheus::register(Box::new(histogram.clone()))?; + Ok(histogram) +} + +/// Attempts to create a `HistogramVec`, returning `Err` if the registry does not accept the counter +/// (potentially due to naming conflict). +pub fn try_create_histogram_vec( + name: &str, + help: &str, + label_names: &[&str], +) -> Result { + let opts = HistogramOpts::new(name, help); + let histogram_vec = HistogramVec::new(opts, label_names)?; + prometheus::register(Box::new(histogram_vec.clone()))?; + Ok(histogram_vec) +} + +/// Attempts to create a `IntGaugeVec`, returning `Err` if the registry does not accept the gauge +/// (potentially due to naming conflict). +pub fn try_create_int_gauge_vec( + name: &str, + help: &str, + label_names: &[&str], +) -> Result { + let opts = Opts::new(name, help); + let counter_vec = IntGaugeVec::new(opts, label_names)?; + prometheus::register(Box::new(counter_vec.clone()))?; + Ok(counter_vec) +} + +/// Attempts to create a `GaugeVec`, returning `Err` if the registry does not accept the gauge +/// (potentially due to naming conflict). +pub fn try_create_float_gauge_vec( + name: &str, + help: &str, + label_names: &[&str], +) -> Result { + let opts = Opts::new(name, help); + let counter_vec = GaugeVec::new(opts, label_names)?; + prometheus::register(Box::new(counter_vec.clone()))?; + Ok(counter_vec) +} + +/// Attempts to create a `IntCounterVec`, returning `Err` if the registry does not accept the gauge +/// (potentially due to naming conflict). +pub fn try_create_int_counter_vec( + name: &str, + help: &str, + label_names: &[&str], +) -> Result { + let opts = Opts::new(name, help); + let counter_vec = IntCounterVec::new(opts, label_names)?; + prometheus::register(Box::new(counter_vec.clone()))?; + Ok(counter_vec) +} + +/// If `int_gauge_vec.is_ok()`, returns a gauge with the given `name`. +pub fn get_int_gauge(int_gauge_vec: &Result, name: &[&str]) -> Option { + if let Ok(int_gauge_vec) = int_gauge_vec { + Some(int_gauge_vec.get_metric_with_label_values(name).ok()?) + } else { + None + } +} + +pub fn get_gauge( + gauge_vec: &Result>, + name: &[&str], +) -> Option> { + if let Ok(gauge_vec) = gauge_vec { + Some(gauge_vec.get_metric_with_label_values(name).ok()?) + } else { + None + } +} + +pub fn set_gauge_entry( + gauge_vec: &Result>, + name: &[&str], + value: P::T, +) { + if let Some(v) = get_gauge(gauge_vec, name) { + v.set(value) + }; +} + +/// If `int_gauge_vec.is_ok()`, sets the gauge with the given `name` to the given `value` +/// otherwise returns false. +pub fn set_int_gauge(int_gauge_vec: &Result, name: &[&str], value: i64) -> bool { + if let Ok(int_gauge_vec) = int_gauge_vec { + int_gauge_vec + .get_metric_with_label_values(name) + .map(|v| { + v.set(value); + true + }) + .unwrap_or_else(|_| false) + } else { + false + } +} + +/// If `int_counter_vec.is_ok()`, returns a counter with the given `name`. +pub fn get_int_counter( + int_counter_vec: &Result, + name: &[&str], +) -> Option { + if let Ok(int_counter_vec) = int_counter_vec { + Some(int_counter_vec.get_metric_with_label_values(name).ok()?) + } else { + None + } +} + +/// Increments the `int_counter_vec` with the given `name`. +pub fn inc_counter_vec(int_counter_vec: &Result, name: &[&str]) { + if let Some(counter) = get_int_counter(int_counter_vec, name) { + counter.inc() + } +} + +pub fn inc_counter_vec_by(int_counter_vec: &Result, name: &[&str], amount: u64) { + if let Some(counter) = get_int_counter(int_counter_vec, name) { + counter.inc_by(amount); + } +} + +/// If `histogram_vec.is_ok()`, returns a histogram with the given `name`. +pub fn get_histogram(histogram_vec: &Result, name: &[&str]) -> Option { + if let Ok(histogram_vec) = histogram_vec { + Some(histogram_vec.get_metric_with_label_values(name).ok()?) + } else { + None + } +} + +/// Starts a timer on `vec` with the given `name`. +pub fn start_timer_vec(vec: &Result, name: &[&str]) -> Option { + get_histogram(vec, name).map(|h| h.start_timer()) +} + +/// Starts a timer for the given `Histogram`, stopping when it gets dropped or given to `stop_timer(..)`. +pub fn start_timer(histogram: &Result) -> Option { + if let Ok(histogram) = histogram { + Some(histogram.start_timer()) + } else { + None + } +} + +/// Starts a timer on `vec` with the given `name`. +pub fn observe_timer_vec(vec: &Result, name: &[&str], duration: Duration) { + if let Some(h) = get_histogram(vec, name) { + h.observe(duration_to_f64(duration)) + } +} + +/// Stops a timer created with `start_timer(..)`. +pub fn stop_timer(timer: Option) { + if let Some(t) = timer { + t.observe_duration() + } +} + +pub fn inc_counter(counter: &Result) { + if let Ok(counter) = counter { + counter.inc(); + } +} + +pub fn inc_counter_by(counter: &Result, value: u64) { + if let Ok(counter) = counter { + counter.inc_by(value); + } +} + +pub fn set_gauge_vec(int_gauge_vec: &Result, name: &[&str], value: i64) { + if let Some(gauge) = get_int_gauge(int_gauge_vec, name) { + gauge.set(value); + } +} + +pub fn inc_gauge_vec(int_gauge_vec: &Result, name: &[&str]) { + if let Some(gauge) = get_int_gauge(int_gauge_vec, name) { + gauge.inc(); + } +} + +pub fn dec_gauge_vec(int_gauge_vec: &Result, name: &[&str]) { + if let Some(gauge) = get_int_gauge(int_gauge_vec, name) { + gauge.dec(); + } +} + +pub fn set_gauge(gauge: &Result, value: i64) { + if let Ok(gauge) = gauge { + gauge.set(value); + } +} + +pub fn set_float_gauge(gauge: &Result, value: f64) { + if let Ok(gauge) = gauge { + gauge.set(value); + } +} + +pub fn set_float_gauge_vec(gauge_vec: &Result, name: &[&str], value: f64) { + if let Some(gauge) = get_gauge(gauge_vec, name) { + gauge.set(value); + } +} + +pub fn inc_gauge(gauge: &Result) { + if let Ok(gauge) = gauge { + gauge.inc(); + } +} + +pub fn dec_gauge(gauge: &Result) { + if let Ok(gauge) = gauge { + gauge.dec(); + } +} + +pub fn maybe_set_gauge(gauge: &Result, value_opt: Option) { + if let Some(value) = value_opt { + set_gauge(gauge, value) + } +} + +pub fn maybe_set_float_gauge(gauge: &Result, value_opt: Option) { + if let Some(value) = value_opt { + set_float_gauge(gauge, value) + } +} + +/// Sets the value of a `Histogram` manually. +pub fn observe(histogram: &Result, value: f64) { + if let Ok(histogram) = histogram { + histogram.observe(value); + } +} + +pub fn observe_duration(histogram: &Result, duration: Duration) { + if let Ok(histogram) = histogram { + histogram.observe(duration_to_f64(duration)) + } +} + +fn duration_to_f64(duration: Duration) -> f64 { + // This conversion was taken from here: + // + // https://docs.rs/prometheus/0.5.0/src/prometheus/histogram.rs.html#550-555 + let nanos = f64::from(duration.subsec_nanos()) / 1e9; + duration.as_secs() as f64 + nanos +} diff --git a/common/merkle_light/Cargo.toml b/common/merkle_light/Cargo.toml new file mode 100644 index 0000000..aa4946c --- /dev/null +++ b/common/merkle_light/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "merkle_light" +version = "0.4.0" +authors = [ + "Ivan Prisyazhnyy " +] +edition = "2021" + +description = "Light merkle tree implementation with SPV support and dependency agnostic." +license = "BSD-3-Clause" +homepage = "https://github.com/sitano/merkle_light" +repository = "https://github.com/sitano/merkle_light" +documentation = "https://sitano.github.io/merkle_light/merkle_light/index.html" +readme = "README.md" +keywords = ["merkle", "merkle-tree", "no_std"] +categories = ["data-structures", "cryptography"] + +[dependencies] + +rayon = "1.5.3" + +[dev-dependencies] + +[features] +default = ["std"] +std = [] + +[package.metadata.release] +sign-commit = true +upload-doc = true +doc-branch = "gh-pages" +pre-release-commit-message = "Release version {{version}}." +pro-release-commit-message = "Start next development iteration {{version}}." +tag-message = "Release version {{version}}." +doc-commit-message = "Update documentation." +dev-version-ext = "pre" diff --git a/common/merkle_light/README.md b/common/merkle_light/README.md new file mode 100644 index 0000000..ae6597f --- /dev/null +++ b/common/merkle_light/README.md @@ -0,0 +1,113 @@ +# merkle + +[![Build Status](https://travis-ci.org/sitano/merkle_light.svg?branch=master&style=flat)](https://travis-ci.org/sitano/merkle_light) +[![Issues](http://img.shields.io/github/issues/sitano/merkle.svg?style=flat)](https://github.com/sitano/merkle_light/issues) +![License](https://img.shields.io/badge/license-bsd3-brightgreen.svg?style=flat) +[![Crates.io](https://img.shields.io/crates/v/merkle_light.svg)](https://crates.io/crates/merkle_light) + +*merkle* is a lightweight Rust implementation of a [Merkle tree](https://en.wikipedia.org/wiki/Merkle_tree). + +## Features + +- external dependency agnostic +- `core::hash::Hasher` compatibility +- standard types hasher implementations +- `#[derive(Hashable)]` support for simple struct +- customizable merkle leaf/node hashing algorithm +- support for custom hash types (e.g. [u8; 16], [u64; 4], [u128; 2], struct) +- customizable hashing algorithm +- linear memory layout, no nodes on heap +- buildable from iterator, objects or hashes +- certificate transparency style merkle hashing support +- SPV included + +## Documentation + +Documentation is [available](https://sitano.github.io/merkle_light/merkle_light/index.html). + +# Examples + +* `test_sip.rs`: algorithm implementation example for std sip hasher, u64 hash items +* `test_xor128.rs`: custom hash example xor128 +* `test_cmh.rs`: custom merkle hasher implementation example +* `crypto_bitcoin_mt.rs`: bitcoin merkle tree using crypto lib +* `crypto_chaincore_mt.rs`: chain core merkle tree using crypto lib +* `ring_bitcoin_mt.rs`: bitcoin merkle tree using ring lib + +# Quick start + +``` +extern crate crypto; +extern crate merkle_light; + +use std::fmt; +use std::hash::Hasher; +use std::iter::FromIterator; +use crypto::sha3::{Sha3, Sha3Mode}; +use crypto::digest::Digest; +use merkle_light::hash::{Algorithm, Hashable}; +use merkle_light::merkle::MerkleTree; + +pub struct ExampleAlgorithm(Sha3); + +impl ExampleAlgorithm { + pub fn new() -> ExampleAlgorithm { + ExampleAlgorithm(Sha3::new(Sha3Mode::Sha3_256)) + } +} + +impl Default for ExampleAlgorithm { + fn default() -> ExampleAlgorithm { + ExampleAlgorithm::new() + } +} + +impl Hasher for ExampleAlgorithm { + #[inline] + fn write(&mut self, msg: &[u8]) { + self.0.input(msg) + } + + #[inline] + fn finish(&self) -> u64 { + unimplemented!() + } +} + +impl Algorithm<[u8; 32]> for ExampleAlgorithm { + #[inline] + fn hash(&mut self) -> [u8; 32] { + let mut h = [0u8; 32]; + self.0.result(&mut h); + h + } + + #[inline] + fn reset(&mut self) { + self.0.reset(); + } +} + +fn main() { + let mut h1 = [0u8; 32]; + let mut h2 = [0u8; 32]; + let mut h3 = [0u8; 32]; + h1[0] = 0x11; + h2[0] = 0x22; + h3[0] = 0x33; + + let t: MerkleTree<[u8; 32], ExampleAlgorithm> = MerkleTree::from_iter(vec![h1, h2, h3]); + println!("{:?}", t.root()); +} +``` + +## Bug Reporting + +Please report bugs either as pull requests or as issues in [the issue +tracker](https://github.com/sitano/merkle_light). *merkle* has a +**full disclosure** vulnerability policy. **Please do NOT attempt to report +any security vulnerability in this code privately to anybody.** + +## License + +See [LICENSE](LICENSE). diff --git a/common/merkle_light/benches/crypto_sha512.rs b/common/merkle_light/benches/crypto_sha512.rs new file mode 100644 index 0000000..23aa2a5 --- /dev/null +++ b/common/merkle_light/benches/crypto_sha512.rs @@ -0,0 +1,170 @@ +//! cargo bench --features "crypto_bench" --verbose +#![cfg(feature = "crypto_bench")] +#![feature(test)] +#![feature(rand)] +#![feature(crypto)] + +mod hash512; + +extern crate crypto; +extern crate merkle_light; +extern crate rand; +extern crate test; + +use crypto::digest::Digest; +use crypto::sha2::Sha512; +use hash512::Hash512; +use merkle_light::hash::{Algorithm, Hashable}; +use merkle_light::merkle::MerkleTree; +use rand::Rng; +use std::hash::Hasher; +use std::iter::FromIterator; +use test::Bencher; + +#[derive(Copy, Clone)] +struct A(Sha512); + +impl A { + fn new() -> A { + A(Sha512::new()) + } +} + +impl Default for A { + fn default() -> Self { + A::new() + } +} + +impl Hasher for A { + #[inline] + fn write(&mut self, msg: &[u8]) { + self.0.input(msg) + } + + #[inline] + fn finish(&self) -> u64 { + unimplemented!() + } +} + +impl Algorithm for A { + #[inline] + fn hash(&mut self) -> Hash512 { + let mut h = [0u8; 64]; + self.0.result(&mut h); + Hash512(h) + } + + #[inline] + fn reset(&mut self) { + self.0.reset(); + } +} + +fn tree_5() -> Vec { + ["one", "two", "three", "four"] + .iter() + .map(|x| { + let mut a = A::new(); + Hashable::hash(x, &mut a); + a.hash() + }) + .collect::>() +} + +fn tree_160() -> Vec { + let mut values = vec![vec![0u8; 256]; 160]; + let mut rng = rand::IsaacRng::new_unseeded(); + + for mut v in &mut values { + rng.fill_bytes(&mut v); + } + + values + .iter() + .map(|x| { + let mut a = A::new(); + a.write(x.as_ref()); + a.hash() + }) + .collect::>() +} + +#[bench] +fn bench_crypto_sha512(b: &mut Bencher) { + let mut h = [0u8; 64]; + b.iter(|| { + let mut x = Sha512::new(); + x.input("12345".as_ref()); + x.result(&mut h); + }); +} + +#[bench] +fn bench_crypto_sha512_from_data_5(b: &mut Bencher) { + let values = tree_5(); + b.iter(|| MerkleTree::::from_iter(values.clone())); +} + +#[bench] +fn bench_crypto_sha512_from_data_5_proof(b: &mut Bencher) { + let values = tree_5(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + + b.iter(|| { + for i in 0..values.len() { + let proof = tree.gen_proof(i); + test::black_box(proof); + } + }); +} + +#[bench] +fn bench_crypto_sha512_from_data_5_proof_check(b: &mut Bencher) { + let values = tree_5(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + let proofs = (0..values.len()) + .map(|i| tree.gen_proof(i)) + .collect::>(); + + b.iter(|| { + for proof in &proofs { + test::black_box(proof.validate::()); + } + }); +} + +#[bench] +fn bench_crypto_sha512_from_data_160(b: &mut Bencher) { + let values = tree_160(); + b.iter(|| MerkleTree::::from_iter(values.clone())); +} + +#[bench] +fn bench_crypto_sha512_from_data_160_proof(b: &mut Bencher) { + let values = tree_160(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + + b.iter(|| { + for i in 0..values.len() { + let proof = tree.gen_proof(i); + test::black_box(proof); + } + }); +} + +#[bench] +fn bench_crypto_sha512_from_data_160_proof_check(b: &mut Bencher) { + let values = tree_160(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + let proofs = (0..values.len()) + .map(|i| tree.gen_proof(i)) + .collect::>(); + + b.iter(|| { + for proof in &proofs { + test::black_box(proof.validate::()); + } + }); +} diff --git a/common/merkle_light/benches/hash512/hash512.rs b/common/merkle_light/benches/hash512/hash512.rs new file mode 100644 index 0000000..90d3e02 --- /dev/null +++ b/common/merkle_light/benches/hash512/hash512.rs @@ -0,0 +1,58 @@ +use std::cmp::Ordering; + +#[derive(Copy, Clone)] +pub struct Hash512(pub [u8; 64]); + +impl Default for Hash512 { + fn default() -> Self { + Hash512([0u8; 64]) + } +} + +impl AsRef<[u8]> for Hash512 { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl PartialOrd for Hash512 { + #[inline] + fn partial_cmp(&self, other: &Hash512) -> Option { + PartialOrd::partial_cmp(&&self.0[..], &&other.0[..]) + } + + #[inline] + fn lt(&self, other: &Hash512) -> bool { + PartialOrd::lt(&&self.0[..], &&other.0[..]) + } + + #[inline] + fn le(&self, other: &Hash512) -> bool { + PartialOrd::le(&&self.0[..], &&other.0[..]) + } + + #[inline] + fn ge(&self, other: &Hash512) -> bool { + PartialOrd::ge(&&self.0[..], &&other.0[..]) + } + + #[inline] + fn gt(&self, other: &Hash512) -> bool { + PartialOrd::gt(&&self.0[..], &&other.0[..]) + } +} + +impl Ord for Hash512 { + #[inline] + fn cmp(&self, other: &Hash512) -> Ordering { + Ord::cmp(&&self.0[..], &&other.0[..]) + } +} + +impl PartialEq for Hash512 { + fn eq(&self, other: &Hash512) -> bool { + self.0.as_ref() == other.0.as_ref() + } +} + +impl Eq for Hash512 {} diff --git a/common/merkle_light/benches/hash512/mod.rs b/common/merkle_light/benches/hash512/mod.rs new file mode 100644 index 0000000..deefc8c --- /dev/null +++ b/common/merkle_light/benches/hash512/mod.rs @@ -0,0 +1,3 @@ +mod hash512; + +pub use hash512::hash512::Hash512; diff --git a/common/merkle_light/benches/ring_sha512.rs b/common/merkle_light/benches/ring_sha512.rs new file mode 100644 index 0000000..eac2c49 --- /dev/null +++ b/common/merkle_light/benches/ring_sha512.rs @@ -0,0 +1,168 @@ +//! cargo bench --features "crypto_bench" --verbose +#![cfg(feature = "crypto_bench")] +#![feature(test)] +#![feature(rand)] + +mod hash512; +mod ringx; + +extern crate merkle_light; +extern crate rand; +extern crate test; + +use hash512::Hash512; +use merkle_light::hash::{Algorithm, Hashable}; +use merkle_light::merkle::MerkleTree; +use rand::Rng; +use ringx::Context; +use ringx::SHA512; +use std::hash::Hasher; +use std::iter::FromIterator; +use test::Bencher; + +#[derive(Clone)] +struct B(Context); + +impl B { + fn new() -> B { + B(Context::new(&SHA512)) + } +} + +impl Default for B { + fn default() -> Self { + B::new() + } +} + +impl Hasher for B { + #[inline] + fn write(&mut self, msg: &[u8]) { + self.0.update(msg) + } + + #[inline] + fn finish(&self) -> u64 { + unimplemented!() + } +} + +impl Algorithm for B { + #[inline] + fn hash(&mut self) -> Hash512 { + let mut h = [0u8; 64]; + h.copy_from_slice(self.0.finish().as_ref()); + Hash512(h) + } + + #[inline] + fn reset(&mut self) { + self.0.reset(); + } +} + +fn tree_5() -> Vec { + ["one", "two", "three", "four"] + .iter() + .map(|x| { + let mut a = B::new(); + Hashable::hash(x, &mut a); + a.hash() + }) + .collect::>() +} + +fn tree_160() -> Vec { + let mut values = vec![vec![0u8; 256]; 160]; + let mut rng = rand::IsaacRng::new_unseeded(); + + for mut v in &mut values { + rng.fill_bytes(&mut v); + } + + values + .iter() + .map(|x| { + let mut a = B::new(); + a.write(x.as_ref()); + a.hash() + }) + .collect::>() +} + +#[bench] +fn bench_ringx_sha512(b: &mut Bencher) { + b.iter(|| { + let mut x = Context::new(&SHA512); + x.update("12345".as_ref()); + x.finish(); + }); +} + +#[bench] +fn bench_ringx_sha512_from_data_5(b: &mut Bencher) { + let values = tree_5(); + b.iter(|| MerkleTree::::from_iter(values.clone())); +} + +#[bench] +fn bench_ringx_sha512_from_data_5_proof(b: &mut Bencher) { + let values = tree_5(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + + b.iter(|| { + for i in 0..values.len() { + let proof = tree.gen_proof(i); + test::black_box(proof); + } + }); +} + +#[bench] +fn bench_ringx_sha512_from_data_5_proof_check(b: &mut Bencher) { + let values = tree_5(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + let proofs = (0..values.len()) + .map(|i| tree.gen_proof(i)) + .collect::>(); + + b.iter(|| { + for proof in &proofs { + test::black_box(proof.validate::()); + } + }); +} + +#[bench] +fn bench_ringx_sha512_from_data_160(b: &mut Bencher) { + let values = tree_160(); + b.iter(|| MerkleTree::::from_iter(values.clone())); +} + +#[bench] +fn bench_ringx_sha512_from_data_160_proof(b: &mut Bencher) { + let values = tree_160(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + + b.iter(|| { + for i in 0..values.len() { + let proof = tree.gen_proof(i); + test::black_box(proof); + } + }); +} + +#[bench] +fn bench_ringx_sha512_from_data_160_proof_check(b: &mut Bencher) { + let values = tree_160(); + let tree: MerkleTree = MerkleTree::from_iter(values.clone()); + let proofs = (0..values.len()) + .map(|i| tree.gen_proof(i)) + .collect::>(); + + b.iter(|| { + for proof in &proofs { + test::black_box(proof.validate::()); + } + }); +} diff --git a/common/merkle_light/benches/ringx/init.rs b/common/merkle_light/benches/ringx/init.rs new file mode 100644 index 0000000..0f70564 --- /dev/null +++ b/common/merkle_light/benches/ringx/init.rs @@ -0,0 +1,26 @@ +// Copyright 2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +#[inline(always)] +pub fn init_once() { + #[cfg(not(target_os = "ios"))] + { + use std; + extern "C" { + fn GFp_cpuid_setup(); + } + static INIT: std::sync::Once = std::sync::ONCE_INIT; + INIT.call_once(|| unsafe { GFp_cpuid_setup() }); + } +} diff --git a/common/merkle_light/benches/ringx/mod.rs b/common/merkle_light/benches/ringx/mod.rs new file mode 100644 index 0000000..b36a8c3 --- /dev/null +++ b/common/merkle_light/benches/ringx/mod.rs @@ -0,0 +1,567 @@ +//! Improvement version of [`ring::digest::Context`]. +//! +//! [`Context.finish`] now has `(&mut self)` instead of `(mut self)`. +//! [`Context`] acquired state `reset` thing. + +#![cfg(feature = "crypto_bench")] +#![allow(dead_code)] + +extern crate rand; +extern crate ring; +extern crate test; + +mod init; + +use std::fmt; + +// XXX: Replace with `const fn` when `const fn` is stable: +// https://github.com/rust-lang/rust/issues/24111 +#[cfg(target_endian = "little")] +macro_rules! u32x2 { + ( $first:expr, $second:expr ) => { + ((($second as u64) << 32) | ($first as u64)) + }; +} + +/// A context for multi-step (Init-Update-Finish) digest calculations. +/// +/// C analog: `EVP_MD_CTX`. +pub struct Context { + state: State, + + // Note that SHA-512 has a 128-bit input bit counter, but this + // implementation only supports up to 2^64-1 input bits for all algorithms, + // so a 64-bit counter is more than sufficient. + completed_data_blocks: u64, + + // TODO: More explicitly force 64-bit alignment for |pending|. + pending: [u8; MAX_BLOCK_LEN], + num_pending: usize, + + /// The context's algorithm. + pub algorithm: &'static Algorithm, +} + +impl Context { + /// Constructs a new context. + /// + /// C analogs: `EVP_DigestInit`, `EVP_DigestInit_ex` + pub fn new(algorithm: &'static Algorithm) -> Context { + init::init_once(); + + Context { + algorithm, + state: algorithm.initial_state, + completed_data_blocks: 0, + pending: [0u8; MAX_BLOCK_LEN], + num_pending: 0, + } + } + + /// Updates the digest with all the data in `data`. `update` may be called + /// zero or more times until `finish` is called. It must not be called + /// after `finish` has been called. + /// + /// C analog: `EVP_DigestUpdate` + pub fn update(&mut self, data: &[u8]) { + if data.len() < self.algorithm.block_len - self.num_pending { + self.pending[self.num_pending..(self.num_pending + data.len())].copy_from_slice(data); + self.num_pending += data.len(); + return; + } + + let mut remaining = data; + if self.num_pending > 0 { + let to_copy = self.algorithm.block_len - self.num_pending; + self.pending[self.num_pending..self.algorithm.block_len] + .copy_from_slice(&data[..to_copy]); + + unsafe { + (self.algorithm.block_data_order)(&mut self.state, self.pending.as_ptr(), 1); + } + self.completed_data_blocks = self.completed_data_blocks.checked_add(1).unwrap(); + + remaining = &remaining[to_copy..]; + self.num_pending = 0; + } + + let num_blocks = remaining.len() / self.algorithm.block_len; + let num_to_save_for_later = remaining.len() % self.algorithm.block_len; + if num_blocks > 0 { + unsafe { + (self.algorithm.block_data_order)(&mut self.state, remaining.as_ptr(), num_blocks); + } + self.completed_data_blocks = self + .completed_data_blocks + .checked_add(polyfill::slice::u64_from_usize(num_blocks)) + .unwrap(); + } + if num_to_save_for_later > 0 { + self.pending[..num_to_save_for_later] + .copy_from_slice(&remaining[(remaining.len() - num_to_save_for_later)..]); + self.num_pending = num_to_save_for_later; + } + } + + /// Finalizes the digest calculation and returns the digest value. `finish` + /// consumes the context so it cannot be (mis-)used after `finish` has been + /// called. + /// + /// C analogs: `EVP_DigestFinal`, `EVP_DigestFinal_ex` + pub fn finish(&mut self) -> Digest { + // We know |num_pending < self.algorithm.block_len|, because we would + // have processed the block otherwise. + + let mut padding_pos = self.num_pending; + self.pending[padding_pos] = 0x80; + padding_pos += 1; + + if padding_pos > self.algorithm.block_len - self.algorithm.len_len { + polyfill::slice::fill(&mut self.pending[padding_pos..self.algorithm.block_len], 0); + unsafe { + (self.algorithm.block_data_order)(&mut self.state, self.pending.as_ptr(), 1); + } + // We don't increase |self.completed_data_blocks| because the + // padding isn't data, and so it isn't included in the data length. + padding_pos = 0; + } + + polyfill::slice::fill( + &mut self.pending[padding_pos..(self.algorithm.block_len - 8)], + 0, + ); + + // Output the length, in bits, in big endian order. + let mut completed_data_bits: u64 = self + .completed_data_blocks + .checked_mul(polyfill::slice::u64_from_usize(self.algorithm.block_len)) + .unwrap() + .checked_add(polyfill::slice::u64_from_usize(self.num_pending)) + .unwrap() + .checked_mul(8) + .unwrap(); + + for b in (&mut self.pending[(self.algorithm.block_len - 8)..self.algorithm.block_len]) + .into_iter() + .rev() + { + *b = completed_data_bits as u8; + completed_data_bits /= 0x100; + } + unsafe { + (self.algorithm.block_data_order)(&mut self.state, self.pending.as_ptr(), 1); + } + + Digest { + algorithm: self.algorithm, + value: (self.algorithm.format_output)(&self.state), + } + } + + /// The algorithm that this context is using. + #[inline(always)] + pub fn algorithm(&self) -> &'static Algorithm { + self.algorithm + } + + /// Reset context state. + pub fn reset(&mut self) { + self.state = self.algorithm.initial_state; + self.pending = [0u8; MAX_BLOCK_LEN]; + self.completed_data_blocks = 0; + self.num_pending = 0; + } +} + +// XXX: This should just be `#[derive(Clone)]` but that doesn't work because +// `[u8; 128]` doesn't implement `Clone`. +impl Clone for Context { + fn clone(&self) -> Context { + Context { + state: self.state, + pending: self.pending, + completed_data_blocks: self.completed_data_blocks, + num_pending: self.num_pending, + algorithm: self.algorithm, + } + } +} + +/// Returns the digest of `data` using the given digest algorithm. +/// +/// C analog: `EVP_Digest` +/// +/// # Examples: +/// +/// ``` +/// # #[cfg(feature = "use_heap")] +/// # fn main() { +/// use ring::{digest, test}; +/// +/// let expected_hex = +/// "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b"; +/// let expected: Vec = test::from_hex(expected_hex).unwrap(); +/// let actual = digest::digest(&digest::SHA256, b"hello, world"); +/// +/// assert_eq!(&expected, &actual.as_ref()); +/// # } +/// +/// # #[cfg(not(feature = "use_heap"))] +/// # fn main() { } +/// ``` +pub fn digest(algorithm: &'static Algorithm, data: &[u8]) -> Digest { + let mut ctx = Context::new(algorithm); + ctx.update(data); + ctx.finish() +} + +/// A calculated digest value. +/// +/// Use `as_ref` to get the value as a `&[u8]`. +#[derive(Clone, Copy)] +pub struct Digest { + value: Output, + algorithm: &'static Algorithm, +} + +impl Digest { + /// The algorithm that was used to calculate the digest value. + #[inline(always)] + pub fn algorithm(&self) -> &'static Algorithm { + self.algorithm + } +} + +impl AsRef<[u8]> for Digest { + #[inline(always)] + fn as_ref(&self) -> &[u8] { + &(polyfill::slice::u64_as_u8(&self.value))[..self.algorithm.output_len] + } +} + +impl fmt::Debug for Digest { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{:?}:", self.algorithm)?; + for byte in self.as_ref() { + write!(fmt, "{:02x}", byte)?; + } + Ok(()) + } +} + +/// A digest algorithm. +/// +/// C analog: `EVP_MD` +pub struct Algorithm { + /// C analog: `EVP_MD_size` + pub output_len: usize, + + /// The size of the chaining value of the digest function, in bytes. For + /// non-truncated algorithms (SHA-1, SHA-256, SHA-512), this is equal to + /// `output_len`. For truncated algorithms (e.g. SHA-384, SHA-512/256), + /// this is equal to the length before truncation. This is mostly helpful + /// for determining the size of an HMAC key that is appropriate for the + /// digest algorithm. + pub chaining_len: usize, + + /// C analog: `EVP_MD_block_size` + pub block_len: usize, + + /// The length of the length in the padding. + pub len_len: usize, + + pub block_data_order: unsafe extern "C" fn(state: &mut State, data: *const u8, num: usize), + pub format_output: fn(input: &State) -> Output, + + pub initial_state: State, + + pub id: AlgorithmID, +} + +#[derive(Eq, PartialEq)] +#[allow(non_camel_case_types)] +pub enum AlgorithmID { + SHA256, + SHA512, + SHA512_256, +} + +impl PartialEq for Algorithm { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Algorithm {} + +impl fmt::Debug for Algorithm { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + // This would have to change if/when we add other algorithms with the + // same lengths. + let (n, suffix) = + if self.output_len == SHA512_256_OUTPUT_LEN && self.block_len == SHA512_BLOCK_LEN { + (512, "_256") + } else if self.output_len == 20 { + (1, "") + } else { + (self.output_len * 8, "") + }; + write!(fmt, "SHA{}{}", n, suffix) + } +} + +/// SHA-256 as specified in [FIPS 180-4]. +/// +/// [FIPS 180-4]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf +pub static SHA256: Algorithm = Algorithm { + output_len: SHA256_OUTPUT_LEN, + chaining_len: SHA256_OUTPUT_LEN, + block_len: 512 / 8, + len_len: 64 / 8, + block_data_order: GFp_sha256_block_data_order, + format_output: sha256_format_output, + initial_state: [ + u32x2!(0x6a09e667u32, 0xbb67ae85u32), + u32x2!(0x3c6ef372u32, 0xa54ff53au32), + u32x2!(0x510e527fu32, 0x9b05688cu32), + u32x2!(0x1f83d9abu32, 0x5be0cd19u32), + 0, + 0, + 0, + 0, + ], + id: AlgorithmID::SHA256, +}; + +/// SHA-512 as specified in [FIPS 180-4]. +/// +/// [FIPS 180-4]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf +pub static SHA512: Algorithm = Algorithm { + output_len: SHA512_OUTPUT_LEN, + chaining_len: SHA512_OUTPUT_LEN, + block_len: SHA512_BLOCK_LEN, + len_len: SHA512_LEN_LEN, + block_data_order: GFp_sha512_block_data_order, + format_output: sha512_format_output, + initial_state: [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, + ], + id: AlgorithmID::SHA512, +}; + +/// SHA-512/256 as specified in [FIPS 180-4]. +/// +/// This is *not* the same as just truncating the output of SHA-512, as +/// SHA-512/256 has its own initial state distinct from SHA-512's initial +/// state. +/// +/// [FIPS 180-4]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf +pub static SHA512_256: Algorithm = Algorithm { + output_len: SHA512_256_OUTPUT_LEN, + chaining_len: SHA512_OUTPUT_LEN, + block_len: SHA512_BLOCK_LEN, + len_len: SHA512_LEN_LEN, + block_data_order: GFp_sha512_block_data_order, + format_output: sha512_format_output, + initial_state: [ + 0x22312194fc2bf72c, + 0x9f555fa3c84c64c2, + 0x2393b86b6f53b151, + 0x963877195940eabd, + 0x96283ee2a88effe3, + 0xbe5e1e2553863992, + 0x2b0199fc2c85b8aa, + 0x0eb72ddc81c52ca2, + ], + id: AlgorithmID::SHA512_256, +}; + +// We use u64 to try to ensure 64-bit alignment/padding. +pub type State = [u64; MAX_CHAINING_LEN / 8]; + +pub type Output = [u64; MAX_OUTPUT_LEN / 8]; + +/// The maximum block length (`Algorithm::block_len`) of all the algorithms in +/// this module. +pub const MAX_BLOCK_LEN: usize = 1024 / 8; + +/// The maximum output length (`Algorithm::output_len`) of all the algorithms +/// in this module. +pub const MAX_OUTPUT_LEN: usize = 512 / 8; + +/// The maximum chaining length (`Algorithm::chaining_len`) of all the +/// algorithms in this module. +pub const MAX_CHAINING_LEN: usize = MAX_OUTPUT_LEN; + +mod polyfill { + pub mod slice { + use std::slice::from_raw_parts; + + // https://internals.rust-lang.org/t/ + // safe-trasnsmute-for-slices-e-g-u64-u32-particularly-simd-types/2871 + #[inline(always)] + pub fn u64_as_u32(src: &[u64]) -> &[u32] { + unsafe { from_raw_parts(src.as_ptr() as *const u32, src.len() * 2) } + } + + #[inline(always)] + pub fn u64_from_usize(x: usize) -> u64 { + x as u64 + } + + // https://internals.rust-lang.org/t/ + // stabilizing-basic-functions-on-arrays-and-slices/2868 + #[inline(always)] + pub fn fill(dest: &mut [u8], value: u8) { + for d in dest { + *d = value; + } + } + + // https://internals.rust-lang.org/t/ + // safe-trasnsmute-for-slices-e-g-u64-u32-particularly-simd-types/2871 + #[inline(always)] + pub fn u64_as_u8(src: &[u64]) -> &[u8] { + unsafe { from_raw_parts(src.as_ptr() as *const u8, src.len() * 8) } + } + } +} + +pub fn sha256_format_output(input: &State) -> Output { + let input = &polyfill::slice::u64_as_u32(input)[..8]; + [ + u32x2!(input[0].to_be(), input[1].to_be()), + u32x2!(input[2].to_be(), input[3].to_be()), + u32x2!(input[4].to_be(), input[5].to_be()), + u32x2!(input[6].to_be(), input[7].to_be()), + 0, + 0, + 0, + 0, + ] +} + +pub fn sha512_format_output(input: &State) -> Output { + [ + input[0].to_be(), + input[1].to_be(), + input[2].to_be(), + input[3].to_be(), + input[4].to_be(), + input[5].to_be(), + input[6].to_be(), + input[7].to_be(), + ] +} + +/// The length of the output of SHA-256, in bytes. +pub const SHA256_OUTPUT_LEN: usize = 256 / 8; + +/// The length of the output of SHA-512, in bytes. +pub const SHA512_OUTPUT_LEN: usize = 512 / 8; + +/// The length of the output of SHA-512/256, in bytes. +pub const SHA512_256_OUTPUT_LEN: usize = 256 / 8; + +/// The length of a block for SHA-512-based algorithms, in bytes. +const SHA512_BLOCK_LEN: usize = 1024 / 8; + +/// The length of the length field for SHA-512-based algorithms, in bytes. +const SHA512_LEN_LEN: usize = 128 / 8; + +extern "C" { + fn GFp_sha256_block_data_order(state: &mut State, data: *const u8, num: usize); + fn GFp_sha512_block_data_order(state: &mut State, data: *const u8, num: usize); +} + +#[cfg(test)] +pub mod test_util { + use super::*; + + pub static ALL_ALGORITHMS: [&'static Algorithm; 3] = [&SHA256, &SHA512, &SHA512_256]; +} + +/* +#[cfg(test)] +mod tests { + + mod max_input { + use super::super::super::digest; + + macro_rules! max_input_tests { + ( $algorithm_name:ident ) => { + #[allow(non_snake_case)] + mod $algorithm_name { + use super::super::super::super::digest; + + #[test] + fn max_input_test() { + super::max_input_test(&digest::$algorithm_name); + } + + #[test] + #[should_panic] + fn too_long_input_test_block() { + super::too_long_input_test_block( + &digest::$algorithm_name); + } + + #[test] + #[should_panic] + fn too_long_input_test_byte() { + super::too_long_input_test_byte( + &digest::$algorithm_name); + } + } + } + } + + fn max_input_test(alg: &'static digest::Algorithm) { + let mut context = nearly_full_context(alg); + let next_input = vec![0u8; alg.block_len - 1]; + context.update(&next_input); + let _ = context.finish(); // no panic + } + + fn too_long_input_test_block(alg: &'static digest::Algorithm) { + let mut context = nearly_full_context(alg); + let next_input = vec![0u8; alg.block_len]; + context.update(&next_input); + let _ = context.finish(); // should panic + } + + fn too_long_input_test_byte(alg: &'static digest::Algorithm) { + let mut context = nearly_full_context(alg); + let next_input = vec![0u8; alg.block_len - 1]; + context.update(&next_input); // no panic + context.update(&[0]); + let _ = context.finish(); // should panic + } + + fn nearly_full_context(alg: &'static digest::Algorithm) + -> digest::Context { + // All implementations currently support up to 2^64-1 bits + // of input; according to the spec, SHA-384 and SHA-512 + // support up to 2^128-1, but that's not implemented yet. + let max_bytes = 1u64 << (64 - 3); + let max_blocks = max_bytes / (alg.block_len as u64); + digest::Context { + algorithm: alg, + state: alg.initial_state, + completed_data_blocks: max_blocks - 1, + pending: [0u8; digest::MAX_BLOCK_LEN], + num_pending: 0, + } + } + + max_input_tests!(SHA256); + max_input_tests!(SHA512); + } +}*/ diff --git a/common/merkle_light/clippy.toml b/common/merkle_light/clippy.toml new file mode 100644 index 0000000..c7b1f02 --- /dev/null +++ b/common/merkle_light/clippy.toml @@ -0,0 +1,3 @@ +disallowed-names = [ + "unreadable_literal" +] diff --git a/common/merkle_light/examples/no_std.rs b/common/merkle_light/examples/no_std.rs new file mode 100644 index 0000000..3835462 --- /dev/null +++ b/common/merkle_light/examples/no_std.rs @@ -0,0 +1,4 @@ +// This is just for no_std testing +#[allow(unused_imports)] + +fn main() {} diff --git a/common/merkle_light/src/hash.rs b/common/merkle_light/src/hash.rs new file mode 100644 index 0000000..3984f12 --- /dev/null +++ b/common/merkle_light/src/hash.rs @@ -0,0 +1,135 @@ +//! Hash infrastructure for items in Merkle Tree. + +use core::hash::Hasher; + +/// A hashable type. +/// +/// Types implementing `Hashable` are able to be [`hash`]ed with an instance of +/// [`Hasher`]. +/// +/// ## Implementing `Hashable` +/// +/// You can derive `Hashable` with `#[derive(Hashable)]` if all fields implement `Hashable`. +/// The resulting hash will be the combination of the values from calling +/// [`hash`] on each field. +/// +/// ```text +/// #[macro_use] +/// extern crate merkle_light_derive; +/// extern crate merkle_light; +/// +/// use merkle_light::hash::Hashable; +/// +/// fn main() { +/// #[derive(Hashable)] +/// struct Foo { +/// name: String, +/// country: String, +/// } +/// } +/// ``` +/// +/// If you need more control over how a value is hashed, you can of course +/// implement the `Hashable` trait yourself: +/// +/// ``` +/// extern crate merkle_light; +/// +/// use merkle_light::hash::Hashable; +/// use std::hash::Hasher; +/// use std::collections::hash_map::DefaultHasher; +/// +/// fn main() { +/// struct Person { +/// id: u32, +/// name: String, +/// phone: u64, +/// } +/// +/// impl Hashable for Person { +/// fn hash(&self, state: &mut H) { +/// self.id.hash(state); +/// self.name.hash(state); +/// self.phone.hash(state); +/// } +/// } +/// +/// let foo = Person{ +/// id: 1, +/// name: String::from("blah"), +/// phone: 2, +/// }; +/// +/// let mut hr = DefaultHasher::new(); +/// foo.hash(&mut hr); +/// assert_eq!(hr.finish(), 7101638158313343130) +/// } +/// ``` +/// +/// ## `Hashable` and `Eq` +/// +/// When implementing both `Hashable` and [`Eq`], it is important that the following +/// property holds: +/// +/// ```text +/// k1 == k2 -> hash(k1) == hash(k2) +/// ``` +/// +/// In other words, if two keys are equal, their hashes must also be equal. +pub trait Hashable { + /// Feeds this value into the given [`Hasher`]. + /// + /// [`Hasher`]: trait.Hasher.html + fn hash(&self, state: &mut H); + + /// Feeds a slice of this type into the given [`Hasher`]. + /// + /// [`Hasher`]: trait.Hasher.html + fn hash_slice(data: &[Self], state: &mut H) + where + Self: Sized, + { + for piece in data { + piece.hash(state); + } + } +} + +/// A trait for hashing an arbitrary stream of bytes for calculating merkle tree +/// nodes. +/// +/// T is a hash item must be of known size at compile time, globally ordered, with +/// default value as a neutral element of the hash space. Neutral element is +/// interpreted as 0 or nil and required for evaluation of merkle tree. +/// +/// [`Algorithm`] breaks the [`Hasher`] contract at `finish()`, but that is intended. +/// This trait extends [`Hasher`] with `hash -> T` and `reset` state methods, +/// plus implements default behavior of evaluation of MT interior nodes. +pub trait Algorithm: Hasher + Default +where + T: Clone + AsRef<[u8]>, +{ + /// Returns the hash value for the data stream written so far. + fn hash(&mut self) -> T; + + /// Reset Hasher state. + #[inline] + fn reset(&mut self) { + *self = Self::default(); + } + + /// Returns hash value for MT leaf (prefix 0x00). + #[inline] + fn leaf(&mut self, leaf: T) -> T { + self.write(leaf.as_ref()); + self.hash() + } + + /// Returns hash value for MT interior node (prefix 0x01). + #[inline] + fn node(&mut self, left: T, right: T) -> T { + self.write(left.as_ref()); + self.write(right.as_ref()); + self.hash() + } +} diff --git a/common/merkle_light/src/hash_impl.rs b/common/merkle_light/src/hash_impl.rs new file mode 100644 index 0000000..253253c --- /dev/null +++ b/common/merkle_light/src/hash_impl.rs @@ -0,0 +1,188 @@ +extern crate alloc; + +use crate::hash::Hashable; +use alloc::string::String; +use alloc::vec::Vec; +use core::hash::Hasher; +use core::mem; +use core::slice; + +macro_rules! impl_write { + ($(($ty:ident, $meth:ident),)*) => {$( + impl Hashable for $ty { + fn hash(&self, state: &mut H) { + state.$meth(*self) + } + + #[allow(trivial_casts, unsafe_code)] + fn hash_slice(data: &[$ty], state: &mut H) { + let newlen = data.len() * mem::size_of::<$ty>(); + let ptr = data.as_ptr() as *const u8; + state.write(unsafe { slice::from_raw_parts(ptr, newlen) }) + } + } + )*} +} + +impl_write! { + (u8, write_u8), + (u16, write_u16), + (u32, write_u32), + (u64, write_u64), + (usize, write_usize), + (i8, write_i8), + (i16, write_i16), + (i32, write_i32), + (i64, write_i64), + (isize, write_isize), + // unstable: (u128, write_u128), + // unstable: (i128, write_i128), +} + +macro_rules! impl_array { + ($ty:ident $($N:expr)+) => {$( + impl Hashable for [$ty; $N] { + fn hash(&self, state: &mut H) { + state.write(self.as_ref()) + } + + #[allow(trivial_casts, unsafe_code)] + fn hash_slice(data: &[[$ty; $N]], state: &mut H) { + let newlen = data.len() * mem::size_of::<[$ty; $N]>(); + let ptr = data.as_ptr() as *const u8; + state.write(unsafe { slice::from_raw_parts(ptr, newlen) }) + } + } + )*} +} + +impl_array! { u8 + 1 2 3 4 5 6 7 8 9 10 + 11 12 13 14 15 16 17 18 19 20 + 21 22 23 24 25 26 27 28 29 30 + 31 32 33 34 35 36 37 38 39 40 + 41 42 43 44 45 46 47 48 49 50 + 51 52 53 54 55 56 57 58 59 60 + 61 62 63 64 +} + +impl Hashable for Vec { + fn hash(&self, state: &mut H) { + state.write(self.as_ref()); + } +} + +impl Hashable for bool { + fn hash(&self, state: &mut H) { + state.write_u8(*self as u8) + } +} + +impl Hashable for char { + fn hash(&self, state: &mut H) { + state.write_u32(*self as u32) + } +} + +impl Hashable for str { + fn hash(&self, state: &mut H) { + state.write(self.as_bytes()); + // empty str nope: state.write_u8(0xff) + } +} + +impl Hashable for String { + fn hash(&self, state: &mut H) { + state.write(self.as_bytes()); + // empty str nope: state.write_u8(0xff) + } +} + +macro_rules! impl_hash_tuple { + () => ( + impl Hashable for () { + fn hash(&self, _: &mut H) {} + } + ); + + ( $($name:ident)+) => ( + impl),*> Hashable for ($($name,)*) + where + last_type!($($name,)+): ?Sized + { + #[allow(non_snake_case)] + fn hash(&self, state: &mut Z) { + let ($(ref $name,)*) = *self; + $($name.hash(state);)* + } + } + ); +} + +macro_rules! last_type { + ($a:ident,) => { $a }; + ($a:ident, $($rest_a:ident,)+) => { last_type!($($rest_a,)+) }; +} + +impl_hash_tuple! {} +impl_hash_tuple! { A } +impl_hash_tuple! { A B } +impl_hash_tuple! { A B C } +impl_hash_tuple! { A B C D } +impl_hash_tuple! { A B C D E } +impl_hash_tuple! { A B C D E F } +impl_hash_tuple! { A B C D E F G } +impl_hash_tuple! { A B C D E F G H } +impl_hash_tuple! { A B C D E F G H I } +impl_hash_tuple! { A B C D E F G H I J } +impl_hash_tuple! { A B C D E F G H I J K } +impl_hash_tuple! { A B C D E F G H I J K L } + +impl> Hashable for [T] { + fn hash(&self, state: &mut H) { + self.len().hash(state); + Hashable::hash_slice(self, state) + } +} + +impl<'a, H: Hasher, T: ?Sized + Hashable> Hashable for &'a T { + fn hash(&self, state: &mut H) { + (**self).hash(state); + } +} + +impl<'a, H: Hasher, T: ?Sized + Hashable> Hashable for &'a mut T { + fn hash(&self, state: &mut H) { + (**self).hash(state); + } +} + +impl Hashable for *const T { + #[allow(trivial_casts, unsafe_code)] + fn hash(&self, state: &mut H) { + if mem::size_of::() == mem::size_of::() { + // Thin pointer + state.write_usize(*self as *const () as usize); + } else { + // Fat pointer + let (a, b) = unsafe { *(self as *const Self as *const (usize, usize)) }; + state.write_usize(a); + state.write_usize(b); + } + } +} + +impl Hashable for *mut T { + #[allow(trivial_casts, unsafe_code)] + fn hash(&self, state: &mut H) { + if mem::size_of::() == mem::size_of::() { + // Thin pointer + state.write_usize(*self as *const () as usize); + } else { + // Fat pointer + let (a, b) = unsafe { *(self as *const Self as *const (usize, usize)) }; + state.write_usize(a); + state.write_usize(b); + } + } +} diff --git a/common/merkle_light/src/lib.rs b/common/merkle_light/src/lib.rs new file mode 100644 index 0000000..1147981 --- /dev/null +++ b/common/merkle_light/src/lib.rs @@ -0,0 +1,178 @@ +//! light _Merkle Tree_ implementation. +//! +//! Merkle tree (MT) implemented as a full binary tree allocated as a vec +//! of statically sized hashes to give hashes more locality. MT specialized +//! to the extent of hashing algorithm and hash item. [`Hashable`] trait is +//! compatible to the `std::hash::Hasher` and supports custom hash algorithms. +//! Implementation does not depend on any external crypto libraries, and tries +//! to be as performant as possible. +//! +//! This tree implementation uses encoding scheme as in _Certificate Transparency_ +//! by default. Encoding scheme for leafs and nodes can be overridden though. +//! [RFC 6962](https://tools.ietf.org/html/rfc6962): +//! +//! ```text +//! MTH({d(0)}) = ALG(0x00 || d(0)). +//! For n > 1, let k be the largest power of two smaller than n (i.e., +//! k < n <= 2k). The Merkle tree Hash of an n-element list D[n] is then +//! defined recursively as +//! MTH(D[n]) = ALG(0x01 || MTH(D[0:k]) || MTH(D[k:n])), +//! ``` +//! +//! Link: [](https://en.wikipedia.org/wiki/Merkle_tree) +//! +//! # Implementation choices +//! +//! Main idea is the whole code must obtain specialization at compile time with +//! minimum allocations calls, hashes must be of fixed size arrays known at +//! compile time, hash algorithm must be a trait and must not depend on any +//! external cryptographic libraries and the lib itself must somehow mimic std Rust api. +//! +//! Standard way in Rust is to hash objects with a `std::hash::Hasher`, and mainly +//! that is the reason behind the choice of the abstractions: +//! +//! `Object : Hashable -> Hasher + Algorithm <- Merkle Tree` +//! +//! Custom [`merkle::hash::Hashable`] trait allows implementations differ +//! from [`std::collection`] related hashes, different implementations for +//! different hashing algorithms / schemas and conforms object-safety trait rules. +//! +//! [`Algorithm`] complements [`Hasher`] to be reusable and follows the idea +//! that the result hash is a mapping of the data stream. +//! +//! [`Algorithm.hash`] had to change its signature to be `&mut self` (`&self`) because +//! most of the cryptographic digest algorithms breaks current state on finalization +//! into unusable. `ring` libra tho contains interfaces incompatible to +//! `start-update-finish-reset` lifecycle. It requires either `cloning()` its state +//! on finalization, or `Cell`-ing via unsafe. +//! +//! Turning back to having [`Algorithm.write(&mut self, &[u8])`] instead of +//! `write(T)` allows to relax [`Algorithm`] trait [`Hasher`] constraint, even tho +//! works together well still. +//! +//! # Interface +//! +//! ```text +//! - build_tree (items) -> tree +//! - get_root -> hash +//! - gen_proof -> proof +//! - validate_proof (proof, leaf, root) -> bool +//! ``` +//! +//! # Examples +//! +//! [`test_cmh.rs`]: custom merkle hasher implementation example +//! [`crypto_bitcoin_mt.rs`]: bitcoin merkle tree using crypto lib +//! [`crypto_chaincore_mt.rs`]: chain core merkle tree using crypto lib +//! [`ring_bitcoin_mt.rs`]: bitcoin merkle tree using ring lib +//! +//! # Quick start +//! +//! ``` +//! #[cfg(feature = "chaincore")] +//! extern crate crypto; +//! extern crate merkle_light; +//! +//! #[cfg(feature = "chaincore")] +//! mod example { +//! use std::fmt; +//! use std::hash::Hasher; +//! use std::iter::FromIterator; +//! use crypto::sha3::{Sha3, Sha3Mode}; +//! use crypto::digest::Digest; +//! use merkle_light::hash::{Algorithm, Hashable}; +//! +//! pub struct ExampleAlgorithm(Sha3); +//! +//! impl ExampleAlgorithm { +//! pub fn new() -> ExampleAlgorithm { +//! ExampleAlgorithm(Sha3::new(Sha3Mode::Sha3_256)) +//! } +//! } +//! +//! impl Default for ExampleAlgorithm { +//! fn default() -> ExampleAlgorithm { +//! ExampleAlgorithm::new() +//! } +//! } +//! +//! impl Hasher for ExampleAlgorithm { +//! #[inline] +//! fn write(&mut self, msg: &[u8]) { +//! self.0.input(msg) +//! } +//! +//! #[inline] +//! fn finish(&self) -> u64 { +//! unimplemented!() +//! } +//! } +//! +//! impl Algorithm<[u8; 32]> for ExampleAlgorithm { +//! #[inline] +//! fn hash(&mut self) -> [u8; 32] { +//! let mut h = [0u8; 32]; +//! self.0.result(&mut h); +//! h +//! } +//! +//! #[inline] +//! fn reset(&mut self) { +//! self.0.reset(); +//! } +//! } +//! } +//! +//! fn main() { +//! #[cfg(feature = "chaincore")] +//! { +//! use example::ExampleAlgorithm; +//! use merkle_light::merkle::MerkleTree; +//! use std::iter::FromIterator; +//! +//! let mut h1 = [0u8; 32]; +//! let mut h2 = [0u8; 32]; +//! let mut h3 = [0u8; 32]; +//! h1[0] = 0x11; +//! h2[0] = 0x22; +//! h3[0] = 0x33; +//! +//! let t: MerkleTree<[u8; 32], ExampleAlgorithm> = MerkleTree::from_iter(vec![h1, h2, h3]); +//! println!("{:?}", t.root()); +//! } +//! } +//! ``` + +#![deny( + missing_docs, + unused_qualifications, + missing_debug_implementations, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unstable_features, + unused_import_braces +)] +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(feature = "nightly", allow(unstable_features))] + +/// Hash infrastructure for items in Merkle tree. +pub mod hash; + +/// Common implementations for [`Hashable`]. +mod hash_impl; + +/// Merkle tree inclusion proof +pub mod proof; + +/// Merkle tree abstractions, implementation and algorithms. +pub mod merkle; + +/// Tests data. +#[cfg(test)] +mod test_item; + +/// Tests for Merkle Hasher Customization +#[cfg(test)] +mod test_cmh; diff --git a/common/merkle_light/src/merkle.rs b/common/merkle_light/src/merkle.rs new file mode 100644 index 0000000..abd9711 --- /dev/null +++ b/common/merkle_light/src/merkle.rs @@ -0,0 +1,305 @@ +extern crate alloc; + +use crate::hash::{Algorithm, Hashable}; +use crate::proof::Proof; +use alloc::vec::Vec; +use core::iter::FromIterator; +use core::marker::PhantomData; +use core::ops; +use rayon::prelude::*; +use std::collections::BTreeMap; +use std::fmt::Debug; + +/// Merkle Tree. +/// +/// All leafs and nodes are stored in a linear array (vec). +/// +/// A merkle tree is a tree in which every non-leaf node is the hash of its +/// children nodes. A diagram depicting how it works: +/// +/// ```text +/// root = h1234 = h(h12 + h34) +/// / \ +/// h12 = h(h1 + h2) h34 = h(h3 + h4) +/// / \ / \ +/// h1 = h(tx1) h2 = h(tx2) h3 = h(tx3) h4 = h(tx4) +/// ``` +/// +/// In memory layout: +/// +/// ```text +/// [h1 h2 h3 h4 h12 h34 root] +/// ``` +/// +/// Merkle root is always the last element in the array. +/// +/// The number of inputs is not always a power of two which results in a +/// balanced tree structure as above. In that case, parent nodes with no +/// children are also zero and parent nodes with only a single left node +/// are calculated by concatenating the left node with itself before hashing. +/// Since this function uses nodes that are pointers to the hashes, empty nodes +/// will be nil. +/// +/// TODO: Ord +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct MerkleTree + Sync + Send, A: Algorithm> { + data: Vec, + leafs: usize, + height: usize, + link_map: BTreeMap, + _a: PhantomData, +} + +impl + Sync + Send, A: Algorithm> + MerkleTree +{ + /// Creates new merkle from a sequence of hashes. + pub fn new>(data: I) -> MerkleTree { + Self::from_iter(data) + } + + /// Creates new merkle tree from a list of hashable objects. + pub fn from_data, I: IntoIterator>(data: I) -> MerkleTree { + let mut a = A::default(); + Self::from_iter(data.into_iter().map(|x| { + a.reset(); + x.hash(&mut a); + a.hash() + })) + } + + fn build(&mut self) { + let mut width = self.leafs; + + // build tree + let mut layer_start: usize = 0; + let mut layer_end: usize = width; + while width > 1 { + // if there is odd num of elements, fill in a NULL. + if width & 1 == 1 { + self.data.push(Self::null_node()); + width += 1; + layer_end += 1; + } + + let layer: Vec<_> = (layer_start..layer_end) + .into_par_iter() + .step_by(2) + .map(|i| { + let mut a = A::default(); + // If the right child is not NULL, the left child is ensured to be not NULL. + let mut link_map_update = None; + let h = if self.data[i + 1] != Self::null_node() { + a.node(self.data[i].clone(), self.data[i + 1].clone()) + } else { + // If a child is NULL, the parent should be a linking node to the actual node hash. + let parent_index = (i - layer_start) / 2 + layer_end; + if self.data[i] == Self::null_node() { + // If both are NULL, the left child must be a linking node. + let linked_to = *self.link_map.get(&i).unwrap(); + link_map_update = Some((parent_index, linked_to, Some(i))); + Self::null_node() + } else { + match self.link_map.get(&(i + 1)) { + // Right child is linked to a hash, so we just compute the parent hash. + Some(index) => { + assert_ne!(self.data[*index], Self::null_node()); + a.node(self.data[i].clone(), self.data[*index].clone()) + } + // Right child is NULL, so link the parent to the left child which has a hash stored. + None => { + link_map_update = Some((parent_index, i, None)); + Self::null_node() + } + } + } + }; + (h, link_map_update) + }) + .collect(); + for (node, maybe_link_map_update) in layer { + self.data.push(node); + if let Some((from, to, maybe_remove)) = maybe_link_map_update { + self.link_map.insert(from, to); + if let Some(remove) = maybe_remove { + self.link_map.remove(&remove); + } + } + } + + layer_start = layer_end; + width >>= 1; + layer_end += width; + } + } + + /// Generate merkle tree inclusion proof for leaf `i` + pub fn gen_proof(&self, i: usize) -> Proof { + if self.leafs == 1 { + assert_eq!(i, 0); + return Proof::new(vec![self.root()], vec![]); + } + + assert!(i < self.leafs); // i in [0 .. self.leafs) + + let mut lemma: Vec = Vec::with_capacity(self.height + 1); // path + root + let mut path: Vec = Vec::with_capacity(self.height - 1); // path - 1 + + let mut base = 0; + let mut j = i; + + // level 1 width + let mut width = self.leafs; + if width & 1 == 1 { + width += 1; + } + + lemma.push(self.data[j].clone()); + while base + 1 < self.len() { + let proof_hash_index = if j & 1 == 0 { + // j is left + let right_index = base + j + 1; + if self.data[right_index] == Self::null_node() { + match self.link_map.get(&right_index) { + // A link node, so the proof uses the linked hash. + Some(index) => { + assert_ne!(self.data[*index], Self::null_node()); + Some(*index) + } + // A NULL node, just skip. + None => None, + } + } else { + Some(right_index) + } + } else { + // j is right + Some(base + j - 1) + }; + if let Some(index) = proof_hash_index { + lemma.push(self.data[index].clone()); + path.push(j & 1 == 0); + } + + base += width; + width >>= 1; + if width & 1 == 1 { + width += 1; + } + j >>= 1; + } + + // root is final + lemma.push(self.root()); + Proof::new(lemma, path) + } + + /// Returns merkle root + pub fn root(&self) -> T { + self.data[self.data.len() - 1].clone() + } + + /// Returns number of elements in the tree. + pub fn len(&self) -> usize { + self.data.len() + } + + /// Returns `true` if the vector contains no elements. + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// Returns height of the tree + pub fn height(&self) -> usize { + self.height + } + + /// Returns original number of elements the tree was built upon. + pub fn leafs(&self) -> usize { + self.leafs + } + + /// Extracts a slice containing the entire vector. + /// + /// Equivalent to `&s[..]`. + pub fn as_slice(&self) -> &[T] { + self + } + + fn null_node() -> T { + T::default() + } +} + +impl + Sync + Send, A: Algorithm> FromIterator + for MerkleTree +{ + /// Creates new merkle tree from an iterator over hashable objects. + fn from_iter>(into: I) -> Self { + let iter = into.into_iter(); + let mut data: Vec = match iter.size_hint().1 { + Some(e) => { + let pow = next_pow2(e); + let size = 2 * pow - 1; + Vec::with_capacity(size) + } + None => Vec::new(), + }; + + // leafs + let mut a = A::default(); + for item in iter { + a.reset(); + data.push(a.leaf(item)); + } + + let leafs = data.len(); + let pow = next_pow2(leafs); + let size = 2 * pow - 1; + + // assert!(leafs > 1); + + let mut mt: MerkleTree = MerkleTree { + data, + leafs, + height: log2_pow2(size + 1), + link_map: Default::default(), + _a: PhantomData, + }; + + mt.build(); + mt + } +} + +impl + Sync + Send, A: Algorithm> ops::Deref for MerkleTree { + type Target = [T]; + + fn deref(&self) -> &[T] { + self.data.deref() + } +} + +/// `next_pow2` returns next highest power of two from a given number if +/// it is not already a power of two. +/// +/// [](http://locklessinc.com/articles/next_pow2/) +/// [](https://stackoverflow.com/questions/466204/rounding-up-to-next-power-of-2/466242#466242) +pub fn next_pow2(mut n: usize) -> usize { + n -= 1; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + #[cfg(target_pointer_width = "64")] + { + n |= n >> 32; + } + n + 1 +} + +/// find power of 2 of a number which is power of 2 +pub fn log2_pow2(n: usize) -> usize { + n.trailing_zeros() as usize +} diff --git a/common/merkle_light/src/proof.rs b/common/merkle_light/src/proof.rs new file mode 100644 index 0000000..3c5f702 --- /dev/null +++ b/common/merkle_light/src/proof.rs @@ -0,0 +1,80 @@ +extern crate alloc; + +use crate::hash::Algorithm; +use alloc::vec::Vec; + +/// Merkle tree inclusion proof for data element, for which item = Leaf(Hash(Data Item)). +/// +/// Lemma layout: +/// +/// ```text +/// [ item h1x h2y h3z ... root ] +/// ``` +/// +/// Proof validation is positioned hash against lemma path to match root hash. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Proof> { + lemma: Vec, + path: Vec, +} + +impl> Proof { + /// Creates new MT inclusion proof + pub fn new(hash: Vec, path: Vec) -> Proof { + if hash.len() > 2 { + assert_eq!(hash.len() - 2, path.len()); + } else { + assert_eq!(hash.len(), 1); + assert_eq!(path, vec![]); + } + Proof { lemma: hash, path } + } + + /// Return proof target leaf + pub fn item(&self) -> T { + self.lemma.first().unwrap().clone() + } + + /// Return tree root + pub fn root(&self) -> T { + self.lemma.last().unwrap().clone() + } + + /// Verifies MT inclusion proof + pub fn validate>(&self) -> bool { + let size = self.lemma.len(); + + // Special case for a single node. + if size == 1 && self.path == vec![] { + return true; + } + + if size < 2 { + return false; + } + + let mut h = self.item(); + let mut a = A::default(); + + for i in 1..size - 1 { + a.reset(); + h = if self.path[i - 1] { + a.node(h, self.lemma[i].clone()) + } else { + a.node(self.lemma[i].clone(), h) + }; + } + + h == self.root() + } + + /// Returns the path of this proof. + pub fn path(&self) -> &[bool] { + &self.path + } + + /// Returns the lemma of this proof. + pub fn lemma(&self) -> &[T] { + &self.lemma + } +} diff --git a/common/merkle_light/src/test_cmh.rs b/common/merkle_light/src/test_cmh.rs new file mode 100644 index 0000000..925662d --- /dev/null +++ b/common/merkle_light/src/test_cmh.rs @@ -0,0 +1,76 @@ +#![cfg(test)] + +use crate::hash::{Algorithm, Hashable}; +use crate::merkle::MerkleTree; +use crate::test_item::Item; +use std::collections::hash_map::DefaultHasher; +use std::hash::Hasher; +use std::iter::FromIterator; + +/// Custom merkle hash util test +#[derive(Debug, Clone, Default)] +struct CMH(DefaultHasher); + +impl CMH { + pub fn new() -> CMH { + CMH(DefaultHasher::new()) + } +} + +impl Hasher for CMH { + #[inline] + fn write(&mut self, msg: &[u8]) { + self.0.write(msg) + } + + #[inline] + fn finish(&self) -> u64 { + self.0.finish() + } +} + +impl Algorithm for CMH { + #[inline] + fn hash(&mut self) -> Item { + Item(self.finish()) + } + + #[inline] + fn reset(&mut self) { + *self = CMH::default() + } + + #[inline] + fn leaf(&mut self, leaf: Item) -> Item { + Item(leaf.0 & 0xff) + } + + #[inline] + fn node(&mut self, left: Item, right: Item) -> Item { + self.write(&[1u8]); + self.write(left.as_ref()); + self.write(&[2u8]); + self.write(right.as_ref()); + Item(self.hash().0 & 0xffff) + } +} + +#[test] +fn test_custom_merkle_hasher() { + let mut a = CMH::new(); + let mt: MerkleTree = MerkleTree::from_iter([1, 2, 3, 4, 5].iter().map(|x| { + a.reset(); + x.hash(&mut a); + a.hash() + })); + + assert_eq!( + mt.as_slice() + .iter() + .take(mt.leafs()) + .filter(|&&x| x.0 > 255) + .count(), + 0 + ); + assert_eq!(mt.as_slice().iter().filter(|&&x| x.0 > 65535).count(), 0); +} diff --git a/common/merkle_light/src/test_item.rs b/common/merkle_light/src/test_item.rs new file mode 100644 index 0000000..8e8c62b --- /dev/null +++ b/common/merkle_light/src/test_item.rs @@ -0,0 +1,39 @@ +#![cfg(test)] +#![allow(unsafe_code)] + +use crate::hash::{Algorithm, Hashable}; +use std::mem; +use std::slice; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Debug)] +pub struct Item(pub u64); + +impl AsRef<[u8]> for Item { + fn as_ref(&self) -> &[u8] { + unsafe { slice::from_raw_parts(mem::transmute(&self.0), 8) } + } +} + +impl PartialEq for Item { + fn eq(&self, other: &u64) -> bool { + self.0 == *other + } +} + +impl From for Item { + fn from(x: u64) -> Self { + Item(x) + } +} + +impl From for u64 { + fn from(val: Item) -> Self { + val.0 + } +} + +impl> Hashable for Item { + fn hash(&self, state: &mut A) { + state.write_u64(self.0) + } +} diff --git a/common/merkle_tree/Cargo.toml b/common/merkle_tree/Cargo.toml new file mode 100644 index 0000000..463bb63 --- /dev/null +++ b/common/merkle_tree/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "merkle_tree" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +merkle_light = { path = "../../common/merkle_light" } +tiny-keccak = { version = "2.0.2", features = ["keccak"] } + +[dev-dependencies] +hex = "0.4.3" \ No newline at end of file diff --git a/common/merkle_tree/src/lib.rs b/common/merkle_tree/src/lib.rs new file mode 100644 index 0000000..db984eb --- /dev/null +++ b/common/merkle_tree/src/lib.rs @@ -0,0 +1,88 @@ +use merkle_light::hash::Algorithm; +use std::hash::Hasher; +use tiny_keccak::{Hasher as KeccakHasher, Keccak}; + +// TODO: Option here is only used for compatibility with `tiny_keccak` and `merkle_light`. +#[derive(Clone)] +pub struct RawLeafSha3Algorithm(Option); + +impl RawLeafSha3Algorithm { + fn new() -> RawLeafSha3Algorithm { + RawLeafSha3Algorithm(Some(Keccak::v256())) + } +} + +impl Default for RawLeafSha3Algorithm { + fn default() -> RawLeafSha3Algorithm { + RawLeafSha3Algorithm::new() + } +} + +impl Hasher for RawLeafSha3Algorithm { + #[inline] + fn write(&mut self, msg: &[u8]) { + self.0.as_mut().unwrap().update(msg) + } + + #[inline] + fn finish(&self) -> u64 { + unimplemented!() + } +} + +pub type CryptoSHA256Hash = [u8; 32]; + +impl Algorithm for RawLeafSha3Algorithm { + #[inline] + fn hash(&mut self) -> CryptoSHA256Hash { + let mut h = [0u8; 32]; + self.0.take().unwrap().finalize(&mut h); + h + } + + fn leaf(&mut self, leaf: CryptoSHA256Hash) -> CryptoSHA256Hash { + // Leave the leaf node untouched so we can save the subtree root + // just as the leaf node for the top tree. + // `LEAF` is prepended for `Chunk` hash computation. + leaf + } + + #[inline] + fn node(&mut self, left: CryptoSHA256Hash, right: CryptoSHA256Hash) -> CryptoSHA256Hash { + self.write(left.as_ref()); + self.write(right.as_ref()); + self.hash() + } +} + +#[cfg(test)] +mod tests { + use crate::RawLeafSha3Algorithm; + use merkle_light::{hash::Algorithm, merkle::MerkleTree}; + use std::hash::Hasher; + + #[test] + fn test_root() { + let results = [ + [ + 86, 124, 71, 168, 121, 121, 77, 212, 137, 162, 16, 222, 193, 125, 49, 204, 89, 25, + 188, 66, 125, 19, 141, 113, 106, 129, 7, 224, 37, 226, 219, 203, + ], + [ + 41, 66, 83, 171, 49, 203, 249, 13, 187, 190, 247, 85, 167, 95, 241, 96, 29, 167, + 144, 227, 92, 54, 95, 83, 14, 124, 26, 28, 169, 4, 220, 248, + ], + ]; + for (test_index, n_chunk) in [6, 7].into_iter().enumerate() { + let mut data = Vec::with_capacity(n_chunk); + for _ in 0..n_chunk { + let mut a = RawLeafSha3Algorithm::default(); + a.write(&[0; 256]); + data.push(a.hash()); + } + let mt = MerkleTree::<_, RawLeafSha3Algorithm>::new(data); + println!("{:?} {}", mt.root(), hex::encode(mt.root())); + assert_eq!(results[test_index], mt.root()); + } + } +} diff --git a/common/spec/Cargo.toml b/common/spec/Cargo.toml new file mode 100644 index 0000000..18e6ec8 --- /dev/null +++ b/common/spec/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "zgs_spec" +version = "0.1.0" +edition = "2021" + +[dependencies] \ No newline at end of file diff --git a/common/spec/src/lib.rs b/common/spec/src/lib.rs new file mode 100644 index 0000000..ed3ca68 --- /dev/null +++ b/common/spec/src/lib.rs @@ -0,0 +1,18 @@ +pub const KB: usize = 1024; +pub const MB: usize = 1024 * KB; +pub const GB: usize = 1024 * MB; +pub const TB: usize = 1024 * GB; + +pub const BYTES_PER_SECTOR: usize = 256; +pub const BYTES_PER_SEAL: usize = 4 * KB; +pub const BYTES_PER_SCRATCHPAD: usize = 64 * KB; +pub const BYTES_PER_LOAD: usize = 256 * KB; +pub const BYTES_PER_PRICING: usize = 8 * GB; +pub const BYTES_PER_MAX_MINING_RANGE: usize = 8 * TB; + +pub const SECTORS_PER_LOAD: usize = BYTES_PER_LOAD / BYTES_PER_SECTOR; +pub const SECTORS_PER_SEAL: usize = BYTES_PER_SEAL / BYTES_PER_SECTOR; +pub const SECTORS_PER_PRICING: usize = BYTES_PER_PRICING / BYTES_PER_SECTOR; +pub const SECTORS_PER_MAX_MINING_RANGE: usize = BYTES_PER_MAX_MINING_RANGE / BYTES_PER_SECTOR; + +pub const SEALS_PER_LOAD: usize = BYTES_PER_LOAD / BYTES_PER_SEAL; diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml new file mode 100644 index 0000000..c4c0b44 --- /dev/null +++ b/common/task_executor/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "task_executor" +version = "0.1.0" +authors = ["Sigma Prime "] +edition = "2021" + +[dependencies] +exit-future = "0.2.0" +futures = "0.3.21" +lazy_static = "1.4.0" +lighthouse_metrics = { path = "../lighthouse_metrics" } +tokio = { version = "1.19.2", features = ["rt"] } +tracing = "0.1.35" diff --git a/common/task_executor/src/lib.rs b/common/task_executor/src/lib.rs new file mode 100644 index 0000000..1a7490c --- /dev/null +++ b/common/task_executor/src/lib.rs @@ -0,0 +1,369 @@ +#[macro_use] +extern crate tracing; + +mod metrics; +pub mod test_utils; + +use futures::channel::mpsc::Sender; +use futures::prelude::*; +use std::sync::Weak; +use tokio::runtime::{Handle, Runtime}; + +pub use tokio::task::JoinHandle; + +/// Provides a reason when node is shut down. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ShutdownReason { + /// The node shut down successfully. + Success(&'static str), + /// The node shut down due to an error condition. + Failure(&'static str), +} + +impl ShutdownReason { + pub fn message(&self) -> &'static str { + match self { + ShutdownReason::Success(msg) => msg, + ShutdownReason::Failure(msg) => msg, + } + } +} + +/// Provides a `Handle` by either: +/// +/// 1. Holding a `Weak` and calling `Runtime::handle`. +/// 2. Directly holding a `Handle` and cloning it. +/// +/// This enum allows the `TaskExecutor` to work in production where a `Weak` is directly +/// accessible and in testing where the `Runtime` is hidden outside our scope. +#[derive(Clone)] +pub enum HandleProvider { + Runtime(Weak), + Handle(Handle), +} + +impl From for HandleProvider { + fn from(handle: Handle) -> Self { + HandleProvider::Handle(handle) + } +} + +impl From> for HandleProvider { + fn from(weak_runtime: Weak) -> Self { + HandleProvider::Runtime(weak_runtime) + } +} + +impl HandleProvider { + /// Returns a `Handle` to a `Runtime`. + /// + /// May return `None` if the weak reference to the `Runtime` has been dropped (this generally + /// means Lighthouse is shutting down). + pub fn handle(&self) -> Option { + match self { + HandleProvider::Runtime(weak_runtime) => weak_runtime + .upgrade() + .map(|runtime| runtime.handle().clone()), + HandleProvider::Handle(handle) => Some(handle.clone()), + } + } +} + +/// A wrapper over a runtime handle which can spawn async and blocking tasks. +#[derive(Clone)] +pub struct TaskExecutor { + /// The handle to the runtime on which tasks are spawned + handle_provider: HandleProvider, + /// The receiver exit future which on receiving shuts down the task + exit: exit_future::Exit, + /// Sender given to tasks, so that if they encounter a state in which execution cannot + /// continue they can request that everything shuts down. + /// + /// The task must provide a reason for shutting down. + signal_tx: Sender, +} + +impl TaskExecutor { + /// Create a new task executor. + /// + /// ## Note + /// + /// This function should only be used during testing. In production, prefer to obtain an + /// instance of `Self` via a `environment::RuntimeContext` (see the `lighthouse/environment` + /// crate). + pub fn new>( + handle: T, + exit: exit_future::Exit, + signal_tx: Sender, + ) -> Self { + Self { + handle_provider: handle.into(), + exit, + signal_tx, + } + } + + /// Clones the task executor adding a service name. + pub fn clone_with_name(&self) -> Self { + TaskExecutor { + handle_provider: self.handle_provider.clone(), + exit: self.exit.clone(), + signal_tx: self.signal_tx.clone(), + } + } + + /// A convenience wrapper for `Self::spawn` which ignores a `Result` as long as both `Ok`/`Err` + /// are of type `()`. + /// + /// The purpose of this function is to create a compile error if some function which previously + /// returned `()` starts returning something else. Such a case may otherwise result in + /// accidental error suppression. + pub fn spawn_ignoring_error( + &self, + task: impl Future> + Send + 'static, + name: &'static str, + ) { + self.spawn(task.map(|_| ()), name) + } + + /// Spawn a task to monitor the completion of another task. + /// + /// If the other task exits by panicking, then the monitor task will shut down the executor. + fn spawn_monitor( + &self, + task_handle: impl Future> + Send + 'static, + name: &'static str, + ) { + let mut shutdown_sender = self.shutdown_sender(); + + if let Some(handle) = self.handle() { + handle.spawn(async move { + let timer = metrics::start_timer_vec(&metrics::TASKS_HISTOGRAM, &[name]); + if let Err(join_error) = task_handle.await { + if let Ok(panic) = join_error.try_into_panic() { + let message = panic.downcast_ref::<&str>().unwrap_or(&""); + + error!( + task_name = name, + message = message, + advice = "Please check above for a backtrace and notify \ + the developers", + "Task panic. This is a bug!", + ); + let _ = shutdown_sender + .try_send(ShutdownReason::Failure("Panic (fatal error)")); + } + } + drop(timer); + }); + } else { + debug!("Couldn't spawn monitor task. Runtime shutting down") + } + } + + /// Spawn a future on the tokio runtime. + /// + /// The future is wrapped in an `exit_future::Exit`. The task is cancelled when the corresponding + /// exit_future `Signal` is fired/dropped. + /// + /// The future is monitored via another spawned future to ensure that it doesn't panic. In case + /// of a panic, the executor will be shut down via `self.signal_tx`. + /// + /// This function generates prometheus metrics on number of tasks and task duration. + pub fn spawn(&self, task: impl Future + Send + 'static, name: &'static str) { + if let Some(task_handle) = self.spawn_handle(task, name) { + self.spawn_monitor(task_handle, name) + } + } + + /// Spawn a future on the tokio runtime. This function does not wrap the task in an `exit_future::Exit` + /// like [spawn](#method.spawn). + /// The caller of this function is responsible for wrapping up the task with an `exit_future::Exit` to + /// ensure that the task gets canceled appropriately. + /// This function generates prometheus metrics on number of tasks and task duration. + /// + /// This is useful in cases where the future to be spawned needs to do additional cleanup work when + /// the task is completed/canceled (e.g. writing local variables to disk) or the task is created from + /// some framework which does its own cleanup (e.g. a hyper server). + pub fn spawn_without_exit( + &self, + task: impl Future + Send + 'static, + name: &'static str, + ) { + if let Some(int_gauge) = metrics::get_int_gauge(&metrics::ASYNC_TASKS_COUNT, &[name]) { + let int_gauge_1 = int_gauge.clone(); + let future = task.then(move |_| { + int_gauge_1.dec(); + futures::future::ready(()) + }); + + int_gauge.inc(); + if let Some(handle) = self.handle() { + handle.spawn(future); + } else { + debug!("Couldn't spawn task. Runtime shutting down"); + } + } + } + + /// Spawn a blocking task on a dedicated tokio thread pool wrapped in an exit future. + /// This function generates prometheus metrics on number of tasks and task duration. + pub fn spawn_blocking(&self, task: F, name: &'static str) + where + F: FnOnce() + Send + 'static, + { + if let Some(task_handle) = self.spawn_blocking_handle(task, name) { + self.spawn_monitor(task_handle, name) + } + } + + /// Spawn a future on the tokio runtime wrapped in an `exit_future::Exit` returning an optional + /// join handle to the future. + /// The task is canceled when the corresponding exit_future `Signal` is fired/dropped. + /// + /// This function generates prometheus metrics on number of tasks and task duration. + pub fn spawn_handle( + &self, + task: impl Future + Send + 'static, + name: &'static str, + ) -> Option>> { + let exit = self.exit.clone(); + + if let Some(int_gauge) = metrics::get_int_gauge(&metrics::ASYNC_TASKS_COUNT, &[name]) { + // Task is shutdown before it completes if `exit` receives + let int_gauge_1 = int_gauge.clone(); + let future = future::select(Box::pin(task), exit).then(move |either| { + let result = match either { + future::Either::Left((value, _)) => { + trace!(task = name, "Async task completed"); + Some(value) + } + future::Either::Right(_) => { + debug!(task = name, "Async task shutdown, exit received"); + None + } + }; + int_gauge_1.dec(); + futures::future::ready(result) + }); + + int_gauge.inc(); + if let Some(handle) = self.handle() { + Some(handle.spawn(future)) + } else { + debug!("Couldn't spawn task. Runtime shutting down"); + None + } + } else { + None + } + } + + /// Spawn a blocking task on a dedicated tokio thread pool wrapped in an exit future returning + /// a join handle to the future. + /// If the runtime doesn't exist, this will return None. + /// The Future returned behaves like the standard JoinHandle which can return an error if the + /// task failed. + /// This function generates prometheus metrics on number of tasks and task duration. + pub fn spawn_blocking_handle( + &self, + task: F, + name: &'static str, + ) -> Option>> + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + let timer = metrics::start_timer_vec(&metrics::BLOCKING_TASKS_HISTOGRAM, &[name]); + metrics::inc_gauge_vec(&metrics::BLOCKING_TASKS_COUNT, &[name]); + + let join_handle = if let Some(handle) = self.handle() { + handle.spawn_blocking(task) + } else { + debug!("Couldn't spawn task. Runtime shutting down"); + return None; + }; + + let future = async move { + let result = match join_handle.await { + Ok(result) => { + trace!(task = name, "Blocking task completed"); + Ok(result) + } + Err(e) => { + debug!(error = %e, "Blocking task ended unexpectedly"); + Err(e) + } + }; + drop(timer); + metrics::dec_gauge_vec(&metrics::BLOCKING_TASKS_COUNT, &[name]); + result + }; + + Some(future) + } + + /// Block the current (non-async) thread on the completion of some future. + /// + /// ## Warning + /// + /// This method is "dangerous" since calling it from an async thread will result in a panic! Any + /// use of this outside of testing should be very deeply considered as Lighthouse has been + /// burned by this function in the past. + /// + /// Determining what is an "async thread" is rather challenging; just because a function isn't + /// marked as `async` doesn't mean it's not being called from an `async` function or there isn't + /// a `tokio` context present in the thread-local storage due to some `rayon` funkiness. Talk to + /// @paulhauner if you plan to use this function in production. He has put metrics in here to + /// track any use of it, so don't think you can pull a sneaky one on him. + pub fn block_on_dangerous( + &self, + future: F, + name: &'static str, + ) -> Option { + let timer = metrics::start_timer_vec(&metrics::BLOCK_ON_TASKS_HISTOGRAM, &[name]); + metrics::inc_gauge_vec(&metrics::BLOCK_ON_TASKS_COUNT, &[name]); + let handle = self.handle()?; + let exit = self.exit.clone(); + + debug!(name = "name", "Starting block_on task"); + + handle.block_on(async { + let output = tokio::select! { + output = future => { + debug!( + name = "name", + "Completed block_on task" + ); + Some(output) + }, + _ = exit => { + debug!( + name = "name", + "Cancelled block_on task" + + ); + None + } + }; + metrics::dec_gauge_vec(&metrics::BLOCK_ON_TASKS_COUNT, &[name]); + drop(timer); + output + }) + } + + /// Returns a `Handle` to the current runtime. + pub fn handle(&self) -> Option { + self.handle_provider.handle() + } + + /// Returns a copy of the `exit_future::Exit`. + pub fn exit(&self) -> exit_future::Exit { + self.exit.clone() + } + + /// Get a channel to request shutting down. + pub fn shutdown_sender(&self) -> Sender { + self.signal_tx.clone() + } +} diff --git a/common/task_executor/src/metrics.rs b/common/task_executor/src/metrics.rs new file mode 100644 index 0000000..6ecea86 --- /dev/null +++ b/common/task_executor/src/metrics.rs @@ -0,0 +1,36 @@ +/// Handles async task metrics +use lazy_static::lazy_static; +pub use lighthouse_metrics::*; + +lazy_static! { + pub static ref ASYNC_TASKS_COUNT: Result = try_create_int_gauge_vec( + "async_tasks_count", + "Total number of async tasks spawned using spawn", + &["async_task_count"] + ); + pub static ref BLOCKING_TASKS_COUNT: Result = try_create_int_gauge_vec( + "blocking_tasks_count", + "Total number of async tasks spawned using spawn_blocking", + &["blocking_task_count"] + ); + pub static ref BLOCKING_TASKS_HISTOGRAM: Result = try_create_histogram_vec( + "blocking_tasks_histogram", + "Time taken by blocking tasks", + &["blocking_task_hist"] + ); + pub static ref BLOCK_ON_TASKS_COUNT: Result = try_create_int_gauge_vec( + "block_on_tasks_count", + "Total number of block_on_dangerous tasks spawned", + &["name"] + ); + pub static ref BLOCK_ON_TASKS_HISTOGRAM: Result = try_create_histogram_vec( + "block_on_tasks_histogram", + "Time taken by block_on_dangerous tasks", + &["name"] + ); + pub static ref TASKS_HISTOGRAM: Result = try_create_histogram_vec( + "async_tasks_time_histogram", + "Time taken by async tasks", + &["async_task_hist"] + ); +} diff --git a/common/task_executor/src/test_utils.rs b/common/task_executor/src/test_utils.rs new file mode 100644 index 0000000..137e2c1 --- /dev/null +++ b/common/task_executor/src/test_utils.rs @@ -0,0 +1,51 @@ +use crate::TaskExecutor; +use std::sync::Arc; +use tokio::runtime; + +/// Whilst the `TestRuntime` is not necessarily useful in itself, it provides the necessary +/// components for creating a `TaskExecutor` during tests. +/// +/// May create its own runtime or use an existing one. +/// +/// ## Warning +/// +/// This struct should never be used in production, only testing. +pub struct TestRuntime { + runtime: Option>, + _runtime_shutdown: exit_future::Signal, + pub task_executor: TaskExecutor, +} + +impl Default for TestRuntime { + /// If called *inside* an existing runtime, instantiates `Self` using a handle to that runtime. If + /// called *outside* any existing runtime, create a new `Runtime` and keep it alive until the + /// `Self` is dropped. + fn default() -> Self { + let (runtime_shutdown, exit) = exit_future::signal(); + let (shutdown_tx, _) = futures::channel::mpsc::channel(1); + + let runtime = Arc::new( + runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(), + ); + let handle = runtime.handle().clone(); + + let task_executor = TaskExecutor::new(handle, exit, shutdown_tx); + + Self { + runtime: Some(runtime), + _runtime_shutdown: runtime_shutdown, + task_executor, + } + } +} + +impl Drop for TestRuntime { + fn drop(&mut self) { + if let Some(runtime) = self.runtime.take() { + Arc::try_unwrap(runtime).unwrap().shutdown_background() + } + } +} diff --git a/common/unused_port/Cargo.toml b/common/unused_port/Cargo.toml new file mode 100644 index 0000000..06c1ca8 --- /dev/null +++ b/common/unused_port/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "unused_port" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/common/unused_port/src/lib.rs b/common/unused_port/src/lib.rs new file mode 100644 index 0000000..4a8cf17 --- /dev/null +++ b/common/unused_port/src/lib.rs @@ -0,0 +1,55 @@ +use std::net::{TcpListener, UdpSocket}; + +#[derive(Copy, Clone)] +pub enum Transport { + Tcp, + Udp, +} + +/// A convenience function for `unused_port(Transport::Tcp)`. +pub fn unused_tcp_port() -> Result { + unused_port(Transport::Tcp) +} + +/// A convenience function for `unused_port(Transport::Tcp)`. +pub fn unused_udp_port() -> Result { + unused_port(Transport::Udp) +} + +/// A bit of hack to find an unused port. +/// +/// Does not guarantee that the given port is unused after the function exits, just that it was +/// unused before the function started (i.e., it does not reserve a port). +/// +/// ## Notes +/// +/// It is possible that users are unable to bind to the ports returned by this function as the OS +/// has a buffer period where it doesn't allow binding to the same port even after the socket is +/// closed. We might have to use SO_REUSEADDR socket option from `std::net2` crate in that case. +pub fn unused_port(transport: Transport) -> Result { + let local_addr = match transport { + Transport::Tcp => { + let listener = TcpListener::bind("127.0.0.1:0").map_err(|e| { + format!("Failed to create TCP listener to find unused port: {:?}", e) + })?; + listener.local_addr().map_err(|e| { + format!( + "Failed to read TCP listener local_addr to find unused port: {:?}", + e + ) + })? + } + Transport::Udp => { + let socket = UdpSocket::bind("127.0.0.1:0") + .map_err(|e| format!("Failed to create UDP socket to find unused port: {:?}", e))?; + socket.local_addr().map_err(|e| { + format!( + "Failed to read UDP socket local_addr to find unused port: {:?}", + e + ) + })? + } + }; + + Ok(local_addr.port()) +} diff --git a/common/zgs_seal/Cargo.toml b/common/zgs_seal/Cargo.toml new file mode 100644 index 0000000..f2d1960 --- /dev/null +++ b/common/zgs_seal/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "zgs_seal" +version = "0.1.0" +edition = "2021" + +[dependencies] +ethereum-types = "0.14" +zgs_spec = {path = "../spec"} +tiny-keccak = { version = "2.0.2", features = ["keccak"] } \ No newline at end of file diff --git a/common/zgs_seal/src/lib.rs b/common/zgs_seal/src/lib.rs new file mode 100644 index 0000000..a33ee42 --- /dev/null +++ b/common/zgs_seal/src/lib.rs @@ -0,0 +1,62 @@ +use ethereum_types::H256; +use tiny_keccak::{Hasher, Keccak}; + +pub fn compute_first_mask_seed( + miner_id: &H256, + context_digest: &H256, + start_sector: u64, +) -> [u8; 96] { + let mut output = [0u8; 96]; + output[0..32].copy_from_slice(&miner_id.0); + output[32..64].copy_from_slice(&context_digest.0); + output[88..96].clone_from_slice(&start_sector.to_be_bytes()); + output +} + +fn keccak(input: impl AsRef<[u8]>) -> [u8; 32] { + let mut hasher = Keccak::v256(); + let mut output = [0u8; 32]; + hasher.update(input.as_ref()); + hasher.finalize(&mut output); + output +} + +pub fn seal_with_mask(data: &mut [u8], first_mask: [u8; 32]) { + assert!(data.len() % 32 == 0); + let mut mask = first_mask; + for word in data.chunks_mut(32) { + word.iter_mut().zip(mask.iter()).for_each(|(x, y)| *x ^= *y); + mask = keccak(&*word); + } +} + +pub fn unseal_with_mask(data: &mut [u8], first_mask: [u8; 32]) { + assert!(data.len() % 32 == 0); + + let mut mask = first_mask; + data.chunks_exact_mut(32).for_each(|x| { + let next_mask = keccak(&*x); + x.iter_mut() + .zip(mask.iter()) + .for_each(|(x, mask)| *x ^= *mask); + mask = next_mask; + }) +} + +pub fn seal_with_mask_seed(data: &mut [u8], first_mask_seed: impl AsRef<[u8]>) { + seal_with_mask(data, keccak(first_mask_seed)) +} + +pub fn unseal_with_mask_seed(data: &mut [u8], first_mask_seed: impl AsRef<[u8]>) { + unseal_with_mask(data, keccak(first_mask_seed)) +} + +pub fn seal(data: &mut [u8], miner_id: &H256, context_digest: &H256, start_sector: u64) { + let first_mask_seed = compute_first_mask_seed(miner_id, context_digest, start_sector); + seal_with_mask_seed(data, first_mask_seed) +} + +pub fn unseal(data: &mut [u8], miner_id: &H256, context_digest: &H256, start_sector: u64) { + let first_mask_seed = compute_first_mask_seed(miner_id, context_digest, start_sector); + unseal_with_mask_seed(data, first_mask_seed) +} diff --git a/common/zgs_version/Cargo.toml b/common/zgs_version/Cargo.toml new file mode 100644 index 0000000..f3f2049 --- /dev/null +++ b/common/zgs_version/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "zgs_version" +version = "0.1.0" +authors = ["Paul Hauner "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +git-version = "0.3.5" +target_info = "0.1.0" + +[dev-dependencies] +regex = "1.5.6" diff --git a/common/zgs_version/src/lib.rs b/common/zgs_version/src/lib.rs new file mode 100644 index 0000000..e0c7d5e --- /dev/null +++ b/common/zgs_version/src/lib.rs @@ -0,0 +1,47 @@ +use git_version::git_version; +use target_info::Target; + +/// Returns the current version of this build of Lighthouse. +/// +/// A plus-sign (`+`) is appended to the git commit if the tree is dirty. +/// +/// ## Example +/// +/// `Lighthouse/v1.5.1-67da032+` +pub const VERSION: &str = git_version!( + args = [ + "--always", + "--dirty=+", + "--abbrev=7", + // NOTE: using --match instead of --exclude for compatibility with old Git + "--match=thiswillnevermatchlol" + ], + prefix = "zgs/v0.0.1-", + fallback = "unknown" +); + +/// Returns `VERSION`, but with platform information appended to the end. +/// +/// ## Example +/// +/// `zgs/v0.0.1-67da032+/x86_64-linux` +pub fn version_with_platform() -> String { + format!("{}/{}-{}", VERSION, Target::arch(), Target::os()) +} + +#[cfg(test)] +mod test { + use super::*; + use regex::Regex; + + #[test] + fn version_formatting() { + let re = + Regex::new(r"^zgs/v[0-9]+\.[0-9]+\.[0-9]+(-rc.[0-9])?-[[:xdigit:]]{7}\+?$").unwrap(); + assert!( + re.is_match(VERSION), + "version doesn't match regex: {}", + VERSION + ); + } +} diff --git a/doc/install.md b/doc/install.md new file mode 100644 index 0000000..d1207ac --- /dev/null +++ b/doc/install.md @@ -0,0 +1,70 @@ +# Install +ZeroGStorage requires Rust 1.71.0 and Go to build. + +## Install Rust + +We recommend installing Rust through [rustup](https://www.rustup.rs/). + +* Linux + + Install Rust + ```shell + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + rustup install 1.65.0 + ``` + + Other dependencies + * Ubuntu + ```shell + sudo apt-get install clang cmake build-essential + ``` + +* Mac + + Install Rust + ```shell + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + rustup install 1.65.0 + ``` + + ```shell + brew install llvm cmake + ``` + +* Windows + Download and run the rustup installer from [this link](https://static.rust-lang.org/rustup/dist/i686-pc-windows-gnu/rustup-init.exe). + Install LLVM, pre-built binaries can be downloaded from [this link](https://releases.llvm.org/download.html). + +## Install Go +* Linux + ```shell + # Download the Go installer + wget https://go.dev/dl/go1.19.3.linux-amd64.tar.gz + + # Extract the archive + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.19.3.linux-amd64.tar.gz + + # Add /usr/local/go/bin to the PATH environment variable by adding the following line to your ~/.profile. + export PATH=$PATH:/usr/local/go/bin + ``` + +* Mac + + Download the Go installer from https://go.dev/dl/go1.19.3.darwin-amd64.pkg. + Open the package file you downloaded and follow the prompts to install Go. + +* Windows + Download the Go installer from https://go.dev/dl/go1.19.3.windows-amd64.msi. + Open the MSI file you downloaded and follow the prompts to install Go. + + +## Build from source +```shell +# Download code +$ git clone https://github.com/zero-gravity-labs/zerog-storage-rust.git +$ cd zerog-storage-rust +$ git submodule update --init + +# Build in release mode +$ cargo build --release +``` diff --git a/doc/run.md b/doc/run.md new file mode 100644 index 0000000..3c2e4d7 --- /dev/null +++ b/doc/run.md @@ -0,0 +1,91 @@ +# Run + +## Deploy contract: Token, Flow and Mine contracts + +### Setup Environment + +Install dependencies Node.js, yarn, hardhat. + +* Linux + * Ubuntu + ```shell + # node >=12.18 + sudo apt install npm + sudo npm install --global yarn + sudo npm install --global hardhat + ``` + +* Mac + ```shell + brew install node + sudo npm install --global yarn + sudo npm install --global hardhat + ``` + +* Windows +Download and install node from [here](https://nodejs.org/en/download/) + ```shell + npm install --global yarn + npm install --global hardhat + ``` + +### Download contract source code +```shell +git clone https://github.com/zero-gravity-labs/zerog-storage-contracts.git +cd zerog-storage-contracts +``` + +Add target network to your hardhat.config.js, i.e. +```shell +# example +networks: { + targetNetwork: { + url: "******", + accounts: [ + "******", + ], + }, + }, +``` + +### Compile +```shell +yarn +yarn compile +``` + +### Deploy contract +```shell +npx hardhat run scripts/deploy.ts --network targetnetwork +``` + +Keep contracts addresses + +## Run ZeroGStorage +Update coinfig run/config.toml as required: + +```shell +# p2p port +network_libp2p_port + +# rpc endpoint +rpc_listen_address + +# peer nodes +network_libp2p_nodes + +# flow contract address +log_contract_address + +# mine contract address +mine_contract_address + +# layer one blockchain rpc endpoint +blockchain_rpc_endpoint +``` + +Run node +```shell +cd run +../target/release/zgs_node --config config.toml +``` diff --git a/node/.gitignore b/node/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/node/.gitignore @@ -0,0 +1 @@ +/target diff --git a/node/Cargo.toml b/node/Cargo.toml new file mode 100644 index 0000000..e7bc4d1 --- /dev/null +++ b/node/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "zgs_node" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +clap = { version = "3.2.5", features = ["cargo"] } +ctrlc = "3.2.2" +error-chain = "0.12.4" +ethereum-types = "0.14" +exit-future = "0.2.0" +futures = "0.3.21" +file_location_cache = { path = "file_location_cache" } +zgs_version = { path = "../common/zgs_version" } +log_entry_sync = { path = "./log_entry_sync" } +miner = { path = "./miner" } +network = { path = "./network" } +router = { path = "./router" } +rpc = { path = "./rpc" } +shared_types = { path = "./shared_types" } +storage = { path = "./storage" } +storage-async = { path = "./storage-async" } +sync = { path = "./sync" } +task_executor = { path = "../common/task_executor" } +tokio = { version = "1.19.2", features = ["full"] } +tokio-stream = { version = "0.1.9", features = ["sync"] } +toml = "0.5.9" +tracing = "0.1.35" +tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } +tracing-appender = { version = "0.2.2" } +chunk_pool = { path = "./chunk_pool" } +itertools = "0.10.5" +serde = { version = "1.0.137", features = ["derive"] } +duration-str = "0.5.1" +config = "0.13.1" + +[dependencies.libp2p] +version = "0.45.1" +default-features = true +features = ["websocket", "identify", "mplex", "yamux", "noise", "gossipsub", "dns-tokio", "tcp-tokio", "plaintext", "secp256k1"] diff --git a/node/build.rs b/node/build.rs new file mode 100644 index 0000000..d6f58bb --- /dev/null +++ b/node/build.rs @@ -0,0 +1,13 @@ +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-changed=../zerog-storage-client"); + + let status = Command::new("go") + .current_dir("../zerog-storage-client") + .args(vec!["build", "-o", "../target"]) + .status() + .unwrap(); + + println!("build zerog-storage-client with status {}", status); +} diff --git a/node/chunk_pool/Cargo.toml b/node/chunk_pool/Cargo.toml new file mode 100644 index 0000000..ab18769 --- /dev/null +++ b/node/chunk_pool/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "chunk_pool" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +shared_types = { path = "../shared_types" } +storage-async = { path = "../storage-async" } +log_entry_sync = { path = "../log_entry_sync" } +network = { path = "../network" } +tokio = { version = "1.19.2", features = ["sync"] } +async-lock = "2.5.0" +hashlink = "0.8.0" +tracing = "0.1.35" diff --git a/node/chunk_pool/src/handler.rs b/node/chunk_pool/src/handler.rs new file mode 100644 index 0000000..1966e7f --- /dev/null +++ b/node/chunk_pool/src/handler.rs @@ -0,0 +1,93 @@ +use super::mem_pool::MemoryChunkPool; +use crate::mem_pool::FileID; +use anyhow::Result; +use network::NetworkMessage; +use shared_types::ChunkArray; +use std::sync::Arc; +use storage_async::Store; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +/// Handle the cached file when uploaded completely and verified from blockchain. +/// Generally, the file will be persisted into log store. +pub struct ChunkPoolHandler { + receiver: UnboundedReceiver, + mem_pool: Arc, + log_store: Store, + sender: UnboundedSender, +} + +impl ChunkPoolHandler { + pub(crate) fn new( + receiver: UnboundedReceiver, + mem_pool: Arc, + log_store: Store, + sender: UnboundedSender, + ) -> Self { + ChunkPoolHandler { + receiver, + mem_pool, + log_store, + sender, + } + } + + /// Writes memory cached chunks into store and finalize transaction. + /// Note, a separate thread should be spawned to call this method. + pub async fn handle(&mut self) -> Result { + let id = match self.receiver.recv().await { + Some(id) => id, + None => return Ok(false), + }; + + debug!(?id, "Received task to finalize transaction"); + + // TODO(qhz): remove from memory pool after transaction finalized, + // when store support to write chunks with reference. + if let Some(file) = self.mem_pool.remove_cached_file(&id.root).await { + // If there is still cache of chunks, write them into store + let mut segments: Vec = file.segments.into_values().collect(); + while let Some(seg) = segments.pop() { + if !self + .log_store + .put_chunks_with_tx_hash(id.tx_id.seq, id.tx_id.hash, seg) + .await? + { + return Ok(false); + } + } + } + + if !self + .log_store + .finalize_tx_with_hash(id.tx_id.seq, id.tx_id.hash) + .await? + { + return Ok(false); + } + + debug!(?id, "Transaction finalized"); + + // always remove file from pool after transaction finalized + self.mem_pool.remove_file(&id.root).await; + + let msg = NetworkMessage::AnnounceLocalFile { tx_id: id.tx_id }; + if let Err(e) = self.sender.send(msg) { + error!( + "Failed to send NetworkMessage::AnnounceLocalFile message, tx_seq={}, err={}", + id.tx_id.seq, e + ); + } + + Ok(true) + } + + pub async fn run(mut self) { + info!("Worker started to finalize transactions"); + + loop { + if let Err(e) = self.handle().await { + warn!("Failed to write chunks or finalize transaction, {:?}", e); + } + } + } +} diff --git a/node/chunk_pool/src/lib.rs b/node/chunk_pool/src/lib.rs new file mode 100644 index 0000000..1e92d8b --- /dev/null +++ b/node/chunk_pool/src/lib.rs @@ -0,0 +1,38 @@ +#[macro_use] +extern crate tracing; + +mod handler; +mod mem_pool; + +pub use handler::ChunkPoolHandler; +pub use mem_pool::{FileID, MemoryChunkPool, SegmentInfo}; + +use std::sync::Arc; +use std::time::Duration; + +#[derive(Clone, Copy, Debug)] +pub struct Config { + pub write_window_size: usize, + pub max_cached_chunks_all: usize, + pub max_writings: usize, + pub expiration_time_secs: u64, +} + +impl Config { + pub fn expiration_time(&self) -> Duration { + Duration::from_secs(self.expiration_time_secs) + } +} + +pub fn unbounded( + config: Config, + log_store: storage_async::Store, + network_send: tokio::sync::mpsc::UnboundedSender, +) -> (Arc, ChunkPoolHandler) { + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + + let mem_pool = Arc::new(MemoryChunkPool::new(config, log_store.clone(), sender)); + let handler = ChunkPoolHandler::new(receiver, mem_pool.clone(), log_store, network_send); + + (mem_pool, handler) +} diff --git a/node/chunk_pool/src/mem_pool/chunk_cache.rs b/node/chunk_pool/src/mem_pool/chunk_cache.rs new file mode 100644 index 0000000..5e525e9 --- /dev/null +++ b/node/chunk_pool/src/mem_pool/chunk_cache.rs @@ -0,0 +1,158 @@ +use super::FileID; +use crate::{Config, SegmentInfo}; +use anyhow::{bail, Result}; +use hashlink::LinkedHashMap; +use shared_types::{bytes_to_chunks, ChunkArray, DataRoot, Transaction, CHUNK_SIZE}; +use std::collections::HashMap; +use std::ops::Add; +use std::time::{Duration, Instant}; + +/// Used to cache chunks in memory pool and persist into db once log entry +/// retrieved from blockchain. +pub struct MemoryCachedFile { + pub id: FileID, + /// Window to control the cache of each file + pub segments: HashMap, + /// Total number of chunks for the cache file, which is updated from log entry. + pub total_chunks: usize, + /// Used for garbage collection. It is updated when new segment uploaded. + expired_at: Instant, + /// Number of chunks that's currently cached for this file + pub cached_chunk_num: usize, +} + +impl MemoryCachedFile { + fn new(root: DataRoot, timeout: Duration) -> Self { + MemoryCachedFile { + id: FileID { + root, + tx_id: Default::default(), + }, + segments: HashMap::default(), + total_chunks: 0, + expired_at: Instant::now().add(timeout), + cached_chunk_num: 0, + } + } + + /// Updates file with transaction once log entry retrieved from blockchain. + /// So that, write memory cached segments into database. + pub fn update_with_tx(&mut self, tx: &Transaction) { + self.total_chunks = bytes_to_chunks(tx.size as usize); + self.id.tx_id = tx.id(); + } + + fn update_expiration_time(&mut self, timeout: Duration) { + self.expired_at = Instant::now().add(timeout); + } + + #[allow(unused)] + fn is_completed(&self) -> bool { + self.total_chunks > 0 && self.cached_chunk_num >= self.total_chunks + } + + fn should_flush(&self) -> bool { + self.total_chunks > 0 && self.cached_chunk_num > 0 + } +} + +/// ChunkPoolCache is used to cache small files that log entry not retrieved +/// from L1 blockchain yet. +pub struct ChunkPoolCache { + config: Config, + /// All cached files. + /// Note, file root is used as key instead of `tx_seq`, since log entry + /// not retrieved yet. + files: LinkedHashMap, + /// Total number of chunks that cached in the memory pool. + pub total_chunks: usize, +} + +impl ChunkPoolCache { + pub fn new(config: Config) -> Self { + ChunkPoolCache { + config, + files: LinkedHashMap::default(), + total_chunks: 0, + } + } + + pub fn get_file(&self, root: &DataRoot) -> Option<&MemoryCachedFile> { + self.files.get(root) + } + + pub fn get_file_mut(&mut self, root: &DataRoot) -> Option<&mut MemoryCachedFile> { + self.files.get_mut(root) + } + + pub fn remove_file(&mut self, root: &DataRoot) -> Option { + let file = self.files.remove(root)?; + self.update_total_chunks_when_remove_file(&file); + Some(file) + } + + /// Remove files that no new segment uploaded for a long time. + /// + /// Note, when log sync delayed, files may be also garbage collected if the + /// entire file uploaded. Because, it is hard to check if log sync delayed + /// or user upload an invalid file, e.g. for attack purpose. + /// + /// Once garbage collected, user could simply upload the entire file again, + /// which is fast enough due to small file size. + fn garbage_collect(&mut self) { + let now = Instant::now(); + + while let Some((_, file)) = self.files.front() { + if file.expired_at > now { + return; + } + + if let Some((r, f)) = self.files.pop_front() { + self.update_total_chunks_when_remove_file(&f); + debug!("Garbage collected for file {}", r); + } + } + } + + fn update_total_chunks_when_remove_file(&mut self, file: &MemoryCachedFile) { + assert!(self.total_chunks >= file.cached_chunk_num); + self.total_chunks -= file.cached_chunk_num; + } + + /// Caches the specified segment in memory. + /// + /// Returns if there are cached segments and log entry also retrieved. + pub fn cache_segment(&mut self, seg_info: SegmentInfo) -> Result { + // always GC at first + self.garbage_collect(); + + let file = self + .files + .entry(seg_info.root) + .or_insert_with(|| MemoryCachedFile::new(seg_info.root, self.config.expiration_time())); + + // Segment already cached in memory. Directly return OK + if file.segments.contains_key(&seg_info.seg_index) { + return Ok(file.should_flush()); + } + + // Otherwise, just cache segment in memory + let num_chunks = seg_info.seg_data.len() / CHUNK_SIZE; + + // Limits the cached chunks in the memory pool. + if self.total_chunks + num_chunks > self.config.max_cached_chunks_all { + bail!( + "exceeds the maximum cached chunks of whole pool: {}", + self.config.max_cached_chunks_all + ); + } + + // Cache segment and update the counter for cached chunks. + self.total_chunks += num_chunks; + file.cached_chunk_num += num_chunks; + file.update_expiration_time(self.config.expiration_time()); + file.segments.insert(seg_info.seg_index, seg_info.into()); + + Ok(file.should_flush()) + } +} diff --git a/node/chunk_pool/src/mem_pool/chunk_pool_inner.rs b/node/chunk_pool/src/mem_pool/chunk_pool_inner.rs new file mode 100644 index 0000000..de8c37b --- /dev/null +++ b/node/chunk_pool/src/mem_pool/chunk_pool_inner.rs @@ -0,0 +1,332 @@ +use super::chunk_cache::{ChunkPoolCache, MemoryCachedFile}; +use super::chunk_write_control::ChunkPoolWriteCtrl; +use super::FileID; +use crate::Config; +use anyhow::{bail, Result}; +use async_lock::Mutex; +use log_entry_sync::LogSyncEvent; +use shared_types::{ + bytes_to_chunks, compute_segment_size, ChunkArray, DataRoot, Transaction, CHUNK_SIZE, +}; +use std::sync::Arc; +use storage_async::Store; +use tokio::sync::broadcast::{error::RecvError, Receiver}; +use tokio::sync::mpsc::UnboundedSender; + +struct Inner { + config: Config, + segment_cache: ChunkPoolCache, + write_control: ChunkPoolWriteCtrl, +} + +impl Inner { + fn new(config: Config) -> Self { + Inner { + config, + segment_cache: ChunkPoolCache::new(config), + write_control: ChunkPoolWriteCtrl::new(config), + } + } + + fn after_flush_cache(&mut self) { + assert!(self.write_control.total_writings > 0); + self.write_control.total_writings -= 1; + } + + /// Return the tx seq and all segments that belong to the root. + fn get_all_cached_segments_to_write( + &mut self, + root: &DataRoot, + ) -> Result<(FileID, Vec)> { + // Limits the number of writing threads. + if self.write_control.total_writings >= self.config.max_writings { + bail!("too many data writing: {}", self.config.max_writings); + } + + let file = match self.segment_cache.remove_file(root) { + Some(f) => f, + None => bail!("file not found to write into store {:?}", root), + }; + let id = file.id; + let segs = file.segments.into_values().collect(); + + self.write_control.total_writings += 1; + + Ok((id, segs)) + } +} + +pub struct SegmentInfo { + pub root: DataRoot, + pub seg_data: Vec, + pub seg_index: usize, + pub chunks_per_segment: usize, +} + +impl From for ChunkArray { + fn from(seg_info: SegmentInfo) -> Self { + let start_index = seg_info.seg_index * seg_info.chunks_per_segment; + ChunkArray { + data: seg_info.seg_data, + start_index: start_index as u64, + } + } +} + +/// Caches data chunks in memory before the entire file uploaded to storage node +/// and data root verified on blockchain. +pub struct MemoryChunkPool { + inner: Mutex, + log_store: Store, + sender: UnboundedSender, +} + +impl MemoryChunkPool { + pub(crate) fn new(config: Config, log_store: Store, sender: UnboundedSender) -> Self { + MemoryChunkPool { + inner: Mutex::new(Inner::new(config)), + log_store, + sender, + } + } + + pub fn validate_segment_size(&self, segment: &Vec) -> Result<()> { + if segment.is_empty() { + bail!("data is empty"); + } + + if segment.len() % CHUNK_SIZE != 0 { + bail!("invalid data length"); + } + + Ok(()) + } + + pub async fn cache_chunks(&self, seg_info: SegmentInfo) -> Result<()> { + let root = seg_info.root; + debug!("cache_chunks, root={:?} index={}", root, seg_info.seg_index); + let should_flush = self + .inner + .lock() + .await + .segment_cache + .cache_segment(seg_info)?; + + // store and finalize the cached file if completed + if should_flush { + debug!("cache_chunk: flush cached chunks"); + self.write_all_cached_chunks_and_finalize(root).await?; + } + + Ok(()) + } + + pub async fn write_chunks( + &self, + seg_info: SegmentInfo, + file_id: FileID, + file_size: usize, + ) -> Result<()> { + let total_chunks = bytes_to_chunks(file_size); + + debug!( + "Begin to write segment, root={}, segment_size={}, segment_index={}", + seg_info.root, + seg_info.seg_data.len(), + seg_info.seg_index, + ); + + //Write the segment in window + let (total_segments, _) = compute_segment_size(total_chunks, seg_info.chunks_per_segment); + self.inner.lock().await.write_control.write_segment( + file_id, + seg_info.seg_index, + total_segments, + )?; + + // Write memory cached segments into store. + // TODO(qhz): error handling + // 1. Push the failed segment back to front. (enhance store to return Err(ChunkArray)) + // 2. Put the incompleted segments back to memory pool. + let seg = ChunkArray { + data: seg_info.seg_data, + start_index: (seg_info.seg_index * seg_info.chunks_per_segment) as u64, + }; + + match self + .log_store + .put_chunks_with_tx_hash(file_id.tx_id.seq, file_id.tx_id.hash, seg) + .await + { + Ok(true) => {} + Ok(false) => { + self.inner + .lock() + .await + .write_control + .on_write_failed(&seg_info.root, seg_info.seg_index); + // remove the file if transaction reverted + self.inner + .lock() + .await + .write_control + .remove_file(&seg_info.root); + bail!("Transaction reverted, please upload again"); + } + Err(e) => { + self.inner + .lock() + .await + .write_control + .on_write_failed(&seg_info.root, seg_info.seg_index); + return Err(e); + } + } + + let all_uploaded = self + .inner + .lock() + .await + .write_control + .on_write_succeeded(&seg_info.root, seg_info.seg_index); + + // Notify to finalize transaction asynchronously. + if all_uploaded { + if let Err(e) = self.sender.send(file_id) { + // Channel receiver will not be dropped until program exit. + bail!("channel send error: {}", e); + } + debug!("Queue to finalize transaction for file {}", seg_info.root); + } + + Ok(()) + } + + /// Updates the cached file info when log entry retrieved from blockchain. + pub async fn update_file_info(&self, tx: &Transaction) -> Result { + let mut inner = self.inner.lock().await; + + // Do nothing if file not uploaded yet. + let file = match inner.segment_cache.get_file_mut(&tx.data_merkle_root) { + Some(f) => f, + None => return Ok(false), + }; + + // Update the file info with transaction. + file.update_with_tx(tx); + + // File partially uploaded and it's up to user thread + // to write chunks into store and finalize transaction. + if file.cached_chunk_num < file.total_chunks { + return Ok(true); + } + + // Otherwise, notify to write all memory cached chunks and finalize transaction. + let file_id = FileID { + root: tx.data_merkle_root, + tx_id: tx.id(), + }; + if let Err(e) = self.sender.send(file_id) { + // Channel receiver will not be dropped until program exit. + bail!("channel send error: {}", e); + } + + Ok(true) + } + + pub async fn monitor_log_entry(chunk_pool: Arc, mut receiver: Receiver) { + info!("Start to monitor log entry"); + + loop { + match receiver.recv().await { + Ok(LogSyncEvent::ReorgDetected { .. }) => {} + Ok(LogSyncEvent::Reverted { .. }) => {} + Ok(LogSyncEvent::TxSynced { tx }) => { + if let Err(_e) = chunk_pool.update_file_info(&tx).await { + error!( + "Failed to update file info. tx seq={}, tx_root={}", + tx.seq, tx.data_merkle_root + ); + } + } + Err(RecvError::Closed) => { + // program terminated + info!("Completed to monitor log entry"); + return; + } + Err(RecvError::Lagged(lagged)) => { + error!(%lagged, "Lagged messages: (Lagged)"); + } + } + } + } + + pub(crate) async fn remove_cached_file(&self, root: &DataRoot) -> Option { + self.inner.lock().await.segment_cache.remove_file(root) + } + + pub(crate) async fn remove_file(&self, root: &DataRoot) -> bool { + let mut inner = self.inner.lock().await; + inner.segment_cache.remove_file(root).is_some() + || inner.write_control.remove_file(root).is_some() + } + + pub async fn check_already_has_cache(&self, root: &DataRoot) -> bool { + self.inner + .lock() + .await + .segment_cache + .get_file(root) + .is_some() + } + + async fn write_all_cached_chunks_and_finalize(&self, root: DataRoot) -> Result<()> { + let (file, mut segments) = self + .inner + .lock() + .await + .get_all_cached_segments_to_write(&root)?; + + while let Some(seg) = segments.pop() { + // TODO(qhz): error handling + // 1. Push the failed segment back to front. (enhance store to return Err(ChunkArray)) + // 2. Put the incompleted segments back to memory pool. + match self + .log_store + .put_chunks_with_tx_hash(file.tx_id.seq, file.tx_id.hash, seg) + .await + { + Ok(true) => {} + Ok(false) => { + self.inner.lock().await.after_flush_cache(); + bail!("Transaction reverted, please upload again"); + } + Err(e) => { + self.inner.lock().await.after_flush_cache(); + return Err(e); + } + } + } + + self.inner.lock().await.after_flush_cache(); + + if let Err(e) = self.sender.send(file) { + // Channel receiver will not be dropped until program exit. + bail!("channel send error: {}", e); + } + + Ok(()) + } + + pub async fn get_uploaded_seg_num(&self, root: &DataRoot) -> (usize, bool) { + let inner = self.inner.lock().await; + + if let Some(file) = inner.segment_cache.get_file(root) { + (file.cached_chunk_num, true) + } else if let Some(file) = inner.write_control.get_file(root) { + (file.uploaded_seg_num(), false) + } else { + (0, false) + } + } +} diff --git a/node/chunk_pool/src/mem_pool/chunk_write_control.rs b/node/chunk_pool/src/mem_pool/chunk_write_control.rs new file mode 100644 index 0000000..6712236 --- /dev/null +++ b/node/chunk_pool/src/mem_pool/chunk_write_control.rs @@ -0,0 +1,200 @@ +use super::FileID; +use crate::Config; +use anyhow::{bail, Result}; +use shared_types::DataRoot; +use std::collections::HashMap; + +/// The segment status in sliding window +#[derive(PartialEq, Eq, Debug)] +enum SlotStatus { + Writing, // segment in writing + Finished, // segment uploaded in store +} + +/// Sliding window is used to control the concurrent uploading process of a file. +/// Bounded window allows segments to be uploaded concurrenly, while having a capacity +/// limit on writing threads per file. Meanwhile, the left_boundary field records +/// how many segments have been uploaded. +struct CtrlWindow { + size: usize, + left_boundary: usize, + slots: HashMap, +} + +impl CtrlWindow { + fn new(size: usize) -> Self { + CtrlWindow { + size, + left_boundary: 0, + slots: HashMap::default(), + } + } + + /// Check if the specified slot by `index` has been already uploaded. + /// Note, this function do not check about the right boundary. + fn check_duplicate(&self, index: usize) -> bool { + index < self.left_boundary || self.slots.contains_key(&index) + } + + /// Should call check_duplicate and handle the duplicated case before calling this function. + /// This function assumes that there are no duplicate slots in the window. + fn start_writing(&mut self, index: usize) -> Result<()> { + assert!(index >= self.left_boundary); + + if index >= self.left_boundary + self.size { + bail!( + "index exceeds window limit, index = {}, left_boundary = {}, window_size = {}", + index, + self.left_boundary, + self.size + ); + } + + assert!(!self.slots.contains_key(&index)); + self.slots.insert(index, SlotStatus::Writing); + + Ok(()) + } + + fn rollback_writing(&mut self, index: usize) { + let slot_status = self.slots.remove(&index); + assert_eq!(slot_status, Some(SlotStatus::Writing)); + } + + fn finish_writing(&mut self, index: usize) { + let old_status = self.slots.insert(index, SlotStatus::Finished); + assert_eq!(old_status, Some(SlotStatus::Writing)); + + // move forward if leftmost slot completed + let mut left_boundary = self.left_boundary; + while let Some(&SlotStatus::Finished) = self.slots.get(&left_boundary) { + self.slots.remove(&left_boundary); + left_boundary += 1; + } + + self.left_boundary = left_boundary; + } +} + +/// To track the file uploading progress. +pub struct FileWriteCtrl { + pub id: FileID, + total_segments: usize, + window: CtrlWindow, +} + +impl FileWriteCtrl { + fn new(id: FileID, total_segments: usize, window_size: usize) -> Self { + FileWriteCtrl { + id, + total_segments, + window: CtrlWindow::new(window_size), + } + } + + pub fn uploaded_seg_num(&self) -> usize { + self.window.left_boundary + } +} + +/// ChunkPoolWriteCtrl is used to track uploading progress for all files, +/// and limits the maximum number of threads to write segments into store. +pub struct ChunkPoolWriteCtrl { + config: Config, + /// Windows to control writing processes of files + files: HashMap, + /// Total number of threads that are writing chunks into store. + pub total_writings: usize, +} + +impl ChunkPoolWriteCtrl { + pub fn new(config: Config) -> Self { + ChunkPoolWriteCtrl { + files: HashMap::default(), + total_writings: 0, + config, + } + } + + pub fn get_file(&self, root: &DataRoot) -> Option<&FileWriteCtrl> { + self.files.get(root) + } + + pub fn remove_file(&mut self, root: &DataRoot) -> Option { + self.files.remove(root) + } + + pub fn write_segment( + &mut self, + id: FileID, + seg_index: usize, + total_segments: usize, + ) -> Result<()> { + let file_ctrl = self.files.entry(id.root).or_insert_with(|| { + FileWriteCtrl::new(id, total_segments, self.config.write_window_size) + }); + + // ensure the tx_id not changed during file uploading + if file_ctrl.id != id { + self.files.remove(&id.root); + bail!("Transaction reverted when uploading segments, please try again"); + } + + if file_ctrl.total_segments != total_segments { + bail!( + "file size in segment doesn't match with file size declared in previous segment. Previous total segments:{}, current total segments:{}s", + file_ctrl.total_segments, + total_segments + ); + } + + // Segment already uploaded. + if file_ctrl.window.check_duplicate(seg_index) { + bail!("segment has already been uploaded or is being uploaded"); + } + + // Limits the number of writing threads. + if self.total_writings >= self.config.max_writings { + bail!("too many data writing: {}", self.config.max_writings); + } + + file_ctrl.window.start_writing(seg_index)?; + + self.total_writings += 1; + + Ok(()) + } + + pub fn on_write_succeeded(&mut self, root: &DataRoot, seg_index: usize) -> bool { + let file_ctrl = match self.files.get_mut(root) { + Some(w) => w, + None => return false, + }; + + file_ctrl.window.finish_writing(seg_index); + + assert!(self.total_writings > 0); + self.total_writings -= 1; + + debug!( + "Succeeded to write segment, root={}, seg_index={}, total_writings={}", + root, seg_index, self.total_writings + ); + + // All chunks of file written into store. + file_ctrl.window.left_boundary >= file_ctrl.total_segments + } + + pub fn on_write_failed(&mut self, root: &DataRoot, seg_index: usize) { + let file_ctrl = match self.files.get_mut(root) { + Some(w) => w, + None => return, + }; + + //Rollback the segment status if failed + file_ctrl.window.rollback_writing(seg_index); + + assert!(self.total_writings > 0); + self.total_writings -= 1; + } +} diff --git a/node/chunk_pool/src/mem_pool/mod.rs b/node/chunk_pool/src/mem_pool/mod.rs new file mode 100644 index 0000000..0fc1299 --- /dev/null +++ b/node/chunk_pool/src/mem_pool/mod.rs @@ -0,0 +1,15 @@ +mod chunk_cache; +mod chunk_pool_inner; +mod chunk_write_control; + +pub use chunk_pool_inner::MemoryChunkPool; +pub use chunk_pool_inner::SegmentInfo; + +use shared_types::DataRoot; +use shared_types::TxID; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct FileID { + pub root: DataRoot, + pub tx_id: TxID, +} diff --git a/node/file_location_cache/Cargo.toml b/node/file_location_cache/Cargo.toml new file mode 100644 index 0000000..2e46d01 --- /dev/null +++ b/node/file_location_cache/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "file_location_cache" +version = "0.1.0" +edition = "2021" + +[dependencies] +hashlink = "0.8.0" +network = { path = "../network" } +parking_lot = "0.12.1" +rand = "0.8.5" +tracing = "0.1.35" +priority-queue = "1.2.3" +shared_types = { path = "../shared_types" } diff --git a/node/file_location_cache/src/file_location_cache.rs b/node/file_location_cache/src/file_location_cache.rs new file mode 100644 index 0000000..f12fe33 --- /dev/null +++ b/node/file_location_cache/src/file_location_cache.rs @@ -0,0 +1,532 @@ +use crate::Config; +use network::types::SignedAnnounceFile; +use network::PeerId; +use parking_lot::Mutex; +use priority_queue::PriorityQueue; +use rand::seq::IteratorRandom; +use shared_types::{timestamp_now, TxID}; +use std::cmp::Reverse; +use std::collections::HashMap; + +/// Caches limited announcements of specified file from different peers. +struct AnnouncementCache { + /// Maximum number of announcements in cache. + capacity: usize, + + /// Timeout in seconds to expire the cached announcement. + /// This is because file may be removed from the announced + /// storage node. + timeout_secs: u32, + + /// All cached announcements that mapped from peer id to announcement. + /// Note, only cache the latest announcement for each peer. + items: HashMap, + + /// All announcements are prioritized by timestamp. + /// The top element is the oldest announcement. + priorities: PriorityQueue>, +} + +impl AnnouncementCache { + fn new(capacity: usize, timeout_secs: u32) -> Self { + assert!(capacity > 0); + + AnnouncementCache { + capacity, + timeout_secs, + items: Default::default(), + priorities: Default::default(), + } + } + + /// Returns the priority of the oldest announcement if any. + fn peek_priority(&self) -> Option> { + let (_, &Reverse(ts)) = self.priorities.peek()?; + Some(Reverse(ts)) + } + + /// Removes the oldest announcement if any. + fn pop(&mut self) -> Option { + let (peer_id, _) = self.priorities.pop()?; + self.items.remove(&peer_id) + } + + fn len(&self) -> usize { + self.items.len() + } + + /// Garbage collects expired announcements. + fn garbage_collect(&mut self) -> usize { + let mut collected = 0; + let now = timestamp_now(); + + while let Some((_, &Reverse(ts))) = self.priorities.peek() { + if ts + self.timeout_secs > now { + break; + } + + self.pop(); + collected += 1; + } + + collected + } + + fn do_insert_or_update(&mut self, announcement: SignedAnnounceFile) { + let peer_id = announcement.peer_id.clone().into(); + self.priorities + .push(peer_id, Reverse(announcement.timestamp)); + self.items.insert(peer_id, announcement); + } + + /// Insert the specified `announcement` into cache. + fn insert(&mut self, announcement: SignedAnnounceFile) { + self.garbage_collect(); + + let peer_id = announcement.peer_id.clone().into(); + + if let Some(existing) = self.items.get(&peer_id) { + // ignore older announcement + if announcement.timestamp <= existing.timestamp { + return; + } + } + + // insert or update + self.do_insert_or_update(announcement); + + // remove oldest one if capacity exceeded + if self.items.len() > self.capacity { + self.pop(); + } + } + + /// Randomly pick an announcement if any. + fn random(&mut self) -> (Option, usize) { + let collected = self.garbage_collect(); + + // all announcements garbage collected + if self.items.is_empty() { + return (None, collected); + } + + let choosed = self + .items + .iter() + .choose(&mut rand::thread_rng()) + .map(|(_, item)| item.clone()); + + (choosed, collected) + } + + /// Returns all announcements. + fn all(&mut self) -> (Vec, usize) { + let collected = self.garbage_collect(); + let result = self.items.values().cloned().collect(); + (result, collected) + } +} + +/// Caches announcements for different files. +struct FileCache { + /// Cache configuration. + config: Config, + + /// Total number of announcements cached for all files. + total_announcements: usize, + + /// All cached files that mapped from `tx_id` to `AnnouncementCache`. + files: HashMap, + + /// All files are prioritized by timestamp. + /// The top element is the `AnnouncementCache` that has the oldest announcement. + priorities: PriorityQueue>, +} + +impl FileCache { + fn new(config: Config) -> Self { + FileCache { + config, + total_announcements: 0, + files: Default::default(), + priorities: Default::default(), + } + } + + /// Insert the specified `announcement` into cache. + fn insert(&mut self, announcement: SignedAnnounceFile) { + let tx_id = announcement.tx_id; + + let item = self.files.entry(tx_id).or_insert_with(|| { + AnnouncementCache::new( + self.config.max_entries_per_file, + self.config.entry_expiration_time_secs, + ) + }); + + assert!(self.total_announcements >= item.len()); + self.total_announcements -= item.len(); + + item.insert(announcement); + + if let Some(priority) = item.peek_priority() { + self.priorities.push(tx_id, priority); + } + + self.total_announcements += item.len(); + if self.total_announcements > self.config.max_entries_total { + self.pop(); + } + } + + /// Removes the oldest file announcement. + fn pop(&mut self) -> Option { + let (&tx_id, _) = self.priorities.peek()?; + let item = self.files.get_mut(&tx_id)?; + + let result = item.pop()?; + + self.update_on_announcement_cache_changed(&tx_id, 1); + + Some(result) + } + + /// Randomly pick a announcement of specified file by `tx_id`. + fn random(&mut self, tx_id: TxID) -> Option { + let item = self.files.get_mut(&tx_id)?; + let (result, collected) = item.random(); + self.update_on_announcement_cache_changed(&tx_id, collected); + result + } + + fn update_on_announcement_cache_changed(&mut self, tx_id: &TxID, removed: usize) { + if removed == 0 { + return; + } + + assert!(self.total_announcements >= removed); + self.total_announcements -= removed; + + let item = match self.files.get_mut(tx_id) { + Some(v) => v, + None => return, + }; + + if let Some(priority) = item.peek_priority() { + // update priority if changed + self.priorities.change_priority(tx_id, priority); + } else { + // remove entry if empty + self.files.remove(tx_id); + self.priorities.remove(tx_id); + } + } + + /// Returns all the announcements of specified file by `tx_id`. + fn all(&mut self, tx_id: TxID) -> Option> { + let item = self.files.get_mut(&tx_id)?; + let (result, collected) = item.all(); + self.update_on_announcement_cache_changed(&tx_id, collected); + Some(result) + } +} + +pub struct FileLocationCache { + cache: Mutex, +} + +impl Default for FileLocationCache { + fn default() -> Self { + FileLocationCache { + cache: Mutex::new(FileCache::new(Default::default())), + } + } +} + +impl FileLocationCache { + pub fn insert(&self, announcement: SignedAnnounceFile) { + self.cache.lock().insert(announcement); + } + + pub fn get_one(&self, tx_id: TxID) -> Option { + self.cache.lock().random(tx_id) + } + + pub fn get_all(&self, tx_id: TxID) -> Vec { + self.cache.lock().all(tx_id).unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use std::cmp::Reverse; + + use network::{types::SignedAnnounceFile, PeerId}; + use shared_types::{timestamp_now, TxID}; + + use crate::{test_util::AnnounceFileBuilder, Config}; + + use super::{AnnouncementCache, FileCache}; + + fn create_file(peer_id: Option, timestamp: u32) -> SignedAnnounceFile { + let builder = AnnounceFileBuilder::default().with_timestamp(timestamp); + if let Some(id) = peer_id { + builder.with_peer_id(id).build() + } else { + builder.build() + } + } + + #[test] + fn test_annoucement_cache_peek_priority() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + // empty by default + assert_eq!(cache.peek_priority(), None); + + // one file with timestamp `now - 5` + let t1 = now - 5; + cache.insert(create_file(None, t1)); + assert_eq!(cache.peek_priority(), Some(Reverse(t1))); + + // newly file with timestamp `now - 4` + let t2 = now - 4; + cache.insert(create_file(None, t2)); + assert_eq!(cache.peek_priority(), Some(Reverse(t1))); + + // old file with timestamp `now - 6` + let t3 = now - 6; + cache.insert(create_file(None, t3)); + assert_eq!(cache.peek_priority(), Some(Reverse(t3))); + } + + #[test] + fn test_annoucement_cache_pop_len() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + // empty by default + assert_eq!(cache.pop(), None); + assert_eq!(cache.len(), 0); + + cache.insert(create_file(None, now - 2)); + cache.insert(create_file(None, now - 3)); + cache.insert(create_file(None, now - 1)); + assert_eq!(cache.len(), 3); + + // pop from oldest to newest + assert_eq!(cache.pop().unwrap().timestamp, now - 3); + assert_eq!(cache.pop().unwrap().timestamp, now - 2); + assert_eq!(cache.pop().unwrap().timestamp, now - 1); + assert_eq!(cache.pop(), None); + assert_eq!(cache.len(), 0); + } + + #[test] + fn test_annoucement_cache_garbage_collect() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + assert_eq!(cache.garbage_collect(), 0); + + cache.do_insert_or_update(create_file(None, now - 5000)); + cache.do_insert_or_update(create_file(None, now - 5001)); + cache.do_insert_or_update(create_file(None, now - 2000)); + cache.do_insert_or_update(create_file(None, now + 10)); + + // gc for expired only + assert_eq!(cache.garbage_collect(), 2); + assert_eq!(cache.len(), 2); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 2000))); + } + + #[test] + fn test_annoucement_cache_insert_gc() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + // prepare expired items + cache.do_insert_or_update(create_file(None, now - 5000)); + cache.do_insert_or_update(create_file(None, now - 5001)); + + // insert with gc + cache.insert(create_file(None, now - 1)); + + assert_eq!(cache.len(), 1); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 1))); + } + + #[test] + fn test_annoucement_cache_insert_ignore_older() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + let peer_id = PeerId::random(); + + // insert `now - 2` + cache.insert(create_file(Some(peer_id), now - 2)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 2))); + assert_eq!(cache.len(), 1); + + // ignore for older announcement of the same peer + cache.insert(create_file(Some(peer_id), now - 3)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 2))); + assert_eq!(cache.len(), 1); + + // however, older announcement allowed from other peer + cache.insert(create_file(None, now - 3)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 3))); + assert_eq!(cache.len(), 2); + } + + #[test] + fn test_annoucement_cache_insert_overwrite() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + let peer_id = PeerId::random(); + + // insert `now - 2` + cache.insert(create_file(Some(peer_id), now - 2)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 2))); + assert_eq!(cache.len(), 1); + + // overwrite with newly item + cache.insert(create_file(Some(peer_id), now - 1)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 1))); + assert_eq!(cache.len(), 1); + } + + #[test] + fn test_annoucement_cache_insert_cap_exceeded() { + let mut cache = AnnouncementCache::new(3, 3600); + let now = timestamp_now(); + + cache.insert(create_file(None, now - 2)); + cache.insert(create_file(None, now - 3)); + cache.insert(create_file(None, now - 4)); + + // oldest `now - 5` will be removed + cache.insert(create_file(None, now - 5)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 4))); + assert_eq!(cache.len(), 3); + + // oldest `now - 4` will be removed + cache.insert(create_file(None, now - 1)); + assert_eq!(cache.peek_priority(), Some(Reverse(now - 3))); + assert_eq!(cache.len(), 3); + } + + #[test] + fn test_annoucement_cache_random() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + assert_eq!(cache.random().0, None); + + cache.insert(create_file(None, now - 1)); + assert_eq!(cache.random().0.unwrap().timestamp, now - 1); + + cache.insert(create_file(None, now - 2)); + cache.insert(create_file(None, now - 3)); + let picked = cache.random().0.unwrap().timestamp; + assert!(picked >= now - 3 && picked < now); + } + + #[test] + fn test_annoucement_cache_all() { + let mut cache = AnnouncementCache::new(100, 3600); + let now = timestamp_now(); + + assert_eq!(cache.all().0, vec![]); + + cache.insert(create_file(None, now - 1)); + cache.insert(create_file(None, now - 2)); + cache.insert(create_file(None, now - 3)); + + assert_all_files(cache.all().0, vec![now - 3, now - 2, now - 1]) + } + + fn assert_all_files(files: Vec, sorted_timestamps: Vec) { + let mut timestamps: Vec = files.iter().map(|f| f.timestamp).collect(); + timestamps.sort(); + assert_eq!(timestamps, sorted_timestamps); + } + + fn create_file_cache(total_entries: usize, file_entries: usize, timeout: u32) -> FileCache { + FileCache::new(Config { + max_entries_total: total_entries, + max_entries_per_file: file_entries, + entry_expiration_time_secs: timeout, + }) + } + + fn create_file_2(tx_id: TxID, peer_id: PeerId, timestamp: u32) -> SignedAnnounceFile { + AnnounceFileBuilder::default() + .with_tx_id(tx_id) + .with_peer_id(peer_id) + .with_timestamp(timestamp) + .build() + } + + fn assert_file(file: &SignedAnnounceFile, tx_id: TxID, peer_id: PeerId, timestamp: u32) { + assert_eq!(file.tx_id, tx_id); + assert_eq!(PeerId::from(file.peer_id.clone()), peer_id); + assert_eq!(file.timestamp, timestamp); + } + + #[test] + fn test_file_cache_insert_pop() { + let mut cache = create_file_cache(100, 3, 3600); + let now = timestamp_now(); + + assert_eq!(cache.total_announcements, 0); + + let peer1 = PeerId::random(); + let peer2 = PeerId::random(); + let tx1 = TxID::random_hash(1); + let tx2 = TxID::random_hash(2); + + cache.insert(create_file_2(tx1, peer1, now - 1)); + assert_eq!(cache.total_announcements, 1); + cache.insert(create_file_2(tx2, peer1, now - 2)); + assert_eq!(cache.total_announcements, 2); + cache.insert(create_file_2(tx1, peer2, now - 3)); + assert_eq!(cache.total_announcements, 3); + + assert_file(&cache.pop().unwrap(), tx1, peer2, now - 3); + assert_eq!(cache.total_announcements, 2); + assert_file(&cache.pop().unwrap(), tx2, peer1, now - 2); + assert_eq!(cache.total_announcements, 1); + assert_file(&cache.pop().unwrap(), tx1, peer1, now - 1); + assert_eq!(cache.total_announcements, 0); + assert_eq!(cache.pop(), None); + } + + #[test] + fn test_file_cache_insert_cap_exceeded() { + let mut cache = create_file_cache(3, 3, 3600); + let now = timestamp_now(); + + let tx1 = TxID::random_hash(1); + cache.insert(create_file_2(tx1, PeerId::random(), now - 7)); + cache.insert(create_file_2(tx1, PeerId::random(), now - 8)); + cache.insert(create_file_2(tx1, PeerId::random(), now - 9)); + assert_eq!(cache.total_announcements, 3); + + // insert more files and cause to max entries limited + let tx2 = TxID::random_hash(2); + cache.insert(create_file_2(tx2, PeerId::random(), now - 1)); + assert_all_files(cache.all(tx1).unwrap_or_default(), vec![now - 8, now - 7]); + cache.insert(create_file_2(tx2, PeerId::random(), now - 2)); + assert_all_files(cache.all(tx1).unwrap_or_default(), vec![now - 7]); + cache.insert(create_file_2(tx2, PeerId::random(), now - 3)); + assert_all_files(cache.all(tx1).unwrap_or_default(), vec![]); + + assert_all_files( + cache.all(tx2).unwrap_or_default(), + vec![now - 3, now - 2, now - 1], + ); + } +} diff --git a/node/file_location_cache/src/lib.rs b/node/file_location_cache/src/lib.rs new file mode 100644 index 0000000..f36b9d0 --- /dev/null +++ b/node/file_location_cache/src/lib.rs @@ -0,0 +1,20 @@ +mod file_location_cache; +pub mod test_util; + +pub use crate::file_location_cache::FileLocationCache; + +pub struct Config { + pub max_entries_total: usize, + pub max_entries_per_file: usize, + pub entry_expiration_time_secs: u32, +} + +impl Default for Config { + fn default() -> Self { + Config { + max_entries_total: 4096, + max_entries_per_file: 4, + entry_expiration_time_secs: 3600, + } + } +} diff --git a/node/file_location_cache/src/test_util.rs b/node/file_location_cache/src/test_util.rs new file mode 100644 index 0000000..adf20ed --- /dev/null +++ b/node/file_location_cache/src/test_util.rs @@ -0,0 +1,47 @@ +use network::{ + libp2p::identity, + types::{AnnounceFile, SignedAnnounceFile}, + Multiaddr, PeerId, +}; +use shared_types::{timestamp_now, TxID}; + +#[derive(Default)] +pub struct AnnounceFileBuilder { + tx_id: Option, + peer_id: Option, + timestamp: Option, +} + +impl AnnounceFileBuilder { + pub fn with_tx_id(mut self, tx_id: TxID) -> Self { + self.tx_id = Some(tx_id); + self + } + + pub fn with_peer_id(mut self, peer_id: PeerId) -> Self { + self.peer_id = Some(peer_id); + self + } + + pub fn with_timestamp(mut self, timestamp: u32) -> Self { + self.timestamp = Some(timestamp); + self + } + + pub fn build(self) -> SignedAnnounceFile { + let tx_id = self.tx_id.unwrap_or_else(|| TxID::random_hash(0)); + let peer_id = self.peer_id.unwrap_or_else(PeerId::random); + let at: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + let timestamp = self.timestamp.unwrap_or_else(timestamp_now); + + let msg = AnnounceFile { + tx_id, + peer_id: peer_id.into(), + at: at.into(), + timestamp, + }; + + let keypair = identity::Keypair::generate_secp256k1(); + msg.into_signed(&keypair).unwrap() + } +} diff --git a/node/log_entry_sync/Cargo.toml b/node/log_entry_sync/Cargo.toml new file mode 100644 index 0000000..dce7bff --- /dev/null +++ b/node/log_entry_sync/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "log_entry_sync" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +append_merkle = { path = "../../common/append_merkle" } +async-trait = "0.1.56" +ethereum-types = "0.14" +futures = "0.3.21" +jsonrpsee = { version = "0.14.0", features = ["full"] } +shared_types = { path = "../shared_types" } +task_executor = { path = "../../common/task_executor" } +tokio = "1.19.2" +ethers = { version = "^2", features = ["ws"] } +serde_json = "1.0.82" +storage = { path = "../storage" } +contract-interface = { path = "../../common/contract-interface" } +futures-core = "0.3.28" +futures-util = "0.3.28" +thiserror = "1.0.44" \ No newline at end of file diff --git a/node/log_entry_sync/src/lib.rs b/node/log_entry_sync/src/lib.rs new file mode 100644 index 0000000..9f27bd2 --- /dev/null +++ b/node/log_entry_sync/src/lib.rs @@ -0,0 +1,10 @@ +extern crate core; + +pub(crate) mod rpc_proxy; +mod sync_manager; + +pub use rpc_proxy::ContractAddress; +pub use sync_manager::{ + config::{CacheConfig, LogSyncConfig}, + LogSyncEvent, LogSyncManager, +}; diff --git a/node/log_entry_sync/src/rpc_proxy/eth.rs b/node/log_entry_sync/src/rpc_proxy/eth.rs new file mode 100644 index 0000000..25566fb --- /dev/null +++ b/node/log_entry_sync/src/rpc_proxy/eth.rs @@ -0,0 +1,30 @@ +use crate::rpc_proxy::{ContractAddress, EvmRpcProxy, SubEvent, SubFilter}; +use async_trait::async_trait; +use ethers::prelude::{Bytes, Middleware, Provider}; +use ethers::providers::Ws; +use ethers::types::TransactionRequest; +use jsonrpsee::core::client::Subscription; + +pub struct EthClient { + client: Provider, +} + +impl EthClient { + #[allow(unused)] + pub async fn new(url: &str) -> anyhow::Result { + let client = Provider::new(Ws::connect(url).await?); + Ok(Self { client }) + } +} + +#[async_trait] +impl EvmRpcProxy for EthClient { + async fn call(&self, to: ContractAddress, data: Bytes) -> anyhow::Result { + let request = TransactionRequest::new().to(to).data(data); + Ok(self.client.call(&request.into(), None).await?) + } + + async fn sub_events(&self, _filter: SubFilter) -> Subscription { + todo!() + } +} diff --git a/node/log_entry_sync/src/rpc_proxy/mod.rs b/node/log_entry_sync/src/rpc_proxy/mod.rs new file mode 100644 index 0000000..a1efe63 --- /dev/null +++ b/node/log_entry_sync/src/rpc_proxy/mod.rs @@ -0,0 +1,38 @@ +use anyhow::Result; +use async_trait::async_trait; +use ethereum_types::{H160, H256}; +use ethers::prelude::Bytes; +use jsonrpsee::core::client::Subscription; + +// TODO: Define accounts/filter/events as associated types? +// TODO: Define an abstraction suitable for other chains. +#[async_trait] +pub trait EvmRpcProxy { + async fn call(&self, to: ContractAddress, data: Bytes) -> Result; + + async fn sub_events(&self, filter: SubFilter) -> Subscription; +} + +pub type ContractAddress = H160; + +pub type Topic = H256; + +#[allow(unused)] +pub struct SubFilter { + to: Option, + topics: Vec, +} + +#[allow(unused)] +pub struct SubEvent { + /// Address + pub address: ContractAddress, + + /// Topics + pub topics: Vec, + + /// Data + pub data: Bytes, +} + +pub(crate) mod eth; diff --git a/node/log_entry_sync/src/sync_manager/config.rs b/node/log_entry_sync/src/sync_manager/config.rs new file mode 100644 index 0000000..fae6e7b --- /dev/null +++ b/node/log_entry_sync/src/sync_manager/config.rs @@ -0,0 +1,65 @@ +use crate::rpc_proxy::ContractAddress; + +pub struct LogSyncConfig { + pub rpc_endpoint_url: String, + pub contract_address: ContractAddress, + pub cache_config: CacheConfig, + + /// The block number where we start to sync data. + /// This is usually the block number when Zgs contract is deployed. + pub start_block_number: u64, + /// The number of blocks needed for confirmation on the blockchain. + /// This is used to rollback to a stable height if reorg happens during node restart. + /// TODO(zz): Some blockchains have better confirmation/finalization mechanisms. + pub confirmation_block_count: u64, + /// Maximum number of event logs to poll at a time. + pub log_page_size: u64, + + // blockchain provider retry params + // the number of retries after a connection times out + pub rate_limit_retries: u32, + // the nubmer of retries for rate limited responses + pub timeout_retries: u32, + // the duration to wait before retry, in ms + pub initial_backoff: u64, + // the duration between each paginated getLogs RPC call, in ms. + // This is set to avoid triggering the throttling mechanism in the RPC server. + pub recover_query_delay: u64, +} + +#[derive(Clone)] +pub struct CacheConfig { + /// The data with a size larger than this will not be cached. + /// This is reasonable because uploading + pub max_data_size: usize, + pub tx_seq_ttl: usize, +} + +impl LogSyncConfig { + #[allow(clippy::too_many_arguments)] + pub fn new( + rpc_endpoint_url: String, + contract_address: ContractAddress, + start_block_number: u64, + confirmation_block_count: u64, + cache_config: CacheConfig, + log_page_size: u64, + rate_limit_retries: u32, + timeout_retries: u32, + initial_backoff: u64, + recover_query_delay: u64, + ) -> Self { + Self { + rpc_endpoint_url, + contract_address, + cache_config, + start_block_number, + confirmation_block_count, + log_page_size, + rate_limit_retries, + timeout_retries, + initial_backoff, + recover_query_delay, + } + } +} diff --git a/node/log_entry_sync/src/sync_manager/data_cache.rs b/node/log_entry_sync/src/sync_manager/data_cache.rs new file mode 100644 index 0000000..00397d6 --- /dev/null +++ b/node/log_entry_sync/src/sync_manager/data_cache.rs @@ -0,0 +1,58 @@ +use crate::sync_manager::config::CacheConfig; +use shared_types::DataRoot; +use std::cmp; +use std::collections::HashMap; + +struct CachedData { + /// Used for garbage collection. + last_seen_tx_seq: u64, + /// Complete data for a given DataRoot. + data: Vec, +} + +pub struct DataCache { + root_to_data: HashMap, + config: CacheConfig, +} + +impl DataCache { + pub fn new(config: CacheConfig) -> Self { + Self { + root_to_data: HashMap::new(), + config, + } + } + + pub fn add_data(&mut self, root: DataRoot, tx_seq: u64, data: Vec) -> bool { + if data.len() > self.config.max_data_size { + // large data are not cached. + return false; + } + // TODO: insert partial data and merge here. + self.root_to_data + .entry(root) + .and_modify(|cached| { + cached.last_seen_tx_seq = cmp::max(tx_seq, cached.last_seen_tx_seq) + }) + .or_insert(CachedData { + last_seen_tx_seq: tx_seq, + data, + }); + true + } + + /// Remove and return the data of a given `DataRoot`. + /// If two completed reverted transactions have the same root and both appear later, + /// the second one will have its data copied in `put_tx`. + pub fn pop_data(&mut self, root: &DataRoot) -> Option> { + self.root_to_data.remove(root).map(|e| e.data) + } + + /// Remove timeout data entries according to TTL. + pub fn garbage_collect(&mut self, latest_tx_seq: u64) { + // We won't keep too many data, so it's okay to just iterate here. + self.root_to_data.retain(|_, cached| { + cached.last_seen_tx_seq + self.config.tx_seq_ttl as u64 >= latest_tx_seq + }) + } +} diff --git a/node/log_entry_sync/src/sync_manager/log_entry_fetcher.rs b/node/log_entry_sync/src/sync_manager/log_entry_fetcher.rs new file mode 100644 index 0000000..eebf938 --- /dev/null +++ b/node/log_entry_sync/src/sync_manager/log_entry_fetcher.rs @@ -0,0 +1,356 @@ +use crate::rpc_proxy::ContractAddress; +use crate::sync_manager::log_query::LogQuery; +use crate::sync_manager::{repeat_run_and_log, RETRY_WAIT_MS}; +use anyhow::{anyhow, bail, Result}; +use append_merkle::{Algorithm, Sha3Algorithm}; +use contract_interface::{ZgsFlow, SubmissionNode, SubmitFilter}; +use ethers::abi::RawLog; +use ethers::prelude::{BlockNumber, EthLogDecode, Http, Log, Middleware, Provider, U256}; +use ethers::providers::{FilterKind, HttpRateLimitRetryPolicy, RetryClient, RetryClientBuilder}; +use ethers::types::H256; +use futures::StreamExt; +use jsonrpsee::tracing::{debug, error, info}; +use shared_types::{DataRoot, Transaction}; +use std::collections::{BTreeMap, VecDeque}; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use task_executor::TaskExecutor; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +pub struct LogEntryFetcher { + contract_address: ContractAddress, + log_page_size: u64, + provider: Arc>>, + + confirmation_delay: u64, +} + +impl LogEntryFetcher { + pub async fn new( + url: &str, + contract_address: ContractAddress, + log_page_size: u64, + confirmation_delay: u64, + rate_limit_retries: u32, + timeout_retries: u32, + initial_backoff: u64, + ) -> Result { + let provider = Arc::new(Provider::new( + RetryClientBuilder::default() + .rate_limit_retries(rate_limit_retries) + .timeout_retries(timeout_retries) + .initial_backoff(Duration::from_millis(initial_backoff)) + .build(Http::from_str(url)?, Box::new(HttpRateLimitRetryPolicy)), + )); + // TODO: `error` types are removed from the ABI json file. + Ok(Self { + contract_address, + provider, + log_page_size, + confirmation_delay, + }) + } + + pub fn start_recover( + &self, + start_block_number: u64, + end_block_number: u64, + executor: &TaskExecutor, + log_query_delay: Duration, + ) -> UnboundedReceiver { + let provider = self.provider.clone(); + let (recover_tx, recover_rx) = tokio::sync::mpsc::unbounded_channel(); + let contract = ZgsFlow::new(self.contract_address, provider.clone()); + let log_page_size = self.log_page_size; + + executor.spawn( + async move { + let mut progress = start_block_number; + let mut filter = contract + .submit_filter() + .from_block(progress) + .to_block(end_block_number) + .filter; + let mut stream = LogQuery::new(&provider, &filter, log_query_delay) + .with_page_size(log_page_size); + debug!( + "start_recover starts, start={} end={}", + start_block_number, end_block_number + ); + while let Some(maybe_log) = stream.next().await { + match maybe_log { + Ok(log) => { + let sync_progress = + if log.block_hash.is_some() && log.block_number.is_some() { + let synced_block = LogFetchProgress::SyncedBlock(( + log.block_number.unwrap().as_u64(), + log.block_hash.unwrap(), + )); + progress = log.block_number.unwrap().as_u64(); + Some(synced_block) + } else { + None + }; + debug!("recover: progress={:?}", sync_progress); + + match SubmitFilter::decode_log(&RawLog { + topics: log.topics, + data: log.data.to_vec(), + }) { + Ok(event) => { + if let Err(e) = recover_tx + .send(submission_event_to_transaction(event)) + .and_then(|_| match sync_progress { + Some(b) => recover_tx.send(b), + None => Ok(()), + }) + { + error!("send error: e={:?}", e); + } + } + Err(e) => { + error!("log decode error: e={:?}", e); + } + } + } + Err(e) => { + error!("log query error: e={:?}", e); + filter = filter.from_block(progress); + stream = LogQuery::new(&provider, &filter, log_query_delay) + .with_page_size(log_page_size); + tokio::time::sleep(Duration::from_millis(RETRY_WAIT_MS)).await; + } + } + } + }, + "log recover", + ); + recover_rx + } + + pub fn start_watch( + &self, + start_block_number: u64, + executor: &TaskExecutor, + ) -> UnboundedReceiver { + let (watch_tx, watch_rx) = tokio::sync::mpsc::unbounded_channel(); + let contract = ZgsFlow::new(self.contract_address, self.provider.clone()); + let provider = self.provider.clone(); + let mut log_confirmation_queue = LogConfirmationQueue::new(self.confirmation_delay); + executor.spawn( + async move { + let mut filter = contract + .submit_filter() + .from_block(start_block_number) + .filter; + debug!("start_watch starts, start={}", start_block_number); + let mut filter_id = + repeat_run_and_log(|| provider.new_filter(FilterKind::Logs(&filter))).await; + let mut progress = start_block_number; + + loop { + match Self::watch_loop( + provider.as_ref(), + filter_id, + &watch_tx, + &mut log_confirmation_queue, + ) + .await + { + Err(e) => { + error!("log sync watch error: e={:?}", e); + filter = filter.from_block(progress); + filter_id = repeat_run_and_log(|| { + provider.new_filter(FilterKind::Logs(&filter)) + }) + .await; + } + Ok(Some(p)) => { + progress = p; + info!("log sync to block number {:?}", progress); + } + Ok(None) => { + error!( + "log sync gets entries without progress? old_progress={}", + progress + ) + } + } + tokio::time::sleep(Duration::from_millis(RETRY_WAIT_MS)).await; + } + }, + "log watch", + ); + watch_rx + } + + async fn watch_loop( + provider: &Provider>, + filter_id: U256, + watch_tx: &UnboundedSender, + log_confirmation_queue: &mut LogConfirmationQueue, + ) -> Result> { + debug!("get block"); + let latest_block = provider + .get_block(BlockNumber::Latest) + .await? + .ok_or_else(|| anyhow!("None for latest block"))?; + debug!("get filter changes"); + let logs: Vec = provider.get_filter_changes(filter_id).await?; + if let Some(reverted) = log_confirmation_queue.push(logs)? { + watch_tx.send(LogFetchProgress::Reverted(reverted))?; + } + debug!("get filter end"); + for log in log_confirmation_queue.confirm_logs(latest_block.number.unwrap().as_u64()) { + assert!(!log.removed.unwrap_or(false)); + // TODO(zz): Log parse error means logs might be lost here. + let tx = SubmitFilter::decode_log(&RawLog { + topics: log.topics, + data: log.data.to_vec(), + })?; + watch_tx.send(submission_event_to_transaction(tx))?; + } + let progress = if latest_block.hash.is_some() && latest_block.number.is_some() { + Some(( + latest_block.number.unwrap().as_u64(), + latest_block.hash.unwrap(), + )) + } else { + None + }; + if let Some(p) = &progress { + watch_tx.send(LogFetchProgress::SyncedBlock(*p))?; + } + Ok(progress.map(|p| p.0)) + } + + pub fn provider(&self) -> &Provider> { + self.provider.as_ref() + } +} + +struct LogConfirmationQueue { + /// Keep the unconfirmed new logs. + /// The key is the block number and the value is the set of needed logs in that block. + queue: VecDeque<(u64, Vec)>, + + latest_block_number: u64, + confirmation_delay: u64, +} + +impl LogConfirmationQueue { + fn new(confirmation_delay: u64) -> Self { + Self { + queue: VecDeque::new(), + latest_block_number: 0, + confirmation_delay, + } + } + /// Push a set of new logs. + /// We assumes that these logs are in order, and removed logs are returned first. + /// + /// Return `Ok(Some(tx_seq))` of the first reverted tx_seq if chain reorg happens. + /// `Err` is returned if assumptions are violated (like the log have missing fields). + fn push(&mut self, logs: Vec) -> Result> { + let mut revert_to = None; + // First merge logs according to the block number. + let mut block_logs: BTreeMap> = BTreeMap::new(); + let mut removed_block_logs = BTreeMap::new(); + for log in logs { + let set = if log.removed.unwrap_or(false) { + &mut removed_block_logs + } else { + &mut block_logs + }; + let block_number = log + .block_number + .ok_or_else(|| anyhow!("block number missing"))? + .as_u64(); + set.entry(block_number).or_default().push(log); + } + + // Handle revert if it happens. + for (block_number, removed_logs) in &removed_block_logs { + if revert_to.is_none() { + let reverted_index = match self.queue.binary_search_by_key(block_number, |e| e.0) { + Ok(x) => x, + Err(x) => x, + }; + self.queue.truncate(reverted_index); + let first = removed_logs.first().expect("not empty"); + let first_reverted_tx_seq = SubmitFilter::decode_log(&RawLog { + topics: first.topics.clone(), + data: first.data.to_vec(), + })? + .submission_index + .as_u64(); + revert_to = Some(first_reverted_tx_seq); + } else { + // Other removed logs should have larger tx seq, so no need to process them. + break; + } + } + + // Add new logs to the queue. + for (block_number, new_logs) in block_logs { + if block_number <= self.queue.back().map(|e| e.0).unwrap_or(0) { + bail!("reverted without being notified"); + } + self.queue.push_back((block_number, new_logs)); + } + + Ok(revert_to) + } + + /// Pass in the latest block number and return the confirmed logs. + fn confirm_logs(&mut self, latest_block_number: u64) -> Vec { + self.latest_block_number = latest_block_number; + let mut confirmed_logs = Vec::new(); + while let Some((block_number, _)) = self.queue.front() { + if *block_number + > self + .latest_block_number + .wrapping_sub(self.confirmation_delay) + { + break; + } + let (_, mut logs) = self.queue.pop_front().unwrap(); + confirmed_logs.append(&mut logs); + } + confirmed_logs + } +} + +#[derive(Debug)] +pub enum LogFetchProgress { + SyncedBlock((u64, H256)), + Transaction(Transaction), + Reverted(u64), +} + +fn submission_event_to_transaction(e: SubmitFilter) -> LogFetchProgress { + LogFetchProgress::Transaction(Transaction { + stream_ids: vec![], + data: vec![], + data_merkle_root: nodes_to_root(&e.submission.nodes), + merkle_nodes: e + .submission + .nodes + .iter() + // the submission height is the height of the root node starting from height 0. + .map(|SubmissionNode { root, height }| (height.as_usize() + 1, root.into())) + .collect(), + start_entry_index: e.start_pos.as_u64(), + size: e.submission.length.as_u64(), + seq: e.submission_index.as_u64(), + }) +} + +fn nodes_to_root(node_list: &Vec) -> DataRoot { + let mut root: DataRoot = node_list.last().expect("not empty").root.into(); + for next_node in node_list[..node_list.len() - 1].iter().rev() { + root = Sha3Algorithm::parent(&next_node.root.into(), &root); + } + root +} diff --git a/node/log_entry_sync/src/sync_manager/log_query.rs b/node/log_entry_sync/src/sync_manager/log_query.rs new file mode 100644 index 0000000..ee7f412 --- /dev/null +++ b/node/log_entry_sync/src/sync_manager/log_query.rs @@ -0,0 +1,183 @@ +use ethers::prelude::{Filter, JsonRpcClient, Log, Middleware, Provider, ProviderError, U64}; +use futures_core::stream::Stream; +use jsonrpsee::tracing::trace; +use std::future::Future; +use std::time::Duration; +use std::{ + collections::VecDeque, + pin::Pin, + task::{Context, Poll}, +}; +use thiserror::Error; + +pub(crate) type PinBoxFut<'a, T> = + Pin> + Send + 'a>>; + +/// A log query provides streaming access to historical logs via a paginated +/// request. For streaming access to future logs, use [`Middleware::watch`] or +/// [`Middleware::subscribe_logs`] +pub struct LogQuery<'a, P> { + provider: &'a Provider

, + filter: Filter, + from_block: Option, + page_size: u64, + current_logs: VecDeque, + last_block: Option, + state: LogQueryState<'a>, + delay: Duration, +} + +enum LogQueryState<'a> { + Initial, + LoadLastBlock(PinBoxFut<'a, U64>), + LoadLogs(PinBoxFut<'a, Vec>), + Consume, +} + +impl<'a, P> LogQuery<'a, P> +where + P: JsonRpcClient, +{ + /// Instantiate a new `LogQuery` + pub fn new(provider: &'a Provider

, filter: &Filter, delay: Duration) -> Self { + Self { + provider, + filter: filter.clone(), + from_block: filter.get_from_block(), + page_size: 10000, + current_logs: VecDeque::new(), + last_block: None, + state: LogQueryState::Initial, + delay, + } + } + + /// set page size for pagination + pub fn with_page_size(mut self, page_size: u64) -> Self { + self.page_size = page_size; + self + } +} + +macro_rules! rewake_with_new_state { + ($ctx:ident, $this:ident, $new_state:expr) => { + $this.state = $new_state; + $ctx.waker().wake_by_ref(); + return Poll::Pending + }; +} + +/// Errors while querying for logs +#[derive(Error, Debug)] +pub enum LogQueryError { + /// Error loading latest block + #[error(transparent)] + LoadLastBlockError(E), + /// Error loading logs from block range + #[error(transparent)] + LoadLogsError(E), +} + +impl<'a, P> Stream for LogQuery<'a, P> +where + P: JsonRpcClient, +{ + type Item = Result>; + + fn poll_next(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + let delay = self.delay; + match &mut self.state { + LogQueryState::Initial => { + if !self.filter.is_paginatable() { + // if not paginatable, load logs and consume + let filter = self.filter.clone(); + let provider = self.provider; + #[allow(clippy::redundant_async_block)] + let fut = Box::pin(async move { + tokio::time::sleep(delay).await; + provider.get_logs(&filter).await + }); + rewake_with_new_state!(ctx, self, LogQueryState::LoadLogs(fut)); + } else { + // if paginatable, load last block + let fut = self.provider.get_block_number(); + rewake_with_new_state!(ctx, self, LogQueryState::LoadLastBlock(fut)); + } + } + LogQueryState::LoadLastBlock(fut) => { + match futures_util::ready!(fut.as_mut().poll(ctx)) { + Ok(last_block) => { + trace!("log_query: last_block={}", last_block); + self.last_block = Some(last_block); + + // this is okay because we will only enter this state when the filter is + // paginatable i.e. from block is set + let from_block = self.filter.get_from_block().unwrap(); + let to_block = from_block + self.page_size; + self.from_block = Some(to_block + 1); + + let filter = self + .filter + .clone() + .from_block(from_block) + .to_block(to_block); + let provider = self.provider; + // load first page of logs + #[allow(clippy::redundant_async_block)] + let fut = Box::pin(async move { + tokio::time::sleep(delay).await; + provider.get_logs(&filter).await + }); + rewake_with_new_state!(ctx, self, LogQueryState::LoadLogs(fut)); + } + Err(err) => Poll::Ready(Some(Err(LogQueryError::LoadLastBlockError(err)))), + } + } + LogQueryState::LoadLogs(fut) => match futures_util::ready!(fut.as_mut().poll(ctx)) { + Ok(logs) => { + self.current_logs = VecDeque::from(logs); + rewake_with_new_state!(ctx, self, LogQueryState::Consume); + } + Err(err) => Poll::Ready(Some(Err(LogQueryError::LoadLogsError(err)))), + }, + LogQueryState::Consume => { + let log = self.current_logs.pop_front(); + if log.is_none() { + // consumed all the logs + if !self.filter.is_paginatable() { + Poll::Ready(None) + } else { + // load new logs if there are still more pages to go through + // can safely assume this will always be set in this state + let from_block = self.from_block.unwrap(); + let to_block = from_block + self.page_size; + + // no more pages to load, and everything is consumed + // can safely assume this will always be set in this state + if from_block > self.last_block.unwrap() { + return Poll::Ready(None); + } + // load next page + self.from_block = Some(to_block + 1); + + let filter = self + .filter + .clone() + .from_block(from_block) + .to_block(to_block); + let provider = self.provider; + #[allow(clippy::redundant_async_block)] + let fut = Box::pin(async move { + tokio::time::sleep(delay).await; + provider.get_logs(&filter).await + }); + + rewake_with_new_state!(ctx, self, LogQueryState::LoadLogs(fut)); + } + } else { + Poll::Ready(log.map(Ok)) + } + } + } + } +} diff --git a/node/log_entry_sync/src/sync_manager/mod.rs b/node/log_entry_sync/src/sync_manager/mod.rs new file mode 100644 index 0000000..1d68328 --- /dev/null +++ b/node/log_entry_sync/src/sync_manager/mod.rs @@ -0,0 +1,319 @@ +use crate::sync_manager::config::LogSyncConfig; +use crate::sync_manager::data_cache::DataCache; +use crate::sync_manager::log_entry_fetcher::{LogEntryFetcher, LogFetchProgress}; +use anyhow::{bail, Result}; +use ethers::prelude::Middleware; +use futures::FutureExt; +use jsonrpsee::tracing::{debug, error, trace, warn}; +use shared_types::{ChunkArray, Transaction}; +use std::fmt::Debug; +use std::future::Future; +use std::sync::Arc; +use std::time::Duration; +use storage::log_store::Store; +use task_executor::{ShutdownReason, TaskExecutor}; +use tokio::sync::broadcast; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::RwLock; + +const RETRY_WAIT_MS: u64 = 500; +const BROADCAST_CHANNEL_CAPACITY: usize = 16; + +#[derive(Clone, Debug)] +pub enum LogSyncEvent { + /// Chain reorg detected without any operation yet. + ReorgDetected { tx_seq: u64 }, + /// Transaction reverted in storage. + Reverted { tx_seq: u64 }, + /// Synced a transaction from blockchain + TxSynced { tx: Transaction }, +} + +pub struct LogSyncManager { + config: LogSyncConfig, + log_fetcher: LogEntryFetcher, + store: Arc>, + data_cache: DataCache, + + next_tx_seq: u64, + + /// To broadcast events to handle in advance. + event_send: broadcast::Sender, +} + +impl LogSyncManager { + pub async fn spawn( + config: LogSyncConfig, + executor: TaskExecutor, + store: Arc>, + ) -> Result> { + let next_tx_seq = store.read().await.next_tx_seq(); + + let executor_clone = executor.clone(); + let mut shutdown_sender = executor.shutdown_sender(); + + let (event_send, _) = broadcast::channel(BROADCAST_CHANNEL_CAPACITY); + let event_send_cloned = event_send.clone(); + + // Spawn the task to sync log entries from the blockchain. + executor.spawn( + run_and_log( + move || { + shutdown_sender + .try_send(ShutdownReason::Failure("log sync failure")) + .expect("shutdown send error") + }, + async move { + let log_fetcher = LogEntryFetcher::new( + &config.rpc_endpoint_url, + config.contract_address, + config.log_page_size, + config.confirmation_block_count, + config.rate_limit_retries, + config.timeout_retries, + config.initial_backoff, + ) + .await?; + let data_cache = DataCache::new(config.cache_config.clone()); + let mut log_sync_manager = Self { + config, + log_fetcher, + next_tx_seq, + store, + data_cache, + event_send, + }; + + // Load previous progress from db and check if chain reorg happens after restart. + // TODO(zz): Handle reorg instead of return. + let start_block_number = + match log_sync_manager.store.read().await.get_sync_progress()? { + // No previous progress, so just use config. + None => log_sync_manager.config.start_block_number, + Some((block_number, block_hash)) => { + match log_sync_manager + .log_fetcher + .provider() + .get_block(block_number) + .await + { + Ok(Some(b)) => { + if b.hash == Some(block_hash) { + block_number + } else { + warn!( + "log sync progress check hash fails, \ + block_number={:?} expect={:?} get={:?}", + block_number, block_hash, b.hash + ); + // Assume the blocks before this are not reverted. + block_number.saturating_sub( + log_sync_manager.config.confirmation_block_count, + ) + } + } + e => { + error!("log sync progress check rpc fails, e={:?}", e); + bail!("log sync start error"); + } + } + } + }; + let latest_block_number = log_sync_manager + .log_fetcher + .provider() + .get_block_number() + .await? + .as_u64(); + + // Start watching before recovery to ensure that no log is skipped. + // TODO(zz): Rate limit to avoid OOM during recovery. + let watch_rx = log_sync_manager + .log_fetcher + .start_watch(latest_block_number, &executor_clone); + let recover_rx = log_sync_manager.log_fetcher.start_recover( + start_block_number, + // -1 so the recover and watch ranges do not overlap. + latest_block_number.wrapping_sub(1), + &executor_clone, + Duration::from_millis(log_sync_manager.config.recover_query_delay), + ); + log_sync_manager.handle_data(recover_rx).await?; + // Syncing `watch_rx` is supposed to block forever. + log_sync_manager.handle_data(watch_rx).await?; + Ok(()) + }, + ) + .map(|_| ()), + "log_sync", + ); + Ok(event_send_cloned) + } + + async fn put_tx(&mut self, tx: Transaction) -> bool { + // We call this after process chain reorg, so the sequence number should match. + match tx.seq.cmp(&self.next_tx_seq) { + std::cmp::Ordering::Less => true, + std::cmp::Ordering::Equal => { + debug!("log entry sync get entry: {:?}", tx); + self.put_tx_inner(tx).await + } + std::cmp::Ordering::Greater => { + error!( + "Unexpected transaction seq: next={} get={}", + self.next_tx_seq, tx.seq + ); + false + } + } + } + + /// `tx_seq` is the first reverted tx seq. + async fn process_reverted(&mut self, tx_seq: u64) { + warn!("revert for chain reorg: seq={}", tx_seq); + { + let store = self.store.read().await; + for seq in tx_seq..self.next_tx_seq { + if matches!(store.check_tx_completed(seq), Ok(true)) { + if let Ok(Some(tx)) = store.get_tx_by_seq_number(seq) { + // TODO(zz): Skip reading the rear padding data? + if let Ok(Some(data)) = + store.get_chunks_by_tx_and_index_range(seq, 0, tx.num_entries()) + { + if !self + .data_cache + .add_data(tx.data_merkle_root, seq, data.data) + { + // TODO(zz): Data too large. Save to disk? + warn!("large reverted data dropped for tx={:?}", tx); + } + } + } + } + } + } + + let _ = self.event_send.send(LogSyncEvent::ReorgDetected { tx_seq }); + + // TODO(zz): `wrapping_sub` here is a hack to handle the case of tx_seq=0. + if let Err(e) = self.store.write().await.revert_to(tx_seq.wrapping_sub(1)) { + error!("revert_to fails: e={:?}", e); + return; + } + self.next_tx_seq = tx_seq; + + let _ = self.event_send.send(LogSyncEvent::Reverted { tx_seq }); + } + + async fn handle_data(&mut self, mut rx: UnboundedReceiver) -> Result<()> { + while let Some(data) = rx.recv().await { + trace!("handle_data: data={:?}", data); + match data { + LogFetchProgress::SyncedBlock(progress) => { + match self + .log_fetcher + .provider() + .get_block( + progress + .0 + .saturating_sub(self.config.confirmation_block_count), + ) + .await + { + Ok(Some(b)) => { + if let (Some(block_number), Some(block_hash)) = (b.number, b.hash) { + self.store + .write() + .await + .put_sync_progress((block_number.as_u64(), block_hash))?; + } + } + e => { + error!("log put progress check rpc fails, e={:?}", e); + } + } + } + LogFetchProgress::Transaction(tx) => { + if !self.put_tx(tx.clone()).await { + // Unexpected error. + error!("log sync write error"); + break; + } + if let Err(e) = self.event_send.send(LogSyncEvent::TxSynced { tx }) { + error!("log sync broadcast error, error={:?}", e); + break; + } + } + LogFetchProgress::Reverted(reverted) => { + self.process_reverted(reverted).await; + } + } + } + Ok(()) + } + + async fn put_tx_inner(&mut self, tx: Transaction) -> bool { + if let Err(e) = self.store.write().await.put_tx(tx.clone()) { + error!("put_tx error: e={:?}", e); + false + } else { + if let Some(data) = self.data_cache.pop_data(&tx.data_merkle_root) { + let mut store = self.store.write().await; + // We are holding a mutable reference of LogSyncManager, so no chain reorg is + // possible after put_tx. + if let Err(e) = store + .put_chunks_with_tx_hash( + tx.seq, + tx.hash(), + ChunkArray { + data, + start_index: 0, + }, + ) + .and_then(|_| store.finalize_tx_with_hash(tx.seq, tx.hash())) + { + error!("put_tx data error: e={:?}", e); + return false; + } + } + self.data_cache.garbage_collect(self.next_tx_seq); + self.next_tx_seq += 1; + true + } + } +} + +async fn run_and_log( + mut on_error: impl FnMut(), + f: impl Future> + Send, +) -> Option +where + E: Debug, +{ + match f.await { + Err(e) => { + error!("log sync failure: e={:?}", e); + on_error(); + None + } + Ok(r) => Some(r), + } +} + +async fn repeat_run_and_log(f: impl Fn() -> F) -> R +where + E: Debug, + F: Future> + Send, +{ + loop { + if let Some(r) = run_and_log(|| {}, f()).await { + break r; + } + tokio::time::sleep(Duration::from_millis(RETRY_WAIT_MS)).await; + } +} + +pub(crate) mod config; +mod data_cache; +mod log_entry_fetcher; +mod log_query; diff --git a/node/miner/Cargo.toml b/node/miner/Cargo.toml new file mode 100644 index 0000000..58d9e3a --- /dev/null +++ b/node/miner/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "miner" +version = "0.1.0" +edition = "2021" + +[dependencies] +network = { path = "../network" } +storage = { path = "../storage" } +zgs_spec = { path = "../../common/spec" } +zgs_seal = { path = "../../common/zgs_seal" } +task_executor = { path = "../../common/task_executor" } +contract-interface = { path = "../../common/contract-interface" } +ethereum-types = "0.14" +tokio = { version = "1.19.2", features = ["full"] } +tracing = "0.1.35" +blake2 = "0.10" +tiny-keccak = {version="2.0",features = ["keccak"]} +rand = "^0.8" +ethers = "^2" +lazy_static = "1.4" +async-trait = "0.1.56" +shared_types = { path = "../shared_types" } diff --git a/node/miner/src/config.rs b/node/miner/src/config.rs new file mode 100644 index 0000000..8531c4f --- /dev/null +++ b/node/miner/src/config.rs @@ -0,0 +1,53 @@ +use ethereum_types::{Address, H256}; +use ethers::core::k256::SecretKey; +use ethers::middleware::SignerMiddleware; +use ethers::providers::Http; +use ethers::providers::Middleware; +use ethers::providers::Provider; +use ethers::signers::LocalWallet; +use ethers::signers::Signer; + +pub struct MinerConfig { + pub(crate) miner_id: H256, + pub(crate) miner_key: H256, + pub(crate) rpc_endpoint_url: String, + pub(crate) mine_address: Address, + pub(crate) flow_address: Address, +} + +pub type MineServiceMiddleware = SignerMiddleware, LocalWallet>; + +impl MinerConfig { + pub fn new( + miner_id: Option, + miner_key: Option, + rpc_endpoint_url: String, + mine_address: Address, + flow_address: Address, + ) -> Option { + match (miner_id, miner_key) { + (Some(miner_id), Some(miner_key)) => Some(MinerConfig { + miner_id, + miner_key, + rpc_endpoint_url, + mine_address, + flow_address, + }), + _ => None, + } + } + + pub(crate) async fn make_provider(&self) -> Result { + let provider = Provider::::try_from(&self.rpc_endpoint_url) + .map_err(|e| format!("Can not parse blockchain endpoint: {:?}", e))?; + let chain_id = provider + .get_chainid() + .await + .map_err(|e| format!("Unable to get chain_id: {:?}", e))?; + let secret_key = SecretKey::from_bytes(self.miner_key.as_ref().into()) + .map_err(|e| format!("Cannot parse private key: {:?}", e))?; + let signer = LocalWallet::from(secret_key).with_chain_id(chain_id.as_u64()); + let middleware = SignerMiddleware::new(provider, signer); + Ok(middleware) + } +} diff --git a/node/miner/src/lib.rs b/node/miner/src/lib.rs new file mode 100644 index 0000000..0c6804c --- /dev/null +++ b/node/miner/src/lib.rs @@ -0,0 +1,19 @@ +#[macro_use] +extern crate tracing; +extern crate contract_interface; +#[macro_use] +extern crate lazy_static; + +mod config; +mod loader; +mod mine; +pub mod pora; +mod sealer; +mod service; +mod submitter; +mod watcher; + +pub use config::MinerConfig; +pub use loader::PoraLoader; +pub use mine::CustomMineRange; +pub use service::{MineService, MinerMessage}; diff --git a/node/miner/src/loader.rs b/node/miner/src/loader.rs new file mode 100644 index 0000000..92b7397 --- /dev/null +++ b/node/miner/src/loader.rs @@ -0,0 +1,20 @@ +use async_trait::async_trait; +use std::sync::Arc; +use storage::log_store::{MineLoadChunk, Store}; +use tokio::sync::RwLock; + +#[async_trait] +pub trait PoraLoader: Send + Sync { + async fn load_sealed_data(&self, index: u64) -> Option; +} + +#[async_trait] +impl PoraLoader for Arc> { + async fn load_sealed_data(&self, chunk_index: u64) -> Option { + let store = &*self.read().await; + match store.flow().load_sealed_data(chunk_index) { + Ok(Some(chunk)) => Some(chunk), + _ => None, + } + } +} diff --git a/node/miner/src/mine.rs b/node/miner/src/mine.rs new file mode 100644 index 0000000..8751a3d --- /dev/null +++ b/node/miner/src/mine.rs @@ -0,0 +1,173 @@ +use contract_interface::zgs_flow::MineContext; +use ethereum_types::{H256, U256}; +use rand::{self, Rng}; +use task_executor::TaskExecutor; +use tokio::sync::{broadcast, mpsc}; + +use zgs_spec::{SECTORS_PER_LOAD, SECTORS_PER_MAX_MINING_RANGE, SECTORS_PER_PRICING}; + +use crate::{ + pora::{AnswerWithoutProof, Miner}, + watcher::MineContextMessage, + MinerConfig, MinerMessage, PoraLoader, +}; + +use std::sync::Arc; + +pub struct PoraService { + mine_context_receiver: mpsc::UnboundedReceiver, + mine_answer_sender: mpsc::UnboundedSender, + msg_recv: broadcast::Receiver, + loader: Arc, + + puzzle: Option, + mine_range: CustomMineRange, + miner_id: H256, +} + +struct PoraPuzzle { + context: MineContext, + target_quality: U256, +} +#[derive(Clone, Copy, Debug, Default)] +pub struct CustomMineRange { + start_position: Option, + end_position: Option, +} + +impl CustomMineRange { + #[inline] + fn to_valid_range(self, context: &MineContext) -> Option<(u64, u64)> { + let self_start_position = self.start_position?; + let self_end_position = self.end_position?; + + if self_start_position >= self_end_position { + return None; + } + let minable_length = + (context.flow_length.as_u64() / SECTORS_PER_LOAD as u64) * SECTORS_PER_LOAD as u64; + + let mining_length = std::cmp::min(minable_length, SECTORS_PER_MAX_MINING_RANGE as u64); + + let start_position = std::cmp::min(self_start_position, minable_length - mining_length); + let start_position = + (start_position / SECTORS_PER_PRICING as u64) * SECTORS_PER_PRICING as u64; + Some((start_position, mining_length)) + } + + #[inline] + pub(crate) fn is_covered(&self, recall_position: u64) -> Option { + let self_start_position = self.start_position?; + let self_end_position = self.end_position?; + + if self.start_position >= self.end_position { + return Some(false); + } + Some( + self_start_position <= recall_position + SECTORS_PER_LOAD as u64 + || self_end_position > recall_position, + ) + } +} + +impl PoraService { + pub fn spawn( + executor: TaskExecutor, + msg_recv: broadcast::Receiver, + mine_context_receiver: mpsc::UnboundedReceiver, + loader: Arc, + config: &MinerConfig, + ) -> mpsc::UnboundedReceiver { + let (mine_answer_sender, mine_answer_receiver) = + mpsc::unbounded_channel::(); + let mine_range = CustomMineRange { + start_position: Some(0), + end_position: Some(u64::MAX), + }; + let pora = PoraService { + mine_context_receiver, + mine_answer_sender, + msg_recv, + puzzle: None, + mine_range, + miner_id: config.miner_id, + loader, + }; + executor.spawn(async move { Box::pin(pora.start()).await }, "pora_master"); + mine_answer_receiver + } + + async fn start(mut self) { + let mut mining_enabled = true; + let mut channel_opened = true; + loop { + tokio::select! { + biased; + + v = self.msg_recv.recv(), if channel_opened => { + trace!("PoraService receives msg={:?}", v); + match v { + Ok(MinerMessage::ToggleMining(enable)) => { + info!("Toggle mining: {}", if enable { "on" } else { "off" }); + mining_enabled = enable; + } + Ok(MinerMessage::SetStartPosition(pos)) => { + info!("Change start position to: {:?}", pos); + self.mine_range.start_position = pos; + } + Ok(MinerMessage::SetEndPosition(pos)) => { + info!("Change end position to: {:?}", pos); + self.mine_range.end_position = pos; + } + Err(broadcast::error::RecvError::Closed)=>{ + warn!("Unexpected: Mine service config channel closed."); + channel_opened = false; + } + Err(_)=>{ + + } + } + } + + maybe_msg = self.mine_context_receiver.recv() => { + trace!("PoraService receives context={:?}", maybe_msg); + if let Some(msg) = maybe_msg { + debug!("Update mine service: {:?}", msg); + self.puzzle = msg.map(|(context, target_quality)| PoraPuzzle { + context, target_quality + }); + } + } + + _ = async {}, if mining_enabled && self.as_miner().is_some() => { + let nonce = H256(rand::thread_rng().gen()); + let miner = self.as_miner().unwrap(); + if let Some(answer) = miner.iteration(nonce).await{ + debug!("Hit Pora answer {:?}", answer); + if self.mine_answer_sender.send(answer).is_err() { + warn!("Mine submitter channel closed"); + } + } + } + } + } + } + + #[inline] + fn as_miner(&self) -> Option { + match self.puzzle.as_ref() { + Some(puzzle) => self.mine_range.to_valid_range(&puzzle.context).map( + |(start_position, mining_length)| Miner { + start_position, + mining_length, + miner_id: &self.miner_id, + custom_mine_range: &self.mine_range, + context: &puzzle.context, + target_quality: &puzzle.target_quality, + loader: &*self.loader, + }, + ), + _ => None, + } + } +} diff --git a/node/miner/src/pora.rs b/node/miner/src/pora.rs new file mode 100644 index 0000000..bdb1924 --- /dev/null +++ b/node/miner/src/pora.rs @@ -0,0 +1,165 @@ +use crate::{CustomMineRange, PoraLoader}; +use blake2::{Blake2b512, Digest}; +use contract_interface::zgs_flow::MineContext; +use ethereum_types::{H256, U256}; +use zgs_spec::{BYTES_PER_SCRATCHPAD, BYTES_PER_SEAL, SECTORS_PER_LOAD, SECTORS_PER_SEAL}; +use storage::log_store::MineLoadChunk; +use tiny_keccak::{Hasher, Keccak}; + +pub const BLAKE2B_OUTPUT_BYTES: usize = 64; +pub const KECCAK256_OUTPUT_BYTES: usize = 32; + +fn keccak(input: impl AsRef<[u8]>) -> [u8; KECCAK256_OUTPUT_BYTES] { + let mut hasher = Keccak::v256(); + let mut output = [0u8; 32]; + hasher.update(input.as_ref()); + hasher.finalize(&mut output); + output +} + +pub(crate) struct Miner<'a> { + pub start_position: u64, + pub mining_length: u64, + pub miner_id: &'a H256, + pub context: &'a MineContext, + pub target_quality: &'a U256, + pub loader: &'a dyn PoraLoader, + pub custom_mine_range: &'a CustomMineRange, +} +#[derive(Debug)] +pub struct AnswerWithoutProof { + pub context_digest: H256, + pub context_flow_root: H256, + pub nonce: H256, + pub miner_id: H256, + pub start_position: u64, + pub mining_length: u64, + pub recall_position: u64, + pub seal_offset: usize, + pub sealed_data: [u8; BYTES_PER_SEAL], +} + +impl<'a> Miner<'a> { + pub async fn iteration(&self, nonce: H256) -> Option { + let (scratch_pad, recall_seed) = self.make_scratch_pad(&nonce); + + let (_, recall_offset) = U256::from_big_endian(&recall_seed) + .div_mod(U256::from((self.mining_length as usize) / SECTORS_PER_LOAD)); + let recall_offset = recall_offset.as_u64(); + if !self + .custom_mine_range + .is_covered(self.start_position + recall_offset * SECTORS_PER_LOAD as u64) + .unwrap() + { + trace!( + "recall offset not in range: recall_offset={}, range={:?}", + recall_offset, + self.custom_mine_range + ); + return None; + } + + let MineLoadChunk { + loaded_chunk, + avalibilities, + } = self + .loader + .load_sealed_data(self.start_position / SECTORS_PER_LOAD as u64 + recall_offset) + .await?; + + let scratch_pad: [[u8; BYTES_PER_SEAL]; BYTES_PER_SCRATCHPAD / BYTES_PER_SEAL] = + unsafe { std::mem::transmute(scratch_pad) }; + + for ((idx, mut sealed_data), scratch_pad) in loaded_chunk + .into_iter() + .enumerate() + .zip(scratch_pad.iter().cycle()) + .zip(avalibilities.into_iter()) + .filter_map(|(data, avaliable)| avaliable.then_some(data)) + { + // Rust can optimize this loop well. + for (x, y) in sealed_data.iter_mut().zip(scratch_pad.iter()) { + *x ^= y; + } + + let quality = self.pora(idx, &nonce, &sealed_data); + if &quality <= self.target_quality { + debug!("Find a PoRA valid answer, quality: {}", quality); + // Undo mix data when find a valid solition + for (x, y) in sealed_data.iter_mut().zip(scratch_pad.iter()) { + *x ^= y; + } + return Some(AnswerWithoutProof { + context_digest: H256::from(self.context.digest), + context_flow_root: self.context.flow_root.into(), + nonce, + miner_id: *self.miner_id, + start_position: self.start_position, + mining_length: self.mining_length, + recall_position: self.start_position + + recall_offset * SECTORS_PER_LOAD as u64 + + idx as u64 * SECTORS_PER_SEAL as u64, + seal_offset: idx, + sealed_data, + }); + } + } + None + } + + fn make_scratch_pad( + &self, + nonce: &H256, + ) -> ([u8; BYTES_PER_SCRATCHPAD], [u8; KECCAK256_OUTPUT_BYTES]) { + let mut digest: [u8; BLAKE2B_OUTPUT_BYTES] = { + let mut hasher = Blake2b512::new(); + hasher.update(self.miner_id); + hasher.update(nonce); + hasher.update(self.context.digest); + + hasher.update([0u8; 24]); + hasher.update(self.start_position.to_be_bytes()); + + hasher.update([0u8; 24]); + hasher.update(self.mining_length.to_be_bytes()); + + hasher.finalize().into() + }; + + let mut scratch_pad = + [[0u8; BLAKE2B_OUTPUT_BYTES]; BYTES_PER_SCRATCHPAD / BLAKE2B_OUTPUT_BYTES]; + for scratch_pad_cell in scratch_pad.iter_mut() { + digest = Blake2b512::new().chain_update(digest).finalize().into(); + *scratch_pad_cell = digest; + } + + let scratch_pad: [u8; BYTES_PER_SCRATCHPAD] = unsafe { std::mem::transmute(scratch_pad) }; + let recall_seed: [u8; KECCAK256_OUTPUT_BYTES] = keccak(digest); + + (scratch_pad, recall_seed) + } + + #[inline] + fn pora(&self, seal_index: usize, nonce: &H256, mixed_data: &[u8; BYTES_PER_SEAL]) -> U256 { + let mut hasher = Blake2b512::new(); + hasher.update([0u8; 24]); + hasher.update(seal_index.to_be_bytes()); + + hasher.update(self.miner_id); + hasher.update(nonce); + hasher.update(self.context.digest); + + hasher.update([0u8; 24]); + hasher.update(self.start_position.to_be_bytes()); + + hasher.update([0u8; 24]); + hasher.update(self.mining_length.to_be_bytes()); + + hasher.update([0u8; 64]); + hasher.update(mixed_data); + + let digest = hasher.finalize(); + + U256::from_big_endian(&digest[0..32]) + } +} diff --git a/node/miner/src/sealer.rs b/node/miner/src/sealer.rs new file mode 100644 index 0000000..c8de81d --- /dev/null +++ b/node/miner/src/sealer.rs @@ -0,0 +1,213 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use ethereum_types::H256; +use tokio::{ + sync::RwLock, + time::{sleep, Duration, Instant}, +}; + +use contract_interface::{EpochRangeWithContextDigest, ZgsFlow}; +use zgs_spec::SECTORS_PER_SEAL; +use storage::{ + error::Result, + log_store::{SealAnswer, SealTask, Store}, +}; +use task_executor::TaskExecutor; + +use crate::config::{MineServiceMiddleware, MinerConfig}; + +const DB_QUERY_PERIOD_ON_NO_TASK: u64 = 1; +const DB_QUERY_PERIOD_ON_ERROR: u64 = 5; +const CHAIN_STATUS_QUERY_PERIOD: u64 = 5; + +pub struct Sealer { + flow_contract: ZgsFlow, + store: Arc>, + context_cache: BTreeMap, + last_context_flow_length: u64, + miner_id: H256, +} + +impl Sealer { + pub fn spawn( + executor: TaskExecutor, + provider: Arc, + store: Arc>, + config: &MinerConfig, + ) { + let flow_contract = ZgsFlow::new(config.flow_address, provider); + let sealer = Sealer { + flow_contract, + store, + context_cache: Default::default(), + last_context_flow_length: 0, + miner_id: config.miner_id, + }; + + executor.spawn(async move { Box::pin(sealer.start()).await }, "data_sealer"); + } + + async fn start(mut self) { + let db_checker_throttle = sleep(Duration::from_secs(0)); + tokio::pin!(db_checker_throttle); + + let contract_checker_throttle = sleep(Duration::from_secs(0)); + tokio::pin!(contract_checker_throttle); + + loop { + tokio::select! { + biased; + + () = &mut contract_checker_throttle, if !contract_checker_throttle.is_elapsed() => { + } + + () = &mut db_checker_throttle, if !db_checker_throttle.is_elapsed() => { + } + + _ = async {}, if contract_checker_throttle.is_elapsed() => { + if let Err(err) = self.update_flow_length().await{ + warn!("Fetch onchain context failed {:?}", err); + } + contract_checker_throttle.as_mut().reset(Instant::now() + Duration::from_secs(CHAIN_STATUS_QUERY_PERIOD)); + } + + _ = async {}, if db_checker_throttle.is_elapsed() => { + match self.seal_iteration().await { + Ok(true) => {}, + Ok(false) => {db_checker_throttle.as_mut().reset(Instant::now() + Duration::from_secs(DB_QUERY_PERIOD_ON_NO_TASK));} + Err(err) => { + warn!("Seal iteration failed {:?}", err); + db_checker_throttle.as_mut().reset(Instant::now() + Duration::from_secs(DB_QUERY_PERIOD_ON_ERROR)); + } + } + } + } + } + } + + async fn update_flow_length(&mut self) -> Result<()> { + let recent_context = self.flow_contract.make_context_with_result().call().await?; + debug!(target: "seal", "Recent context is {:?}", recent_context); + + let recent_flow_length = recent_context.flow_length.as_u64(); + if self.last_context_flow_length < recent_flow_length { + let epoch_range = self + .flow_contract + .get_epoch_range(recent_context.digest) + .call() + .await?; + self.context_cache.insert( + epoch_range.start, + EpochRangeWithContextDigest { + start: epoch_range.start, + end: epoch_range.end, + digest: recent_context.digest, + }, + ); + self.last_context_flow_length = recent_flow_length; + info!(target: "seal", "Update sealable flow length: {}", recent_flow_length) + } + Ok(()) + } + + async fn fetch_context(&mut self, seal_index: u64) -> Result> { + let last_entry = ((seal_index as usize + 1) * SECTORS_PER_SEAL - 1) as u128; + if self.last_context_flow_length <= last_entry as u64 { + return Ok(None); + } + + if let Some((_, context)) = self.context_cache.range(..=last_entry).next_back() { + if context.start <= last_entry && context.end > last_entry { + return Ok(Some(( + H256(context.digest), + context.end as u64 / SECTORS_PER_SEAL as u64, + ))); + } + } + + let context = match self + .flow_contract + .query_context_at_position(last_entry) + .call() + .await + { + Ok(context) => context, + Err(err) => { + info!("Error when fetch entries {:?}", err); + return Ok(None); + } + }; + info!( + target:"seal", "Fetch new context: range {} -> {}", + context.start, context.end + ); + self.context_cache.insert(context.start, context.clone()); + + Ok(Some(( + H256(context.digest), + context.end as u64 / SECTORS_PER_SEAL as u64, + ))) + } + + async fn fetch_task(&self) -> Result>> { + let seal_index_max = self.last_context_flow_length as usize / SECTORS_PER_SEAL; + self.store + .read() + .await + .flow() + .pull_seal_chunk(seal_index_max) + } + + async fn submit_answer(&self, answers: Vec) -> Result<()> { + self.store + .write() + .await + .flow_mut() + .submit_seal_result(answers) + } + + async fn seal_iteration(&mut self) -> Result { + let tasks = match self.fetch_task().await? { + Some(tasks) if !tasks.is_empty() => tasks, + _ => { + return Ok(false); + } + }; + + debug!( + "Get seal tasks at seal index {:?}", + tasks.iter().map(|x| x.seal_index).collect::>() + ); + + let mut answers = Vec::with_capacity(tasks.len()); + + for task in tasks { + let (context_digest, end_seal) = + if let Some(context) = self.fetch_context(task.seal_index).await? { + context + } else { + trace!(target: "seal", "Index {} is not ready for seal", task.seal_index); + continue; + }; + let mut data = task.non_sealed_data; + zgs_seal::seal( + &mut data, + &self.miner_id, + &context_digest, + task.seal_index * SECTORS_PER_SEAL as u64, + ); + answers.push(SealAnswer { + seal_index: task.seal_index, + version: task.version, + sealed_data: data, + miner_id: self.miner_id, + seal_context: context_digest, + context_end_seal: end_seal, + }); + } + + self.submit_answer(answers).await?; + + Ok(true) + } +} diff --git a/node/miner/src/service.rs b/node/miner/src/service.rs new file mode 100644 index 0000000..5575597 --- /dev/null +++ b/node/miner/src/service.rs @@ -0,0 +1,62 @@ +use crate::sealer::Sealer; +use crate::submitter::Submitter; +use crate::{config::MinerConfig, mine::PoraService, watcher::MineContextWatcher}; +use network::NetworkMessage; +use std::sync::Arc; +use storage::log_store::Store; +use tokio::sync::mpsc; +use tokio::sync::{broadcast, RwLock}; + +#[derive(Clone, Debug)] +pub enum MinerMessage { + /// Enable / Disable Mining + ToggleMining(bool), + + /// Change mining range + SetStartPosition(Option), + SetEndPosition(Option), +} + +pub struct MineService; + +impl MineService { + pub async fn spawn( + executor: task_executor::TaskExecutor, + _network_send: mpsc::UnboundedSender, + config: MinerConfig, + store: Arc>, + ) -> Result, String> { + let provider = Arc::new(config.make_provider().await?); + + let (msg_send, msg_recv) = broadcast::channel(1024); + + let mine_context_receiver = MineContextWatcher::spawn( + executor.clone(), + msg_recv.resubscribe(), + provider.clone(), + &config, + ); + + let mine_answer_receiver = PoraService::spawn( + executor.clone(), + msg_recv.resubscribe(), + mine_context_receiver, + Arc::new(store.clone()), + &config, + ); + + Submitter::spawn( + executor.clone(), + mine_answer_receiver, + provider.clone(), + store.clone(), + &config, + ); + + Sealer::spawn(executor, provider, store, &config); + + debug!("Starting miner service"); + + Ok(msg_send) + } +} diff --git a/node/miner/src/submitter.rs b/node/miner/src/submitter.rs new file mode 100644 index 0000000..958915f --- /dev/null +++ b/node/miner/src/submitter.rs @@ -0,0 +1,128 @@ +use contract_interface::PoraAnswer; +use contract_interface::{ZgsFlow, PoraMine}; +use ethers::providers::PendingTransaction; +use shared_types::FlowRangeProof; +use std::sync::Arc; +use storage::log_store::Store; +use task_executor::TaskExecutor; +use tokio::sync::{mpsc, RwLock}; + +use crate::config::{MineServiceMiddleware, MinerConfig}; +use crate::pora::AnswerWithoutProof; + +use zgs_spec::SECTORS_PER_SEAL; + +const SUBMISSION_RETIES: usize = 3; + +pub struct Submitter { + mine_answer_receiver: mpsc::UnboundedReceiver, + mine_contract: PoraMine, + flow_contract: ZgsFlow, + store: Arc>, +} + +impl Submitter { + pub fn spawn( + executor: TaskExecutor, + mine_answer_receiver: mpsc::UnboundedReceiver, + provider: Arc, + store: Arc>, + config: &MinerConfig, + ) { + let mine_contract = PoraMine::new(config.mine_address, provider.clone()); + let flow_contract = ZgsFlow::new(config.flow_address, provider); + + let submitter = Submitter { + mine_answer_receiver, + mine_contract, + flow_contract, + store, + }; + executor.spawn( + async move { Box::pin(submitter.start()).await }, + "mine_answer_submitter", + ); + } + + async fn start(mut self) { + loop { + match self.mine_answer_receiver.recv().await { + Some(answer) => { + if let Err(e) = self.submit_answer(answer).await { + warn!(e) + } + } + None => { + warn!("Mine submitter stopped because mine answer channel is closed."); + break; + } + }; + } + } + + async fn submit_answer(&mut self, mine_answer: AnswerWithoutProof) -> Result<(), String> { + debug!("submit answer: {:?}", mine_answer); + let sealed_context_digest = self + .flow_contract + .query_context_at_position( + (mine_answer.recall_position + SECTORS_PER_SEAL as u64 - 1) as u128, + ) + .call() + .await + .map_err(|e| format!("Failed to fetch sealed contest digest: {:?}", e))?; + debug!("Fetch sealed context: {:?}", sealed_context_digest); + + let flow_proof = self + .store + .read() + .await + .get_proof_at_root( + &mine_answer.context_flow_root, + mine_answer.recall_position, + SECTORS_PER_SEAL as u64, + ) + .map_err(|e| e.to_string())?; + + let answer = PoraAnswer { + context_digest: mine_answer.context_digest.0, + nonce: mine_answer.nonce.0, + miner_id: mine_answer.miner_id.0, + start_position: mine_answer.start_position.into(), + mine_length: mine_answer.mining_length.into(), + recall_position: mine_answer.recall_position.into(), + seal_offset: mine_answer.seal_offset.into(), + sealed_context_digest: sealed_context_digest.digest, // TODO(kevin): wait for implementation of data sealing. + sealed_data: unsafe { std::mem::transmute(mine_answer.sealed_data) }, + merkle_proof: flow_proof_to_pora_merkle_proof(flow_proof), + }; + trace!("submit_answer: answer={:?}", answer); + + let submission_call = self.mine_contract.submit(answer).legacy(); + let pending_transaction: PendingTransaction<'_, _> = submission_call + .send() + .await + .map_err(|e| format!("Fail to send mine answer transaction: {:?}", e))?; + + let receipt = pending_transaction + .retries(SUBMISSION_RETIES) + .await + .map_err(|e| format!("Fail to execute mine answer transaction: {:?}", e))? + .ok_or(format!( + "Mine answer transaction dropped after {} retires", + SUBMISSION_RETIES + ))?; + + info!("Submit PoRA sucess"); + debug!("Receipt: {:?}", receipt); + + Ok(()) + } +} + +// TODO: The conversion will be simpler if we optimize range proof structure. +fn flow_proof_to_pora_merkle_proof(flow_proof: FlowRangeProof) -> Vec<[u8; 32]> { + let depth_in_sealed_data = SECTORS_PER_SEAL.trailing_zeros() as usize; + let full_proof: Vec<[u8; 32]> = flow_proof.left_proof.lemma().iter().map(|h| h.0).collect(); + // Exclude `item`, the nodes in the sealed data subtree, and `root`. + full_proof[depth_in_sealed_data + 1..full_proof.len() - 1].to_vec() +} diff --git a/node/miner/src/watcher.rs b/node/miner/src/watcher.rs new file mode 100644 index 0000000..b4dcf97 --- /dev/null +++ b/node/miner/src/watcher.rs @@ -0,0 +1,142 @@ +#![allow(unused)] + +use contract_interface::{zgs_flow::MineContext, ZgsFlow, PoraMine}; +use ethereum_types::{Address, H256, U256}; +use ethers::{ + contract::Contract, + providers::{JsonRpcClient, Middleware, Provider, StreamExt}, + types::BlockId, +}; +use task_executor::TaskExecutor; +use tokio::{ + sync::{broadcast, mpsc}, + time::{sleep, Instant, Sleep}, + try_join, +}; + +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use std::{ops::DerefMut, str::FromStr}; + +use crate::{config::MineServiceMiddleware, MinerConfig, MinerMessage}; + +pub type MineContextMessage = Option<(MineContext, U256)>; + +lazy_static! { + pub static ref EMPTY_HASH: H256 = + H256::from_str("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470").unwrap(); +} + +pub struct MineContextWatcher { + provider: Arc, + flow_contract: ZgsFlow, + mine_contract: PoraMine, + + mine_context_sender: mpsc::UnboundedSender, + last_report: MineContextMessage, + + msg_recv: broadcast::Receiver, +} + +impl MineContextWatcher { + pub fn spawn( + executor: TaskExecutor, + msg_recv: broadcast::Receiver, + provider: Arc, + config: &MinerConfig, + ) -> mpsc::UnboundedReceiver { + let provider = provider; + + let mine_contract = PoraMine::new(config.mine_address, provider.clone()); + let flow_contract = ZgsFlow::new(config.flow_address, provider.clone()); + + let (mine_context_sender, mine_context_receiver) = + mpsc::unbounded_channel::(); + let watcher = MineContextWatcher { + provider, + flow_contract, + mine_contract, + mine_context_sender, + msg_recv, + last_report: None, + }; + executor.spawn( + async move { Box::pin(watcher.start()).await }, + "mine_context_watcher", + ); + mine_context_receiver + } + + async fn start(mut self) { + let mut mining_enabled = true; + let mut channel_opened = true; + + let mut mining_throttle = sleep(Duration::from_secs(0)); + tokio::pin!(mining_throttle); + + loop { + tokio::select! { + biased; + + v = self.msg_recv.recv(), if channel_opened => { + match v { + Ok(MinerMessage::ToggleMining(enable)) => { + mining_enabled = enable; + } + Err(broadcast::error::RecvError::Closed) => { + channel_opened = false; + } + _ => {} + } + } + + () = &mut mining_throttle, if !mining_throttle.is_elapsed() => { + } + + _ = async {}, if mining_enabled && mining_throttle.is_elapsed() => { + mining_throttle.as_mut().reset(Instant::now() + Duration::from_secs(1)); + if let Err(err) = self.query_recent_context().await { + warn!(err); + } + } + } + } + } + + async fn query_recent_context(&mut self) -> Result<(), String> { + // let mut watcher = self + // .provider + // .watch_blocks() + // .await + // .expect("should success") + // .stream(); + // watcher.next().await + let context_call = self.flow_contract.make_context_with_result(); + let epoch_call = self.mine_contract.last_mined_epoch(); + let quality_call = self.mine_contract.target_quality(); + + let (context, epoch, quality) = + try_join!(context_call.call(), epoch_call.call(), quality_call.call()) + .map_err(|e| format!("Failed to query mining context: {:?}", e))?; + let report = if context.epoch > epoch && context.digest != EMPTY_HASH.0 { + if context.block_digest == [0; 32] { + warn!("Mine Context is not updated on time."); + None + } else { + Some((context, quality)) + } + } else { + None + }; + + if report != self.last_report { + self.mine_context_sender + .send(report.clone()) + .map_err(|e| format!("Failed to send out the most recent mine context: {:?}", e))?; + } + self.last_report = report; + + Ok(()) + } +} diff --git a/node/network/Cargo.toml b/node/network/Cargo.toml new file mode 100644 index 0000000..3cc7052 --- /dev/null +++ b/node/network/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "network" +version = "0.2.0" +edition = "2021" + +[dependencies] +directory = { path = "../../common/directory" } +dirs = "4.0.0" +discv5 = { version = "0.1.0-beta.13", features = ["libp2p"] } +error-chain = "0.12.4" +eth2_ssz = "0.4.0" +eth2_ssz_derive = "0.3.0" +eth2_ssz_types = "0.2.1" +ethereum-types = "0.14" +fnv = "1.0.7" +futures = "0.3.21" +hashset_delay = { path = "../../common/hashset_delay" } +hex = "0.4.3" +zgs_version = { path = "../../common/zgs_version" } +lazy_static = "1.4.0" +lighthouse_metrics = { path = "../../common/lighthouse_metrics" } +lru = "0.7.7" +parking_lot = "0.12.1" +prometheus-client = "0.16.0" +rand = "0.8.5" +regex = "1.5.6" +serde = { version = "1.0.137", features = ["derive"] } +serde_derive = "1.0.137" +sha2 = "0.10.2" +shared_types = { path = "../shared_types" } +smallvec = "1.8.0" +snap = "1.0.5" +strum = { version = "0.24.1", features = ["derive"] } +task_executor = { path = "../../common/task_executor" } +tiny-keccak = "2.0.2" +tokio = { version = "1.19.2", features = ["time", "macros"] } +tokio-io-timeout = "1.2.0" +tokio-util = { version = "0.6.10", features = ["codec", "compat", "time"] } +tracing = "0.1.35" +unsigned-varint = { version = "0.7.1", features = ["codec"] } +if-addrs = "0.10.1" +slog = "2.7.0" +igd = "0.12.1" + +[dependencies.libp2p] +version = "0.45.1" +default-features = false +features = ["websocket", "identify", "mplex", "yamux", "noise", "gossipsub", "dns-tokio", "tcp-tokio", "plaintext", "secp256k1"] + +[dev-dependencies] +exit-future = "0.2.0" +tempfile = "3.3.0" +tracing-test = "0.2.2" +unused_port = { path = "../../common/unused_port" } +void = "1.0.2" diff --git a/node/network/src/behaviour/gossip_cache.rs b/node/network/src/behaviour/gossip_cache.rs new file mode 100644 index 0000000..4ea3cad --- /dev/null +++ b/node/network/src/behaviour/gossip_cache.rs @@ -0,0 +1,183 @@ +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +use crate::types::GossipKind; +use crate::GossipTopic; + +use tokio_util::time::delay_queue::{DelayQueue, Key}; + +/// Store of gossip messages that we failed to publish and will try again later. By default, all +/// messages are ignored. This behaviour can be changed using `GossipCacheBuilder::default_timeout` +/// to apply the same delay to every kind. Individual timeouts for specific kinds can be set and +/// will overwrite the default_timeout if present. +pub struct GossipCache { + /// Expire timeouts for each topic-msg pair. + expirations: DelayQueue<(GossipTopic, Vec)>, + /// Messages cached for each topic. + topic_msgs: HashMap, Key>>, + /// Timeout for Example messages. + example: Option, + /// Timeout for FindFile messages. + find_file: Option, + /// Timeout for AnnounceFile. + announce_file: Option, +} + +#[derive(Default)] +pub struct GossipCacheBuilder { + default_timeout: Option, + /// Timeout for Example messages. + example: Option, + /// Timeout for blocks FindFile messages. + find_file: Option, + /// Timeout for AnnounceFile messages. + announce_file: Option, +} + +#[allow(dead_code)] +impl GossipCacheBuilder { + /// By default, all timeouts all disabled. Setting a default timeout will enable all timeout + /// that are not already set. + pub fn default_timeout(mut self, timeout: Duration) -> Self { + self.default_timeout = Some(timeout); + self + } + + /// Timeout for Example messages. + pub fn example_timeout(mut self, timeout: Duration) -> Self { + self.example = Some(timeout); + self + } + + /// Timeout for FindFile messages. + pub fn find_file_timeout(mut self, timeout: Duration) -> Self { + self.find_file = Some(timeout); + self + } + + /// Timeout for AnnounceFile messages. + pub fn announce_file_timeout(mut self, timeout: Duration) -> Self { + self.announce_file = Some(timeout); + self + } + + pub fn build(self) -> GossipCache { + let GossipCacheBuilder { + default_timeout, + example, + find_file, + announce_file, + } = self; + + GossipCache { + expirations: DelayQueue::default(), + topic_msgs: HashMap::default(), + example: example.or(default_timeout), + find_file: find_file.or(default_timeout), + announce_file: announce_file.or(default_timeout), + } + } +} + +impl GossipCache { + /// Get a builder of a `GossipCache`. Topic kinds for which no timeout is defined will be + /// ignored if added in `insert`. + pub fn builder() -> GossipCacheBuilder { + GossipCacheBuilder::default() + } + + // Insert a message to be sent later. + pub fn insert(&mut self, topic: GossipTopic, data: Vec) { + let expire_timeout = match topic.kind() { + GossipKind::Example => self.example, + GossipKind::FindFile => self.find_file, + GossipKind::AnnounceFile => self.announce_file, + }; + + let expire_timeout = match expire_timeout { + Some(expire_timeout) => expire_timeout, + None => return, + }; + + match self + .topic_msgs + .entry(topic.clone()) + .or_default() + .entry(data.clone()) + { + Entry::Occupied(key) => self.expirations.reset(key.get(), expire_timeout), + Entry::Vacant(entry) => { + let key = self.expirations.insert((topic, data), expire_timeout); + entry.insert(key); + } + } + } + + // Get the registered messages for this topic. + pub fn retrieve(&mut self, topic: &GossipTopic) -> Option> + '_> { + if let Some(msgs) = self.topic_msgs.remove(topic) { + for (_, key) in msgs.iter() { + self.expirations.remove(key); + } + Some(msgs.into_keys()) + } else { + None + } + } +} + +impl futures::stream::Stream for GossipCache { + type Item = Result; // We don't care to retrieve the expired data. + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.expirations.poll_expired(cx) { + Poll::Ready(Some(Ok(expired))) => { + let expected_key = expired.key(); + let (topic, data) = expired.into_inner(); + match self.topic_msgs.get_mut(&topic) { + Some(msgs) => { + let key = msgs.remove(&data); + debug_assert_eq!(key, Some(expected_key)); + if msgs.is_empty() { + // no more messages for this topic. + self.topic_msgs.remove(&topic); + } + } + None => { + #[cfg(debug_assertions)] + panic!("Topic for registered message is not present.") + } + } + Poll::Ready(Some(Ok(topic))) + } + Poll::Ready(Some(Err(x))) => Poll::Ready(Some(Err(x.to_string()))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +#[cfg(test)] +mod tests { + use crate::types::GossipKind; + + use super::*; + use futures::stream::StreamExt; + + #[tokio::test] + async fn test_stream() { + let mut cache = GossipCache::builder() + .default_timeout(Duration::from_millis(300)) + .build(); + let test_topic = + GossipTopic::new(GossipKind::Example, crate::types::GossipEncoding::SSZSnappy); + cache.insert(test_topic, vec![]); + tokio::time::sleep(Duration::from_millis(300)).await; + while cache.next().await.is_some() {} + assert!(cache.expirations.is_empty()); + assert!(cache.topic_msgs.is_empty()); + } +} diff --git a/node/network/src/behaviour/gossipsub_scoring_parameters.rs b/node/network/src/behaviour/gossipsub_scoring_parameters.rs new file mode 100644 index 0000000..dbe6e68 --- /dev/null +++ b/node/network/src/behaviour/gossipsub_scoring_parameters.rs @@ -0,0 +1,354 @@ +// use crate::error; +use libp2p::gossipsub::PeerScoreThresholds; +// use std::cmp::max; +// use std::collections::HashMap; +// use std::time::Duration; + +// const MAX_IN_MESH_SCORE: f64 = 10.0; +// const MAX_FIRST_MESSAGE_DELIVERIES_SCORE: f64 = 40.0; +// const BEACON_BLOCK_WEIGHT: f64 = 0.5; +// const BEACON_AGGREGATE_PROOF_WEIGHT: f64 = 0.5; +// const VOLUNTARY_EXIT_WEIGHT: f64 = 0.05; +// const PROPOSER_SLASHING_WEIGHT: f64 = 0.05; +// const ATTESTER_SLASHING_WEIGHT: f64 = 0.05; + +/// The time window (seconds) that we expect messages to be forwarded to us in the mesh. +// const MESH_MESSAGE_DELIVERIES_WINDOW: u64 = 2; + +// Const as this is used in the peer manager to prevent gossip from disconnecting peers. +pub const GREYLIST_THRESHOLD: f64 = -16000.0; + +/// Builds the peer score thresholds. +pub fn lighthouse_gossip_thresholds() -> PeerScoreThresholds { + PeerScoreThresholds { + gossip_threshold: -4000.0, + publish_threshold: -8000.0, + graylist_threshold: GREYLIST_THRESHOLD, + accept_px_threshold: 100.0, + opportunistic_graft_threshold: 5.0, + } +} + +// pub struct PeerScoreSettings { +// // slot: Duration, +// epoch: Duration, + +// // beacon_attestation_subnet_weight: f64, +// max_positive_score: f64, + +// decay_interval: Duration, +// decay_to_zero: f64, + +// // mesh_n: usize, +// // max_committees_per_slot: usize, +// // target_committee_size: usize, +// // target_aggregators_per_committee: usize, +// attestation_subnet_count: u64, +// } + +// impl PeerScoreSettings { +// pub fn new(chain_spec: &ChainSpec) -> PeerScoreSettings { +// let slot = Duration::from_secs(chain_spec.seconds_per_slot); +// let beacon_attestation_subnet_weight = 1.0 / chain_spec.attestation_subnet_count as f64; +// let max_positive_score = (MAX_IN_MESH_SCORE + MAX_FIRST_MESSAGE_DELIVERIES_SCORE) +// * (BEACON_BLOCK_WEIGHT +// + BEACON_AGGREGATE_PROOF_WEIGHT +// + beacon_attestation_subnet_weight * chain_spec.attestation_subnet_count as f64 +// + VOLUNTARY_EXIT_WEIGHT +// + PROPOSER_SLASHING_WEIGHT +// + ATTESTER_SLASHING_WEIGHT); + +// PeerScoreSettings { +// // slot, +// // epoch: slot * TSpec::slots_per_epoch() as u32, +// epoch: slot * 32 as u32, +// // beacon_attestation_subnet_weight, +// max_positive_score, +// decay_interval: max(Duration::from_secs(1), slot), +// decay_to_zero: 0.01, +// // mesh_n: gs_config.mesh_n(), +// // max_committees_per_slot: chain_spec.max_committees_per_slot, +// // target_committee_size: chain_spec.target_committee_size, +// // target_aggregators_per_committee: chain_spec.target_aggregators_per_committee as usize, +// attestation_subnet_count: chain_spec.attestation_subnet_count, +// } +// } + +// pub fn get_peer_score_params( +// &self, +// thresholds: &PeerScoreThresholds, +// ) -> error::Result { +// let mut params = PeerScoreParams { +// decay_interval: self.decay_interval, +// decay_to_zero: self.decay_to_zero, +// retain_score: self.epoch * 100, +// app_specific_weight: 1.0, +// ip_colocation_factor_threshold: 8.0, // Allow up to 8 nodes per IP +// behaviour_penalty_threshold: 6.0, +// behaviour_penalty_decay: self.score_parameter_decay(self.epoch * 10), +// ..Default::default() +// }; + +// let target_value = Self::decay_convergence( +// params.behaviour_penalty_decay, +// // 10.0 / TSpec::slots_per_epoch() as f64, +// 10.0 / 32 as f64, +// ) - params.behaviour_penalty_threshold; +// params.behaviour_penalty_weight = thresholds.gossip_threshold / target_value.powi(2); + +// params.topic_score_cap = self.max_positive_score * 0.5; +// params.ip_colocation_factor_weight = -params.topic_score_cap; + +// params.topics = HashMap::new(); + +// // let get_hash = |kind: GossipKind| -> TopicHash { +// // let topic: Topic = +// // GossipTopic::new(kind, GossipEncoding::default()).into(); +// // topic.hash() +// // }; + +// // //first all fixed topics +// // params.topics.insert( +// // get_hash(GossipKind::VoluntaryExit), +// // Self::get_topic_params( +// // self, +// // VOLUNTARY_EXIT_WEIGHT, +// // // 4.0 / TSpec::slots_per_epoch() as f64, +// // 4.0 / 32 as f64, +// // self.epoch * 100, +// // None, +// // ), +// // ); +// // params.topics.insert( +// // get_hash(GossipKind::AttesterSlashing), +// // Self::get_topic_params( +// // self, +// // ATTESTER_SLASHING_WEIGHT, +// // // 1.0 / 5.0 / TSpec::slots_per_epoch() as f64, +// // 1.0 / 5.0 / 32 as f64, +// // self.epoch * 100, +// // None, +// // ), +// // ); +// // params.topics.insert( +// // get_hash(GossipKind::ProposerSlashing), +// // Self::get_topic_params( +// // self, +// // PROPOSER_SLASHING_WEIGHT, +// // // 1.0 / 5.0 / TSpec::slots_per_epoch() as f64, +// // 1.0 / 5.0 / 32 as f64, +// // self.epoch * 100, +// // None, +// // ), +// // ); + +// //dynamic topics +// // let (beacon_block_params, beacon_aggregate_proof_params, beacon_attestation_subnet_params) = +// // self.get_dynamic_topic_params(active_validators, current_slot)?; + +// // params +// // .topics +// // .insert(get_hash(GossipKind::BeaconBlock), beacon_block_params); + +// // params.topics.insert( +// // get_hash(GossipKind::BeaconAggregateAndProof), +// // beacon_aggregate_proof_params, +// // ); + +// // for i in 0..self.attestation_subnet_count { +// // params.topics.insert( +// // get_hash(GossipKind::Attestation(SubnetId::new(i))), +// // beacon_attestation_subnet_params.clone(), +// // ); +// // } + +// Ok(params) +// } + +// // pub fn get_dynamic_topic_params( +// // &self, +// // active_validators: usize, +// // current_slot: Slot, +// // ) -> error::Result<(TopicScoreParams, TopicScoreParams, TopicScoreParams)> { +// // let (aggregators_per_slot, committees_per_slot) = +// // self.expected_aggregator_count_per_slot(active_validators)?; +// // let multiple_bursts_per_subnet_per_epoch = committees_per_slot as u64 +// // >= 2 * self.attestation_subnet_count / TSpec::slots_per_epoch(); + +// // let beacon_block_params = Self::get_topic_params( +// // self, +// // BEACON_BLOCK_WEIGHT, +// // 1.0, +// // self.epoch * 20, +// // Some((TSpec::slots_per_epoch() * 5, 3.0, self.epoch, current_slot)), +// // ); + +// // let beacon_aggregate_proof_params = Self::get_topic_params( +// // self, +// // BEACON_AGGREGATE_PROOF_WEIGHT, +// // aggregators_per_slot, +// // self.epoch, +// // Some((TSpec::slots_per_epoch() * 2, 4.0, self.epoch, current_slot)), +// // ); +// // let beacon_attestation_subnet_params = Self::get_topic_params( +// // self, +// // self.beacon_attestation_subnet_weight, +// // active_validators as f64 +// // / self.attestation_subnet_count as f64 +// // / TSpec::slots_per_epoch() as f64, +// // self.epoch +// // * (if multiple_bursts_per_subnet_per_epoch { +// // 1 +// // } else { +// // 4 +// // }), +// // Some(( +// // TSpec::slots_per_epoch() +// // * (if multiple_bursts_per_subnet_per_epoch { +// // 4 +// // } else { +// // 16 +// // }), +// // 16.0, +// // if multiple_bursts_per_subnet_per_epoch { +// // self.slot * (TSpec::slots_per_epoch() as u32 / 2 + 1) +// // } else { +// // self.epoch * 3 +// // }, +// // current_slot, +// // )), +// // ); + +// // Ok(( +// // beacon_block_params, +// // beacon_aggregate_proof_params, +// // beacon_attestation_subnet_params, +// // )) +// // } + +// pub fn attestation_subnet_count(&self) -> u64 { +// self.attestation_subnet_count +// } + +// fn score_parameter_decay_with_base( +// decay_time: Duration, +// decay_interval: Duration, +// decay_to_zero: f64, +// ) -> f64 { +// let ticks = decay_time.as_secs_f64() / decay_interval.as_secs_f64(); +// decay_to_zero.powf(1.0 / ticks) +// } + +// fn decay_convergence(decay: f64, rate: f64) -> f64 { +// rate / (1.0 - decay) +// } + +// // fn threshold(decay: f64, rate: f64) -> f64 { +// // Self::decay_convergence(decay, rate) * decay +// // } + +// // fn expected_aggregator_count_per_slot( +// // &self, +// // active_validators: usize, +// // ) -> error::Result<(f64, usize)> { +// // let committees_per_slot = TSpec::get_committee_count_per_slot_with( +// // active_validators, +// // self.max_committees_per_slot, +// // self.target_committee_size, +// // ) +// // .map_err(|e| format!("Could not get committee count from spec: {:?}", e))?; + +// // let committees = committees_per_slot * TSpec::slots_per_epoch() as usize; + +// // let smaller_committee_size = active_validators / committees; +// // let num_larger_committees = active_validators - smaller_committee_size * committees; + +// // let modulo_smaller = max( +// // 1, +// // smaller_committee_size / self.target_aggregators_per_committee as usize, +// // ); +// // let modulo_larger = max( +// // 1, +// // (smaller_committee_size + 1) / self.target_aggregators_per_committee as usize, +// // ); + +// // Ok(( +// // (((committees - num_larger_committees) * smaller_committee_size) as f64 +// // / modulo_smaller as f64 +// // + (num_larger_committees * (smaller_committee_size + 1)) as f64 +// // / modulo_larger as f64) +// // / TSpec::slots_per_epoch() as f64, +// // committees_per_slot, +// // )) +// // } + +// fn score_parameter_decay(&self, decay_time: Duration) -> f64 { +// Self::score_parameter_decay_with_base(decay_time, self.decay_interval, self.decay_to_zero) +// } + +// // fn get_topic_params( +// // &self, +// // topic_weight: f64, +// // expected_message_rate: f64, +// // first_message_decay_time: Duration, +// // // decay slots (decay time in slots), cap factor, activation window, current slot +// // mesh_message_info: Option<(u64, f64, Duration, Slot)>, +// // ) -> TopicScoreParams { +// // let mut t_params = TopicScoreParams::default(); + +// // t_params.topic_weight = topic_weight; + +// // t_params.time_in_mesh_quantum = self.slot; +// // t_params.time_in_mesh_cap = 3600.0 / t_params.time_in_mesh_quantum.as_secs_f64(); +// // t_params.time_in_mesh_weight = 10.0 / t_params.time_in_mesh_cap; + +// // t_params.first_message_deliveries_decay = +// // self.score_parameter_decay(first_message_decay_time); +// // t_params.first_message_deliveries_cap = Self::decay_convergence( +// // t_params.first_message_deliveries_decay, +// // 2.0 * expected_message_rate / self.mesh_n as f64, +// // ); +// // t_params.first_message_deliveries_weight = 40.0 / t_params.first_message_deliveries_cap; + +// // if let Some((decay_slots, cap_factor, activation_window, current_slot)) = mesh_message_info +// // { +// // let decay_time = self.slot * decay_slots as u32; +// // t_params.mesh_message_deliveries_decay = self.score_parameter_decay(decay_time); +// // t_params.mesh_message_deliveries_threshold = Self::threshold( +// // t_params.mesh_message_deliveries_decay, +// // expected_message_rate / 50.0, +// // ); +// // t_params.mesh_message_deliveries_cap = +// // if cap_factor * t_params.mesh_message_deliveries_threshold < 2.0 { +// // 2.0 +// // } else { +// // cap_factor * t_params.mesh_message_deliveries_threshold +// // }; +// // t_params.mesh_message_deliveries_activation = activation_window; +// // t_params.mesh_message_deliveries_window = +// // Duration::from_secs(MESH_MESSAGE_DELIVERIES_WINDOW); +// // t_params.mesh_failure_penalty_decay = t_params.mesh_message_deliveries_decay; +// // t_params.mesh_message_deliveries_weight = -t_params.topic_weight; +// // t_params.mesh_failure_penalty_weight = t_params.mesh_message_deliveries_weight; +// // if decay_slots >= current_slot.as_u64() { +// // t_params.mesh_message_deliveries_threshold = 0.0; +// // t_params.mesh_message_deliveries_weight = 0.0; +// // } +// // } else { +// // t_params.mesh_message_deliveries_weight = 0.0; +// // t_params.mesh_message_deliveries_threshold = 0.0; +// // t_params.mesh_message_deliveries_decay = 0.0; +// // t_params.mesh_message_deliveries_cap = 0.0; +// // t_params.mesh_message_deliveries_window = Duration::from_secs(0); +// // t_params.mesh_message_deliveries_activation = Duration::from_secs(0); +// // t_params.mesh_failure_penalty_decay = 0.0; +// // t_params.mesh_failure_penalty_weight = 0.0; +// // } + +// // t_params.invalid_message_deliveries_weight = +// // -self.max_positive_score / t_params.topic_weight; +// // t_params.invalid_message_deliveries_decay = self.score_parameter_decay(self.epoch * 50); + +// // t_params +// // } +// } diff --git a/node/network/src/behaviour/mod.rs b/node/network/src/behaviour/mod.rs new file mode 100644 index 0000000..c035d44 --- /dev/null +++ b/node/network/src/behaviour/mod.rs @@ -0,0 +1,989 @@ +use crate::behaviour::gossipsub_scoring_parameters::lighthouse_gossip_thresholds; +use crate::config::gossipsub_config; +use crate::discovery::{Discovery, DiscoveryEvent, FIND_NODE_QUERY_CLOSEST_PEERS}; +use crate::peer_manager::{ + config::Config as PeerManagerCfg, peerdb::score::PeerAction, peerdb::score::ReportSource, + ConnectionDirection, PeerManager, PeerManagerEvent, +}; +use crate::rpc::methods::DataByHashRequest; +use crate::rpc::methods::GetChunksRequest; +use crate::rpc::*; +use crate::service::Context as ServiceContext; +use crate::types::{GossipEncoding, GossipKind, GossipTopic, SnappyTransform}; +use crate::{error, metrics, Enr, NetworkGlobals, PubsubMessage, TopicHash}; +use futures::stream::StreamExt; +use libp2p::gossipsub::error::PublishError; +use libp2p::{ + core::{ + connection::ConnectionId, identity::Keypair, multiaddr::Protocol as MProtocol, Multiaddr, + }, + gossipsub::{ + subscription_filter::{AllowAllSubscriptionFilter, MaxCountSubscriptionFilter}, + Gossipsub as BaseGossipsub, GossipsubEvent, IdentTopic as Topic, MessageAcceptance, + MessageAuthenticity, MessageId, + }, + identify::{Identify, IdentifyConfig, IdentifyEvent}, + swarm::{ + dial_opts::{DialOpts, PeerCondition}, + AddressScore, NetworkBehaviour, NetworkBehaviourAction as NBAction, + NetworkBehaviourEventProcess, PollParameters, + }, + NetworkBehaviour, PeerId, +}; +use shared_types::ChunkArrayWithProof; +use std::{ + collections::VecDeque, + sync::Arc, + task::{Context, Poll}, +}; + +use self::gossip_cache::GossipCache; + +mod gossip_cache; +pub mod gossipsub_scoring_parameters; + +/// The number of peers we target per subnet for discovery queries. +pub const TARGET_SUBNET_PEERS: usize = 6; + +const MAX_IDENTIFY_ADDRESSES: usize = 10; + +/// Identifier of requests sent by a peer. +pub type PeerRequestId = (ConnectionId, SubstreamId); + +pub type SubscriptionFilter = MaxCountSubscriptionFilter; +pub type Gossipsub = BaseGossipsub; + +/// Identifier of a request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RequestId { + Application(AppReqId), + Behaviour, +} + +/// The types of events than can be obtained from polling the behaviour. +#[derive(Debug)] +pub enum BehaviourEvent { + /// We have successfully dialed and connected to a peer. + PeerConnectedOutgoing(PeerId), + /// A peer has successfully dialed and connected to us. + PeerConnectedIncoming(PeerId), + /// A peer has disconnected. + PeerDisconnected(PeerId), + /// The peer needs to be banned. + PeerBanned(PeerId), + /// The peer has been unbanned. + PeerUnbanned(PeerId), + /// An RPC Request that was sent failed. + RPCFailed { + /// The id of the failed request. + id: AppReqId, + /// The peer to which this request was sent. + peer_id: PeerId, + }, + RequestReceived { + /// The peer that sent the request. + peer_id: PeerId, + /// Identifier of the request. All responses to this request must use this id. + id: PeerRequestId, + /// Request the peer sent. + request: Request, + }, + ResponseReceived { + /// Peer that sent the response. + peer_id: PeerId, + /// Id of the request to which the peer is responding. + id: AppReqId, + /// Response the peer sent. + response: Response, + }, + PubsubMessage { + /// The gossipsub message id. Used when propagating blocks after validation. + id: MessageId, + /// The peer from which we received this message, not the peer that published it. + propagation_source: PeerId, + /// The peer who published and signed this message. + source: PeerId, + /// The topic that this message was sent on. + topic: TopicHash, + /// The message itself. + message: PubsubMessage, + }, + /// Inform the network to send a Status to this peer. + StatusPeer(PeerId), +} + +/// Internal type to pass messages from sub-behaviours to the poll of the global behaviour to be +/// specified as an NBAction. +enum InternalBehaviourMessage { + /// Dial a Peer. + DialPeer(PeerId), + /// The socket has been updated. + SocketUpdated(Multiaddr), +} + +/// Builds the network behaviour that manages the core protocols of eth2. +/// This core behaviour is managed by `Behaviour` which adds peer management to all core +/// behaviours. +#[derive(NetworkBehaviour)] +#[behaviour( + out_event = "BehaviourEvent", + poll_method = "poll", + event_process = true +)] +pub struct Behaviour { + /* Sub-Behaviours */ + /// The routing pub-sub mechanism for eth2. + gossipsub: Gossipsub, + /// The Eth2 RPC specified in the wire-0 protocol. + eth2_rpc: RPC>, + /// Discv5 Discovery protocol. + discovery: Discovery, + /// Keep regular connection to peers and disconnect if absent. + // NOTE: The id protocol is used for initial interop. This will be removed by mainnet. + /// Provides IP addresses and peer information. + identify: Identify, + /// The peer manager that keeps track of peer's reputation and status. + peer_manager: PeerManager, + + /* Auxiliary Fields */ + /// The output events generated by this behaviour to be consumed in the swarm poll. + #[behaviour(ignore)] + events: VecDeque>, + /// Internal behaviour events, the NBAction type is composed of sub-behaviours, so we use a + /// custom type here to avoid having to specify the concrete type. + #[behaviour(ignore)] + internal_events: VecDeque, + /// A collections of variables accessible outside the network service. + #[behaviour(ignore)] + network_globals: Arc, + /// The waker for the current task. This is used to wake the task when events are added to the + /// queue. + #[behaviour(ignore)] + waker: Option, + /// The interval for updating gossipsub scores + #[behaviour(ignore)] + update_gossipsub_scores: tokio::time::Interval, + #[behaviour(ignore)] + gossip_cache: GossipCache, +} + +/// Implements the combined behaviour for the libp2p service. +impl Behaviour { + pub async fn new( + local_key: &Keypair, + ctx: ServiceContext<'_>, + network_globals: Arc, + ) -> error::Result { + let mut config = ctx.config.clone(); + + // Set up the Identify Behaviour + let identify_config = if config.private { + IdentifyConfig::new( + "".into(), + local_key.public(), // Still send legitimate public key + ) + .with_cache_size(0) + } else { + IdentifyConfig::new("eth2/1.0.0".into(), local_key.public()) + .with_agent_version(zgs_version::version_with_platform()) + .with_cache_size(0) + }; + + // Build and start the discovery sub-behaviour + let mut discovery = Discovery::new(local_key, &config, network_globals.clone()).await?; + // start searching for peers + discovery.discover_peers(FIND_NODE_QUERY_CLOSEST_PEERS); + + let filter = MaxCountSubscriptionFilter { + filter: AllowAllSubscriptionFilter {}, + max_subscribed_topics: 200, + max_subscriptions_per_request: 150, // 148 in theory = (64 attestation + 4 sync committee + 6 core topics) * 2 + }; + + config.gs_config = gossipsub_config(config.network_load); + + // If metrics are enabled for gossipsub build the configuration + let snappy_transform = SnappyTransform::new(config.gs_config.max_transmit_size()); + let mut gossipsub = Gossipsub::new_with_subscription_filter_and_transform( + MessageAuthenticity::Signed(local_key.clone()), + config.gs_config.clone(), + None, // gossipsub_metrics + filter, + snappy_transform, + ) + .map_err(|e| format!("Could not construct gossipsub: {:?}", e))?; + + // Construct a set of gossipsub peer scoring parameters + // We don't know the number of active validators and the current slot yet + let thresholds = lighthouse_gossip_thresholds(); + + // let score_settings = PeerScoreSettings::new(ctx.chain_spec); + + // // Prepare scoring parameters + // let params = score_settings.get_peer_score_params( + // &thresholds, + // )?; + + // trace!(behaviour_log, "Using peer score params"; "params" => ?params); + + let params = libp2p::gossipsub::PeerScoreParams::default(); + + // Set up a scoring update interval + let update_gossipsub_scores = tokio::time::interval(params.decay_interval); + + gossipsub + .with_peer_score(params, thresholds) + .expect("Valid score params and thresholds"); + + let peer_manager_cfg = PeerManagerCfg { + discovery_enabled: !config.disable_discovery, + metrics_enabled: config.metrics_enabled, + target_peer_count: config.target_peers, + ..Default::default() + }; + + let slot_duration = std::time::Duration::from_secs(12); + // let slot_duration = std::time::Duration::from_secs(ctx.chain_spec.seconds_per_slot); + + let gossip_cache = GossipCache::builder() + .example_timeout(slot_duration) // TODO + .build(); + + Ok(Behaviour { + // Sub-behaviours + gossipsub, + eth2_rpc: RPC::new(), + discovery, + identify: Identify::new(identify_config), + // Auxiliary fields + peer_manager: PeerManager::new(peer_manager_cfg, network_globals.clone()).await?, + events: VecDeque::new(), + internal_events: VecDeque::new(), + network_globals, + waker: None, + gossip_cache, + update_gossipsub_scores, + }) + } + + /* Public Accessible Functions to interact with the behaviour */ + + /// Get a mutable reference to the underlying discovery sub-behaviour. + pub fn discovery_mut(&mut self) -> &mut Discovery { + &mut self.discovery + } + + /// Get a mutable reference to the peer manager. + pub fn peer_manager_mut(&mut self) -> &mut PeerManager { + &mut self.peer_manager + } + + /// Returns the local ENR of the node. + pub fn local_enr(&self) -> Enr { + self.network_globals.local_enr() + } + + /// Obtain a reference to the gossipsub protocol. + pub fn gs(&self) -> &Gossipsub { + &self.gossipsub + } + + /* Pubsub behaviour functions */ + + /// Subscribes to a gossipsub topic kind, letting the network service determine the + /// encoding and fork version. + pub fn subscribe_kind(&mut self, kind: GossipKind) -> bool { + let gossip_topic = GossipTopic::new(kind, GossipEncoding::default()); + + self.subscribe(gossip_topic) + } + + /// Unsubscribes from a gossipsub topic kind, letting the network service determine the + /// encoding and fork version. + pub fn unsubscribe_kind(&mut self, kind: GossipKind) -> bool { + let gossip_topic = GossipTopic::new(kind, GossipEncoding::default()); + self.unsubscribe(gossip_topic) + } + + /// Subscribes to a gossipsub topic. + /// + /// Returns `true` if the subscription was successful and `false` otherwise. + pub fn subscribe(&mut self, topic: GossipTopic) -> bool { + // update the network globals + self.network_globals + .gossipsub_subscriptions + .write() + .insert(topic.clone()); + + let topic: Topic = topic.into(); + + match self.gossipsub.subscribe(&topic) { + Err(e) => { + warn!(%topic, error = ?e, "Failed to subscribe to topic"); + false + } + Ok(_) => { + debug!(%topic, "Subscribed to topic"); + true + } + } + } + + /// Unsubscribe from a gossipsub topic. + pub fn unsubscribe(&mut self, topic: GossipTopic) -> bool { + // update the network globals + self.network_globals + .gossipsub_subscriptions + .write() + .remove(&topic); + + // unsubscribe from the topic + let libp2p_topic: Topic = topic.clone().into(); + + match self.gossipsub.unsubscribe(&libp2p_topic) { + Err(_) => { + warn!(topic = %libp2p_topic, "Failed to unsubscribe from topic"); + false + } + Ok(v) => { + // Inform the network + debug!(%topic, "Unsubscribed to topic"); + v + } + } + } + + /// Publishes a list of messages on the pubsub (gossipsub) behaviour, choosing the encoding. + pub fn publish(&mut self, messages: Vec) { + for message in messages { + for topic in message.topics(GossipEncoding::default()) { + let message_data = message.encode(GossipEncoding::default()); + if let Err(e) = self + .gossipsub + .publish(topic.clone().into(), message_data.clone()) + { + warn!(error = ?e, "Could not publish message"); + + // add to metrics + if let Some(v) = metrics::get_int_gauge( + &metrics::FAILED_PUBLISHES_PER_MAIN_TOPIC, + &[&format!("{:?}", topic.kind())], + ) { + v.inc() + }; + + if let PublishError::InsufficientPeers = e { + self.gossip_cache.insert(topic, message_data); + } + } + } + } + } + + /// Informs the gossipsub about the result of a message validation. + /// If the message is valid it will get propagated by gossipsub. + pub fn report_message_validation_result( + &mut self, + propagation_source: &PeerId, + message_id: MessageId, + validation_result: MessageAcceptance, + ) { + if let Some(result) = match validation_result { + MessageAcceptance::Accept => None, + MessageAcceptance::Ignore => Some("ignore"), + MessageAcceptance::Reject => Some("reject"), + } { + if let Some(client) = self + .network_globals + .peers + .read() + .peer_info(propagation_source) + .map(|info| info.client().kind.as_ref()) + { + metrics::inc_counter_vec( + &metrics::GOSSIP_UNACCEPTED_MESSAGES_PER_CLIENT, + &[client, result], + ) + } + } + + if let Err(e) = self.gossipsub.report_message_validation_result( + &message_id, + propagation_source, + validation_result, + ) { + warn!( + %message_id, + peer_id = %propagation_source, + error = ?e, + "Failed to report message validation", + ); + } + } + + /* Eth2 RPC behaviour functions */ + + /// Send a request to a peer over RPC. + pub fn send_request(&mut self, peer_id: PeerId, request_id: AppReqId, request: Request) { + self.eth2_rpc + .send_request(peer_id, RequestId::Application(request_id), request.into()) + } + + /// Send a successful response to a peer over RPC. + pub fn send_successful_response( + &mut self, + peer_id: PeerId, + id: PeerRequestId, + response: Response, + ) { + self.eth2_rpc.send_response(peer_id, id, response.into()) + } + + /// Inform the peer that their request produced an error. + pub fn send_error_reponse( + &mut self, + peer_id: PeerId, + id: PeerRequestId, + error: RPCResponseErrorCode, + reason: String, + ) { + self.eth2_rpc + .send_response(peer_id, id, RPCCodedResponse::Error(error, reason.into())) + } + + /* Peer management functions */ + + /// Disconnects from a peer providing a reason. + /// + /// This will send a goodbye, disconnect and then ban the peer. + /// This is fatal for a peer, and should be used in unrecoverable circumstances. + pub fn goodbye_peer(&mut self, peer_id: &PeerId, reason: GoodbyeReason, source: ReportSource) { + self.peer_manager.goodbye_peer(peer_id, reason, source); + } + + /// Returns an iterator over all enr entries in the DHT. + pub fn enr_entries(&mut self) -> Vec { + self.discovery.table_entries_enr() + } + + /// Add an ENR to the routing table of the discovery mechanism. + pub fn add_enr(&mut self, enr: Enr) { + self.discovery.add_enr(enr); + } + + /* Private internal functions */ + + /// Sends a Ping request to the peer. + fn ping(&mut self, peer_id: PeerId) { + let ping = crate::rpc::Ping { data: 1 }; + trace!(%peer_id, "Sending Ping"); + let id = RequestId::Behaviour; + self.eth2_rpc + .send_request(peer_id, id, OutboundRequest::Ping(ping)); + } + + /// Sends a Pong response to the peer. + fn pong(&mut self, id: PeerRequestId, peer_id: PeerId) { + let ping = crate::rpc::Ping { data: 1 }; + trace!(request_id = ?id.1, %peer_id, "Sending Pong"); + let event = RPCCodedResponse::Success(RPCResponse::Pong(ping)); + self.eth2_rpc.send_response(peer_id, id, event); + } + + /// Returns a reference to the peer manager to allow the swarm to notify the manager of peer + /// status + pub fn peer_manager(&mut self) -> &mut PeerManager { + &mut self.peer_manager + } + + // RPC Propagation methods + /// Queues the response to be sent upwards as long at it was requested outside the Behaviour. + fn propagate_response(&mut self, id: RequestId, peer_id: PeerId, response: Response) { + match id { + RequestId::Application(id) => self.add_event(BehaviourEvent::ResponseReceived { + peer_id, + id, + response, + }), + RequestId::Behaviour => {} + } + } + + /// Convenience function to propagate a request. + fn propagate_request(&mut self, id: PeerRequestId, peer_id: PeerId, request: Request) { + // Increment metrics + match &request { + Request::Status(_) => { + metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["status"]) + } + Request::DataByHash { .. } => { + metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["data_by_hash"]) + } + Request::GetChunks { .. } => { + metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["get_chunks"]) + } + } + self.add_event(BehaviourEvent::RequestReceived { + peer_id, + id, + request, + }); + } + + /// Adds an event to the queue waking the current task to process it. + fn add_event(&mut self, event: BehaviourEvent) { + self.events.push_back(event); + if let Some(waker) = &self.waker { + waker.wake_by_ref(); + } + } +} + +/* Behaviour Event Process Implementations + * + * These implementations dictate how to process each event that is emitted from each + * sub-behaviour. + */ + +// Gossipsub +impl NetworkBehaviourEventProcess for Behaviour +where + AppReqId: ReqId, +{ + fn inject_event(&mut self, event: GossipsubEvent) { + match event { + GossipsubEvent::Message { + propagation_source, + message_id: id, + message: gs_msg, + } => { + // Note: We are keeping track here of the peer that sent us the message, not the + // peer that originally published the message. + match PubsubMessage::decode(&gs_msg.topic, &gs_msg.data) { + Err(e) => { + debug!(topic = ?gs_msg.topic, error = ?e, "Could not decode gossipsub message"); + //reject the message + if let Err(e) = self.gossipsub.report_message_validation_result( + &id, + &propagation_source, + MessageAcceptance::Reject, + ) { + warn!(message_id = %id, peer_id = %propagation_source, error = ?e, "Failed to report message validation"); + } + } + Ok(msg) => { + // Notify the network + self.add_event(BehaviourEvent::PubsubMessage { + id, + propagation_source, + source: gs_msg.source.expect("message is signed"), // TODO: is this guaranteed? + topic: gs_msg.topic, + message: msg, + }); + } + } + } + GossipsubEvent::Subscribed { peer_id: _, topic } => { + if let Ok(topic) = GossipTopic::decode(topic.as_str()) { + // if let Some(subnet_id) = topic.subnet_id() { + // self.network_globals + // .peers + // .write() + // .add_subscription(&peer_id, subnet_id); + // } + // Try to send the cached messages for this topic + if let Some(msgs) = self.gossip_cache.retrieve(&topic) { + for data in msgs { + let topic_str: &str = topic.kind().as_ref(); + match self.gossipsub.publish(topic.clone().into(), data) { + Ok(_) => { + warn!(topic = topic_str, "Gossip message published on retry"); + if let Some(v) = metrics::get_int_counter( + &metrics::GOSSIP_LATE_PUBLISH_PER_TOPIC_KIND, + &[topic_str], + ) { + v.inc() + }; + } + Err(e) => { + warn!(topic = topic_str, error = %e, "Gossip message publish failed on retry"); + if let Some(v) = metrics::get_int_counter( + &metrics::GOSSIP_FAILED_LATE_PUBLISH_PER_TOPIC_KIND, + &[topic_str], + ) { + v.inc() + }; + } + } + } + } + } + } + GossipsubEvent::Unsubscribed { + peer_id: _, + topic: _, + } => { + // if let Some(subnet_id) = subnet_from_topic_hash(&topic) { + // self.network_globals + // .peers + // .write() + // .remove_subscription(&peer_id, &subnet_id); + // } + } + GossipsubEvent::GossipsubNotSupported { peer_id } => { + debug!(%peer_id, "Peer does not support gossipsub"); + self.peer_manager.report_peer( + &peer_id, + PeerAction::LowToleranceError, + ReportSource::Gossipsub, + Some(GoodbyeReason::Unknown), + "does_not_support_gossipsub", + ); + } + } + } +} + +// RPC +impl NetworkBehaviourEventProcess>> for Behaviour +where + AppReqId: ReqId, +{ + fn inject_event(&mut self, event: RPCMessage>) { + let peer_id = event.peer_id; + + if !self.peer_manager.is_connected(&peer_id) { + debug!( + peer = %peer_id, + "Ignoring rpc message of disconnecting peer" + ); + return; + } + + let handler_id = event.conn_id; + // The METADATA and PING RPC responses are handled within the behaviour and not propagated + match event.event { + Err(handler_err) => { + match handler_err { + HandlerErr::Inbound { + id: _, + proto, + error, + } => { + if matches!(error, RPCError::HandlerRejected) { + // this peer's request got canceled + } + // Inform the peer manager of the error. + // An inbound error here means we sent an error to the peer, or the stream + // timed out. + self.peer_manager.handle_rpc_error( + &peer_id, + proto, + &error, + ConnectionDirection::Incoming, + ); + } + HandlerErr::Outbound { id, proto, error } => { + // Inform the peer manager that a request we sent to the peer failed + self.peer_manager.handle_rpc_error( + &peer_id, + proto, + &error, + ConnectionDirection::Outgoing, + ); + // inform failures of requests comming outside the behaviour + if let RequestId::Application(id) = id { + self.add_event(BehaviourEvent::RPCFailed { peer_id, id }); + } + } + } + } + Ok(RPCReceived::Request(id, request)) => { + let peer_request_id = (handler_id, id); + match request { + /* Behaviour managed protocols: Ping and Metadata */ + InboundRequest::Ping(ping) => { + // inform the peer manager and send the response + self.peer_manager.ping_request(&peer_id, ping.data); + // send a ping response + self.pong(peer_request_id, peer_id); + } + InboundRequest::Goodbye(reason) => { + // queue for disconnection without a goodbye message + debug!( + %peer_id, + %reason, + client = %self.network_globals.client(&peer_id), + "Peer sent Goodbye" + ); + // NOTE: We currently do not inform the application that we are + // disconnecting here. The RPC handler will automatically + // disconnect for us. + // The actual disconnection event will be relayed to the application. + } + /* Protocols propagated to the Network */ + InboundRequest::Status(msg) => { + // inform the peer manager that we have received a status from a peer + self.peer_manager.peer_statusd(&peer_id); + // propagate the STATUS message upwards + self.propagate_request(peer_request_id, peer_id, Request::Status(msg)) + } + InboundRequest::DataByHash(req) => { + self.propagate_request(peer_request_id, peer_id, Request::DataByHash(req)) + } + InboundRequest::GetChunks(req) => { + self.propagate_request(peer_request_id, peer_id, Request::GetChunks(req)) + } + } + } + Ok(RPCReceived::Response(id, resp)) => { + match resp { + /* Behaviour managed protocols */ + RPCResponse::Pong(ping) => self.peer_manager.pong_response(&peer_id, ping.data), + /* Network propagated protocols */ + RPCResponse::Status(msg) => { + // inform the peer manager that we have received a status from a peer + self.peer_manager.peer_statusd(&peer_id); + // propagate the STATUS message upwards + self.propagate_response(id, peer_id, Response::Status(msg)); + } + RPCResponse::DataByHash(resp) => { + self.propagate_response(id, peer_id, Response::DataByHash(Some(resp))) + } + RPCResponse::Chunks(resp) => { + self.propagate_response(id, peer_id, Response::Chunks(resp)) + } + } + } + Ok(RPCReceived::EndOfStream(id, termination)) => { + let response = match termination { + ResponseTermination::DataByHash => Response::DataByHash(None), + }; + self.propagate_response(id, peer_id, response); + } + } + } +} + +// Discovery +impl NetworkBehaviourEventProcess for Behaviour +where + AppReqId: ReqId, +{ + fn inject_event(&mut self, event: DiscoveryEvent) { + match event { + DiscoveryEvent::SocketUpdated(socket_addr) => { + // A new UDP socket has been detected. + // Build a multiaddr to report to libp2p + let mut multiaddr = Multiaddr::from(socket_addr.ip()); + // NOTE: This doesn't actually track the external TCP port. More sophisticated NAT handling + // should handle this. + multiaddr.push(MProtocol::Tcp(self.network_globals.listen_port_tcp())); + self.internal_events + .push_back(InternalBehaviourMessage::SocketUpdated(multiaddr)); + } + DiscoveryEvent::QueryResult(results) => { + let to_dial_peers = self.peer_manager.peers_discovered(results); + for peer_id in to_dial_peers { + debug!(%peer_id, "Dialing discovered peer"); + // For any dial event, inform the peer manager + let enr = self.discovery_mut().enr_of_peer(&peer_id); + self.peer_manager.inject_dialing(&peer_id, enr); + self.internal_events + .push_back(InternalBehaviourMessage::DialPeer(peer_id)); + } + } + } + } +} + +// Identify +impl NetworkBehaviourEventProcess for Behaviour +where + AppReqId: ReqId, +{ + fn inject_event(&mut self, event: IdentifyEvent) { + match event { + IdentifyEvent::Received { peer_id, mut info } => { + if info.listen_addrs.len() > MAX_IDENTIFY_ADDRESSES { + debug!("More than 10 addresses have been identified, truncating"); + info.listen_addrs.truncate(MAX_IDENTIFY_ADDRESSES); + } + // send peer info to the peer manager. + self.peer_manager.identify(&peer_id, &info); + } + IdentifyEvent::Sent { .. } => {} + IdentifyEvent::Error { .. } => {} + IdentifyEvent::Pushed { .. } => {} + } + } +} + +type BehaviourHandler = as NetworkBehaviour>::ConnectionHandler; + +impl Behaviour +where + AppReqId: ReqId, +{ + /// Consumes the events list and drives the Lighthouse global NetworkBehaviour. + fn poll( + &mut self, + cx: &mut Context, + _: &mut impl PollParameters, + ) -> Poll, BehaviourHandler>> { + if let Some(waker) = &self.waker { + if waker.will_wake(cx.waker()) { + self.waker = Some(cx.waker().clone()); + } + } else { + self.waker = Some(cx.waker().clone()); + } + + // Handle internal events first + if let Some(event) = self.internal_events.pop_front() { + match event { + InternalBehaviourMessage::DialPeer(peer_id) => { + // Submit the event + let handler = self.new_handler(); + return Poll::Ready(NBAction::Dial { + opts: DialOpts::peer_id(peer_id) + .condition(PeerCondition::Disconnected) + .build(), + handler, + }); + } + InternalBehaviourMessage::SocketUpdated(address) => { + return Poll::Ready(NBAction::ReportObservedAddr { + address, + score: AddressScore::Finite(1), + }); + } + } + } + + if let Some(event) = self.events.pop_front() { + return Poll::Ready(NBAction::GenerateEvent(event)); + } + + // perform gossipsub score updates when necessary + while self.update_gossipsub_scores.poll_tick(cx).is_ready() { + self.peer_manager.update_gossipsub_scores(&self.gossipsub); + } + + // poll the gossipsub cache to clear expired messages + while let Poll::Ready(Some(result)) = self.gossip_cache.poll_next_unpin(cx) { + match result { + Err(e) => warn!(error = ?e, "Gossip cache error"), + Ok(expired_topic) => { + if let Some(v) = metrics::get_int_counter( + &metrics::GOSSIP_EXPIRED_LATE_PUBLISH_PER_TOPIC_KIND, + &[expired_topic.kind().as_ref()], + ) { + v.inc() + }; + } + } + } + + Poll::Pending + } +} + +impl NetworkBehaviourEventProcess for Behaviour { + fn inject_event(&mut self, event: PeerManagerEvent) { + match event { + PeerManagerEvent::PeerConnectedIncoming(peer_id) => { + self.add_event(BehaviourEvent::PeerConnectedIncoming(peer_id)); + } + PeerManagerEvent::PeerConnectedOutgoing(peer_id) => { + self.add_event(BehaviourEvent::PeerConnectedOutgoing(peer_id)); + } + PeerManagerEvent::PeerDisconnected(peer_id) => { + self.add_event(BehaviourEvent::PeerDisconnected(peer_id)); + } + PeerManagerEvent::Banned(peer_id, associated_ips) => { + self.discovery.ban_peer(&peer_id, associated_ips); + self.add_event(BehaviourEvent::PeerBanned(peer_id)); + } + PeerManagerEvent::UnBanned(peer_id, associated_ips) => { + self.discovery.unban_peer(&peer_id, associated_ips); + self.add_event(BehaviourEvent::PeerUnbanned(peer_id)); + } + PeerManagerEvent::Status(peer_id) => { + // it's time to status. We don't keep a beacon chain reference here, so we inform + // the network to send a status to this peer + self.add_event(BehaviourEvent::StatusPeer(peer_id)); + } + PeerManagerEvent::DiscoverPeers(peers_to_find) => { + // Peer manager has requested a discovery query for more peers. + self.discovery.discover_peers(peers_to_find); + } + PeerManagerEvent::Ping(peer_id) => { + // send a ping request to this peer + self.ping(peer_id); + } + PeerManagerEvent::DisconnectPeer(peer_id, reason) => { + debug!(%peer_id, %reason, "Peer Manager disconnecting peer"); + // send one goodbye + self.eth2_rpc + .shutdown(peer_id, RequestId::Behaviour, reason); + } + } + } +} + +/* Public API types */ + +/// The type of RPC requests the Behaviour informs it has received and allows for sending. +/// +// NOTE: This is an application-level wrapper over the lower network level requests that can be +// sent. The main difference is the absence of the Ping, Metadata and Goodbye protocols, which don't +// leave the Behaviour. For all protocols managed by RPC see `RPCRequest`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Request { + /// A Status message. + Status(StatusMessage), + /// A data by hash request. + DataByHash(DataByHashRequest), + /// A GetChunks request. + GetChunks(GetChunksRequest), +} + +impl std::convert::From for OutboundRequest { + fn from(req: Request) -> OutboundRequest { + match req { + Request::Status(s) => OutboundRequest::Status(s), + Request::DataByHash(r) => OutboundRequest::DataByHash(r), + Request::GetChunks(r) => OutboundRequest::GetChunks(r), + } + } +} + +/// The type of RPC responses the Behaviour informs it has received, and allows for sending. +/// +// NOTE: This is an application-level wrapper over the lower network level responses that can be +// sent. The main difference is the absense of Pong and Metadata, which don't leave the +// Behaviour. For all protocol reponses managed by RPC see `RPCResponse` and +// `RPCCodedResponse`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Response { + /// A Status message. + Status(StatusMessage), + /// A response to a get DATA_BY_HASH request. A None response signals the end of the batch. + DataByHash(Option>), + /// A response to a GET_CHUNKS request. + Chunks(ChunkArrayWithProof), +} + +impl std::convert::From for RPCCodedResponse { + fn from(resp: Response) -> RPCCodedResponse { + match resp { + Response::Status(s) => RPCCodedResponse::Success(RPCResponse::Status(s)), + Response::DataByHash(r) => match r { + Some(b) => RPCCodedResponse::Success(RPCResponse::DataByHash(b)), + None => RPCCodedResponse::StreamTermination(ResponseTermination::DataByHash), + }, + Response::Chunks(c) => RPCCodedResponse::Success(RPCResponse::Chunks(c)), + } + } +} diff --git a/node/network/src/config.rs b/node/network/src/config.rs new file mode 100644 index 0000000..e93ac53 --- /dev/null +++ b/node/network/src/config.rs @@ -0,0 +1,351 @@ +use crate::types::GossipKind; +use crate::{Enr, PeerIdSerialized}; +use directory::{ + DEFAULT_BEACON_NODE_DIR, DEFAULT_HARDCODED_NETWORK, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR, +}; +use discv5::{Discv5Config, Discv5ConfigBuilder}; +use libp2p::gossipsub::{ + FastMessageId, GossipsubConfig, GossipsubConfigBuilder, GossipsubMessage, MessageId, + RawGossipsubMessage, ValidationMode, +}; +use libp2p::Multiaddr; +use serde_derive::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::path::PathBuf; +use std::time::Duration; + +/// The maximum transmit size of gossip messages in bytes post-merge. +const GOSSIP_MAX_SIZE_POST_MERGE: usize = 10 * 1_048_576; // 10M + +/// The cache time is set to accommodate the circulation time of an attestation. +/// +/// The p2p spec declares that we accept attestations within the following range: +/// +/// ```ignore +/// ATTESTATION_PROPAGATION_SLOT_RANGE = 32 +/// attestation.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= attestation.data.slot +/// ``` +/// +/// Therefore, we must accept attestations across a span of 33 slots (where each slot is 12 +/// seconds). We add an additional second to account for the 500ms gossip clock disparity, and +/// another 500ms for "fudge factor". +pub const DUPLICATE_CACHE_TIME: Duration = Duration::from_secs(33 * 12 + 1); + +// We treat uncompressed messages as invalid and never use the INVALID_SNAPPY_DOMAIN as in the +// specification. We leave it here for posterity. +// const MESSAGE_DOMAIN_INVALID_SNAPPY: [u8; 4] = [0, 0, 0, 0]; +const MESSAGE_DOMAIN_VALID_SNAPPY: [u8; 4] = [1, 0, 0, 0]; + +/// The maximum size of gossip messages. +pub fn gossip_max_size() -> usize { + GOSSIP_MAX_SIZE_POST_MERGE +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(default)] +/// Network configuration for lighthouse. +pub struct Config { + /// Data directory where node's keyfile is stored + pub network_dir: PathBuf, + + /// IP address to listen on. + pub listen_address: std::net::IpAddr, + + /// The TCP port that libp2p listens on. + pub libp2p_port: u16, + + /// UDP port that discovery listens on. + pub discovery_port: u16, + + /// The address to broadcast to peers about which address we are listening on. None indicates + /// that no discovery address has been set in the CLI args. + pub enr_address: Option, + + /// The udp port to broadcast to peers in order to reach back for discovery. + pub enr_udp_port: Option, + + /// The tcp port to broadcast to peers in order to reach back for libp2p services. + pub enr_tcp_port: Option, + + /// Target number of connected peers. + pub target_peers: usize, + + /// Gossipsub configuration parameters. + #[serde(skip)] + pub gs_config: GossipsubConfig, + + /// Discv5 configuration parameters. + #[serde(skip)] + pub discv5_config: Discv5Config, + + /// List of nodes to initially connect to. + pub boot_nodes_enr: Vec, + + /// List of nodes to initially connect to, on Multiaddr format. + pub boot_nodes_multiaddr: Vec, + + /// List of libp2p nodes to initially connect to. + pub libp2p_nodes: Vec, + + /// List of trusted libp2p nodes which are not scored. + pub trusted_peers: Vec, + + /// Client version + pub client_version: String, + + /// Disables the discovery protocol from starting. + pub disable_discovery: bool, + + /// Attempt to construct external port mappings with UPnP. + pub upnp_enabled: bool, + + /// Subscribe to all subnets for the duration of the runtime. + pub subscribe_all_subnets: bool, + + /// Import/aggregate all attestations recieved on subscribed subnets for the duration of the + /// runtime. + pub import_all_attestations: bool, + + /// A setting specifying a range of values that tune the network parameters of lighthouse. The + /// lower the value the less bandwidth used, but the slower messages will be received. + pub network_load: u8, + + /// Indicates if the user has set the network to be in private mode. Currently this + /// prevents sending client identifying information over identify. + pub private: bool, + + /// Shutdown beacon node after sync is completed. + pub shutdown_after_sync: bool, + + /// List of extra topics to initially subscribe to as strings. + pub topics: Vec, + + /// Whether metrics are enabled. + pub metrics_enabled: bool, +} + +impl Default for Config { + /// Generate a default network configuration. + fn default() -> Self { + // WARNING: this directory default should be always overwritten with parameters + // from cli for specific networks. + let network_dir = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(DEFAULT_ROOT_DIR) + .join(DEFAULT_HARDCODED_NETWORK) + .join(DEFAULT_BEACON_NODE_DIR) + .join(DEFAULT_NETWORK_DIR); + + // Note: Using the default config here. Use `gossipsub_config` function for getting + // Lighthouse specific configuration for gossipsub. + let gs_config = GossipsubConfigBuilder::default() + .build() + .expect("valid gossipsub configuration"); + + // Discv5 Unsolicited Packet Rate Limiter + let filter_rate_limiter = Some( + discv5::RateLimiterBuilder::new() + .total_n_every(10, Duration::from_secs(1)) // Allow bursts, average 10 per second + .ip_n_every(9, Duration::from_secs(1)) // Allow bursts, average 9 per second + .node_n_every(8, Duration::from_secs(1)) // Allow bursts, average 8 per second + .build() + .expect("The total rate limit has been specified"), + ); + + // discv5 configuration + let discv5_config = Discv5ConfigBuilder::new() + .enable_packet_filter() + .session_cache_capacity(5000) + .request_timeout(Duration::from_secs(1)) + .query_peer_timeout(Duration::from_secs(2)) + .query_timeout(Duration::from_secs(30)) + .request_retries(1) + .enr_peer_update_min(10) + .query_parallelism(5) + .disable_report_discovered_peers() + .ip_limit() // limits /24 IP's in buckets. + .incoming_bucket_limit(8) // half the bucket size + .filter_rate_limiter(filter_rate_limiter) + .filter_max_bans_per_ip(Some(5)) + .filter_max_nodes_per_ip(Some(10)) + .table_filter(|enr| enr.ip().map_or(false, |ip| is_global(&ip))) // Filter non-global IPs + .ban_duration(Some(Duration::from_secs(3600))) + .ping_interval(Duration::from_secs(300)) + .build(); + + // NOTE: Some of these get overridden by the corresponding CLI default values. + Config { + network_dir, + listen_address: "0.0.0.0".parse().expect("valid ip address"), + libp2p_port: 9000, + discovery_port: 9000, + enr_address: None, + enr_udp_port: None, + enr_tcp_port: None, + target_peers: 50, + gs_config, + discv5_config, + boot_nodes_enr: vec![], + boot_nodes_multiaddr: vec![], + libp2p_nodes: vec![], + trusted_peers: vec![], + client_version: zgs_version::version_with_platform(), + disable_discovery: false, + upnp_enabled: true, + network_load: 3, + private: false, + subscribe_all_subnets: false, + import_all_attestations: false, + shutdown_after_sync: false, + topics: Vec::new(), + metrics_enabled: false, + } + } +} + +/// Controls sizes of gossipsub meshes to tune a Lighthouse node's bandwidth/performance. +pub struct NetworkLoad { + pub name: &'static str, + pub mesh_n_low: usize, + pub outbound_min: usize, + pub mesh_n: usize, + pub mesh_n_high: usize, + pub gossip_lazy: usize, + pub history_gossip: usize, + pub heartbeat_interval: Duration, +} + +impl From for NetworkLoad { + fn from(load: u8) -> NetworkLoad { + match load { + 1 => NetworkLoad { + name: "Low", + mesh_n_low: 1, + outbound_min: 1, + mesh_n: 3, + mesh_n_high: 4, + gossip_lazy: 3, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(1200), + }, + 2 => NetworkLoad { + name: "Low", + mesh_n_low: 2, + outbound_min: 2, + mesh_n: 4, + mesh_n_high: 8, + gossip_lazy: 3, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(1000), + }, + 3 => NetworkLoad { + name: "Average", + mesh_n_low: 3, + outbound_min: 2, + mesh_n: 5, + mesh_n_high: 10, + gossip_lazy: 3, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(700), + }, + 4 => NetworkLoad { + name: "Average", + mesh_n_low: 4, + outbound_min: 3, + mesh_n: 8, + mesh_n_high: 12, + gossip_lazy: 3, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(700), + }, + // 5 and above + _ => NetworkLoad { + name: "High", + mesh_n_low: 5, + outbound_min: 3, + mesh_n: 10, + mesh_n_high: 15, + gossip_lazy: 5, + history_gossip: 6, + heartbeat_interval: Duration::from_millis(500), + }, + } + } +} + +/// Return a Lighthouse specific `GossipsubConfig` where the `message_id_fn` depends on the current fork. +pub fn gossipsub_config(network_load: u8) -> GossipsubConfig { + // The function used to generate a gossipsub message id + // We use the first 8 bytes of SHA256(data) for content addressing + let fast_gossip_message_id = + |message: &RawGossipsubMessage| FastMessageId::from(&Sha256::digest(&message.data)[..8]); + fn prefix(prefix: [u8; 4], message: &GossipsubMessage) -> Vec { + let topic_bytes = message.topic.as_str().as_bytes(); + + // according to: https://github.com/ethereum/consensus-specs/blob/dev/specs/merge/p2p-interface.md#the-gossip-domain-gossipsub + // the derivation of the message-id remains the same in the merge + let topic_len_bytes = topic_bytes.len().to_le_bytes(); + let mut vec = Vec::with_capacity( + prefix.len() + topic_len_bytes.len() + topic_bytes.len() + message.data.len(), + ); + vec.extend_from_slice(&prefix); + vec.extend_from_slice(&topic_len_bytes); + vec.extend_from_slice(topic_bytes); + vec.extend_from_slice(&message.data); + vec + } + + let gossip_message_id = move |message: &GossipsubMessage| { + MessageId::from( + &Sha256::digest(prefix(MESSAGE_DOMAIN_VALID_SNAPPY, message).as_slice())[..20], + ) + }; + + let load = NetworkLoad::from(network_load); + + GossipsubConfigBuilder::default() + .max_transmit_size(gossip_max_size()) + .heartbeat_interval(load.heartbeat_interval) + .mesh_n(load.mesh_n) + .mesh_n_low(load.mesh_n_low) + .mesh_outbound_min(load.outbound_min) + .mesh_n_high(load.mesh_n_high) + .gossip_lazy(load.gossip_lazy) + .fanout_ttl(Duration::from_secs(60)) + .history_length(12) + .max_messages_per_rpc(Some(500)) // Responses to IWANT can be quite large + .history_gossip(load.history_gossip) + .validate_messages() // require validation before propagation + .validation_mode(ValidationMode::Strict) + .duplicate_cache_time(DUPLICATE_CACHE_TIME) + .message_id_fn(gossip_message_id) + .fast_message_id_fn(fast_gossip_message_id) + .allow_self_origin(true) + .build() + .expect("valid gossipsub configuration") +} + +/// Helper function to determine if the IpAddr is a global address or not. The `is_global()` +/// function is not yet stable on IpAddr. +#[allow(clippy::nonminimal_bool)] +fn is_global(addr: &std::net::Ipv4Addr) -> bool { + // check if this address is 192.0.0.9 or 192.0.0.10. These addresses are the only two + // globally routable addresses in the 192.0.0.0/24 range. + if u32::from_be_bytes(addr.octets()) == 0xc0000009 + || u32::from_be_bytes(addr.octets()) == 0xc000000a + { + return true; + } + !addr.is_private() + && !addr.is_loopback() + && !addr.is_link_local() + && !addr.is_broadcast() + && !addr.is_documentation() + // shared + && !(addr.octets()[0] == 100 && (addr.octets()[1] & 0b1100_0000 == 0b0100_0000)) &&!(addr.octets()[0] & 240 == 240 && !addr.is_broadcast()) + // addresses reserved for future protocols (`192.0.0.0/24`) + // reserved + && !(addr.octets()[0] == 192 && addr.octets()[1] == 0 && addr.octets()[2] == 0) + // Make sure the address is not in 0.0.0.0/8 + && addr.octets()[0] != 0 +} diff --git a/node/network/src/discovery/enr.rs b/node/network/src/discovery/enr.rs new file mode 100644 index 0000000..1e661c3 --- /dev/null +++ b/node/network/src/discovery/enr.rs @@ -0,0 +1,152 @@ +//! Helper functions and an extension trait for Ethereum 2 ENRs. + +pub use discv5::enr::{self, CombinedKey, EnrBuilder}; + +use super::enr_ext::CombinedKeyExt; +use super::ENR_FILENAME; +use crate::types::Enr; +use crate::NetworkConfig; +use discv5::enr::EnrKey; +use libp2p::core::identity::Keypair; +use std::fs::File; +use std::io::prelude::*; +use std::path::Path; +use std::str::FromStr; + +/// Either use the given ENR or load an ENR from file if it exists and matches the current NodeId +/// and sequence number. +/// If an ENR exists, with the same NodeId, this function checks to see if the loaded ENR from +/// disk is suitable to use, otherwise we increment the given ENR's sequence number. +pub fn use_or_load_enr( + enr_key: &CombinedKey, + local_enr: &mut Enr, + config: &NetworkConfig, +) -> Result<(), String> { + let enr_f = config.network_dir.join(ENR_FILENAME); + if let Ok(mut enr_file) = File::open(enr_f.clone()) { + let mut enr_string = String::new(); + match enr_file.read_to_string(&mut enr_string) { + Err(_) => debug!("Could not read ENR from file"), + Ok(_) => { + match Enr::from_str(&enr_string) { + Ok(disk_enr) => { + // if the same node id, then we may need to update our sequence number + if local_enr.node_id() == disk_enr.node_id() { + if compare_enr(local_enr, &disk_enr) { + debug!(file = ?enr_f, "ENR loaded from disk"); + // the stored ENR has the same configuration, use it + *local_enr = disk_enr; + return Ok(()); + } + + // same node id, different configuration - update the sequence number + // Note: local_enr is generated with default(0) attnets value, + // so a non default value in persisted enr will also update sequence number. + let new_seq_no = disk_enr.seq().checked_add(1).ok_or("ENR sequence number on file is too large. Remove it to generate a new NodeId")?; + local_enr.set_seq(new_seq_no, enr_key).map_err(|e| { + format!("Could not update ENR sequence number: {:?}", e) + })?; + debug!(seq = new_seq_no, "ENR sequence number increased"); + } + } + Err(e) => { + warn!(error = ?e, "ENR from file could not be decoded"); + } + } + } + } + } + + save_enr_to_disk(&config.network_dir, local_enr); + + Ok(()) +} + +/// Loads an ENR from file if it exists and matches the current NodeId and sequence number. If none +/// exists, generates a new one. +/// +/// If an ENR exists, with the same NodeId, this function checks to see if the loaded ENR from +/// disk is suitable to use, otherwise we increment our newly generated ENR's sequence number. +pub fn build_or_load_enr(local_key: Keypair, config: &NetworkConfig) -> Result { + // Build the local ENR. + // Note: Discovery should update the ENR record's IP to the external IP as seen by the + // majority of our peers, if the CLI doesn't expressly forbid it. + let enr_key = CombinedKey::from_libp2p(&local_key)?; + let mut local_enr = build_enr(&enr_key, config)?; + + use_or_load_enr(&enr_key, &mut local_enr, config)?; + Ok(local_enr) +} + +pub fn create_enr_builder_from_config( + config: &NetworkConfig, + enable_tcp: bool, +) -> EnrBuilder { + let mut builder = EnrBuilder::new("v4"); + if let Some(enr_address) = config.enr_address { + builder.ip(enr_address); + } + if let Some(udp_port) = config.enr_udp_port { + builder.udp(udp_port); + } + // we always give it our listening tcp port + if enable_tcp { + let tcp_port = config.enr_tcp_port.unwrap_or(config.libp2p_port); + builder.tcp(tcp_port); + } + builder +} + +/// Builds a lighthouse ENR given a `NetworkConfig`. +pub fn build_enr(enr_key: &CombinedKey, config: &NetworkConfig) -> Result { + let mut builder = create_enr_builder_from_config(config, true); + + builder + .build(enr_key) + .map_err(|e| format!("Could not build Local ENR: {:?}", e)) +} + +/// Defines the conditions under which we use the locally built ENR or the one stored on disk. +/// If this function returns true, we use the `disk_enr`. +fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool { + // take preference over disk_enr address if one is not specified + (local_enr.ip().is_none() || local_enr.ip() == disk_enr.ip()) + // tcp ports must match + && local_enr.tcp() == disk_enr.tcp() + // take preference over disk udp port if one is not specified + && (local_enr.udp().is_none() || local_enr.udp() == disk_enr.udp()) +} + +/// Loads enr from the given directory +pub fn load_enr_from_disk(dir: &Path) -> Result { + let enr_f = dir.join(ENR_FILENAME); + let mut enr_file = + File::open(enr_f).map_err(|e| format!("Failed to open enr file: {:?}", e))?; + let mut enr_string = String::new(); + match enr_file.read_to_string(&mut enr_string) { + Err(_) => Err("Could not read ENR from file".to_string()), + Ok(_) => match Enr::from_str(&enr_string) { + Ok(disk_enr) => Ok(disk_enr), + Err(e) => Err(format!("ENR from file could not be decoded: {:?}", e)), + }, + } +} + +/// Saves an ENR to disk +pub fn save_enr_to_disk(dir: &Path, enr: &Enr) { + let _ = std::fs::create_dir_all(dir); + match File::create(dir.join(Path::new(ENR_FILENAME))) + .and_then(|mut f| f.write_all(enr.to_base64().as_bytes())) + { + Ok(_) => { + debug!("ENR written to disk"); + } + Err(e) => { + warn!( + file = %format!("{:?}{:?}",dir, ENR_FILENAME), + error = %e, + "Could not write ENR to file", + ); + } + } +} diff --git a/node/network/src/discovery/enr_ext.rs b/node/network/src/discovery/enr_ext.rs new file mode 100644 index 0000000..81eaaaf --- /dev/null +++ b/node/network/src/discovery/enr_ext.rs @@ -0,0 +1,314 @@ +//! ENR extension trait to support libp2p integration. +use crate::{Enr, Multiaddr, PeerId}; +use discv5::enr::{CombinedKey, CombinedPublicKey}; +use libp2p::core::{identity::Keypair, identity::PublicKey, multiaddr::Protocol}; +use tiny_keccak::{Hasher, Keccak}; + +/// Extend ENR for libp2p types. +pub trait EnrExt { + /// The libp2p `PeerId` for the record. + fn peer_id(&self) -> PeerId; + + /// Returns a list of multiaddrs if the ENR has an `ip` and either a `tcp` or `udp` key **or** an `ip6` and either a `tcp6` or `udp6`. + /// The vector remains empty if these fields are not defined. + fn multiaddr(&self) -> Vec; + + /// Returns a list of multiaddrs with the `PeerId` prepended. + fn multiaddr_p2p(&self) -> Vec; + + /// Returns any multiaddrs that contain the TCP protocol with the `PeerId` prepended. + fn multiaddr_p2p_tcp(&self) -> Vec; + + /// Returns any multiaddrs that contain the UDP protocol with the `PeerId` prepended. + fn multiaddr_p2p_udp(&self) -> Vec; + + /// Returns any multiaddrs that contain the TCP protocol. + fn multiaddr_tcp(&self) -> Vec; +} + +/// Extend ENR CombinedPublicKey for libp2p types. +pub trait CombinedKeyPublicExt { + /// Converts the publickey into a peer id, without consuming the key. + fn as_peer_id(&self) -> PeerId; +} + +/// Extend ENR CombinedKey for conversion to libp2p keys. +pub trait CombinedKeyExt { + /// Converts a libp2p key into an ENR combined key. + fn from_libp2p(key: &libp2p::core::identity::Keypair) -> Result; +} + +impl EnrExt for Enr { + /// The libp2p `PeerId` for the record. + fn peer_id(&self) -> PeerId { + self.public_key().as_peer_id() + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and either a `tcp` or `udp` key **or** an `ip6` and either a `tcp6` or `udp6`. + /// The vector remains empty if these fields are not defined. + fn multiaddr(&self) -> Vec { + let mut multiaddrs: Vec = Vec::new(); + if let Some(ip) = self.ip() { + if let Some(udp) = self.udp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Udp(udp)); + multiaddrs.push(multiaddr); + } + + if let Some(tcp) = self.tcp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Tcp(tcp)); + multiaddrs.push(multiaddr); + } + } + if let Some(ip6) = self.ip6() { + if let Some(udp6) = self.udp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Udp(udp6)); + multiaddrs.push(multiaddr); + } + + if let Some(tcp6) = self.tcp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Tcp(tcp6)); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and either a `tcp` or `udp` key **or** an `ip6` and either a `tcp6` or `udp6`. + /// The vector remains empty if these fields are not defined. + /// + /// This also prepends the `PeerId` into each multiaddr with the `P2p` protocol. + fn multiaddr_p2p(&self) -> Vec { + let peer_id = self.peer_id(); + let mut multiaddrs: Vec = Vec::new(); + if let Some(ip) = self.ip() { + if let Some(udp) = self.udp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Udp(udp)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + + if let Some(tcp) = self.tcp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Tcp(tcp)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + if let Some(ip6) = self.ip6() { + if let Some(udp6) = self.udp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Udp(udp6)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + + if let Some(tcp6) = self.tcp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Tcp(tcp6)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and a `tcp` key **or** an `ip6` and a `tcp6`. + /// The vector remains empty if these fields are not defined. + /// + /// This also prepends the `PeerId` into each multiaddr with the `P2p` protocol. + fn multiaddr_p2p_tcp(&self) -> Vec { + let peer_id = self.peer_id(); + let mut multiaddrs: Vec = Vec::new(); + if let Some(ip) = self.ip() { + if let Some(tcp) = self.tcp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Tcp(tcp)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + if let Some(ip6) = self.ip6() { + if let Some(tcp6) = self.tcp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Tcp(tcp6)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and a `udp` key **or** an `ip6` and a `udp6`. + /// The vector remains empty if these fields are not defined. + /// + /// This also prepends the `PeerId` into each multiaddr with the `P2p` protocol. + fn multiaddr_p2p_udp(&self) -> Vec { + let peer_id = self.peer_id(); + let mut multiaddrs: Vec = Vec::new(); + if let Some(ip) = self.ip() { + if let Some(udp) = self.udp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Udp(udp)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + if let Some(ip6) = self.ip6() { + if let Some(udp6) = self.udp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Udp(udp6)); + multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and either a `tcp` or `udp` key **or** an `ip6` and either a `tcp6` or `udp6`. + /// The vector remains empty if these fields are not defined. + fn multiaddr_tcp(&self) -> Vec { + let mut multiaddrs: Vec = Vec::new(); + if let Some(ip) = self.ip() { + if let Some(tcp) = self.tcp() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Tcp(tcp)); + multiaddrs.push(multiaddr); + } + } + if let Some(ip6) = self.ip6() { + if let Some(tcp6) = self.tcp6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Tcp(tcp6)); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } +} + +impl CombinedKeyPublicExt for CombinedPublicKey { + /// Converts the publickey into a peer id, without consuming the key. + /// + /// This is only available with the `libp2p` feature flag. + fn as_peer_id(&self) -> PeerId { + match self { + Self::Secp256k1(pk) => { + let pk_bytes = pk.to_bytes(); + let libp2p_pk = libp2p::core::PublicKey::Secp256k1( + libp2p::core::identity::secp256k1::PublicKey::decode(&pk_bytes) + .expect("valid public key"), + ); + PeerId::from_public_key(&libp2p_pk) + } + Self::Ed25519(pk) => { + let pk_bytes = pk.to_bytes(); + let libp2p_pk = libp2p::core::PublicKey::Ed25519( + libp2p::core::identity::ed25519::PublicKey::decode(&pk_bytes) + .expect("valid public key"), + ); + PeerId::from_public_key(&libp2p_pk) + } + } + } +} + +impl CombinedKeyExt for CombinedKey { + fn from_libp2p(key: &libp2p::core::identity::Keypair) -> Result { + match key { + Keypair::Secp256k1(key) => { + let secret = + discv5::enr::k256::ecdsa::SigningKey::from_bytes(&key.secret().to_bytes()) + .expect("libp2p key must be valid"); + Ok(CombinedKey::Secp256k1(secret)) + } + Keypair::Ed25519(key) => { + let ed_keypair = + discv5::enr::ed25519_dalek::SecretKey::from_bytes(&key.encode()[..32]) + .expect("libp2p key must be valid"); + Ok(CombinedKey::from(ed_keypair)) + } + _ => Err("ENR: Unsupported libp2p key type"), + } + } +} + +// helper function to convert a peer_id to a node_id. This is only possible for secp256k1/ed25519 libp2p +// peer_ids +pub fn peer_id_to_node_id(peer_id: &PeerId) -> Result { + // A libp2p peer id byte representation should be 2 length bytes + 4 protobuf bytes + compressed pk bytes + // if generated from a PublicKey with Identity multihash. + let pk_bytes = &peer_id.to_bytes()[2..]; + + match PublicKey::from_protobuf_encoding(pk_bytes).map_err(|e| { + format!( + " Cannot parse libp2p public key public key from peer id: {}", + e + ) + })? { + PublicKey::Secp256k1(pk) => { + let uncompressed_key_bytes = &pk.encode_uncompressed()[1..]; + let mut output = [0_u8; 32]; + let mut hasher = Keccak::v256(); + hasher.update(uncompressed_key_bytes); + hasher.finalize(&mut output); + Ok(discv5::enr::NodeId::parse(&output).expect("Must be correct length")) + } + PublicKey::Ed25519(pk) => { + let uncompressed_key_bytes = pk.encode(); + let mut output = [0_u8; 32]; + let mut hasher = Keccak::v256(); + hasher.update(&uncompressed_key_bytes); + hasher.finalize(&mut output); + Ok(discv5::enr::NodeId::parse(&output).expect("Must be correct length")) + } + _ => Err("Unsupported public key".into()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_secp256k1_peer_id_conversion() { + let sk_hex = "df94a73d528434ce2309abb19c16aedb535322797dbd59c157b1e04095900f48"; + let sk_bytes = hex::decode(sk_hex).unwrap(); + let secret_key = discv5::enr::k256::ecdsa::SigningKey::from_bytes(&sk_bytes).unwrap(); + + let libp2p_sk = libp2p::identity::secp256k1::SecretKey::from_bytes(sk_bytes).unwrap(); + let secp256k1_kp: libp2p::identity::secp256k1::Keypair = libp2p_sk.into(); + let libp2p_kp = Keypair::Secp256k1(secp256k1_kp); + let peer_id = libp2p_kp.public().to_peer_id(); + + let enr = discv5::enr::EnrBuilder::new("v4") + .build(&secret_key) + .unwrap(); + let node_id = peer_id_to_node_id(&peer_id).unwrap(); + + assert_eq!(enr.node_id(), node_id); + } + + #[test] + fn test_ed25519_peer_conversion() { + let sk_hex = "4dea8a5072119927e9d243a7d953f2f4bc95b70f110978e2f9bc7a9000e4b261"; + let sk_bytes = hex::decode(sk_hex).unwrap(); + let secret = discv5::enr::ed25519_dalek::SecretKey::from_bytes(&sk_bytes).unwrap(); + let public = discv5::enr::ed25519_dalek::PublicKey::from(&secret); + let keypair = discv5::enr::ed25519_dalek::Keypair { secret, public }; + + let libp2p_sk = libp2p::identity::ed25519::SecretKey::from_bytes(sk_bytes).unwrap(); + let ed25519_kp: libp2p::identity::ed25519::Keypair = libp2p_sk.into(); + let libp2p_kp = Keypair::Ed25519(ed25519_kp); + let peer_id = libp2p_kp.public().to_peer_id(); + + let enr = discv5::enr::EnrBuilder::new("v4").build(&keypair).unwrap(); + let node_id = peer_id_to_node_id(&peer_id).unwrap(); + + assert_eq!(enr.node_id(), node_id); + } +} diff --git a/node/network/src/discovery/mod.rs b/node/network/src/discovery/mod.rs new file mode 100644 index 0000000..307377c --- /dev/null +++ b/node/network/src/discovery/mod.rs @@ -0,0 +1,632 @@ +//! The discovery sub-behaviour of Lighthouse. +//! +//! This module creates a libp2p dummy-behaviour built around the discv5 protocol. It handles +//! queries and manages access to the discovery routing table. + +pub(crate) mod enr; +pub mod enr_ext; + +use crate::metrics; +use crate::{error, Enr, NetworkConfig, NetworkGlobals}; +use discv5::{enr::NodeId, Discv5, Discv5Event}; +pub use enr::{ + build_enr, create_enr_builder_from_config, load_enr_from_disk, use_or_load_enr, CombinedKey, +}; +pub use enr_ext::{peer_id_to_node_id, CombinedKeyExt, EnrExt}; +pub use libp2p::core::identity::{Keypair, PublicKey}; + +use futures::prelude::*; +use futures::stream::FuturesUnordered; +pub use libp2p::{ + core::{connection::ConnectionId, ConnectedPoint, Multiaddr, PeerId}, + swarm::{ + handler::ConnectionHandler, DialError, NetworkBehaviour, + NetworkBehaviourAction as NBAction, NotifyHandler, PollParameters, SubstreamProtocol, + }, +}; +use lru::LruCache; +use std::{ + collections::HashMap, + net::{IpAddr, SocketAddr}, + path::Path, + pin::Pin, + sync::Arc, + task::{Context, Poll}, + time::Instant, +}; +use tokio::sync::mpsc; + +/// Local ENR storage filename. +pub const ENR_FILENAME: &str = "enr.dat"; +/// The number of closest peers to search for when doing a regular peer search. +/// +/// We could reduce this constant to speed up queries however at the cost of security. It will +/// make it easier to peers to eclipse this node. Kademlia suggests a value of 16. +pub const FIND_NODE_QUERY_CLOSEST_PEERS: usize = 16; +/// The threshold for updating `min_ttl` on a connected peer. + +/// The events emitted by polling discovery. +pub enum DiscoveryEvent { + /// A query has completed. This result contains a mapping of discovered peer IDs to the `min_ttl` + /// of the peer if it is specified. + QueryResult(HashMap>), + /// This indicates that our local UDP socketaddr has been updated and we should inform libp2p. + SocketUpdated(SocketAddr), +} + +#[derive(Debug, Clone, PartialEq)] +enum QueryType { + /// We are searching for more peers without ENR or time constraints. + FindPeers, +} + +/// The result of a query. +struct QueryResult { + query_type: QueryType, + result: Result, discv5::QueryError>, +} + +// Awaiting the event stream future +enum EventStream { + /// Awaiting an event stream to be generated. This is required due to the poll nature of + /// `Discovery` + Awaiting( + Pin< + Box< + dyn Future, discv5::Discv5Error>> + + Send, + >, + >, + ), + /// The future has completed. + Present(mpsc::Receiver), + // The future has failed or discv5 has been disabled. There are no events from discv5. + InActive, +} + +/// The main discovery service. This can be disabled via CLI arguements. When disabled the +/// underlying processes are not started, but this struct still maintains our current ENR. +pub struct Discovery { + /// A collection of seen live ENRs for quick lookup and to map peer-id's to ENRs. + cached_enrs: LruCache, + + /// The directory where the ENR is stored. + enr_dir: String, + + /// The handle for the underlying discv5 Server. + /// + /// This is behind a Reference counter to allow for futures to be spawned and polled with a + /// static lifetime. + discv5: Discv5, + + /// A collection of network constants that can be read from other threads. + network_globals: Arc, + + /// Indicates if we are actively searching for peers. We only allow a single FindPeers query at + /// a time, regardless of the query concurrency. + find_peer_active: bool, + + /// Active discovery queries. + active_queries: FuturesUnordered + Send>>>, + + /// The discv5 event stream. + event_stream: EventStream, + + /// Indicates if the discovery service has been started. When the service is disabled, this is + /// always false. + pub started: bool, +} + +impl Discovery { + /// NOTE: Creating discovery requires running within a tokio execution environment. + pub async fn new( + local_key: &Keypair, + config: &NetworkConfig, + network_globals: Arc, + ) -> error::Result { + let enr_dir = match config.network_dir.to_str() { + Some(path) => String::from(path), + None => String::from(""), + }; + + let local_enr = network_globals.local_enr.read().clone(); + + info!( + enr = %local_enr.to_base64(), + seq = %local_enr.seq(), + id = %local_enr.node_id(), + ip = ?local_enr.ip(), + udp = ?local_enr.udp(), + tcp = ?local_enr.tcp(), + udp4_socket = ?local_enr.udp_socket(), + "ENR Initialised", + ); + + let listen_socket = SocketAddr::new(config.listen_address, config.discovery_port); + + // convert the keypair into an ENR key + let enr_key: CombinedKey = CombinedKey::from_libp2p(local_key)?; + + let mut discv5 = Discv5::new(local_enr, enr_key, config.discv5_config.clone()) + .map_err(|e| format!("Discv5 service failed. Error: {:?}", e))?; + + // Add bootnodes to routing table + for bootnode_enr in config.boot_nodes_enr.clone() { + debug!( + node_id = %bootnode_enr.node_id(), + peer_id = %bootnode_enr.peer_id(), + ip = ?bootnode_enr.ip(), + udp = ?bootnode_enr.udp(), + tcp = ?bootnode_enr.tcp(), + "Adding node to routing table", + ); + let repr = bootnode_enr.to_string(); + let _ = discv5.add_enr(bootnode_enr).map_err(|e| { + error!( + addr = %repr, + error = %e.to_string(), + "Could not add peer to the local routing table", + ) + }); + } + + // Start the discv5 service and obtain an event stream + let event_stream = if !config.disable_discovery { + discv5 + .start(listen_socket) + .map_err(|e| e.to_string()) + .await?; + debug!("Discovery service started"); + EventStream::Awaiting(Box::pin(discv5.event_stream())) + } else { + EventStream::InActive + }; + + if !config.boot_nodes_multiaddr.is_empty() { + info!("Contacting Multiaddr boot-nodes for their ENR"); + } + + // get futures for requesting the Enrs associated to these multiaddr and wait for their + // completion + let mut fut_coll = config + .boot_nodes_multiaddr + .iter() + .map(|addr| addr.to_string()) + // request the ENR for this multiaddr and keep the original for logging + .map(|addr| { + futures::future::join( + discv5.request_enr(addr.clone()), + futures::future::ready(addr), + ) + }) + .collect::>(); + + while let Some((result, original_addr)) = fut_coll.next().await { + match result { + Ok(enr) => { + debug!( + node_id = %enr.node_id(), + peer_id = %enr.peer_id(), + ip = ?enr.ip(), + udp = ?enr.udp(), + tcp = ?enr.tcp(), + "Adding node to routing table", + ); + let _ = discv5.add_enr(enr).map_err(|e| { + error!( + addr = %original_addr.to_string(), + error = %e.to_string(), + "Could not add peer to the local routing table", + ) + }); + } + Err(e) => { + error!( + multiaddr = %original_addr.to_string(), + error = %e.to_string(), + "Error getting mapping to ENR", + ) + } + } + } + + Ok(Self { + cached_enrs: LruCache::new(50), + network_globals, + find_peer_active: false, + active_queries: FuturesUnordered::new(), + discv5, + event_stream, + started: !config.disable_discovery, + enr_dir, + }) + } + + /// Return the nodes local ENR. + pub fn local_enr(&self) -> Enr { + self.discv5.local_enr() + } + + /// Return the cached enrs. + pub fn cached_enrs(&self) -> impl Iterator { + self.cached_enrs.iter() + } + + /// Removes a cached ENR from the list. + pub fn remove_cached_enr(&mut self, peer_id: &PeerId) -> Option { + self.cached_enrs.pop(peer_id) + } + + /// This adds a new `FindPeers` query to the queue if one doesn't already exist. + /// The `target_peers` parameter informs discovery to end the query once the target is found. + /// The maximum this can be is 16. + pub fn discover_peers(&mut self, target_peers: usize) { + // If the discv5 service isn't running or we are in the process of a query, don't bother queuing a new one. + if !self.started || self.find_peer_active { + return; + } + // Immediately start a FindNode query + let target_peers = std::cmp::min(FIND_NODE_QUERY_CLOSEST_PEERS, target_peers); + debug!(%target_peers, "Starting a peer discovery request"); + self.find_peer_active = true; + self.start_query(QueryType::FindPeers, target_peers); + } + + /// Add an ENR to the routing table of the discovery mechanism. + pub fn add_enr(&mut self, enr: Enr) { + // add the enr to seen caches + self.cached_enrs.put(enr.peer_id(), enr.clone()); + + if let Err(e) = self.discv5.add_enr(enr) { + debug!( + error = %e, + "Could not add peer to the local routing table", + ) + } + } + + /// Returns an iterator over all enr entries in the DHT. + pub fn table_entries_enr(&mut self) -> Vec { + self.discv5.table_entries_enr() + } + + /// Returns the ENR of a known peer if it exists. + pub fn enr_of_peer(&mut self, peer_id: &PeerId) -> Option { + // first search the local cache + if let Some(enr) = self.cached_enrs.get(peer_id) { + return Some(enr.clone()); + } + // not in the local cache, look in the routing table + if let Ok(node_id) = enr_ext::peer_id_to_node_id(peer_id) { + self.discv5.find_enr(&node_id) + } else { + None + } + } + + /// Updates the local ENR TCP port. + /// There currently isn't a case to update the address here. We opt for discovery to + /// automatically update the external address. + /// + /// If the external address needs to be modified, use `update_enr_udp_socket. + pub fn update_enr_tcp_port(&mut self, port: u16) -> Result<(), String> { + self.discv5 + .enr_insert("tcp", &port.to_be_bytes()) + .map_err(|e| format!("{:?}", e))?; + + // replace the global version + *self.network_globals.local_enr.write() = self.discv5.local_enr(); + // persist modified enr to disk + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); + Ok(()) + } + + /// Updates the local ENR UDP socket. + /// + /// This is with caution. Discovery should automatically maintain this. This should only be + /// used when automatic discovery is disabled. + pub fn update_enr_udp_socket(&mut self, socket_addr: SocketAddr) -> Result<(), String> { + match socket_addr { + SocketAddr::V4(socket) => { + self.discv5 + .enr_insert("ip", &socket.ip().octets()) + .map_err(|e| format!("{:?}", e))?; + self.discv5 + .enr_insert("udp", &socket.port().to_be_bytes()) + .map_err(|e| format!("{:?}", e))?; + } + SocketAddr::V6(socket) => { + self.discv5 + .enr_insert("ip6", &socket.ip().octets()) + .map_err(|e| format!("{:?}", e))?; + self.discv5 + .enr_insert("udp6", &socket.port().to_be_bytes()) + .map_err(|e| format!("{:?}", e))?; + } + } + + // replace the global version + *self.network_globals.local_enr.write() = self.discv5.local_enr(); + // persist modified enr to disk + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); + Ok(()) + } + + // Bans a peer and it's associated seen IP addresses. + pub fn ban_peer(&mut self, peer_id: &PeerId, ip_addresses: Vec) { + // first try and convert the peer_id to a node_id. + if let Ok(node_id) = peer_id_to_node_id(peer_id) { + // If we could convert this peer id, remove it from the DHT and ban it from discovery. + self.discv5.ban_node(&node_id, None); + // Remove the node from the routing table. + self.discv5.remove_node(&node_id); + } + + for ip_address in ip_addresses { + self.discv5.ban_ip(ip_address, None); + } + } + + /// Unbans the peer in discovery. + pub fn unban_peer(&mut self, peer_id: &PeerId, ip_addresses: Vec) { + // first try and convert the peer_id to a node_id. + if let Ok(node_id) = peer_id_to_node_id(peer_id) { + self.discv5.ban_node_remove(&node_id); + } + + for ip_address in ip_addresses { + self.discv5.ban_ip_remove(&ip_address); + } + } + + /// Marks node as disconnected in the DHT, freeing up space for other nodes, this also removes + /// nodes from the cached ENR list. + pub fn disconnect_peer(&mut self, peer_id: &PeerId) { + if let Ok(node_id) = peer_id_to_node_id(peer_id) { + self.discv5.disconnect_node(&node_id); + } + // Remove the peer from the cached list, to prevent redialing disconnected + // peers. + self.cached_enrs.pop(peer_id); + } + + /* Internal Functions */ + + /// Search for a specified number of new peers using the underlying discovery mechanism. + /// + /// This can optionally search for peers for a given predicate. Regardless of the predicate + /// given, this will only search for peers on the same enr_fork_id as specified in the local + /// ENR. + fn start_query(&mut self, query: QueryType, target_peers: usize) { + // Generate a random target node id. + let random_node = NodeId::random(); + + // Build the future + let query_future = self + .discv5 + .find_node_predicate(random_node, Box::new(|_| true), target_peers) + .map(|v| QueryResult { + query_type: query, + result: v, + }); + + // Add the future to active queries, to be executed. + self.active_queries.push(Box::pin(query_future)); + } + + /// Process the completed QueryResult returned from discv5. + fn process_completed_queries( + &mut self, + query: QueryResult, + ) -> Option>> { + match query.query_type { + QueryType::FindPeers => { + self.find_peer_active = false; + match query.result { + Ok(r) if r.is_empty() => { + debug!("Discovery query yielded no results."); + } + Ok(r) => { + debug!(peers_found = r.len(), "Discovery query completed"); + let mut results: HashMap<_, Option> = HashMap::new(); + r.iter().for_each(|enr| { + // cache the found ENR's + self.cached_enrs.put(enr.peer_id(), enr.clone()); + results.insert(enr.peer_id(), None); + }); + return Some(results); + } + Err(e) => { + warn!(error = %e, "Discovery query failed"); + } + } + } + } + + None + } + + /// Drives the queries returning any results from completed queries. + fn poll_queries(&mut self, cx: &mut Context) -> Option>> { + while let Poll::Ready(Some(query_result)) = self.active_queries.poll_next_unpin(cx) { + let result = self.process_completed_queries(query_result); + if result.is_some() { + return result; + } + } + None + } +} + +/* NetworkBehaviour Implementation */ + +impl NetworkBehaviour for Discovery { + // Discovery is not a real NetworkBehaviour... + type ConnectionHandler = libp2p::swarm::handler::DummyConnectionHandler; + type OutEvent = DiscoveryEvent; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + libp2p::swarm::handler::DummyConnectionHandler::default() + } + + // Handles the libp2p request to obtain multiaddrs for peer_id's in order to dial them. + fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec { + if let Some(enr) = self.enr_of_peer(peer_id) { + // ENR's may have multiple Multiaddrs. The multi-addr associated with the UDP + // port is removed, which is assumed to be associated with the discv5 protocol (and + // therefore irrelevant for other libp2p components). + enr.multiaddr_tcp() + } else { + // PeerId is not known + Vec::new() + } + } + + fn inject_event( + &mut self, + _: PeerId, + _: ConnectionId, + _: ::OutEvent, + ) { + } + + fn inject_dial_failure( + &mut self, + peer_id: Option, + _handler: Self::ConnectionHandler, + error: &DialError, + ) { + if let Some(peer_id) = peer_id { + match error { + DialError::Banned + | DialError::LocalPeerId + | DialError::InvalidPeerId(_) + | DialError::ConnectionIo(_) + | DialError::NoAddresses + | DialError::Transport(_) + | DialError::WrongPeerId { .. } => { + // set peer as disconnected in discovery DHT + debug!(peer_id = %peer_id, "Marking peer disconnected in DHT"); + self.disconnect_peer(&peer_id); + } + DialError::ConnectionLimit(_) + | DialError::DialPeerConditionFalse(_) + | DialError::Aborted => {} + } + } + } + + // Main execution loop to drive the behaviour + fn poll( + &mut self, + cx: &mut Context, + _: &mut impl PollParameters, + ) -> Poll> { + if !self.started { + return Poll::Pending; + } + + // Drive the queries and return any results from completed queries + if let Some(results) = self.poll_queries(cx) { + // return the result to the peer manager + return Poll::Ready(NBAction::GenerateEvent(DiscoveryEvent::QueryResult( + results, + ))); + } + + // Process the server event stream + match self.event_stream { + EventStream::Awaiting(ref mut fut) => { + // Still awaiting the event stream, poll it + if let Poll::Ready(event_stream) = fut.poll_unpin(cx) { + match event_stream { + Ok(stream) => { + debug!("Discv5 event stream ready"); + self.event_stream = EventStream::Present(stream); + } + Err(e) => { + error!(error = %e, "Discv5 event stream failed"); + self.event_stream = EventStream::InActive; + } + } + } + } + EventStream::InActive => {} // ignore checking the stream + EventStream::Present(ref mut stream) => { + while let Poll::Ready(Some(event)) = stream.poll_recv(cx) { + match event { + // We filter out unwanted discv5 events here and only propagate useful results to + // the peer manager. + Discv5Event::Discovered(_enr) => { + // Peers that get discovered during a query but are not contactable or + // don't match a predicate can end up here. For debugging purposes we + // log these to see if we are unnecessarily dropping discovered peers + /* + if enr.eth2() == self.local_enr().eth2() { + trace!(self.log, "Peer found in process of query"; "peer_id" => format!("{}", enr.peer_id()), "tcp_socket" => enr.tcp_socket()); + } else { + // this is temporary warning for debugging the DHT + warn!(self.log, "Found peer during discovery not on correct fork"; "peer_id" => format!("{}", enr.peer_id()), "tcp_socket" => enr.tcp_socket()); + } + */ + } + Discv5Event::SocketUpdated(socket) => { + info!(ip = %socket.ip(), udp_port = %socket.port(), "Address updated"); + metrics::inc_counter(&metrics::ADDRESS_UPDATE_COUNT); + metrics::check_nat(); + // Discv5 will have updated our local ENR. We save the updated version + // to disk. + let enr = self.discv5.local_enr(); + enr::save_enr_to_disk(Path::new(&self.enr_dir), &enr); + // update network globals + *self.network_globals.local_enr.write() = enr; + return Poll::Ready(NBAction::GenerateEvent( + DiscoveryEvent::SocketUpdated(socket), + )); + } + Discv5Event::EnrAdded { .. } + | Discv5Event::TalkRequest(_) + | Discv5Event::NodeInserted { .. } => {} // Ignore all other discv5 server events + } + } + } + } + Poll::Pending + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use enr::EnrBuilder; +// use unused_port::unused_udp_port; + +// async fn build_discovery() -> Discovery { +// let keypair = libp2p::identity::Keypair::generate_secp256k1(); +// let config = NetworkConfig { +// discovery_port: unused_udp_port().unwrap(), +// ..Default::default() +// }; + +// let enr_key: CombinedKey = CombinedKey::from_libp2p(&keypair).unwrap(); +// let enr: Enr = build_enr(&enr_key, &config).unwrap(); + +// let globals = NetworkGlobals::new( +// enr, +// 9000, +// 9000, +// vec![], +// ); + +// Discovery::new(&keypair, &config, Arc::new(globals)) +// .await +// .unwrap() +// } + +// fn make_enr(subnet_ids: Vec) -> Enr { +// let mut builder = EnrBuilder::new("v4"); +// let keypair = libp2p::identity::Keypair::generate_secp256k1(); +// let enr_key: CombinedKey = CombinedKey::from_libp2p(&keypair).unwrap(); +// builder.build(&enr_key).unwrap() +// } +// } diff --git a/node/network/src/lib.rs b/node/network/src/lib.rs new file mode 100644 index 0000000..6eb318b --- /dev/null +++ b/node/network/src/lib.rs @@ -0,0 +1,155 @@ +/// This crate contains the main link for lighthouse to rust-libp2p. It therefore re-exports +/// all required libp2p functionality. +/// +/// This crate builds and manages the libp2p services required by the beacon node. +#[macro_use] +extern crate lazy_static; + +#[macro_use] +extern crate tracing; + +pub mod behaviour; +mod config; + +#[allow(clippy::mutable_key_type)] +// PeerId in hashmaps are no longer permitted by clippy +pub mod discovery; +pub mod metrics; +pub mod nat; +pub mod peer_manager; +pub mod rpc; +mod service; +pub mod types; + +pub use config::gossip_max_size; +use std::net::SocketAddr; + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use shared_types::TxID; +use std::str::FromStr; + +/// Wrapper over a libp2p `PeerId` which implements `Serialize` and `Deserialize` +#[derive(Clone, Debug)] +pub struct PeerIdSerialized(libp2p::PeerId); + +impl From for PeerId { + fn from(peer_id: PeerIdSerialized) -> Self { + peer_id.0 + } +} + +impl FromStr for PeerIdSerialized { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(Self( + PeerId::from_str(s).map_err(|e| format!("Invalid peer id: {}", e))?, + )) + } +} + +impl Serialize for PeerIdSerialized { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.0.to_string()) + } +} + +impl<'de> Deserialize<'de> for PeerIdSerialized { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: String = Deserialize::deserialize(deserializer)?; + Ok(Self(PeerId::from_str(&s).map_err(|e| { + de::Error::custom(format!("Failed to deserialise peer id: {:?}", e)) + })?)) + } +} + +pub use crate::types::{error, Enr, GossipTopic, NetworkGlobals, PubsubMessage}; + +pub use prometheus_client; + +pub use behaviour::{BehaviourEvent, Gossipsub, PeerRequestId, Request, Response}; +pub use config::Config as NetworkConfig; +pub use discovery::{CombinedKeyExt, EnrExt}; +pub use discv5; +pub use libp2p; +pub use libp2p::bandwidth::BandwidthSinks; +pub use libp2p::core::identity::{error::SigningError, Keypair, PublicKey}; +pub use libp2p::gossipsub::{IdentTopic, MessageAcceptance, MessageId, Topic, TopicHash}; +pub use libp2p::{core::ConnectedPoint, PeerId, Swarm}; +pub use libp2p::{multiaddr, Multiaddr}; +pub use metrics::scrape_discovery_metrics; +pub use peer_manager::{ + peerdb::client::Client, + peerdb::score::{PeerAction, ReportSource}, + peerdb::PeerDB, + ConnectionDirection, PeerConnectionStatus, PeerInfo, PeerManager, SyncInfo, SyncStatus, +}; +pub use service::{load_private_key, Context, Libp2pEvent, Service, NETWORK_KEY_FILENAME}; + +/// Application level requests sent to the network. +#[derive(Debug, Clone, Copy)] +pub enum RequestId { + Router, + Sync(SyncId), +} + +#[derive(Debug, Clone, Copy)] +pub enum SyncId { + SerialSync { tx_id: TxID }, +} + +/// Types of messages that the network service can receive. +#[derive(Debug)] +pub enum NetworkMessage { + /// Send an RPC request to the libp2p service. + SendRequest { + peer_id: PeerId, + request: Request, + request_id: RequestId, + }, + /// Send a successful Response to the libp2p service. + SendResponse { + peer_id: PeerId, + response: Response, + id: PeerRequestId, + }, + /// Send an error response to an RPC request. + SendErrorResponse { + peer_id: PeerId, + error: rpc::RPCResponseErrorCode, + reason: String, + id: PeerRequestId, + }, + /// Publish a list of messages to the gossipsub protocol. + Publish { messages: Vec }, + /// Reports a peer to the peer manager for performing an action. + ReportPeer { + peer_id: PeerId, + action: PeerAction, + source: ReportSource, + msg: &'static str, + }, + /// Disconnect an ban a peer, providing a reason. + GoodbyePeer { + peer_id: PeerId, + reason: rpc::GoodbyeReason, + source: ReportSource, + }, + /// Start dialing a new peer. + DialPeer { address: Multiaddr, peer_id: PeerId }, + /// Notify that new file stored in db. + AnnounceLocalFile { tx_id: TxID }, + /// Called if a known external TCP socket address has been updated. + UPnPMappingEstablished { + /// The external TCP address has been updated. + tcp_socket: Option, + /// The external UDP address has been updated. + udp_socket: Option, + }, +} diff --git a/node/network/src/metrics.rs b/node/network/src/metrics.rs new file mode 100644 index 0000000..66d7a1f --- /dev/null +++ b/node/network/src/metrics.rs @@ -0,0 +1,162 @@ +pub use lighthouse_metrics::*; + +lazy_static! { + pub static ref NAT_OPEN: Result = try_create_int_counter( + "nat_open", + "An estimate indicating if the local node is exposed to the internet." + ); + pub static ref ADDRESS_UPDATE_COUNT: Result = try_create_int_counter( + "libp2p_address_update_total", + "Count of libp2p socked updated events (when our view of our IP address has changed)" + ); + pub static ref PEERS_CONNECTED: Result = try_create_int_gauge( + "libp2p_peers", + "Count of libp2p peers currently connected" + ); + + pub static ref PEER_CONNECT_EVENT_COUNT: Result = try_create_int_counter( + "libp2p_peer_connect_event_total", + "Count of libp2p peer connect events (not the current number of connected peers)" + ); + pub static ref PEER_DISCONNECT_EVENT_COUNT: Result = try_create_int_counter( + "libp2p_peer_disconnect_event_total", + "Count of libp2p peer disconnect events" + ); + pub static ref DISCOVERY_SENT_BYTES: Result = try_create_int_gauge( + "discovery_sent_bytes", + "The number of bytes sent in discovery" + ); + pub static ref DISCOVERY_RECV_BYTES: Result = try_create_int_gauge( + "discovery_recv_bytes", + "The number of bytes received in discovery" + ); + pub static ref DISCOVERY_QUEUE: Result = try_create_int_gauge( + "discovery_queue_size", + "The number of discovery queries awaiting execution" + ); + pub static ref DISCOVERY_REQS: Result = try_create_float_gauge( + "discovery_requests", + "The number of unsolicited discovery requests per second" + ); + pub static ref DISCOVERY_SESSIONS: Result = try_create_int_gauge( + "discovery_sessions", + "The number of active discovery sessions with peers" + ); + + pub static ref PEERS_PER_CLIENT: Result = try_create_int_gauge_vec( + "libp2p_peers_per_client", + "The connected peers via client implementation", + &["Client"] + ); + pub static ref FAILED_ATTESTATION_PUBLISHES_PER_SUBNET: Result = + try_create_int_gauge_vec( + "gossipsub_failed_attestation_publishes_per_subnet", + "Failed attestation publishes per subnet", + &["subnet"] + ); + pub static ref FAILED_PUBLISHES_PER_MAIN_TOPIC: Result = try_create_int_gauge_vec( + "gossipsub_failed_publishes_per_main_topic", + "Failed gossip publishes", + &["topic_hash"] + ); + pub static ref TOTAL_RPC_ERRORS_PER_CLIENT: Result = try_create_int_counter_vec( + "libp2p_rpc_errors_per_client", + "RPC errors per client", + &["client", "rpc_error", "direction"] + ); + pub static ref TOTAL_RPC_REQUESTS: Result = try_create_int_counter_vec( + "libp2p_rpc_requests_total", + "RPC requests total", + &["type"] + ); + pub static ref PEER_ACTION_EVENTS_PER_CLIENT: Result = + try_create_int_counter_vec( + "libp2p_peer_actions_per_client", + "Score reports per client", + &["client", "action", "source"] + ); + pub static ref GOSSIP_UNACCEPTED_MESSAGES_PER_CLIENT: Result = + try_create_int_counter_vec( + "gossipsub_unaccepted_messages_per_client", + "Gossipsub messages that we did not accept, per client", + &["client", "validation_result"] + ); + pub static ref GOSSIP_LATE_PUBLISH_PER_TOPIC_KIND: Result = + try_create_int_counter_vec( + "gossipsub_late_publish_per_topic_kind", + "Messages published late to gossipsub per topic kind.", + &["topic_kind"] + ); + pub static ref GOSSIP_EXPIRED_LATE_PUBLISH_PER_TOPIC_KIND: Result = + try_create_int_counter_vec( + "gossipsub_expired_late_publish_per_topic_kind", + "Messages that expired waiting to be published on retry to gossipsub per topic kind.", + &["topic_kind"] + ); + pub static ref GOSSIP_FAILED_LATE_PUBLISH_PER_TOPIC_KIND: Result = + try_create_int_counter_vec( + "gossipsub_failed_late_publish_per_topic_kind", + "Messages that failed to be published on retry to gossipsub per topic kind.", + &["topic_kind"] + ); + pub static ref PEER_SCORE_DISTRIBUTION: Result = + try_create_int_gauge_vec( + "peer_score_distribution", + "The distribution of connected peer scores", + &["position"] + ); + pub static ref PEER_SCORE_PER_CLIENT: Result = + try_create_float_gauge_vec( + "peer_score_per_client", + "Average score per client", + &["client"] + ); + + /* + * Inbound/Outbound peers + */ + /// The number of peers that dialed us. + pub static ref NETWORK_INBOUND_PEERS: Result = + try_create_int_gauge("network_inbound_peers","The number of peers that are currently connected that have dialed us."); + + /// The number of peers that we dialed us. + pub static ref NETWORK_OUTBOUND_PEERS: Result = + try_create_int_gauge("network_outbound_peers","The number of peers that are currently connected that we dialed."); + + /* + * Peer Reporting + */ + pub static ref REPORT_PEER_MSGS: Result = try_create_int_counter_vec( + "libp2p_report_peer_msgs_total", + "Number of peer reports per msg", + &["msg"] + ); +} + +/// Checks if we consider the NAT open. +/// +/// Conditions for an open NAT: +/// 1. We have 1 or more SOCKET_UPDATED messages. This occurs when discovery has a majority of +/// users reporting an external port and our ENR gets updated. +/// 2. We have 0 SOCKET_UPDATED messages (can be true if the port was correct on boot), then we +/// rely on whether we have any inbound messages. If we have no socket update messages, but +/// manage to get at least one inbound peer, we are exposed correctly. +pub fn check_nat() { + // NAT is already deemed open. + if NAT_OPEN.as_ref().map(|v| v.get()).unwrap_or(0) != 0 { + return; + } + if ADDRESS_UPDATE_COUNT.as_ref().map(|v| v.get()).unwrap_or(0) == 0 + || NETWORK_INBOUND_PEERS.as_ref().map(|v| v.get()).unwrap_or(0) != 0_i64 + { + inc_counter(&NAT_OPEN); + } +} + +pub fn scrape_discovery_metrics() { + let metrics = discv5::metrics::Metrics::from(discv5::Discv5::raw_metrics()); + set_float_gauge(&DISCOVERY_REQS, metrics.unsolicited_requests_per_second); + set_gauge(&DISCOVERY_SESSIONS, metrics.active_sessions as i64); + set_gauge(&DISCOVERY_SENT_BYTES, metrics.bytes_sent as i64); + set_gauge(&DISCOVERY_RECV_BYTES, metrics.bytes_recv as i64); +} diff --git a/node/network/src/nat.rs b/node/network/src/nat.rs new file mode 100644 index 0000000..5914cc1 --- /dev/null +++ b/node/network/src/nat.rs @@ -0,0 +1,231 @@ +//! This houses various NAT hole punching strategies. +//! +//! Currently supported strategies: +//! - UPnP + +use crate::{NetworkConfig, NetworkMessage}; +use if_addrs::get_if_addrs; +use std::net::{IpAddr, SocketAddr, SocketAddrV4}; +use tokio::sync::mpsc; + +/// Configuration required to construct the UPnP port mappings. +pub struct UPnPConfig { + /// The local tcp port. + tcp_port: u16, + /// The local udp port. + udp_port: u16, + /// Whether discovery is enabled or not. + disable_discovery: bool, +} + +impl UPnPConfig { + pub fn from_config(config: &NetworkConfig) -> Option { + if config.enr_tcp_port.is_some() + || config.enr_udp_port.is_some() + || config.enr_address.is_some() + { + None + } else { + Some(UPnPConfig { + tcp_port: config.libp2p_port, + udp_port: config.discovery_port, + disable_discovery: config.disable_discovery, + }) + } + } +} + +/// Attempts to construct external port mappings with UPnP. +pub fn construct_upnp_mappings( + config: UPnPConfig, + network_send: mpsc::UnboundedSender, +) { + info!("UPnP Attempting to initialise routes"); + match igd::search_gateway(Default::default()) { + Err(e) => info!(error = %e, "UPnP not available"), + Ok(gateway) => { + // Need to find the local listening address matched with the router subnet + let interfaces = match get_if_addrs() { + Ok(v) => v, + Err(e) => { + info!(error = %e, "UPnP failed to get local interfaces"); + return; + } + }; + let local_ip = interfaces.iter().find_map(|interface| { + // Just use the first IP of the first interface that is not a loopback and not an + // ipv6 address. + if !interface.is_loopback() { + interface.ip().is_ipv4().then(|| interface.ip()) + } else { + None + } + }); + + let local_ip = match local_ip { + None => { + info!("UPnP failed to find local IP address"); + return; + } + Some(v) => v, + }; + + debug!(ip = %local_ip, "UPnP Local IP Discovered"); + + match local_ip { + IpAddr::V4(address) => { + let libp2p_socket = SocketAddrV4::new(address, config.tcp_port); + let external_ip = gateway.get_external_ip(); + // We add specific port mappings rather than getting the router to arbitrary assign + // one. + // I've found this to be more reliable. If multiple users are behind a single + // router, they should ideally try to set different port numbers. + let tcp_socket = add_port_mapping( + &gateway, + igd::PortMappingProtocol::TCP, + libp2p_socket, + "tcp", + ) + .and_then(|_| { + let external_socket = external_ip + .as_ref() + .map(|ip| SocketAddr::new((*ip).into(), config.tcp_port)) + .map_err(|_| ()); + info!( + external_socket = format!( + "{}:{}", + external_socket + .as_ref() + .map(|ip| ip.to_string()) + .unwrap_or_else(|_| "".into()), + config.tcp_port + ) + ); + external_socket + }) + .ok(); + + let udp_socket = if !config.disable_discovery { + let discovery_socket = SocketAddrV4::new(address, config.udp_port); + add_port_mapping( + &gateway, + igd::PortMappingProtocol::UDP, + discovery_socket, + "udp", + ) + .and_then(|_| { + let external_socket = external_ip + .map(|ip| SocketAddr::new(ip.into(), config.udp_port)) + .map_err(|_| ()); + info!( + external_socket = format!( + "{}:{}", + external_socket + .as_ref() + .map(|ip| ip.to_string()) + .unwrap_or_else(|_| "".into()), + config.udp_port + ), + "UPnP UDP route established" + ); + external_socket + }) + .ok() + } else { + None + }; + + // report any updates to the network service. + network_send + .send(NetworkMessage::UPnPMappingEstablished { + tcp_socket, + udp_socket, + }) + .unwrap_or_else( + |e| debug!(error = %e, "Could not send message to the network service"), + ); + } + _ => debug!("UPnP no routes constructed. IPv6 not supported"), + } + } + }; +} + +/// Sets up a port mapping for a protocol returning the mapped port if successful. +fn add_port_mapping( + gateway: &igd::Gateway, + protocol: igd::PortMappingProtocol, + socket: SocketAddrV4, + protocol_string: &'static str, +) -> Result<(), ()> { + // We add specific port mappings rather than getting the router to arbitrary assign + // one. + // I've found this to be more reliable. If multiple users are behind a single + // router, they should ideally try to set different port numbers. + let mapping_string = &format!("lighthouse-{}", protocol_string); + for _ in 0..2 { + match gateway.add_port(protocol, socket.port(), socket, 0, mapping_string) { + Err(e) => { + match e { + igd::AddPortError::PortInUse => { + // Try and remove and re-create + debug!( + protocol = protocol_string, + port = socket.port(), + "UPnP port in use, attempting to remap" + ); + match gateway.remove_port(protocol, socket.port()) { + Ok(()) => { + debug!( + protocol = protocol_string, + port = socket.port(), + "UPnP Removed port mapping" + ) + } + Err(e) => { + debug!(protocol = protocol_string, port = socket.port(), error = %e, "UPnP Port remove failure"); + return Err(()); + } + } + } + e => { + info!(error = %e, "UPnP TCP route not set"); + return Err(()); + } + } + } + Ok(_) => { + return Ok(()); + } + } + } + Err(()) +} + +/// Removes the specified TCP and UDP port mappings. +pub fn remove_mappings(tcp_port: Option, udp_port: Option) { + if tcp_port.is_some() || udp_port.is_some() { + debug!("Removing UPnP port mappings"); + match igd::search_gateway(Default::default()) { + Ok(gateway) => { + if let Some(tcp_port) = tcp_port { + match gateway.remove_port(igd::PortMappingProtocol::TCP, tcp_port) { + Ok(()) => debug!(port = tcp_port, "UPnP Removed TCP port mapping"), + Err(e) => { + debug!(port = tcp_port, error = %e, "UPnP Failed to remove TCP port mapping") + } + } + } + if let Some(udp_port) = udp_port { + match gateway.remove_port(igd::PortMappingProtocol::UDP, udp_port) { + Ok(()) => debug!(port = tcp_port, "UPnP Removed UDP port mapping"), + Err(e) => { + debug!(port = tcp_port, error = %e, "UPnP Failed to remove UDP port mapping") + } + } + } + } + Err(e) => debug!(error = %e, "UPnP failed to remove mappings"), + } + } +} diff --git a/node/network/src/peer_manager/config.rs b/node/network/src/peer_manager/config.rs new file mode 100644 index 0000000..6c5523d --- /dev/null +++ b/node/network/src/peer_manager/config.rs @@ -0,0 +1,46 @@ +/// The time in seconds between re-status's peers. +pub const DEFAULT_STATUS_INTERVAL: u64 = 300; + +/// Default ping interval for outbound connections, in seconds. +pub const DEFAULT_PING_INTERVAL_OUTBOUND: u64 = 15; + +/// Default interval for inbound connections. +pub const DEFAULT_PING_INTERVAL_INBOUND: u64 = 20; + +/// Default number of peers to connect to. +pub const DEFAULT_TARGET_PEERS: usize = 50; + +/// Configurations for the PeerManager. +#[derive(Debug)] +pub struct Config { + /* Peer count related configurations */ + /// Whether discovery is enabled. + pub discovery_enabled: bool, + /// Whether metrics are enabled. + pub metrics_enabled: bool, + /// Target number of peers to connect to. + pub target_peer_count: usize, + + /* RPC related configurations */ + /// Time in seconds between status requests sent to peers. + pub status_interval: u64, + /// The time in seconds between PING events. We do not send a ping if the other peer has PING'd + /// us within this time frame (Seconds). This is asymmetric to avoid simultaneous pings. This + /// interval applies to inbound connections: those in which we are not the dialer. + pub ping_interval_inbound: u64, + /// Interval between PING events for peers dialed by us. + pub ping_interval_outbound: u64, +} + +impl Default for Config { + fn default() -> Self { + Config { + discovery_enabled: true, + metrics_enabled: false, + target_peer_count: DEFAULT_TARGET_PEERS, + status_interval: DEFAULT_STATUS_INTERVAL, + ping_interval_inbound: DEFAULT_PING_INTERVAL_INBOUND, + ping_interval_outbound: DEFAULT_PING_INTERVAL_OUTBOUND, + } + } +} diff --git a/node/network/src/peer_manager/mod.rs b/node/network/src/peer_manager/mod.rs new file mode 100644 index 0000000..f7b0f76 --- /dev/null +++ b/node/network/src/peer_manager/mod.rs @@ -0,0 +1,1329 @@ +//! Implementation of Lighthouse's peer management system. + +use crate::rpc::{GoodbyeReason, Protocol, RPCError, RPCResponseErrorCode}; +use crate::{error, metrics, Gossipsub}; +use crate::{NetworkGlobals, PeerId}; +use discv5::Enr; +use hashset_delay::HashSetDelay; +use libp2p::identify::IdentifyInfo; +use peerdb::{client::ClientKind, BanOperation, BanResult, ScoreUpdateResult}; +use smallvec::SmallVec; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; +use strum::IntoEnumIterator; + +pub use libp2p::core::{identity::Keypair, Multiaddr}; + +#[allow(clippy::mutable_key_type)] +// PeerId in hashmaps are no longer permitted by clippy +pub mod peerdb; + +pub use peerdb::peer_info::{ + ConnectionDirection, PeerConnectionStatus, PeerConnectionStatus::*, PeerInfo, +}; +use peerdb::score::{PeerAction, ReportSource}; +pub use peerdb::sync_status::{SyncInfo, SyncStatus}; +use std::collections::HashMap; +use std::net::IpAddr; +pub mod config; +mod network_behaviour; + +/// The heartbeat performs regular updates such as updating reputations and performing discovery +/// requests. This defines the interval in seconds. +const HEARTBEAT_INTERVAL: u64 = 30; + +/// This is used in the pruning logic. We avoid pruning peers on sync-committees if doing so would +/// lower our peer count below this number. Instead we favour a non-uniform distribution of subnet +/// peers. +pub const MIN_SYNC_COMMITTEE_PEERS: u64 = 2; +/// A fraction of `PeerManager::target_peers` that we allow to connect to us in excess of +/// `PeerManager::target_peers`. For clarity, if `PeerManager::target_peers` is 50 and +/// PEER_EXCESS_FACTOR = 0.1 we allow 10% more nodes, i.e 55. +pub const PEER_EXCESS_FACTOR: f32 = 0.1; +/// A fraction of `PeerManager::target_peers` that we want to be outbound-only connections. +pub const TARGET_OUTBOUND_ONLY_FACTOR: f32 = 0.3; +/// A fraction of `PeerManager::target_peers` that if we get below, we start a discovery query to +/// reach our target. MIN_OUTBOUND_ONLY_FACTOR must be < TARGET_OUTBOUND_ONLY_FACTOR. +pub const MIN_OUTBOUND_ONLY_FACTOR: f32 = 0.2; +/// The fraction of extra peers beyond the PEER_EXCESS_FACTOR that we allow us to dial for when +/// requiring subnet peers. More specifically, if our target peer limit is 50, and our excess peer +/// limit is 55, and we are at 55 peers, the following parameter provisions a few more slots of +/// dialing priority peers we need for validator duties. +pub const PRIORITY_PEER_EXCESS: f32 = 0.2; + +/// The main struct that handles peer's reputation and connection status. +pub struct PeerManager { + /// Storage of network globals to access the `PeerDB`. + network_globals: Arc, + /// A queue of events that the `PeerManager` is waiting to produce. + events: SmallVec<[PeerManagerEvent; 16]>, + /// A collection of inbound-connected peers awaiting to be Ping'd. + inbound_ping_peers: HashSetDelay, + /// A collection of outbound-connected peers awaiting to be Ping'd. + outbound_ping_peers: HashSetDelay, + /// A collection of peers awaiting to be Status'd. + status_peers: HashSetDelay, + /// The target number of peers we would like to connect to. + target_peers: usize, + /// The heartbeat interval to perform routine maintenance. + heartbeat: tokio::time::Interval, + /// Keeps track of whether the discovery service is enabled or not. + discovery_enabled: bool, + /// Keeps track if the current instance is reporting metrics or not. + metrics_enabled: bool, +} + +/// The events that the `PeerManager` outputs (requests). +#[derive(Debug)] +pub enum PeerManagerEvent { + /// A peer has dialed us. + PeerConnectedIncoming(PeerId), + /// A peer has been dialed. + PeerConnectedOutgoing(PeerId), + /// A peer has disconnected. + PeerDisconnected(PeerId), + /// Sends a STATUS to a peer. + Status(PeerId), + /// Sends a PING to a peer. + Ping(PeerId), + /// The peer should be disconnected. + DisconnectPeer(PeerId, GoodbyeReason), + /// Inform the behaviour to ban this peer and associated ip addresses. + Banned(PeerId, Vec), + /// The peer should be unbanned with the associated ip addresses. + UnBanned(PeerId, Vec), + /// Request the behaviour to discover more peers and the amount of peers to discover. + DiscoverPeers(usize), +} + +impl PeerManager { + // NOTE: Must be run inside a tokio executor. + pub async fn new( + cfg: config::Config, + network_globals: Arc, + ) -> error::Result { + let config::Config { + discovery_enabled, + metrics_enabled, + target_peer_count, + status_interval, + ping_interval_inbound, + ping_interval_outbound, + } = cfg; + + // Set up the peer manager heartbeat interval + let heartbeat = tokio::time::interval(tokio::time::Duration::from_secs(HEARTBEAT_INTERVAL)); + + Ok(PeerManager { + network_globals, + events: SmallVec::new(), + inbound_ping_peers: HashSetDelay::new(Duration::from_secs(ping_interval_inbound)), + outbound_ping_peers: HashSetDelay::new(Duration::from_secs(ping_interval_outbound)), + status_peers: HashSetDelay::new(Duration::from_secs(status_interval)), + target_peers: target_peer_count, + heartbeat, + discovery_enabled, + metrics_enabled, + }) + } + + /* Public accessible functions */ + + /// The application layer wants to disconnect from a peer for a particular reason. + /// + /// All instant disconnections are fatal and we ban the associated peer. + /// + /// This will send a goodbye and disconnect the peer if it is connected or dialing. + pub fn goodbye_peer(&mut self, peer_id: &PeerId, reason: GoodbyeReason, source: ReportSource) { + // Update the sync status if required + if let Some(info) = self.network_globals.peers.write().peer_info_mut(peer_id) { + debug!(peer_id = %peer_id, reason = %reason, score = %info.score(), "Sending goodbye to peer"); + if matches!(reason, GoodbyeReason::IrrelevantNetwork) { + info.update_sync_status(SyncStatus::IrrelevantPeer); + } + } + + self.report_peer( + peer_id, + PeerAction::Fatal, + source, + Some(reason), + "goodbye_peer", + ); + } + + /// Reports a peer for some action. + /// + /// If the peer doesn't exist, log a warning and insert defaults. + pub fn report_peer( + &mut self, + peer_id: &PeerId, + action: PeerAction, + source: ReportSource, + reason: Option, + msg: &'static str, + ) { + let action = self + .network_globals + .peers + .write() + .report_peer(peer_id, action, source, msg); + self.handle_score_action(peer_id, action, reason); + } + + /// Upon adjusting a Peer's score, there are times the peer manager must pass messages up to + /// libp2p. This function handles the conditional logic associated with each score update + /// result. + fn handle_score_action( + &mut self, + peer_id: &PeerId, + action: ScoreUpdateResult, + reason: Option, + ) { + match action { + ScoreUpdateResult::Ban(ban_operation) => { + // The peer has been banned and we need to handle the banning operation + // NOTE: When we ban a peer, its IP address can be banned. We do not recursively search + // through all our connected peers banning all other peers that are using this IP address. + // If these peers are behaving fine, we permit their current connections. However, if any new + // nodes or current nodes try to reconnect on a banned IP, they will be instantly banned + // and disconnected. + self.handle_ban_operation(peer_id, ban_operation, reason); + } + ScoreUpdateResult::Disconnect => { + // The peer has transitioned to a disconnect state and has been marked as such in + // the peer db. We must inform libp2p to disconnect this peer. + self.events.push(PeerManagerEvent::DisconnectPeer( + *peer_id, + GoodbyeReason::BadScore, + )); + } + ScoreUpdateResult::NoAction => { + // The report had no effect on the peer and there is nothing to do. + } + ScoreUpdateResult::Unbanned(unbanned_ips) => { + // Inform the Swarm to unban the peer + self.events + .push(PeerManagerEvent::UnBanned(*peer_id, unbanned_ips)); + } + } + } + + /// If a peer is being banned, this handles the banning operation. + fn handle_ban_operation( + &mut self, + peer_id: &PeerId, + ban_operation: BanOperation, + reason: Option, + ) { + match ban_operation { + BanOperation::DisconnectThePeer => { + // The peer was currently connected, so we start a disconnection. + // Once the peer has disconnected, its connection state will transition to a + // banned state. + self.events.push(PeerManagerEvent::DisconnectPeer( + *peer_id, + reason.unwrap_or(GoodbyeReason::BadScore), + )); + } + BanOperation::PeerDisconnecting => { + // The peer is currently being disconnected and will be banned once the + // disconnection completes. + } + BanOperation::ReadyToBan(banned_ips) => { + // The peer is not currently connected, we can safely ban it at the swarm + // level. + // Inform the Swarm to ban the peer + self.events + .push(PeerManagerEvent::Banned(*peer_id, banned_ips)); + } + } + } + + /// Peers that have been returned by discovery requests that are suitable for dialing are + /// returned here. + /// + /// NOTE: By dialing `PeerId`s and not multiaddrs, libp2p requests the multiaddr associated + /// with a new `PeerId` which involves a discovery routing table lookup. We could dial the + /// multiaddr here, however this could relate to duplicate PeerId's etc. If the lookup + /// proves resource constraining, we should switch to multiaddr dialling here. + #[allow(clippy::mutable_key_type)] + pub fn peers_discovered(&mut self, results: HashMap>) -> Vec { + let mut to_dial_peers = Vec::new(); + + let connected_or_dialing = self.network_globals.connected_or_dialing_peers(); + for (peer_id, min_ttl) in results { + // There are two conditions in deciding whether to dial this peer. + // 1. If we are less than our max connections. Discovery queries are executed to reach + // our target peers, so its fine to dial up to our max peers (which will get pruned + // in the next heartbeat down to our target). + // 2. If the peer is one our validators require for a specific subnet, then it is + // considered a priority. We have pre-allocated some extra priority slots for these + // peers as specified by PRIORITY_PEER_EXCESS. Therefore we dial these peers, even + // if we are already at our max_peer limit. + if (min_ttl.is_some() + && connected_or_dialing + to_dial_peers.len() < self.max_priority_peers() + || connected_or_dialing + to_dial_peers.len() < self.max_peers()) + && self.network_globals.peers.read().should_dial(&peer_id) + { + // This should be updated with the peer dialing. In fact created once the peer is + // dialed + if let Some(min_ttl) = min_ttl { + self.network_globals + .peers + .write() + .update_min_ttl(&peer_id, min_ttl); + } + to_dial_peers.push(peer_id); + } + } + + // Queue another discovery if we need to + self.maintain_peer_count(to_dial_peers.len()); + + to_dial_peers + } + + /// A STATUS message has been received from a peer. This resets the status timer. + pub fn peer_statusd(&mut self, peer_id: &PeerId) { + self.status_peers.insert(*peer_id); + } + + /// The maximum number of peers we allow to connect to us. This is `target_peers` * (1 + + /// PEER_EXCESS_FACTOR) + fn max_peers(&self) -> usize { + (self.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR)).ceil() as usize + } + + /// The maximum number of peers we allow when dialing a priority peer (i.e a peer that is + /// subscribed to subnets that our validator requires. This is `target_peers` * (1 + + /// PEER_EXCESS_FACTOR + PRIORITY_PEER_EXCESS) + fn max_priority_peers(&self) -> usize { + (self.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR + PRIORITY_PEER_EXCESS)).ceil() + as usize + } + + /// The minimum number of outbound peers that we reach before we start another discovery query. + fn min_outbound_only_peers(&self) -> usize { + (self.target_peers as f32 * MIN_OUTBOUND_ONLY_FACTOR).ceil() as usize + } + + /// The minimum number of outbound peers that we reach before we start another discovery query. + fn target_outbound_peers(&self) -> usize { + (self.target_peers as f32 * TARGET_OUTBOUND_ONLY_FACTOR).ceil() as usize + } + + /// The maximum number of peers that are connected or dialing before we refuse to do another + /// discovery search for more outbound peers. We can use up to half the priority peer excess allocation. + fn max_outbound_dialing_peers(&self) -> usize { + (self.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR + PRIORITY_PEER_EXCESS / 2.0)).ceil() + as usize + } + + /* Notifications from the Swarm */ + + // A peer is being dialed. + pub fn inject_dialing(&mut self, peer_id: &PeerId, enr: Option) { + self.inject_peer_connection(peer_id, ConnectingType::Dialing, enr); + } + + /// Reports if a peer is banned or not. + /// + /// This is used to determine if we should accept incoming connections. + pub fn ban_status(&self, peer_id: &PeerId) -> BanResult { + self.network_globals.peers.read().ban_status(peer_id) + } + + pub fn is_connected(&self, peer_id: &PeerId) -> bool { + self.network_globals.peers.read().is_connected(peer_id) + } + + /// Reports whether the peer limit is reached in which case we stop allowing new incoming + /// connections. + pub fn peer_limit_reached(&self, count_dialing: bool) -> bool { + if count_dialing { + // This is an incoming connection so limit by the standard max peers + self.network_globals.connected_or_dialing_peers() >= self.max_peers() + } else { + // We dialed this peer, allow up to max_outbound_dialing_peers + self.network_globals.connected_peers() >= self.max_outbound_dialing_peers() + } + } + + /// Updates `PeerInfo` with `identify` information. + pub fn identify(&mut self, peer_id: &PeerId, info: &IdentifyInfo) { + if let Some(peer_info) = self.network_globals.peers.write().peer_info_mut(peer_id) { + let previous_kind = peer_info.client().kind; + let previous_listening_addresses = + peer_info.set_listening_addresses(info.listen_addrs.clone()); + peer_info.set_client(peerdb::client::Client::from_identify_info(info)); + + if previous_kind != peer_info.client().kind + || *peer_info.listening_addresses() != previous_listening_addresses + { + debug!( + peer = %peer_id, + protocol_version = %info.protocol_version, + agent_version = %info.agent_version, + // listening_ addresses = ?info.listen_addrs, + observed_address = ?info.observed_addr, + protocols = ?info.protocols, + "Identified Peer", + ); + + // update the peer client kind metric if the peer is connected + if matches!( + peer_info.connection_status(), + PeerConnectionStatus::Connected { .. } + | PeerConnectionStatus::Disconnecting { .. } + ) { + metrics::inc_gauge_vec( + &metrics::PEERS_PER_CLIENT, + &[peer_info.client().kind.as_ref()], + ); + metrics::dec_gauge_vec(&metrics::PEERS_PER_CLIENT, &[previous_kind.as_ref()]); + } + } + } else { + error!(%peer_id, "Received an Identify response from an unknown peer"); + } + } + + /// An error has occurred in the RPC. + /// + /// This adjusts a peer's score based on the error. + pub fn handle_rpc_error( + &mut self, + peer_id: &PeerId, + protocol: Protocol, + err: &RPCError, + direction: ConnectionDirection, + ) { + let client = self.network_globals.client(peer_id); + let score = self.network_globals.peers.read().score(peer_id); + debug!(%protocol, %err, %client, %peer_id, %score, ?direction, "RPC Error"); + + metrics::inc_counter_vec( + &metrics::TOTAL_RPC_ERRORS_PER_CLIENT, + &[ + client.kind.as_ref(), + err.as_static_str(), + direction.as_ref(), + ], + ); + + // Map this error to a `PeerAction` (if any) + let peer_action = match err { + RPCError::IncompleteStream => { + // They closed early, this could mean poor connection + PeerAction::MidToleranceError + } + RPCError::InternalError(e) => { + debug!(error = %e, %peer_id, "Internal RPC Error"); + return; + } + RPCError::HandlerRejected => { + // Our fault. Do nothing + return; + } + RPCError::InvalidData(_) => { + // Peer is not complying with the protocol. This is considered a malicious action + PeerAction::Fatal + } + RPCError::IoError(_e) => { + // this could their fault or ours, so we tolerate this + PeerAction::HighToleranceError + } + RPCError::ErrorResponse(code, _) => match code { + RPCResponseErrorCode::Unknown => PeerAction::HighToleranceError, + RPCResponseErrorCode::ResourceUnavailable => { + // NOTE: This error only makes sense for the `BlocksByRange` and `BlocksByRoot` + // protocols. + // + // If we are syncing, there is no point keeping these peers around and + // continually failing to request blocks. We instantly ban them and hope that + // by the time the ban lifts, the peers will have completed their backfill + // sync. + // + // TODO: Potentially a more graceful way of handling such peers, would be to + // implement a new sync type which tracks these peers and prevents the sync + // algorithms from requesting blocks from them (at least for a set period of + // time, multiple failures would then lead to a ban). + PeerAction::Fatal + } + RPCResponseErrorCode::ServerError => PeerAction::MidToleranceError, + RPCResponseErrorCode::InvalidRequest => PeerAction::HighToleranceError, + RPCResponseErrorCode::RateLimited => match protocol { + Protocol::Ping => PeerAction::MidToleranceError, + Protocol::Goodbye => PeerAction::LowToleranceError, + Protocol::Status => PeerAction::LowToleranceError, + Protocol::DataByHash => PeerAction::MidToleranceError, + Protocol::GetChunks => PeerAction::MidToleranceError, + }, + }, + RPCError::SSZDecodeError(_) => PeerAction::Fatal, + RPCError::UnsupportedProtocol => { + // Not supporting a protocol shouldn't be considered a malicious action, but + // it is an action that in some cases will make the peer unfit to continue + // communicating. + + match protocol { + Protocol::Ping => PeerAction::Fatal, + Protocol::Goodbye => return, + Protocol::Status => PeerAction::LowToleranceError, + Protocol::DataByHash => return, + Protocol::GetChunks => return, + } + } + RPCError::StreamTimeout => match direction { + ConnectionDirection::Incoming => { + // There was a timeout responding to a peer. + debug!(%peer_id, "Timed out responding to RPC Request"); + return; + } + ConnectionDirection::Outgoing => match protocol { + Protocol::Ping => PeerAction::LowToleranceError, + Protocol::Goodbye => return, + Protocol::Status => return, + Protocol::DataByHash => PeerAction::MidToleranceError, + Protocol::GetChunks => PeerAction::MidToleranceError, + }, + }, + RPCError::NegotiationTimeout => PeerAction::LowToleranceError, + RPCError::Disconnected => return, // No penalty for a graceful disconnection + }; + + self.report_peer( + peer_id, + peer_action, + ReportSource::RPC, + None, + "handle_rpc_error", + ); + } + + /// A ping request has been received. + // NOTE: The behaviour responds with a PONG automatically + pub fn ping_request(&mut self, peer_id: &PeerId, seq: u64) { + if let Some(peer_info) = self.network_globals.peers.read().peer_info(peer_id) { + // received a ping + // reset the to-ping timer for this peer + trace!(%peer_id, seq_no = %seq, "Received a ping request"); + match peer_info.connection_direction() { + Some(ConnectionDirection::Incoming) => { + self.inbound_ping_peers.insert(*peer_id); + } + Some(ConnectionDirection::Outgoing) => { + self.outbound_ping_peers.insert(*peer_id); + } + None => { + warn!(%peer_id, "Received a ping from a peer with an unknown connection direction"); + } + } + } else { + error!(%peer_id, "Received a PING from an unknown peer"); + } + } + + /// A PONG has been returned from a peer. + pub fn pong_response(&mut self, peer_id: &PeerId, _seq: u64) { + if let Some(_peer_info) = self.network_globals.peers.read().peer_info(peer_id) { + // received a pong + } else { + error!(%peer_id, "Received a PONG from an unknown peer"); + } + } + + /// Updates the gossipsub scores for all known peers in gossipsub. + pub(crate) fn update_gossipsub_scores(&mut self, gossipsub: &Gossipsub) { + let actions = self + .network_globals + .peers + .write() + .update_gossipsub_scores(self.target_peers, gossipsub); + + for (peer_id, score_action) in actions { + self.handle_score_action(&peer_id, score_action, None); + } + } + + // This function updates metrics for all connected peers. + fn update_connected_peer_metrics(&self) { + // Do nothing if we don't have metrics enabled. + if !self.metrics_enabled { + return; + } + + let mut connected_peer_count = 0; + let mut inbound_connected_peers = 0; + let mut outbound_connected_peers = 0; + let mut clients_per_peer = HashMap::new(); + + for (_peer, peer_info) in self.network_globals.peers.read().connected_peers() { + connected_peer_count += 1; + if let PeerConnectionStatus::Connected { n_in, .. } = peer_info.connection_status() { + if *n_in > 0 { + inbound_connected_peers += 1; + } else { + outbound_connected_peers += 1; + } + } + *clients_per_peer + .entry(peer_info.client().kind.to_string()) + .or_default() += 1; + } + + metrics::set_gauge(&metrics::PEERS_CONNECTED, connected_peer_count); + metrics::set_gauge(&metrics::NETWORK_INBOUND_PEERS, inbound_connected_peers); + metrics::set_gauge(&metrics::NETWORK_OUTBOUND_PEERS, outbound_connected_peers); + + for client_kind in ClientKind::iter() { + let value = clients_per_peer.get(&client_kind.to_string()).unwrap_or(&0); + metrics::set_gauge_vec( + &metrics::PEERS_PER_CLIENT, + &[client_kind.as_ref()], + *value as i64, + ); + } + } + + /* Internal functions */ + + /// Sets a peer as connected as long as their reputation allows it + /// Informs if the peer was accepted + fn inject_connect_ingoing( + &mut self, + peer_id: &PeerId, + multiaddr: Multiaddr, + enr: Option, + ) -> bool { + self.inject_peer_connection(peer_id, ConnectingType::IngoingConnected { multiaddr }, enr) + } + + /// Sets a peer as connected as long as their reputation allows it + /// Informs if the peer was accepted + fn inject_connect_outgoing( + &mut self, + peer_id: &PeerId, + multiaddr: Multiaddr, + enr: Option, + ) -> bool { + self.inject_peer_connection( + peer_id, + ConnectingType::OutgoingConnected { multiaddr }, + enr, + ) + } + + /// Updates the state of the peer as disconnected. + /// + /// This is also called when dialing a peer fails. + fn inject_disconnect(&mut self, peer_id: &PeerId) { + let (ban_operation, purged_peers) = self + .network_globals + .peers + .write() + .inject_disconnect(peer_id); + + if let Some(ban_operation) = ban_operation { + // The peer was awaiting a ban, continue to ban the peer. + self.handle_ban_operation(peer_id, ban_operation, None); + } + + // Remove the ping and status timer for the peer + self.inbound_ping_peers.remove(peer_id); + self.outbound_ping_peers.remove(peer_id); + self.status_peers.remove(peer_id); + self.events.extend( + purged_peers + .into_iter() + .map(|(peer_id, unbanned_ips)| PeerManagerEvent::UnBanned(peer_id, unbanned_ips)), + ); + } + + /// Registers a peer as connected. The `ingoing` parameter determines if the peer is being + /// dialed or connecting to us. + /// + /// This is called by `connect_ingoing` and `connect_outgoing`. + /// + /// Informs if the peer was accepted in to the db or not. + fn inject_peer_connection( + &mut self, + peer_id: &PeerId, + connection: ConnectingType, + enr: Option, + ) -> bool { + { + let mut peerdb = self.network_globals.peers.write(); + if !matches!(peerdb.ban_status(peer_id), BanResult::NotBanned) { + // don't connect if the peer is banned + error!(%peer_id, "Connection has been allowed to a banned peer"); + } + + match connection { + ConnectingType::Dialing => { + peerdb.dialing_peer(peer_id, enr); + return true; + } + ConnectingType::IngoingConnected { multiaddr } => { + peerdb.connect_ingoing(peer_id, multiaddr, enr); + // start a timer to ping inbound peers. + self.inbound_ping_peers.insert(*peer_id); + } + ConnectingType::OutgoingConnected { multiaddr } => { + peerdb.connect_outgoing(peer_id, multiaddr, enr); + // start a timer for to ping outbound peers. + self.outbound_ping_peers.insert(*peer_id); + } + } + } + + // start a ping and status timer for the peer + self.status_peers.insert(*peer_id); + + let connected_peers = self.network_globals.connected_peers() as i64; + + // increment prometheus metrics + metrics::inc_counter(&metrics::PEER_CONNECT_EVENT_COUNT); + metrics::set_gauge(&metrics::PEERS_CONNECTED, connected_peers); + + true + } + + // Gracefully disconnects a peer without banning them. + fn disconnect_peer(&mut self, peer_id: PeerId, reason: GoodbyeReason) { + self.events + .push(PeerManagerEvent::DisconnectPeer(peer_id, reason)); + self.network_globals + .peers + .write() + .notify_disconnecting(&peer_id, false); + } + + /// This function checks the status of our current peers and optionally requests a discovery + /// query if we need to find more peers to maintain the current number of peers + fn maintain_peer_count(&mut self, dialing_peers: usize) { + // Check if we need to do a discovery lookup + if self.discovery_enabled { + let peer_count = self.network_globals.connected_or_dialing_peers(); + let outbound_only_peer_count = self.network_globals.connected_outbound_only_peers(); + let wanted_peers = if peer_count < self.target_peers.saturating_sub(dialing_peers) { + // We need more peers in general. + // The maximum discovery query is for 16 peers, but we can search for less if + // needed. + std::cmp::min( + self.target_peers.saturating_sub(dialing_peers) - peer_count, + 16, + ) + } else if outbound_only_peer_count < self.min_outbound_only_peers() + && peer_count < self.max_outbound_dialing_peers() + { + std::cmp::min( + self.max_outbound_dialing_peers() + .saturating_sub(dialing_peers) + - peer_count, + 16, + ) + } else { + 0 + }; + + if wanted_peers != 0 { + // We need more peers, re-queue a discovery lookup. + debug!( + connected = peer_count, + target = self.target_peers, + outbound = outbound_only_peer_count, + wanted = wanted_peers, + "Starting a new peer discovery query", + ); + self.events + .push(PeerManagerEvent::DiscoverPeers(wanted_peers)); + } + } + } + + /// Remove excess peers back down to our target values. + /// This prioritises peers with a good score and uniform distribution of peers across + /// subnets. + /// + /// The logic for the peer pruning is as follows: + /// + /// Global rules: + /// - Always maintain peers we need for a validator duty. + /// - Do not prune outbound peers to exceed our outbound target. + /// - Do not prune more peers than our target peer count. + /// - If we have an option to remove a number of peers, remove ones that have the least + /// long-lived subnets. + /// - When pruning peers based on subnet count. If multiple peers can be chosen, choose a peer + /// that is not subscribed to a long-lived sync committee subnet. + /// - When pruning peers based on subnet count, do not prune a peer that would lower us below the + /// MIN_SYNC_COMMITTEE_PEERS peer count. To keep it simple, we favour a minimum number of sync-committee-peers over + /// uniformity subnet peers. NOTE: We could apply more sophisticated logic, but the code is + /// simpler and easier to maintain if we take this approach. If we are pruning subnet peers + /// below the MIN_SYNC_COMMITTEE_PEERS and maintaining the sync committee peers, this should be + /// fine as subnet peers are more likely to be found than sync-committee-peers. Also, we're + /// in a bit of trouble anyway if we have so few peers on subnets. The + /// MIN_SYNC_COMMITTEE_PEERS + /// number should be set low as an absolute lower bound to maintain peers on the sync + /// committees. + /// + /// Prune peers in the following order: + /// 1. Remove worst scoring peers + /// 2. Remove peers that are not subscribed to a subnet (they have less value) + /// 3. Remove peers that we have many on any particular subnet + /// 4. Randomly remove peers if all the above are satisfied + /// + fn prune_excess_peers(&mut self) { + // The current number of connected peers. + let connected_peer_count = self.network_globals.connected_peers(); + if connected_peer_count <= self.target_peers { + // No need to prune peers + return; + } + + // Keep a list of peers we are pruning. + let mut peers_to_prune = std::collections::HashSet::new(); + let connected_outbound_peer_count = self.network_globals.connected_outbound_only_peers(); + + // Keep track of the number of outbound peers we are pruning. + let mut outbound_peers_pruned = 0; + + macro_rules! prune_peers { + ($filter: expr) => { + for (peer_id, info) in self + .network_globals + .peers + .read() + .worst_connected_peers() + .iter() + .filter(|(_, info)| !info.has_future_duty() && $filter(*info)) + { + if peers_to_prune.len() + >= connected_peer_count.saturating_sub(self.target_peers) + { + // We have found all the peers we need to drop, end. + break; + } + if peers_to_prune.contains(*peer_id) { + continue; + } + // Only remove up to the target outbound peer count. + if info.is_outbound_only() { + if self.target_outbound_peers() + outbound_peers_pruned + < connected_outbound_peer_count + { + outbound_peers_pruned += 1; + } else { + continue; + } + } + peers_to_prune.insert(**peer_id); + } + }; + } + + // 1. Look through peers that have the worst score (ignoring non-penalized scored peers). + prune_peers!(|info: &PeerInfo| { info.score().score() < 0.0 }); + + // 2. Attempt to remove peers that are not subscribed to a subnet, if we still need to + // prune more. + if peers_to_prune.len() < connected_peer_count.saturating_sub(self.target_peers) { + // prune_peers!(|info: &PeerInfo| { !info.has_long_lived_subnet() }); + prune_peers!(|_info: &PeerInfo| { true }); + } + + // 3. and 4. Remove peers that are too grouped on any given subnet. If all subnets are + // uniformly distributed, remove random peers. + // if peers_to_prune.len() < connected_peer_count.saturating_sub(self.target_peers) { + // // Of our connected peers, build a map from subnet_id -> Vec<(PeerId, PeerInfo)> + // let mut subnet_to_peer: HashMap> = + // HashMap::new(); + // // These variables are used to track if a peer is in a long-lived sync-committee as we + // // may wish to retain this peer over others when pruning. + // let mut sync_committee_peer_count: HashMap = HashMap::new(); + // let peer_to_sync_committee: HashMap< + // PeerId, + // std::collections::HashSet, + // > = HashMap::new(); + + // for (peer_id, _info) in self.network_globals.peers.read().connected_peers() { + // // Ignore peers we are already pruning + // if peers_to_prune.contains(peer_id) { + // continue; + // } + + // // Count based on long-lived subnets not short-lived subnets + // // NOTE: There are only 4 sync committees. These are likely to be denser than the + // // subnets, so our priority here to make the subnet peer count uniform, ignoring + // // the dense sync committees. + // for subnet in info.long_lived_subnets() { + // match subnet { + // Subnet::Attestation(_) => { + // subnet_to_peer + // .entry(subnet) + // .or_insert_with(Vec::new) + // .push((*peer_id, info.clone())); + // } + // Subnet::SyncCommittee(id) => { + // *sync_committee_peer_count.entry(id).or_default() += 1; + // peer_to_sync_committee + // .entry(*peer_id) + // .or_default() + // .insert(id); + // } + // } + // } + // } + + // // Add to the peers to prune mapping + // while peers_to_prune.len() < connected_peer_count.saturating_sub(self.target_peers) { + // if let Some((_, peers_on_subnet)) = subnet_to_peer + // .iter_mut() + // .max_by_key(|(_, peers)| peers.len()) + // { + // // and the subnet still contains peers + // if !peers_on_subnet.is_empty() { + // // Order the peers by the number of subnets they are long-lived + // // subscribed too, shuffle equal peers. + // peers_on_subnet.shuffle(&mut rand::thread_rng()); + // // peers_on_subnet.sort_by_key(|(_, info)| info.long_lived_subnet_count()); + + // // Try and find a candidate peer to remove from the subnet. + // // We ignore peers that would put us below our target outbound peers + // // and we currently ignore peers that would put us below our + // // sync-committee threshold, if we can avoid it. + + // let mut removed_peer_index = None; + // for (index, (candidate_peer, info)) in peers_on_subnet.iter().enumerate() { + // // Ensure we don't remove too many outbound peers + // if info.is_outbound_only() { + // if self.target_outbound_peers() + // < connected_outbound_peer_count + // .saturating_sub(outbound_peers_pruned) + // { + // outbound_peers_pruned += 1; + // } else { + // // Restart the main loop with the outbound peer removed from + // // the list. This will lower the peers per subnet count and + // // potentially a new subnet may be chosen to remove peers. This + // // can occur recursively until we have no peers left to choose + // // from. + // continue; + // } + // } + + // // Check the sync committee + // if let Some(subnets) = peer_to_sync_committee.get(candidate_peer) { + // // The peer is subscribed to some long-lived sync-committees + // // Of all the subnets this peer is subscribed too, the minimum + // // peer count of all of them is min_subnet_count + // if let Some(min_subnet_count) = subnets + // .iter() + // .filter_map(|v| sync_committee_peer_count.get(v).copied()) + // .min() + // { + // // If the minimum count is our target or lower, we + // // shouldn't remove this peer, because it drops us lower + // // than our target + // if min_subnet_count <= MIN_SYNC_COMMITTEE_PEERS { + // // Do not drop this peer in this pruning interval + // continue; + // } + // } + // } + + // // This peer is suitable to be pruned + // removed_peer_index = Some(index); + // break; + // } + + // // If we have successfully found a candidate peer to prune, prune it, + // // otherwise all peers on this subnet should not be removed due to our + // // outbound limit or min_subnet_count. In this case, we remove all + // // peers from the pruning logic and try another subnet. + // if let Some(index) = removed_peer_index { + // let (candidate_peer, _) = peers_on_subnet.remove(index); + // // Remove pruned peers from other subnet counts + // for subnet_peers in subnet_to_peer.values_mut() { + // subnet_peers.retain(|(peer_id, _)| peer_id != &candidate_peer); + // } + // // Remove pruned peers from all sync-committee counts + // if let Some(known_sync_committes) = + // peer_to_sync_committee.get(&candidate_peer) + // { + // for sync_committee in known_sync_committes { + // if let Some(sync_committee_count) = + // sync_committee_peer_count.get_mut(sync_committee) + // { + // *sync_committee_count = + // sync_committee_count.saturating_sub(1); + // } + // } + // } + // peers_to_prune.insert(candidate_peer); + // } else { + // peers_on_subnet.clear(); + // } + // continue; + // } + // } + // // If there are no peers left to prune exit. + // break; + // } + // } + + // Disconnect the pruned peers. + for peer_id in peers_to_prune { + self.disconnect_peer(peer_id, GoodbyeReason::TooManyPeers); + } + } + + /// The Peer manager's heartbeat maintains the peer count and maintains peer reputations. + /// + /// It will request discovery queries if the peer count has not reached the desired number of + /// overall peers, as well as the desired number of outbound-only peers. + /// + /// NOTE: Discovery will only add a new query if one isn't already queued. + fn heartbeat(&mut self) { + info!( + connected = self.network_globals.connected_or_dialing_peers(), + target = self.target_peers, + outbound = self.network_globals.connected_outbound_only_peers(), + "Peer statistics", + ); + + // Optionally run a discovery query if we need more peers. + self.maintain_peer_count(0); + + // Cleans up the connection state of dialing peers. + // Libp2p dials peer-ids, but sometimes the response is from another peer-id or libp2p + // returns dial errors without a peer-id attached. This function reverts peers that have a + // dialing status long than DIAL_TIMEOUT seconds to a disconnected status. This is important because + // we count the number of dialing peers in our inbound connections. + self.network_globals.peers.write().cleanup_dialing_peers(); + + // Updates peer's scores and unban any peers if required. + let actions = self.network_globals.peers.write().update_scores(); + for (peer_id, action) in actions { + self.handle_score_action(&peer_id, action, None); + } + + // Update peer score metrics; + self.update_peer_score_metrics(); + + // Prune any excess peers back to our target in such a way that incentivises good scores and + // a uniform distribution of subnets. + self.prune_excess_peers(); + } + + // Update metrics related to peer scoring. + fn update_peer_score_metrics(&self) { + if !self.metrics_enabled { + return; + } + // reset the gauges + let _ = metrics::PEER_SCORE_DISTRIBUTION + .as_ref() + .map(|gauge| gauge.reset()); + let _ = metrics::PEER_SCORE_PER_CLIENT + .as_ref() + .map(|gauge| gauge.reset()); + + let mut avg_score_per_client: HashMap = HashMap::with_capacity(5); + { + let peers_db_read_lock = self.network_globals.peers.read(); + let connected_peers = peers_db_read_lock.best_peers_by_status(PeerInfo::is_connected); + let total_peers = connected_peers.len(); + for (id, (_peer, peer_info)) in connected_peers.into_iter().enumerate() { + // First quartile + if id == 0 { + metrics::set_gauge_vec( + &metrics::PEER_SCORE_DISTRIBUTION, + &["1st"], + peer_info.score().score() as i64, + ); + } else if id == (total_peers * 3 / 4).saturating_sub(1) { + metrics::set_gauge_vec( + &metrics::PEER_SCORE_DISTRIBUTION, + &["3/4"], + peer_info.score().score() as i64, + ); + } else if id == (total_peers / 2).saturating_sub(1) { + metrics::set_gauge_vec( + &metrics::PEER_SCORE_DISTRIBUTION, + &["1/2"], + peer_info.score().score() as i64, + ); + } else if id == (total_peers / 4).saturating_sub(1) { + metrics::set_gauge_vec( + &metrics::PEER_SCORE_DISTRIBUTION, + &["1/4"], + peer_info.score().score() as i64, + ); + } else if id == total_peers.saturating_sub(1) { + metrics::set_gauge_vec( + &metrics::PEER_SCORE_DISTRIBUTION, + &["last"], + peer_info.score().score() as i64, + ); + } + + let score_peers: &mut (f64, usize) = avg_score_per_client + .entry(peer_info.client().kind.to_string()) + .or_default(); + score_peers.0 += peer_info.score().score(); + score_peers.1 += 1; + } + } // read lock ended + + for (client, (score, peers)) in avg_score_per_client { + metrics::set_float_gauge_vec( + &metrics::PEER_SCORE_PER_CLIENT, + &[&client.to_string()], + score / (peers as f64), + ); + } + } +} + +enum ConnectingType { + /// We are in the process of dialing this peer. + Dialing, + /// A peer has dialed us. + IngoingConnected { + // The multiaddr the peer connected to us on. + multiaddr: Multiaddr, + }, + /// We have successfully dialed a peer. + OutgoingConnected { + /// The multiaddr we dialed to reach the peer. + multiaddr: Multiaddr, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn build_peer_manager(target_peer_count: usize) -> PeerManager { + let config = config::Config { + target_peer_count, + discovery_enabled: false, + ..Default::default() + }; + let globals = NetworkGlobals::new_test_globals(); + PeerManager::new(config, Arc::new(globals)).await.unwrap() + } + + #[tokio::test] + async fn test_peer_manager_disconnects_correctly_during_heartbeat() { + let mut peer_manager = build_peer_manager(3).await; + + // Create 5 peers to connect to. + // 2 will be outbound-only, and have the lowest score. + let peer0 = PeerId::random(); + let peer1 = PeerId::random(); + let peer2 = PeerId::random(); + let outbound_only_peer1 = PeerId::random(); + let outbound_only_peer2 = PeerId::random(); + + peer_manager.inject_connect_ingoing(&peer0, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_ingoing(&peer1, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_ingoing(&peer2, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_outgoing( + &outbound_only_peer1, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + peer_manager.inject_connect_outgoing( + &outbound_only_peer2, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + + // Set the outbound-only peers to have the lowest score. + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&outbound_only_peer1) + .unwrap() + .add_to_score(-1.0); + + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&outbound_only_peer2) + .unwrap() + .add_to_score(-2.0); + + // Check initial connected peers. + assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 5); + + peer_manager.heartbeat(); + + // Check that we disconnected from two peers. + // Check that one outbound-only peer was removed because it had the worst score + // and that we did not disconnect the other outbound peer due to the minimum outbound quota. + assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); + assert!(peer_manager + .network_globals + .peers + .read() + .is_connected(&outbound_only_peer1)); + assert!(!peer_manager + .network_globals + .peers + .read() + .is_connected(&outbound_only_peer2)); + + peer_manager.heartbeat(); + + // Check that if we are at target number of peers, we do not disconnect any. + assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); + } + + #[tokio::test] + async fn test_peer_manager_not_enough_outbound_peers_no_panic_during_heartbeat() { + let mut peer_manager = build_peer_manager(20).await; + + // Connect to 20 ingoing-only peers. + for _i in 0..19 { + let peer = PeerId::random(); + peer_manager.inject_connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); + } + + // Connect an outbound-only peer. + // Give it the lowest score so that it is evaluated first in the disconnect list iterator. + let outbound_only_peer = PeerId::random(); + peer_manager.inject_connect_ingoing( + &outbound_only_peer, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(outbound_only_peer)) + .unwrap() + .add_to_score(-1.0); + // After heartbeat, we will have removed one peer. + // Having less outbound-only peers than minimum won't cause panic when the outbound-only peer is being considered for disconnection. + peer_manager.heartbeat(); + assert_eq!( + peer_manager.network_globals.connected_or_dialing_peers(), + 20 + ); + } + + #[tokio::test] + async fn test_peer_manager_remove_unhealthy_peers_brings_peers_below_target() { + let mut peer_manager = build_peer_manager(3).await; + + // Create 4 peers to connect to. + // One pair will be unhealthy inbound only and outbound only peers. + let peer0 = PeerId::random(); + let peer1 = PeerId::random(); + let inbound_only_peer1 = PeerId::random(); + let outbound_only_peer1 = PeerId::random(); + + peer_manager.inject_connect_ingoing(&peer0, "/ip4/0.0.0.0/tcp/8000".parse().unwrap(), None); + peer_manager.inject_connect_ingoing(&peer1, "/ip4/0.0.0.0/tcp/8000".parse().unwrap(), None); + + // Connect to two peers that are on the threshold of being disconnected. + peer_manager.inject_connect_ingoing( + &inbound_only_peer1, + "/ip4/0.0.0.0/tcp/8000".parse().unwrap(), + None, + ); + peer_manager.inject_connect_outgoing( + &outbound_only_peer1, + "/ip4/0.0.0.0/tcp/8000".parse().unwrap(), + None, + ); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(inbound_only_peer1)) + .unwrap() + .add_to_score(-19.8); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(outbound_only_peer1)) + .unwrap() + .add_to_score(-19.8); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(inbound_only_peer1)) + .unwrap() + .set_gossipsub_score(-85.0); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(outbound_only_peer1)) + .unwrap() + .set_gossipsub_score(-85.0); + peer_manager.heartbeat(); + // Tests that when we are over the target peer limit, after disconnecting one unhealthy peer, + // the loop to check for disconnecting peers will stop because we have removed enough peers (only needed to remove 1 to reach target). + assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); + } + + #[tokio::test] + async fn test_peer_manager_removes_enough_peers_when_one_is_unhealthy() { + let mut peer_manager = build_peer_manager(3).await; + + // Create 5 peers to connect to. + // One will be unhealthy inbound only and outbound only peers. + let peer0 = PeerId::random(); + let peer1 = PeerId::random(); + let peer2 = PeerId::random(); + let inbound_only_peer1 = PeerId::random(); + let outbound_only_peer1 = PeerId::random(); + + peer_manager.inject_connect_ingoing(&peer0, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_ingoing(&peer1, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_ingoing(&peer2, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_manager.inject_connect_outgoing( + &outbound_only_peer1, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + // Have one peer be on the verge of disconnection. + peer_manager.inject_connect_ingoing( + &inbound_only_peer1, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(inbound_only_peer1)) + .unwrap() + .add_to_score(-19.9); + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&(inbound_only_peer1)) + .unwrap() + .set_gossipsub_score(-85.0); + + peer_manager.heartbeat(); + // Tests that when we are over the target peer limit, after disconnecting an unhealthy peer, + // the number of connected peers updates and we will not remove too many peers. + assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); + } +} diff --git a/node/network/src/peer_manager/network_behaviour.rs b/node/network/src/peer_manager/network_behaviour.rs new file mode 100644 index 0000000..da5c0a3 --- /dev/null +++ b/node/network/src/peer_manager/network_behaviour.rs @@ -0,0 +1,251 @@ +use std::task::{Context, Poll}; + +use futures::StreamExt; +use libp2p::core::connection::ConnectionId; +use libp2p::core::ConnectedPoint; +use libp2p::swarm::handler::DummyConnectionHandler; +use libp2p::swarm::{ + ConnectionHandler, DialError, NetworkBehaviour, NetworkBehaviourAction, PollParameters, +}; +use libp2p::{Multiaddr, PeerId}; + +use crate::metrics; +use crate::rpc::GoodbyeReason; + +use super::peerdb::BanResult; +use super::{PeerManager, PeerManagerEvent, ReportSource}; + +impl NetworkBehaviour for PeerManager { + type ConnectionHandler = DummyConnectionHandler; + + type OutEvent = PeerManagerEvent; + + /* Required trait members */ + + fn new_handler(&mut self) -> Self::ConnectionHandler { + DummyConnectionHandler::default() + } + + fn inject_event( + &mut self, + _: PeerId, + _: ConnectionId, + _: ::OutEvent, + ) { + unreachable!("Dummy handler does not emit events") + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + _params: &mut impl PollParameters, + ) -> Poll> { + // perform the heartbeat when necessary + while self.heartbeat.poll_tick(cx).is_ready() { + self.heartbeat(); + } + + // poll the timeouts for pings and status' + loop { + match self.inbound_ping_peers.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(peer_id))) => { + self.inbound_ping_peers.insert(peer_id); + self.events.push(PeerManagerEvent::Ping(peer_id)); + } + Poll::Ready(Some(Err(e))) => { + error!(error = %e.to_string(), "Failed to check for inbound peers to ping") + } + Poll::Ready(None) | Poll::Pending => break, + } + } + + loop { + match self.outbound_ping_peers.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(peer_id))) => { + self.outbound_ping_peers.insert(peer_id); + self.events.push(PeerManagerEvent::Ping(peer_id)); + } + Poll::Ready(Some(Err(e))) => { + error!(error = %e.to_string(), "Failed to check for outbound peers to ping") + } + Poll::Ready(None) | Poll::Pending => break, + } + } + + loop { + match self.status_peers.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(peer_id))) => { + self.status_peers.insert(peer_id); + self.events.push(PeerManagerEvent::Status(peer_id)) + } + Poll::Ready(Some(Err(e))) => { + error!(error = %e.to_string(), "Failed to check for peers to ping") + } + Poll::Ready(None) | Poll::Pending => break, + } + } + + if !self.events.is_empty() { + return Poll::Ready(NetworkBehaviourAction::GenerateEvent(self.events.remove(0))); + } else { + self.events.shrink_to_fit(); + } + + Poll::Pending + } + + /* Overwritten trait members */ + + fn inject_connection_established( + &mut self, + peer_id: &PeerId, + _connection_id: &ConnectionId, + endpoint: &ConnectedPoint, + _failed_addresses: Option<&Vec>, + other_established: usize, + ) { + debug!(%peer_id, connection = ?endpoint.to_endpoint(), "Connection established"); + if other_established == 0 { + // self.events.push(PeerManagerEvent::MetaData(*peer_id)); + } + + // Check NAT if metrics are enabled + if self.network_globals.local_enr.read().udp().is_some() { + metrics::check_nat(); + } + + // Check to make sure the peer is not supposed to be banned + match self.ban_status(peer_id) { + // TODO: directly emit the ban event? + BanResult::BadScore => { + // This is a faulty state + error!(%peer_id, "Connected to a banned peer, re-banning"); + // Reban the peer + self.goodbye_peer(peer_id, GoodbyeReason::Banned, ReportSource::PeerManager); + return; + } + BanResult::BannedIp(ip_addr) => { + // A good peer has connected to us via a banned IP address. We ban the peer and + // prevent future connections. + debug!(%peer_id, banned_ip = %ip_addr, "Peer connected via banned IP. Banning"); + self.goodbye_peer(peer_id, GoodbyeReason::BannedIP, ReportSource::PeerManager); + return; + } + BanResult::NotBanned => {} + } + + // Count dialing peers in the limit if the peer dialied us. + let count_dialing = endpoint.is_listener(); + // Check the connection limits + if self.peer_limit_reached(count_dialing) + && self + .network_globals + .peers + .read() + .peer_info(peer_id) + .map_or(true, |peer| !peer.has_future_duty()) + { + // Gracefully disconnect the peer. + self.disconnect_peer(*peer_id, GoodbyeReason::TooManyPeers); + return; + } + + // NOTE: We don't register peers that we are disconnecting immediately. The network service + // does not need to know about these peers. + match endpoint { + ConnectedPoint::Listener { send_back_addr, .. } => { + self.inject_connect_ingoing(peer_id, send_back_addr.clone(), None); + self.events + .push(PeerManagerEvent::PeerConnectedIncoming(*peer_id)); + } + ConnectedPoint::Dialer { address, .. } => { + self.inject_connect_outgoing(peer_id, address.clone(), None); + self.events + .push(PeerManagerEvent::PeerConnectedOutgoing(*peer_id)); + } + } + + // increment prometheus metrics + self.update_connected_peer_metrics(); + metrics::inc_counter(&metrics::PEER_CONNECT_EVENT_COUNT); + } + fn inject_connection_closed( + &mut self, + peer_id: &PeerId, + _: &ConnectionId, + _: &ConnectedPoint, + _: DummyConnectionHandler, + remaining_established: usize, + ) { + if remaining_established > 0 { + return; + } + + // There are no more connections + if self + .network_globals + .peers + .read() + .is_connected_or_disconnecting(peer_id) + { + // We are disconnecting the peer or the peer has already been connected. + // Both these cases, the peer has been previously registered by the peer manager and + // potentially the application layer. + // Inform the application. + self.events + .push(PeerManagerEvent::PeerDisconnected(*peer_id)); + debug!(%peer_id, "Peer disconnected"); + } + + // NOTE: It may be the case that a rejected node, due to too many peers is disconnected + // here and the peer manager has no knowledge of its connection. We insert it here for + // reference so that peer manager can track this peer. + self.inject_disconnect(peer_id); + + // Update the prometheus metrics + self.update_connected_peer_metrics(); + metrics::inc_counter(&metrics::PEER_DISCONNECT_EVENT_COUNT); + } + + fn inject_address_change( + &mut self, + _peer_id: &PeerId, + _connection_id: &ConnectionId, + old: &ConnectedPoint, + new: &ConnectedPoint, + ) { + debug_assert!( + matches!( + (old, new), + ( + // inbound remains inbound + ConnectedPoint::Listener { .. }, + ConnectedPoint::Listener { .. } + ) | ( + // outbound remains outbound + ConnectedPoint::Dialer { .. }, + ConnectedPoint::Dialer { .. } + ) + ), + "A peer has changed between inbound and outbound" + ) + } + + /// A dial attempt has failed. + /// + /// NOTE: It can be the case that we are dialing a peer and during the dialing process the peer + /// connects and the dial attempt later fails. To handle this, we only update the peer_db if + /// the peer is not already connected. + fn inject_dial_failure( + &mut self, + peer_id: Option, + _handler: DummyConnectionHandler, + _error: &DialError, + ) { + if let Some(peer_id) = peer_id { + if !self.network_globals.peers.read().is_connected(&peer_id) { + self.inject_disconnect(&peer_id); + } + } + } +} diff --git a/node/network/src/peer_manager/peerdb.rs b/node/network/src/peer_manager/peerdb.rs new file mode 100644 index 0000000..ef82990 --- /dev/null +++ b/node/network/src/peer_manager/peerdb.rs @@ -0,0 +1,1930 @@ +use crate::{ + metrics, + multiaddr::{Multiaddr, Protocol}, + Enr, Gossipsub, PeerId, +}; +use peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo}; +use rand::seq::SliceRandom; +use score::{PeerAction, ReportSource, Score, ScoreState}; +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; +use std::net::{IpAddr, SocketAddr}; +use std::time::Instant; +use sync_status::SyncStatus; + +pub mod client; +pub mod peer_info; +pub mod score; +pub mod sync_status; + +/// Max number of disconnected nodes to remember. +const MAX_DC_PEERS: usize = 500; +/// The maximum number of banned nodes to remember. +pub const MAX_BANNED_PEERS: usize = 1000; +/// We ban an IP if there are more than `BANNED_PEERS_PER_IP_THRESHOLD` banned peers with this IP. +const BANNED_PEERS_PER_IP_THRESHOLD: usize = 5; +/// Relative factor of peers that are allowed to have a negative gossipsub score without penalizing +/// them in lighthouse. +const ALLOWED_NEGATIVE_GOSSIPSUB_FACTOR: f32 = 0.1; +/// The time we allow peers to be in the dialing state in our PeerDb before we revert them to a +/// disconnected state. +const DIAL_TIMEOUT: u64 = 15; + +/// Storage of known peers, their reputation and information +pub struct PeerDB { + /// The collection of known connected peers, their status and reputation + peers: HashMap, + /// The number of disconnected nodes in the database. + disconnected_peers: usize, + /// Counts banned peers in total and per ip + banned_peers_count: BannedPeersCount, +} + +impl PeerDB { + pub fn new(trusted_peers: Vec) -> Self { + // Initialize the peers hashmap with trusted peers + let peers = trusted_peers + .into_iter() + .map(|peer_id| (peer_id, PeerInfo::trusted_peer_info())) + .collect(); + Self { + disconnected_peers: 0, + banned_peers_count: BannedPeersCount::default(), + peers, + } + } + + /* Getters */ + + /// Gives the score of a peer, or default score if it is unknown. + pub fn score(&self, peer_id: &PeerId) -> f64 { + self.peers + .get(peer_id) + .map_or(&Score::default(), |info| info.score()) + .score() + } + + /// Returns an iterator over all peers in the db. + pub fn peers(&self) -> impl Iterator { + self.peers.iter() + } + + /// Gives the ids of all known peers. + pub fn peer_ids(&self) -> impl Iterator { + self.peers.keys() + } + + /// Returns a peer's info, if known. + pub fn peer_info(&self, peer_id: &PeerId) -> Option<&PeerInfo> { + self.peers.get(peer_id) + } + + /// Returns a mutable reference to a peer's info if known. + // VISIBILITY: The peer manager is able to modify some elements of the peer info, such as sync + // status. + pub(super) fn peer_info_mut(&mut self, peer_id: &PeerId) -> Option<&mut PeerInfo> { + self.peers.get_mut(peer_id) + } + + /// Returns if the peer is already connected. + pub fn is_connected(&self, peer_id: &PeerId) -> bool { + matches!( + self.connection_status(peer_id), + Some(PeerConnectionStatus::Connected { .. }) + ) + } + + /// If we are connected or currently dialing the peer returns true. + pub fn is_connected_or_dialing(&self, peer_id: &PeerId) -> bool { + matches!( + self.connection_status(peer_id), + Some(PeerConnectionStatus::Connected { .. }) + | Some(PeerConnectionStatus::Dialing { .. }) + ) + } + + /// If we are connected or in the process of disconnecting + pub fn is_connected_or_disconnecting(&self, peer_id: &PeerId) -> bool { + matches!( + self.connection_status(peer_id), + Some(PeerConnectionStatus::Connected { .. }) + | Some(PeerConnectionStatus::Disconnecting { .. }) + ) + } + + /// Returns true if the peer should be dialed. This checks the connection state and the + /// score state and determines if the peer manager should dial this peer. + pub fn should_dial(&self, peer_id: &PeerId) -> bool { + matches!( + self.connection_status(peer_id), + Some(PeerConnectionStatus::Disconnected { .. }) + | Some(PeerConnectionStatus::Unknown { .. }) + | None + ) && !self.score_state_banned_or_disconnected(peer_id) + } + + /// Returns true if the peer is synced at least to our current head. + pub fn is_synced(&self, peer_id: &PeerId) -> bool { + match self.peers.get(peer_id).map(|info| info.sync_status()) { + Some(SyncStatus::Synced { .. }) => true, + Some(_) => false, + None => false, + } + } + + /// Returns the current [`BanResult`] of the peer. This doesn't check the connection state, rather the + /// underlying score of the peer. A peer may be banned but still in the connected state + /// temporarily. + /// + /// This is used to determine if we should accept incoming connections or not. + pub fn ban_status(&self, peer_id: &PeerId) -> BanResult { + if let Some(peer) = self.peers.get(peer_id) { + match peer.score_state() { + ScoreState::Banned => BanResult::BadScore, + _ => { + if let Some(ip) = self.ip_is_banned(peer) { + BanResult::BannedIp(ip) + } else { + BanResult::NotBanned + } + } + } + } else { + BanResult::NotBanned + } + } + + /// Checks if the peer's known addresses are currently banned. + fn ip_is_banned(&self, peer: &PeerInfo) -> Option { + peer.seen_ip_addresses() + .find(|ip| self.banned_peers_count.ip_is_banned(ip)) + } + + /// Returns true if the IP is banned. + pub fn is_ip_banned(&self, ip: &IpAddr) -> bool { + self.banned_peers_count.ip_is_banned(ip) + } + + /// Returns true if the Peer is either banned or in the disconnected state. + fn score_state_banned_or_disconnected(&self, peer_id: &PeerId) -> bool { + if let Some(peer) = self.peers.get(peer_id) { + match peer.score_state() { + ScoreState::Banned | ScoreState::Disconnected => true, + _ => self.ip_is_banned(peer).is_some(), + } + } else { + false + } + } + + /// Gives the ids and info of all known connected peers. + pub fn connected_peers(&self) -> impl Iterator { + self.peers.iter().filter(|(_, info)| info.is_connected()) + } + + /// Gives the ids of all known connected peers. + pub fn connected_peer_ids(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.is_connected()) + .map(|(peer_id, _)| peer_id) + } + + /// Connected or dialing peers + pub fn connected_or_dialing_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.is_connected() || info.is_dialing()) + .map(|(peer_id, _)| peer_id) + } + + /// Connected outbound-only peers + pub fn connected_outbound_only_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.is_outbound_only()) + .map(|(peer_id, _)| peer_id) + } + + /// Gives the `peer_id` of all known connected and synced peers. + pub fn synced_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| { + if info.sync_status().is_synced() || info.sync_status().is_advanced() { + return info.is_connected(); + } + false + }) + .map(|(peer_id, _)| peer_id) + } + + /// Gives the `peer_id` of all known connected and advanced peers. + pub fn advanced_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| { + if info.sync_status().is_advanced() { + return info.is_connected(); + } + false + }) + .map(|(peer_id, _)| peer_id) + } + + /// Gives the ids of all known disconnected peers. + pub fn disconnected_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.is_disconnected()) + .map(|(peer_id, _)| peer_id) + } + + /// Returns the ids of all known banned peers. + pub fn banned_peers(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.is_banned()) + .map(|(peer_id, _)| peer_id) + } + + /// Gives the ids of all known banned peers. + pub fn banned_peers_by_score(&self) -> impl Iterator { + self.peers + .iter() + .filter(|(_, info)| info.score_is_banned()) + .map(|(peer_id, _)| peer_id) + } + + /// Returns a vector of all connected peers sorted by score beginning with the worst scores. + /// Ties get broken randomly. + pub fn worst_connected_peers(&self) -> Vec<(&PeerId, &PeerInfo)> { + let mut connected = self + .peers + .iter() + .filter(|(_, info)| info.is_connected()) + .collect::>(); + + connected.shuffle(&mut rand::thread_rng()); + connected.sort_by_key(|(_, info)| info.score()); + connected + } + + /// Returns a vector containing peers (their ids and info), sorted by + /// score from highest to lowest, and filtered using `is_status` + pub fn best_peers_by_status(&self, is_status: F) -> Vec<(&PeerId, &PeerInfo)> + where + F: Fn(&PeerInfo) -> bool, + { + let mut by_status = self + .peers + .iter() + .filter(|(_, info)| is_status(info)) + .collect::>(); + by_status.sort_by_key(|(_, info)| info.score()); + by_status.into_iter().rev().collect() + } + + /// Returns the peer with highest reputation that satisfies `is_status` + pub fn best_by_status(&self, is_status: F) -> Option<&PeerId> + where + F: Fn(&PeerInfo) -> bool, + { + self.peers + .iter() + .filter(|(_, info)| is_status(info)) + .max_by_key(|(_, info)| info.score()) + .map(|(id, _)| id) + } + + /// Returns the peer's connection status. Returns unknown if the peer is not in the DB. + pub fn connection_status(&self, peer_id: &PeerId) -> Option { + self.peer_info(peer_id) + .map(|info| info.connection_status().clone()) + } + + /* Mutability */ + + /// Cleans up the connection state of dialing peers. + // Libp2p dial's peerids, but sometimes the response is from another peer-id or libp2p + // returns dial errors without a peer-id attached. This function reverts peers that have a + // dialing status longer than DIAL_TIMEOUT seconds to a disconnected status. This is important because + // we count the number of dialing peers in our inbound connections. + pub fn cleanup_dialing_peers(&mut self) { + let peers_to_disconnect: Vec<_> = self + .peers + .iter() + .filter_map(|(peer_id, info)| { + if let PeerConnectionStatus::Dialing { since } = info.connection_status() { + if (*since) + std::time::Duration::from_secs(DIAL_TIMEOUT) + < std::time::Instant::now() + { + return Some(*peer_id); + } + } + None + }) + .collect(); + + for peer_id in peers_to_disconnect { + self.update_connection_state(&peer_id, NewConnectionState::Disconnected); + } + } + + /// Allows the sync module to update sync status' of peers. Returns None, if the peer doesn't + /// exist and returns Some(bool) representing if the sync state was modified. + pub fn update_sync_status( + &mut self, + peer_id: &PeerId, + sync_status: SyncStatus, + ) -> Option { + let info = self.peers.get_mut(peer_id)?; + Some(info.update_sync_status(sync_status)) + } + + /// Updates the scores of known peers according to their connection status and the time that + /// has passed. This function returns a list of peers that have been unbanned. + /// NOTE: Peer scores cannot be penalized during the update, they can only increase. Therefore + /// it not possible to ban peers when updating scores. + #[must_use = "The unbanned peers must be sent to libp2p"] + pub(super) fn update_scores(&mut self) -> Vec<(PeerId, ScoreUpdateResult)> { + // Peer can be unbanned in this process. + // We return the result, such that the peer manager can inform the swarm to lift the libp2p + // ban on these peers. + let mut peers_to_unban = Vec::new(); + let mut result = Vec::new(); + + for (peer_id, info) in self.peers.iter_mut() { + let previous_state = info.score_state(); + // Update scores + info.score_update(); + + match Self::handle_score_transition(previous_state, peer_id, info) { + // A peer should not be able to be banned from a score update. + ScoreTransitionResult::Banned => { + error!(peer_id = %peer_id, "Peer has been banned in an update") + } + // A peer should not be able to transition to a disconnected state from a healthy + // state in a score update. + ScoreTransitionResult::Disconnected => { + error!(peer_id = %peer_id, "Peer has been disconnected in an update") + } + ScoreTransitionResult::Unbanned => { + peers_to_unban.push(*peer_id); + } + ScoreTransitionResult::NoAction => {} + } + } + + // Update the state in the peerdb + for unbanned_peer in peers_to_unban { + self.update_connection_state(&unbanned_peer, NewConnectionState::Unbanned); + let seen_ip_addresses = self + .peers + .get(&unbanned_peer) + .map(|info| { + info.seen_ip_addresses() + .filter(|ip| !self.is_ip_banned(ip)) + .collect::>() + }) + .unwrap_or_default(); + result.push(( + unbanned_peer, + ScoreUpdateResult::Unbanned(seen_ip_addresses), + )); + } + // Return the list so that the peer manager can update libp2p + result + } + + /// Updates gossipsub scores for all peers. + #[must_use = "Score updates need to be reported to libp2p"] + pub(super) fn update_gossipsub_scores( + &mut self, + target_peers: usize, + gossipsub: &Gossipsub, + ) -> Vec<(PeerId, ScoreUpdateResult)> { + let mut actions = Vec::new(); + let mut results = Vec::new(); + + let mut peers: Vec<_> = self + .peers + .iter_mut() + .filter(|(_peer_id, info)| info.is_connected()) + .filter_map(|(peer_id, info)| { + gossipsub + .peer_score(peer_id) + .map(|score| (peer_id, info, score)) + }) + .collect(); + + // sort descending by score + peers.sort_unstable_by(|(.., s1), (.., s2)| s2.partial_cmp(s1).unwrap_or(Ordering::Equal)); + + let mut to_ignore_negative_peers = + (target_peers as f32 * ALLOWED_NEGATIVE_GOSSIPSUB_FACTOR).ceil() as usize; + + for (peer_id, info, score) in peers { + let previous_state = info.score_state(); + info.update_gossipsub_score( + score, + if score < 0.0 && to_ignore_negative_peers > 0 { + to_ignore_negative_peers -= 1; + // We ignore the negative score for the best negative peers so that their + // gossipsub score can recover without getting disconnected. + true + } else { + false + }, + ); + + actions.push(( + *peer_id, + Self::handle_score_transition(previous_state, peer_id, info), + )); + } + + for (peer_id, action) in actions { + let result = match action { + ScoreTransitionResult::Banned => { + // The peer was banned as a result of this action. + self.update_connection_state(&peer_id, NewConnectionState::Banned) + .into() + } + ScoreTransitionResult::Disconnected => { + // The peer needs to be disconnected + + // Update the state + self.update_connection_state( + &peer_id, + NewConnectionState::Disconnecting { to_ban: false }, + ); + ScoreUpdateResult::Disconnect + } + ScoreTransitionResult::NoAction => ScoreUpdateResult::NoAction, + ScoreTransitionResult::Unbanned => { + self.update_connection_state(&peer_id, NewConnectionState::Unbanned); + let seen_ip_addresses = self + .peers + .get(&peer_id) + .map(|info| { + info.seen_ip_addresses() + .filter(|ip| !self.is_ip_banned(ip)) + .collect::>() + }) + .unwrap_or_default(); + + ScoreUpdateResult::Unbanned(seen_ip_addresses) + } + }; + + // Actions to be handled by the peer manager for each peer id + if !matches!(result, ScoreUpdateResult::NoAction) { + results.push((peer_id, result)); + } + } + results + } + + /// Reports a peer for some action. + /// + /// The action can only cause a negative effect. This can lead to disconnecting or banning a + /// specific peer. Therefore the result of this function returns if the peer needs to be banned + /// or disconnected. + /// + /// If the peer doesn't exist, log a warning and insert defaults. + #[must_use = "Banned and disconnected peers need to be handled in libp2p"] + pub(super) fn report_peer( + &mut self, + peer_id: &PeerId, + action: PeerAction, + source: ReportSource, + msg: &'static str, + ) -> ScoreUpdateResult { + metrics::inc_counter_vec(&metrics::REPORT_PEER_MSGS, &[msg]); + + match self.peers.get_mut(peer_id) { + Some(info) => { + let previous_state = info.score_state(); + info.apply_peer_action_to_score(action); + metrics::inc_counter_vec( + &metrics::PEER_ACTION_EVENTS_PER_CLIENT, + &[info.client().kind.as_ref(), action.as_ref(), source.into()], + ); + let result = Self::handle_score_transition(previous_state, peer_id, info); + if previous_state == info.score_state() { + debug!( + msg = %msg, + peer_id = %peer_id, + score = %info.score(), + "Peer score adjusted" + ); + } + match result { + ScoreTransitionResult::Banned => { + // The peer was banned as a result of this action. + self.update_connection_state(peer_id, NewConnectionState::Banned) + .into() + } + ScoreTransitionResult::Disconnected => { + // The peer needs to be disconnected + + // Update the state + self.update_connection_state( + peer_id, + NewConnectionState::Disconnecting { to_ban: false }, + ); + ScoreUpdateResult::Disconnect + } + ScoreTransitionResult::NoAction => ScoreUpdateResult::NoAction, + ScoreTransitionResult::Unbanned => { + error!( + msg = %msg, + peer_id = %peer_id, + "Report peer action lead to an unbanning", + ); + ScoreUpdateResult::NoAction + } + } + } + None => { + debug!( + msg = %msg, + peer_id =%peer_id, + "Reporting a peer that doesn't exist", + ); + ScoreUpdateResult::NoAction + } + } + } + + /// Update min ttl of a peer. + // VISIBILITY: Only the peer manager can update the min_ttl + pub(super) fn update_min_ttl(&mut self, peer_id: &PeerId, min_ttl: Instant) { + let info = self.peers.entry(*peer_id).or_default(); + + // only update if the ttl is longer + if info.min_ttl().is_none() || Some(&min_ttl) > info.min_ttl() { + info.set_min_ttl(min_ttl); + + let min_ttl_secs = min_ttl + .checked_duration_since(Instant::now()) + .map(|duration| duration.as_secs()) + .unwrap_or_else(|| 0); + + debug!( + peer_id = %peer_id, + future_min_ttl_secs = min_ttl_secs, + "Updating the time a peer is required for", + ); + } + } + + /// A peer is being dialed. + // VISIBILITY: Only the peer manager can adjust the connection state + pub(super) fn dialing_peer(&mut self, peer_id: &PeerId, enr: Option) { + self.update_connection_state(peer_id, NewConnectionState::Dialing { enr }); + } + + /// Sets a peer as connected with an ingoing connection. + // VISIBILITY: Only the peer manager can adjust the connection state. + pub(super) fn connect_ingoing( + &mut self, + peer_id: &PeerId, + seen_address: Multiaddr, + enr: Option, + ) { + self.update_connection_state( + peer_id, + NewConnectionState::Connected { + enr, + seen_address, + direction: ConnectionDirection::Incoming, + }, + ); + } + + /// Sets a peer as connected with an outgoing connection. + // VISIBILITY: Only the peer manager can adjust the connection state. + pub(super) fn connect_outgoing( + &mut self, + peer_id: &PeerId, + seen_address: Multiaddr, + enr: Option, + ) { + self.update_connection_state( + peer_id, + NewConnectionState::Connected { + enr, + seen_address, + direction: ConnectionDirection::Outgoing, + }, + ); + } + + /// The connection state of the peer has been changed. Modify the peer in the db to ensure all + /// variables are in sync with libp2p. + /// Updating the state can lead to a `BanOperation` which needs to be processed via the peer + /// manager and should be handled in the peer manager. + // NOTE: This function is vital in keeping the connection state, and thus the peerdb size in + // check and up to date with libp2p. + fn update_connection_state( + &mut self, + peer_id: &PeerId, + new_state: NewConnectionState, + ) -> Option { + let info = self.peers.entry(*peer_id).or_insert_with(|| { + // If we are not creating a new connection (or dropping a current inbound connection) log a warning indicating we are updating a + // connection state for an unknown peer. + if !matches!( + new_state, + NewConnectionState::Connected { .. } // We have established a new connection (peer may not have been seen before) + | NewConnectionState::Disconnecting { .. }// We are disconnecting from a peer that may not have been registered before + | NewConnectionState::Dialing { .. } // We are dialing a potentially new peer + | NewConnectionState::Disconnected { .. } // Dialing a peer that responds by a different ID can be immediately + // disconnected without having being stored in the db before + ) { + warn!(peer_id = %peer_id, new_state = ?new_state, "Updating state of unknown peer"); + } + PeerInfo::default() + }); + + // Ban the peer if the score is not already low enough. + if matches!(new_state, NewConnectionState::Banned) { + match info.score_state() { + ScoreState::Banned => {} + _ => { + // If score isn't low enough to ban, this function has been called incorrectly. + error!(peer_id = %peer_id, "Banning a peer with a good score"); + info.apply_peer_action_to_score(score::PeerAction::Fatal); + } + } + } + + // Handle all the possible state changes + match (info.connection_status().clone(), new_state) { + /* CONNECTED + * + * + * Handles the transition to a connected state + */ + ( + current_state, + NewConnectionState::Connected { + enr, + direction, + seen_address, + }, + ) => { + // Update the ENR if one exists + if let Some(enr) = enr { + info.set_enr(enr); + } + + match current_state { + PeerConnectionStatus::Disconnected { .. } => { + self.disconnected_peers = self.disconnected_peers.saturating_sub(1); + } + PeerConnectionStatus::Banned { .. } => { + error!(peer_id = %peer_id, "Accepted a connection from a banned peer"); + // TODO: check if this happens and report the unban back + self.banned_peers_count + .remove_banned_peer(info.seen_ip_addresses()); + } + PeerConnectionStatus::Disconnecting { .. } => { + warn!(peer_id = %peer_id, "Connected to a disconnecting peer") + } + PeerConnectionStatus::Unknown + | PeerConnectionStatus::Connected { .. } + | PeerConnectionStatus::Dialing { .. } => {} + } + + // Add the seen ip address and port to the peer's info + let socket_addr = match seen_address.iter().fold( + (None, None), + |(found_ip, found_port), protocol| match protocol { + Protocol::Ip4(ip) => (Some(ip.into()), found_port), + Protocol::Ip6(ip) => (Some(ip.into()), found_port), + Protocol::Tcp(port) => (found_ip, Some(port)), + _ => (found_ip, found_port), + }, + ) { + (Some(ip), Some(port)) => Some(SocketAddr::new(ip, port)), + (Some(_ip), None) => { + error!(peer_id = %peer_id, "Connected peer has an IP but no TCP port"); + None + } + _ => None, + }; + + // Update the connection state + match direction { + ConnectionDirection::Incoming => info.connect_ingoing(socket_addr), + ConnectionDirection::Outgoing => info.connect_outgoing(socket_addr), + } + } + + /* DIALING + * + * + * Handles the transition to a dialing state + */ + (old_state, NewConnectionState::Dialing { enr }) => { + match old_state { + PeerConnectionStatus::Banned { .. } => { + warn!(peer_id = %peer_id, "Dialing a banned peer"); + self.banned_peers_count + .remove_banned_peer(info.seen_ip_addresses()); + } + PeerConnectionStatus::Disconnected { .. } => { + self.disconnected_peers = self.disconnected_peers.saturating_sub(1); + } + PeerConnectionStatus::Connected { .. } => { + warn!(peer_id = %peer_id, "Dialing an already connected peer") + } + PeerConnectionStatus::Dialing { .. } => { + warn!(peer_id = %peer_id, "Dialing an already dialing peer") + } + PeerConnectionStatus::Disconnecting { .. } => { + warn!(peer_id = %peer_id, "Dialing a disconnecting peer") + } + PeerConnectionStatus::Unknown => {} // default behaviour + } + // Update the ENR if one is known. + if let Some(enr) = enr { + info.set_enr(enr); + } + + if let Err(e) = info.set_dialing_peer() { + error!(peer_id = %peer_id, "{}", e); + } + } + + /* DISCONNECTED + * + * + * Handle the transition to the disconnected state + */ + (old_state, NewConnectionState::Disconnected) => { + // Remove all subnets for disconnected peers. + // info.clear_subnets(); + + match old_state { + PeerConnectionStatus::Banned { .. } => {} + PeerConnectionStatus::Disconnected { .. } => {} + PeerConnectionStatus::Disconnecting { to_ban } if to_ban => { + // Update the status. + info.set_connection_status(PeerConnectionStatus::Banned { + since: Instant::now(), + }); + self.banned_peers_count + .add_banned_peer(info.seen_ip_addresses()); + let known_banned_ips = self.banned_peers_count.banned_ips(); + let banned_ips = info + .seen_ip_addresses() + .filter(|ip| known_banned_ips.contains(ip)) + .collect::>(); + return Some(BanOperation::ReadyToBan(banned_ips)); + } + PeerConnectionStatus::Disconnecting { .. } + | PeerConnectionStatus::Unknown + | PeerConnectionStatus::Connected { .. } + | PeerConnectionStatus::Dialing { .. } => { + self.disconnected_peers += 1; + info.set_connection_status(PeerConnectionStatus::Disconnected { + since: Instant::now(), + }); + } + } + } + + /* DISCONNECTING + * + * + * Handles the transition to a disconnecting state + */ + (PeerConnectionStatus::Banned { .. }, NewConnectionState::Disconnecting { to_ban }) => { + error!(peer_id = %peer_id, "Disconnecting from a banned peer"); + info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban }); + } + ( + PeerConnectionStatus::Disconnected { .. }, + NewConnectionState::Disconnecting { to_ban }, + ) => { + // If the peer was previously disconnected and is now being disconnected, decrease + // the disconnected_peers counter. + self.disconnected_peers = self.disconnected_peers.saturating_sub(1); + info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban }); + } + (_, NewConnectionState::Disconnecting { to_ban }) => { + // We overwrite all states and set this peer to be disconnecting. + // NOTE: A peer can be in the disconnected state and transition straight to a + // disconnected state. This occurs when a disconnected peer dials us, we have too + // many peers and we transition them straight to the disconnecting state. + info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban }); + } + + /* BANNED + * + * + * Handles the transition to a banned state + */ + (PeerConnectionStatus::Disconnected { .. }, NewConnectionState::Banned) => { + // It is possible to ban a peer that is currently disconnected. This can occur when + // there are many events that score it poorly and are processed after it has disconnected. + info.set_connection_status(PeerConnectionStatus::Banned { + since: Instant::now(), + }); + self.banned_peers_count + .add_banned_peer(info.seen_ip_addresses()); + self.disconnected_peers = self.disconnected_peers.saturating_sub(1); + let known_banned_ips = self.banned_peers_count.banned_ips(); + let banned_ips = info + .seen_ip_addresses() + .filter(|ip| known_banned_ips.contains(ip)) + .collect::>(); + return Some(BanOperation::ReadyToBan(banned_ips)); + } + (PeerConnectionStatus::Disconnecting { .. }, NewConnectionState::Banned) => { + // NOTE: This can occur due a rapid downscore of a peer. It goes through the + // disconnection phase and straight into banning in a short time-frame. + debug!(peer_id = %peer_id, "Banning peer that is currently disconnecting"); + // Ban the peer once the disconnection process completes. + info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban: true }); + return Some(BanOperation::PeerDisconnecting); + } + (PeerConnectionStatus::Banned { .. }, NewConnectionState::Banned) => { + error!(peer_id = %peer_id, "Banning already banned peer"); + let known_banned_ips = self.banned_peers_count.banned_ips(); + let banned_ips = info + .seen_ip_addresses() + .filter(|ip| known_banned_ips.contains(ip)) + .collect::>(); + return Some(BanOperation::ReadyToBan(banned_ips)); + } + ( + PeerConnectionStatus::Connected { .. } | PeerConnectionStatus::Dialing { .. }, + NewConnectionState::Banned, + ) => { + // update the state + info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban: true }); + return Some(BanOperation::DisconnectThePeer); + } + (PeerConnectionStatus::Unknown, NewConnectionState::Banned) => { + // shift the peer straight to banned + warn!(peer_id = %peer_id, "Banning a peer of unknown connection state"); + self.banned_peers_count + .add_banned_peer(info.seen_ip_addresses()); + info.set_connection_status(PeerConnectionStatus::Banned { + since: Instant::now(), + }); + let known_banned_ips = self.banned_peers_count.banned_ips(); + let banned_ips = info + .seen_ip_addresses() + .filter(|ip| known_banned_ips.contains(ip)) + .collect::>(); + return Some(BanOperation::ReadyToBan(banned_ips)); + } + + /* UNBANNED + * + * + * Handles the transition to an unbanned state + */ + (old_state, NewConnectionState::Unbanned) => { + if matches!(info.score_state(), ScoreState::Banned) { + error!(peer_id = %peer_id, "Unbanning a banned peer"); + } + match old_state { + PeerConnectionStatus::Unknown | PeerConnectionStatus::Connected { .. } => { + error!(peer_id = %peer_id, "Unbanning a connected peer"); + } + PeerConnectionStatus::Disconnected { .. } + | PeerConnectionStatus::Disconnecting { .. } => { + debug!(peer_id = %peer_id, "Unbanning disconnected or disconnecting peer"); + } // These are odd but fine. + PeerConnectionStatus::Dialing { .. } => {} // Also odd but acceptable + PeerConnectionStatus::Banned { since } => { + info.set_connection_status(PeerConnectionStatus::Disconnected { since }); + + // Increment the disconnected count and reduce the banned count + self.banned_peers_count + .remove_banned_peer(info.seen_ip_addresses()); + self.disconnected_peers = self.disconnected_peers.saturating_add(1); + } + } + } + } + None + } + + /// Sets the peer as disconnected. A banned peer remains banned. If the node has become banned, + /// this returns true, otherwise this is false. + // VISIBILITY: Only the peer manager can adjust the connection state. + pub(super) fn inject_disconnect( + &mut self, + peer_id: &PeerId, + ) -> (Option, Vec<(PeerId, Vec)>) { + // A peer can be banned for disconnecting. Thus another peer could be purged + let maybe_ban_op = self.update_connection_state(peer_id, NewConnectionState::Disconnected); + let purged_peers = self.shrink_to_fit(); + (maybe_ban_op, purged_peers) + } + + /// The peer manager has notified us that the peer is undergoing a normal disconnect. Optionally tag + /// the peer to be banned after the disconnect. + // VISIBILITY: Only the peer manager can adjust the connection state. + pub(super) fn notify_disconnecting(&mut self, peer_id: &PeerId, to_ban: bool) { + self.update_connection_state(peer_id, NewConnectionState::Disconnecting { to_ban }); + } + + /// Removes banned and disconnected peers from the DB if we have reached any of our limits. + /// Drops the peers with the lowest reputation so that the number of disconnected peers is less + /// than MAX_DC_PEERS + #[must_use = "Unbanned peers need to be reported to libp2p."] + fn shrink_to_fit(&mut self) -> Vec<(PeerId, Vec)> { + let excess_peers = self + .banned_peers_count + .banned_peers() + .saturating_sub(MAX_BANNED_PEERS); + let mut unbanned_peers = Vec::with_capacity(excess_peers); + + // Remove excess banned peers + while self.banned_peers_count.banned_peers() > MAX_BANNED_PEERS { + if let Some((to_drop, unbanned_ips)) = if let Some((id, info, _)) = self + .peers + .iter() + .filter_map(|(id, info)| match info.connection_status() { + PeerConnectionStatus::Banned { since } => Some((id, info, since)), + _ => None, + }) + .min_by_key(|(_, _, since)| *since) + { + self.banned_peers_count + .remove_banned_peer(info.seen_ip_addresses()); + let unbanned_ips = info + .seen_ip_addresses() + .filter(|ip| !self.is_ip_banned(ip)) + .collect::>(); + + Some((*id, unbanned_ips)) + } else { + // If there is no minimum, this is a coding error. + error!("banned_peers > MAX_BANNED_PEERS despite no banned peers in db!"); + // reset banned_peers this will also exit the loop + self.banned_peers_count = BannedPeersCount::default(); + None + } { + debug!(peer_id = %to_drop, "Removing old banned peer"); + self.peers.remove(&to_drop); + unbanned_peers.push((to_drop, unbanned_ips)) + } + } + + // Remove excess disconnected peers + while self.disconnected_peers > MAX_DC_PEERS { + if let Some(to_drop) = self + .peers + .iter() + .filter(|(_, info)| info.is_disconnected()) + .filter_map(|(id, info)| match info.connection_status() { + PeerConnectionStatus::Disconnected { since } => Some((id, since)), + _ => None, + }) + .min_by_key(|(_, age)| *age) + .map(|(id, _)| *id) + { + debug!(peer_id = %to_drop, disconnected_size = self.disconnected_peers.saturating_sub(1), "Removing old disconnected peer"); + self.peers.remove(&to_drop); + } + // If there is no minimum, this is a coding error. For safety we decrease + // the count to avoid a potential infinite loop. + self.disconnected_peers = self.disconnected_peers.saturating_sub(1); + } + + unbanned_peers + } + + /// This handles score transitions between states. It transitions peers states from + /// disconnected/banned/connected. + fn handle_score_transition( + previous_state: ScoreState, + peer_id: &PeerId, + info: &PeerInfo, + ) -> ScoreTransitionResult { + match (info.score_state(), previous_state) { + (ScoreState::Banned, ScoreState::Healthy | ScoreState::Disconnected) => { + debug!(peer_id = %peer_id, score = %info.score(), "Peer has been banned"); + ScoreTransitionResult::Banned + } + (ScoreState::Disconnected, ScoreState::Banned | ScoreState::Healthy) => { + debug!(peer_id = %peer_id, score = %info.score(), past_state = %previous_state, "Peer transitioned to disconnect state"); + // disconnect the peer if it's currently connected or dialing + if info.is_connected_or_dialing() { + ScoreTransitionResult::Disconnected + } else if previous_state == ScoreState::Banned { + ScoreTransitionResult::Unbanned + } else { + // The peer was healthy, but is already disconnected, so there is no action to + // take. + ScoreTransitionResult::NoAction + } + } + (ScoreState::Healthy, ScoreState::Disconnected) => { + debug!(peer_id = %peer_id, score = %info.score(), past_state = %previous_state, "Peer transitioned to healthy state"); + ScoreTransitionResult::NoAction + } + (ScoreState::Healthy, ScoreState::Banned) => { + debug!(peer_id = %peer_id, score = %info.score(), past_state = %previous_state, "Peer transitioned to healthy state"); + // unban the peer if it was previously banned. + ScoreTransitionResult::Unbanned + } + // Explicitly ignore states that haven't transitioned. + (ScoreState::Healthy, ScoreState::Healthy) => ScoreTransitionResult::NoAction, + (ScoreState::Disconnected, ScoreState::Disconnected) => ScoreTransitionResult::NoAction, + + (ScoreState::Banned, ScoreState::Banned) => ScoreTransitionResult::NoAction, + } + } +} + +/// Internal enum for managing connection state transitions. +#[derive(Debug)] +enum NewConnectionState { + /// A peer has connected to us. + Connected { + /// An optional known ENR if the peer was dialed. + enr: Option, + /// The seen socket address associated with the connection. + seen_address: Multiaddr, + /// The direction, incoming/outgoing. + direction: ConnectionDirection, + }, + /// The peer is in the process of being disconnected. + Disconnecting { + /// Whether the peer should be banned after the disconnect occurs. + to_ban: bool, + }, + /// We are dialing this peer. + Dialing { + /// An optional known ENR for the peer we are dialing. + enr: Option, + }, + /// The peer has been disconnected from our local node. + Disconnected, + /// The peer has been banned and actions to shift the peer to the banned state should be + /// undertaken + Banned, + /// The peer has been unbanned and the connection state should be updated to reflect this. + Unbanned, +} + +/// The result of applying a score transition to a peer. +enum ScoreTransitionResult { + /// The peer has become disconnected. + Disconnected, + /// The peer has been banned. + Banned, + /// The peer has been unbanned. + Unbanned, + /// No state change occurred. + NoAction, +} + +/// The type of results that can happen from executing the `report_peer` function. +pub enum ScoreUpdateResult { + /// The reported peer must be banned. + Ban(BanOperation), + /// The reported peer transitioned to the disconnected state and must be disconnected. + Disconnect, + /// The peer has been unbanned and this needs to be propagated to libp2p. The list of unbanned + /// IP addresses are sent along with it. + Unbanned(Vec), + /// The report requires no further action. + NoAction, +} + +impl From> for ScoreUpdateResult { + fn from(ban_operation: Option) -> Self { + match ban_operation { + None => ScoreUpdateResult::NoAction, + Some(bo) => ScoreUpdateResult::Ban(bo), + } + } +} + +/// When attempting to ban a peer provides the peer manager with the operation that must be taken. +pub enum BanOperation { + // The peer is currently connected. Perform a graceful disconnect before banning at the swarm + // level. + DisconnectThePeer, + // The peer is disconnected, it has now been banned and can be banned at the swarm level. It + // stores a collection of banned IP addresses to inform the swarm. + ReadyToBan(Vec), + // The peer is currently being disconnected, nothing to do. + PeerDisconnecting, +} + +/// When checking if a peer is banned, it can be banned for multiple reasons. +pub enum BanResult { + /// The peer's score is too low causing it to be banned. + BadScore, + /// The peer should be banned because it is connecting from a banned IP address. + BannedIp(IpAddr), + /// The peer is not banned. + NotBanned, +} + +// Helper function for unit tests +#[cfg(test)] +impl BanResult { + pub fn is_banned(&self) -> bool { + !matches!(self, BanResult::NotBanned) + } +} + +#[derive(Default)] +pub struct BannedPeersCount { + /// The number of banned peers in the database. + banned_peers: usize, + /// maps ips to number of banned peers with this ip + banned_peers_per_ip: HashMap, +} + +impl BannedPeersCount { + /// Removes the peer from the counts if it is banned. Returns true if the peer was banned and + /// false otherwise. + pub fn remove_banned_peer(&mut self, ip_addresses: impl Iterator) { + self.banned_peers = self.banned_peers.saturating_sub(1); + for address in ip_addresses { + if let Some(count) = self.banned_peers_per_ip.get_mut(&address) { + *count = count.saturating_sub(1); + } + } + } + + pub fn add_banned_peer(&mut self, ip_addresses: impl Iterator) { + self.banned_peers = self.banned_peers.saturating_add(1); + for address in ip_addresses { + *self.banned_peers_per_ip.entry(address).or_insert(0) += 1; + } + } + + pub fn banned_peers(&self) -> usize { + self.banned_peers + } + + pub fn banned_ips(&self) -> HashSet { + self.banned_peers_per_ip + .iter() + .filter(|(_ip, count)| **count > BANNED_PEERS_PER_IP_THRESHOLD) + .map(|(ip, _count)| *ip) + .collect() + } + + /// An IP is considered banned if more than BANNED_PEERS_PER_IP_THRESHOLD banned peers + /// exist with this IP + pub fn ip_is_banned(&self, ip: &IpAddr) -> bool { + self.banned_peers_per_ip + .get(ip) + .map_or(false, |count| *count > BANNED_PEERS_PER_IP_THRESHOLD) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use libp2p::core::Multiaddr; + use std::net::{Ipv4Addr, Ipv6Addr}; + + fn add_score(db: &mut PeerDB, peer_id: &PeerId, score: f64) { + if let Some(info) = db.peer_info_mut(peer_id) { + info.add_to_score(score); + } + } + + fn reset_score(db: &mut PeerDB, peer_id: &PeerId) { + if let Some(info) = db.peer_info_mut(peer_id) { + info.reset_score(); + } + } + + fn get_db() -> PeerDB { + PeerDB::new(vec![]) + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_peer_connected_successfully() { + let mut pdb = get_db(); + let random_peer = PeerId::random(); + + let (n_in, n_out) = (10, 20); + for _ in 0..n_in { + pdb.connect_ingoing(&random_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + } + for _ in 0..n_out { + pdb.connect_outgoing(&random_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + } + + // the peer is known + let peer_info = pdb.peer_info(&random_peer); + assert!(peer_info.is_some()); + // this is the only peer + assert_eq!(pdb.peers().count(), 1); + // the peer has the default reputation + assert_eq!(pdb.score(&random_peer), Score::default().score()); + // it should be connected, and therefore not counted as disconnected + assert_eq!(pdb.disconnected_peers, 0); + assert!(peer_info.unwrap().is_connected()); + assert_eq!(peer_info.unwrap().connections(), (n_in, n_out)); + } + + #[test] + fn test_outbound_only_peers_counted_correctly() { + let mut pdb = get_db(); + let p0 = PeerId::random(); + let p1 = PeerId::random(); + let p2 = PeerId::random(); + // Create peer with no connections. + let _p3 = PeerId::random(); + + pdb.connect_ingoing(&p0, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_ingoing(&p1, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_outgoing(&p1, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_outgoing(&p2, "/ip4/0.0.0.0".parse().unwrap(), None); + + // We should only have one outbound-only peer (p2). + // Peers that are inbound-only, have both types of connections, or no connections should not be counted. + assert_eq!(pdb.connected_outbound_only_peers().count(), 1); + } + + #[test] + fn test_disconnected_removed_in_correct_order() { + let mut pdb = get_db(); + + use std::collections::BTreeMap; + let mut peer_list = BTreeMap::new(); + for id in 0..MAX_DC_PEERS + 1 { + let new_peer = PeerId::random(); + pdb.connect_ingoing(&new_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_list.insert(id, new_peer); + } + assert_eq!(pdb.disconnected_peers, 0); + + for (_, p) in peer_list.iter() { + pdb.inject_disconnect(p); + // Allow the timing to update correctly + } + assert_eq!(pdb.disconnected_peers, MAX_DC_PEERS); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + // Only the oldest peer should have been removed + for (id, peer_id) in peer_list.iter().rev().take(MAX_DC_PEERS) { + println!("Testing id {}", id); + assert!( + pdb.peer_info(peer_id).is_some(), + "Latest peer should not be pruned" + ); + } + + assert!( + pdb.peer_info(peer_list.iter().next().unwrap().1).is_none(), + "First peer should be removed" + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + } + + #[test] + fn new_connection_should_remain() { + let mut pdb = get_db(); + + use std::collections::BTreeMap; + let mut peer_list = BTreeMap::new(); + for id in 0..MAX_DC_PEERS + 20 { + let new_peer = PeerId::random(); + pdb.connect_ingoing(&new_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_list.insert(id, new_peer); + } + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + for (_, p) in peer_list.iter() { + pdb.inject_disconnect(p); + } + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + println!("{}", pdb.disconnected_peers); + + peer_list.clear(); + for id in 0..MAX_DC_PEERS + 20 { + let new_peer = PeerId::random(); + pdb.connect_ingoing(&new_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + peer_list.insert(id, new_peer); + } + + let new_peer = PeerId::random(); + // New peer gets its min_ttl updated because it exists on a subnet + let min_ttl = Instant::now() + std::time::Duration::from_secs(12); + + pdb.update_min_ttl(&new_peer, min_ttl); + // Peer then gets dialed + pdb.dialing_peer(&new_peer, None); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + // Dialing fails, remove the peer + pdb.inject_disconnect(&new_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + assert!( + pdb.peer_info(&new_peer).is_some(), + "Peer should exist as disconnected" + ); + + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + println!("{}", pdb.disconnected_peers); + } + + #[test] + fn test_disconnected_are_bounded() { + let mut pdb = get_db(); + + for _ in 0..MAX_DC_PEERS + 1 { + let p = PeerId::random(); + pdb.connect_ingoing(&p, "/ip4/0.0.0.0".parse().unwrap(), None); + } + assert_eq!(pdb.disconnected_peers, 0); + + for p in pdb.connected_peer_ids().cloned().collect::>() { + pdb.inject_disconnect(&p); + } + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + assert_eq!(pdb.disconnected_peers, MAX_DC_PEERS); + } + + #[test] + fn test_banned_are_bounded() { + let mut pdb = get_db(); + + for _ in 0..MAX_BANNED_PEERS + 1 { + let p = PeerId::random(); + pdb.connect_ingoing(&p, "/ip4/0.0.0.0".parse().unwrap(), None); + } + assert_eq!(pdb.banned_peers_count.banned_peers(), 0); + + for p in pdb.connected_peer_ids().cloned().collect::>() { + let _ = pdb.report_peer(&p, PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(&p); + } + + assert_eq!(pdb.banned_peers_count.banned_peers(), MAX_BANNED_PEERS); + } + + #[test] + fn test_best_peers() { + let mut pdb = get_db(); + + let p0 = PeerId::random(); + let p1 = PeerId::random(); + let p2 = PeerId::random(); + pdb.connect_ingoing(&p0, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_ingoing(&p1, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_ingoing(&p2, "/ip4/0.0.0.0".parse().unwrap(), None); + add_score(&mut pdb, &p0, 70.0); + add_score(&mut pdb, &p1, 100.0); + add_score(&mut pdb, &p2, 50.0); + + let best_peers: Vec<&PeerId> = pdb + .best_peers_by_status(PeerInfo::is_connected) + .iter() + .map(|p| p.0) + .collect(); + assert_eq!(vec![&p1, &p0, &p2], best_peers); + } + + #[test] + fn test_the_best_peer() { + let mut pdb = get_db(); + + let p0 = PeerId::random(); + let p1 = PeerId::random(); + let p2 = PeerId::random(); + pdb.connect_ingoing(&p0, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_ingoing(&p1, "/ip4/0.0.0.0".parse().unwrap(), None); + pdb.connect_ingoing(&p2, "/ip4/0.0.0.0".parse().unwrap(), None); + add_score(&mut pdb, &p0, 70.0); + add_score(&mut pdb, &p1, 100.0); + add_score(&mut pdb, &p2, 50.0); + + let the_best = pdb.best_by_status(PeerInfo::is_connected); + assert!(the_best.is_some()); + // Consistency check + let best_peers = pdb.best_peers_by_status(PeerInfo::is_connected); + assert_eq!(the_best.unwrap(), best_peers.get(0).unwrap().0); + } + + #[test] + fn test_disconnected_consistency() { + let mut pdb = get_db(); + + let random_peer = PeerId::random(); + + pdb.connect_ingoing(&random_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + pdb.connect_ingoing(&random_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + pdb.inject_disconnect(&random_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + pdb.connect_outgoing(&random_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + pdb.inject_disconnect(&random_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + let _ = pdb.report_peer( + &random_peer, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&random_peer); + let _ = pdb.report_peer( + &random_peer, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + pdb.inject_disconnect(&random_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + + pdb.inject_disconnect(&random_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + pdb.inject_disconnect(&random_peer); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + } + + #[test] + fn test_disconnected_ban_consistency() { + let mut pdb = get_db(); + let mut multiaddr = Multiaddr::empty(); + multiaddr.push(Protocol::Tcp(9000)); + multiaddr.push(Protocol::Ip4("0.0.0.0".parse().unwrap())); + + let random_peer = PeerId::random(); + let random_peer1 = PeerId::random(); + let random_peer2 = PeerId::random(); + let random_peer3 = PeerId::random(); + println!("{}", random_peer); + println!("{}", random_peer1); + println!("{}", random_peer2); + println!("{}", random_peer3); + + // All 4 peers connected on the same IP + pdb.connect_ingoing(&random_peer, multiaddr.clone(), None); + pdb.connect_ingoing(&random_peer1, multiaddr.clone(), None); + pdb.connect_ingoing(&random_peer2, multiaddr.clone(), None); + pdb.connect_ingoing(&random_peer3, multiaddr.clone(), None); + + // Should be no disconnected or banned peers + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers_by_score().count() + ); + + // Should be no disconnected peers + println!( + "1:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // Disconnect one peer + pdb.inject_disconnect(&random_peer1); + // Should be 1 disconnected peer + println!( + "2:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // Disconnect and ban peer 2 + let _ = pdb.report_peer( + &random_peer2, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + // Should be 1 disconnected peer and one peer in the process of being disconnected + println!( + "3:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // The peer is now disconnected and banned + pdb.inject_disconnect(&random_peer2); + // There should be 2 disconnected peers. + println!( + "4:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // Now that the peer is disconnected, register the ban. + let _ = pdb.report_peer( + &random_peer2, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + // There should be 1 disconnected peer and one banned peer. + println!( + "5:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // Re-connect peer3, should have no effect + pdb.connect_ingoing(&random_peer3, multiaddr.clone(), None); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + // Now ban peer 1. + let _ = pdb.report_peer( + &random_peer1, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + // There should be no disconnected peers and 2 banned peers + println!( + "6:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // This should have no effect + pdb.inject_disconnect(&random_peer1); + // Should still be no disconnected peers and 2 banned peers + println!( + "7:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + // Same thing here. + let _ = pdb.report_peer( + &random_peer1, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + println!( + "8:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + println!( + "{}, {:?}, {}", + pdb.disconnected_peers, + pdb.disconnected_peers().collect::>(), + pdb.banned_peers_count.banned_peers + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Try and reconnect banned peer 2. + pdb.connect_outgoing(&random_peer2, multiaddr.clone(), None); + pdb.peer_info_mut(&random_peer2) + .unwrap() + .add_to_score(100.0); + // This removes the banned peer and should give us 0 disconnected, 1 banned peer + // (peer1) + println!( + "9:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Ban peer 3 + let _ = pdb.report_peer( + &random_peer3, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&random_peer3); + + // This should add a new banned peer, there should be 0 disconnected and 2 banned + // peers (peer1 and peer3) + println!( + "10:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Ban peer 3 + let _ = pdb.report_peer( + &random_peer3, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&random_peer3); + + // Should still have 2 banned peers + println!( + "11:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + // Unban peer 1 + pdb.connect_ingoing(&random_peer1, multiaddr.clone(), None); + pdb.peer_info_mut(&random_peer1) + .unwrap() + .add_to_score(100.0); + // Should have 1 banned peer (peer 3) + println!( + "12:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + // Disconnect peer 2 + pdb.inject_disconnect(&random_peer2); + + // Should have 1 disconnect (peer 2) and one banned (peer 3) + println!( + "12:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + // Ban peer 3 + let _ = pdb.report_peer( + &random_peer3, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&random_peer3); + + // Should have 1 disconnect (peer 2) and one banned (peer 3) + println!( + "13:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + + // Add peer 0 + pdb.connect_ingoing(&random_peer, multiaddr, None); + pdb.peer_info_mut(&random_peer).unwrap().add_to_score(100.0); + + // Should have 1 disconnect (peer 2) and one banned (peer 3) + println!( + "14:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Disconnect peer 0 + pdb.inject_disconnect(&random_peer); + // Should have 2 disconnect (peer 0, peer 2) and one banned (peer 3) + println!( + "15:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Disconnect peer 0 + pdb.inject_disconnect(&random_peer); + // Should have 2 disconnect (peer 0, peer 2) and one banned (peer 3) + println!( + "16:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + assert_eq!( + pdb.banned_peers_count.banned_peers(), + pdb.banned_peers().count() + ); + + // Ban peer 0 + let _ = pdb.report_peer( + &random_peer, + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&random_peer); + + // Should have 1 disconnect ( peer 2) and two banned (peer0, peer 3) + println!( + "17:{},{}", + pdb.disconnected_peers, pdb.banned_peers_count.banned_peers + ); + assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); + } + + fn connect_peer_with_ips(pdb: &mut PeerDB, ips: Vec) -> PeerId { + let p = PeerId::random(); + + for ip in ips { + let mut addr = Multiaddr::empty(); + addr.push(Protocol::from(ip)); + addr.push(Protocol::Tcp(9000)); + pdb.connect_ingoing(&p, addr, None); + } + p + } + + #[test] + fn test_ban_address() { + let mut pdb = get_db(); + + let ip1 = Ipv4Addr::new(1, 2, 3, 4).into(); + let ip2 = Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8).into(); + let ip3 = Ipv4Addr::new(1, 2, 3, 5).into(); + let ip4 = Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 9).into(); + let ip5 = Ipv4Addr::new(2, 2, 3, 4).into(); + + let mut peers = Vec::new(); + for i in 0..BANNED_PEERS_PER_IP_THRESHOLD + 2 { + peers.push(connect_peer_with_ips( + &mut pdb, + if i == 0 { + vec![ip1, ip2] + } else { + vec![ip1, ip2, ip3, ip4] + }, + )); + } + + let p1 = connect_peer_with_ips(&mut pdb, vec![ip1]); + let p2 = connect_peer_with_ips(&mut pdb, vec![ip2, ip5]); + let p3 = connect_peer_with_ips(&mut pdb, vec![ip3, ip5]); + let p4 = connect_peer_with_ips(&mut pdb, vec![ip5, ip4]); + let p5 = connect_peer_with_ips(&mut pdb, vec![ip5]); + + for p in &peers[..BANNED_PEERS_PER_IP_THRESHOLD + 1] { + let _ = pdb.report_peer(p, PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(p); + } + + //check that ip1 and ip2 are banned but ip3-5 not + assert!(pdb.ban_status(&p1).is_banned()); + assert!(pdb.ban_status(&p2).is_banned()); + assert!(!pdb.ban_status(&p3).is_banned()); + assert!(!pdb.ban_status(&p4).is_banned()); + assert!(!pdb.ban_status(&p5).is_banned()); + + //ban also the last peer in peers + let _ = pdb.report_peer( + &peers[BANNED_PEERS_PER_IP_THRESHOLD + 1], + PeerAction::Fatal, + ReportSource::PeerManager, + "", + ); + pdb.inject_disconnect(&peers[BANNED_PEERS_PER_IP_THRESHOLD + 1]); + + //check that ip1-ip4 are banned but ip5 not + assert!(pdb.ban_status(&p1).is_banned()); + assert!(pdb.ban_status(&p2).is_banned()); + assert!(pdb.ban_status(&p3).is_banned()); + assert!(pdb.ban_status(&p4).is_banned()); + assert!(!pdb.ban_status(&p5).is_banned()); + + //peers[0] gets unbanned + reset_score(&mut pdb, &peers[0]); + pdb.update_connection_state(&peers[0], NewConnectionState::Unbanned); + let _ = pdb.shrink_to_fit(); + + //nothing changed + assert!(pdb.ban_status(&p1).is_banned()); + assert!(pdb.ban_status(&p2).is_banned()); + assert!(pdb.ban_status(&p3).is_banned()); + assert!(pdb.ban_status(&p4).is_banned()); + assert!(!pdb.ban_status(&p5).is_banned()); + + //peers[1] gets unbanned + reset_score(&mut pdb, &peers[1]); + pdb.update_connection_state(&peers[1], NewConnectionState::Unbanned); + let _ = pdb.shrink_to_fit(); + + //all ips are unbanned + assert!(!pdb.ban_status(&p1).is_banned()); + assert!(!pdb.ban_status(&p2).is_banned()); + assert!(!pdb.ban_status(&p3).is_banned()); + assert!(!pdb.ban_status(&p4).is_banned()); + assert!(!pdb.ban_status(&p5).is_banned()); + } + + #[test] + fn test_banned_ip_consistent_after_changing_ips() { + let mut pdb = get_db(); + + let ip1: IpAddr = Ipv4Addr::new(1, 2, 3, 4).into(); + let ip2: IpAddr = Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8).into(); + + let mut peers = Vec::new(); + for _ in 0..BANNED_PEERS_PER_IP_THRESHOLD + 1 { + peers.push(connect_peer_with_ips(&mut pdb, vec![ip1])); + } + + let p1 = connect_peer_with_ips(&mut pdb, vec![ip1]); + let p2 = connect_peer_with_ips(&mut pdb, vec![ip2]); + + // ban all peers + for p in &peers { + let _ = pdb.report_peer(p, PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(p); + } + + // check ip is banned + assert!(pdb.ban_status(&p1).is_banned()); + assert!(!pdb.ban_status(&p2).is_banned()); + + // unban a peer + reset_score(&mut pdb, &peers[0]); + pdb.update_connection_state(&peers[0], NewConnectionState::Unbanned); + let _ = pdb.shrink_to_fit(); + + // check not banned anymore + assert!(!pdb.ban_status(&p1).is_banned()); + assert!(!pdb.ban_status(&p2).is_banned()); + + // unban all peers + for p in &peers { + reset_score(&mut pdb, p); + pdb.update_connection_state(p, NewConnectionState::Unbanned); + let _ = pdb.shrink_to_fit(); + } + + // add ip2 to all peers and ban them. + let mut socker_addr = Multiaddr::from(ip2); + socker_addr.push(Protocol::Tcp(8080)); + for p in &peers { + pdb.connect_ingoing(p, socker_addr.clone(), None); + let _ = pdb.report_peer(p, PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(p); + } + + // both IP's are now banned + assert!(pdb.ban_status(&p1).is_banned()); + assert!(pdb.ban_status(&p2).is_banned()); + + // unban all peers + for p in &peers { + reset_score(&mut pdb, p); + pdb.update_connection_state(p, NewConnectionState::Unbanned); + let _ = pdb.shrink_to_fit(); + } + + // reban every peer except one + for p in &peers[1..] { + let _ = pdb.report_peer(p, PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(p); + } + + // nothing is banned + assert!(!pdb.ban_status(&p1).is_banned()); + assert!(!pdb.ban_status(&p2).is_banned()); + + // reban last peer + let _ = pdb.report_peer(&peers[0], PeerAction::Fatal, ReportSource::PeerManager, ""); + pdb.inject_disconnect(&peers[0]); + + //Ip's are banned again + assert!(pdb.ban_status(&p1).is_banned()); + assert!(pdb.ban_status(&p2).is_banned()); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_trusted_peers_score() { + let trusted_peer = PeerId::random(); + let mut pdb: PeerDB = PeerDB::new(vec![trusted_peer]); + + pdb.connect_ingoing(&trusted_peer, "/ip4/0.0.0.0".parse().unwrap(), None); + + // Check trusted status and score + assert!(pdb.peer_info(&trusted_peer).unwrap().is_trusted()); + assert_eq!( + pdb.peer_info(&trusted_peer).unwrap().score().score(), + Score::max_score().score() + ); + + // Adding/Subtracting score should have no effect on a trusted peer + add_score(&mut pdb, &trusted_peer, -50.0); + + assert_eq!( + pdb.peer_info(&trusted_peer).unwrap().score().score(), + Score::max_score().score() + ); + } +} diff --git a/node/network/src/peer_manager/peerdb/client.rs b/node/network/src/peer_manager/peerdb/client.rs new file mode 100644 index 0000000..aaacbcc --- /dev/null +++ b/node/network/src/peer_manager/peerdb/client.rs @@ -0,0 +1,102 @@ +use libp2p::identify::IdentifyInfo; +use serde::Serialize; +use strum::{AsRefStr, EnumIter, IntoStaticStr}; + +/// Various client and protocol information related to a node. +#[derive(Clone, Debug, Serialize)] +pub struct Client { + /// The client's name. + pub kind: ClientKind, + /// The client's version. + pub version: String, + /// The OS version of the client. + pub os_version: String, + /// The libp2p protocol version. + pub protocol_version: String, + /// Identify agent string + pub agent_string: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq, AsRefStr, IntoStaticStr, EnumIter)] +pub enum ClientKind { + /// An Zgs node. + Zgs, + /// An unknown client. + Unknown, +} + +impl Default for Client { + fn default() -> Self { + Client { + kind: ClientKind::Unknown, + version: "unknown".into(), + os_version: "unknown".into(), + protocol_version: "unknown".into(), + agent_string: None, + } + } +} + +impl Client { + /// Builds a `Client` from `IdentifyInfo`. + pub fn from_identify_info(info: &IdentifyInfo) -> Self { + let (kind, version, os_version) = client_from_agent_version(&info.agent_version); + + Client { + kind, + version, + os_version, + protocol_version: info.protocol_version.clone(), + agent_string: Some(info.agent_version.clone()), + } + } +} + +impl std::fmt::Display for Client { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.kind { + ClientKind::Zgs => write!( + f, + "zgs: version: {}, os_version: {}", + self.version, self.os_version + ), + ClientKind::Unknown => { + if let Some(agent_string) = &self.agent_string { + write!(f, "Unknown: {}", agent_string) + } else { + write!(f, "Unknown") + } + } + } + } +} + +impl std::fmt::Display for ClientKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_ref()) + } +} + +// helper function to identify clients from their agent_version. Returns the client +// kind and it's associated version and the OS kind. +fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String) { + let mut agent_split = agent_version.split('/'); + match agent_split.next() { + Some("zgs") => { + let kind = ClientKind::Zgs; + let mut version = String::from("unknown"); + let mut os_version = version.clone(); + if let Some(agent_version) = agent_split.next() { + version = agent_version.into(); + if let Some(agent_os_version) = agent_split.next() { + os_version = agent_os_version.into(); + } + } + (kind, version, os_version) + } + _ => { + let unknown = String::from("unknown"); + (ClientKind::Unknown, unknown.clone(), unknown) + } + } +} diff --git a/node/network/src/peer_manager/peerdb/peer_info.rs b/node/network/src/peer_manager/peerdb/peer_info.rs new file mode 100644 index 0000000..ae75458 --- /dev/null +++ b/node/network/src/peer_manager/peerdb/peer_info.rs @@ -0,0 +1,424 @@ +use super::client::Client; +use super::score::{PeerAction, Score, ScoreState}; +use super::sync_status::SyncStatus; +use crate::Multiaddr; +use discv5::Enr; +use serde::{ + ser::{SerializeStruct, Serializer}, + Serialize, +}; +use std::collections::HashSet; +use std::net::{IpAddr, SocketAddr}; +use std::time::Instant; +use strum::AsRefStr; +use PeerConnectionStatus::*; + +/// Information about a given connected peer. +#[derive(Clone, Debug, Serialize)] +pub struct PeerInfo { + /// The peers reputation + score: Score, + /// Client managing this peer + client: Client, + /// Connection status of this peer + connection_status: PeerConnectionStatus, + /// The known listening addresses of this peer. This is given by identify and can be arbitrary + /// (including local IPs). + listening_addresses: Vec, + /// This is addresses we have physically seen and this is what we use for banning/un-banning + /// peers. + seen_addresses: HashSet, + /// The current syncing state of the peer. The state may be determined after it's initial + /// connection. + sync_status: SyncStatus, + /// The time we would like to retain this peer. After this time, the peer is no longer + /// necessary. + #[serde(skip)] + min_ttl: Option, + /// Is the peer a trusted peer. + is_trusted: bool, + /// Direction of the first connection of the last (or current) connected session with this peer. + /// None if this peer was never connected. + connection_direction: Option, + /// The enr of the peer, if known. + enr: Option, +} + +impl Default for PeerInfo { + fn default() -> PeerInfo { + PeerInfo { + score: Score::default(), + client: Client::default(), + connection_status: Default::default(), + listening_addresses: Vec::new(), + seen_addresses: HashSet::new(), + sync_status: SyncStatus::Unknown, + min_ttl: None, + is_trusted: false, + connection_direction: None, + enr: None, + } + } +} + +impl PeerInfo { + /// Return a PeerInfo struct for a trusted peer. + pub fn trusted_peer_info() -> Self { + PeerInfo { + score: Score::max_score(), + is_trusted: true, + ..Default::default() + } + } + + /// Obtains the client of the peer. + pub fn client(&self) -> &Client { + &self.client + } + + /// Returns the listening addresses of the Peer. + pub fn listening_addresses(&self) -> &Vec { + &self.listening_addresses + } + + /// Returns the connection direction for the peer. + pub fn connection_direction(&self) -> Option<&ConnectionDirection> { + self.connection_direction.as_ref() + } + + /// Returns the sync status of the peer. + pub fn sync_status(&self) -> &SyncStatus { + &self.sync_status + } + + /// Returns whether the peer is a trusted peer or not. + pub fn is_trusted(&self) -> bool { + self.is_trusted + } + + /// The time a peer is expected to be useful until for an attached validator. If this is set to + /// None, the peer is not required for any upcoming duty. + pub fn min_ttl(&self) -> Option<&Instant> { + self.min_ttl.as_ref() + } + + /// The ENR of the peer if it is known. + pub fn enr(&self) -> Option<&Enr> { + self.enr.as_ref() + } + + /// Returns the seen addresses of the peer. + pub fn seen_addresses(&self) -> impl Iterator + '_ { + self.seen_addresses.iter() + } + + /// Returns a list of seen IP addresses for the peer. + pub fn seen_ip_addresses(&self) -> impl Iterator + '_ { + self.seen_addresses + .iter() + .map(|socket_addr| socket_addr.ip()) + } + + /// Returns the connection status of the peer. + pub fn connection_status(&self) -> &PeerConnectionStatus { + &self.connection_status + } + + /// Reports if this peer has some future validator duty in which case it is valuable to keep it. + pub fn has_future_duty(&self) -> bool { + self.min_ttl.map_or(false, |i| i >= Instant::now()) + } + + /// Returns score of the peer. + pub fn score(&self) -> &Score { + &self.score + } + + /// Returns the state of the peer based on the score. + pub(crate) fn score_state(&self) -> ScoreState { + self.score.state() + } + + /// Returns true if the gossipsub score is sufficient. + pub fn is_good_gossipsub_peer(&self) -> bool { + self.score.is_good_gossipsub_peer() + } + + /* Peer connection status API */ + + /// Checks if the status is connected. + pub fn is_connected(&self) -> bool { + matches!( + self.connection_status, + PeerConnectionStatus::Connected { .. } + ) + } + + /// Checks if the status is connected. + pub fn is_dialing(&self) -> bool { + matches!(self.connection_status, PeerConnectionStatus::Dialing { .. }) + } + + /// The peer is either connected or in the process of being dialed. + pub fn is_connected_or_dialing(&self) -> bool { + self.is_connected() || self.is_dialing() + } + + /// Checks if the connection status is banned. This can lag behind the score state + /// temporarily. + pub fn is_banned(&self) -> bool { + matches!(self.connection_status, PeerConnectionStatus::Banned { .. }) + } + + /// Checks if the peer's score is banned. + pub fn score_is_banned(&self) -> bool { + matches!(self.score.state(), ScoreState::Banned) + } + + /// Checks if the status is disconnected. + pub fn is_disconnected(&self) -> bool { + matches!(self.connection_status, Disconnected { .. }) + } + + /// Checks if the peer is outbound-only + pub fn is_outbound_only(&self) -> bool { + matches!(self.connection_status, Connected {n_in, n_out} if n_in == 0 && n_out > 0) + } + + /// Returns the number of connections with this peer. + pub fn connections(&self) -> (u8, u8) { + match self.connection_status { + Connected { n_in, n_out } => (n_in, n_out), + _ => (0, 0), + } + } + + /* Mutable Functions */ + + /// Updates the sync status. Returns true if the status was changed. + // VISIBILITY: Both the peer manager the network sync is able to update the sync state of a peer + pub fn update_sync_status(&mut self, sync_status: SyncStatus) -> bool { + self.sync_status.update(sync_status) + } + + /// Sets the client of the peer. + // VISIBILITY: The peer manager is able to set the client + pub(in crate::peer_manager) fn set_client(&mut self, client: Client) { + self.client = client + } + + /// Replaces the current listening addresses with those specified, returning the current + /// listening addresses. + // VISIBILITY: The peer manager is able to set the listening addresses + pub(in crate::peer_manager) fn set_listening_addresses( + &mut self, + listening_addresses: Vec, + ) -> Vec { + std::mem::replace(&mut self.listening_addresses, listening_addresses) + } + + /// Sets the connection status of the peer. + pub(super) fn set_connection_status(&mut self, connection_status: PeerConnectionStatus) { + self.connection_status = connection_status + } + + /// Sets the ENR of the peer if one is known. + pub(super) fn set_enr(&mut self, enr: Enr) { + self.enr = Some(enr) + } + + /// Sets the time that the peer is expected to be needed until for an attached validator duty. + pub(super) fn set_min_ttl(&mut self, min_ttl: Instant) { + self.min_ttl = Some(min_ttl) + } + + /// Applies decay rates to a non-trusted peer's score. + pub(super) fn score_update(&mut self) { + if !self.is_trusted { + self.score.update() + } + } + + /// Apply peer action to a non-trusted peer's score. + // VISIBILITY: The peer manager is able to modify the score of a peer. + pub(in crate::peer_manager) fn apply_peer_action_to_score(&mut self, peer_action: PeerAction) { + if !self.is_trusted { + self.score.apply_peer_action(peer_action) + } + } + + /// Updates the gossipsub score with a new score. Optionally ignore the gossipsub score. + pub(super) fn update_gossipsub_score(&mut self, new_score: f64, ignore: bool) { + self.score.update_gossipsub_score(new_score, ignore); + } + + #[cfg(test)] + /// Resets the peers score. + pub fn reset_score(&mut self) { + self.score.test_reset(); + } + + /// Modifies the status to Dialing + /// Returns an error if the current state is unexpected. + pub(super) fn set_dialing_peer(&mut self) -> Result<(), &'static str> { + match &mut self.connection_status { + Connected { .. } => return Err("Dialing connected peer"), + Dialing { .. } => return Err("Dialing an already dialing peer"), + Disconnecting { .. } => return Err("Dialing a disconnecting peer"), + Disconnected { .. } | Banned { .. } | Unknown => {} + } + self.connection_status = Dialing { + since: Instant::now(), + }; + Ok(()) + } + + /// Modifies the status to Connected and increases the number of ingoing + /// connections by one + pub(super) fn connect_ingoing(&mut self, seen_address: Option) { + match &mut self.connection_status { + Connected { n_in, .. } => *n_in += 1, + Disconnected { .. } + | Banned { .. } + | Dialing { .. } + | Disconnecting { .. } + | Unknown => { + self.connection_status = Connected { n_in: 1, n_out: 0 }; + self.connection_direction = Some(ConnectionDirection::Incoming); + } + } + + if let Some(socket_addr) = seen_address { + self.seen_addresses.insert(socket_addr); + } + } + + /// Modifies the status to Connected and increases the number of outgoing + /// connections by one + pub(super) fn connect_outgoing(&mut self, seen_address: Option) { + match &mut self.connection_status { + Connected { n_out, .. } => *n_out += 1, + Disconnected { .. } + | Banned { .. } + | Dialing { .. } + | Disconnecting { .. } + | Unknown => { + self.connection_status = Connected { n_in: 0, n_out: 1 }; + self.connection_direction = Some(ConnectionDirection::Outgoing); + } + } + if let Some(ip_addr) = seen_address { + self.seen_addresses.insert(ip_addr); + } + } + + #[cfg(test)] + /// Add an f64 to a non-trusted peer's score abiding by the limits. + pub fn add_to_score(&mut self, score: f64) { + if !self.is_trusted { + self.score.test_add(score) + } + } + + #[cfg(test)] + pub fn set_gossipsub_score(&mut self, score: f64) { + self.score.set_gossipsub_score(score); + } +} + +/// Connection Direction of connection. +#[derive(Debug, Clone, Serialize, AsRefStr)] +#[strum(serialize_all = "snake_case")] +pub enum ConnectionDirection { + /// The connection was established by a peer dialing us. + Incoming, + /// The connection was established by us dialing a peer. + Outgoing, +} + +/// Connection Status of the peer. +#[derive(Debug, Clone, Default)] +pub enum PeerConnectionStatus { + /// The peer is connected. + Connected { + /// number of ingoing connections. + n_in: u8, + /// number of outgoing connections. + n_out: u8, + }, + /// The peer is being disconnected. + Disconnecting { + // After the disconnection the peer will be considered banned. + to_ban: bool, + }, + /// The peer has disconnected. + Disconnected { + /// last time the peer was connected or discovered. + since: Instant, + }, + /// The peer has been banned and is disconnected. + Banned { + /// moment when the peer was banned. + since: Instant, + }, + /// We are currently dialing this peer. + Dialing { + /// time since we last communicated with the peer. + since: Instant, + }, + /// The connection status has not been specified. + #[default] + Unknown, +} + +/// Serialization for http requests. +impl Serialize for PeerConnectionStatus { + fn serialize(&self, serializer: S) -> Result { + let mut s = serializer.serialize_struct("connection_status", 6)?; + match self { + Connected { n_in, n_out } => { + s.serialize_field("status", "connected")?; + s.serialize_field("connections_in", n_in)?; + s.serialize_field("connections_out", n_out)?; + s.serialize_field("last_seen", &0)?; + s.end() + } + Disconnecting { .. } => { + s.serialize_field("status", "disconnecting")?; + s.serialize_field("connections_in", &0)?; + s.serialize_field("connections_out", &0)?; + s.serialize_field("last_seen", &0)?; + s.end() + } + Disconnected { since } => { + s.serialize_field("status", "disconnected")?; + s.serialize_field("connections_in", &0)?; + s.serialize_field("connections_out", &0)?; + s.serialize_field("last_seen", &since.elapsed().as_secs())?; + s.serialize_field("banned_ips", &Vec::::new())?; + s.end() + } + Banned { since } => { + s.serialize_field("status", "banned")?; + s.serialize_field("connections_in", &0)?; + s.serialize_field("connections_out", &0)?; + s.serialize_field("last_seen", &since.elapsed().as_secs())?; + s.end() + } + Dialing { since } => { + s.serialize_field("status", "dialing")?; + s.serialize_field("connections_in", &0)?; + s.serialize_field("connections_out", &0)?; + s.serialize_field("last_seen", &since.elapsed().as_secs())?; + s.end() + } + Unknown => { + s.serialize_field("status", "unknown")?; + s.serialize_field("connections_in", &0)?; + s.serialize_field("connections_out", &0)?; + s.serialize_field("last_seen", &0)?; + s.end() + } + } + } +} diff --git a/node/network/src/peer_manager/peerdb/score.rs b/node/network/src/peer_manager/peerdb/score.rs new file mode 100644 index 0000000..3b67c44 --- /dev/null +++ b/node/network/src/peer_manager/peerdb/score.rs @@ -0,0 +1,425 @@ +//! This contains the scoring logic for peers. +//! +//! A peer's score is a rational number in the range [-100, 100]. +//! +//! As the logic develops this documentation will advance. +//! +//! The scoring algorithms are currently experimental. +use crate::behaviour::gossipsub_scoring_parameters::GREYLIST_THRESHOLD as GOSSIPSUB_GREYLIST_THRESHOLD; +use serde::Serialize; +use std::time::Instant; +use strum::AsRefStr; +use tokio::time::Duration; + +lazy_static! { + static ref HALFLIFE_DECAY: f64 = -(2.0f64.ln()) / SCORE_HALFLIFE; +} + +/// The default score for new peers. +pub(crate) const DEFAULT_SCORE: f64 = 0.0; +/// The minimum reputation before a peer is disconnected. +const MIN_SCORE_BEFORE_DISCONNECT: f64 = -20.0; +/// The minimum reputation before a peer is banned. +const MIN_SCORE_BEFORE_BAN: f64 = -50.0; +/// If a peer has a lighthouse score below this constant all other score parts will get ignored and +/// the peer will get banned regardless of the other parts. +const MIN_LIGHTHOUSE_SCORE_BEFORE_BAN: f64 = -60.0; +/// The maximum score a peer can obtain. +const MAX_SCORE: f64 = 100.0; +/// The minimum score a peer can obtain. +const MIN_SCORE: f64 = -100.0; +/// The halflife of a peer's score. I.e the number of seconds it takes for the score to decay to half its value. +const SCORE_HALFLIFE: f64 = 600.0; +/// The number of seconds we ban a peer for before their score begins to decay. +const BANNED_BEFORE_DECAY: Duration = Duration::from_secs(12 * 3600); // 12 hours + +/// We weight negative gossipsub scores in such a way that they never result in a disconnect by +/// themselves. This "solves" the problem of non-decaying gossipsub scores for disconnected peers. +const GOSSIPSUB_NEGATIVE_SCORE_WEIGHT: f64 = + (MIN_SCORE_BEFORE_DISCONNECT + 1.0) / GOSSIPSUB_GREYLIST_THRESHOLD; +const GOSSIPSUB_POSITIVE_SCORE_WEIGHT: f64 = GOSSIPSUB_NEGATIVE_SCORE_WEIGHT; + +/// A collection of actions a peer can perform which will adjust its score. +/// Each variant has an associated score change. +// To easily assess the behaviour of scores changes the number of variants should stay low, and +// somewhat generic. +#[derive(Debug, Clone, Copy, AsRefStr)] +#[strum(serialize_all = "snake_case")] +pub enum PeerAction { + /// We should not communicate more with this peer. + /// This action will cause the peer to get banned. + Fatal, + /// This peer's action is not malicious but will not be tolerated. A few occurrences will cause + /// the peer to get kicked. + /// NOTE: ~5 occurrences will get the peer banned + LowToleranceError, + /// An error occurred with this peer but it is not necessarily malicious. + /// We have high tolerance for this actions: several occurrences are needed for a peer to get + /// kicked. + /// NOTE: ~10 occurrences will get the peer banned + MidToleranceError, + /// An error occurred with this peer but it is not necessarily malicious. + /// We have high tolerance for this actions: several occurrences are needed for a peer to get + /// kicked. + /// NOTE: ~50 occurrences will get the peer banned + HighToleranceError, +} + +/// Service reporting a `PeerAction` for a peer. +#[derive(Debug)] +pub enum ReportSource { + Gossipsub, + RPC, + Processor, + SyncService, + PeerManager, +} + +impl From for &'static str { + fn from(report_source: ReportSource) -> &'static str { + match report_source { + ReportSource::Gossipsub => "gossipsub", + ReportSource::RPC => "rpc_error", + ReportSource::Processor => "processor", + ReportSource::SyncService => "sync", + ReportSource::PeerManager => "peer_manager", + } + } +} + +impl std::fmt::Display for PeerAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PeerAction::Fatal => write!(f, "Fatal"), + PeerAction::LowToleranceError => write!(f, "Low Tolerance Error"), + PeerAction::MidToleranceError => write!(f, "Mid Tolerance Error"), + PeerAction::HighToleranceError => write!(f, "High Tolerance Error"), + } + } +} + +/// The expected state of the peer given the peer's score. +#[derive(Debug, PartialEq, Clone, Copy)] +pub(crate) enum ScoreState { + /// We are content with the peers performance. We permit connections and messages. + Healthy, + /// The peer should be disconnected. We allow re-connections if the peer is persistent. + Disconnected, + /// The peer is banned. We disallow new connections until it's score has decayed into a + /// tolerable threshold. + Banned, +} + +impl std::fmt::Display for ScoreState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ScoreState::Healthy => write!(f, "Healthy"), + ScoreState::Banned => write!(f, "Banned"), + ScoreState::Disconnected => write!(f, "Disconnected"), + } + } +} + +/// A peer's score (perceived potential usefulness). +/// +/// This simplistic version consists of a global score per peer which decays to 0 over time. The +/// decay rate applies equally to positive and negative scores. +#[derive(PartialEq, Clone, Debug, Serialize)] +pub struct RealScore { + /// The global score. + // NOTE: In the future we may separate this into sub-scores involving the RPC, Gossipsub and + // lighthouse. + lighthouse_score: f64, + gossipsub_score: f64, + /// We ignore the negative gossipsub scores of some peers to allow decaying without + /// disconnecting. + ignore_negative_gossipsub_score: bool, + score: f64, + /// The time the score was last updated to perform time-based adjustments such as score-decay. + #[serde(skip)] + last_updated: Instant, +} + +impl Default for RealScore { + fn default() -> Self { + RealScore { + lighthouse_score: DEFAULT_SCORE, + gossipsub_score: DEFAULT_SCORE, + score: DEFAULT_SCORE, + last_updated: Instant::now(), + ignore_negative_gossipsub_score: false, + } + } +} + +impl RealScore { + /// Access to the underlying score. + fn recompute_score(&mut self) { + self.score = self.lighthouse_score; + if self.lighthouse_score <= MIN_LIGHTHOUSE_SCORE_BEFORE_BAN { + //ignore all other scores, i.e. do nothing here + } else if self.gossipsub_score >= 0.0 { + self.score += self.gossipsub_score * GOSSIPSUB_POSITIVE_SCORE_WEIGHT; + } else if !self.ignore_negative_gossipsub_score { + self.score += self.gossipsub_score * GOSSIPSUB_NEGATIVE_SCORE_WEIGHT; + } + } + + fn score(&self) -> f64 { + self.score + } + + /// Modifies the score based on a peer's action. + pub fn apply_peer_action(&mut self, peer_action: PeerAction) { + match peer_action { + PeerAction::Fatal => self.set_lighthouse_score(MIN_SCORE), // The worst possible score + PeerAction::LowToleranceError => self.add(-10.0), + PeerAction::MidToleranceError => self.add(-5.0), + PeerAction::HighToleranceError => self.add(-1.0), + } + } + + fn set_lighthouse_score(&mut self, new_score: f64) { + self.lighthouse_score = new_score; + self.update_state(); + } + + /// Add an f64 to the score abiding by the limits. + fn add(&mut self, score: f64) { + let mut new_score = self.lighthouse_score + score; + if new_score > MAX_SCORE { + new_score = MAX_SCORE; + } + if new_score < MIN_SCORE { + new_score = MIN_SCORE; + } + + self.set_lighthouse_score(new_score); + } + + fn update_state(&mut self) { + let was_not_banned = self.score > MIN_SCORE_BEFORE_BAN; + self.recompute_score(); + if was_not_banned && self.score <= MIN_SCORE_BEFORE_BAN { + //we ban this peer for at least BANNED_BEFORE_DECAY seconds + self.last_updated += BANNED_BEFORE_DECAY; + } + } + + /// Add an f64 to the score abiding by the limits. + #[cfg(test)] + pub fn test_add(&mut self, score: f64) { + self.add(score); + } + + #[cfg(test)] + // reset the score + pub fn test_reset(&mut self) { + self.set_lighthouse_score(0f64); + } + + // Set the gossipsub_score to a specific f64. + // Used in testing to induce score status changes during a heartbeat. + #[cfg(test)] + pub fn set_gossipsub_score(&mut self, score: f64) { + self.gossipsub_score = score; + } + + /// Applies time-based logic such as decay rates to the score. + /// This function should be called periodically. + pub fn update(&mut self) { + self.update_at(Instant::now()) + } + + /// Applies time-based logic such as decay rates to the score with the given now value. + /// This private sub function is mainly used for testing. + fn update_at(&mut self, now: Instant) { + // Decay the current score + // Using exponential decay based on a constant half life. + + // It is important that we use here `checked_duration_since` instead of elapsed, since + // we set last_updated to the future when banning peers. Therefore `checked_duration_since` + // will return None in this case and the score does not get decayed. + if let Some(secs_since_update) = now + .checked_duration_since(self.last_updated) + .map(|d| d.as_secs()) + { + // e^(-ln(2)/HL*t) + let decay_factor = (*HALFLIFE_DECAY * secs_since_update as f64).exp(); + self.lighthouse_score *= decay_factor; + self.last_updated = now; + self.update_state(); + } + } + + pub fn update_gossipsub_score(&mut self, new_score: f64, ignore: bool) { + // we only update gossipsub if last_updated is in the past which means either the peer is + // not banned or the BANNED_BEFORE_DECAY time is over. + if self.last_updated <= Instant::now() { + self.gossipsub_score = new_score; + self.ignore_negative_gossipsub_score = ignore; + self.update_state(); + } + } + + pub fn is_good_gossipsub_peer(&self) -> bool { + self.gossipsub_score >= 0.0 + } +} + +#[derive(PartialEq, Clone, Debug, Serialize)] +pub enum Score { + Max, + Real(RealScore), +} + +impl Default for Score { + fn default() -> Self { + Self::Real(RealScore::default()) + } +} + +macro_rules! apply { + ( $method:ident $(, $param_name: ident: $param_type: ty)*) => { + impl Score { + pub fn $method( + &mut self, $($param_name: $param_type, )* + ) { + if let Self::Real(score) = self { + score.$method($($param_name, )*); + } + } + } + }; +} + +apply!(apply_peer_action, peer_action: PeerAction); +apply!(update); +apply!(update_gossipsub_score, new_score: f64, ignore: bool); +#[cfg(test)] +apply!(test_add, score: f64); +#[cfg(test)] +apply!(test_reset); +#[cfg(test)] +apply!(set_gossipsub_score, score: f64); + +impl Score { + pub fn score(&self) -> f64 { + match self { + Self::Max => f64::INFINITY, + Self::Real(score) => score.score(), + } + } + + pub fn max_score() -> Self { + Self::Max + } + + /// Returns the expected state of the peer given it's score. + pub(crate) fn state(&self) -> ScoreState { + match self.score() { + x if x <= MIN_SCORE_BEFORE_BAN => ScoreState::Banned, + x if x <= MIN_SCORE_BEFORE_DISCONNECT => ScoreState::Disconnected, + _ => ScoreState::Healthy, + } + } + + pub fn is_good_gossipsub_peer(&self) -> bool { + match self { + Self::Max => true, + Self::Real(score) => score.is_good_gossipsub_peer(), + } + } +} + +impl Eq for Score {} + +impl PartialOrd for Score { + fn partial_cmp(&self, other: &Score) -> Option { + self.score().partial_cmp(&other.score()) + } +} + +impl Ord for Score { + fn cmp(&self, other: &Score) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal) + } +} + +impl std::fmt::Display for Score { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:.2}", self.score()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[allow(clippy::float_cmp)] + fn test_reputation_change() { + let mut score = Score::default(); + + // 0 change does not change de reputation + // + let change = 0.0; + score.test_add(change); + assert_eq!(score.score(), DEFAULT_SCORE); + + // underflowing change is capped + let mut score = Score::default(); + let change = MIN_SCORE - 50.0; + score.test_add(change); + assert_eq!(score.score(), MIN_SCORE); + + // overflowing change is capped + let mut score = Score::default(); + let change = MAX_SCORE + 50.0; + score.test_add(change); + assert_eq!(score.score(), MAX_SCORE); + + // Score adjusts + let mut score = Score::default(); + let change = 1.32; + score.test_add(change); + assert_eq!(score.score(), DEFAULT_SCORE + change); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_ban_time() { + let mut score = RealScore::default(); + let now = Instant::now(); + + let change = MIN_SCORE_BEFORE_BAN; + score.test_add(change); + assert_eq!(score.score(), MIN_SCORE_BEFORE_BAN); + + score.update_at(now + BANNED_BEFORE_DECAY); + assert_eq!(score.score(), MIN_SCORE_BEFORE_BAN); + + score.update_at(now + BANNED_BEFORE_DECAY + Duration::from_secs(1)); + assert!(score.score() > MIN_SCORE_BEFORE_BAN); + } + + #[test] + fn test_very_negative_gossipsub_score() { + let mut score = Score::default(); + score.update_gossipsub_score(GOSSIPSUB_GREYLIST_THRESHOLD, false); + assert!(!score.is_good_gossipsub_peer()); + assert!(score.score() < 0.0); + assert_eq!(score.state(), ScoreState::Healthy); + score.test_add(-1.0001); + assert_eq!(score.state(), ScoreState::Disconnected); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_ignored_gossipsub_score() { + let mut score = Score::default(); + score.update_gossipsub_score(GOSSIPSUB_GREYLIST_THRESHOLD, true); + assert!(!score.is_good_gossipsub_peer()); + assert_eq!(score.score(), 0.0); + } +} diff --git a/node/network/src/peer_manager/peerdb/sync_status.rs b/node/network/src/peer_manager/peerdb/sync_status.rs new file mode 100644 index 0000000..b23cf57 --- /dev/null +++ b/node/network/src/peer_manager/peerdb/sync_status.rs @@ -0,0 +1,82 @@ +//! Handles individual sync status for peers. + +use ethereum_types::H256; +use serde::Serialize; + +#[derive(Clone, Debug, Serialize)] +/// The current sync status of the peer. +pub enum SyncStatus { + /// At the current state as our node or ahead of us. + Synced { info: SyncInfo }, + /// The peer has greater knowledge about the canonical chain than we do. + Advanced { info: SyncInfo }, + /// Is behind our current head and not useful for block downloads. + Behind { info: SyncInfo }, + /// This peer is in an incompatible network. + IrrelevantPeer, + /// Not currently known as a STATUS handshake has not occurred. + Unknown, +} + +/// A relevant peer's sync information. +#[derive(Clone, Debug, Serialize)] +pub struct SyncInfo { + pub head_root: H256, + pub finalized_root: H256, +} + +impl std::cmp::PartialEq for SyncStatus { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (SyncStatus::Synced { .. }, SyncStatus::Synced { .. }) + | (SyncStatus::Advanced { .. }, SyncStatus::Advanced { .. }) + | (SyncStatus::Behind { .. }, SyncStatus::Behind { .. }) + | (SyncStatus::IrrelevantPeer, SyncStatus::IrrelevantPeer) + | (SyncStatus::Unknown, SyncStatus::Unknown) + ) + } +} + +impl SyncStatus { + /// Returns true if the peer has advanced knowledge of the chain. + pub fn is_advanced(&self) -> bool { + matches!(self, SyncStatus::Advanced { .. }) + } + + /// Returns true if the peer is up to date with the current chain. + pub fn is_synced(&self) -> bool { + matches!(self, SyncStatus::Synced { .. }) + } + + /// Returns true if the peer is behind the current chain. + pub fn is_behind(&self) -> bool { + matches!(self, SyncStatus::Behind { .. }) + } + + /// Updates the peer's sync status, returning whether the status transitioned. + /// + /// E.g. returns `true` if the state changed from `Synced` to `Advanced`, but not if + /// the status remained `Synced` with different `SyncInfo` within. + pub fn update(&mut self, new_state: SyncStatus) -> bool { + let changed_status = *self != new_state; + *self = new_state; + changed_status + } + + pub fn as_str(&self) -> &'static str { + match self { + SyncStatus::Advanced { .. } => "Advanced", + SyncStatus::Behind { .. } => "Behind", + SyncStatus::Synced { .. } => "Synced", + SyncStatus::Unknown => "Unknown", + SyncStatus::IrrelevantPeer => "Irrelevant", + } + } +} + +impl std::fmt::Display for SyncStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} diff --git a/node/network/src/rpc/codec/base.rs b/node/network/src/rpc/codec/base.rs new file mode 100644 index 0000000..7aa9e2c --- /dev/null +++ b/node/network/src/rpc/codec/base.rs @@ -0,0 +1,262 @@ +//! This handles the various supported encoding mechanism for the Eth 2.0 RPC. + +use crate::rpc::methods::ErrorType; +use crate::rpc::{InboundRequest, OutboundRequest, RPCCodedResponse, RPCResponse}; +use libp2p::bytes::BufMut; +use libp2p::bytes::BytesMut; +use tokio_util::codec::{Decoder, Encoder}; + +pub trait OutboundCodec: Encoder + Decoder { + type CodecErrorType; + + fn decode_error( + &mut self, + src: &mut BytesMut, + ) -> Result, ::Error>; +} + +/* Global Inbound Codec */ +// This deals with Decoding RPC Requests from other peers and encoding our responses + +pub struct BaseInboundCodec +where + TCodec: Encoder + Decoder, +{ + /// Inner codec for handling various encodings + inner: TCodec, +} + +impl BaseInboundCodec +where + TCodec: Encoder + Decoder, +{ + pub fn new(codec: TCodec) -> Self { + BaseInboundCodec { inner: codec } + } +} + +/* Global Outbound Codec */ +// This deals with Decoding RPC Responses from other peers and encoding our requests +pub struct BaseOutboundCodec +where + TOutboundCodec: OutboundCodec, +{ + /// Inner codec for handling various encodings. + inner: TOutboundCodec, + /// Keeps track of the current response code for a chunk. + current_response_code: Option, +} + +impl BaseOutboundCodec +where + TOutboundCodec: OutboundCodec, +{ + pub fn new(codec: TOutboundCodec) -> Self { + BaseOutboundCodec { + inner: codec, + current_response_code: None, + } + } +} + +/* Implementation of the Encoding/Decoding for the global codecs */ + +/* Base Inbound Codec */ + +// This Encodes RPC Responses sent to external peers +impl Encoder for BaseInboundCodec +where + TCodec: Decoder + Encoder, +{ + type Error = >::Error; + + fn encode(&mut self, item: RPCCodedResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { + dst.clear(); + dst.reserve(1); + dst.put_u8( + item.as_u8() + .expect("Should never encode a stream termination"), + ); + self.inner.encode(item, dst) + } +} + +// This Decodes RPC Requests from external peers +impl Decoder for BaseInboundCodec +where + TCodec: Encoder + Decoder, +{ + type Item = InboundRequest; + type Error = ::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + self.inner.decode(src) + } +} + +/* Base Outbound Codec */ + +// This Encodes RPC Requests sent to external peers +impl Encoder for BaseOutboundCodec +where + TCodec: OutboundCodec + Encoder, +{ + type Error = >::Error; + + fn encode(&mut self, item: OutboundRequest, dst: &mut BytesMut) -> Result<(), Self::Error> { + self.inner.encode(item, dst) + } +} + +// This decodes RPC Responses received from external peers +impl Decoder for BaseOutboundCodec +where + TCodec: + OutboundCodec + Decoder, +{ + type Item = RPCCodedResponse; + type Error = ::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + // if we have only received the response code, wait for more bytes + if src.len() <= 1 { + return Ok(None); + } + // using the response code determine which kind of payload needs to be decoded. + let response_code = self.current_response_code.unwrap_or_else(|| { + let resp_code = src.split_to(1)[0]; + self.current_response_code = Some(resp_code); + resp_code + }); + + let inner_result = { + if RPCCodedResponse::is_response(response_code) { + // decode an actual response and mutates the buffer if enough bytes have been read + // returning the result. + self.inner + .decode(src) + .map(|r| r.map(RPCCodedResponse::Success)) + } else { + // decode an error + self.inner + .decode_error(src) + .map(|r| r.map(|resp| RPCCodedResponse::from_error(response_code, resp))) + } + }; + // if the inner decoder was capable of decoding a chunk, we need to reset the current + // response code for the next chunk + if let Ok(Some(_)) = inner_result { + self.current_response_code = None; + } + // return the result + inner_result + } +} + +#[cfg(test)] +mod tests { + use super::super::ssz_snappy::*; + use super::*; + use crate::rpc::protocol::*; + + use unsigned_varint::codec::Uvi; + + // #[test] + // fn test_decode_status_message() { + // let message = hex::decode("0054ff060000734e615070590032000006e71e7b54989925efd6c9cbcb8ceb9b5f71216f5137282bf6a1e3b50f64e42d6c7fb347abe07eb0db8200000005029e2800").unwrap(); + // let mut buf = BytesMut::new(); + // buf.extend_from_slice(&message); + + // let snappy_protocol_id = + // ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy); + + // let mut snappy_outbound_codec = SSZSnappyOutboundCodec::new( + // snappy_protocol_id, + // max_rpc_size(), + // ); + + // // remove response code + // let mut snappy_buf = buf.clone(); + // let _ = snappy_buf.split_to(1); + + // // decode message just as snappy message + // let _snappy_decoded_message = snappy_outbound_codec.decode(&mut snappy_buf).unwrap(); + + // // build codecs for entire chunk + // let mut snappy_base_outbound_codec = BaseOutboundCodec::new(snappy_outbound_codec); + + // // decode message as ssz snappy chunk + // let _snappy_decoded_chunk = snappy_base_outbound_codec.decode(&mut buf).unwrap(); + // } + + #[test] + fn test_invalid_length_prefix() { + let mut uvi_codec: Uvi = Uvi::default(); + let mut dst = BytesMut::with_capacity(1024); + + // Smallest > 10 byte varint + let len: u128 = 2u128.pow(70); + + // Insert length-prefix + uvi_codec.encode(len, &mut dst).unwrap(); + + let snappy_protocol_id = + ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy); + + let mut snappy_outbound_codec = + SSZSnappyOutboundCodec::new(snappy_protocol_id, max_rpc_size()); + + let snappy_decoded_message = snappy_outbound_codec.decode(&mut dst).unwrap_err(); + + assert_eq!( + snappy_decoded_message, + RPCError::IoError("input bytes exceed maximum".to_string()), + "length-prefix of > 10 bytes is invalid" + ); + } + + #[test] + fn test_length_limits() { + fn encode_len(len: usize) -> BytesMut { + let mut uvi_codec: Uvi = Uvi::default(); + let mut dst = BytesMut::with_capacity(1024); + uvi_codec.encode(len, &mut dst).unwrap(); + dst + } + + let protocol_id = ProtocolId::new(Protocol::DataByHash, Version::V1, Encoding::SSZSnappy); + + // Response limits + let max_rpc_size = max_rpc_size(); + let limit = protocol_id.rpc_response_limits(); + let mut max = encode_len(limit.max + 1); + let mut codec = SSZSnappyOutboundCodec::new(protocol_id.clone(), max_rpc_size); + assert!(matches!( + codec.decode(&mut max).unwrap_err(), + RPCError::InvalidData(_) + )); + + let mut min = encode_len(limit.min - 1); + let mut codec = SSZSnappyOutboundCodec::new(protocol_id.clone(), max_rpc_size); + assert!(matches!( + codec.decode(&mut min).unwrap_err(), + RPCError::InvalidData(_) + )); + + // Request limits + let limit = protocol_id.rpc_request_limits(); + let mut max = encode_len(limit.max + 1); + let mut codec = SSZSnappyOutboundCodec::new(protocol_id.clone(), max_rpc_size); + assert!(matches!( + codec.decode(&mut max).unwrap_err(), + RPCError::InvalidData(_) + )); + + let mut min = encode_len(limit.min - 1); + let mut codec = SSZSnappyOutboundCodec::new(protocol_id, max_rpc_size); + assert!(matches!( + codec.decode(&mut min).unwrap_err(), + RPCError::InvalidData(_) + )); + } +} diff --git a/node/network/src/rpc/codec/mod.rs b/node/network/src/rpc/codec/mod.rs new file mode 100644 index 0000000..34ccb3d --- /dev/null +++ b/node/network/src/rpc/codec/mod.rs @@ -0,0 +1,60 @@ +pub(crate) mod base; +pub(crate) mod ssz_snappy; + +use self::base::{BaseInboundCodec, BaseOutboundCodec}; +use self::ssz_snappy::{SSZSnappyInboundCodec, SSZSnappyOutboundCodec}; +use crate::rpc::protocol::RPCError; +use crate::rpc::{InboundRequest, OutboundRequest, RPCCodedResponse}; +use libp2p::bytes::BytesMut; +use tokio_util::codec::{Decoder, Encoder}; + +// Known types of codecs +pub enum InboundCodec { + SSZSnappy(BaseInboundCodec), +} + +pub enum OutboundCodec { + SSZSnappy(BaseOutboundCodec), +} + +impl Encoder for InboundCodec { + type Error = RPCError; + + fn encode(&mut self, item: RPCCodedResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { + match self { + InboundCodec::SSZSnappy(codec) => codec.encode(item, dst), + } + } +} + +impl Decoder for InboundCodec { + type Item = InboundRequest; + type Error = RPCError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + match self { + InboundCodec::SSZSnappy(codec) => codec.decode(src), + } + } +} + +impl Encoder for OutboundCodec { + type Error = RPCError; + + fn encode(&mut self, item: OutboundRequest, dst: &mut BytesMut) -> Result<(), Self::Error> { + match self { + OutboundCodec::SSZSnappy(codec) => codec.encode(item, dst), + } + } +} + +impl Decoder for OutboundCodec { + type Item = RPCCodedResponse; + type Error = RPCError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + match self { + OutboundCodec::SSZSnappy(codec) => codec.decode(src), + } + } +} diff --git a/node/network/src/rpc/codec/ssz_snappy.rs b/node/network/src/rpc/codec/ssz_snappy.rs new file mode 100644 index 0000000..604f0ba --- /dev/null +++ b/node/network/src/rpc/codec/ssz_snappy.rs @@ -0,0 +1,585 @@ +use crate::rpc::methods::*; +use crate::rpc::{ + codec::base::OutboundCodec, + protocol::{Encoding, Protocol, ProtocolId, RPCError, Version, ERROR_TYPE_MAX, ERROR_TYPE_MIN}, +}; +use crate::rpc::{InboundRequest, OutboundRequest, RPCCodedResponse, RPCResponse}; +use libp2p::bytes::BytesMut; +use shared_types::ChunkArrayWithProof; +use snap::read::FrameDecoder; +use snap::write::FrameEncoder; +use ssz::{Decode, Encode}; +use ssz_types::VariableList; +use std::io::Cursor; +use std::io::ErrorKind; +use std::io::{Read, Write}; +use tokio_util::codec::{Decoder, Encoder}; +use unsigned_varint::codec::Uvi; + +/* Inbound Codec */ + +pub struct SSZSnappyInboundCodec { + protocol: ProtocolId, + inner: Uvi, + len: Option, + /// Maximum bytes that can be sent in one req/resp chunked responses. + max_packet_size: usize, +} + +impl SSZSnappyInboundCodec { + pub fn new(protocol: ProtocolId, max_packet_size: usize) -> Self { + let uvi_codec = Uvi::default(); + // this encoding only applies to ssz_snappy. + debug_assert_eq!(protocol.encoding, Encoding::SSZSnappy); + + SSZSnappyInboundCodec { + inner: uvi_codec, + protocol, + len: None, + max_packet_size, + } + } +} + +// Encoder for inbound streams: Encodes RPC Responses sent to peers. +impl Encoder for SSZSnappyInboundCodec { + type Error = RPCError; + + fn encode(&mut self, item: RPCCodedResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { + let bytes = match &item { + RPCCodedResponse::Success(resp) => match &resp { + RPCResponse::Status(res) => res.as_ssz_bytes(), + RPCResponse::Pong(res) => res.data.as_ssz_bytes(), + RPCResponse::DataByHash(res) => res.as_ssz_bytes(), + RPCResponse::Chunks(res) => res.as_ssz_bytes(), + }, + RPCCodedResponse::Error(_, err) => err.as_ssz_bytes(), + RPCCodedResponse::StreamTermination(_) => { + unreachable!("Code error - attempting to encode a stream termination") + } + }; + // SSZ encoded bytes should be within `max_packet_size` + if bytes.len() > self.max_packet_size { + return Err(RPCError::InternalError( + "attempting to encode data > max_packet_size", + )); + } + + // Inserts the length prefix of the uncompressed bytes into dst + // encoded as a unsigned varint + self.inner + .encode(bytes.len(), dst) + .map_err(RPCError::from)?; + + let mut writer = FrameEncoder::new(Vec::new()); + writer.write_all(&bytes).map_err(RPCError::from)?; + writer.flush().map_err(RPCError::from)?; + + // Write compressed bytes to `dst` + dst.extend_from_slice(writer.get_ref()); + Ok(()) + } +} + +// Decoder for inbound streams: Decodes RPC requests from peers +impl Decoder for SSZSnappyInboundCodec { + type Item = InboundRequest; + type Error = RPCError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let length = match handle_length(&mut self.inner, &mut self.len, src)? { + Some(len) => len, + None => return Ok(None), + }; + + // Should not attempt to decode rpc chunks with `length > max_packet_size` or not within bounds of + // packet size for ssz container corresponding to `self.protocol`. + let ssz_limits = self.protocol.rpc_request_limits(); + if ssz_limits.is_out_of_bounds(length, self.max_packet_size) { + return Err(RPCError::InvalidData(format!( + "RPC request length is out of bounds, length {} bounds = {:?}, protocol = {:?}", + length, ssz_limits, self.protocol, + ))); + } + // Calculate worst case compression length for given uncompressed length + let max_compressed_len = snap::raw::max_compress_len(length) as u64; + + // Create a limit reader as a wrapper that reads only upto `max_compressed_len` from `src`. + let limit_reader = Cursor::new(src.as_ref()).take(max_compressed_len); + let mut reader = FrameDecoder::new(limit_reader); + let mut decoded_buffer = vec![0; length]; + + match reader.read_exact(&mut decoded_buffer) { + Ok(()) => { + // `n` is how many bytes the reader read in the compressed stream + let n = reader.get_ref().get_ref().position(); + self.len = None; + let _read_bytes = src.split_to(n as usize); + + match self.protocol.version { + Version::V1 => handle_v1_request(self.protocol.message_name, &decoded_buffer), + } + } + Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), + } + } +} + +/* Outbound Codec: Codec for initiating RPC requests */ +pub struct SSZSnappyOutboundCodec { + inner: Uvi, + len: Option, + protocol: ProtocolId, + /// Maximum bytes that can be sent in one req/resp chunked responses. + max_packet_size: usize, +} + +impl SSZSnappyOutboundCodec { + pub fn new(protocol: ProtocolId, max_packet_size: usize) -> Self { + let uvi_codec = Uvi::default(); + // this encoding only applies to ssz_snappy. + debug_assert_eq!(protocol.encoding, Encoding::SSZSnappy); + + SSZSnappyOutboundCodec { + inner: uvi_codec, + protocol, + max_packet_size, + len: None, + } + } +} + +// Encoder for outbound streams: Encodes RPC Requests to peers +impl Encoder for SSZSnappyOutboundCodec { + type Error = RPCError; + + fn encode(&mut self, item: OutboundRequest, dst: &mut BytesMut) -> Result<(), Self::Error> { + let bytes = match item { + OutboundRequest::Status(req) => req.as_ssz_bytes(), + OutboundRequest::Goodbye(req) => req.as_ssz_bytes(), + OutboundRequest::Ping(req) => req.as_ssz_bytes(), + OutboundRequest::DataByHash(req) => req.hashes.as_ssz_bytes(), + OutboundRequest::GetChunks(req) => req.as_ssz_bytes(), + }; + // SSZ encoded bytes should be within `max_packet_size` + if bytes.len() > self.max_packet_size { + return Err(RPCError::InternalError( + "attempting to encode data > max_packet_size", + )); + } + + // Inserts the length prefix of the uncompressed bytes into dst + // encoded as a unsigned varint + self.inner + .encode(bytes.len(), dst) + .map_err(RPCError::from)?; + + let mut writer = FrameEncoder::new(Vec::new()); + writer.write_all(&bytes).map_err(RPCError::from)?; + writer.flush().map_err(RPCError::from)?; + + // Write compressed bytes to `dst` + dst.extend_from_slice(writer.get_ref()); + Ok(()) + } +} + +// Decoder for outbound streams: Decodes RPC responses from peers. +// +// The majority of the decoding has now been pushed upstream due to the changing specification. +// We prefer to decode blocks and attestations with extra knowledge about the chain to perform +// faster verification checks before decoding entire blocks/attestations. +impl Decoder for SSZSnappyOutboundCodec { + type Item = RPCResponse; + type Error = RPCError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let length = match handle_length(&mut self.inner, &mut self.len, src)? { + Some(len) => len, + None => return Ok(None), + }; + + // Should not attempt to decode rpc chunks with `length > max_packet_size` or not within bounds of + // packet size for ssz container corresponding to `self.protocol`. + let ssz_limits = self.protocol.rpc_response_limits(); + + if ssz_limits.is_out_of_bounds(length, self.max_packet_size) { + return Err(RPCError::InvalidData(format!( + "RPC response length is out of bounds, length {}", + length, + ))); + } + // Calculate worst case compression length for given uncompressed length + let max_compressed_len = snap::raw::max_compress_len(length) as u64; + // Create a limit reader as a wrapper that reads only upto `max_compressed_len` from `src`. + let limit_reader = Cursor::new(src.as_ref()).take(max_compressed_len); + let mut reader = FrameDecoder::new(limit_reader); + + let mut decoded_buffer = vec![0; length]; + + match reader.read_exact(&mut decoded_buffer) { + Ok(()) => { + // `n` is how many bytes the reader read in the compressed stream + let n = reader.get_ref().get_ref().position(); + self.len = None; + let _read_bytes = src.split_to(n as usize); + + match self.protocol.version { + Version::V1 => handle_v1_response(self.protocol.message_name, &decoded_buffer), + } + } + Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), + } + } +} + +impl OutboundCodec for SSZSnappyOutboundCodec { + type CodecErrorType = ErrorType; + + fn decode_error( + &mut self, + src: &mut BytesMut, + ) -> Result, RPCError> { + let length = match handle_length(&mut self.inner, &mut self.len, src)? { + Some(len) => len, + None => return Ok(None), + }; + + // Should not attempt to decode rpc chunks with `length > max_packet_size` or not within bounds of + // packet size for ssz container corresponding to `ErrorType`. + if length > self.max_packet_size || length > *ERROR_TYPE_MAX || length < *ERROR_TYPE_MIN { + return Err(RPCError::InvalidData(format!( + "RPC Error length is out of bounds, length {}", + length + ))); + } + + // Calculate worst case compression length for given uncompressed length + let max_compressed_len = snap::raw::max_compress_len(length) as u64; + // Create a limit reader as a wrapper that reads only upto `max_compressed_len` from `src`. + let limit_reader = Cursor::new(src.as_ref()).take(max_compressed_len); + let mut reader = FrameDecoder::new(limit_reader); + let mut decoded_buffer = vec![0; length]; + match reader.read_exact(&mut decoded_buffer) { + Ok(()) => { + // `n` is how many bytes the reader read in the compressed stream + let n = reader.get_ref().get_ref().position(); + self.len = None; + let _read_bytes = src.split_to(n as usize); + Ok(Some(ErrorType(VariableList::from_ssz_bytes( + &decoded_buffer, + )?))) + } + Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), + } + } +} + +/// Handle errors that we get from decoding an RPC message from the stream. +/// `num_bytes_read` is the number of bytes the snappy decoder has read from the underlying stream. +/// `max_compressed_len` is the maximum compressed size for a given uncompressed size. +fn handle_error( + err: std::io::Error, + num_bytes: u64, + max_compressed_len: u64, +) -> Result, RPCError> { + match err.kind() { + ErrorKind::UnexpectedEof => { + // If snappy has read `max_compressed_len` from underlying stream and still can't fill buffer, we have a malicious message. + // Report as `InvalidData` so that malicious peer gets banned. + if num_bytes >= max_compressed_len { + Err(RPCError::InvalidData(format!( + "Received malicious snappy message, num_bytes {}, max_compressed_len {}", + num_bytes, max_compressed_len + ))) + } else { + // Haven't received enough bytes to decode yet, wait for more + Ok(None) + } + } + _ => Err(err).map_err(RPCError::from), + } +} + +/// Decodes the length-prefix from the bytes as an unsigned protobuf varint. +/// +/// Returns `Ok(Some(length))` by decoding the bytes if required. +/// Returns `Ok(None)` if more bytes are needed to decode the length-prefix. +/// Returns an `RPCError` for a decoding error. +fn handle_length( + uvi_codec: &mut Uvi, + len: &mut Option, + bytes: &mut BytesMut, +) -> Result, RPCError> { + if let Some(length) = len { + Ok(Some(*length)) + } else { + // Decode the length of the uncompressed bytes from an unsigned varint + // Note: length-prefix of > 10 bytes(uint64) would be a decoding error + match uvi_codec.decode(bytes).map_err(RPCError::from)? { + Some(length) => { + *len = Some(length); + Ok(Some(length)) + } + None => Ok(None), // need more bytes to decode length + } + } +} + +/// Decodes a `Version::V1` `InboundRequest` from the byte stream. +/// `decoded_buffer` should be an ssz-encoded bytestream with +// length = length-prefix received in the beginning of the stream. +fn handle_v1_request( + protocol: Protocol, + decoded_buffer: &[u8], +) -> Result, RPCError> { + match protocol { + Protocol::Status => Ok(Some(InboundRequest::Status(StatusMessage::from_ssz_bytes( + decoded_buffer, + )?))), + Protocol::Goodbye => Ok(Some(InboundRequest::Goodbye( + GoodbyeReason::from_ssz_bytes(decoded_buffer)?, + ))), + Protocol::Ping => Ok(Some(InboundRequest::Ping(Ping { + data: u64::from_ssz_bytes(decoded_buffer)?, + }))), + Protocol::DataByHash => Ok(Some(InboundRequest::DataByHash(DataByHashRequest { + hashes: VariableList::from_ssz_bytes(decoded_buffer)?, + }))), + Protocol::GetChunks => Ok(Some(InboundRequest::GetChunks( + GetChunksRequest::from_ssz_bytes(decoded_buffer)?, + ))), + } +} + +/// Decodes a `Version::V1` `RPCResponse` from the byte stream. +/// `decoded_buffer` should be an ssz-encoded bytestream with +// length = length-prefix received in the beginning of the stream. +fn handle_v1_response( + protocol: Protocol, + decoded_buffer: &[u8], +) -> Result, RPCError> { + match protocol { + Protocol::Status => Ok(Some(RPCResponse::Status(StatusMessage::from_ssz_bytes( + decoded_buffer, + )?))), + // This case should be unreachable as `Goodbye` has no response. + Protocol::Goodbye => Err(RPCError::InvalidData( + "Goodbye RPC message has no valid response".to_string(), + )), + Protocol::Ping => Ok(Some(RPCResponse::Pong(Ping { + data: u64::from_ssz_bytes(decoded_buffer)?, + }))), + Protocol::DataByHash => Ok(Some(RPCResponse::DataByHash(Box::new( + ZgsData::from_ssz_bytes(decoded_buffer)?, + )))), + Protocol::GetChunks => Ok(Some(RPCResponse::Chunks( + ChunkArrayWithProof::from_ssz_bytes(decoded_buffer)?, + ))), + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::rpc::protocol::*; + use crate::rpc::{methods::StatusMessage, Ping}; + + use snap::write::FrameEncoder; + use ssz::Encode; + use std::io::Write; + + fn status_message() -> StatusMessage { + StatusMessage { data: 1 } + } + + fn ping_message() -> Ping { + Ping { data: 1 } + } + + /// Encodes the given protocol response as bytes. + fn encode( + protocol: Protocol, + version: Version, + message: RPCCodedResponse, + ) -> Result { + let snappy_protocol_id = ProtocolId::new(protocol, version, Encoding::SSZSnappy); + let max_packet_size = max_rpc_size(); + + let mut buf = BytesMut::new(); + let mut snappy_inbound_codec = + SSZSnappyInboundCodec::new(snappy_protocol_id, max_packet_size); + + snappy_inbound_codec.encode(message, &mut buf)?; + Ok(buf) + } + + // fn encode_without_length_checks( + // bytes: Vec, + // ) -> Result { + // let mut dst = BytesMut::new(); + + // let mut uvi_codec: Uvi = Uvi::default(); + + // // Inserts the length prefix of the uncompressed bytes into dst + // // encoded as a unsigned varint + // uvi_codec + // .encode(bytes.len(), &mut dst) + // .map_err(RPCError::from)?; + + // let mut writer = FrameEncoder::new(Vec::new()); + // writer.write_all(&bytes).map_err(RPCError::from)?; + // writer.flush().map_err(RPCError::from)?; + + // // Write compressed bytes to `dst` + // dst.extend_from_slice(writer.get_ref()); + + // Ok(dst) + // } + + /// Attempts to decode the given protocol bytes as an rpc response + fn decode( + protocol: Protocol, + version: Version, + message: &mut BytesMut, + ) -> Result, RPCError> { + let snappy_protocol_id = ProtocolId::new(protocol, version, Encoding::SSZSnappy); + let max_packet_size = max_rpc_size(); + let mut snappy_outbound_codec = + SSZSnappyOutboundCodec::new(snappy_protocol_id, max_packet_size); + // decode message just as snappy message + snappy_outbound_codec.decode(message) + } + + /// Encodes the provided protocol message as bytes and tries to decode the encoding bytes. + fn encode_then_decode( + protocol: Protocol, + version: Version, + message: RPCCodedResponse, + ) -> Result, RPCError> { + let mut encoded = encode(protocol, version.clone(), message)?; + decode(protocol, version, &mut encoded) + } + + // Test RPCResponse encoding/decoding for V1 messages + #[test] + fn test_encode_then_decode_v1() { + assert_eq!( + encode_then_decode( + Protocol::Status, + Version::V1, + RPCCodedResponse::Success(RPCResponse::Status(status_message())), + ), + Ok(Some(RPCResponse::Status(status_message()))) + ); + + assert_eq!( + encode_then_decode( + Protocol::Ping, + Version::V1, + RPCCodedResponse::Success(RPCResponse::Pong(ping_message())), + ), + Ok(Some(RPCResponse::Pong(ping_message()))) + ); + + assert_eq!( + encode_then_decode( + Protocol::DataByHash, + Version::V1, + RPCCodedResponse::Success(RPCResponse::DataByHash(Box::new(ZgsData { + hash: Default::default() + }))), + ), + Ok(Some(RPCResponse::DataByHash(Box::new(ZgsData { + hash: Default::default() + })))) + ); + + // TODO: add tests for outbound requests + } + + // /// Test a malicious snappy encoding for a V1 `Status` message where the attacker + // /// sends a valid message filled with a stream of useless padding before the actual message. + // #[test] + // fn test_decode_malicious_v1_message() { + // // 10 byte snappy stream identifier + // let stream_identifier: &'static [u8] = b"\xFF\x06\x00\x00sNaPpY"; + + // assert_eq!(stream_identifier.len(), 10); + + // // byte 0(0xFE) is padding chunk type identifier for snappy messages + // // byte 1,2,3 are chunk length (little endian) + // let malicious_padding: &'static [u8] = b"\xFE\x00\x00\x00"; + + // // Status message is 84 bytes uncompressed. `max_compressed_len` is 32 + 84 + 84/6 = 130. + // let status_message_bytes = StatusMessage { + // data: 1 + // } + // .as_ssz_bytes(); + + // assert_eq!(status_message_bytes.len(), 8); + // assert_eq!(snap::raw::max_compress_len(status_message_bytes.len()), 41); + + // let mut uvi_codec: Uvi = Uvi::default(); + // let mut dst = BytesMut::with_capacity(1024); + + // // Insert length-prefix + // uvi_codec + // .encode(status_message_bytes.len(), &mut dst) + // .unwrap(); + + // // Insert snappy stream identifier + // dst.extend_from_slice(stream_identifier); + + // // Insert malicious padding of 80 bytes. + // for _ in 0..20 { + // dst.extend_from_slice(malicious_padding); + // } + + // // Insert payload (42 bytes compressed) + // let mut writer = FrameEncoder::new(Vec::new()); + // writer.write_all(&status_message_bytes).unwrap(); + // writer.flush().unwrap(); + // assert_eq!(writer.get_ref().len(), 42); + // dst.extend_from_slice(writer.get_ref()); + + // // 10 (for stream identifier) + 80 + 42 = 132 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`. + // assert!(matches!( + // decode(Protocol::Status, Version::V1, &mut dst).unwrap_err(), + // RPCError::InvalidData(_) + // )); + // } + + /// Test sending a message with encoded length prefix > max_rpc_size. + #[test] + fn test_decode_invalid_length() { + // 10 byte snappy stream identifier + let stream_identifier: &'static [u8] = b"\xFF\x06\x00\x00sNaPpY"; + + assert_eq!(stream_identifier.len(), 10); + + // Status message is 84 bytes uncompressed. `max_compressed_len` is 32 + 84 + 84/6 = 130. + let status_message_bytes = StatusMessage { data: 1 }.as_ssz_bytes(); + + let mut uvi_codec: Uvi = Uvi::default(); + let mut dst = BytesMut::with_capacity(1024); + + // Insert length-prefix + uvi_codec.encode(MAX_RPC_SIZE + 1, &mut dst).unwrap(); + + // Insert snappy stream identifier + dst.extend_from_slice(stream_identifier); + + // Insert payload + let mut writer = FrameEncoder::new(Vec::new()); + writer.write_all(&status_message_bytes).unwrap(); + writer.flush().unwrap(); + dst.extend_from_slice(writer.get_ref()); + + assert!(matches!( + decode(Protocol::Status, Version::V1, &mut dst).unwrap_err(), + RPCError::InvalidData(_) + )); + } +} diff --git a/node/network/src/rpc/handler.rs b/node/network/src/rpc/handler.rs new file mode 100644 index 0000000..fef268b --- /dev/null +++ b/node/network/src/rpc/handler.rs @@ -0,0 +1,983 @@ +#![allow(clippy::type_complexity)] +#![allow(clippy::cognitive_complexity)] + +use super::methods::{GoodbyeReason, RPCCodedResponse, RPCResponseErrorCode, ResponseTermination}; +use super::outbound::OutboundRequestContainer; +use super::protocol::{max_rpc_size, InboundRequest, Protocol, RPCError, RPCProtocol}; +use super::{RPCReceived, RPCSend, ReqId}; +use crate::rpc::outbound::{OutboundFramed, OutboundRequest}; +use crate::rpc::protocol::InboundFramed; +use fnv::FnvHashMap; +use futures::prelude::*; +use futures::{Sink, SinkExt}; +use libp2p::core::upgrade::{ + InboundUpgrade, NegotiationError, OutboundUpgrade, ProtocolError, UpgradeError, +}; +use libp2p::swarm::handler::{ + ConnectionHandler, ConnectionHandlerEvent, ConnectionHandlerUpgrErr, KeepAlive, + SubstreamProtocol, +}; +use libp2p::swarm::NegotiatedSubstream; +use smallvec::SmallVec; +use std::{ + collections::{hash_map::Entry, VecDeque}, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; +use tokio::time::{sleep_until, Instant as TInstant, Sleep}; +use tokio_util::time::{delay_queue, DelayQueue}; + +/// The time (in seconds) before a substream that is awaiting a response from the user times out. +pub const RESPONSE_TIMEOUT: u64 = 30; + +/// The number of times to retry an outbound upgrade in the case of IO errors. +const IO_ERROR_RETRIES: u8 = 3; + +/// Maximum time given to the handler to perform shutdown operations. +const SHUTDOWN_TIMEOUT_SECS: u8 = 45; + +/// Identifier of inbound and outbound substreams from the handler's perspective. +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub struct SubstreamId(pub usize); + +type InboundSubstream = InboundFramed; + +/// Events the handler emits to the behaviour. +pub type HandlerEvent = Result, HandlerErr>; + +/// An error encountered by the handler. +#[derive(Debug)] +pub enum HandlerErr { + /// An error occurred for this peer's request. This can occur during protocol negotiation, + /// message passing, or if the handler identifies that we are sending an error response to the peer. + Inbound { + /// Id of the peer's request for which an error occurred. + id: SubstreamId, + /// Information of the negotiated protocol. + proto: Protocol, + /// The error that occurred. + error: RPCError, + }, + /// An error occurred for this request. Such error can occur during protocol negotiation, + /// message passing, or if we successfully received a response from the peer, but this response + /// indicates an error. + Outbound { + /// Application-given Id of the request for which an error occurred. + id: Id, + /// Information of the protocol. + proto: Protocol, + /// The error that occurred. + error: RPCError, + }, +} + +/// Implementation of `ConnectionHandler` for the RPC protocol. +pub struct RPCHandler { + /// The upgrade for inbound substreams. + listen_protocol: SubstreamProtocol, + + /// Queue of events to produce in `poll()`. + events_out: SmallVec<[HandlerEvent; 4]>, + + /// Queue of outbound substreams to open. + dial_queue: SmallVec<[(Id, OutboundRequest); 4]>, + + /// Current number of concurrent outbound substreams being opened. + dial_negotiated: u32, + + /// Current inbound substreams awaiting processing. + inbound_substreams: FnvHashMap, + + /// Inbound substream `DelayQueue` which keeps track of when an inbound substream will timeout. + inbound_substreams_delay: DelayQueue, + + /// Map of outbound substreams that need to be driven to completion. + outbound_substreams: FnvHashMap>, + + /// Inbound substream `DelayQueue` which keeps track of when an inbound substream will timeout. + outbound_substreams_delay: DelayQueue, + + /// Sequential ID for waiting substreams. For inbound substreams, this is also the inbound request ID. + current_inbound_substream_id: SubstreamId, + + /// Sequential ID for outbound substreams. + current_outbound_substream_id: SubstreamId, + + /// Maximum number of concurrent outbound substreams being opened. Value is never modified. + max_dial_negotiated: u32, + + /// State of the handler. + state: HandlerState, + + /// Try to negotiate the outbound upgrade a few times if there is an IO error before reporting the request as failed. + /// This keeps track of the number of attempts. + outbound_io_error_retries: u8, + + /// Waker, to be sure the handler gets polled when needed. + waker: Option, +} + +enum HandlerState { + /// The handler is active. All messages are sent and received. + Active, + /// The handler is shutting_down. + /// + /// While in this state the handler rejects new requests but tries to finish existing ones. + /// Once the timer expires, all messages are killed. + ShuttingDown(Box), + /// The handler is deactivated. A goodbye has been sent and no more messages are sent or + /// received. + Deactivated, +} + +/// Contains the information the handler keeps on established inbound substreams. +struct InboundInfo { + /// State of the substream. + state: InboundState, + /// Responses queued for sending. + pending_items: VecDeque, + /// Protocol of the original request we received from the peer. + protocol: Protocol, + /// Responses that the peer is still expecting from us. + remaining_chunks: u64, + /// Useful to timing how long each request took to process. Currently only used by + /// BlocksByRange. + request_start_time: Instant, + /// Key to keep track of the substream's timeout via `self.inbound_substreams_delay`. + delay_key: Option, +} + +/// Contains the information the handler keeps on established outbound substreams. +struct OutboundInfo { + /// State of the substream. + state: OutboundSubstreamState, + /// Key to keep track of the substream's timeout via `self.outbound_substreams_delay`. + delay_key: delay_queue::Key, + /// Info over the protocol this substream is handling. + proto: Protocol, + /// Number of chunks to be seen from the peer's response. + remaining_chunks: Option, + /// `Id` as given by the application that sent the request. + req_id: Id, +} + +/// State of an inbound substream connection. +enum InboundState { + /// The underlying substream is not being used. + Idle(InboundSubstream), + /// The underlying substream is processing responses. + /// The return value of the future is (substream, stream_was_closed). The stream_was_closed boolean + /// indicates if the stream was closed due to an error or successfully completing a response. + Busy(Pin> + Send>>), + /// Temporary state during processing + Poisoned, +} + +/// State of an outbound substream. Either waiting for a response, or in the process of sending. +pub enum OutboundSubstreamState { + /// A request has been sent, and we are awaiting a response. This future is driven in the + /// handler because GOODBYE requests can be handled and responses dropped instantly. + RequestPendingResponse { + /// The framed negotiated substream. + substream: Box>, + /// Keeps track of the actual request sent. + request: OutboundRequest, + }, + /// Closing an outbound substream> + Closing(Box>), + /// Temporary state during processing + Poisoned, +} + +impl RPCHandler { + pub fn new(listen_protocol: SubstreamProtocol) -> Self { + RPCHandler { + listen_protocol, + events_out: SmallVec::new(), + dial_queue: SmallVec::new(), + dial_negotiated: 0, + inbound_substreams: FnvHashMap::default(), + outbound_substreams: FnvHashMap::default(), + inbound_substreams_delay: DelayQueue::new(), + outbound_substreams_delay: DelayQueue::new(), + current_inbound_substream_id: SubstreamId(0), + current_outbound_substream_id: SubstreamId(0), + state: HandlerState::Active, + max_dial_negotiated: 8, + outbound_io_error_retries: 0, + waker: None, + } + } + + /// Initiates the handler's shutdown process, sending an optional Goodbye message to the + /// peer. + fn shutdown(&mut self, goodbye_reason: Option<(Id, GoodbyeReason)>) { + if matches!(self.state, HandlerState::Active) { + if !self.dial_queue.is_empty() { + debug!(unsent_queued_requests = %self.dial_queue.len(), "Starting handler shutdown"); + } + // We now drive to completion communications already dialed/established + while let Some((id, req)) = self.dial_queue.pop() { + self.events_out.push(Err(HandlerErr::Outbound { + error: RPCError::HandlerRejected, + proto: req.protocol(), + id, + })); + } + + // Queue our goodbye message. + if let Some((id, reason)) = goodbye_reason { + self.dial_queue.push((id, OutboundRequest::Goodbye(reason))); + } + + self.state = HandlerState::ShuttingDown(Box::new(sleep_until( + TInstant::now() + Duration::from_secs(SHUTDOWN_TIMEOUT_SECS as u64), + ))); + } + } + + /// Opens an outbound substream with a request. + fn send_request(&mut self, id: Id, req: OutboundRequest) { + match self.state { + HandlerState::Active => { + self.dial_queue.push((id, req)); + } + _ => self.events_out.push(Err(HandlerErr::Outbound { + error: RPCError::HandlerRejected, + proto: req.protocol(), + id, + })), + } + } + + /// Sends a response to a peer's request. + // NOTE: If the substream has closed due to inactivity, or the substream is in the + // wrong state a response will fail silently. + fn send_response(&mut self, inbound_id: SubstreamId, response: RPCCodedResponse) { + // check if the stream matching the response still exists + let inbound_info = if let Some(info) = self.inbound_substreams.get_mut(&inbound_id) { + info + } else { + if !matches!(response, RPCCodedResponse::StreamTermination(..)) { + // the stream is closed after sending the expected number of responses + trace!(response = %response, id = ?inbound_id, "Inbound stream has expired, response not sent"); + } + return; + }; + + // If the response we are sending is an error, report back for handling + if let RPCCodedResponse::Error(ref code, ref reason) = response { + self.events_out.push(Err(HandlerErr::Inbound { + error: RPCError::ErrorResponse(*code, reason.to_string()), + proto: inbound_info.protocol, + id: inbound_id, + })); + } + + if matches!(self.state, HandlerState::Deactivated) { + // we no longer send responses after the handler is deactivated + debug!(response = %response, id = ?inbound_id, "Response not sent. Deactivated handler"); + return; + } + inbound_info.pending_items.push_back(response); + } +} + +impl ConnectionHandler for RPCHandler +where + Id: ReqId, +{ + type InEvent = RPCSend; + type OutEvent = HandlerEvent; + type Error = RPCError; + type InboundProtocol = RPCProtocol; + type OutboundProtocol = OutboundRequestContainer; + type OutboundOpenInfo = (Id, OutboundRequest); // Keep track of the id and the request + type InboundOpenInfo = (); + + fn listen_protocol(&self) -> SubstreamProtocol { + self.listen_protocol.clone() + } + + fn inject_fully_negotiated_inbound( + &mut self, + substream: >::Output, + _info: Self::InboundOpenInfo, + ) { + // only accept new peer requests when active + if !matches!(self.state, HandlerState::Active) { + return; + } + + let (req, substream) = substream; + let expected_responses = req.expected_responses(); + + // store requests that expect responses + if expected_responses > 0 { + // Store the stream and tag the output. + let delay_key = self.inbound_substreams_delay.insert( + self.current_inbound_substream_id, + Duration::from_secs(RESPONSE_TIMEOUT), + ); + let awaiting_stream = InboundState::Idle(substream); + self.inbound_substreams.insert( + self.current_inbound_substream_id, + InboundInfo { + state: awaiting_stream, + pending_items: VecDeque::with_capacity(expected_responses as usize), + delay_key: Some(delay_key), + protocol: req.protocol(), + request_start_time: Instant::now(), + remaining_chunks: expected_responses, + }, + ); + } + + // If we received a goodbye, shutdown the connection. + if let InboundRequest::Goodbye(_) = req { + self.shutdown(None); + } + + self.events_out.push(Ok(RPCReceived::Request( + self.current_inbound_substream_id, + req, + ))); + self.current_inbound_substream_id.0 += 1; + } + + fn inject_fully_negotiated_outbound( + &mut self, + + out: >::Output, + request_info: Self::OutboundOpenInfo, + ) { + self.dial_negotiated -= 1; + let (id, request) = request_info; + let proto = request.protocol(); + + // accept outbound connections only if the handler is not deactivated + if matches!(self.state, HandlerState::Deactivated) { + self.events_out.push(Err(HandlerErr::Outbound { + error: RPCError::HandlerRejected, + proto, + id, + })); + } + + // add the stream to substreams if we expect a response, otherwise drop the stream. + let expected_responses = request.expected_responses(); + if expected_responses > 0 { + // new outbound request. Store the stream and tag the output. + let delay_key = self.outbound_substreams_delay.insert( + self.current_outbound_substream_id, + Duration::from_secs(RESPONSE_TIMEOUT), + ); + let awaiting_stream = OutboundSubstreamState::RequestPendingResponse { + substream: Box::new(out), + request, + }; + let expected_responses = if expected_responses > 1 { + // Currently enforced only for multiple responses + Some(expected_responses) + } else { + None + }; + if self + .outbound_substreams + .insert( + self.current_outbound_substream_id, + OutboundInfo { + state: awaiting_stream, + delay_key, + proto, + remaining_chunks: expected_responses, + req_id: id, + }, + ) + .is_some() + { + error!(id = ?self.current_outbound_substream_id, "Duplicate outbound substream id"); + } + self.current_outbound_substream_id.0 += 1; + } + } + + fn inject_event(&mut self, rpc_event: Self::InEvent) { + match rpc_event { + RPCSend::Request(id, req) => self.send_request(id, req), + RPCSend::Response(inbound_id, response) => self.send_response(inbound_id, response), + RPCSend::Shutdown(id, reason) => self.shutdown(Some((id, reason))), + } + // In any case, we need the handler to process the event. + if let Some(waker) = &self.waker { + waker.wake_by_ref(); + } + } + + fn inject_dial_upgrade_error( + &mut self, + request_info: Self::OutboundOpenInfo, + error: ConnectionHandlerUpgrErr< + >::Error, + >, + ) { + let (id, req) = request_info; + if let ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Apply(RPCError::IoError(_))) = error + { + self.outbound_io_error_retries += 1; + if self.outbound_io_error_retries < IO_ERROR_RETRIES { + self.send_request(id, req); + return; + } + } + + // This dialing is now considered failed + self.dial_negotiated -= 1; + + self.outbound_io_error_retries = 0; + // map the error + let error = match error { + ConnectionHandlerUpgrErr::Timer => RPCError::InternalError("Timer failed"), + ConnectionHandlerUpgrErr::Timeout => RPCError::NegotiationTimeout, + ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Apply(e)) => e, + ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Select(NegotiationError::Failed)) => { + RPCError::UnsupportedProtocol + } + ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Select( + NegotiationError::ProtocolError(e), + )) => match e { + ProtocolError::IoError(io_err) => RPCError::IoError(io_err.to_string()), + ProtocolError::InvalidProtocol => { + RPCError::InternalError("Protocol was deemed invalid") + } + ProtocolError::InvalidMessage | ProtocolError::TooManyProtocols => { + // Peer is sending invalid data during the negotiation phase, not + // participating in the protocol + RPCError::InvalidData("Invalid message during negotiation".to_string()) + } + }, + }; + self.events_out.push(Err(HandlerErr::Outbound { + error, + proto: req.protocol(), + id, + })); + } + + fn connection_keep_alive(&self) -> KeepAlive { + // Check that we don't have outbound items pending for dialing, nor dialing, nor + // established. Also check that there are no established inbound substreams. + // Errors and events need to be reported back, so check those too. + let should_shutdown = match self.state { + HandlerState::ShuttingDown(_) => { + self.dial_queue.is_empty() + && self.outbound_substreams.is_empty() + && self.inbound_substreams.is_empty() + && self.events_out.is_empty() + && self.dial_negotiated == 0 + } + HandlerState::Deactivated => { + // Regardless of events, the timeout has expired. Force the disconnect. + true + } + _ => false, + }; + if should_shutdown { + KeepAlive::No + } else { + KeepAlive::Yes + } + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll< + ConnectionHandlerEvent< + Self::OutboundProtocol, + Self::OutboundOpenInfo, + Self::OutEvent, + Self::Error, + >, + > { + if let Some(waker) = &self.waker { + if waker.will_wake(cx.waker()) { + self.waker = Some(cx.waker().clone()); + } + } else { + self.waker = Some(cx.waker().clone()); + } + // return any events that need to be reported + if !self.events_out.is_empty() { + return Poll::Ready(ConnectionHandlerEvent::Custom(self.events_out.remove(0))); + } else { + self.events_out.shrink_to_fit(); + } + + // Check if we are shutting down, and if the timer ran out + if let HandlerState::ShuttingDown(delay) = &self.state { + if delay.is_elapsed() { + self.state = HandlerState::Deactivated; + debug!("Handler deactivated"); + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::InternalError( + "Shutdown timeout", + ))); + } + } + + // purge expired inbound substreams and send an error + loop { + match self.inbound_substreams_delay.poll_expired(cx) { + Poll::Ready(Some(Ok(inbound_id))) => { + // handle a stream timeout for various states + if let Some(info) = self.inbound_substreams.get_mut(inbound_id.get_ref()) { + // the delay has been removed + info.delay_key = None; + self.events_out.push(Err(HandlerErr::Inbound { + error: RPCError::StreamTimeout, + proto: info.protocol, + id: *inbound_id.get_ref(), + })); + + if info.pending_items.back().map(|l| l.close_after()) == Some(false) { + // if the last chunk does not close the stream, append an error + info.pending_items.push_back(RPCCodedResponse::Error( + RPCResponseErrorCode::ServerError, + "Request timed out".into(), + )); + } + } + } + Poll::Ready(Some(Err(e))) => { + warn!(error = ?e, "Inbound substream poll failed"); + // drops the peer if we cannot read the delay queue + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::InternalError( + "Could not poll inbound stream timer", + ))); + } + Poll::Pending | Poll::Ready(None) => break, + } + } + + // purge expired outbound substreams + loop { + match self.outbound_substreams_delay.poll_expired(cx) { + Poll::Ready(Some(Ok(outbound_id))) => { + if let Some(OutboundInfo { proto, req_id, .. }) = + self.outbound_substreams.remove(outbound_id.get_ref()) + { + let outbound_err = HandlerErr::Outbound { + id: req_id, + proto, + error: RPCError::StreamTimeout, + }; + // notify the user + return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); + } else { + error!(stream_id = ?outbound_id.get_ref(), "timed out substream not in the books"); + } + } + Poll::Ready(Some(Err(e))) => { + warn!(error = ?e, "Outbound substream poll failed"); + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::InternalError( + "Could not poll outbound stream timer", + ))); + } + Poll::Pending | Poll::Ready(None) => break, + } + } + + // when deactivated, close all streams + let deactivated = matches!(self.state, HandlerState::Deactivated); + + // drive inbound streams that need to be processed + let mut substreams_to_remove = Vec::new(); // Closed substreams that need to be removed + for (id, info) in self.inbound_substreams.iter_mut() { + loop { + match std::mem::replace(&mut info.state, InboundState::Poisoned) { + // This state indicates that we are not currently sending any messages to the + // peer. We need to check if there are messages to send, if so, start the + // sending process. + InboundState::Idle(substream) if !deactivated => { + // Process one more message if one exists. + if let Some(message) = info.pending_items.pop_front() { + // If this is the last chunk, terminate the stream. + let last_chunk = info.remaining_chunks <= 1; + let fut = + send_message_to_inbound_substream(substream, message, last_chunk) + .boxed(); + // Update the state and try to process this further. + info.state = InboundState::Busy(Box::pin(fut)); + } else { + // There is nothing left to process. Set the stream to idle and + // move on to the next one. + info.state = InboundState::Idle(substream); + break; + } + } + // This state indicates we are not sending at the moment, and the handler is in + // the process of closing the connection to the peer. + InboundState::Idle(mut substream) => { + // Handler is deactivated, close the stream and mark it for removal + match substream.close().poll_unpin(cx) { + // if we can't close right now, put the substream back and try again + // immediately, continue to do this until we close the substream. + Poll::Pending => info.state = InboundState::Idle(substream), + Poll::Ready(res) => { + // The substream closed, we remove it from the mapping and remove + // the timeout + substreams_to_remove.push(*id); + if let Some(ref delay_key) = info.delay_key { + self.inbound_substreams_delay.remove(delay_key); + } + // If there was an error in shutting down the substream report the + // error + if let Err(error) = res { + self.events_out.push(Err(HandlerErr::Inbound { + error, + proto: info.protocol, + id: *id, + })); + } + // If there are still requests to send, report that we are in the + // process of closing a connection to the peer and that we are not + // processing these excess requests. + if info.pending_items.back().map(|l| l.close_after()) == Some(false) + { + // if the request was still active, report back to cancel it + self.events_out.push(Err(HandlerErr::Inbound { + error: RPCError::HandlerRejected, + proto: info.protocol, + id: *id, + })); + } + } + } + break; + } + // This state indicates that there are messages to send back to the peer. + // The future here is built by the `process_inbound_substream` function. The + // output returns a substream and whether it was closed in this operation. + InboundState::Busy(mut fut) => { + // Check if the future has completed (i.e we have completed sending all our + // pending items) + match fut.poll_unpin(cx) { + // The pending messages have been sent successfully + Poll::Ready(Ok((substream, substream_was_closed))) + if !substream_was_closed => + { + // The substream is still active, decrement the remaining + // chunks expected. + info.remaining_chunks = info.remaining_chunks.saturating_sub(1); + + // If this substream has not ended, we reset the timer. + // Each chunk is allowed RESPONSE_TIMEOUT to be sent. + if let Some(ref delay_key) = info.delay_key { + self.inbound_substreams_delay + .reset(delay_key, Duration::from_secs(RESPONSE_TIMEOUT)); + } + + // The stream may be currently idle. Attempt to process more + // elements + if !deactivated && !info.pending_items.is_empty() { + // Process one more message if one exists. + if let Some(message) = info.pending_items.pop_front() { + // If this is the last chunk, terminate the stream. + let last_chunk = info.remaining_chunks <= 1; + let fut = send_message_to_inbound_substream( + substream, message, last_chunk, + ) + .boxed(); + // Update the state and try to process this further. + info.state = InboundState::Busy(Box::pin(fut)); + } + } else { + // There is nothing left to process. Set the stream to idle and + // move on to the next one. + info.state = InboundState::Idle(substream); + break; + } + } + // The pending messages have been sent successfully and the stream has + // terminated + Poll::Ready(Ok((_substream, _substream_was_closed))) => { + // The substream has closed. Remove the timeout related to the + // substream. + substreams_to_remove.push(*id); + if let Some(ref delay_key) = info.delay_key { + self.inbound_substreams_delay.remove(delay_key); + } + + // BlocksByRange is the one that typically consumes the most time. + // Its useful to log when the request was completed. + if matches!(info.protocol, Protocol::DataByHash) { + debug!(duration = %Instant::now().duration_since(info.request_start_time).as_secs(), "DataByHash Response sent"); + } + + // There is nothing more to process on this substream as it has + // been closed. Move on to the next one. + break; + } + // An error occurred when trying to send a response. + // This means we terminate the substream. + Poll::Ready(Err(error)) => { + // Remove the stream timeout from the mapping + substreams_to_remove.push(*id); + if let Some(ref delay_key) = info.delay_key { + self.inbound_substreams_delay.remove(delay_key); + } + // Report the error that occurred during the send process + self.events_out.push(Err(HandlerErr::Inbound { + error, + proto: info.protocol, + id: *id, + })); + + if matches!(info.protocol, Protocol::DataByHash) { + debug!(duration = %info.request_start_time.elapsed().as_secs(), "DataByHash Response failed"); + } + break; + } + // The sending future has not completed. Leave the state as busy and + // try to progress later. + Poll::Pending => { + info.state = InboundState::Busy(fut); + break; + } + }; + } + InboundState::Poisoned => { + unreachable!("Poisoned inbound substream") + } + } + } + } + + // Remove closed substreams + for inbound_id in substreams_to_remove { + self.inbound_substreams.remove(&inbound_id); + } + + // drive outbound streams that need to be processed + for outbound_id in self.outbound_substreams.keys().copied().collect::>() { + // get the state and mark it as poisoned + let (mut entry, state) = match self.outbound_substreams.entry(outbound_id) { + Entry::Occupied(mut entry) => { + let state = std::mem::replace( + &mut entry.get_mut().state, + OutboundSubstreamState::Poisoned, + ); + (entry, state) + } + Entry::Vacant(_) => unreachable!(), + }; + + match state { + OutboundSubstreamState::RequestPendingResponse { + substream, + request: _, + } if deactivated => { + // the handler is deactivated. Close the stream + entry.get_mut().state = OutboundSubstreamState::Closing(substream); + self.events_out.push(Err(HandlerErr::Outbound { + error: RPCError::HandlerRejected, + proto: entry.get().proto, + id: entry.get().req_id, + })) + } + OutboundSubstreamState::RequestPendingResponse { + mut substream, + request, + } => match substream.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(response))) => { + if request.expected_responses() > 1 && !response.close_after() { + let substream_entry = entry.get_mut(); + let delay_key = &substream_entry.delay_key; + // chunks left after this one + let remaining_chunks = substream_entry + .remaining_chunks + .map(|count| count.saturating_sub(1)) + .unwrap_or_else(|| 0); + if remaining_chunks == 0 { + // this is the last expected message, close the stream as all expected chunks have been received + substream_entry.state = OutboundSubstreamState::Closing(substream); + } else { + // If the response chunk was expected update the remaining number of chunks expected and reset the Timeout + substream_entry.state = + OutboundSubstreamState::RequestPendingResponse { + substream, + request, + }; + substream_entry.remaining_chunks = Some(remaining_chunks); + self.outbound_substreams_delay + .reset(delay_key, Duration::from_secs(RESPONSE_TIMEOUT)); + } + } else { + // either this is a single response request or this response closes the + // stream + entry.get_mut().state = OutboundSubstreamState::Closing(substream); + } + + // Check what type of response we got and report it accordingly + let id = entry.get().req_id; + let proto = entry.get().proto; + + let received = match response { + RPCCodedResponse::StreamTermination(t) => { + Ok(RPCReceived::EndOfStream(id, t)) + } + RPCCodedResponse::Success(resp) => Ok(RPCReceived::Response(id, resp)), + RPCCodedResponse::Error(ref code, ref r) => Err(HandlerErr::Outbound { + id, + proto, + error: RPCError::ErrorResponse(*code, r.to_string()), + }), + }; + + return Poll::Ready(ConnectionHandlerEvent::Custom(received)); + } + Poll::Ready(None) => { + // stream closed + // if we expected multiple streams send a stream termination, + // else report the stream terminating only. + //trace!(self.log, "RPC Response - stream closed by remote"); + // drop the stream + let delay_key = &entry.get().delay_key; + let request_id = entry.get().req_id; + self.outbound_substreams_delay.remove(delay_key); + entry.remove_entry(); + // notify the application error + if request.expected_responses() > 1 { + // return an end of stream result + return Poll::Ready(ConnectionHandlerEvent::Custom(Ok( + RPCReceived::EndOfStream(request_id, request.stream_termination()), + ))); + } + + // else we return an error, stream should not have closed early. + let outbound_err = HandlerErr::Outbound { + id: request_id, + proto: request.protocol(), + error: RPCError::IncompleteStream, + }; + return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); + } + Poll::Pending => { + entry.get_mut().state = + OutboundSubstreamState::RequestPendingResponse { substream, request } + } + Poll::Ready(Some(Err(e))) => { + // drop the stream + let delay_key = &entry.get().delay_key; + self.outbound_substreams_delay.remove(delay_key); + let outbound_err = HandlerErr::Outbound { + id: entry.get().req_id, + proto: entry.get().proto, + error: e, + }; + entry.remove_entry(); + return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); + } + }, + OutboundSubstreamState::Closing(mut substream) => { + match Sink::poll_close(Pin::new(&mut substream), cx) { + Poll::Ready(_) => { + // drop the stream and its corresponding timeout + let delay_key = &entry.get().delay_key; + let protocol = entry.get().proto; + let request_id = entry.get().req_id; + self.outbound_substreams_delay.remove(delay_key); + entry.remove_entry(); + + // report the stream termination to the user + // + // Streams can be terminated here if a responder tries to + // continue sending responses beyond what we would expect. Here + // we simply terminate the stream and report a stream + // termination to the application + let termination = match protocol { + Protocol::DataByHash => Some(ResponseTermination::DataByHash), + _ => None, // all other protocols are do not have multiple responses and we do not inform the user, we simply drop the stream. + }; + + if let Some(termination) = termination { + return Poll::Ready(ConnectionHandlerEvent::Custom(Ok( + RPCReceived::EndOfStream(request_id, termination), + ))); + } + } + Poll::Pending => { + entry.get_mut().state = OutboundSubstreamState::Closing(substream); + } + } + } + OutboundSubstreamState::Poisoned => { + error!("Poisoned outbound substream"); + unreachable!("Coding Error: Outbound substream is poisoned") + } + } + } + + // establish outbound substreams + if !self.dial_queue.is_empty() && self.dial_negotiated < self.max_dial_negotiated { + self.dial_negotiated += 1; + let (id, req) = self.dial_queue.remove(0); + self.dial_queue.shrink_to_fit(); + return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest { + protocol: SubstreamProtocol::new( + OutboundRequestContainer { + req: req.clone(), + max_rpc_size: max_rpc_size(), + }, + (), + ) + .map_info(|()| (id, req)), + }); + } + + // Check if we have completed sending a goodbye, disconnect. + if let HandlerState::ShuttingDown(_) = self.state { + if self.dial_queue.is_empty() + && self.outbound_substreams.is_empty() + && self.inbound_substreams.is_empty() + && self.events_out.is_empty() + && self.dial_negotiated == 0 + { + return Poll::Ready(ConnectionHandlerEvent::Close(RPCError::Disconnected)); + } + } + + Poll::Pending + } +} + +/// Creates a future that can be polled that will send any queued message to the peer. +/// +/// This function returns the given substream, along with whether it has been closed or not. Any +/// error that occurred with sending a message is reported also. +async fn send_message_to_inbound_substream( + mut substream: InboundSubstream, + message: RPCCodedResponse, + last_chunk: bool, +) -> Result<(InboundSubstream, bool), RPCError> { + if matches!(message, RPCCodedResponse::StreamTermination(_)) { + substream.close().await.map(|_| (substream, true)) + } else { + // chunks that are not stream terminations get sent, and the stream is closed if + // the response is an error + let is_error = matches!(message, RPCCodedResponse::Error(..)); + + let send_result = substream.send(message).await; + + // If we need to close the substream, do so and return the result. + if last_chunk || is_error || send_result.is_err() { + let close_result = substream.close().await.map(|_| (substream, true)); + // If there was an error in sending, return this error, otherwise, return the + // result of closing the substream. + return send_result.and(close_result); + } + // Everything worked as expected return the result. + send_result.map(|_| (substream, false)) + } +} diff --git a/node/network/src/rpc/methods.rs b/node/network/src/rpc/methods.rs new file mode 100644 index 0000000..c2f2777 --- /dev/null +++ b/node/network/src/rpc/methods.rs @@ -0,0 +1,367 @@ +//! Available RPC methods types and ids. + +use regex::bytes::Regex; +use ssz_derive::{Decode, Encode}; +use ssz_types::{ + typenum::{U1024, U256}, + VariableList, +}; +use std::ops::Deref; +use strum::IntoStaticStr; +pub type Hash256 = ethereum_types::H256; +use shared_types::{ChunkArrayWithProof, TxID}; + +pub use ssz_types::{typenum, typenum::Unsigned, BitList, BitVector, FixedVector}; + +/// Maximum number of blocks in a single request. +pub type MaxRequestBlocks = U1024; +pub const MAX_REQUEST_BLOCKS: u64 = 1024; + +/// Maximum length of error message. +pub type MaxErrorLen = U256; +pub const MAX_ERROR_LEN: u64 = 256; + +/// Maximum length of data message. +pub type MaxDataLen = U256; +pub const MAX_DATA_LEN: u64 = 256; + +// Maximum length of GetChunksResponse chunk data. +pub const MAX_CHUNKS_LENGTH: usize = 10 * 1024 * 1024; // 10M + +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct ZgsData { + pub hash: Hash256, +} + +/// Wrapper over SSZ List to represent error message in rpc responses. +#[derive(Debug, Clone)] +pub struct ErrorType(pub VariableList); + +impl From for ErrorType { + fn from(s: String) -> Self { + Self(VariableList::from(s.as_bytes().to_vec())) + } +} + +impl From<&str> for ErrorType { + fn from(s: &str) -> Self { + Self(VariableList::from(s.as_bytes().to_vec())) + } +} + +impl Deref for ErrorType { + type Target = VariableList; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ToString for ErrorType { + fn to_string(&self) -> String { + #[allow(clippy::invalid_regex)] + let re = Regex::new("\\p{C}").expect("Regex is valid"); + String::from_utf8_lossy(&re.replace_all(self.0.deref(), &b""[..])).to_string() + } +} + +/* Request/Response data structures for RPC methods */ + +/* Requests */ + +/// The STATUS request/response handshake message. +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct StatusMessage { + pub data: u64, +} + +/// The PING request/response message. +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct Ping { + /// The metadata sequence number. + pub data: u64, +} + +/// The reason given for a `Goodbye` message. +/// +/// Note: any unknown `u64::into(n)` will resolve to `Goodbye::Unknown` for any unknown `n`, +/// however `GoodbyeReason::Unknown.into()` will go into `0_u64`. Therefore de-serializing then +/// re-serializing may not return the same bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GoodbyeReason { + /// This node has shutdown. + ClientShutdown = 1, + + /// Incompatible networks. + IrrelevantNetwork = 2, + + /// Error/fault in the RPC. + Fault = 3, + + /// Teku uses this code for not being able to verify a network. + UnableToVerifyNetwork = 128, + + /// The node has too many connected peers. + TooManyPeers = 129, + + /// Scored poorly. + BadScore = 250, + + /// The peer is banned + Banned = 251, + + /// The IP address the peer is using is banned. + BannedIP = 252, + + /// Unknown reason. + Unknown = 0, +} + +impl From for GoodbyeReason { + fn from(id: u64) -> GoodbyeReason { + match id { + 1 => GoodbyeReason::ClientShutdown, + 2 => GoodbyeReason::IrrelevantNetwork, + 3 => GoodbyeReason::Fault, + 128 => GoodbyeReason::UnableToVerifyNetwork, + 129 => GoodbyeReason::TooManyPeers, + 250 => GoodbyeReason::BadScore, + 251 => GoodbyeReason::Banned, + 252 => GoodbyeReason::BannedIP, + _ => GoodbyeReason::Unknown, + } + } +} + +impl From for u64 { + fn from(reason: GoodbyeReason) -> u64 { + reason as u64 + } +} + +impl ssz::Encode for GoodbyeReason { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + ::ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + 0_u64.ssz_bytes_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + let conv: u64 = self.clone().into(); + conv.ssz_append(buf) + } +} + +impl ssz::Decode for GoodbyeReason { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + ::ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + u64::from_ssz_bytes(bytes).map(|n| n.into()) + } +} + +/// Request a number of beacon block bodies from a peer. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DataByHashRequest { + /// The list of beacon block bodies being requested. + pub hashes: VariableList, +} + +/// Request a chunk array from a peer. +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct GetChunksRequest { + pub tx_id: TxID, + pub index_start: u64, + pub index_end: u64, +} + +/* RPC Handling and Grouping */ +// Collection of enums and structs used by the Codecs to encode/decode RPC messages + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RPCResponse { + /// A HELLO message. + Status(StatusMessage), + + /// A PONG response to a PING request. + Pong(Ping), + + /// A response to a get DATA_BY_HASH request. + DataByHash(Box), + + /// A response to a GET_CHUNKS request. + Chunks(ChunkArrayWithProof), +} + +/// Indicates which response is being terminated by a stream termination response. +#[derive(Debug, Clone)] +pub enum ResponseTermination { + /// Data by hash stream termination. + DataByHash, +} + +/// The structured response containing a result/code indicating success or failure +/// and the contents of the response +#[derive(Debug)] +pub enum RPCCodedResponse { + /// The response is a successful. + Success(RPCResponse), + + Error(RPCResponseErrorCode, ErrorType), + + /// Received a stream termination indicating which response is being terminated. + StreamTermination(ResponseTermination), +} + +/// The code assigned to an erroneous `RPCResponse`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum RPCResponseErrorCode { + RateLimited, + InvalidRequest, + ServerError, + /// Error spec'd to indicate that a peer does not have blocks on a requested range. + ResourceUnavailable, + Unknown, +} + +impl RPCCodedResponse { + /// Used to encode the response in the codec. + pub fn as_u8(&self) -> Option { + match self { + RPCCodedResponse::Success(_) => Some(0), + RPCCodedResponse::Error(code, _) => Some(code.as_u8()), + RPCCodedResponse::StreamTermination(_) => None, + } + } + + /// Tells the codec whether to decode as an RPCResponse or an error. + pub fn is_response(response_code: u8) -> bool { + matches!(response_code, 0) + } + + /// Builds an RPCCodedResponse from a response code and an ErrorMessage + pub fn from_error(response_code: u8, err: ErrorType) -> Self { + let code = match response_code { + 1 => RPCResponseErrorCode::InvalidRequest, + 2 => RPCResponseErrorCode::ServerError, + 3 => RPCResponseErrorCode::ResourceUnavailable, + 139 => RPCResponseErrorCode::RateLimited, + _ => RPCResponseErrorCode::Unknown, + }; + RPCCodedResponse::Error(code, err) + } + + /// Specifies which response allows for multiple chunks for the stream handler. + pub fn multiple_responses(&self) -> bool { + match self { + RPCCodedResponse::Success(resp) => match resp { + RPCResponse::Status(_) => false, + RPCResponse::Pong(_) => false, + RPCResponse::DataByHash(_) => true, + RPCResponse::Chunks(_) => false, + }, + RPCCodedResponse::Error(_, _) => true, + // Stream terminations are part of responses that have chunks + RPCCodedResponse::StreamTermination(_) => true, + } + } + + /// Returns true if this response always terminates the stream. + pub fn close_after(&self) -> bool { + !matches!(self, RPCCodedResponse::Success(_)) + } +} + +impl RPCResponseErrorCode { + fn as_u8(&self) -> u8 { + match self { + RPCResponseErrorCode::InvalidRequest => 1, + RPCResponseErrorCode::ServerError => 2, + RPCResponseErrorCode::ResourceUnavailable => 3, + RPCResponseErrorCode::Unknown => 255, + RPCResponseErrorCode::RateLimited => 139, + } + } +} + +impl std::fmt::Display for RPCResponseErrorCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + RPCResponseErrorCode::InvalidRequest => "The request was invalid", + RPCResponseErrorCode::ResourceUnavailable => "Resource unavailable", + RPCResponseErrorCode::ServerError => "Server error occurred", + RPCResponseErrorCode::Unknown => "Unknown error occurred", + RPCResponseErrorCode::RateLimited => "Rate limited", + }; + f.write_str(repr) + } +} + +impl std::fmt::Display for StatusMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Status Message: Data: {:?}", self.data) + } +} + +impl std::fmt::Display for RPCResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RPCResponse::Status(status) => write!(f, "{}", status), + RPCResponse::Pong(ping) => write!(f, "Pong: {}", ping.data), + RPCResponse::DataByHash(data) => { + write!(f, "DataByHash: Hash: {:?}", data.hash) + } + RPCResponse::Chunks(data) => { + write!( + f, + "Chunks Response, data length: {}", + data.chunks.data.len() + ) + } + } + } +} + +impl std::fmt::Display for RPCCodedResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RPCCodedResponse::Success(res) => write!(f, "{}", res), + RPCCodedResponse::Error(code, err) => { + write!(f, "{}: {}", code, err.to_string()) + } + RPCCodedResponse::StreamTermination(_) => { + write!(f, "Stream Termination") + } + } + } +} + +impl std::fmt::Display for GoodbyeReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GoodbyeReason::ClientShutdown => write!(f, "Client Shutdown"), + GoodbyeReason::IrrelevantNetwork => write!(f, "Irrelevant Network"), + GoodbyeReason::Fault => write!(f, "Fault"), + GoodbyeReason::UnableToVerifyNetwork => { + write!(f, "Unable to verify network") + } + GoodbyeReason::TooManyPeers => write!(f, "Too many peers"), + GoodbyeReason::BadScore => write!(f, "Bad Score"), + GoodbyeReason::Banned => write!(f, "Banned"), + GoodbyeReason::BannedIP => write!(f, "BannedIP"), + GoodbyeReason::Unknown => write!(f, "Unknown Reason"), + } + } +} diff --git a/node/network/src/rpc/mod.rs b/node/network/src/rpc/mod.rs new file mode 100644 index 0000000..51552d4 --- /dev/null +++ b/node/network/src/rpc/mod.rs @@ -0,0 +1,260 @@ +//! The Ethereum 2.0 Wire Protocol +//! +//! This protocol is a purpose built Ethereum 2.0 libp2p protocol. It's role is to facilitate +//! direct peer-to-peer communication primarily for sending/receiving chain information for +//! syncing. + +use futures::future::FutureExt; +use handler::{HandlerEvent, RPCHandler}; +use libp2p::core::connection::ConnectionId; +use libp2p::swarm::{ + handler::ConnectionHandler, NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, + PollParameters, SubstreamProtocol, +}; +use libp2p::PeerId; +use rate_limiter::{RPCRateLimiter as RateLimiter, RPCRateLimiterBuilder, RateLimitedErr}; +use std::task::{Context, Poll}; +use std::time::Duration; + +pub(crate) use handler::HandlerErr; +pub(crate) use methods::{Ping, RPCCodedResponse, RPCResponse}; +pub(crate) use protocol::{InboundRequest, RPCProtocol}; + +pub use handler::SubstreamId; +pub use methods::{ + DataByHashRequest, GetChunksRequest, GoodbyeReason, MaxRequestBlocks, ZgsData, + RPCResponseErrorCode, ResponseTermination, StatusMessage, MAX_REQUEST_BLOCKS, +}; +pub(crate) use outbound::OutboundRequest; +pub use protocol::{max_rpc_size, Protocol, RPCError}; + +pub(crate) mod codec; +mod handler; +pub mod methods; +mod outbound; +mod protocol; +mod rate_limiter; + +/// Composite trait for a request id. +pub trait ReqId: Send + 'static + std::fmt::Debug + Copy + Clone {} +impl ReqId for T where T: Send + 'static + std::fmt::Debug + Copy + Clone {} + +/// RPC events sent from Lighthouse. +#[derive(Debug)] +pub enum RPCSend { + /// A request sent from Lighthouse. + /// + /// The `Id` is given by the application making the request. These + /// go over *outbound* connections. + Request(Id, OutboundRequest), + /// A response sent from Lighthouse. + /// + /// The `SubstreamId` must correspond to the RPC-given ID of the original request received from the + /// peer. The second parameter is a single chunk of a response. These go over *inbound* + /// connections. + Response(SubstreamId, RPCCodedResponse), + /// Lighthouse has requested to terminate the connection with a goodbye message. + Shutdown(Id, GoodbyeReason), +} + +/// RPC events received from outside Lighthouse. +#[derive(Debug)] +pub enum RPCReceived { + /// A request received from the outside. + /// + /// The `SubstreamId` is given by the `RPCHandler` as it identifies this request with the + /// *inbound* substream over which it is managed. + Request(SubstreamId, InboundRequest), + /// A response received from the outside. + /// + /// The `Id` corresponds to the application given ID of the original request sent to the + /// peer. The second parameter is a single chunk of a response. These go over *outbound* + /// connections. + Response(Id, RPCResponse), + /// Marks a request as completed + EndOfStream(Id, ResponseTermination), +} + +impl std::fmt::Display for RPCSend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RPCSend::Request(id, req) => { + write!(f, "RPC Request(id: {:?}, {})", id, req) + } + RPCSend::Response(id, res) => { + write!(f, "RPC Response(id: {:?}, {})", id, res) + } + RPCSend::Shutdown(_id, reason) => { + write!(f, "Sending Goodbye: {}", reason) + } + } + } +} + +/// Messages sent to the user from the RPC protocol. +pub struct RPCMessage { + /// The peer that sent the message. + pub peer_id: PeerId, + /// Handler managing this message. + pub conn_id: ConnectionId, + /// The message that was sent. + pub event: HandlerEvent, +} + +/// Implements the libp2p `NetworkBehaviour` trait and therefore manages network-level +/// logic. +pub struct RPC { + /// Rate limiter + limiter: RateLimiter, + /// Queue of events to be processed. + events: Vec, RPCHandler>>, +} + +impl RPC { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let limiter = RPCRateLimiterBuilder::new() + .n_every(Protocol::Ping, 2, Duration::from_secs(10)) + .n_every(Protocol::Status, 5, Duration::from_secs(15)) + .one_every(Protocol::Goodbye, Duration::from_secs(10)) + .n_every(Protocol::DataByHash, 128, Duration::from_secs(10)) + .n_every(Protocol::GetChunks, 4096, Duration::from_secs(10)) + .build() + .expect("Configuration parameters are valid"); + RPC { + limiter, + events: Vec::new(), + } + } + + /// Sends an RPC response. + /// + /// The peer must be connected for this to succeed. + pub fn send_response( + &mut self, + peer_id: PeerId, + id: (ConnectionId, SubstreamId), + event: RPCCodedResponse, + ) { + self.events.push(NetworkBehaviourAction::NotifyHandler { + peer_id, + handler: NotifyHandler::One(id.0), + event: RPCSend::Response(id.1, event), + }); + } + + /// Submits an RPC request. + /// + /// The peer must be connected for this to succeed. + pub fn send_request(&mut self, peer_id: PeerId, request_id: Id, event: OutboundRequest) { + self.events.push(NetworkBehaviourAction::NotifyHandler { + peer_id, + handler: NotifyHandler::Any, + event: RPCSend::Request(request_id, event), + }); + } + + /// Lighthouse wishes to disconnect from this peer by sending a Goodbye message. This + /// gracefully terminates the RPC behaviour with a goodbye message. + pub fn shutdown(&mut self, peer_id: PeerId, id: Id, reason: GoodbyeReason) { + self.events.push(NetworkBehaviourAction::NotifyHandler { + peer_id, + handler: NotifyHandler::Any, + event: RPCSend::Shutdown(id, reason), + }); + } +} + +impl NetworkBehaviour for RPC +where + Id: ReqId, +{ + type ConnectionHandler = RPCHandler; + type OutEvent = RPCMessage; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + RPCHandler::new(SubstreamProtocol::new( + RPCProtocol { + max_rpc_size: max_rpc_size(), + }, + (), + )) + } + + fn inject_event( + &mut self, + peer_id: PeerId, + conn_id: ConnectionId, + event: ::OutEvent, + ) { + if let Ok(RPCReceived::Request(ref id, ref req)) = event { + // check if the request is conformant to the quota + match self.limiter.allows(&peer_id, req) { + Ok(()) => { + // send the event to the user + self.events + .push(NetworkBehaviourAction::GenerateEvent(RPCMessage { + peer_id, + conn_id, + event, + })) + } + Err(RateLimitedErr::TooLarge) => { + // we set the batch sizes, so this is a coding/config err for most protocols + let protocol = req.protocol(); + error!(%protocol, "Request size too large to ever be processed"); + + // send an error code to the peer. + // the handler upon receiving the error code will send it back to the behaviour + self.send_response( + peer_id, + (conn_id, *id), + RPCCodedResponse::Error( + RPCResponseErrorCode::RateLimited, + "Rate limited. Request too large".into(), + ), + ); + } + Err(RateLimitedErr::TooSoon(wait_time)) => { + debug!( + request = %req, + %peer_id, + wait_time_ms = %wait_time.as_millis(), + "Request exceeds the rate limit", + ); + + // send an error code to the peer. + // the handler upon receiving the error code will send it back to the behaviour + self.send_response( + peer_id, + (conn_id, *id), + RPCCodedResponse::Error( + RPCResponseErrorCode::RateLimited, + format!("Wait {:?}", wait_time).into(), + ), + ); + } + } + } else { + self.events + .push(NetworkBehaviourAction::GenerateEvent(RPCMessage { + peer_id, + conn_id, + event, + })); + } + } + + fn poll( + &mut self, + cx: &mut Context, + _: &mut impl PollParameters, + ) -> Poll> { + // let the rate limiter prune + let _ = self.limiter.poll_unpin(cx); + if !self.events.is_empty() { + return Poll::Ready(self.events.remove(0)); + } + Poll::Pending + } +} diff --git a/node/network/src/rpc/outbound.rs b/node/network/src/rpc/outbound.rs new file mode 100644 index 0000000..30a6bdc --- /dev/null +++ b/node/network/src/rpc/outbound.rs @@ -0,0 +1,178 @@ +use super::methods::*; +use super::protocol::Protocol; +use super::protocol::ProtocolId; +use super::RPCError; +use crate::rpc::protocol::Encoding; +use crate::rpc::protocol::Version; +use crate::rpc::{ + codec::{base::BaseOutboundCodec, ssz_snappy::SSZSnappyOutboundCodec, OutboundCodec}, + methods::ResponseTermination, +}; +use futures::future::BoxFuture; +use futures::prelude::{AsyncRead, AsyncWrite}; +use futures::{FutureExt, SinkExt}; +use libp2p::core::{OutboundUpgrade, UpgradeInfo}; +use tokio_util::{ + codec::Framed, + compat::{Compat, FuturesAsyncReadCompatExt}, +}; + +/* Outbound request */ + +// Combines all the RPC requests into a single enum to implement `UpgradeInfo` and +// `OutboundUpgrade` + +#[derive(Debug, Clone)] +pub struct OutboundRequestContainer { + pub req: OutboundRequest, + pub max_rpc_size: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OutboundRequest { + Status(StatusMessage), + Goodbye(GoodbyeReason), + Ping(Ping), + DataByHash(DataByHashRequest), + GetChunks(GetChunksRequest), +} + +impl UpgradeInfo for OutboundRequestContainer { + type Info = ProtocolId; + type InfoIter = Vec; + + // add further protocols as we support more encodings/versions + fn protocol_info(&self) -> Self::InfoIter { + self.req.supported_protocols() + } +} + +/// Implements the encoding per supported protocol for `RPCRequest`. +impl OutboundRequest { + pub fn supported_protocols(&self) -> Vec { + match self { + // add more protocols when versions/encodings are supported + OutboundRequest::Status(_) => vec![ProtocolId::new( + Protocol::Status, + Version::V1, + Encoding::SSZSnappy, + )], + OutboundRequest::Goodbye(_) => vec![ProtocolId::new( + Protocol::Goodbye, + Version::V1, + Encoding::SSZSnappy, + )], + OutboundRequest::Ping(_) => vec![ProtocolId::new( + Protocol::Ping, + Version::V1, + Encoding::SSZSnappy, + )], + OutboundRequest::DataByHash(_) => vec![ProtocolId::new( + Protocol::DataByHash, + Version::V1, + Encoding::SSZSnappy, + )], + OutboundRequest::GetChunks(_) => vec![ProtocolId::new( + Protocol::GetChunks, + Version::V1, + Encoding::SSZSnappy, + )], + } + } + + /* These functions are used in the handler for stream management */ + + /// Number of responses expected for this request. + pub fn expected_responses(&self) -> u64 { + match self { + OutboundRequest::Status(_) => 1, + OutboundRequest::Goodbye(_) => 0, + OutboundRequest::Ping(_) => 1, + OutboundRequest::DataByHash(req) => req.hashes.len() as u64, + OutboundRequest::GetChunks(_) => 1, + } + } + + /// Gives the corresponding `Protocol` to this request. + pub fn protocol(&self) -> Protocol { + match self { + OutboundRequest::Status(_) => Protocol::Status, + OutboundRequest::Goodbye(_) => Protocol::Goodbye, + OutboundRequest::Ping(_) => Protocol::Ping, + OutboundRequest::DataByHash(_) => Protocol::DataByHash, + OutboundRequest::GetChunks(_) => Protocol::GetChunks, + } + } + + /// Returns the `ResponseTermination` type associated with the request if a stream gets + /// terminated. + pub fn stream_termination(&self) -> ResponseTermination { + match self { + // this only gets called after `multiple_responses()` returns true. Therefore, only + // variants that have `multiple_responses()` can have values. + OutboundRequest::DataByHash(_) => ResponseTermination::DataByHash, + OutboundRequest::Status(_) => unreachable!(), + OutboundRequest::Goodbye(_) => unreachable!(), + OutboundRequest::Ping(_) => unreachable!(), + OutboundRequest::GetChunks(_) => unreachable!(), + } + } +} + +/* RPC Response type - used for outbound upgrades */ + +/* Outbound upgrades */ + +pub type OutboundFramed = Framed, OutboundCodec>; + +impl OutboundUpgrade for OutboundRequestContainer +where + TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + type Output = OutboundFramed; + type Error = RPCError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_outbound(self, socket: TSocket, protocol: Self::Info) -> Self::Future { + // convert to a tokio compatible socket + let socket = socket.compat(); + let codec = match protocol.encoding { + Encoding::SSZSnappy => { + let ssz_snappy_codec = BaseOutboundCodec::new(SSZSnappyOutboundCodec::new( + protocol, + self.max_rpc_size, + )); + OutboundCodec::SSZSnappy(ssz_snappy_codec) + } + }; + + let mut socket = Framed::new(socket, codec); + + async { + socket.send(self.req).await?; + socket.close().await?; + Ok(socket) + } + .boxed() + } +} + +impl std::fmt::Display for OutboundRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OutboundRequest::Status(status) => { + write!(f, "Status Message: {}", status) + } + OutboundRequest::Goodbye(reason) => { + write!(f, "Goodbye: {}", reason) + } + OutboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), + OutboundRequest::DataByHash(req) => { + write!(f, "Data by hash: {:?}", req) + } + OutboundRequest::GetChunks(req) => { + write!(f, "GetChunks: {:?}", req) + } + } + } +} diff --git a/node/network/src/rpc/protocol.rs b/node/network/src/rpc/protocol.rs new file mode 100644 index 0000000..53de8df --- /dev/null +++ b/node/network/src/rpc/protocol.rs @@ -0,0 +1,542 @@ +use super::methods::*; +use crate::rpc::{ + codec::{base::BaseInboundCodec, ssz_snappy::SSZSnappyInboundCodec, InboundCodec}, + methods::{MaxErrorLen, ResponseTermination, MAX_CHUNKS_LENGTH, MAX_ERROR_LEN}, + MaxRequestBlocks, MAX_REQUEST_BLOCKS, +}; +use futures::future::BoxFuture; +use futures::prelude::{AsyncRead, AsyncWrite}; +use futures::{FutureExt, StreamExt}; +use libp2p::core::{InboundUpgrade, ProtocolName, UpgradeInfo}; +use shared_types::{ChunkArray, ChunkArrayWithProof, FlowRangeProof}; +use ssz::Encode; +use ssz_types::VariableList; +use std::io; +use std::time::Duration; +use strum::IntoStaticStr; +use tokio_io_timeout::TimeoutStream; +use tokio_util::{ + codec::Framed, + compat::{Compat, FuturesAsyncReadCompatExt}, +}; + +pub type Hash256 = ethereum_types::H256; + +lazy_static! { + pub static ref DATA_BY_HASH_REQUEST_MIN: usize = + VariableList::::from(Vec::::new()) + .as_ssz_bytes() + .len(); + pub static ref DATA_BY_HASH_REQUEST_MAX: usize = + VariableList::::from(vec![ + Hash256::zero(); + MAX_REQUEST_BLOCKS as usize + ]) + .as_ssz_bytes() + .len(); + pub static ref ERROR_TYPE_MIN: usize = VariableList::::from(Vec::::new()) + .as_ssz_bytes() + .len(); + pub static ref ERROR_TYPE_MAX: usize = + VariableList::::from(vec![0u8; MAX_ERROR_LEN as usize]) + .as_ssz_bytes() + .len(); + pub static ref CHUNKS_RESPONSE_MIN: usize = ChunkArrayWithProof { + chunks: ChunkArray { + data: vec![], + start_index: 0, + }, + proof: FlowRangeProof::new_empty(), + } + .as_ssz_bytes() + .len(); + // TODO + pub static ref CHUNKS_RESPONSE_MAX: usize = ChunkArrayWithProof { + chunks: ChunkArray { + data: vec![0u8; MAX_CHUNKS_LENGTH], + start_index: 0, + }, + proof: FlowRangeProof::new_empty(), + } + .as_ssz_bytes() + .len(); +} + +// /// The maximum bytes that can be sent across the RPC pre-merge. +// pub(crate) const MAX_RPC_SIZE: usize = 1_048_576; // 1M +/// The maximum bytes that can be sent across the RPC post-merge. +pub(crate) const MAX_RPC_SIZE: usize = 10 * 1_048_576; // 10M +/// The protocol prefix the RPC protocol id. +const PROTOCOL_PREFIX: &str = "/zgs/req"; +/// Time allowed for the first byte of a request to arrive before we time out (Time To First Byte). +const TTFB_TIMEOUT: u64 = 5; +/// The number of seconds to wait for the first bytes of a request once a protocol has been +/// established before the stream is terminated. +const REQUEST_TIMEOUT: u64 = 15; + +/// Returns the maximum bytes that can be sent across the RPC. +pub fn max_rpc_size() -> usize { + MAX_RPC_SIZE +} + +/// Protocol names to be used. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Protocol { + /// The Status protocol name. + Status, + /// The Goodbye protocol name. + Goodbye, + /// The `Ping` protocol name. + Ping, + /// TODO + DataByHash, + + /// The Chunk sync protocol. + GetChunks, +} + +/// RPC Versions +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Version { + /// Version 1 of RPC + V1, +} + +/// RPC Encondings supported. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Encoding { + SSZSnappy, +} + +impl std::fmt::Display for Protocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + Protocol::Status => "status", + Protocol::Goodbye => "goodbye", + Protocol::Ping => "ping", + Protocol::DataByHash => "data_by_hash", + Protocol::GetChunks => "get_chunks", + }; + f.write_str(repr) + } +} + +impl std::fmt::Display for Encoding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + Encoding::SSZSnappy => "ssz_snappy", + }; + f.write_str(repr) + } +} + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + Version::V1 => "1", + }; + f.write_str(repr) + } +} + +#[derive(Debug, Clone)] +pub struct RPCProtocol { + pub max_rpc_size: usize, +} + +impl UpgradeInfo for RPCProtocol { + type Info = ProtocolId; + type InfoIter = Vec; + + /// The list of supported RPC protocols for Lighthouse. + fn protocol_info(&self) -> Self::InfoIter { + vec![ + ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(Protocol::Goodbye, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(Protocol::Ping, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(Protocol::DataByHash, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(Protocol::GetChunks, Version::V1, Encoding::SSZSnappy), + ] + } +} + +/// Represents the ssz length bounds for RPC messages. +#[derive(Debug, PartialEq, Eq)] +pub struct RpcLimits { + pub min: usize, + pub max: usize, +} + +impl RpcLimits { + pub fn new(min: usize, max: usize) -> Self { + Self { min, max } + } + + /// Returns true if the given length is greater than `max_rpc_size` or out of + /// bounds for the given ssz type, returns false otherwise. + pub fn is_out_of_bounds(&self, length: usize, max_rpc_size: usize) -> bool { + length > std::cmp::min(self.max, max_rpc_size) || length < self.min + } +} + +/// Tracks the types in a protocol id. +#[derive(Clone, Debug)] +pub struct ProtocolId { + /// The RPC message type/name. + pub message_name: Protocol, + + /// The version of the RPC. + pub version: Version, + + /// The encoding of the RPC. + pub encoding: Encoding, + + /// The protocol id that is formed from the above fields. + protocol_id: String, +} + +impl ProtocolId { + /// Returns min and max size for messages of given protocol id requests. + pub fn rpc_request_limits(&self) -> RpcLimits { + match self.message_name { + Protocol::Status => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + Protocol::Goodbye => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + Protocol::Ping => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + Protocol::DataByHash => { + // RpcLimits::new(*DATA_BY_HASH_REQUEST_MIN, *DATA_BY_HASH_REQUEST_MAX) + // TODO + RpcLimits::new(1, *DATA_BY_HASH_REQUEST_MAX) + } + Protocol::GetChunks => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + } + } + + /// Returns min and max size for messages of given protocol id responses. + pub fn rpc_response_limits(&self) -> RpcLimits { + match self.message_name { + Protocol::Status => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + Protocol::Goodbye => RpcLimits::new(0, 0), // Goodbye request has no response + + Protocol::Ping => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + + // TODO: not fixed size? + Protocol::DataByHash => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), + + Protocol::GetChunks => RpcLimits::new(*CHUNKS_RESPONSE_MIN, *CHUNKS_RESPONSE_MAX), + } + } +} + +/// An RPC protocol ID. +impl ProtocolId { + pub fn new(message_name: Protocol, version: Version, encoding: Encoding) -> Self { + let protocol_id = format!( + "{}/{}/{}/{}", + PROTOCOL_PREFIX, message_name, version, encoding + ); + + ProtocolId { + message_name, + version, + encoding, + protocol_id, + } + } +} + +impl ProtocolName for ProtocolId { + fn protocol_name(&self) -> &[u8] { + self.protocol_id.as_bytes() + } +} + +/* Inbound upgrade */ + +// The inbound protocol reads the request, decodes it and returns the stream to the protocol +// handler to respond to once ready. + +pub type InboundOutput = (InboundRequest, InboundFramed); +pub type InboundFramed = + Framed>>>, InboundCodec>; + +impl InboundUpgrade for RPCProtocol +where + TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + type Output = InboundOutput; + type Error = RPCError; + type Future = BoxFuture<'static, Result>; + + fn upgrade_inbound(self, socket: TSocket, protocol: ProtocolId) -> Self::Future { + async move { + // convert the socket to tokio compatible socket + let socket = socket.compat(); + let codec = match protocol.encoding { + Encoding::SSZSnappy => { + let ssz_snappy_codec = BaseInboundCodec::new(SSZSnappyInboundCodec::new( + protocol, + self.max_rpc_size, + )); + InboundCodec::SSZSnappy(ssz_snappy_codec) + } + }; + let mut timed_socket = TimeoutStream::new(socket); + timed_socket.set_read_timeout(Some(Duration::from_secs(TTFB_TIMEOUT))); + + let socket = Framed::new(Box::pin(timed_socket), codec); + + match tokio::time::timeout(Duration::from_secs(REQUEST_TIMEOUT), socket.into_future()) + .await + { + Err(e) => Err(RPCError::from(e)), + Ok((Some(Ok(request)), stream)) => Ok((request, stream)), + Ok((Some(Err(e)), _)) => Err(e), + Ok((None, _)) => Err(RPCError::IncompleteStream), + } + } + .boxed() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InboundRequest { + Status(StatusMessage), + Goodbye(GoodbyeReason), + Ping(Ping), + DataByHash(DataByHashRequest), + GetChunks(GetChunksRequest), +} + +impl UpgradeInfo for InboundRequest { + type Info = ProtocolId; + type InfoIter = Vec; + + // add further protocols as we support more encodings/versions + fn protocol_info(&self) -> Self::InfoIter { + self.supported_protocols() + } +} + +/// Implements the encoding per supported protocol for `RPCRequest`. +impl InboundRequest { + pub fn supported_protocols(&self) -> Vec { + match self { + // add more protocols when versions/encodings are supported + InboundRequest::Status(_) => vec![ProtocolId::new( + Protocol::Status, + Version::V1, + Encoding::SSZSnappy, + )], + InboundRequest::Goodbye(_) => vec![ProtocolId::new( + Protocol::Goodbye, + Version::V1, + Encoding::SSZSnappy, + )], + InboundRequest::Ping(_) => vec![ProtocolId::new( + Protocol::Ping, + Version::V1, + Encoding::SSZSnappy, + )], + InboundRequest::DataByHash(_) => vec![ProtocolId::new( + Protocol::DataByHash, + Version::V1, + Encoding::SSZSnappy, + )], + InboundRequest::GetChunks(_) => vec![ProtocolId::new( + Protocol::GetChunks, + Version::V1, + Encoding::SSZSnappy, + )], + } + } + + /* These functions are used in the handler for stream management */ + + /// Number of responses expected for this request. + pub fn expected_responses(&self) -> u64 { + match self { + InboundRequest::Status(_) => 1, + InboundRequest::Goodbye(_) => 0, + InboundRequest::DataByHash(req) => req.hashes.len() as u64, + InboundRequest::Ping(_) => 1, + InboundRequest::GetChunks(_) => 1, + } + } + + /// Gives the corresponding `Protocol` to this request. + pub fn protocol(&self) -> Protocol { + match self { + InboundRequest::Status(_) => Protocol::Status, + InboundRequest::Goodbye(_) => Protocol::Goodbye, + InboundRequest::Ping(_) => Protocol::Ping, + InboundRequest::DataByHash(_) => Protocol::DataByHash, + InboundRequest::GetChunks(_) => Protocol::GetChunks, + } + } + + /// Returns the `ResponseTermination` type associated with the request if a stream gets + /// terminated. + pub fn stream_termination(&self) -> ResponseTermination { + match self { + // this only gets called after `multiple_responses()` returns true. Therefore, only + // variants that have `multiple_responses()` can have values. + InboundRequest::DataByHash(_) => ResponseTermination::DataByHash, + InboundRequest::Status(_) => unreachable!(), + InboundRequest::Goodbye(_) => unreachable!(), + InboundRequest::Ping(_) => unreachable!(), + InboundRequest::GetChunks(_) => unreachable!(), + } + } +} + +/// Error in RPC Encoding/Decoding. +#[derive(Debug, Clone, PartialEq, IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum RPCError { + /// Error when decoding the raw buffer from ssz. + // NOTE: in the future a ssz::DecodeError should map to an InvalidData error + #[strum(serialize = "decode_error")] + SSZDecodeError(ssz::DecodeError), + /// IO Error. + IoError(String), + /// The peer returned a valid response but the response indicated an error. + ErrorResponse(RPCResponseErrorCode, String), + /// Timed out waiting for a response. + StreamTimeout, + /// Peer does not support the protocol. + UnsupportedProtocol, + /// Stream ended unexpectedly. + IncompleteStream, + /// Peer sent invalid data. + InvalidData(String), + /// An error occurred due to internal reasons. Ex: timer failure. + InternalError(&'static str), + /// Negotiation with this peer timed out. + NegotiationTimeout, + /// Handler rejected this request. + HandlerRejected, + /// We have intentionally disconnected. + Disconnected, +} + +impl From for RPCError { + #[inline] + fn from(err: ssz::DecodeError) -> Self { + RPCError::SSZDecodeError(err) + } +} +impl From for RPCError { + fn from(_: tokio::time::error::Elapsed) -> Self { + RPCError::StreamTimeout + } +} + +impl From for RPCError { + fn from(err: io::Error) -> Self { + RPCError::IoError(err.to_string()) + } +} + +// Error trait is required for `ProtocolsHandler` +impl std::fmt::Display for RPCError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + RPCError::SSZDecodeError(ref err) => { + write!(f, "Error while decoding ssz: {:?}", err) + } + RPCError::InvalidData(ref err) => { + write!(f, "Peer sent unexpected data: {}", err) + } + RPCError::IoError(ref err) => write!(f, "IO Error: {}", err), + RPCError::ErrorResponse(ref code, ref reason) => write!( + f, + "RPC response was an error: {} with reason: {}", + code, reason + ), + RPCError::StreamTimeout => write!(f, "Stream Timeout"), + RPCError::UnsupportedProtocol => { + write!(f, "Peer does not support the protocol") + } + RPCError::IncompleteStream => { + write!(f, "Stream ended unexpectedly") + } + RPCError::InternalError(ref err) => { + write!(f, "Internal error: {}", err) + } + RPCError::NegotiationTimeout => write!(f, "Negotiation timeout"), + RPCError::HandlerRejected => { + write!(f, "Handler rejected the request") + } + RPCError::Disconnected => write!(f, "Gracefully Disconnected"), + } + } +} + +impl std::error::Error for RPCError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match *self { + // NOTE: this does have a source + RPCError::SSZDecodeError(_) => None, + RPCError::IoError(_) => None, + RPCError::StreamTimeout => None, + RPCError::UnsupportedProtocol => None, + RPCError::IncompleteStream => None, + RPCError::InvalidData(_) => None, + RPCError::InternalError(_) => None, + RPCError::ErrorResponse(_, _) => None, + RPCError::NegotiationTimeout => None, + RPCError::HandlerRejected => None, + RPCError::Disconnected => None, + } + } +} + +impl std::fmt::Display for InboundRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InboundRequest::Status(status) => { + write!(f, "Status Message: {}", status) + } + InboundRequest::Goodbye(reason) => write!(f, "Goodbye: {}", reason), + InboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), + InboundRequest::DataByHash(req) => { + write!(f, "Data by hash: {:?}", req) + } + InboundRequest::GetChunks(req) => { + write!(f, "Get Chunks: {:?}", req) + } + } + } +} + +impl RPCError { + /// Get a `str` representation of the error. + /// Used for metrics. + pub fn as_static_str(&self) -> &'static str { + match self { + RPCError::ErrorResponse(ref code, ..) => code.into(), + e => e.into(), + } + } +} diff --git a/node/network/src/rpc/rate_limiter.rs b/node/network/src/rpc/rate_limiter.rs new file mode 100644 index 0000000..c213146 --- /dev/null +++ b/node/network/src/rpc/rate_limiter.rs @@ -0,0 +1,388 @@ +use crate::rpc::{InboundRequest, Protocol}; +use fnv::FnvHashMap; +use libp2p::PeerId; +use std::convert::TryInto; +use std::future::Future; +use std::hash::Hash; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::{Duration, Instant}; +use tokio::time::Interval; + +/// Nanoseconds since a given time. +// Maintained as u64 to reduce footprint +// NOTE: this also implies that the rate limiter will manage checking if a batch is allowed for at +// most + u64::MAX nanosecs, ~500 years. So it is realistic to assume this is fine. +type Nanosecs = u64; + +/// User-friendly rate limiting parameters of the GCRA. +/// +/// A quota of `max_tokens` tokens every `replenish_all_every` units of time means that: +/// 1. One token is replenished every `replenish_all_every`/`max_tokens` units of time. +/// 2. Instantaneous bursts (batches) of up to `max_tokens` tokens are allowed. +/// +/// The above implies that if `max_tokens` is greater than 1, the perceived rate may be higher (but +/// bounded) than the defined rate when instantaneous bursts occur. For instance, for a rate of +/// 4T/2s a first burst of 4T is allowed with subsequent requests of 1T every 0.5s forever, +/// producing a perceived rate over the window of the first 2s of 8T. However, subsequent sliding +/// windows of 2s keep the limit. +/// +/// In this scenario using the same rate as above, the sender is always maxing out their tokens, +/// except at seconds 1.5, 3, 3.5 and 4 +/// +/// ```ignore +/// x +/// used x +/// tokens x x x +/// at a x x x x x x +/// given +--+--+--o--+--+--o--o--o--> seconds +/// time | | | | | | | | | +/// 0 1 2 3 4 +/// +/// 4 1 1 1 2 1 1 2 3 <= available tokens when the batch is received +/// ``` +/// +/// For a sender to request a batch of `n`T, they would need to wait at least +/// n*`replenish_all_every`/`max_tokens` units of time since their last request. +/// +/// To produce hard limits, set `max_tokens` to 1. +pub struct Quota { + /// How often are `max_tokens` fully replenished. + replenish_all_every: Duration, + /// Token limit. This translates on how large can an instantaneous batch of + /// tokens be. + max_tokens: u64, +} + +/// Manages rate limiting of requests per peer, with differentiated rates per protocol. +pub struct RPCRateLimiter { + /// Interval to prune peers for which their timer ran out. + prune_interval: Interval, + /// Creation time of the rate limiter. + init_time: Instant, + /// Goodbye rate limiter. + goodbye_rl: Limiter, + /// Ping rate limiter. + ping_rl: Limiter, + /// Status rate limiter. + status_rl: Limiter, + /// DataByHash rate limiter. + data_by_hash_rl: Limiter, + /// GetChunks rate limiter. + get_chunks_rl: Limiter, +} + +/// Error type for non conformant requests +pub enum RateLimitedErr { + /// Required tokens for this request exceed the maximum + TooLarge, + /// Request does not fit in the quota. Gives the earliest time the request could be accepted. + TooSoon(Duration), +} + +/// User-friendly builder of a `RPCRateLimiter` +#[derive(Default)] +pub struct RPCRateLimiterBuilder { + /// Quota for the Goodbye protocol. + goodbye_quota: Option, + /// Quota for the Ping protocol. + ping_quota: Option, + /// Quota for the Status protocol. + status_quota: Option, + /// Quota for the DataByHash protocol. + data_by_hash_quota: Option, + /// Quota for the GetChunks protocol. + get_chunks_quota: Option, +} + +impl RPCRateLimiterBuilder { + /// Get an empty `RPCRateLimiterBuilder`. + pub fn new() -> Self { + Default::default() + } + + /// Set a quota for a protocol. + fn set_quota(mut self, protocol: Protocol, quota: Quota) -> Self { + let q = Some(quota); + match protocol { + Protocol::Ping => self.ping_quota = q, + Protocol::Status => self.status_quota = q, + Protocol::Goodbye => self.goodbye_quota = q, + Protocol::DataByHash => self.data_by_hash_quota = q, + Protocol::GetChunks => self.get_chunks_quota = q, + } + self + } + + /// Allow one token every `time_period` to be used for this `protocol`. + /// This produces a hard limit. + pub fn one_every(self, protocol: Protocol, time_period: Duration) -> Self { + self.set_quota( + protocol, + Quota { + replenish_all_every: time_period, + max_tokens: 1, + }, + ) + } + + /// Allow `n` tokens to be use used every `time_period` for this `protocol`. + pub fn n_every(self, protocol: Protocol, n: u64, time_period: Duration) -> Self { + self.set_quota( + protocol, + Quota { + max_tokens: n, + replenish_all_every: time_period, + }, + ) + } + + pub fn build(self) -> Result { + // get our quotas + let ping_quota = self.ping_quota.ok_or("Ping quota not specified")?; + let status_quota = self.status_quota.ok_or("Status quota not specified")?; + let goodbye_quota = self.goodbye_quota.ok_or("Goodbye quota not specified")?; + let data_by_hash_quota = self + .data_by_hash_quota + .ok_or("DataByHash quota not specified")?; + let get_chunks_quota = self + .get_chunks_quota + .ok_or("GetChunks quota not specified")?; + + // create the rate limiters + let ping_rl = Limiter::from_quota(ping_quota)?; + let status_rl = Limiter::from_quota(status_quota)?; + let goodbye_rl = Limiter::from_quota(goodbye_quota)?; + let data_by_hash_rl = Limiter::from_quota(data_by_hash_quota)?; + let get_chunks_rl = Limiter::from_quota(get_chunks_quota)?; + + // check for peers to prune every 30 seconds, starting in 30 seconds + let prune_every = tokio::time::Duration::from_secs(30); + let prune_start = tokio::time::Instant::now() + prune_every; + let prune_interval = tokio::time::interval_at(prune_start, prune_every); + Ok(RPCRateLimiter { + prune_interval, + ping_rl, + status_rl, + goodbye_rl, + data_by_hash_rl, + get_chunks_rl, + init_time: Instant::now(), + }) + } +} + +impl RPCRateLimiter { + pub fn allows( + &mut self, + peer_id: &PeerId, + request: &InboundRequest, + ) -> Result<(), RateLimitedErr> { + let time_since_start = self.init_time.elapsed(); + let tokens = request.expected_responses().max(1); + + // Increase the rate limit for blocks by range requests with large step counts. + // We count to tokens as a quadratic increase with step size. + // Using (step_size/5)^2 + 1 as penalty factor allows step sizes of 1-4 to have no penalty + // but step sizes higher than this add a quadratic penalty. + // Penalty's go: + // Step size | Penalty Factor + // 1 | 1 + // 2 | 1 + // 3 | 1 + // 4 | 1 + // 5 | 2 + // 6 | 2 + // 7 | 2 + // 8 | 3 + // 9 | 4 + // 10 | 5 + + // if let InboundRequest::BlocksByRange(bbr_req) = request { + // let penalty_factor = (bbr_req.step as f64 / 5.0).powi(2) as u64 + 1; + // tokens *= penalty_factor; + // } + + let check = + |limiter: &mut Limiter| limiter.allows(time_since_start, peer_id, tokens); + let limiter = match request.protocol() { + Protocol::Ping => &mut self.ping_rl, + Protocol::Status => &mut self.status_rl, + Protocol::Goodbye => &mut self.goodbye_rl, + Protocol::DataByHash => &mut self.data_by_hash_rl, + Protocol::GetChunks => &mut self.get_chunks_rl, + }; + check(limiter) + } + + pub fn prune(&mut self) { + let time_since_start = self.init_time.elapsed(); + self.ping_rl.prune(time_since_start); + self.status_rl.prune(time_since_start); + self.goodbye_rl.prune(time_since_start); + self.data_by_hash_rl.prune(time_since_start); + self.get_chunks_rl.prune(time_since_start); + } +} + +impl Future for RPCRateLimiter { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + while self.prune_interval.poll_tick(cx).is_ready() { + self.prune(); + } + + Poll::Pending + } +} + +/// Per key rate limiter using the token bucket / leaky bucket as a meter rate limiting algorithm, +/// with the GCRA implementation. +pub struct Limiter { + /// After how long is the bucket considered full via replenishing 1T every `t`. + tau: Nanosecs, + /// How often is 1T replenished. + t: Nanosecs, + /// Time when the bucket will be full for each peer. TAT (theoretical arrival time) from GCRA. + tat_per_key: FnvHashMap, +} + +impl Limiter { + pub fn from_quota(quota: Quota) -> Result { + if quota.max_tokens == 0 { + return Err("Max number of tokens should be positive"); + } + let tau = quota.replenish_all_every.as_nanos(); + if tau == 0 { + return Err("Replenish time must be positive"); + } + let t = (tau / quota.max_tokens as u128) + .try_into() + .map_err(|_| "total replenish time is too long")?; + let tau = tau + .try_into() + .map_err(|_| "total replenish time is too long")?; + Ok(Limiter { + tau, + t, + tat_per_key: FnvHashMap::default(), + }) + } + + pub fn allows( + &mut self, + time_since_start: Duration, + key: &Key, + tokens: u64, + ) -> Result<(), RateLimitedErr> { + let time_since_start = time_since_start.as_nanos() as u64; + let tau = self.tau; + let t = self.t; + // how long does it take to replenish these tokens + let additional_time = t * tokens; + if additional_time > tau { + // the time required to process this amount of tokens is longer than the time that + // makes the bucket full. So, this batch can _never_ be processed + return Err(RateLimitedErr::TooLarge); + } + // If the key is new, we consider their bucket full (which means, their request will be + // allowed) + let tat = self + .tat_per_key + .entry(key.clone()) + .or_insert(time_since_start); + // check how soon could the request be made + let earliest_time = (*tat + additional_time).saturating_sub(tau); + // earliest_time is in the future + if time_since_start < earliest_time { + Err(RateLimitedErr::TooSoon(Duration::from_nanos( + /* time they need to wait, i.e. how soon were they */ + earliest_time - time_since_start, + ))) + } else { + // calculate the new TAT + *tat = time_since_start.max(*tat) + additional_time; + Ok(()) + } + } + + /// Removes keys for which their bucket is full by `time_limit` + pub fn prune(&mut self, time_limit: Duration) { + let lim = &mut (time_limit.as_nanos() as u64); + // remove those for which tat < lim + self.tat_per_key.retain(|_k, tat| tat >= lim) + } +} + +#[cfg(test)] +mod tests { + use crate::rpc::rate_limiter::{Limiter, Quota}; + use std::time::Duration; + + #[test] + fn it_works_a() { + let mut limiter = Limiter::from_quota(Quota { + replenish_all_every: Duration::from_secs(2), + max_tokens: 4, + }) + .unwrap(); + let key = 10; + // x + // used x + // tokens x x + // x x x x + // +--+--+--+--+----> seconds + // | | | | | + // 0 1 2 + + assert!(limiter + .allows(Duration::from_secs_f32(0.0), &key, 4) + .is_ok()); + limiter.prune(Duration::from_secs_f32(0.1)); + assert!(limiter + .allows(Duration::from_secs_f32(0.1), &key, 1) + .is_err()); + assert!(limiter + .allows(Duration::from_secs_f32(0.5), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(1.0), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(1.4), &key, 1) + .is_err()); + assert!(limiter + .allows(Duration::from_secs_f32(2.0), &key, 2) + .is_ok()); + } + + #[test] + fn it_works_b() { + let mut limiter = Limiter::from_quota(Quota { + replenish_all_every: Duration::from_secs(2), + max_tokens: 4, + }) + .unwrap(); + let key = 10; + // if we limit to 4T per 2s, check that 4 requests worth 1 token can be sent before the + // first half second, when one token will be available again. Check also that before + // regaining a token, another request is rejected + + assert!(limiter + .allows(Duration::from_secs_f32(0.0), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.1), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.2), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.3), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.4), &key, 1) + .is_err()); + } +} diff --git a/node/network/src/service.rs b/node/network/src/service.rs new file mode 100644 index 0000000..80bc986 --- /dev/null +++ b/node/network/src/service.rs @@ -0,0 +1,508 @@ +use crate::behaviour::{Behaviour, BehaviourEvent, PeerRequestId, Request, Response}; +use crate::config::NetworkLoad; +use crate::discovery::enr; +use crate::multiaddr::Protocol; +use crate::rpc::{GoodbyeReason, RPCResponseErrorCode, ReqId}; +use crate::types::{error, GossipKind}; +use crate::{EnrExt, NetworkMessage}; +use crate::{NetworkConfig, NetworkGlobals, PeerAction, ReportSource}; +use futures::prelude::*; +use libp2p::core::{ + identity::Keypair, multiaddr::Multiaddr, muxing::StreamMuxerBox, transport::Boxed, +}; +use libp2p::{ + bandwidth::{BandwidthLogging, BandwidthSinks}, + core, noise, + swarm::{ConnectionLimits, SwarmBuilder, SwarmEvent}, + PeerId, Swarm, Transport, +}; +use std::fs::File; +use std::io::prelude::*; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::mpsc::UnboundedSender; + +use crate::peer_manager::{MIN_OUTBOUND_ONLY_FACTOR, PEER_EXCESS_FACTOR, PRIORITY_PEER_EXCESS}; + +pub const NETWORK_KEY_FILENAME: &str = "key"; +/// The maximum simultaneous libp2p connections per peer. +const MAX_CONNECTIONS_PER_PEER: u32 = 1; + +/// The types of events than can be obtained from polling the libp2p service. +/// +/// This is a subset of the events that a libp2p swarm emits. +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum Libp2pEvent { + /// A behaviour event + Behaviour(BehaviourEvent), + /// A new listening address has been established. + NewListenAddr(Multiaddr), + /// We reached zero listening addresses. + ZeroListeners, +} + +/// The configuration and state of the libp2p components for the beacon node. +pub struct Service { + /// The libp2p Swarm handler. + pub swarm: Swarm>, + /// The bandwidth logger for the underlying libp2p transport. + pub bandwidth: Arc, + /// This node's PeerId. + pub local_peer_id: PeerId, +} + +pub struct Context<'a> { + pub config: &'a NetworkConfig, +} + +impl Service { + pub async fn new( + executor: task_executor::TaskExecutor, + network_sender: UnboundedSender, + ctx: Context<'_>, + ) -> error::Result<(Arc, Keypair, Self)> { + trace!("Libp2p Service starting"); + + let config = ctx.config; + // initialise the node's ID + let local_keypair = load_private_key(config); + + // Create an ENR or load from disk if appropriate + let enr = enr::build_or_load_enr(local_keypair.clone(), config)?; + + let local_peer_id = enr.peer_id(); + + // set up a collection of variables accessible outside of the network crate + let network_globals = Arc::new(NetworkGlobals::new( + enr.clone(), + config.libp2p_port, + config.discovery_port, + config + .trusted_peers + .iter() + .map(|x| PeerId::from(x.clone())) + .collect(), + )); + + // try and construct UPnP port mappings if required. + if let Some(upnp_config) = crate::nat::UPnPConfig::from_config(config) { + if config.upnp_enabled { + executor.spawn_blocking( + move || crate::nat::construct_upnp_mappings(upnp_config, network_sender), + "UPnP", + ); + } + } + + info!( + peer_id = %enr.peer_id(), + bandwidth_config = %format!("{}-{}", config.network_load, NetworkLoad::from(config.network_load).name), + "Libp2p Starting", + ); + + let discovery_string = if config.disable_discovery { + "None".into() + } else { + config.discovery_port.to_string() + }; + + debug!( + address = ?config.listen_address, + tcp_port = %config.libp2p_port, + udp_port = %discovery_string, + "Attempting to open listening ports", + ); + + let (mut swarm, bandwidth) = { + // Set up the transport - tcp/ws with noise and mplex + let (transport, bandwidth) = build_transport(local_keypair.clone()) + .map_err(|e| format!("Failed to build transport: {:?}", e))?; + + // Lighthouse network behaviour + let behaviour = + Behaviour::new(&local_keypair.clone(), ctx, network_globals.clone()).await?; + + // use the executor for libp2p + struct Executor(task_executor::TaskExecutor); + impl libp2p::core::Executor for Executor { + fn exec(&self, f: Pin + Send>>) { + self.0.spawn(f, "libp2p"); + } + } + + // sets up the libp2p connection limits + let limits = ConnectionLimits::default() + .with_max_pending_incoming(Some(5)) + .with_max_pending_outgoing(Some(16)) + .with_max_established_incoming(Some( + (config.target_peers as f32 + * (1.0 + PEER_EXCESS_FACTOR - MIN_OUTBOUND_ONLY_FACTOR)) + .ceil() as u32, + )) + .with_max_established_outgoing(Some( + (config.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR)).ceil() as u32, + )) + .with_max_established(Some( + (config.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR + PRIORITY_PEER_EXCESS)) + .ceil() as u32, + )) + .with_max_established_per_peer(Some(MAX_CONNECTIONS_PER_PEER)); + + ( + SwarmBuilder::new(transport, behaviour, local_peer_id) + .notify_handler_buffer_size(std::num::NonZeroUsize::new(7).expect("Not zero")) + .connection_event_buffer_size(64) + .connection_limits(limits) + .executor(Box::new(Executor(executor))) + .build(), + bandwidth, + ) + }; + + // listen on the specified address + let listen_multiaddr = { + let mut m = Multiaddr::from(config.listen_address); + m.push(Protocol::Tcp(config.libp2p_port)); + m + }; + + match Swarm::listen_on(&mut swarm, listen_multiaddr.clone()) { + Ok(_) => { + let mut log_address = listen_multiaddr; + log_address.push(Protocol::P2p(local_peer_id.into())); + info!(address = %log_address, "Listening established"); + } + Err(err) => { + error!( + error = ?err, + listen_multiaddr = %listen_multiaddr, + "Unable to listen on libp2p address", + ); + return Err("Libp2p was unable to listen on the given listen address.".into()); + } + }; + + // helper closure for dialing peers + let mut dial = |multiaddr: Multiaddr| { + // strip the p2p protocol if it exists + match Swarm::dial(&mut swarm, multiaddr.clone()) { + Ok(()) => debug!(address = %multiaddr, "Dialing libp2p peer"), + Err(err) => { + debug!(address = %multiaddr, error = ?err, "Could not connect to peer") + } + }; + }; + + // attempt to connect to user-input libp2p nodes + for multiaddr in &config.libp2p_nodes { + dial(multiaddr.clone()); + } + + // attempt to connect to any specified boot-nodes + let mut boot_nodes = config.boot_nodes_enr.clone(); + boot_nodes.dedup(); + + for bootnode_enr in boot_nodes { + for multiaddr in &bootnode_enr.multiaddr() { + // ignore udp multiaddr if it exists + let components = multiaddr.iter().collect::>(); + if let Protocol::Udp(_) = components[1] { + continue; + } + + if !network_globals + .peers + .read() + .is_connected_or_dialing(&bootnode_enr.peer_id()) + { + dial(multiaddr.clone()); + } + } + } + + for multiaddr in &config.boot_nodes_multiaddr { + // check TCP support for dialing + if multiaddr + .iter() + .any(|proto| matches!(proto, Protocol::Tcp(_))) + { + dial(multiaddr.clone()); + } + } + + let mut subscribed_topics: Vec = vec![]; + + // for topic_kind in &config.topics { + // if swarm.behaviour_mut().subscribe_kind(topic_kind.clone()) { + // subscribed_topics.push(topic_kind.clone()); + // } else { + // warn!(topic = ?topic_kind, "Could not subscribe to topic"); + // } + // } + + for topic_kind in &crate::types::CORE_TOPICS { + if swarm.behaviour_mut().subscribe_kind(topic_kind.clone()) { + subscribed_topics.push(topic_kind.clone()); + } else { + warn!(topic = ?topic_kind, "Could not subscribe to topic"); + } + } + + if !subscribed_topics.is_empty() { + info!(topics = ?subscribed_topics, "Subscribed to topics"); + } + + let service = Service { + swarm, + bandwidth, + local_peer_id, + }; + + Ok((network_globals, local_keypair, service)) + } + + /// Sends a request to a peer, with a given Id. + pub fn send_request(&mut self, peer_id: PeerId, request_id: AppReqId, request: Request) { + self.swarm + .behaviour_mut() + .send_request(peer_id, request_id, request); + } + + /// Informs the peer that their request failed. + pub fn respond_with_error( + &mut self, + peer_id: PeerId, + id: PeerRequestId, + error: RPCResponseErrorCode, + reason: String, + ) { + self.swarm + .behaviour_mut() + .send_error_reponse(peer_id, id, error, reason); + } + + /// Report a peer's action. + pub fn report_peer( + &mut self, + peer_id: &PeerId, + action: PeerAction, + source: ReportSource, + msg: &'static str, + ) { + self.swarm + .behaviour_mut() + .peer_manager_mut() + .report_peer(peer_id, action, source, None, msg); + } + + /// Disconnect and ban a peer, providing a reason. + pub fn goodbye_peer(&mut self, peer_id: &PeerId, reason: GoodbyeReason, source: ReportSource) { + self.swarm + .behaviour_mut() + .goodbye_peer(peer_id, reason, source); + } + + /// Sends a response to a peer's request. + pub fn send_response(&mut self, peer_id: PeerId, id: PeerRequestId, response: Response) { + self.swarm + .behaviour_mut() + .send_successful_response(peer_id, id, response); + } + + pub async fn next_event(&mut self) -> Libp2pEvent { + loop { + match self.swarm.select_next_some().await { + SwarmEvent::Behaviour(behaviour) => { + // Handle banning here + match &behaviour { + BehaviourEvent::PeerBanned(peer_id) => { + self.swarm.ban_peer_id(*peer_id); + } + BehaviourEvent::PeerUnbanned(peer_id) => { + self.swarm.unban_peer_id(*peer_id); + } + _ => {} + } + return Libp2pEvent::Behaviour(behaviour); + } + SwarmEvent::ConnectionEstablished { + peer_id: _, + endpoint: _, + num_established: _, + concurrent_dial_errors: _, + } => {} + SwarmEvent::ConnectionClosed { + peer_id: _, + cause: _, + endpoint: _, + num_established: _, + } => {} + SwarmEvent::NewListenAddr { address, .. } => { + return Libp2pEvent::NewListenAddr(address) + } + SwarmEvent::IncomingConnection { + local_addr, + send_back_addr, + } => { + trace!(our_addr = %local_addr, from = %send_back_addr, "Incoming connection") + } + SwarmEvent::IncomingConnectionError { + local_addr, + send_back_addr, + error, + } => { + debug!(our_addr = %local_addr, from = %send_back_addr, error = %error, "Failed incoming connection"); + } + SwarmEvent::BannedPeer { peer_id, .. } => { + debug!(peer_id = %peer_id, "Banned peer connection rejected"); + } + SwarmEvent::OutgoingConnectionError { peer_id, error } => { + debug!(peer_id = ?peer_id, error = %error, "Failed to dial address"); + } + SwarmEvent::ExpiredListenAddr { address, .. } => { + debug!(address = %address, "Listen address expired") + } + SwarmEvent::ListenerClosed { + addresses, reason, .. + } => { + error!(addresses = ?addresses, reason = ?reason, "Listener closed"); + if Swarm::listeners(&self.swarm).count() == 0 { + return Libp2pEvent::ZeroListeners; + } + } + SwarmEvent::ListenerError { error, .. } => { + // this is non fatal, but we still check + warn!(error = ?error, "Listener error"); + if Swarm::listeners(&self.swarm).count() == 0 { + return Libp2pEvent::ZeroListeners; + } + } + SwarmEvent::Dialing(_peer_id) => {} + } + } + } +} + +type BoxedTransport = Boxed<(PeerId, StreamMuxerBox)>; + +/// The implementation supports TCP/IP, WebSockets over TCP/IP, noise as the encryption layer, and +/// mplex as the multiplexing layer. +fn build_transport( + local_private_key: Keypair, +) -> std::io::Result<(BoxedTransport, Arc)> { + let tcp = libp2p::tcp::TokioTcpConfig::new().nodelay(true); + let transport = libp2p::dns::TokioDnsConfig::system(tcp)?; + #[cfg(feature = "libp2p-websocket")] + let transport = { + let trans_clone = transport.clone(); + transport.or_transport(libp2p::websocket::WsConfig::new(trans_clone)) + }; + + let (transport, bandwidth) = BandwidthLogging::new(transport); + + // mplex config + let mut mplex_config = libp2p::mplex::MplexConfig::new(); + mplex_config.set_max_buffer_size(256); + mplex_config.set_max_buffer_behaviour(libp2p::mplex::MaxBufferBehaviour::Block); + + // yamux config + let mut yamux_config = libp2p::yamux::YamuxConfig::default(); + yamux_config.set_window_update_mode(libp2p::yamux::WindowUpdateMode::on_read()); + + // Authentication + Ok(( + transport + .upgrade(core::upgrade::Version::V1) + .authenticate(generate_noise_config(&local_private_key)) + .multiplex(core::upgrade::SelectUpgrade::new( + yamux_config, + mplex_config, + )) + .timeout(Duration::from_secs(10)) + .boxed(), + bandwidth, + )) +} + +// Useful helper functions for debugging. Currently not used in the client. +#[allow(dead_code)] +fn keypair_from_hex(hex_bytes: &str) -> error::Result { + let hex_bytes = if let Some(stripped) = hex_bytes.strip_prefix("0x") { + stripped.to_string() + } else { + hex_bytes.to_string() + }; + + hex::decode(hex_bytes) + .map_err(|e| format!("Failed to parse p2p secret key bytes: {:?}", e).into()) + .and_then(keypair_from_bytes) +} + +#[allow(dead_code)] +fn keypair_from_bytes(mut bytes: Vec) -> error::Result { + libp2p::core::identity::secp256k1::SecretKey::from_bytes(&mut bytes) + .map(|secret| { + let keypair: libp2p::core::identity::secp256k1::Keypair = secret.into(); + Keypair::Secp256k1(keypair) + }) + .map_err(|e| format!("Unable to parse p2p secret key: {:?}", e).into()) +} + +/// Loads a private key from disk. If this fails, a new key is +/// generated and is then saved to disk. +/// +/// Currently only secp256k1 keys are allowed, as these are the only keys supported by discv5. +pub fn load_private_key(config: &NetworkConfig) -> Keypair { + // check for key from disk + let network_key_f = config.network_dir.join(NETWORK_KEY_FILENAME); + if let Ok(mut network_key_file) = File::open(network_key_f.clone()) { + let mut key_bytes: Vec = Vec::with_capacity(36); + match network_key_file.read_to_end(&mut key_bytes) { + Err(_) => debug!("Could not read network key file"), + Ok(_) => { + // only accept secp256k1 keys for now + if let Ok(secret_key) = + libp2p::core::identity::secp256k1::SecretKey::from_bytes(&mut key_bytes) + { + let kp: libp2p::core::identity::secp256k1::Keypair = secret_key.into(); + debug!("Loaded network key from disk."); + return Keypair::Secp256k1(kp); + } else { + debug!("Network key file is not a valid secp256k1 key"); + } + } + } + } + + // if a key could not be loaded from disk, generate a new one and save it + let local_private_key = Keypair::generate_secp256k1(); + if let Keypair::Secp256k1(key) = local_private_key.clone() { + let _ = std::fs::create_dir_all(&config.network_dir); + match File::create(network_key_f.clone()) + .and_then(|mut f| f.write_all(&key.secret().to_bytes())) + { + Ok(_) => { + debug!("New network key generated and written to disk"); + } + Err(e) => { + warn!( + "Could not write node key to file: {:?}. error: {}", + network_key_f, e + ); + } + } + } + local_private_key +} + +/// Generate authenticated XX Noise config from identity keys +fn generate_noise_config( + identity_keypair: &Keypair, +) -> noise::NoiseAuthenticated { + let static_dh_keys = noise::Keypair::::new() + .into_authentic(identity_keypair) + .expect("signing can fail only once during starting a node"); + noise::NoiseConfig::xx(static_dh_keys).into_authenticated() +} diff --git a/node/network/src/types/error.rs b/node/network/src/types/error.rs new file mode 100644 index 0000000..a291e8f --- /dev/null +++ b/node/network/src/types/error.rs @@ -0,0 +1,5 @@ +// generates error types + +use error_chain::error_chain; + +error_chain! {} diff --git a/node/network/src/types/globals.rs b/node/network/src/types/globals.rs new file mode 100644 index 0000000..3875560 --- /dev/null +++ b/node/network/src/types/globals.rs @@ -0,0 +1,100 @@ +//! A collection of variables that are accessible outside of the network thread itself. +use crate::peer_manager::peerdb::PeerDB; +use crate::Client; +use crate::EnrExt; +use crate::{Enr, GossipTopic, Multiaddr, PeerId}; +use parking_lot::RwLock; +use std::collections::HashSet; +use std::sync::atomic::{AtomicU16, Ordering}; + +pub struct NetworkGlobals { + /// The current local ENR. + pub local_enr: RwLock, + /// The local peer_id. + pub peer_id: RwLock, + /// Listening multiaddrs. + pub listen_multiaddrs: RwLock>, + /// The TCP port that the libp2p service is listening on + pub listen_port_tcp: AtomicU16, + /// The UDP port that the discovery service is listening on + pub listen_port_udp: AtomicU16, + /// The collection of known peers. + pub peers: RwLock, + /// The current gossipsub topic subscriptions. + pub gossipsub_subscriptions: RwLock>, +} + +impl NetworkGlobals { + pub fn new(enr: Enr, tcp_port: u16, udp_port: u16, trusted_peers: Vec) -> Self { + NetworkGlobals { + local_enr: RwLock::new(enr.clone()), + peer_id: RwLock::new(enr.peer_id()), + listen_multiaddrs: RwLock::new(Vec::new()), + listen_port_tcp: AtomicU16::new(tcp_port), + listen_port_udp: AtomicU16::new(udp_port), + peers: RwLock::new(PeerDB::new(trusted_peers)), + gossipsub_subscriptions: RwLock::new(HashSet::new()), + } + } + + /// Returns the local ENR from the underlying Discv5 behaviour that external peers may connect + /// to. + pub fn local_enr(&self) -> Enr { + self.local_enr.read().clone() + } + + /// Returns the local libp2p PeerID. + pub fn local_peer_id(&self) -> PeerId { + *self.peer_id.read() + } + + /// Returns the list of `Multiaddr` that the underlying libp2p instance is listening on. + pub fn listen_multiaddrs(&self) -> Vec { + self.listen_multiaddrs.read().clone() + } + + /// Returns the libp2p TCP port that this node has been configured to listen on. + pub fn listen_port_tcp(&self) -> u16 { + self.listen_port_tcp.load(Ordering::Relaxed) + } + + /// Returns the UDP discovery port that this node has been configured to listen on. + pub fn listen_port_udp(&self) -> u16 { + self.listen_port_udp.load(Ordering::Relaxed) + } + + /// Returns the number of libp2p connected peers. + pub fn connected_peers(&self) -> usize { + self.peers.read().connected_peer_ids().count() + } + + /// Returns the number of libp2p connected peers with outbound-only connections. + pub fn connected_outbound_only_peers(&self) -> usize { + self.peers.read().connected_outbound_only_peers().count() + } + + /// Returns the number of libp2p peers that are either connected or being dialed. + pub fn connected_or_dialing_peers(&self) -> usize { + self.peers.read().connected_or_dialing_peers().count() + } + + /// Returns a `Client` type if one is known for the `PeerId`. + pub fn client(&self, peer_id: &PeerId) -> Client { + self.peers + .read() + .peer_info(peer_id) + .map(|info| info.client().clone()) + .unwrap_or_default() + } + + /// TESTING ONLY. Build a dummy NetworkGlobals instance. + #[allow(dead_code)] + pub fn new_test_globals() -> NetworkGlobals { + use crate::CombinedKeyExt; + let keypair = libp2p::identity::Keypair::generate_secp256k1(); + let enr_key: discv5::enr::CombinedKey = + discv5::enr::CombinedKey::from_libp2p(&keypair).unwrap(); + let enr = discv5::enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); + NetworkGlobals::new(enr, 9000, 9000, vec![]) + } +} diff --git a/node/network/src/types/mod.rs b/node/network/src/types/mod.rs new file mode 100644 index 0000000..0269cf4 --- /dev/null +++ b/node/network/src/types/mod.rs @@ -0,0 +1,10 @@ +pub mod error; +mod globals; +mod pubsub; +mod topics; + +pub type Enr = discv5::enr::Enr; + +pub use globals::NetworkGlobals; +pub use pubsub::{AnnounceFile, FindFile, PubsubMessage, SignedAnnounceFile, SnappyTransform}; +pub use topics::{GossipEncoding, GossipKind, GossipTopic, CORE_TOPICS}; diff --git a/node/network/src/types/pubsub.rs b/node/network/src/types/pubsub.rs new file mode 100644 index 0000000..81cb830 --- /dev/null +++ b/node/network/src/types/pubsub.rs @@ -0,0 +1,307 @@ +//! Handles the encoding and decoding of pubsub messages. + +use crate::types::{GossipEncoding, GossipKind, GossipTopic}; +use crate::{Keypair, PublicKey, SigningError, TopicHash}; +use libp2p::{ + gossipsub::{DataTransform, GossipsubMessage, RawGossipsubMessage}, + Multiaddr, PeerId, +}; +use shared_types::TxID; +use snap::raw::{decompress_len, Decoder, Encoder}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::{ + io::{Error, ErrorKind}, + ops::Deref, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WrappedMultiaddr(Multiaddr); + +impl From for WrappedMultiaddr { + fn from(addr: Multiaddr) -> Self { + WrappedMultiaddr(addr) + } +} + +impl From for Multiaddr { + fn from(addr: WrappedMultiaddr) -> Self { + addr.0 + } +} + +impl ssz::Encode for WrappedMultiaddr { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_bytes_len(&self) -> usize { + self.0.len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.0.to_vec().ssz_append(buf) + } +} + +impl ssz::Decode for WrappedMultiaddr { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + // TODO: limit length + match Multiaddr::try_from(bytes.to_vec()) { + Ok(addr) => Ok(WrappedMultiaddr(addr)), + Err(_) => Err(ssz::DecodeError::BytesInvalid( + "Cannot parse multiaddr".into(), + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WrappedPeerId(PeerId); + +impl Deref for WrappedPeerId { + type Target = PeerId; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for WrappedPeerId { + fn from(addr: PeerId) -> Self { + WrappedPeerId(addr) + } +} + +impl From for PeerId { + fn from(addr: WrappedPeerId) -> Self { + addr.0 + } +} + +impl ssz::Encode for WrappedPeerId { + fn is_ssz_fixed_len() -> bool { + // TODO: we can probably encode PeerId as fixed-length + false + } + + fn ssz_bytes_len(&self) -> usize { + self.0.to_bytes().len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.0.to_bytes().ssz_append(buf) + } +} + +impl ssz::Decode for WrappedPeerId { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + // TODO: limit length + match PeerId::from_bytes(bytes) { + Ok(addr) => Ok(WrappedPeerId(addr)), + Err(_) => Err(ssz::DecodeError::BytesInvalid( + "Cannot parse peer id".into(), + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct FindFile { + pub tx_id: TxID, + pub timestamp: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Encode, Decode)] +pub struct AnnounceFile { + pub tx_id: TxID, + pub peer_id: WrappedPeerId, + pub at: WrappedMultiaddr, + pub timestamp: u32, +} + +impl AnnounceFile { + pub fn into_signed(self, keypair: &Keypair) -> Result { + let raw = self.as_ssz_bytes(); + let signature = keypair.sign(&raw)?; + + Ok(SignedAnnounceFile { + inner: self, + signature, + resend_timestamp: 0, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Encode, Decode)] +pub struct SignedAnnounceFile { + pub inner: AnnounceFile, + pub signature: Vec, + pub resend_timestamp: u32, +} + +impl SignedAnnounceFile { + pub fn verify_signature(&self, public_key: &PublicKey) -> bool { + let raw = self.inner.as_ssz_bytes(); + public_key.verify(&raw, &self.signature) + } +} + +impl Deref for SignedAnnounceFile { + type Target = AnnounceFile; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PubsubMessage { + ExampleMessage(u64), + FindFile(FindFile), + AnnounceFile(SignedAnnounceFile), +} + +// Implements the `DataTransform` trait of gossipsub to employ snappy compression +pub struct SnappyTransform { + /// Sets the maximum size we allow gossipsub messages to decompress to. + max_size_per_message: usize, +} + +impl SnappyTransform { + pub fn new(max_size_per_message: usize) -> Self { + SnappyTransform { + max_size_per_message, + } + } +} + +impl DataTransform for SnappyTransform { + // Provides the snappy decompression from RawGossipsubMessages + fn inbound_transform( + &self, + raw_message: RawGossipsubMessage, + ) -> Result { + // check the length of the raw bytes + let len = decompress_len(&raw_message.data)?; + if len > self.max_size_per_message { + return Err(Error::new( + ErrorKind::InvalidData, + "ssz_snappy decoded data > GOSSIP_MAX_SIZE", + )); + } + + let mut decoder = Decoder::new(); + let decompressed_data = decoder.decompress_vec(&raw_message.data)?; + + // Build the GossipsubMessage struct + Ok(GossipsubMessage { + source: raw_message.source, + data: decompressed_data, + sequence_number: raw_message.sequence_number, + topic: raw_message.topic, + }) + } + + /// Provides the snappy compression logic to gossipsub. + fn outbound_transform( + &self, + _topic: &TopicHash, + data: Vec, + ) -> Result, std::io::Error> { + // Currently we are not employing topic-based compression. Everything is expected to be + // snappy compressed. + if data.len() > self.max_size_per_message { + return Err(Error::new( + ErrorKind::InvalidData, + "ssz_snappy Encoded data > GOSSIP_MAX_SIZE", + )); + } + let mut encoder = Encoder::new(); + encoder.compress_vec(&data).map_err(Into::into) + } +} + +impl PubsubMessage { + /// Returns the topics that each pubsub message will be sent across, given a supported + /// gossipsub encoding and fork version. + pub fn topics(&self, encoding: GossipEncoding) -> Vec { + vec![GossipTopic::new(self.kind(), encoding)] + } + + /// Returns the kind of gossipsub topic associated with the message. + pub fn kind(&self) -> GossipKind { + match self { + PubsubMessage::ExampleMessage(_) => GossipKind::Example, + PubsubMessage::FindFile(_) => GossipKind::FindFile, + PubsubMessage::AnnounceFile(_) => GossipKind::AnnounceFile, + } + } + + /// This decodes `data` into a `PubsubMessage` given a topic. + /* Note: This is assuming we are not hashing topics. If we choose to hash topics, these will + * need to be modified. + */ + pub fn decode(topic: &TopicHash, data: &[u8]) -> Result { + match GossipTopic::decode(topic.as_str()) { + Err(_) => Err(format!("Unknown gossipsub topic: {:?}", topic)), + Ok(gossip_topic) => { + // All topics are currently expected to be compressed and decompressed with snappy. + // This is done in the `SnappyTransform` struct. + // Therefore compression has already been handled for us by the time we are + // decoding the objects here. + + // the ssz decoders + match gossip_topic.kind() { + GossipKind::Example => Ok(PubsubMessage::ExampleMessage( + u64::from_ssz_bytes(data).map_err(|e| format!("{:?}", e))?, + )), + GossipKind::FindFile => Ok(PubsubMessage::FindFile( + FindFile::from_ssz_bytes(data).map_err(|e| format!("{:?}", e))?, + )), + GossipKind::AnnounceFile => Ok(PubsubMessage::AnnounceFile( + SignedAnnounceFile::from_ssz_bytes(data).map_err(|e| format!("{:?}", e))?, + )), + } + } + } + } + + /// Encodes a `PubsubMessage` based on the topic encodings. The first known encoding is used. If + /// no encoding is known, and error is returned. + pub fn encode(&self, _encoding: GossipEncoding) -> Vec { + // Currently do not employ encoding strategies based on the topic. All messages are ssz + // encoded. + // Also note, that the compression is handled by the `SnappyTransform` struct. Gossipsub will compress the + // messages for us. + match &self { + PubsubMessage::ExampleMessage(data) => data.as_ssz_bytes(), + PubsubMessage::FindFile(data) => data.as_ssz_bytes(), + PubsubMessage::AnnounceFile(data) => data.as_ssz_bytes(), + } + } +} + +impl std::fmt::Display for PubsubMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PubsubMessage::ExampleMessage(msg) => { + write!(f, "Example message: {}", msg) + } + PubsubMessage::FindFile(msg) => { + write!(f, "FindFile message: {:?}", msg) + } + PubsubMessage::AnnounceFile(msg) => { + write!(f, "AnnounceFile message: {:?}", msg) + } + } + } +} diff --git a/node/network/src/types/topics.rs b/node/network/src/types/topics.rs new file mode 100644 index 0000000..5836781 --- /dev/null +++ b/node/network/src/types/topics.rs @@ -0,0 +1,177 @@ +use libp2p::gossipsub::IdentTopic as Topic; +use serde_derive::{Deserialize, Serialize}; +use strum::AsRefStr; + +/// The gossipsub topic names. +// These constants form a topic name of the form /TOPIC_PREFIX/TOPIC/ENCODING_POSTFIX +// For example /eth2/beacon_block/ssz +pub const TOPIC_PREFIX: &str = "eth2"; +pub const SSZ_SNAPPY_ENCODING_POSTFIX: &str = "ssz_snappy"; +pub const EXAMPLE_TOPIC: &str = "example"; +pub const FIND_FILE_TOPIC: &str = "find_file"; +pub const ANNOUNCE_FILE_TOPIC: &str = "announce_file"; + +pub const CORE_TOPICS: [GossipKind; 2] = [GossipKind::FindFile, GossipKind::AnnounceFile]; + +/// A gossipsub topic which encapsulates the type of messages that should be sent and received over +/// the pubsub protocol and the way the messages should be encoded. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct GossipTopic { + /// The encoding of the topic. + encoding: GossipEncoding, + /// The kind of topic. + kind: GossipKind, +} + +/// Enum that brings these topics into the rust type system. +// NOTE: There is intentionally no unknown type here. We only allow known gossipsub topics. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, AsRefStr)] +#[strum(serialize_all = "snake_case")] +pub enum GossipKind { + Example, + FindFile, + AnnounceFile, +} + +/// The known encoding types for gossipsub messages. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Default)] +pub enum GossipEncoding { + /// Messages are encoded with SSZSnappy. + #[default] + SSZSnappy, +} + +impl GossipTopic { + pub fn new(kind: GossipKind, encoding: GossipEncoding) -> Self { + GossipTopic { encoding, kind } + } + + /// Returns the encoding type for the gossipsub topic. + pub fn encoding(&self) -> &GossipEncoding { + &self.encoding + } + + /// Returns the kind of message expected on the gossipsub topic. + pub fn kind(&self) -> &GossipKind { + &self.kind + } + + pub fn decode(topic: &str) -> Result { + let topic_parts: Vec<&str> = topic.split('/').collect(); + if topic_parts.len() == 4 && topic_parts[1] == TOPIC_PREFIX { + let encoding = match topic_parts[3] { + SSZ_SNAPPY_ENCODING_POSTFIX => GossipEncoding::SSZSnappy, + _ => return Err(format!("Unknown encoding: {}", topic)), + }; + + let kind = match topic_parts[2] { + EXAMPLE_TOPIC => GossipKind::Example, + FIND_FILE_TOPIC => GossipKind::FindFile, + ANNOUNCE_FILE_TOPIC => GossipKind::AnnounceFile, + _ => return Err(format!("Unknown topic: {}", topic)), + }; + + return Ok(GossipTopic { encoding, kind }); + } + + Err(format!("Unknown topic: {}", topic)) + } +} + +impl From for Topic { + fn from(topic: GossipTopic) -> Topic { + Topic::new(topic) + } +} + +impl From for String { + fn from(topic: GossipTopic) -> String { + let encoding = match topic.encoding { + GossipEncoding::SSZSnappy => SSZ_SNAPPY_ENCODING_POSTFIX, + }; + + let kind = match topic.kind { + GossipKind::Example => EXAMPLE_TOPIC, + GossipKind::FindFile => FIND_FILE_TOPIC, + GossipKind::AnnounceFile => ANNOUNCE_FILE_TOPIC, + }; + + format!("/{}/{}/{}", TOPIC_PREFIX, kind, encoding) + } +} + +impl std::fmt::Display for GossipTopic { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let encoding = match self.encoding { + GossipEncoding::SSZSnappy => SSZ_SNAPPY_ENCODING_POSTFIX, + }; + + let kind = match self.kind { + GossipKind::Example => EXAMPLE_TOPIC, + GossipKind::FindFile => FIND_FILE_TOPIC, + GossipKind::AnnounceFile => ANNOUNCE_FILE_TOPIC, + }; + + write!(f, "/{}/{}/{}", TOPIC_PREFIX, kind, encoding) + } +} + +#[cfg(test)] +mod tests { + use super::GossipKind::*; + use super::*; + + const BAD_PREFIX: &str = "tezos"; + const BAD_ENCODING: &str = "rlp"; + const BAD_KIND: &str = "blocks"; + + fn topics() -> Vec { + let mut topics = Vec::new(); + + for encoding in [GossipEncoding::SSZSnappy].iter() { + { + let kind = &Example; + topics.push(GossipTopic::new(kind.clone(), encoding.clone()).into()); + } + } + topics + } + + fn create_topic(prefix: &str, kind: &str, encoding: &str) -> String { + format!("/{}/{}/{}", prefix, kind, encoding) + } + + #[test] + fn test_decode() { + for topic in topics().iter() { + assert!(GossipTopic::decode(topic.as_str()).is_ok()); + } + } + + #[test] + fn test_decode_malicious() { + let bad_prefix_str = create_topic(BAD_PREFIX, EXAMPLE_TOPIC, SSZ_SNAPPY_ENCODING_POSTFIX); + assert!(GossipTopic::decode(bad_prefix_str.as_str()).is_err()); + + let bad_kind_str = create_topic(TOPIC_PREFIX, BAD_KIND, SSZ_SNAPPY_ENCODING_POSTFIX); + assert!(GossipTopic::decode(bad_kind_str.as_str()).is_err()); + + let bad_encoding_str = create_topic(TOPIC_PREFIX, EXAMPLE_TOPIC, BAD_ENCODING); + assert!(GossipTopic::decode(bad_encoding_str.as_str()).is_err()); + + // Extra parts + assert!( + GossipTopic::decode("/eth2/beacon_block/ssz_snappy/yolo").is_err(), + "should have exactly 4 parts" + ); + // Empty string + assert!(GossipTopic::decode("").is_err()); + // Empty parts + assert!(GossipTopic::decode("////").is_err()); + } + + #[test] + fn test_as_str_ref() { + assert_eq!("example", Example.as_ref()); + } +} diff --git a/node/network/tests/common/behaviour.rs b/node/network/tests/common/behaviour.rs new file mode 100644 index 0000000..76eecfc --- /dev/null +++ b/node/network/tests/common/behaviour.rs @@ -0,0 +1,394 @@ +// NOTE: Taken from libp2p's swarm's testing utils. +// +// Copyright 2020 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use std::collections::HashMap; +use std::task::{Context, Poll}; + +use libp2p::core::connection::{ConnectedPoint, ConnectionId, ListenerId}; +use libp2p::swarm::handler::{ConnectionHandler, DummyConnectionHandler, IntoConnectionHandler}; +use libp2p::swarm::{DialError, NetworkBehaviour, NetworkBehaviourAction, PollParameters}; +use libp2p::{Multiaddr, PeerId}; + +/// A `MockBehaviour` is a `NetworkBehaviour` that allows for +/// the instrumentation of return values, without keeping +/// any further state. +pub struct MockBehaviour< + THandler = DummyConnectionHandler, + TOutEvent = ::OutEvent, +> where + THandler: ConnectionHandler, +{ + /// The prototype protocols handler that is cloned for every + /// invocation of `new_handler`. + pub handler_proto: THandler, + /// The addresses to return from `addresses_of_peer`. + pub addresses: HashMap>, + /// The next action to return from `poll`. + /// + /// An action is only returned once. + pub next_action: Option>, +} + +impl MockBehaviour +where + THandler: ConnectionHandler, +{ + pub fn new(handler_proto: THandler) -> Self { + MockBehaviour { + handler_proto, + addresses: HashMap::new(), + next_action: None, + } + } +} + +impl NetworkBehaviour for MockBehaviour +where + THandler: ConnectionHandler + Clone, + THandler::OutEvent: Clone, + TOutEvent: Send + 'static, +{ + type ConnectionHandler = THandler; + type OutEvent = TOutEvent; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + self.handler_proto.clone() + } + + fn addresses_of_peer(&mut self, p: &PeerId) -> Vec { + self.addresses.get(p).map_or(Vec::new(), |v| v.clone()) + } + + fn inject_event(&mut self, _: PeerId, _: ConnectionId, _: THandler::OutEvent) {} + + fn poll( + &mut self, + _: &mut Context, + _: &mut impl PollParameters, + ) -> Poll> { + Option::take(&mut self.next_action).map_or(Poll::Pending, Poll::Ready) + } +} + +/// A `CallTraceBehaviour` is a `NetworkBehaviour` that tracks invocations of callback methods and +/// their arguments, wrapping around an inner behaviour. It ensures certain invariants are met. +pub struct CallTraceBehaviour +where + TInner: NetworkBehaviour, +{ + inner: TInner, + + pub addresses_of_peer: Vec, + pub inject_connection_established: Vec<(PeerId, ConnectionId, ConnectedPoint, usize)>, + pub inject_connection_closed: Vec<(PeerId, ConnectionId, ConnectedPoint, usize)>, + pub inject_event: Vec<( + PeerId, + ConnectionId, + <::Handler as ConnectionHandler>::OutEvent, + )>, + pub inject_dial_failure: Vec>, + pub inject_new_listener: Vec, + pub inject_new_listen_addr: Vec<(ListenerId, Multiaddr)>, + pub inject_new_external_addr: Vec, + pub inject_expired_listen_addr: Vec<(ListenerId, Multiaddr)>, + pub inject_expired_external_addr: Vec, + pub inject_listener_error: Vec, + pub inject_listener_closed: Vec<(ListenerId, bool)>, + pub poll: usize, +} + +impl CallTraceBehaviour +where + TInner: NetworkBehaviour, +{ + pub fn new(inner: TInner) -> Self { + Self { + inner, + addresses_of_peer: Vec::new(), + inject_connection_established: Vec::new(), + inject_connection_closed: Vec::new(), + inject_event: Vec::new(), + inject_dial_failure: Vec::new(), + inject_new_listener: Vec::new(), + inject_new_listen_addr: Vec::new(), + inject_new_external_addr: Vec::new(), + inject_expired_listen_addr: Vec::new(), + inject_expired_external_addr: Vec::new(), + inject_listener_error: Vec::new(), + inject_listener_closed: Vec::new(), + poll: 0, + } + } + + #[allow(dead_code)] + pub fn reset(&mut self) { + self.addresses_of_peer = Vec::new(); + self.inject_connection_established = Vec::new(); + self.inject_connection_closed = Vec::new(); + self.inject_event = Vec::new(); + self.inject_dial_failure = Vec::new(); + self.inject_new_listen_addr = Vec::new(); + self.inject_new_external_addr = Vec::new(); + self.inject_expired_listen_addr = Vec::new(); + self.inject_listener_error = Vec::new(); + self.inject_listener_closed = Vec::new(); + self.poll = 0; + } + + pub fn inner(&mut self) -> &mut TInner { + &mut self.inner + } + + /// Checks that when the expected number of closed connection notifications are received, a + /// given number of expected disconnections have been received as well. + /// + /// Returns if the first condition is met. + pub fn assert_disconnected( + &self, + expected_closed_connections: usize, + expected_disconnections: usize, + ) -> bool { + if self.inject_connection_closed.len() == expected_closed_connections { + assert_eq!( + self.inject_connection_closed + .iter() + .filter(|(.., remaining_established)| { *remaining_established == 0 }) + .count(), + expected_disconnections + ); + return true; + } + + false + } + + /// Checks that when the expected number of established connection notifications are received, + /// a given number of expected connections have been received as well. + /// + /// Returns if the first condition is met. + pub fn assert_connected( + &self, + expected_established_connections: usize, + expected_connections: usize, + ) -> bool { + if self.inject_connection_established.len() == expected_established_connections { + assert_eq!( + self.inject_connection_established + .iter() + .filter(|(.., reported_aditional_connections)| { + *reported_aditional_connections == 0 + }) + .count(), + expected_connections + ); + return true; + } + + false + } +} + +impl NetworkBehaviour for CallTraceBehaviour +where + TInner: NetworkBehaviour, + <::Handler as ConnectionHandler>::OutEvent: + Clone, +{ + type ConnectionHandler = TInner::ConnectionHandler; + type OutEvent = TInner::OutEvent; + + fn new_handler(&mut self) -> Self::ConnectionHandler { + self.inner.new_handler() + } + + fn addresses_of_peer(&mut self, p: &PeerId) -> Vec { + self.addresses_of_peer.push(*p); + self.inner.addresses_of_peer(p) + } + + fn inject_connection_established( + &mut self, + p: &PeerId, + c: &ConnectionId, + e: &ConnectedPoint, + errors: Option<&Vec>, + other_established: usize, + ) { + let mut other_peer_connections = self + .inject_connection_established + .iter() + .rev() // take last to first + .filter_map(|(peer, .., other_established)| { + if p == peer { + Some(other_established) + } else { + None + } + }) + .take(other_established); + + // We are informed that there are `other_established` additional connections. Ensure that the + // number of previous connections is consistent with this + if let Some(&prev) = other_peer_connections.next() { + if prev < other_established { + assert_eq!( + prev, + other_established - 1, + "Inconsistent connection reporting" + ) + } + assert_eq!(other_peer_connections.count(), other_established - 1); + } else { + assert_eq!(other_established, 0) + } + self.inject_connection_established + .push((*p, *c, e.clone(), other_established)); + self.inner + .inject_connection_established(p, c, e, errors, other_established); + } + + fn inject_connection_closed( + &mut self, + p: &PeerId, + c: &ConnectionId, + e: &ConnectedPoint, + handler: ::Handler, + remaining_established: usize, + ) { + let mut other_closed_connections = self + .inject_connection_established + .iter() + .rev() // take last to first + .filter_map(|(peer, .., remaining_established)| { + if p == peer { + Some(remaining_established) + } else { + None + } + }) + .take(remaining_established); + + // We are informed that there are `other_established` additional connections. Ensure that the + // number of previous connections is consistent with this + if let Some(&prev) = other_closed_connections.next() { + if prev < remaining_established { + assert_eq!( + prev, + remaining_established - 1, + "Inconsistent closed connection reporting" + ) + } + assert_eq!(other_closed_connections.count(), remaining_established - 1); + } else { + assert_eq!(remaining_established, 0) + } + assert!( + self.inject_connection_established + .iter() + .any(|(peer, conn_id, endpoint, _)| (peer, conn_id, endpoint) == (p, c, e)), + "`inject_connection_closed` is called only for connections for \ + which `inject_connection_established` was called first." + ); + self.inject_connection_closed + .push((*p, *c, e.clone(), remaining_established)); + self.inner + .inject_connection_closed(p, c, e, handler, remaining_established); + } + + fn inject_event( + &mut self, + p: PeerId, + c: ConnectionId, + e: <::Handler as ConnectionHandler>::OutEvent, + ) { + assert!( + self.inject_connection_established + .iter() + .any(|(peer_id, conn_id, ..)| *peer_id == p && c == *conn_id), + "`inject_event` is called for reported connections." + ); + assert!( + !self + .inject_connection_closed + .iter() + .any(|(peer_id, conn_id, ..)| *peer_id == p && c == *conn_id), + "`inject_event` is never called for closed connections." + ); + + self.inject_event.push((p, c, e.clone())); + self.inner.inject_event(p, c, e); + } + + fn inject_dial_failure( + &mut self, + p: Option, + handler: Self::ConnectionHandler, + error: &DialError, + ) { + self.inject_dial_failure.push(p); + self.inner.inject_dial_failure(p, handler, error); + } + + fn inject_new_listener(&mut self, id: ListenerId) { + self.inject_new_listener.push(id); + self.inner.inject_new_listener(id); + } + + fn inject_new_listen_addr(&mut self, id: ListenerId, a: &Multiaddr) { + self.inject_new_listen_addr.push((id, a.clone())); + self.inner.inject_new_listen_addr(id, a); + } + + fn inject_expired_listen_addr(&mut self, id: ListenerId, a: &Multiaddr) { + self.inject_expired_listen_addr.push((id, a.clone())); + self.inner.inject_expired_listen_addr(id, a); + } + + fn inject_new_external_addr(&mut self, a: &Multiaddr) { + self.inject_new_external_addr.push(a.clone()); + self.inner.inject_new_external_addr(a); + } + + fn inject_expired_external_addr(&mut self, a: &Multiaddr) { + self.inject_expired_external_addr.push(a.clone()); + self.inner.inject_expired_external_addr(a); + } + + fn inject_listener_error(&mut self, l: ListenerId, e: &(dyn std::error::Error + 'static)) { + self.inject_listener_error.push(l); + self.inner.inject_listener_error(l, e); + } + + fn inject_listener_closed(&mut self, l: ListenerId, r: Result<(), &std::io::Error>) { + self.inject_listener_closed.push((l, r.is_ok())); + self.inner.inject_listener_closed(l, r); + } + + fn poll( + &mut self, + cx: &mut Context, + args: &mut impl PollParameters, + ) -> Poll> { + self.poll += 1; + self.inner.poll(cx, args) + } +} diff --git a/node/network/tests/common/mod.rs b/node/network/tests/common/mod.rs new file mode 100644 index 0000000..ac7f019 --- /dev/null +++ b/node/network/tests/common/mod.rs @@ -0,0 +1,175 @@ +#![cfg(test)] + +use libp2p::gossipsub::GossipsubConfigBuilder; +use network::Enr; +use network::EnrExt; +use network::Multiaddr; +use network::Service as LibP2PService; +use network::{Libp2pEvent, NetworkConfig}; +use std::sync::Weak; +use std::time::Duration; +use tokio::runtime::Runtime; +use tracing::{debug, error}; +use unused_port::unused_tcp_port; + +#[allow(clippy::type_complexity)] +#[allow(unused)] +pub mod behaviour; +#[allow(clippy::type_complexity)] +#[allow(unused)] +pub mod swarm; + +type ReqId = usize; + +use tempfile::Builder as TempBuilder; +use tokio::sync::mpsc::unbounded_channel; + +pub struct Libp2pInstance(LibP2PService, exit_future::Signal); + +impl std::ops::Deref for Libp2pInstance { + type Target = LibP2PService; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for Libp2pInstance { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub fn build_config(port: u16, mut boot_nodes: Vec) -> NetworkConfig { + let mut config = NetworkConfig::default(); + let path = TempBuilder::new() + .prefix(&format!("libp2p_test{}", port)) + .tempdir() + .unwrap(); + + config.libp2p_port = port; // tcp port + config.discovery_port = port; // udp port + config.enr_tcp_port = Some(port); + config.enr_udp_port = Some(port); + config.enr_address = Some("127.0.0.1".parse().unwrap()); + config.boot_nodes_enr.append(&mut boot_nodes); + config.network_dir = path.into_path(); + // Reduce gossipsub heartbeat parameters + config.gs_config = GossipsubConfigBuilder::from(config.gs_config) + .heartbeat_initial_delay(Duration::from_millis(500)) + .heartbeat_interval(Duration::from_millis(500)) + .build() + .unwrap(); + config +} + +pub async fn build_libp2p_instance(rt: Weak, boot_nodes: Vec) -> Libp2pInstance { + let port = unused_tcp_port().unwrap(); + let config = build_config(port, boot_nodes); + // launch libp2p service + + let (signal, exit) = exit_future::signal(); + let (shutdown_tx, _) = futures::channel::mpsc::channel(1); + let executor = task_executor::TaskExecutor::new(rt, exit, shutdown_tx); + let libp2p_context = network::Context { config: &config }; + let (sender, _) = unbounded_channel(); + Libp2pInstance( + LibP2PService::new(executor, sender, libp2p_context) + .await + .expect("should build libp2p instance") + .2, + signal, + ) +} + +#[allow(dead_code)] +pub fn get_enr(node: &LibP2PService) -> Enr { + node.swarm.behaviour().local_enr() +} + +// Returns `n` libp2p peers in fully connected topology. +#[allow(dead_code)] +pub async fn build_full_mesh(rt: Weak, n: usize) -> Vec { + let mut nodes = Vec::with_capacity(n); + for _ in 0..n { + nodes.push(build_libp2p_instance(rt.clone(), vec![]).await); + } + let multiaddrs: Vec = nodes + .iter() + .map(|x| get_enr(x).multiaddr()[1].clone()) + .collect(); + + for (i, node) in nodes.iter_mut().enumerate().take(n) { + for (j, multiaddr) in multiaddrs.iter().enumerate().skip(i) { + if i != j { + match libp2p::Swarm::dial(&mut node.swarm, multiaddr.clone()) { + Ok(()) => debug!("Connected"), + Err(_) => error!("Failed to connect"), + }; + } + } + } + nodes +} + +// Constructs a pair of nodes with separate loggers. The sender dials the receiver. +// This returns a (sender, receiver) pair. +#[allow(dead_code)] +pub async fn build_node_pair(rt: Weak) -> (Libp2pInstance, Libp2pInstance) { + let mut sender = build_libp2p_instance(rt.clone(), vec![]).await; + let mut receiver = build_libp2p_instance(rt, vec![]).await; + + let receiver_multiaddr = receiver.swarm.behaviour_mut().local_enr().multiaddr()[1].clone(); + + // let the two nodes set up listeners + let sender_fut = async { + loop { + if let Libp2pEvent::NewListenAddr(_) = sender.next_event().await { + return; + } + } + }; + let receiver_fut = async { + loop { + if let Libp2pEvent::NewListenAddr(_) = receiver.next_event().await { + return; + } + } + }; + + let joined = futures::future::join(sender_fut, receiver_fut); + + // wait for either both nodes to listen or a timeout + tokio::select! { + _ = tokio::time::sleep(Duration::from_millis(500)) => {} + _ = joined => {} + } + + match libp2p::Swarm::dial(&mut sender.swarm, receiver_multiaddr.clone()) { + Ok(()) => { + debug!(address = %format!("{:?}", receiver_multiaddr), "Sender dialed receiver") + } + Err(_) => error!("Dialing failed"), + }; + (sender, receiver) +} + +// Returns `n` peers in a linear topology +#[allow(dead_code)] +pub async fn build_linear(rt: Weak, n: usize) -> Vec { + let mut nodes = Vec::with_capacity(n); + for _ in 0..n { + nodes.push(build_libp2p_instance(rt.clone(), vec![]).await); + } + + let multiaddrs: Vec = nodes + .iter() + .map(|x| get_enr(x).multiaddr()[1].clone()) + .collect(); + for i in 0..n - 1 { + match libp2p::Swarm::dial(&mut nodes[i].swarm, multiaddrs[i + 1].clone()) { + Ok(()) => debug!("Connected"), + Err(_) => error!("Failed to connect"), + }; + } + nodes +} diff --git a/node/network/tests/common/swarm.rs b/node/network/tests/common/swarm.rs new file mode 100644 index 0000000..224718d --- /dev/null +++ b/node/network/tests/common/swarm.rs @@ -0,0 +1,99 @@ +use std::collections::HashMap; +use std::pin::Pin; + +use super::behaviour::{CallTraceBehaviour, MockBehaviour}; + +use futures::stream::Stream; +use futures::task::{Context, Poll}; +use libp2p::swarm::handler::ConnectionHandler; +use libp2p::swarm::{IntoConnectionHandler, NetworkBehaviour, Swarm, SwarmBuilder, SwarmEvent}; +use libp2p::{PeerId, Transport}; + +use futures::StreamExt; + +pub fn new_test_swarm(behaviour: B) -> Swarm +where + B: NetworkBehaviour, +{ + let id_keys = libp2p::identity::Keypair::generate_ed25519(); + let local_public_key = id_keys.public(); + let transport = libp2p::core::transport::MemoryTransport + .upgrade(libp2p::core::upgrade::Version::V1) + .authenticate(libp2p::plaintext::PlainText2Config { + local_public_key: local_public_key.clone(), + }) + .multiplex(libp2p::yamux::YamuxConfig::default()) + .boxed(); + SwarmBuilder::new(transport, behaviour, local_public_key.into()).build() +} + +pub fn random_multiaddr() -> libp2p::multiaddr::Multiaddr { + libp2p::multiaddr::Protocol::Memory(rand::random::()).into() +} + +/// Bind a memory multiaddr to a compatible swarm. +pub async fn bind_listener( + swarm: &mut Swarm, +) -> libp2p::multiaddr::Multiaddr { + swarm.listen_on(random_multiaddr()).unwrap(); + match swarm.select_next_some().await { + SwarmEvent::NewListenAddr { + listener_id: _, + address, + } => address, + _ => panic!("Testing swarm's first event should be a new listener"), + } +} + +#[derive(Default)] +pub struct SwarmPool { + swarms: HashMap>, +} + +impl SwarmPool { + pub fn with_capacity(capacity: usize) -> Self { + Self { + swarms: HashMap::with_capacity(capacity), + } + } + pub fn insert(&mut self, swarm: Swarm) -> PeerId { + let peer_id = *swarm.local_peer_id(); + self.swarms.insert(peer_id, swarm); + peer_id + } + + pub fn remove(&mut self, peer_id: &PeerId) { + self.swarms.remove(peer_id); + } + + pub fn get_mut(&mut self, peer_id: &PeerId) -> Option<&mut Swarm> { + self.swarms.get_mut(peer_id) + } + + pub fn swarms(&self) -> &HashMap> { + &self.swarms + } + + pub fn swarms_mut(&mut self) -> &mut HashMap> { + &mut self.swarms + } +} + +impl Stream for SwarmPool +where + B: NetworkBehaviour, + ::ConnectionHandler: ConnectionHandler, +{ + type Item = (PeerId, + SwarmEvent<::OutEvent, <<::ConnectionHandler as IntoConnectionHandler>::Handler as ConnectionHandler>::Error>); + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut polls = self + .get_mut() + .swarms + .iter_mut() + .map(|(&peer_id, swarm)| swarm.map(move |ev| (peer_id, ev))) + .collect::>(); + polls.poll_next_unpin(cx) + } +} diff --git a/node/network/tests/gossipsub_tests.rs b/node/network/tests/gossipsub_tests.rs new file mode 100644 index 0000000..c5b661c --- /dev/null +++ b/node/network/tests/gossipsub_tests.rs @@ -0,0 +1,171 @@ +/* These are temporarily disabled due to their non-deterministic behaviour and impending update to + * gossipsub 1.1. We leave these here as a template for future test upgrades + + +#![cfg(test)] +use crate::types::GossipEncoding; +use ::types::{BeaconBlock, EthSpec, MinimalEthSpec, Signature, SignedBeaconBlock}; +use lighthouse_network::*; +use slog::{debug, Level}; + +type E = MinimalEthSpec; + +mod common; + +/* Gossipsub tests */ +// Note: The aim of these tests is not to test the robustness of the gossip network +// but to check if the gossipsub implementation is behaving according to the specifications. + +// Test if gossipsub message are forwarded by nodes with a simple linear topology. +// +// Topology used in test +// +// node1 <-> node2 <-> node3 ..... <-> node(n-1) <-> node(n) + +#[tokio::test] +async fn test_gossipsub_forward() { + // set up the logging. The level and enabled or not + let log = common::build_log(Level::Info, false); + + let num_nodes = 20; + let mut nodes = common::build_linear(log.clone(), num_nodes); + let mut received_count = 0; + let spec = E::default_spec(); + let empty_block = BeaconBlock::empty(&spec); + let signed_block = SignedBeaconBlock { + message: empty_block, + signature: Signature::empty_signature(), + }; + let pubsub_message = PubsubMessage::BeaconBlock(Box::new(signed_block)); + let publishing_topic: String = pubsub_message + .topics(GossipEncoding::default(), [0, 0, 0, 0]) + .first() + .unwrap() + .clone() + .into(); + let mut subscribed_count = 0; + let fut = async move { + for node in nodes.iter_mut() { + loop { + match node.next_event().await { + Libp2pEvent::Behaviour(b) => match b { + BehaviourEvent::PubsubMessage { + topics, + message, + source, + id, + } => { + assert_eq!(topics.len(), 1); + // Assert topic is the published topic + assert_eq!( + topics.first().unwrap(), + &TopicHash::from_raw(publishing_topic.clone()) + ); + // Assert message received is the correct one + assert_eq!(message, pubsub_message.clone()); + received_count += 1; + // Since `propagate_message` is false, need to propagate manually + node.swarm.propagate_message(&source, id); + // Test should succeed if all nodes except the publisher receive the message + if received_count == num_nodes - 1 { + debug!(log.clone(), "Received message at {} nodes", num_nodes - 1); + return; + } + } + BehaviourEvent::PeerSubscribed(_, topic) => { + // Publish on beacon block topic + if topic == TopicHash::from_raw(publishing_topic.clone()) { + subscribed_count += 1; + // Every node except the corner nodes are connected to 2 nodes. + if subscribed_count == (num_nodes * 2) - 2 { + node.swarm.publish(vec![pubsub_message.clone()]); + } + } + } + _ => break, + }, + _ => break, + } + } + } + }; + + tokio::select! { + _ = fut => {} + _ = tokio::time::delay_for(tokio::time::Duration::from_millis(800)) => { + panic!("Future timed out"); + } + } +} + +// Test publishing of a message with a full mesh for the topic +// Not very useful but this is the bare minimum functionality. +#[tokio::test] +async fn test_gossipsub_full_mesh_publish() { + // set up the logging. The level and enabled or not + let log = common::build_log(Level::Debug, false); + + // Note: This test does not propagate gossipsub messages. + // Having `num_nodes` > `mesh_n_high` may give inconsistent results + // as nodes may get pruned out of the mesh before the gossipsub message + // is published to them. + let num_nodes = 12; + let mut nodes = common::build_full_mesh(log, num_nodes); + let mut publishing_node = nodes.pop().unwrap(); + let spec = E::default_spec(); + let empty_block = BeaconBlock::empty(&spec); + let signed_block = SignedBeaconBlock { + message: empty_block, + signature: Signature::empty_signature(), + }; + let pubsub_message = PubsubMessage::BeaconBlock(Box::new(signed_block)); + let publishing_topic: String = pubsub_message + .topics(GossipEncoding::default(), [0, 0, 0, 0]) + .first() + .unwrap() + .clone() + .into(); + let mut subscribed_count = 0; + let mut received_count = 0; + let fut = async move { + for node in nodes.iter_mut() { + while let Libp2pEvent::Behaviour(BehaviourEvent::PubsubMessage { + topics, + message, + .. + }) = node.next_event().await + { + assert_eq!(topics.len(), 1); + // Assert topic is the published topic + assert_eq!( + topics.first().unwrap(), + &TopicHash::from_raw(publishing_topic.clone()) + ); + // Assert message received is the correct one + assert_eq!(message, pubsub_message.clone()); + received_count += 1; + if received_count == num_nodes - 1 { + return; + } + } + } + while let Libp2pEvent::Behaviour(BehaviourEvent::PeerSubscribed(_, topic)) = + publishing_node.next_event().await + { + // Publish on beacon block topic + if topic == TopicHash::from_raw(publishing_topic.clone()) { + subscribed_count += 1; + if subscribed_count == num_nodes - 1 { + publishing_node.swarm.publish(vec![pubsub_message.clone()]); + } + } + } + }; + tokio::select! { + _ = fut => {} + _ = tokio::time::delay_for(tokio::time::Duration::from_millis(800)) => { + panic!("Future timed out"); + } + } +} +*/ diff --git a/node/network/tests/pm_tests.rs b/node/network/tests/pm_tests.rs new file mode 100644 index 0000000..414e1aa --- /dev/null +++ b/node/network/tests/pm_tests.rs @@ -0,0 +1,197 @@ +mod common; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +use common::{ + behaviour::{CallTraceBehaviour, MockBehaviour}, + swarm, +}; +use network::{ + peer_manager::{self, config::Config, PeerManagerEvent}, + NetworkGlobals, PeerAction, PeerInfo, PeerManager, ReportSource, +}; + +use futures::StreamExt; +use libp2p::{ + core::either::EitherError, + swarm::SwarmEvent, + swarm::{handler::DummyConnectionHandler, DummyBehaviour, KeepAlive, Swarm}, + NetworkBehaviour, +}; + +use tracing::debug; + +/// Struct that mimics the lighthouse_network::Service with respect to handling peer manager +/// events. +// TODO: make this a real struct for more accurate testing. +struct Service { + swarm: Swarm, +} + +impl Service { + async fn select_next_some(&mut self) -> SwarmEvent> { + let ev = self.swarm.select_next_some().await; + match &ev { + SwarmEvent::Behaviour(Ev(PeerManagerEvent::Banned(peer_id, _addr_vec))) => { + self.swarm.ban_peer_id(*peer_id); + } + SwarmEvent::Behaviour(Ev(PeerManagerEvent::UnBanned(peer_id, _addr_vec))) => { + self.swarm.unban_peer_id(*peer_id); + } + SwarmEvent::Behaviour(Ev(PeerManagerEvent::DisconnectPeer(peer_id, _reason))) => { + // directly disconnect here. + let _ = self.swarm.disconnect_peer_id(*peer_id); + } + _ => {} + } + ev + } +} + +#[derive(Debug)] +struct Ev(PeerManagerEvent); +impl From for Ev { + fn from(_: void::Void) -> Self { + unreachable!("No events are emmited") + } +} +impl From for Ev { + fn from(ev: PeerManagerEvent) -> Self { + Ev(ev) + } +} + +#[derive(NetworkBehaviour)] +#[behaviour(out_event = "Ev")] +struct Behaviour { + pm_call_trace: CallTraceBehaviour, + sibling: MockBehaviour, +} + +impl Behaviour { + fn new(pm: PeerManager) -> Self { + Behaviour { + pm_call_trace: CallTraceBehaviour::new(pm), + sibling: MockBehaviour::new(DummyConnectionHandler { + // The peer manager votes No, so we make sure the combined handler stays alive this + // way. + keep_alive: KeepAlive::Yes, + }), + } + } +} + +#[tokio::test] +async fn banned_peers_consistency() { + let globals: Arc = Arc::new(NetworkGlobals::new_test_globals()); + + // Build the peer manager. + let (mut pm_service, pm_addr) = { + let pm_config = Config { + discovery_enabled: false, + ..Default::default() + }; + let pm = PeerManager::new(pm_config, globals.clone()).await.unwrap(); + let mut pm_swarm = swarm::new_test_swarm(Behaviour::new(pm)); + let pm_addr = swarm::bind_listener(&mut pm_swarm).await; + let service = Service { swarm: pm_swarm }; + (service, pm_addr) + }; + + let excess_banned_peers = 15; + let peers_to_ban = peer_manager::peerdb::MAX_BANNED_PEERS + excess_banned_peers; + + // Build all the dummy peers needed. + let (mut swarm_pool, peers) = { + let mut pool = swarm::SwarmPool::with_capacity(peers_to_ban); + let mut peers = HashSet::with_capacity(peers_to_ban); + for _ in 0..peers_to_ban { + let mut peer_swarm = + swarm::new_test_swarm(DummyBehaviour::with_keep_alive(KeepAlive::Yes)); + let _peer_addr = swarm::bind_listener(&mut peer_swarm).await; + // It is ok to dial all at the same time since the swarm handles an event at a time. + peer_swarm.dial(pm_addr.clone()).unwrap(); + let peer_id = pool.insert(peer_swarm); + peers.insert(peer_id); + } + (pool, peers) + }; + + // we track banned peers at the swarm level here since there is no access to that info. + let mut swarm_banned_peers = HashMap::with_capacity(peers_to_ban); + let mut peers_unbanned = 0; + let timeout = tokio::time::sleep(tokio::time::Duration::from_secs(30)); + futures::pin_mut!(timeout); + + loop { + // poll the pm and dummy swarms. + tokio::select! { + pm_event = pm_service.select_next_some() => { + debug!("[PM] {:?}", pm_event); + match pm_event { + SwarmEvent::Behaviour(Ev(ev)) => match ev { + PeerManagerEvent::Banned(peer_id, _) => { + let has_been_unbanned = false; + swarm_banned_peers.insert(peer_id, has_been_unbanned); + } + PeerManagerEvent::UnBanned(peer_id, _) => { + *swarm_banned_peers.get_mut(&peer_id).expect("Unbanned peer must be banned first") = true; + peers_unbanned += 1; + } + _ => {} + } + SwarmEvent::ConnectionEstablished { + peer_id, + endpoint: _, + num_established: _, + concurrent_dial_errors: _, + } => { + assert!(peers.contains(&peer_id)); + // now we report the peer as banned. + pm_service + .swarm + .behaviour_mut() + .pm_call_trace + .inner() + .report_peer( + &peer_id, + PeerAction::Fatal, + ReportSource::Processor, + None, + "" + ); + }, + _ => {} + } + } + Some((_peer_id, _peer_ev)) = swarm_pool.next() => { + // we need to poll the swarms to keep the peers going + } + _ = timeout.as_mut() => { + panic!("Test timeout.") + } + } + + if peers_unbanned == excess_banned_peers { + let pdb = globals.peers.read(); + let inconsistencies = swarm_banned_peers + .into_iter() + .map(|(peer_id, was_unbanned)| { + was_unbanned + != pdb.peer_info(&peer_id).map_or( + false, /* We forgot about a banned peer */ + PeerInfo::is_banned, + ) + }); + assert_eq!( + inconsistencies + .filter(|is_consistent| *is_consistent) + .count(), + peers_to_ban + ); + return; + } + } +} diff --git a/node/network/tests/rpc_tests.rs b/node/network/tests/rpc_tests.rs new file mode 100644 index 0000000..f931cca --- /dev/null +++ b/node/network/tests/rpc_tests.rs @@ -0,0 +1,393 @@ +#![cfg(test)] +use network::rpc::methods::*; +use network::{BehaviourEvent, Libp2pEvent, ReportSource, Request, Response}; +use ssz_types::VariableList; +use std::sync::Arc; +use std::time::Duration; +use tokio::runtime::Runtime; +use tokio::time::sleep; +use tracing::{debug, warn}; +use tracing_test::traced_test; + +mod common; + +// Tests the STATUS RPC message +#[test] +#[traced_test] +#[allow(clippy::single_match)] +fn test_status_rpc() { + let rt = Arc::new(Runtime::new().unwrap()); + + rt.block_on(async { + // get sender/receiver + let (mut sender, mut receiver) = common::build_node_pair(Arc::downgrade(&rt)).await; + + // Dummy STATUS RPC message + let rpc_request = Request::Status(StatusMessage { data: 2 }); + + // Dummy STATUS RPC message + let rpc_response = Response::Status(StatusMessage { data: 3 }); + + // build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { + // Send a STATUS message + debug!("Sending RPC"); + sender + .swarm + .behaviour_mut() + .send_request(peer_id, 10, rpc_request.clone()); + } + Libp2pEvent::Behaviour(BehaviourEvent::ResponseReceived { + peer_id: _, + id: 10, + response, + }) => { + // Should receive the RPC response + debug!("Sender Received"); + assert_eq!(response, rpc_response.clone()); + debug!("Sender Completed"); + return; + } + _ => {} + } + } + }; + + // build the receiver future + let receiver_future = async { + loop { + match receiver.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::RequestReceived { + peer_id, + id, + request, + }) => { + if request == rpc_request { + // send the response + debug!("Receiver Received"); + receiver.swarm.behaviour_mut().send_successful_response( + peer_id, + id, + rpc_response.clone(), + ); + } + } + _ => {} // Ignore other events + } + } + }; + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }) +} + +// Tests a streamed DataByHash RPC Message +#[test] +#[traced_test] +#[allow(clippy::single_match)] +fn test_data_by_hash_chunked_rpc() { + let messages_to_send = 6; + let rt = Arc::new(Runtime::new().unwrap()); + + rt.block_on(async { + // get sender/receiver + let (mut sender, mut receiver) = common::build_node_pair(Arc::downgrade(&rt)).await; + + // DataByHash Request + let rpc_request = Request::DataByHash(DataByHashRequest { + hashes: VariableList::from(vec![ + Hash256::from_low_u64_be(0), + Hash256::from_low_u64_be(0), + Hash256::from_low_u64_be(0), + Hash256::from_low_u64_be(0), + Hash256::from_low_u64_be(0), + Hash256::from_low_u64_be(0), + ]), + }); + + // DataByHash Response + let data = ZgsData { + hash: Hash256::from_low_u64_be(0), + }; + let rpc_response = Response::DataByHash(Some(Box::new(data))); + + // keep count of the number of messages received + let mut messages_received = 0; + let request_id = messages_to_send as usize; + + // build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { + // Send a DATA_BY_HASH message + debug!("Sending RPC"); + sender.swarm.behaviour_mut().send_request( + peer_id, + request_id, + rpc_request.clone(), + ); + } + Libp2pEvent::Behaviour(BehaviourEvent::ResponseReceived { + peer_id: _, + id: _, + response, + }) => { + warn!("Sender received a response: {:?}", &response); + match response { + Response::DataByHash(Some(_)) => { + assert_eq!(response, rpc_response.clone()); + messages_received += 1; + warn!("Chunk received"); + } + Response::DataByHash(None) => { + // should be exactly `messages_to_send` messages before terminating + assert_eq!(messages_received, messages_to_send); + // end the test + return; + } + _ => panic!("Invalid RPC received"), + } + } + _ => {} // Ignore other behaviour events + } + } + }; + + // build the receiver future + let receiver_future = async { + loop { + match receiver.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::RequestReceived { + peer_id, + id, + request, + }) => { + if request == rpc_request { + // send the response + warn!("Receiver got request"); + for _ in 0..messages_to_send { + let rpc_response = rpc_response.clone(); + + receiver.swarm.behaviour_mut().send_successful_response( + peer_id, + id, + rpc_response.clone(), + ); + } + // send the stream termination + receiver.swarm.behaviour_mut().send_successful_response( + peer_id, + id, + Response::DataByHash(None), + ); + } + } + _ => {} // Ignore other events + } + } + }; + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }) +} + +// Tests that a streamed DataByHash RPC Message terminates when all expected chunks were received +#[test] +#[traced_test] +fn test_data_by_hash_chunked_rpc_terminates_correctly() { + let messages_to_send = 10; + let extra_messages_to_send = 10; + let rt = Arc::new(Runtime::new().unwrap()); + + rt.block_on(async { + // get sender/receiver + let (mut sender, mut receiver) = common::build_node_pair(Arc::downgrade(&rt)).await; + + // DataByHash Request + let rpc_request = Request::DataByHash(DataByHashRequest { + hashes: VariableList::from( + (0..messages_to_send) + .map(|_| Hash256::from_low_u64_be(0)) + .collect::>(), + ), + }); + + // DataByHash Response + let data = ZgsData { + hash: Hash256::from_low_u64_be(0), + }; + let rpc_response = Response::DataByHash(Some(Box::new(data))); + + // keep count of the number of messages received + let mut messages_received: u64 = 0; + let request_id = messages_to_send as usize; + // build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { + // Send a STATUS message + debug!("Sending RPC"); + sender.swarm.behaviour_mut().send_request( + peer_id, + request_id, + rpc_request.clone(), + ); + } + Libp2pEvent::Behaviour(BehaviourEvent::ResponseReceived { + peer_id: _, + id: _, + response, + }) => + // Should receive the RPC response + { + debug!("Sender received a response"); + match response { + Response::DataByHash(Some(_)) => { + assert_eq!(response, rpc_response.clone()); + messages_received += 1; + } + Response::DataByHash(None) => { + // should be exactly 10 messages, as requested + assert_eq!(messages_received, messages_to_send); + } + _ => panic!("Invalid RPC received"), + } + } + + _ => {} // Ignore other behaviour events + } + } + }; + + // determine messages to send (PeerId, RequestId). If some, indicates we still need to send + // messages + let mut message_info = None; + // the number of messages we've sent + let mut messages_sent = 0; + let receiver_future = async { + loop { + // this future either drives the sending/receiving or times out allowing messages to be + // sent in the timeout + match futures::future::select( + Box::pin(receiver.next_event()), + Box::pin(tokio::time::sleep(Duration::from_secs(1))), + ) + .await + { + futures::future::Either::Left(( + Libp2pEvent::Behaviour(BehaviourEvent::RequestReceived { + peer_id, + id, + request, + }), + _, + )) => { + if request == rpc_request { + // send the response + warn!("Receiver got request"); + message_info = Some((peer_id, id)); + } + } + futures::future::Either::Right((_, _)) => {} // The timeout hit, send messages if required + _ => continue, + } + + // if we need to send messages send them here. This will happen after a delay + if message_info.is_some() { + messages_sent += 1; + let (peer_id, stream_id) = message_info.as_ref().unwrap(); + receiver.swarm.behaviour_mut().send_successful_response( + *peer_id, + *stream_id, + rpc_response.clone(), + ); + debug!("Sending message {}", messages_sent); + if messages_sent == messages_to_send + extra_messages_to_send { + // stop sending messages + return; + } + } + } + }; + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }) +} + +// Tests a Goodbye RPC message +#[test] +#[traced_test] +#[allow(clippy::single_match)] +fn test_goodbye_rpc() { + let rt = Arc::new(Runtime::new().unwrap()); + + rt.block_on(async { + let (mut sender, mut receiver) = common::build_node_pair(Arc::downgrade(&rt)).await; + + // build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::PeerConnectedOutgoing(peer_id)) => { + // Send a goodbye and disconnect + debug!("Sending RPC"); + sender.swarm.behaviour_mut().goodbye_peer( + &peer_id, + GoodbyeReason::IrrelevantNetwork, + ReportSource::SyncService, + ); + } + Libp2pEvent::Behaviour(BehaviourEvent::PeerDisconnected(_)) => { + return; + } + _ => {} // Ignore other RPC messages + } + } + }; + + // build the receiver future + let receiver_future = async { + loop { + match receiver.next_event().await { + Libp2pEvent::Behaviour(BehaviourEvent::PeerDisconnected(_)) => { + // Should receive sent RPC request + return; + } + _ => {} // Ignore other events + } + } + }; + + let total_future = futures::future::join(sender_future, receiver_future); + + tokio::select! { + _ = total_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }) +} diff --git a/node/router/Cargo.toml b/node/router/Cargo.toml new file mode 100644 index 0000000..b9c89be --- /dev/null +++ b/node/router/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "router" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = "0.4.19" +error-chain = "0.12.4" +futures = "0.3.21" +file_location_cache = { path = "../file_location_cache" } +lazy_static = "1.4.0" +miner = { path = "../miner" } +network = { path = "../network" } +shared_types = { path = "../shared_types" } +storage = { path = "../storage" } +storage-async = { path = "../storage-async" } +sync = { path = "../sync" } +task_executor = { path = "../../common/task_executor" } +tokio = { version = "1.19.2", features = ["full"] } +tracing = "0.1.35" +rand = "0.8.5" + +[dev-dependencies] +channel = { path = "../../common/channel" } diff --git a/node/router/src/lib.rs b/node/router/src/lib.rs new file mode 100644 index 0000000..8b619d8 --- /dev/null +++ b/node/router/src/lib.rs @@ -0,0 +1,32 @@ +#[macro_use] +extern crate tracing; + +mod libp2p_event_handler; +mod peer_manager; +mod service; + +use network::Multiaddr; + +pub use crate::service::RouterService; + +#[derive(Debug, Clone)] +pub struct Config { + pub heartbeat_interval_secs: u64, + + pub idle_time_secs: u64, + pub max_idle_incoming_peers: usize, + pub max_idle_outgoing_peers: usize, + pub libp2p_nodes: Vec, +} + +impl Default for Config { + fn default() -> Self { + Self { + heartbeat_interval_secs: 5, + idle_time_secs: 180, + max_idle_incoming_peers: 12, + max_idle_outgoing_peers: 20, + libp2p_nodes: vec![], + } + } +} diff --git a/node/router/src/libp2p_event_handler.rs b/node/router/src/libp2p_event_handler.rs new file mode 100644 index 0000000..0b6c25f --- /dev/null +++ b/node/router/src/libp2p_event_handler.rs @@ -0,0 +1,786 @@ +use std::{ops::Neg, sync::Arc}; + +use file_location_cache::FileLocationCache; +use network::{ + rpc::StatusMessage, + types::{AnnounceFile, FindFile, SignedAnnounceFile}, + Keypair, MessageAcceptance, MessageId, NetworkGlobals, NetworkMessage, PeerId, PeerRequestId, + PublicKey, PubsubMessage, Request, RequestId, Response, +}; +use shared_types::{timestamp_now, TxID}; +use storage_async::Store; +use sync::{SyncMessage, SyncSender}; +use tokio::sync::{mpsc, RwLock}; + +use crate::peer_manager::PeerManager; + +lazy_static::lazy_static! { + pub static ref FIND_FILE_TIMEOUT: chrono::Duration = chrono::Duration::minutes(2); + pub static ref ANNOUNCE_FILE_TIMEOUT: chrono::Duration = chrono::Duration::minutes(2); + pub static ref TOLERABLE_DRIFT: chrono::Duration = chrono::Duration::seconds(5); +} + +fn duration_since(timestamp: u32) -> chrono::Duration { + let timestamp = i64::try_from(timestamp).expect("Should fit"); + let timestamp = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0).expect("should fit"); + let now = chrono::Utc::now().naive_utc(); + now.signed_duration_since(timestamp) +} + +fn peer_id_to_public_key(peer_id: &PeerId) -> Result { + // A libp2p peer id byte representation should be 2 length bytes + 4 protobuf bytes + compressed pk bytes + // if generated from a PublicKey with Identity multihash. + let pk_bytes = &peer_id.to_bytes()[2..]; + + PublicKey::from_protobuf_encoding(pk_bytes).map_err(|e| { + format!( + " Cannot parse libp2p public key public key from peer id: {}", + e + ) + }) +} + +pub struct Libp2pEventHandler { + /// A collection of global variables, accessible outside of the network service. + network_globals: Arc, + /// A channel to the router service. + network_send: mpsc::UnboundedSender, + /// A channel to the syncing service. + sync_send: SyncSender, + /// Node keypair for signing messages. + local_keypair: Keypair, + /// Log and transaction storage. + store: Store, + /// Cache for storing and serving gossip messages. + file_location_cache: Arc, + /// All connected peers. + peers: Arc>, +} + +impl Libp2pEventHandler { + pub fn new( + network_globals: Arc, + network_send: mpsc::UnboundedSender, + sync_send: SyncSender, + local_keypair: Keypair, + store: Store, + file_location_cache: Arc, + peers: Arc>, + ) -> Self { + Self { + network_globals, + network_send, + sync_send, + local_keypair, + store, + file_location_cache, + peers, + } + } + + fn send_to_network(&self, message: NetworkMessage) { + self.network_send.send(message).unwrap_or_else(|err| { + warn!(%err, "Could not send message to the network service"); + }); + } + + pub fn send_to_sync(&self, message: SyncMessage) { + self.sync_send.notify(message).unwrap_or_else(|err| { + warn!(%err, "Could not send message to the sync service"); + }); + } + + pub fn publish(&self, msg: PubsubMessage) { + self.send_to_network(NetworkMessage::Publish { + messages: vec![msg], + }); + } + + pub fn send_status(&self, peer_id: PeerId) { + let status_message = StatusMessage { data: 123 }; // dummy status message + debug!(%peer_id, ?status_message, "Sending Status request"); + + self.send_to_network(NetworkMessage::SendRequest { + peer_id, + request_id: RequestId::Router, + request: Request::Status(status_message), + }); + } + + pub async fn on_peer_connected(&self, peer_id: PeerId, outgoing: bool) { + self.peers.write().await.add(peer_id, outgoing); + + if outgoing { + self.send_status(peer_id); + self.send_to_sync(SyncMessage::PeerConnected { peer_id }); + } + } + + pub async fn on_peer_disconnected(&self, peer_id: PeerId) { + self.peers.write().await.remove(&peer_id); + self.send_to_sync(SyncMessage::PeerDisconnected { peer_id }); + } + + pub async fn on_rpc_request( + &self, + peer_id: PeerId, + request_id: PeerRequestId, + request: Request, + ) { + self.peers.write().await.update(&peer_id); + + match request { + Request::Status(status) => { + self.on_status_request(peer_id, request_id, status); + } + Request::GetChunks(request) => { + self.send_to_sync(SyncMessage::RequestChunks { + peer_id, + request_id, + request, + }); + } + Request::DataByHash(_) => { + // ignore + } + } + } + + fn on_status_request(&self, peer_id: PeerId, request_id: PeerRequestId, status: StatusMessage) { + debug!(%peer_id, ?status, "Received Status request"); + + let status_message = StatusMessage { data: 456 }; // dummy status message + debug!(%peer_id, ?status_message, "Sending Status response"); + + self.send_to_network(NetworkMessage::SendResponse { + peer_id, + id: request_id, + response: Response::Status(status_message), + }); + } + + pub async fn on_rpc_response( + &self, + peer_id: PeerId, + request_id: RequestId, + response: Response, + ) { + self.peers.write().await.update(&peer_id); + + match response { + Response::Status(status_message) => { + debug!(%peer_id, ?status_message, "Received Status response"); + } + Response::Chunks(response) => { + let request_id = match request_id { + RequestId::Sync(sync_id) => sync_id, + _ => unreachable!("All Chunks responses belong to sync"), + }; + + self.send_to_sync(SyncMessage::ChunksResponse { + peer_id, + request_id, + response, + }); + } + Response::DataByHash(_) => { + // ignore + } + } + } + + pub async fn on_rpc_error(&self, peer_id: PeerId, request_id: RequestId) { + self.peers.write().await.update(&peer_id); + + // Check if the failed RPC belongs to sync + if let RequestId::Sync(request_id) = request_id { + self.send_to_sync(SyncMessage::RpcError { + peer_id, + request_id, + }); + } + } + + pub async fn on_pubsub_message( + &self, + propagation_source: PeerId, + source: PeerId, + id: &MessageId, + message: PubsubMessage, + ) -> MessageAcceptance { + info!(?message, %propagation_source, %source, %id, "Received pubsub message"); + + match message { + PubsubMessage::ExampleMessage(_) => MessageAcceptance::Ignore, + PubsubMessage::FindFile(msg) => self.on_find_file(msg).await, + PubsubMessage::AnnounceFile(msg) => self.on_announce_file(propagation_source, msg), + } + } + + pub fn construct_announce_file_message(&self, tx_id: TxID) -> Option { + let peer_id = *self.network_globals.peer_id.read(); + + let addr = match self.network_globals.listen_multiaddrs.read().first() { + Some(addr) => addr.clone(), + None => { + error!("No listen address available"); + return None; + } + }; + + let timestamp = timestamp_now(); + + let msg = AnnounceFile { + tx_id, + peer_id: peer_id.into(), + at: addr.into(), + timestamp, + }; + + let mut signed = match msg.into_signed(&self.local_keypair) { + Ok(signed) => signed, + Err(e) => { + error!(%tx_id.seq, %e, "Failed to sign AnnounceFile message"); + return None; + } + }; + + signed.resend_timestamp = timestamp; + + Some(PubsubMessage::AnnounceFile(signed)) + } + + async fn on_find_file(&self, msg: FindFile) -> MessageAcceptance { + let FindFile { tx_id, timestamp } = msg; + + // verify timestamp + let d = duration_since(timestamp); + if d < TOLERABLE_DRIFT.neg() || d > *FIND_FILE_TIMEOUT { + debug!(%timestamp, "Invalid timestamp, ignoring FindFile message"); + return MessageAcceptance::Ignore; + } + + // check if we have it + if matches!(self.store.check_tx_completed(tx_id.seq).await, Ok(true)) { + if let Ok(Some(tx)) = self.store.get_tx_by_seq_number(tx_id.seq).await { + if tx.id() == tx_id { + debug!(?tx_id, "Found file locally, responding to FindFile query"); + + return match self.construct_announce_file_message(tx_id) { + Some(msg) => { + self.publish(msg); + MessageAcceptance::Ignore + } + // propagate FindFile query to other nodes + None => MessageAcceptance::Accept, + }; + } + } + } + + // try from cache + if let Some(mut msg) = self.file_location_cache.get_one(tx_id) { + debug!(?tx_id, "Found file in cache, responding to FindFile query"); + + msg.resend_timestamp = timestamp_now(); + self.publish(PubsubMessage::AnnounceFile(msg)); + + return MessageAcceptance::Ignore; + } + + // propagate FindFile query to other nodes + MessageAcceptance::Accept + } + + fn on_announce_file( + &self, + propagation_source: PeerId, + msg: SignedAnnounceFile, + ) -> MessageAcceptance { + // verify message signature + let pk = match peer_id_to_public_key(&msg.peer_id) { + Ok(pk) => pk, + Err(e) => { + error!( + "Failed to convert peer id {:?} to public key: {:?}", + msg.peer_id, e + ); + return MessageAcceptance::Reject; + } + }; + + if !msg.verify_signature(&pk) { + warn!( + "Received message with invalid signature from peer {:?}", + propagation_source + ); + return MessageAcceptance::Reject; + } + + // propagate gossip to peers + let d = duration_since(msg.resend_timestamp); + if d < TOLERABLE_DRIFT.neg() || d > *ANNOUNCE_FILE_TIMEOUT { + debug!(%msg.resend_timestamp, "Invalid resend timestamp, ignoring AnnounceFile message"); + return MessageAcceptance::Ignore; + } + + // notify sync layer + self.send_to_sync(SyncMessage::AnnounceFileGossip { + tx_id: msg.tx_id, + peer_id: msg.peer_id.clone().into(), + addr: msg.at.clone().into(), + }); + + // insert message to cache + self.file_location_cache.insert(msg); + + MessageAcceptance::Accept + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use channel::Message::*; + use file_location_cache::{test_util::AnnounceFileBuilder, FileLocationCache}; + use network::{ + discovery::{CombinedKey, ConnectionId}, + discv5::enr::EnrBuilder, + rpc::{GetChunksRequest, StatusMessage, SubstreamId}, + types::FindFile, + CombinedKeyExt, Keypair, MessageAcceptance, MessageId, Multiaddr, NetworkGlobals, + NetworkMessage, PeerId, PubsubMessage, Request, RequestId, Response, SyncId, + }; + use shared_types::{timestamp_now, ChunkArray, ChunkArrayWithProof, FlowRangeProof, TxID}; + use storage::{ + log_store::{log_manager::LogConfig, Store}, + LogManager, + }; + use sync::{test_util::create_2_store, SyncMessage, SyncReceiver, SyncSender}; + use task_executor::test_utils::TestRuntime; + use tokio::sync::{ + mpsc::{self, error::TryRecvError}, + RwLock, + }; + + use crate::{peer_manager::PeerManager, Config}; + + use super::*; + + struct Context { + runtime: TestRuntime, + network_globals: Arc, + keypair: Keypair, + network_send: mpsc::UnboundedSender, + network_recv: mpsc::UnboundedReceiver, + sync_send: SyncSender, + sync_recv: SyncReceiver, + store: Arc>, + file_location_cache: Arc, + peers: Arc>, + } + + impl Default for Context { + fn default() -> Self { + let runtime = TestRuntime::default(); + let (network_globals, keypair) = Context::new_network_globals(); + let (network_send, network_recv) = mpsc::unbounded_channel(); + let (sync_send, sync_recv) = channel::Channel::unbounded(); + let store = LogManager::memorydb(LogConfig::default()).unwrap(); + Self { + runtime, + network_globals: Arc::new(network_globals), + keypair, + network_send, + network_recv, + sync_send, + sync_recv, + store: Arc::new(RwLock::new(store)), + file_location_cache: Arc::new(FileLocationCache::default()), + peers: Arc::new(RwLock::new(PeerManager::new(Config::default()))), + } + } + } + + impl Context { + fn new_handler(&self) -> Libp2pEventHandler { + Libp2pEventHandler::new( + self.network_globals.clone(), + self.network_send.clone(), + self.sync_send.clone(), + self.keypair.clone(), + storage_async::Store::new(self.store.clone(), self.runtime.task_executor.clone()), + self.file_location_cache.clone(), + self.peers.clone(), + ) + } + + fn new_network_globals() -> (NetworkGlobals, Keypair) { + let keypair = Keypair::generate_secp256k1(); + let enr_key = CombinedKey::from_libp2p(&keypair).unwrap(); + let enr = EnrBuilder::new("v4").build(&enr_key).unwrap(); + let network_globals = NetworkGlobals::new(enr, 30000, 30000, vec![]); + + let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/30000".parse().unwrap(); + network_globals.listen_multiaddrs.write().push(listen_addr); + + (network_globals, keypair) + } + + fn assert_status_request(&mut self, expected_peer_id: PeerId) { + match self.network_recv.try_recv() { + Ok(NetworkMessage::SendRequest { + peer_id, + request, + request_id, + }) => { + assert_eq!(peer_id, expected_peer_id); + assert!(matches!(request, Request::Status(..))); + assert!(matches!(request_id, RequestId::Router)) + } + Ok(_) => panic!("Unexpected network message type received"), + Err(e) => panic!("No network message received: {:?}", e), + } + } + + fn assert_file_announcement_published(&mut self, expected_tx_id: TxID) { + match self.network_recv.try_recv() { + Ok(NetworkMessage::Publish { messages }) => { + assert_eq!(messages.len(), 1); + assert!( + matches!(&messages[0], PubsubMessage::AnnounceFile(file) if file.tx_id == expected_tx_id) + ); + } + Ok(_) => panic!("Unexpected network message type received"), + Err(e) => panic!("No network message received: {:?}", e), + } + } + } + + #[test] + fn test_send_status() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + assert!(matches!( + ctx.network_recv.try_recv(), + Err(TryRecvError::Empty) + )); + + let alice = PeerId::random(); + handler.send_status(alice); + + ctx.assert_status_request(alice); + } + + #[tokio::test] + async fn test_on_peer_connected_incoming() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + assert_eq!(handler.peers.read().await.size(), 0); + + let alice = PeerId::random(); + handler.on_peer_connected(alice, false).await; + + assert_eq!(handler.peers.read().await.size(), 1); + assert!(matches!( + ctx.network_recv.try_recv(), + Err(TryRecvError::Empty) + )); + assert!(matches!(ctx.sync_recv.try_recv(), Err(TryRecvError::Empty))); + } + + #[tokio::test] + async fn test_on_peer_connected_outgoing() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + assert_eq!(handler.peers.read().await.size(), 0); + + let alice = PeerId::random(); + handler.on_peer_connected(alice, true).await; + + assert_eq!(handler.peers.read().await.size(), 1); + ctx.assert_status_request(alice); + assert!(matches!( + ctx.sync_recv.try_recv(), + Ok(Notification(SyncMessage::PeerConnected {peer_id})) if peer_id == alice + )); + } + + #[tokio::test] + async fn test_on_peer_disconnected() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + let alice = PeerId::random(); + handler.on_peer_connected(alice, false).await; + assert_eq!(handler.peers.read().await.size(), 1); + + handler.on_peer_disconnected(alice).await; + assert_eq!(handler.peers.read().await.size(), 0); + assert!(matches!( + ctx.sync_recv.try_recv(), + Ok(Notification(SyncMessage::PeerDisconnected {peer_id})) if peer_id == alice + )); + } + + #[tokio::test] + async fn test_on_rpc_request_status() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + let alice = PeerId::random(); + let req_id = (ConnectionId::new(4), SubstreamId(12)); + let request = Request::Status(StatusMessage { data: 412 }); + handler.on_rpc_request(alice, req_id, request).await; + + match ctx.network_recv.try_recv() { + Ok(NetworkMessage::SendResponse { + peer_id, + response, + id, + }) => { + assert_eq!(peer_id, alice); + assert!(matches!(response, Response::Status(..))); + assert_eq!(id, req_id); + } + Ok(_) => panic!("Unexpected network message type received"), + Err(e) => panic!("No network message received: {:?}", e), + } + } + + #[tokio::test] + async fn test_on_rpc_request_get_chunks() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + let alice = PeerId::random(); + let id = (ConnectionId::new(4), SubstreamId(12)); + let raw_request = GetChunksRequest { + tx_id: TxID::random_hash(7), + index_start: 66, + index_end: 99, + }; + handler + .on_rpc_request(alice, id, Request::GetChunks(raw_request.clone())) + .await; + + match ctx.sync_recv.try_recv() { + Ok(Notification(SyncMessage::RequestChunks { + peer_id, + request_id, + request, + })) => { + assert_eq!(peer_id, alice); + assert_eq!(request_id, id); + assert_eq!(request, raw_request); + } + Ok(_) => panic!("Unexpected sync message type received"), + Err(e) => panic!("No sync message received: {:?}", e), + } + } + + #[tokio::test] + async fn test_on_rpc_response() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + let alice = PeerId::random(); + let id = TxID::random_hash(555); + let data = ChunkArrayWithProof { + chunks: ChunkArray { + data: vec![1, 2, 3, 4], + start_index: 16, + }, + proof: FlowRangeProof::new_empty(), + }; + handler + .on_rpc_response( + alice, + RequestId::Sync(SyncId::SerialSync { tx_id: id }), + Response::Chunks(data.clone()), + ) + .await; + + match ctx.sync_recv.try_recv() { + Ok(Notification(SyncMessage::ChunksResponse { + peer_id, + request_id, + response, + })) => { + assert_eq!(peer_id, alice); + assert!(matches!(request_id, SyncId::SerialSync { tx_id } if tx_id == id )); + assert_eq!(response, data); + } + Ok(_) => panic!("Unexpected sync message type received"), + Err(e) => panic!("No sync message received: {:?}", e), + } + } + + #[tokio::test] + async fn test_on_rpc_error() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + let alice = PeerId::random(); + let id = TxID::random_hash(555); + handler + .on_rpc_error(alice, RequestId::Sync(SyncId::SerialSync { tx_id: id })) + .await; + + match ctx.sync_recv.try_recv() { + Ok(Notification(SyncMessage::RpcError { + peer_id, + request_id, + })) => { + assert_eq!(peer_id, alice); + assert!(matches!(request_id, SyncId::SerialSync { tx_id } if tx_id == id )); + } + Ok(_) => panic!("Unexpected sync message type received"), + Err(e) => panic!("No sync message received: {:?}", e), + } + } + + async fn handle_find_file_msg( + handler: &Libp2pEventHandler, + tx_id: TxID, + timestamp: u32, + ) -> MessageAcceptance { + let (alice, bob) = (PeerId::random(), PeerId::random()); + let id = MessageId::new(b"dummy message"); + let message = PubsubMessage::FindFile(FindFile { tx_id, timestamp }); + handler.on_pubsub_message(alice, bob, &id, message).await + } + + #[tokio::test] + async fn test_on_pubsub_find_file_invalid_timestamp() { + let ctx = Context::default(); + let handler = ctx.new_handler(); + + // message too future + let result = handle_find_file_msg( + &handler, + TxID::random_hash(412), + timestamp_now() + 10 + TOLERABLE_DRIFT.num_seconds() as u32, + ) + .await; + assert!(matches!(result, MessageAcceptance::Ignore)); + + // message too old + let result = handle_find_file_msg( + &handler, + TxID::random_hash(412), + timestamp_now() - 10 - FIND_FILE_TIMEOUT.num_seconds() as u32, + ) + .await; + assert!(matches!(result, MessageAcceptance::Ignore)); + } + + #[tokio::test] + async fn test_on_pubsub_find_file_not_found() { + let ctx = Context::default(); + let handler = ctx.new_handler(); + + let result = handle_find_file_msg(&handler, TxID::random_hash(412), timestamp_now()).await; + assert!(matches!(result, MessageAcceptance::Accept)); + } + + #[tokio::test] + async fn test_on_pubsub_find_file_in_store() { + let mut ctx = Context::default(); + + // prepare store with txs + let (_, store, txs, _) = create_2_store(vec![1314]); + ctx.store = store; + + let handler = ctx.new_handler(); + + // receive find file request + let result = handle_find_file_msg(&handler, txs[0].id(), timestamp_now()).await; + assert!(matches!(result, MessageAcceptance::Ignore)); + ctx.assert_file_announcement_published(txs[0].id()); + } + + #[tokio::test] + async fn test_on_pubsub_find_file_in_cache() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + // prepare tx in cache + let tx_id = TxID::random_hash(412); + let signed = AnnounceFileBuilder::default() + .with_tx_id(tx_id) + .with_timestamp(timestamp_now() - 5) + .build(); + ctx.file_location_cache.insert(signed); + + // receive find file request + let result = handle_find_file_msg(&handler, tx_id, timestamp_now()).await; + assert!(matches!(result, MessageAcceptance::Ignore)); + ctx.assert_file_announcement_published(tx_id); + } + + #[tokio::test] + async fn test_on_pubsub_announce_file_invalid_sig() { + let ctx = Context::default(); + let handler = ctx.new_handler(); + + let (alice, bob) = (PeerId::random(), PeerId::random()); + let id = MessageId::new(b"dummy message"); + let tx_id = TxID::random_hash(412); + + // change signed message + let message = match handler.construct_announce_file_message(tx_id).unwrap() { + PubsubMessage::AnnounceFile(mut file) => { + let malicious_addr: Multiaddr = "/ip4/127.0.0.38/tcp/30000".parse().unwrap(); + file.inner.at = malicious_addr.into(); + PubsubMessage::AnnounceFile(file) + } + _ => panic!("Unexpected pubsub message type"), + }; + + // failed to verify signature + let result = handler.on_pubsub_message(alice, bob, &id, message).await; + assert!(matches!(result, MessageAcceptance::Reject)); + } + + #[tokio::test] + async fn test_on_pubsub_announce_file() { + let mut ctx = Context::default(); + let handler = ctx.new_handler(); + + // prepare message + let (alice, bob) = (PeerId::random(), PeerId::random()); + let id = MessageId::new(b"dummy message"); + let tx = TxID::random_hash(412); + let message = handler.construct_announce_file_message(tx).unwrap(); + + // succeeded to handle + let result = handler.on_pubsub_message(alice, bob, &id, message).await; + assert!(matches!(result, MessageAcceptance::Accept)); + + // ensure notify to sync layer + match ctx.sync_recv.try_recv() { + Ok(Notification(SyncMessage::AnnounceFileGossip { + tx_id, + peer_id, + addr, + })) => { + assert_eq!(tx_id, tx); + assert_eq!(peer_id, *ctx.network_globals.peer_id.read()); + assert_eq!( + addr, + *ctx.network_globals.listen_multiaddrs.read().get(0).unwrap() + ); + } + Ok(_) => panic!("Unexpected sync message type received"), + Err(e) => panic!("No sync message received: {:?}", e), + } + + // ensure cache updated + assert_eq!(ctx.file_location_cache.get_all(tx).len(), 1); + } +} diff --git a/node/router/src/peer_manager.rs b/node/router/src/peer_manager.rs new file mode 100644 index 0000000..243e3ac --- /dev/null +++ b/node/router/src/peer_manager.rs @@ -0,0 +1,221 @@ +use crate::Config; +use network::PeerId; +use rand::seq::IteratorRandom; +use std::collections::HashMap; +use std::time::Instant; + +/// Connected peer info. +struct PeerInfo { + /// Outgoing or incoming connection. + outgoing: bool, + /// Last update time. + since: Instant, +} + +impl PeerInfo { + fn new(outgoing: bool) -> Self { + Self { + outgoing, + since: Instant::now(), + } + } + + fn elapsed_secs(&self) -> u64 { + self.since.elapsed().as_secs() + } +} + +/// Manages connected outgoing and incoming peers. +/// +/// Basically, records the last update time for RPC requests/responses, +/// and disconnect some peers periodically if too many idle ones. So that, +/// there are enough incoming connections available for other peers to +/// sync file from this peer. +/// +/// On the other hand, pub-sub message propagation rely on peer connections, +/// so a peer should have enough peers connected to broadcast pub-sub messages. +#[derive(Default)] +pub struct PeerManager { + peers: HashMap, + config: Config, +} + +impl PeerManager { + pub fn new(config: Config) -> Self { + Self { + peers: Default::default(), + config, + } + } + + pub fn add(&mut self, peer_id: PeerId, outgoing: bool) -> bool { + let old = self.peers.insert(peer_id, PeerInfo::new(outgoing)); + if old.is_none() { + debug!(%peer_id, %outgoing, "New peer added"); + true + } else { + // peer should not be connected multiple times + error!(%peer_id, %outgoing, "Peer already exists"); + false + } + } + + pub fn remove(&mut self, peer_id: &PeerId) -> bool { + if self.peers.remove(peer_id).is_some() { + debug!(%peer_id, "Peer removed"); + true + } else { + error!(%peer_id, "Peer not found to remove"); + false + } + } + + /// Updates the timestamp of specified peer if any. + pub fn update(&mut self, peer_id: &PeerId) -> bool { + match self.peers.get_mut(peer_id) { + Some(peer) => { + peer.since = Instant::now(); + trace!(%peer_id, "Peer updated"); + true + } + None => { + error!(%peer_id, "Peer not found to update"); + false + } + } + } + + /// Finds idle peers for garbage collection in advance. + pub fn expired_peers(&self) -> Vec { + let mut expired_outgoing = self.expired(true, self.config.max_idle_outgoing_peers); + let mut expired_incoming = self.expired(false, self.config.max_idle_incoming_peers); + expired_outgoing.append(&mut expired_incoming); + expired_outgoing + } + + fn expired(&self, outgoing: bool, max_idle: usize) -> Vec { + let expired: Vec = self + .peers + .iter() + .filter(|(_, peer)| { + peer.outgoing == outgoing && peer.elapsed_secs() >= self.config.idle_time_secs + }) + .map(|(peer_id, _)| *peer_id) + .collect(); + + if expired.len() <= max_idle { + return vec![]; + } + + let num_expired = expired.len() - max_idle; + expired + .into_iter() + .choose_multiple(&mut rand::thread_rng(), num_expired) + } +} + +#[cfg(test)] +mod tests { + use std::{ + ops::Sub, + time::{Duration, Instant}, + }; + + use network::PeerId; + + use crate::Config; + + use super::PeerManager; + + impl PeerManager { + pub fn size(&self) -> usize { + self.peers.len() + } + } + + #[test] + fn test_add() { + let mut manager = PeerManager::new(Config::default()); + + let peer1 = PeerId::random(); + assert!(manager.add(peer1, false)); + assert!(!manager.add(peer1, false)); + assert!(!manager.add(peer1, true)); + assert_eq!(manager.size(), 1); + + let peer2 = PeerId::random(); + assert!(manager.add(peer2, false)); + assert_eq!(manager.size(), 2); + } + + #[test] + fn test_remove() { + let mut manager = PeerManager::new(Config::default()); + + let peer1 = PeerId::random(); + assert!(manager.add(peer1, false)); + let peer2 = PeerId::random(); + assert!(manager.add(peer2, true)); + + let peer3 = PeerId::random(); + assert!(!manager.remove(&peer3)); + assert!(manager.remove(&peer1)); + assert!(manager.remove(&peer2)); + assert_eq!(manager.size(), 0); + } + + #[test] + fn test_update() { + let mut manager = PeerManager::new(Config::default()); + + let peer1 = PeerId::random(); + assert!(manager.add(peer1, false)); + let ts1 = manager.peers.get(&peer1).unwrap().since; + + let peer2 = PeerId::random(); + assert!(!manager.update(&peer2)); + assert!(manager.update(&peer1)); + + let ts2 = manager.peers.get(&peer1).unwrap().since; + assert!(ts2 > ts1); + } + + #[test] + fn test_expired() { + let config = Config::default(); + let mut manager = PeerManager::new(config.clone()); + + let mut peers = vec![]; + + // setup incoming peers: max + 3 + for _ in 0..(config.max_idle_incoming_peers + 3) { + let peer_id = PeerId::random(); + peers.push(peer_id); + assert!(manager.add(peer_id, false)); + } + + // setup outgoing peers: max + 2 + for _ in 0..(config.max_idle_outgoing_peers + 2) { + let peer_id = PeerId::random(); + peers.push(peer_id); + assert!(manager.add(peer_id, true)); + } + + // change timestamp for all peers + let idle_timeout = Duration::from_secs(config.idle_time_secs); + for peer_id in peers.iter() { + let peer = manager.peers.get_mut(peer_id).unwrap(); + peer.since = Instant::now().sub(idle_timeout); + } + + assert_eq!( + manager.expired(false, config.max_idle_incoming_peers).len(), + 3 + ); + assert_eq!( + manager.expired(true, config.max_idle_outgoing_peers).len(), + 2 + ); + assert_eq!(manager.expired_peers().len(), 5); + } +} diff --git a/node/router/src/service.rs b/node/router/src/service.rs new file mode 100644 index 0000000..2621f15 --- /dev/null +++ b/node/router/src/service.rs @@ -0,0 +1,347 @@ +use crate::Config; +use crate::{libp2p_event_handler::Libp2pEventHandler, peer_manager::PeerManager}; +use file_location_cache::FileLocationCache; +use futures::{channel::mpsc::Sender, prelude::*}; +use miner::MinerMessage; +use network::{ + BehaviourEvent, Keypair, Libp2pEvent, NetworkGlobals, NetworkMessage, RequestId, + Service as LibP2PService, Swarm, +}; +use std::sync::Arc; +use std::time::Duration; +use storage::log_store::Store as LogStore; +use storage_async::Store; +use sync::{SyncMessage, SyncSender}; +use task_executor::ShutdownReason; +use tokio::sync::{broadcast, mpsc, RwLock}; +use tokio::time::interval; + +/// Service that handles communication between internal services and the libp2p service. +pub struct RouterService { + config: Config, + + /// The underlying libp2p service that drives all the network interactions. + libp2p: LibP2PService, + + /// A collection of global variables, accessible outside of the network service. + network_globals: Arc, + + /// The receiver channel for Zgs to communicate with the network service. + network_recv: mpsc::UnboundedReceiver, + + /// All connected peers. + peers: Arc>, + + /// Handler for libp2p events. + libp2p_event_handler: Libp2pEventHandler, + + /// Stores potentially created UPnP mappings to be removed on shutdown. (TCP port and UDP + /// port). + upnp_mappings: (Option, Option), +} + +impl RouterService { + #[allow(clippy::too_many_arguments)] + pub fn spawn( + executor: task_executor::TaskExecutor, + libp2p: LibP2PService, + network_globals: Arc, + network_recv: mpsc::UnboundedReceiver, + network_send: mpsc::UnboundedSender, + sync_send: SyncSender, + _miner_send: Option>, + store: Arc>, + file_location_cache: Arc, + local_keypair: Keypair, + config: Config, + ) { + let store = Store::new(store, executor.clone()); + let peers = Arc::new(RwLock::new(PeerManager::new(config.clone()))); + + // create the network service and spawn the task + let router = RouterService { + config, + libp2p, + network_globals: network_globals.clone(), + network_recv, + peers: peers.clone(), + libp2p_event_handler: Libp2pEventHandler::new( + network_globals, + network_send, + sync_send, + local_keypair, + store, + file_location_cache, + peers, + ), + upnp_mappings: (None, None), + }; + + // spawn service + let shutdown_sender = executor.shutdown_sender(); + + executor.spawn(router.main(shutdown_sender), "router"); + } + + async fn main(mut self, mut shutdown_sender: Sender) { + let mut heartbeat = interval(Duration::from_secs(self.config.heartbeat_interval_secs)); + + loop { + tokio::select! { + // handle a message sent to the network + Some(msg) = self.network_recv.recv() => self.on_network_msg(msg, &mut shutdown_sender).await, + + // handle event coming from the network + event = self.libp2p.next_event() => self.on_libp2p_event(event, &mut shutdown_sender).await, + + // heartbeat + _ = heartbeat.tick() => self.on_heartbeat().await, + } + } + } + + /// Handle an event received from the network. + async fn on_libp2p_event( + &mut self, + ev: Libp2pEvent, + shutdown_sender: &mut Sender, + ) { + debug!(?ev, "Received new event from libp2p"); + + match ev { + Libp2pEvent::Behaviour(event) => match event { + BehaviourEvent::PeerConnectedOutgoing(peer_id) => { + self.libp2p_event_handler + .on_peer_connected(peer_id, true) + .await; + } + BehaviourEvent::PeerConnectedIncoming(peer_id) => { + self.libp2p_event_handler + .on_peer_connected(peer_id, false) + .await; + } + BehaviourEvent::PeerBanned(_) | BehaviourEvent::PeerUnbanned(_) => { + // No action required for these events. + } + BehaviourEvent::PeerDisconnected(peer_id) => { + self.libp2p_event_handler + .on_peer_disconnected(peer_id) + .await; + } + BehaviourEvent::RequestReceived { + peer_id, + id, + request, + } => { + if self.network_globals.peers.read().is_connected(&peer_id) { + self.libp2p_event_handler + .on_rpc_request(peer_id, id, request) + .await; + } else { + debug!(%peer_id, ?request, "Dropping request of disconnected peer"); + } + } + BehaviourEvent::ResponseReceived { + peer_id, + id, + response, + } => { + self.libp2p_event_handler + .on_rpc_response(peer_id, id, response) + .await; + } + BehaviourEvent::RPCFailed { id, peer_id } => { + self.libp2p_event_handler.on_rpc_error(peer_id, id).await; + } + BehaviourEvent::StatusPeer(peer_id) => { + self.libp2p_event_handler.send_status(peer_id); + } + BehaviourEvent::PubsubMessage { + id, + propagation_source, + source, + message, + .. + } => { + let result = self + .libp2p_event_handler + .on_pubsub_message(propagation_source, source, &id, message) + .await; + + self.libp2p + .swarm + .behaviour_mut() + .report_message_validation_result(&propagation_source, id, result); + } + }, + Libp2pEvent::NewListenAddr(multiaddr) => { + self.network_globals + .listen_multiaddrs + .write() + .push(multiaddr); + } + Libp2pEvent::ZeroListeners => { + let _ = shutdown_sender + .send(ShutdownReason::Failure( + "All listeners are closed. Unable to listen", + )) + .await + .map_err(|e| { + warn!( + error = %e, + "failed to send a shutdown signal", + ) + }); + } + } + } + + /// Handle a message sent to the network service. + async fn on_network_msg( + &mut self, + msg: NetworkMessage, + _shutdown_sender: &mut Sender, + ) { + debug!(?msg, "Received new message"); + + match msg { + NetworkMessage::SendRequest { + peer_id, + request, + request_id, + } => { + self.libp2p.send_request(peer_id, request_id, request); + } + NetworkMessage::SendResponse { + peer_id, + response, + id, + } => { + self.libp2p.send_response(peer_id, id, response); + } + NetworkMessage::SendErrorResponse { + peer_id, + error, + id, + reason, + } => { + self.libp2p.respond_with_error(peer_id, id, error, reason); + } + NetworkMessage::Publish { messages } => { + if self.libp2p.swarm.connected_peers().next().is_none() { + // this is a boardcast message, when current node doesn't have any peers connected, try to connect any peer in config + for multiaddr in &self.config.libp2p_nodes { + match Swarm::dial(&mut self.libp2p.swarm, multiaddr.clone()) { + Ok(()) => { + debug!(address = %multiaddr, "Dialing libp2p peer"); + break; + } + Err(err) => { + debug!(address = %multiaddr, error = ?err, "Could not connect to peer") + } + }; + } + } + + let mut topic_kinds = Vec::new(); + for message in &messages { + if !topic_kinds.contains(&message.kind()) { + topic_kinds.push(message.kind()); + } + } + debug!( + count = messages.len(), + topics = ?topic_kinds, + "Sending pubsub messages", + ); + self.libp2p.swarm.behaviour_mut().publish(messages); + } + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => self.libp2p.report_peer(&peer_id, action, source, msg), + NetworkMessage::GoodbyePeer { + peer_id, + reason, + source, + } => self.libp2p.goodbye_peer(&peer_id, reason, source), + NetworkMessage::DialPeer { address, peer_id } => { + if self.libp2p.swarm.is_connected(&peer_id) { + self.libp2p_event_handler + .send_to_sync(SyncMessage::PeerConnected { peer_id }); + } else { + match Swarm::dial(&mut self.libp2p.swarm, address.clone()) { + Ok(()) => debug!(%address, "Dialing libp2p peer"), + Err(err) => { + info!(%address, error = ?err, "Failed to dial peer"); + self.libp2p_event_handler + .send_to_sync(SyncMessage::DailFailed { peer_id, err }); + } + }; + } + } + NetworkMessage::AnnounceLocalFile { tx_id } => { + if let Some(msg) = self + .libp2p_event_handler + .construct_announce_file_message(tx_id) + { + self.libp2p_event_handler.publish(msg); + } + } + NetworkMessage::UPnPMappingEstablished { + tcp_socket, + udp_socket, + } => { + self.upnp_mappings = (tcp_socket.map(|s| s.port()), udp_socket.map(|s| s.port())); + // If there is an external TCP port update, modify our local ENR. + if let Some(tcp_socket) = tcp_socket { + if let Err(e) = self + .libp2p + .swarm + .behaviour_mut() + .discovery_mut() + .update_enr_tcp_port(tcp_socket.port()) + { + warn!(error = %e, "Failed to update ENR"); + } + } + if let Some(udp_socket) = udp_socket { + if let Err(e) = self + .libp2p + .swarm + .behaviour_mut() + .discovery_mut() + .update_enr_udp_socket(udp_socket) + { + warn!(error = %e, "Failed to update ENR"); + } + } + } + } + } + + async fn on_heartbeat(&mut self) { + let expired_peers = self.peers.write().await.expired_peers(); + + trace!("heartbeat, expired peers = {:?}", expired_peers.len()); + + for peer_id in expired_peers { + // async operation, once peer disconnected, swarm event `PeerDisconnected` + // will be polled to handle in advance. + match self.libp2p.swarm.disconnect_peer_id(peer_id) { + Ok(_) => debug!(%peer_id, "Peer expired and disconnect it"), + Err(_) => error!(%peer_id, "Peer expired but failed to disconnect"), + } + } + } +} + +impl Drop for RouterService { + fn drop(&mut self) { + info!("Router service shutdown"); + // attempt to remove port mappings + network::nat::remove_mappings(self.upnp_mappings.0, self.upnp_mappings.1); + } +} diff --git a/node/rpc/Cargo.toml b/node/rpc/Cargo.toml new file mode 100644 index 0000000..0a26652 --- /dev/null +++ b/node/rpc/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "rpc" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +append_merkle = { path = "../../common/append_merkle" } +miner = {path = "../miner"} +futures = "0.3.21" +jsonrpsee = { version = "0.14.0", features = ["full"] } +network = { path = "../network" } +serde = { version = "1.0.137", features = ["derive"] } +base64 = "0.13.0" +shared_types = { path = "../shared_types" } +sync = { path = "../sync" } +task_executor = { path = "../../common/task_executor" } +tokio = { version = "1.19.2", features = ["macros", "sync"] } +tracing = "0.1.35" +chunk_pool = { path = "../chunk_pool" } +storage = { path = "../storage" } +storage-async = { path = "../storage-async" } +merkle_light = { path = "../../common/merkle_light" } +merkle_tree = { path = "../../common/merkle_tree"} +futures-channel = "^0.3" + +[dev-dependencies] +serde_json = "1.0.82" diff --git a/node/rpc/src/admin/api.rs b/node/rpc/src/admin/api.rs new file mode 100644 index 0000000..d4bc63d --- /dev/null +++ b/node/rpc/src/admin/api.rs @@ -0,0 +1,23 @@ +use crate::types::NetworkInfo; +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use std::collections::HashMap; +use sync::FileSyncInfo; + +#[rpc(server, client, namespace = "admin")] +pub trait Rpc { + #[method(name = "shutdown")] + async fn shutdown(&self) -> RpcResult<()>; + + #[method(name = "startSyncFile")] + async fn start_sync_file(&self, tx_seq: u64) -> RpcResult<()>; + + #[method(name = "getSyncStatus")] + async fn get_sync_status(&self, tx_seq: u64) -> RpcResult; + + #[method(name = "getSyncInfo")] + async fn get_sync_info(&self, tx_seq: Option) -> RpcResult>; + + #[method(name = "getNetworkInfo")] + async fn get_network_info(&self) -> RpcResult; +} diff --git a/node/rpc/src/admin/impl.rs b/node/rpc/src/admin/impl.rs new file mode 100644 index 0000000..0406c62 --- /dev/null +++ b/node/rpc/src/admin/impl.rs @@ -0,0 +1,101 @@ +use super::api::RpcServer; +use crate::types::NetworkInfo; +use crate::{error, Context}; +use futures::prelude::*; +use jsonrpsee::core::async_trait; +use jsonrpsee::core::RpcResult; +use std::collections::HashMap; +use sync::{FileSyncInfo, SyncRequest, SyncResponse}; +use task_executor::ShutdownReason; + +pub struct RpcServerImpl { + pub ctx: Context, +} + +#[async_trait] +impl RpcServer for RpcServerImpl { + #[tracing::instrument(skip(self), err)] + async fn shutdown(&self) -> RpcResult<()> { + info!("admin_shutdown()"); + + self.ctx + .shutdown_sender + .clone() + .send(ShutdownReason::Success("Shutdown by admin")) + .await + .map_err(|e| error::internal_error(format!("Failed to send shutdown command: {:?}", e))) + } + + #[tracing::instrument(skip(self), err)] + async fn start_sync_file(&self, tx_seq: u64) -> RpcResult<()> { + info!("admin_startSyncFile({tx_seq})"); + + let response = self + .ctx + .request_sync(SyncRequest::SyncFile { tx_seq }) + .await?; + + match response { + SyncResponse::SyncFile { err } => { + if err.is_empty() { + Ok(()) + } else { + Err(error::internal_error(err)) + } + } + _ => Err(error::internal_error("unexpected response type")), + } + } + + #[tracing::instrument(skip(self), err)] + async fn get_sync_status(&self, tx_seq: u64) -> RpcResult { + info!("admin_getSyncStatus({tx_seq})"); + + let response = self + .ctx + .request_sync(SyncRequest::SyncStatus { tx_seq }) + .await?; + + match response { + SyncResponse::SyncStatus { status } => Ok(status + .map(|x| format!("{:?}", x)) + .unwrap_or_else(|| "unknown".into())), + _ => Err(error::internal_error("unexpected response type")), + } + } + + #[tracing::instrument(skip(self), err)] + async fn get_sync_info(&self, tx_seq: Option) -> RpcResult> { + info!(?tx_seq, "admin_getSyncInfo()"); + + let response = self + .ctx + .request_sync(SyncRequest::FileSyncInfo { tx_seq }) + .await?; + + match response { + SyncResponse::FileSyncInfo { result } => Ok(result), + _ => Err(error::internal_error("unexpected response type")), + } + } + + #[tracing::instrument(skip(self), err)] + async fn get_network_info(&self) -> RpcResult { + info!("admin_getNetworkInfo()"); + + let db = self.ctx.network_globals.peers.read(); + + let connected_peers = db.connected_peers().count(); + let connected_outgoing_peers = db.connected_outbound_only_peers().count(); + + Ok(NetworkInfo { + peer_id: self.ctx.network_globals.local_peer_id().to_base58(), + total_peers: db.peers().count(), + banned_peers: db.banned_peers().count(), + disconnected_peers: db.disconnected_peers().count(), + connected_peers, + connected_outgoing_peers, + connected_incoming_peers: connected_peers - connected_outgoing_peers, + }) + } +} diff --git a/node/rpc/src/admin/mod.rs b/node/rpc/src/admin/mod.rs new file mode 100644 index 0000000..2882f83 --- /dev/null +++ b/node/rpc/src/admin/mod.rs @@ -0,0 +1,5 @@ +mod api; +mod r#impl; + +pub use api::RpcServer; +pub use r#impl::RpcServerImpl; diff --git a/node/rpc/src/config.rs b/node/rpc/src/config.rs new file mode 100644 index 0000000..c21a369 --- /dev/null +++ b/node/rpc/src/config.rs @@ -0,0 +1,9 @@ +use std::net::SocketAddr; + +#[derive(Clone)] +pub struct Config { + pub enabled: bool, + pub listen_address: SocketAddr, + pub chunks_per_segment: usize, + pub max_cache_file_size: usize, +} diff --git a/node/rpc/src/error.rs b/node/rpc/src/error.rs new file mode 100644 index 0000000..9369a4a --- /dev/null +++ b/node/rpc/src/error.rs @@ -0,0 +1,30 @@ +#![allow(dead_code)] + +use jsonrpsee::core::Error; +use jsonrpsee::types::error::{CallError, ErrorCode, ErrorObject}; + +pub fn not_supported() -> Error { + Error::Call(CallError::Custom(ErrorObject::borrowed( + ErrorCode::MethodNotFound.code(), + &"Not supported", + None, + ))) +} + +pub fn internal_error(msg: impl std::convert::AsRef) -> Error { + Error::Call(CallError::Custom(ErrorObject::owned( + ErrorCode::InternalError.code(), + "Internal error", + Some(msg.as_ref()), + ))) +} + +pub fn invalid_params(param: &str, msg: impl std::convert::AsRef) -> Error { + let error = &format!("Invalid params: {:}", param); + + Error::Call(CallError::Custom(ErrorObject::owned( + ErrorCode::InvalidParams.code(), + error, + Some(msg.as_ref()), + ))) +} diff --git a/node/rpc/src/lib.rs b/node/rpc/src/lib.rs new file mode 100644 index 0000000..b7ec0a0 --- /dev/null +++ b/node/rpc/src/lib.rs @@ -0,0 +1,82 @@ +#[macro_use] +extern crate tracing; + +extern crate miner as zgs_miner; + +mod admin; +mod config; +mod error; +mod miner; +mod zgs; +mod types; + +use crate::miner::RpcServer as MinerRpcServer; +use admin::RpcServer as AdminRpcServer; +use chunk_pool::MemoryChunkPool; +use futures::channel::mpsc::Sender; +use jsonrpsee::core::RpcResult; +use jsonrpsee::http_server::{HttpServerBuilder, HttpServerHandle}; +use network::NetworkGlobals; +use network::NetworkMessage; +use zgs::RpcServer as ZgsRpcServer; +use zgs_miner::MinerMessage; +use std::error::Error; +use std::sync::Arc; +use storage_async::Store; +use sync::{SyncRequest, SyncResponse, SyncSender}; +use task_executor::ShutdownReason; +use tokio::sync::broadcast; +use tokio::sync::mpsc::UnboundedSender; + +pub use config::Config as RPCConfig; + +/// A wrapper around all the items required to spawn the HTTP server. +/// +/// The server will gracefully handle the case where any fields are `None`. +#[derive(Clone)] +pub struct Context { + pub config: RPCConfig, + pub network_globals: Arc, + pub network_send: UnboundedSender, + pub sync_send: SyncSender, + pub chunk_pool: Arc, + pub log_store: Store, + pub shutdown_sender: Sender, + pub mine_service_sender: Option>, +} + +impl Context { + pub fn send_network(&self, msg: NetworkMessage) -> RpcResult<()> { + self.network_send + .send(msg) + .map_err(|e| error::internal_error(format!("Failed to send network message: {:?}", e))) + } + + pub async fn request_sync(&self, request: SyncRequest) -> RpcResult { + self.sync_send + .request(request) + .await + .map_err(|e| error::internal_error(format!("Failed to send sync request: {:?}", e))) + } +} + +pub async fn run_server(ctx: Context) -> Result> { + let server = HttpServerBuilder::default() + .build(ctx.config.listen_address) + .await?; + + let mut zgs = (zgs::RpcServerImpl { ctx: ctx.clone() }).into_rpc(); + let admin = (admin::RpcServerImpl { ctx: ctx.clone() }).into_rpc(); + zgs.merge(admin)?; + + if ctx.mine_service_sender.is_some() { + let mine = (miner::RpcServerImpl { ctx }).into_rpc(); + zgs.merge(mine)?; + } + + let addr = server.local_addr()?; + let handle = server.start(zgs)?; + info!("Server started http://{}", addr); + + Ok(handle) +} diff --git a/node/rpc/src/miner/api.rs b/node/rpc/src/miner/api.rs new file mode 100644 index 0000000..8d4417f --- /dev/null +++ b/node/rpc/src/miner/api.rs @@ -0,0 +1,14 @@ +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; + +#[rpc(server, client, namespace = "miner")] +pub trait Rpc { + #[method(name = "start")] + async fn start(&self) -> RpcResult; + + #[method(name = "stop")] + async fn stop(&self) -> RpcResult; + + #[method(name = "setStartPosition")] + async fn set_start_position(&self, index: u64) -> RpcResult; +} diff --git a/node/rpc/src/miner/impl.rs b/node/rpc/src/miner/impl.rs new file mode 100644 index 0000000..5eed003 --- /dev/null +++ b/node/rpc/src/miner/impl.rs @@ -0,0 +1,49 @@ +use super::api::RpcServer; +use crate::Context; +use futures::prelude::*; +use jsonrpsee::core::async_trait; +use jsonrpsee::core::{Error, RpcResult}; +use miner::MinerMessage; +use tokio::sync::broadcast; + +pub struct RpcServerImpl { + pub ctx: Context, +} + +impl RpcServerImpl { + fn mine_service_sender(&self) -> &broadcast::Sender { + self.ctx.mine_service_sender.as_ref().unwrap() + } +} + +#[async_trait] +impl RpcServer for RpcServerImpl { + async fn start(&self) -> RpcResult { + info!("mine_start()"); + let success = self + .mine_service_sender() + .send(MinerMessage::ToggleMining(true)) + .is_ok(); + Ok(success) + } + + async fn stop(&self) -> RpcResult { + info!("mine_stop()"); + + let success = self + .mine_service_sender() + .send(MinerMessage::ToggleMining(false)) + .is_ok(); + Ok(success) + } + + async fn set_start_position(&self, index: u64) -> RpcResult { + info!("mine_setStartPosition({})", index); + + let success = self + .mine_service_sender() + .send(MinerMessage::SetStartPosition(Some(index))) + .is_ok(); + Ok(success) + } +} diff --git a/node/rpc/src/miner/mod.rs b/node/rpc/src/miner/mod.rs new file mode 100644 index 0000000..2640fc7 --- /dev/null +++ b/node/rpc/src/miner/mod.rs @@ -0,0 +1,6 @@ +#![allow(unused)] +mod api; +mod r#impl; + +pub use api::RpcServer; +pub use r#impl::RpcServerImpl; diff --git a/node/rpc/src/types.rs b/node/rpc/src/types.rs new file mode 100644 index 0000000..91ca92d --- /dev/null +++ b/node/rpc/src/types.rs @@ -0,0 +1,226 @@ +use crate::error; +use jsonrpsee::core::RpcResult; +use merkle_light::hash::Algorithm; +use merkle_light::merkle::MerkleTree; +use merkle_tree::RawLeafSha3Algorithm; +use serde::{Deserialize, Serialize}; +use shared_types::{ + compute_padded_chunk_size, compute_segment_size, DataRoot, FileProof, Transaction, CHUNK_SIZE, +}; +use std::hash::Hasher; +use storage::H256; + +const ZERO_HASH: [u8; 32] = [ + 0xd3, 0x97, 0xb3, 0xb0, 0x43, 0xd8, 0x7f, 0xcd, 0x6f, 0xad, 0x12, 0x91, 0xff, 0xb, 0xfd, 0x16, + 0x40, 0x1c, 0x27, 0x48, 0x96, 0xd8, 0xc6, 0x3a, 0x92, 0x37, 0x27, 0xf0, 0x77, 0xb8, 0xe0, 0xb5, +]; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Status { + pub connected_peers: usize, + pub log_sync_height: u64, + pub log_sync_block: H256, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkInfo { + pub peer_id: String, + pub total_peers: usize, + pub banned_peers: usize, + pub disconnected_peers: usize, + pub connected_peers: usize, + pub connected_outgoing_peers: usize, + pub connected_incoming_peers: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileInfo { + pub tx: Transaction, + pub finalized: bool, + pub is_cached: bool, + pub uploaded_seg_num: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Segment(#[serde(with = "base64")] pub Vec); + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SegmentWithProof { + /// File merkle root. + pub root: DataRoot, + #[serde(with = "base64")] + /// With fixed data size except the last segment. + pub data: Vec, + /// Segment index. + pub index: usize, + /// File merkle proof whose leaf node is segment root. + pub proof: FileProof, + /// File size + pub file_size: usize, +} + +impl SegmentWithProof { + /// Splits file into segments and returns the total number of segments and the last segment size. + pub fn split_file_into_segments( + file_size: usize, + chunks_per_segment: usize, + ) -> RpcResult<(usize, usize)> { + if file_size == 0 { + return Err(error::invalid_params("file_size", "file is empty")); + } + + let segment_size = chunks_per_segment * CHUNK_SIZE; + let remaining_size = file_size % segment_size; + let mut num_segments = file_size / segment_size; + + if remaining_size == 0 { + return Ok((num_segments, segment_size)); + } + + // Otherwise, the last segment is not full. + num_segments += 1; + + let last_chunk_size = remaining_size % CHUNK_SIZE; + if last_chunk_size == 0 { + Ok((num_segments, remaining_size)) + } else { + // Padding last chunk with zeros. + let last_segment_size = remaining_size - last_chunk_size + CHUNK_SIZE; + Ok((num_segments, last_segment_size)) + } + } + + fn validate_data_size_and_index( + &self, + file_size: usize, + chunks_per_segment: usize, + ) -> RpcResult { + let (num_segments, last_segment_size) = + SegmentWithProof::split_file_into_segments(file_size, chunks_per_segment)?; + + if self.index >= num_segments { + return Err(error::invalid_params("index", "index out of bound")); + } + + let data_size = if self.index == num_segments - 1 { + last_segment_size + } else { + chunks_per_segment * CHUNK_SIZE + }; + + if self.data.len() != data_size { + return Err(error::invalid_params("data", "invalid data length")); + } + + Ok(num_segments) + } + + fn calculate_segment_merkle_root(&self, extend_chunk_length: usize) -> [u8; 32] { + let mut a = RawLeafSha3Algorithm::default(); + let hashes = self.data.chunks_exact(CHUNK_SIZE).map(|x| { + a.reset(); + a.write(x); + a.hash() + }); + let mut hash_data = hashes.collect::>(); + hash_data.append(&mut vec![ZERO_HASH; extend_chunk_length]); + + MerkleTree::<_, RawLeafSha3Algorithm>::new(hash_data).root() + } + + fn validate_proof(&self, num_segments: usize, expected_data_length: usize) -> RpcResult<()> { + // Validate proof data format at first. + if self.proof.path.is_empty() { + if self.proof.lemma.len() != 1 { + return Err(error::invalid_params("proof", "invalid proof")); + } + } else if self.proof.lemma.len() != self.proof.path.len() + 2 { + return Err(error::invalid_params("proof", "invalid proof")); + } + + // Calculate segment merkle root to verify proof. + let extend_chunk_length = if expected_data_length > self.data.len() { + let extend_data_length = expected_data_length - self.data.len(); + if extend_data_length % CHUNK_SIZE != 0 { + return Err(error::invalid_params("proof", "invalid data len")); + } + + extend_data_length / CHUNK_SIZE + } else { + 0 + }; + + let segment_root = self.calculate_segment_merkle_root(extend_chunk_length); + if !self + .proof + .validate(&segment_root, &self.root, self.index, num_segments)? + { + return Err(error::invalid_params("proof", "validation failed")); + } + + Ok(()) + } + + /// Validates the segment data size and proof. + pub fn validate(&self, chunks_per_segment: usize) -> RpcResult<()> { + self.validate_data_size_and_index(self.file_size, chunks_per_segment)?; + + let (chunks, _) = compute_padded_chunk_size(self.file_size); + let (segments_for_proof, last_segment_size) = + compute_segment_size(chunks, chunks_per_segment); + + let expected_data_length = if self.index == segments_for_proof - 1 { + last_segment_size * CHUNK_SIZE + } else { + chunks_per_segment * CHUNK_SIZE + }; + + debug!( + "data len: {}, expected len: {}", + self.data.len(), + expected_data_length + ); + + self.validate_proof(segments_for_proof, expected_data_length)?; + Ok(()) + } + + /// Returns the index of first chunk in the segment. + #[allow(dead_code)] + pub fn chunk_index(&self, chunks_per_segment: usize) -> usize { + self.index * chunks_per_segment + } +} + +mod base64 { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(v: &Vec, s: S) -> Result { + let base64 = base64::encode(v); + String::serialize(&base64, s) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let base64 = String::deserialize(d)?; + base64::decode(base64.as_bytes()).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::Segment; + + #[test] + fn test_segment_serde() { + let seg = Segment("hello, world".as_bytes().to_vec()); + let result = serde_json::to_string(&seg).unwrap(); + assert_eq!(result.as_str(), "\"aGVsbG8sIHdvcmxk\""); + + let seg2: Segment = serde_json::from_str("\"aGVsbG8sIHdvcmxk\"").unwrap(); + assert_eq!(String::from_utf8(seg2.0).unwrap().as_str(), "hello, world"); + } +} diff --git a/node/rpc/src/zgs/api.rs b/node/rpc/src/zgs/api.rs new file mode 100644 index 0000000..f1f6c75 --- /dev/null +++ b/node/rpc/src/zgs/api.rs @@ -0,0 +1,34 @@ +use crate::types::{FileInfo, Segment, SegmentWithProof, Status}; +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use shared_types::DataRoot; + +#[rpc(server, client, namespace = "zgs")] +pub trait Rpc { + #[method(name = "getStatus")] + async fn get_status(&self) -> RpcResult; + + #[method(name = "uploadSegment")] + async fn upload_segment(&self, segment: SegmentWithProof) -> RpcResult<()>; + + #[method(name = "downloadSegment")] + async fn download_segment( + &self, + data_root: DataRoot, + start_index: usize, + end_index: usize, + ) -> RpcResult>; + + #[method(name = "downloadSegmentWithProof")] + async fn download_segment_with_proof( + &self, + data_root: DataRoot, + index: usize, + ) -> RpcResult>; + + #[method(name = "getFileInfo")] + async fn get_file_info(&self, data_root: DataRoot) -> RpcResult>; + + #[method(name = "getFileInfoByTxSeq")] + async fn get_file_info_by_tx_seq(&self, tx_seq: u64) -> RpcResult>; +} diff --git a/node/rpc/src/zgs/impl.rs b/node/rpc/src/zgs/impl.rs new file mode 100644 index 0000000..1408855 --- /dev/null +++ b/node/rpc/src/zgs/impl.rs @@ -0,0 +1,238 @@ +use super::api::RpcServer; +use crate::error; +use crate::types::{FileInfo, Segment, SegmentWithProof, Status}; +use crate::Context; +use chunk_pool::{FileID, SegmentInfo}; +use jsonrpsee::core::async_trait; +use jsonrpsee::core::RpcResult; +use shared_types::{DataRoot, Transaction, CHUNK_SIZE}; +use storage::try_option; + +pub struct RpcServerImpl { + pub ctx: Context, +} + +#[async_trait] +impl RpcServer for RpcServerImpl { + #[tracing::instrument(skip(self), err)] + async fn get_status(&self) -> RpcResult { + info!("zgs_getStatus()"); + let sync_progress = self + .ctx + .log_store + .get_store() + .read() + .await + .get_sync_progress()? + .unwrap_or_default(); + + Ok(Status { + connected_peers: self.ctx.network_globals.connected_peers(), + log_sync_height: sync_progress.0, + log_sync_block: sync_progress.1, + }) + } + + async fn upload_segment(&self, segment: SegmentWithProof) -> RpcResult<()> { + debug!(root = %segment.root, index = %segment.index, "zgs_uploadSegment"); + + let _ = self.ctx.chunk_pool.validate_segment_size(&segment.data)?; + + let maybe_tx = self + .ctx + .log_store + .get_tx_by_data_root(&segment.root) + .await?; + let mut need_cache = false; + + if self + .ctx + .chunk_pool + .check_already_has_cache(&segment.root) + .await + { + need_cache = true; + } + + if !need_cache { + need_cache = self.check_need_cache(&maybe_tx, segment.file_size).await?; + } + + segment.validate(self.ctx.config.chunks_per_segment)?; + + let seg_info = SegmentInfo { + root: segment.root, + seg_data: segment.data, + seg_index: segment.index, + chunks_per_segment: self.ctx.config.chunks_per_segment, + }; + + if need_cache { + self.ctx.chunk_pool.cache_chunks(seg_info).await?; + } else { + let file_id = FileID { + root: seg_info.root, + tx_id: maybe_tx.unwrap().id(), + }; + self.ctx + .chunk_pool + .write_chunks(seg_info, file_id, segment.file_size) + .await?; + } + + Ok(()) + } + + async fn download_segment( + &self, + data_root: DataRoot, + start_index: usize, + end_index: usize, + ) -> RpcResult> { + debug!(%data_root, %start_index, %end_index, "zgs_downloadSegment"); + + if start_index >= end_index { + return Err(error::invalid_params("end_index", "invalid chunk index")); + } + + if end_index - start_index > self.ctx.config.chunks_per_segment { + return Err(error::invalid_params( + "end_index", + format!( + "exceeds maximum chunks {}", + self.ctx.config.chunks_per_segment + ), + )); + } + + let tx_seq = try_option!( + self.ctx + .log_store + .get_tx_seq_by_data_root(&data_root) + .await? + ); + let segment = try_option!( + self.ctx + .log_store + .get_chunks_by_tx_and_index_range(tx_seq, start_index, end_index) + .await? + ); + + Ok(Some(Segment(segment.data))) + } + + async fn download_segment_with_proof( + &self, + data_root: DataRoot, + index: usize, + ) -> RpcResult> { + debug!(%data_root, %index, "zgs_downloadSegmentWithProof"); + + let tx = try_option!(self.ctx.log_store.get_tx_by_data_root(&data_root).await?); + + // validate index + let chunks_per_segment = self.ctx.config.chunks_per_segment; + let (num_segments, last_segment_size) = + SegmentWithProof::split_file_into_segments(tx.size as usize, chunks_per_segment)?; + + if index >= num_segments { + return Err(error::invalid_params("index", "index out of bound")); + } + + // calculate chunk start and end index + let start_index = index * chunks_per_segment; + let end_index = if index == num_segments - 1 { + // last segment without padding chunks by flow + start_index + last_segment_size / CHUNK_SIZE + } else { + start_index + chunks_per_segment + }; + + let segment = try_option!( + self.ctx + .log_store + .get_chunks_with_proof_by_tx_and_index_range(tx.seq, start_index, end_index) + .await? + ); + + let proof = tx.compute_segment_proof(&segment, chunks_per_segment)?; + + Ok(Some(SegmentWithProof { + root: data_root, + data: segment.chunks.data, + index, + proof, + file_size: tx.size as usize, + })) + } + + async fn get_file_info(&self, data_root: DataRoot) -> RpcResult> { + debug!(%data_root, "zgs_getFileInfo"); + + let tx = try_option!(self.ctx.log_store.get_tx_by_data_root(&data_root).await?); + + Ok(Some(self.get_file_info_by_tx(tx).await?)) + } + + async fn get_file_info_by_tx_seq(&self, tx_seq: u64) -> RpcResult> { + debug!(%tx_seq, "zgs_getFileInfoByTxSeq"); + + let tx = try_option!(self.ctx.log_store.get_tx_by_seq_number(tx_seq).await?); + + Ok(Some(self.get_file_info_by_tx(tx).await?)) + } +} + +impl RpcServerImpl { + async fn check_need_cache( + &self, + maybe_tx: &Option, + file_size: usize, + ) -> RpcResult { + if let Some(tx) = maybe_tx { + if tx.size != file_size as u64 { + return Err(error::invalid_params( + "file_size", + "segment file size not matched with tx file size", + )); + } + + // Transaction already finalized for the specified file data root. + if self.ctx.log_store.check_tx_completed(tx.seq).await? { + return Err(error::invalid_params( + "root", + "already uploaded and finalized", + )); + } + + Ok(false) + } else { + //Check whether file is small enough to cache in the system + if file_size > self.ctx.config.max_cache_file_size { + return Err(error::invalid_params( + "file_size", + "caching of large file when tx is unavailable is not supported", + )); + } + + Ok(true) + } + } + + async fn get_file_info_by_tx(&self, tx: Transaction) -> RpcResult { + let (uploaded_seg_num, is_cached) = self + .ctx + .chunk_pool + .get_uploaded_seg_num(&tx.data_merkle_root) + .await; + + let finalized = self.ctx.log_store.check_tx_completed(tx.seq).await?; + + Ok(FileInfo { + tx, + finalized, + is_cached, + uploaded_seg_num, + }) + } +} diff --git a/node/rpc/src/zgs/mod.rs b/node/rpc/src/zgs/mod.rs new file mode 100644 index 0000000..2882f83 --- /dev/null +++ b/node/rpc/src/zgs/mod.rs @@ -0,0 +1,5 @@ +mod api; +mod r#impl; + +pub use api::RpcServer; +pub use r#impl::RpcServerImpl; diff --git a/node/shared_types/Cargo.toml b/node/shared_types/Cargo.toml new file mode 100644 index 0000000..bc30a7c --- /dev/null +++ b/node/shared_types/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "shared_types" +version = "0.1.0" +edition = "2021" + +[dependencies] +append_merkle = { path = "../../common/append_merkle"} +merkle_tree = { path = "../../common/merkle_tree"} +zgs_spec = { path = "../../common/spec"} +anyhow = { version = "=1.0.58", features = ["backtrace"] } +ethereum-types = "0.14" +eth2_ssz = "0.4.0" +eth2_ssz_derive = "0.3.0" +eth2_ssz_types = "0.2.1" +merkle_light = { path = "../../common/merkle_light" } +tiny-keccak = "2.0.2" +tracing = "0.1.35" +typenum = "1.15.0" +serde = { version = "1.0.137", features = ["derive"] } +chrono = "0.4.19" diff --git a/node/shared_types/src/lib.rs b/node/shared_types/src/lib.rs new file mode 100644 index 0000000..7a7b1ab --- /dev/null +++ b/node/shared_types/src/lib.rs @@ -0,0 +1,348 @@ +mod proof; + +use anyhow::bail; +use append_merkle::{ + AppendMerkleTree, Proof as RawProof, RangeProof as RawRangeProof, Sha3Algorithm, +}; +use ethereum_types::{H256, U256}; +use merkle_light::merkle::MerkleTree; +use merkle_light::proof::Proof as RawFileProof; +use merkle_light::{hash::Algorithm, merkle::next_pow2}; +use merkle_tree::RawLeafSha3Algorithm; +use serde::{Deserialize, Serialize}; +use ssz::Encode; +use ssz_derive::{Decode as DeriveDecode, Encode as DeriveEncode}; +use std::fmt; +use std::hash::Hasher; +use tiny_keccak::{Hasher as KeccakHasher, Keccak}; +use tracing::debug; + +const ZERO_HASH: [u8; 32] = [ + 0xd3, 0x97, 0xb3, 0xb0, 0x43, 0xd8, 0x7f, 0xcd, 0x6f, 0xad, 0x12, 0x91, 0xff, 0xb, 0xfd, 0x16, + 0x40, 0x1c, 0x27, 0x48, 0x96, 0xd8, 0xc6, 0x3a, 0x92, 0x37, 0x27, 0xf0, 0x77, 0xb8, 0xe0, 0xb5, +]; + +/// Application level requests sent to the network. +#[derive(Debug, Clone, Copy)] +pub enum RequestId { + Router, +} + +pub type DataRoot = H256; + +pub type FlowProof = RawProof; +pub type FlowRangeProof = RawRangeProof; +pub type Merkle = AppendMerkleTree; + +// Each chunk is 32 bytes. +pub const CHUNK_SIZE: usize = 256; + +pub fn bytes_to_chunks(size_bytes: usize) -> usize { + if size_bytes % CHUNK_SIZE == 0 { + size_bytes / CHUNK_SIZE + } else { + size_bytes / CHUNK_SIZE + 1 + } +} + +pub fn compute_padded_chunk_size(size_bytes: usize) -> (usize, usize) { + let chunk_len = bytes_to_chunks(size_bytes); + let chunks_next_pow2 = next_pow2(chunk_len); + + if chunks_next_pow2 == chunk_len { + return (chunks_next_pow2, chunks_next_pow2); + } + + let min_chunk = if chunks_next_pow2 < 16 { + 1 + } else { + chunks_next_pow2 >> 4 + }; + + // chunk_len will be always greater than 0, size_byte comes from tx.size which is file size, the flow contract doesn't allowy upload 0-size file + let padded_chunks = ((chunk_len - 1) / min_chunk + 1) * min_chunk; + + (padded_chunks, chunks_next_pow2) +} + +pub fn compute_segment_size(chunks: usize, chunks_per_segment: usize) -> (usize, usize) { + if chunks % chunks_per_segment == 0 { + (chunks / chunks_per_segment, chunks_per_segment) + } else { + (chunks / chunks_per_segment + 1, chunks % chunks_per_segment) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Chunk(pub [u8; CHUNK_SIZE]); + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, DeriveDecode, DeriveEncode)] +pub struct TxID { + pub seq: u64, + pub hash: H256, +} + +impl TxID { + pub fn random_hash(seq: u64) -> Self { + Self { + seq, + hash: H256::random(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, DeriveDecode, DeriveEncode, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Transaction { + pub stream_ids: Vec, + /// In-place data. + pub data: Vec, + pub data_merkle_root: DataRoot, + /// `(subtree_depth, subtree_root)` + pub merkle_nodes: Vec<(usize, DataRoot)>, + + pub start_entry_index: u64, + pub size: u64, + pub seq: u64, +} + +impl Transaction { + pub fn num_entries_of_node(depth: usize) -> usize { + 1 << (depth - 1) + } + + pub fn num_entries(&self) -> usize { + self.merkle_nodes.iter().fold(0, |size, &(depth, _)| { + size + Transaction::num_entries_of_node(depth) + }) + } + + pub fn hash(&self) -> H256 { + let bytes = self.as_ssz_bytes(); + let mut h = Keccak::v256(); + let mut e = H256::zero(); + h.update(&bytes); + h.finalize(e.as_mut()); + e + } + + pub fn id(&self) -> TxID { + TxID { + seq: self.seq, + hash: self.hash(), + } + } +} + +pub struct ChunkWithProof { + pub chunk: Chunk, + pub proof: FlowProof, +} + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEncode, DeriveDecode)] +pub struct ChunkArrayWithProof { + pub chunks: ChunkArray, + // TODO: The top levels of the two proofs can be merged. + pub proof: FlowRangeProof, +} + +#[derive(Clone, Eq, PartialEq, DeriveEncode, DeriveDecode)] +pub struct ChunkArray { + // The length is exactly a multiple of `CHUNK_SIZE` + pub data: Vec, + pub start_index: u64, +} + +impl fmt::Debug for ChunkArray { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ChunkArray: start_index={} data_len={}", + self.start_index, + self.data.len() + ) + } +} + +impl ChunkArray { + pub fn first_chunk(&self) -> Option { + self.chunk_at(self.start_index as usize) + } + + pub fn last_chunk(&self) -> Option { + let last_index = + (self.start_index as usize + self.data.len() / CHUNK_SIZE).checked_sub(1)?; + self.chunk_at(last_index) + } + + pub fn chunk_at(&self, index: usize) -> Option { + if index >= self.data.len() / CHUNK_SIZE + self.start_index as usize + || index < self.start_index as usize + { + return None; + } + let offset = (index - self.start_index as usize) * CHUNK_SIZE; + Some(Chunk( + self.data[offset..offset + CHUNK_SIZE] + .try_into() + .expect("length match"), + )) + } + + pub fn sub_array(&self, start: u64, end: u64) -> Option { + if start >= (self.data.len() / CHUNK_SIZE) as u64 + self.start_index + || start < self.start_index + || end > (self.data.len() / CHUNK_SIZE) as u64 + self.start_index + || end <= self.start_index + || end <= start + { + return None; + } + let start_offset = (start - self.start_index) as usize * CHUNK_SIZE; + let end_offset = (end - self.start_index) as usize * CHUNK_SIZE; + Some(ChunkArray { + data: self.data[start_offset..end_offset].to_vec(), + start_index: start, + }) + } +} + +impl std::fmt::Display for ChunkArray { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ChunkArray {{ chunks = {}, start_index = {} }}", + self.data.len() / CHUNK_SIZE, + self.start_index + ) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEncode, DeriveDecode, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileProof { + pub lemma: Vec, + pub path: Vec, +} +impl FileProof { + pub fn new(mut lemma: Vec, path: Vec) -> Self { + if path.is_empty() { + lemma.truncate(1); + } + + FileProof { lemma, path } + } + + pub fn validate( + &self, + leaf_hash: &[u8; 32], + root: &DataRoot, + position: usize, + leaf_count: usize, + ) -> anyhow::Result { + let proof_position = self.position(leaf_count)?; + if proof_position != position { + bail!( + "wrong position: proof_pos={} provided={}", + proof_position, + position + ); + } + + let proof: RawFileProof<[u8; 32]> = self.try_into()?; + + if !proof.validate::() { + debug!("Proof validate fails"); + return Ok(false); + } + + if proof.root() != root.0 { + bail!( + "root mismatch, proof_root={:?} provided={:?}", + proof.root(), + root.0 + ); + } + + if proof.item() != *leaf_hash { + bail!( + "data hash mismatch: leaf_hash={:?} proof_item={:?}", + leaf_hash, + proof.item(), + ); + } + + Ok(true) + } + + fn position(&self, total_chunk_count: usize) -> anyhow::Result { + let mut left_chunk_count = total_chunk_count; + let mut proof_position = 0; + // TODO: After the first `is_left == true`, the tree depth is fixed. + for is_left in self.path.iter().rev() { + if left_chunk_count <= 1 { + bail!( + "Proof path too long for a tree size: path={:?}, size={}", + self.path, + total_chunk_count + ); + } + let subtree_size = next_pow2(left_chunk_count) >> 1; + if !is_left { + proof_position += subtree_size; + left_chunk_count -= subtree_size; + } else { + left_chunk_count = subtree_size; + } + } + if left_chunk_count != 1 { + bail!( + "Proof path too short for a tree size: path={:?}, size={}", + self.path, + total_chunk_count + ); + } + Ok(proof_position) + } +} + +impl TryFrom<&FileProof> for RawFileProof<[u8; 32]> { + type Error = anyhow::Error; + + fn try_from(value: &FileProof) -> std::result::Result { + if (value.lemma.len() == 1 && value.path.is_empty()) + || (value.lemma.len() > 2 && value.lemma.len() == value.path.len() + 2) + { + Ok(RawFileProof::<[u8; 32]>::new( + value.lemma.iter().map(|e| e.0).collect(), + value.path.clone(), + )) + } else { + bail!("Invalid proof: proof={:?}", value) + } + } +} + +pub fn timestamp_now() -> u32 { + let timestamp = chrono::Utc::now().timestamp(); + u32::try_from(timestamp).expect("The year is between 1970 and 2106") +} + +pub fn compute_segment_merkle_root(data: &[u8], segment_chunks: usize) -> [u8; 32] { + let mut a = RawLeafSha3Algorithm::default(); + let mut hashes: Vec<[u8; 32]> = data + .chunks_exact(CHUNK_SIZE) + .map(|x| { + a.reset(); + a.write(x); + a.hash() + }) + .collect(); + + let num_chunks = data.len() / CHUNK_SIZE; + if num_chunks < segment_chunks { + hashes.append(&mut vec![ZERO_HASH; segment_chunks - num_chunks]); + } + + MerkleTree::<_, RawLeafSha3Algorithm>::new(hashes).root() +} diff --git a/node/shared_types/src/proof.rs b/node/shared_types/src/proof.rs new file mode 100644 index 0000000..e2c8ea3 --- /dev/null +++ b/node/shared_types/src/proof.rs @@ -0,0 +1,220 @@ +use crate::{compute_segment_merkle_root, ChunkArrayWithProof, FileProof, Transaction, CHUNK_SIZE}; +use anyhow::{bail, Result}; +use append_merkle::{Algorithm, Sha3Algorithm}; +use ethereum_types::H256; +use merkle_light::merkle::log2_pow2; + +enum NodeProofLeaf { + /// Segment in a single sub-tree. + Full { node_depth: usize }, + + /// Segment is not full and is made up of one or multiple sub-trees + /// that smaller than a full segment. + Partial, +} + +/// NodeProof represents a merkle proof from submission node to file root. +struct NodeProof { + proof: FileProof, + leaf: NodeProofLeaf, +} + +impl NodeProof { + fn compute_segment_proof( + self, + segment: &ChunkArrayWithProof, + chunks_per_segment: usize, + ) -> Result { + let mut node_depth = match self.leaf { + // In this case, some proof pathes missed between segment + // root and submission node, which could be retrieved from + // the flow proof. + NodeProofLeaf::Full { node_depth } => node_depth, + + // Client could compute the segment root via returned + // segment data, and use the NodeProof to validate. + NodeProofLeaf::Partial => return Ok(self.proof), + }; + + // node depth in Transaction is by number of nodes, + // here we use depth by number of edges. + node_depth -= 1; + + self.compute_segment_proof_full(node_depth, segment, chunks_per_segment) + } + + fn compute_segment_proof_full( + self, + node_depth: usize, // node depth by edge + segment: &ChunkArrayWithProof, + chunks_per_segment: usize, + ) -> Result { + // when segment equals to sub-tree, just return the proof directly + let segment_depth = log2_pow2(chunks_per_segment); + if node_depth == segment_depth { + return Ok(self.proof); + } + + // otherwise, segment should be smaller than sub-tree + assert!(node_depth > segment_depth); + + // flow tree should not be smaller than any sub-tree + let flow_proof_path_len = segment.proof.left_proof.path().len(); + if flow_proof_path_len < node_depth { + bail!( + "flow proof path too small, path_len = {}, node_depth = {}", + flow_proof_path_len, + node_depth + ); + } + + // segment root as proof leaf + let segment_root: H256 = + compute_segment_merkle_root(&segment.chunks.data, chunks_per_segment).into(); + let mut lemma = vec![segment_root]; + let mut path = vec![]; + + // copy from flow proof + for i in segment_depth..node_depth { + lemma.push(segment.proof.left_proof.lemma()[i + 1]); + path.push(segment.proof.left_proof.path()[i]); + } + + // combine with node proof + if self.proof.path.is_empty() { + // append file root only + lemma.push(self.proof.lemma[0]); + } else { + // append lemma/path and file root, and ignore the sub-tree root + // which could be constructed via proof. + lemma.extend_from_slice(&self.proof.lemma[1..]); + path.extend_from_slice(&self.proof.path); + } + + Ok(FileProof::new(lemma, path)) + } +} + +impl Transaction { + /// Computes file merkle proof for the specified segment. + /// + /// The leaf of proof is segment root, and root of proof is file merkle root. + pub fn compute_segment_proof( + &self, + segment: &ChunkArrayWithProof, + chunks_per_segment: usize, + ) -> Result { + // validate index + let chunk_start_index = segment.chunks.start_index as usize; + if chunk_start_index % chunks_per_segment != 0 { + bail!("start index not aligned"); + } + + let total_entries = self.num_entries(); + if chunk_start_index >= total_entries { + bail!("start index out of bound"); + } + + let data_len = segment.chunks.data.len(); + if chunk_start_index + data_len / CHUNK_SIZE > total_entries { + bail!("end index out of bound"); + } + + // compute merkle proof from node root to file root + let node_proof = self.compute_node_proof(chunk_start_index, chunks_per_segment); + + node_proof.compute_segment_proof(segment, chunks_per_segment) + } + + fn compute_node_proof(&self, chunk_start_index: usize, chunks_per_segment: usize) -> NodeProof { + // construct `lemma` in the order: root -> interier nodes -> leaf, + // and reverse the `lemma` and `path` to create proof. + let mut lemma = vec![self.data_merkle_root]; + let mut path = vec![]; + + // Basically, a segment should be in a sub-tree except the last segment. + // As for the last segment, it should be also in a sub-tree if the smallest + // sub-tree is bigger than a segment. Otherwise, segment is made up of + // all sub-trees that smaller than a segment. + + // try to find a single node for segment + let mut node = None; + let mut total_chunks = 0; + for (depth, root) in self.merkle_nodes.iter().cloned() { + let node_chunks = Transaction::num_entries_of_node(depth); + + // ignore nodes that smaller than a segment + if node_chunks < chunks_per_segment { + break; + } + + total_chunks += node_chunks; + + // segment in a single node + if chunk_start_index < total_chunks { + node = Some((depth, root)); + break; + } + + // otherwise, segment not in current node + lemma.push(root); + path.push(false); + } + + let leaf = match node { + // segment in a single node + Some((depth, root)) => { + // append lemma and path if right sibling exists + if let Some(right_root) = self.compute_merkle_root(depth) { + lemma.push(right_root); + path.push(true); + } + + // append leaf + lemma.push(root); + + NodeProofLeaf::Full { node_depth: depth } + } + + // segment is made up of multiple nodes + None => { + let segment_depth = log2_pow2(chunks_per_segment) + 1; + let right_root = self + .compute_merkle_root(segment_depth) + .expect("merkle root should be exists"); + + // append leaf + lemma.push(right_root); + + NodeProofLeaf::Partial + } + }; + + // change to bottom-top order: leaf -> (interiers) -> root + lemma.reverse(); + path.reverse(); + + NodeProof { + proof: FileProof::new(lemma, path), + leaf, + } + } + + /// Computes the merkle root of nodes that of depth less than `max_depth_exclusive`. + fn compute_merkle_root(&self, max_depth_exclusive: usize) -> Option { + let (depth, mut root) = self.merkle_nodes.last().cloned()?; + if depth >= max_depth_exclusive { + return None; + } + + for (node_depth, node_root) in self.merkle_nodes.iter().rev().skip(1) { + if *node_depth >= max_depth_exclusive { + break; + } + + root = Sha3Algorithm::parent(node_root, &root); + } + + Some(root) + } +} diff --git a/node/src/cli/mod.rs b/node/src/cli/mod.rs new file mode 100644 index 0000000..804ec59 --- /dev/null +++ b/node/src/cli/mod.rs @@ -0,0 +1,7 @@ +use clap::{arg, command, Command}; + +pub fn cli_app<'a>() -> Command<'a> { + command!() + .arg(arg!(-c --config "Sets a custom config file")) + .allow_external_subcommands(true) +} diff --git a/node/src/client/builder.rs b/node/src/client/builder.rs new file mode 100644 index 0000000..15aea1f --- /dev/null +++ b/node/src/client/builder.rs @@ -0,0 +1,273 @@ +use super::{Client, RuntimeContext}; +use chunk_pool::{Config as ChunkPoolConfig, MemoryChunkPool}; +use file_location_cache::FileLocationCache; +use log_entry_sync::{LogSyncConfig, LogSyncEvent, LogSyncManager}; +use miner::{MineService, MinerConfig, MinerMessage}; +use network::{ + self, Keypair, NetworkConfig, NetworkGlobals, NetworkMessage, RequestId, + Service as LibP2PService, +}; +use router::RouterService; +use rpc::RPCConfig; +use std::sync::Arc; +use storage::log_store::log_manager::LogConfig; +use storage::log_store::Store; +use storage::{LogManager, StorageConfig}; +use sync::{SyncSender, SyncService}; +use tokio::sync::{broadcast, mpsc, RwLock}; + +macro_rules! require { + ($component:expr, $self:ident, $e:ident) => { + $self + .$e + .as_ref() + .ok_or(format!("{} requires {}", $component, std::stringify!($e)))? + }; +} + +struct NetworkComponents { + send: mpsc::UnboundedSender, + globals: Arc, + keypair: Keypair, + + // note: these will be owned by the router service + owned: Option<( + LibP2PService, + mpsc::UnboundedReceiver, + )>, +} + +struct SyncComponents { + send: SyncSender, +} + +struct MinerComponents { + send: broadcast::Sender, +} + +struct LogSyncComponents { + send: broadcast::Sender, +} + +/// Builds a `Client` instance. +/// +/// ## Notes +/// +/// The builder may start some services (e.g.., libp2p, http server) immediately after they are +/// initialized, _before_ the `self.build(..)` method has been called. +#[derive(Default)] +pub struct ClientBuilder { + runtime_context: Option, + store: Option>>, + async_store: Option, + file_location_cache: Option>, + network: Option, + sync: Option, + miner: Option, + log_sync: Option, +} + +impl ClientBuilder { + /// Specifies the runtime context (tokio executor, logger, etc) for client services. + pub fn with_runtime_context(mut self, context: RuntimeContext) -> Self { + self.runtime_context = Some(context); + self + } + + /// Initializes in-memory storage. + pub fn with_memory_store(mut self) -> Result { + // TODO(zz): Set config. + let store = Arc::new(RwLock::new( + LogManager::memorydb(LogConfig::default()) + .map_err(|e| format!("Unable to start in-memory store: {:?}", e))?, + )); + + self.store = Some(store.clone()); + + if let Some(ctx) = self.runtime_context.as_ref() { + self.async_store = Some(storage_async::Store::new(store, ctx.executor.clone())); + } + + Ok(self) + } + + /// Initializes RocksDB storage. + pub fn with_rocksdb_store(mut self, config: &StorageConfig) -> Result { + let store = Arc::new(RwLock::new( + LogManager::rocksdb(LogConfig::default(), &config.db_dir) + .map_err(|e| format!("Unable to start RocksDB store: {:?}", e))?, + )); + + self.store = Some(store.clone()); + + if let Some(ctx) = self.runtime_context.as_ref() { + self.async_store = Some(storage_async::Store::new(store, ctx.executor.clone())); + } + + Ok(self) + } + + pub fn with_file_location_cache(mut self) -> Self { + let file_location_cache = Default::default(); + self.file_location_cache = Some(Arc::new(file_location_cache)); + self + } + + /// Starts the networking stack. + pub async fn with_network(mut self, config: &NetworkConfig) -> Result { + let executor = require!("network", self, runtime_context).clone().executor; + + // construct the libp2p service context + let service_context = network::Context { config }; + + // construct communication channel + let (send, recv) = mpsc::unbounded_channel::(); + + // launch libp2p service + let (globals, keypair, libp2p) = + LibP2PService::new(executor, send.clone(), service_context) + .await + .map_err(|e| format!("Failed to start network service: {:?}", e))?; + + self.network = Some(NetworkComponents { + send, + globals, + keypair, + owned: Some((libp2p, recv)), + }); + + Ok(self) + } + + pub async fn with_sync(mut self, config: sync::Config) -> Result { + let executor = require!("sync", self, runtime_context).clone().executor; + let store = require!("sync", self, store).clone(); + let file_location_cache = require!("sync", self, file_location_cache).clone(); + let network_send = require!("sync", self, network).send.clone(); + let event_recv = require!("sync", self, log_sync).send.subscribe(); + + let send = SyncService::spawn_with_config( + config, + executor, + network_send, + store, + file_location_cache, + event_recv, + ) + .await + .map_err(|e| format!("Failed to start sync service: {:?}", e))?; + self.sync = Some(SyncComponents { send }); + + Ok(self) + } + + pub async fn with_miner(mut self, config: Option) -> Result { + if let Some(config) = config { + let executor = require!("miner", self, runtime_context).clone().executor; + let network_send = require!("miner", self, network).send.clone(); + let store = self.store.as_ref().unwrap().clone(); + + let send = MineService::spawn(executor, network_send, config, store).await?; + self.miner = Some(MinerComponents { send }); + } + + Ok(self) + } + + /// Starts the networking stack. + pub fn with_router(mut self, router_config: router::Config) -> Result { + let executor = require!("router", self, runtime_context).clone().executor; + let sync_send = require!("router", self, sync).send.clone(); // note: we can make this optional in the future + let miner_send = self.miner.as_ref().map(|x| x.send.clone()); + let store = require!("router", self, store).clone(); + let file_location_cache = require!("router", self, file_location_cache).clone(); + + let network = self.network.as_mut().ok_or("router requires a network")?; + + let (libp2p, network_recv) = network + .owned + .take() // router takes ownership of libp2p and network_recv + .ok_or("router requires a network")?; + + RouterService::spawn( + executor, + libp2p, + network.globals.clone(), + network_recv, + network.send.clone(), + sync_send, + miner_send, + store, + file_location_cache, + network.keypair.clone(), + router_config, + ); + + Ok(self) + } + + pub async fn with_rpc( + self, + rpc_config: RPCConfig, + chunk_pool_config: ChunkPoolConfig, + ) -> Result { + if !rpc_config.enabled { + return Ok(self); + } + + let executor = require!("rpc", self, runtime_context).clone().executor; + let async_store = require!("rpc", self, async_store).clone(); + let network_send = require!("rpc", self, network).send.clone(); + let mine_send = self.miner.as_ref().map(|x| x.send.clone()); + let synced_tx_recv = require!("sync", self, log_sync).send.subscribe(); + + let (chunk_pool, chunk_pool_handler) = + chunk_pool::unbounded(chunk_pool_config, async_store.clone(), network_send.clone()); + + let chunk_pool_clone = chunk_pool.clone(); + let ctx = rpc::Context { + config: rpc_config, + network_globals: require!("rpc", self, network).globals.clone(), + network_send, + sync_send: require!("rpc", self, sync).send.clone(), + log_store: async_store, + chunk_pool, + shutdown_sender: executor.shutdown_sender(), + mine_service_sender: mine_send, + }; + + let rpc_handle = rpc::run_server(ctx) + .await + .map_err(|e| format!("Unable to start HTTP RPC server: {:?}", e))?; + + executor.spawn(rpc_handle, "rpc"); + executor.spawn(chunk_pool_handler.run(), "chunk_pool_handler"); + executor.spawn( + MemoryChunkPool::monitor_log_entry(chunk_pool_clone, synced_tx_recv), + "chunk_pool_log_monitor", + ); + + Ok(self) + } + + pub async fn with_log_sync(mut self, config: LogSyncConfig) -> Result { + let executor = require!("log_sync", self, runtime_context).clone().executor; + let store = require!("log_sync", self, store).clone(); + let send = LogSyncManager::spawn(config, executor, store) + .await + .map_err(|e| e.to_string())?; + + self.log_sync = Some(LogSyncComponents { send }); + Ok(self) + } + + /// Consumes the builder, returning a `Client` if all necessary components have been + /// specified. + pub fn build(self) -> Result { + require!("client", self, runtime_context); + + Ok(Client { + network_globals: self.network.as_ref().map(|network| network.globals.clone()), + }) + } +} diff --git a/node/src/client/environment.rs b/node/src/client/environment.rs new file mode 100644 index 0000000..a484a98 --- /dev/null +++ b/node/src/client/environment.rs @@ -0,0 +1,253 @@ +//! This crate aims to provide a common set of tools that can be used to create a "environment" to +//! run Zgs services. This allows for the unification of creating tokio runtimes, etc. +//! +//! The idea is that the main thread creates an `Environment`, which is then used to spawn a +//! `Context` which can be handed to any service that wishes to start async tasks. + +use futures::channel::mpsc::{channel, Receiver, Sender}; +use futures::{future, StreamExt}; +use std::sync::Arc; +use task_executor::{ShutdownReason, TaskExecutor}; +use tokio::runtime::{Builder as RuntimeBuilder, Runtime}; + +#[cfg(target_family = "unix")] +use { + futures::Future, + std::{pin::Pin, task::Context, task::Poll}, + tokio::signal::unix::{signal, Signal, SignalKind}, +}; + +#[cfg(not(target_family = "unix"))] +use {futures::channel::oneshot, std::cell::RefCell}; + +/// The maximum time in seconds the client will wait for all internal tasks to shutdown. +const MAXIMUM_SHUTDOWN_TIME: u64 = 15; + +/// Builds an `Environment`. +pub struct EnvironmentBuilder { + runtime: Option>, +} + +impl EnvironmentBuilder { + pub fn new() -> Self { + Self { runtime: None } + } +} + +impl EnvironmentBuilder { + /// Specifies that a multi-threaded tokio runtime should be used. Ideal for production uses. + /// + /// The `Runtime` used is just the standard tokio runtime. + pub fn multi_threaded_tokio_runtime(mut self) -> Result { + self.runtime = Some(Arc::new( + RuntimeBuilder::new_multi_thread() + .enable_all() + .build() + .map_err(|e| format!("Failed to start runtime: {:?}", e))?, + )); + Ok(self) + } + + /// Consumes the builder, returning an `Environment`. + pub fn build(self) -> Result { + let (signal, exit) = exit_future::signal(); + let (signal_tx, signal_rx) = channel(1); + Ok(Environment { + runtime: self + .runtime + .ok_or("Cannot build environment without runtime")?, + signal_tx, + signal_rx: Some(signal_rx), + signal: Some(signal), + exit, + }) + } +} + +/// An execution context that can be used by a service. +/// +/// Distinct from an `Environment` because a `Context` is not able to give a mutable reference to a +/// `Runtime`, instead it only has access to a `Runtime`. +#[derive(Clone)] +pub struct RuntimeContext { + pub executor: TaskExecutor, +} + +/// An environment where Zgs services can run. +pub struct Environment { + runtime: Arc, + + /// Receiver side of an internal shutdown signal. + signal_rx: Option>, + + /// Sender to request shutting down. + signal_tx: Sender, + signal: Option, + exit: exit_future::Exit, +} + +impl Environment { + /// Returns a mutable reference to the `tokio` runtime. + /// + /// Useful in the rare scenarios where it's necessary to block the current thread until a task + /// is finished (e.g., during testing). + pub fn runtime(&self) -> &Arc { + &self.runtime + } + + /// Returns a `Context`. + pub fn core_context(&mut self) -> RuntimeContext { + RuntimeContext { + executor: TaskExecutor::new( + self.runtime().handle().clone(), + self.exit.clone(), + self.signal_tx.clone(), + ), + } + } + + /// Block the current thread until a shutdown signal is received. + /// + /// This can be either the user Ctrl-C'ing or a task requesting to shutdown. + #[cfg(target_family = "unix")] + pub fn block_until_shutdown_requested(&mut self) -> Result { + // future of a task requesting to shutdown + let mut rx = self + .signal_rx + .take() + .ok_or("Inner shutdown already received")?; + let inner_shutdown = + async move { rx.next().await.ok_or("Internal shutdown channel exhausted") }; + futures::pin_mut!(inner_shutdown); + + match self.runtime().block_on(async { + let mut handles = vec![]; + + // setup for handling SIGTERM + match signal(SignalKind::terminate()) { + Ok(terminate_stream) => { + let terminate = SignalFuture::new(terminate_stream, "Received SIGTERM"); + handles.push(terminate); + } + Err(e) => error!(error = %e, "Could not register SIGTERM handler"), + }; + + // setup for handling SIGINT + match signal(SignalKind::interrupt()) { + Ok(interrupt_stream) => { + let interrupt = SignalFuture::new(interrupt_stream, "Received SIGINT"); + handles.push(interrupt); + } + Err(e) => error!(error = %e, "Could not register SIGINT handler"), + } + + // setup for handling a SIGHUP + match signal(SignalKind::hangup()) { + Ok(hup_stream) => { + let hup = SignalFuture::new(hup_stream, "Received SIGHUP"); + handles.push(hup); + } + Err(e) => error!(error = %e, "Could not register SIGHUP handler"), + } + + future::select(inner_shutdown, future::select_all(handles.into_iter())).await + }) { + future::Either::Left((Ok(reason), _)) => { + info!(reason = reason.message(), "Internal shutdown received"); + Ok(reason) + } + future::Either::Left((Err(e), _)) => Err(e.into()), + future::Either::Right(((res, _, _), _)) => { + res.ok_or_else(|| "Handler channel closed".to_string()) + } + } + } + + /// Block the current thread until a shutdown signal is received. + /// + /// This can be either the user Ctrl-C'ing or a task requesting to shutdown. + #[cfg(not(target_family = "unix"))] + pub fn block_until_shutdown_requested(&mut self) -> Result { + // future of a task requesting to shutdown + let mut rx = self + .signal_rx + .take() + .ok_or("Inner shutdown already received")?; + let inner_shutdown = + async move { rx.next().await.ok_or("Internal shutdown channel exhausted") }; + futures::pin_mut!(inner_shutdown); + + // setup for handling a Ctrl-C + let (ctrlc_send, ctrlc_oneshot) = oneshot::channel(); + let ctrlc_send_c = RefCell::new(Some(ctrlc_send)); + ctrlc::set_handler(move || { + if let Some(ctrlc_send) = ctrlc_send_c.try_borrow_mut().unwrap().take() { + if let Err(e) = ctrlc_send.send(()) { + error!("Error sending ctrl-c message: {:?}", e); + } + } + }) + .map_err(|e| format!("Could not set ctrlc handler: {:?}", e))?; + + // Block this thread until a shutdown signal is received. + match self + .runtime() + .block_on(future::select(inner_shutdown, ctrlc_oneshot)) + { + future::Either::Left((Ok(reason), _)) => { + info!(reasion = reason.message(), "Internal shutdown received"); + Ok(reason) + } + future::Either::Left((Err(e), _)) => Err(e.into()), + future::Either::Right((x, _)) => x + .map(|()| ShutdownReason::Success("Received Ctrl+C")) + .map_err(|e| format!("Ctrlc oneshot failed: {}", e)), + } + } + + /// Shutdown the `tokio` runtime when all tasks are idle. + pub fn shutdown_on_idle(self) { + match Arc::try_unwrap(self.runtime) { + Ok(runtime) => { + runtime.shutdown_timeout(std::time::Duration::from_secs(MAXIMUM_SHUTDOWN_TIME)) + } + Err(e) => warn!( + error = ?e, + "Failed to obtain runtime access to shutdown gracefully", + ), + } + } + + /// Fire exit signal which shuts down all spawned services + pub fn fire_signal(&mut self) { + if let Some(signal) = self.signal.take() { + let _ = signal.fire(); + } + } +} + +#[cfg(target_family = "unix")] +struct SignalFuture { + signal: Signal, + message: &'static str, +} + +#[cfg(target_family = "unix")] +impl SignalFuture { + pub fn new(signal: Signal, message: &'static str) -> SignalFuture { + SignalFuture { signal, message } + } +} + +#[cfg(target_family = "unix")] +impl Future for SignalFuture { + type Output = Option; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.signal.poll_recv(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(Some(_)) => Poll::Ready(Some(ShutdownReason::Success(self.message))), + Poll::Ready(None) => Poll::Ready(None), + } + } +} diff --git a/node/src/client/mod.rs b/node/src/client/mod.rs new file mode 100644 index 0000000..4074b04 --- /dev/null +++ b/node/src/client/mod.rs @@ -0,0 +1,34 @@ +#![allow(dead_code)] + +mod builder; +mod environment; + +use network::{Enr, Multiaddr, NetworkGlobals}; +use std::sync::Arc; + +pub use builder::ClientBuilder; +pub use environment::{Environment, EnvironmentBuilder, RuntimeContext}; + +/// The core Zgs client. +/// +/// Holds references to running services, cleanly shutting them down when dropped. +pub struct Client { + network_globals: Option>, +} + +impl Client { + /// Returns the port of the client's libp2p stack, if it was started. + pub fn libp2p_listen_port(&self) -> Option { + self.network_globals.as_ref().map(|n| n.listen_port_tcp()) + } + + /// Returns the list of libp2p addresses the client is listening to. + pub fn libp2p_listen_addresses(&self) -> Option> { + self.network_globals.as_ref().map(|n| n.listen_multiaddrs()) + } + + /// Returns the local libp2p ENR of this node, for network discovery. + pub fn enr(&self) -> Option { + self.network_globals.as_ref().map(|n| n.local_enr()) + } +} diff --git a/node/src/config/config_macro.rs b/node/src/config/config_macro.rs new file mode 100644 index 0000000..99eb278 --- /dev/null +++ b/node/src/config/config_macro.rs @@ -0,0 +1,86 @@ +// Copyright 2019 Conflux Foundation. All rights reserved. +// Conflux is free software and distributed under GNU General Public License. +// See http://www.gnu.org/licenses/ + +macro_rules! if_option { + (Option<$type:ty>, THEN {$($then:tt)*} ELSE {$($otherwise:tt)*}) => ( + $($then)* + ); + ($type:ty, THEN {$($then:tt)*} ELSE {$($otherwise:tt)*}) => ( + $($otherwise)* + ); +} + +macro_rules! if_not_vector { + (Vec<$type:ty>, THEN {$($then:tt)*}) => ( + {} + ); + ($type:ty, THEN {$($then:tt)*}) => ( + $($then)* + ); +} + +macro_rules! underscore_to_hyphen { + ($e:expr) => { + str::replace($e, "_", "-") + }; +} + +macro_rules! build_config{ + ($(($name:ident, ($($type:tt)+), $default:expr))*) => { + #[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize)] + pub struct RawConfiguration { + $(pub $name: $($type)+,)* + } + + impl Default for RawConfiguration { + fn default() -> Self { + RawConfiguration { + $($name: $default,)* + } + } + } + + impl RawConfiguration { + // First parse arguments from config file, and then parse them from command line. + // Replace the ones from config file with the ones from commandline for duplicates. + pub fn parse(matches: &clap::ArgMatches) -> Result { + let mut config = RawConfiguration::default(); + + // read from config file + if let Some(config_file) = matches.value_of("config") { + let config_value = std::fs::read_to_string(config_file) + .map_err(|e| format!("failed to read configuration file: {:?}", e))? + .parse::() + .map_err(|e| format!("failed to parse configuration file: {:?}", e))?; + + $( + if let Some(value) = config_value.get(stringify!($name)) { + config.$name = if_option!($($type)+, + THEN { Some(value.clone().try_into().map_err(|e| format!("Invalid {}: err={:?}", stringify!($name), e).to_owned())?) } + ELSE { value.clone().try_into().map_err(|e| format!("Invalid {}: err={:?}", stringify!($name), e).to_owned())? } + ); + } + )* + } + + // read from command line + $( + #[allow(unused_variables)] + if let Some(value) = matches.value_of(underscore_to_hyphen!(stringify!($name))) { + if_not_vector!($($type)+, THEN { + config.$name = if_option!($($type)+, + THEN{ Some(value.parse().map_err(|_| concat!("Invalid ", stringify!($name)).to_owned())?) } + ELSE{ value.parse().map_err(|_| concat!("Invalid ", stringify!($name)).to_owned())? } + )} + ) + } + )* + + Ok(config) + } + } + } +} + +pub(crate) use {build_config, if_not_vector, if_option, underscore_to_hyphen}; diff --git a/node/src/config/convert.rs b/node/src/config/convert.rs new file mode 100644 index 0000000..57be42c --- /dev/null +++ b/node/src/config/convert.rs @@ -0,0 +1,158 @@ +#![allow(clippy::field_reassign_with_default)] + +use crate::ZgsConfig; +use ethereum_types::H256; +use log_entry_sync::{CacheConfig, ContractAddress, LogSyncConfig}; +use miner::MinerConfig; +use network::NetworkConfig; +use rpc::RPCConfig; +use std::time::Duration; +use storage::StorageConfig; + +impl ZgsConfig { + pub fn network_config(&self) -> Result { + let mut network_config = NetworkConfig::default(); + + network_config.listen_address = self + .network_listen_address + .parse::() + .map_err(|e| format!("Unable to parse network_listen_address: {:?}", e))?; + + network_config.network_dir = self.network_dir.clone().into(); + network_config.libp2p_port = self.network_libp2p_port; + network_config.disable_discovery = self.network_disable_discovery; + network_config.discovery_port = self.network_discovery_port; + network_config.enr_tcp_port = self.network_enr_tcp_port; + network_config.enr_udp_port = self.network_enr_udp_port; + network_config.enr_address = self + .network_enr_address + .as_ref() + .map(|x| x.parse::().unwrap()); + + network_config.boot_nodes_multiaddr = self + .network_boot_nodes + .iter() + .map(|addr| addr.parse::()) + .collect::>() + .map_err(|e| format!("Unable to parse network_boot_nodes: {:?}", e))?; + + network_config.libp2p_nodes = self + .network_libp2p_nodes + .iter() + .map(|addr| addr.parse::()) + .collect::>() + .map_err(|e| format!("Unable to parse network_libp2p_nodes: {:?}", e))?; + + network_config.discv5_config.table_filter = |_| true; + + // TODO + network_config.target_peers = self.network_target_peers; + network_config.private = self.network_private; + + Ok(network_config) + } + + pub fn storage_config(&self) -> Result { + Ok(StorageConfig { + db_dir: self.db_dir.clone().into(), + }) + } + + pub fn rpc_config(&self) -> Result { + let listen_address = self + .rpc_listen_address + .parse::() + .map_err(|e| format!("Unable to parse rpc_listen_address: {:?}", e))?; + + Ok(RPCConfig { + enabled: self.rpc_enabled, + listen_address, + chunks_per_segment: self.rpc_chunks_per_segment, + max_cache_file_size: self.rpc_max_cache_file_size, + }) + } + + pub fn log_sync_config(&self) -> Result { + let contract_address = self + .log_contract_address + .parse::() + .map_err(|e| format!("Unable to parse log_contract_address: {:?}", e))?; + let cache_config = CacheConfig { + // 100 MB. + max_data_size: self.max_cache_data_size, + // This should be enough if we have about one Zgs tx per block. + tx_seq_ttl: self.cache_tx_seq_ttl, + }; + Ok(LogSyncConfig::new( + self.blockchain_rpc_endpoint.clone(), + contract_address, + self.log_sync_start_block_number, + self.confirmation_block_count, + cache_config, + self.log_page_size, + self.rate_limit_retries, + self.timeout_retries, + self.initial_backoff, + self.recover_query_delay, + )) + } + + pub fn mine_config(&self) -> Result, String> { + let flow_address = self + .log_contract_address + .parse::() + .map_err(|e| format!("Unable to parse log_contract_address: {:?}", e))?; + let mine_address = self + .mine_contract_address + .parse::() + .map_err(|e| format!("Unable to parse mine_address: {:?}", e))?; + + let miner_id = if let Some(ref miner_id) = self.miner_id { + Some( + miner_id + .parse::() + .map_err(|e| format!("Unable to parse miner_id: {:?}", e))?, + ) + } else { + None + }; + let miner_key = if let Some(ref miner_key) = self.miner_key { + Some( + miner_key + .parse::() + .map_err(|e| format!("Unable to parse miner_key: {:?}", e))?, + ) + } else { + None + }; + Ok(MinerConfig::new( + miner_id, + miner_key, + self.blockchain_rpc_endpoint.clone(), + mine_address, + flow_address, + )) + } + + pub fn chunk_pool_config(&self) -> chunk_pool::Config { + chunk_pool::Config { + write_window_size: self.chunk_pool_write_window_size, + max_cached_chunks_all: self.chunk_pool_max_cached_chunks_all, + max_writings: self.chunk_pool_max_writings, + expiration_time_secs: self.chunk_pool_expiration_time_secs, + } + } + + pub fn router_config(&self, network_config: &NetworkConfig) -> Result { + let mut router_config = router::Config::default(); + router_config.libp2p_nodes = network_config.libp2p_nodes.to_vec(); + Ok(router_config) + } + + pub fn sync_config(&self) -> sync::Config { + let mut config = self.sync.clone(); + config.find_peer_timeout = Duration::from_secs(self.find_peer_timeout_secs); + config.enable_chunk_request = self.enable_chunk_request; + config + } +} diff --git a/node/src/config/mod.rs b/node/src/config/mod.rs new file mode 100644 index 0000000..5ecfba6 --- /dev/null +++ b/node/src/config/mod.rs @@ -0,0 +1,100 @@ +mod config_macro; + +mod convert; +use config_macro::*; +use serde::Deserialize; +use std::ops::Deref; + +build_config! { + // network + (network_dir, (String), "network".to_string()) + (network_listen_address, (String), "0.0.0.0".to_string()) + (network_enr_address, (Option), None) + (network_enr_tcp_port, (Option), None) + (network_enr_udp_port, (Option), None) + (network_libp2p_port, (u16), 1234) + (network_discovery_port, (u16), 1234) + (network_target_peers, (usize), 3) + (network_boot_nodes, (Vec), vec![]) + (network_libp2p_nodes, (Vec), vec![]) + (network_private, (bool), false) + (network_disable_discovery, (bool), false) + + // log sync + (blockchain_rpc_endpoint, (String), "http://127.0.0.1:8545".to_string()) + (log_contract_address, (String), "".to_string()) + (log_sync_start_block_number, (u64), 0) + (confirmation_block_count, (u64), 12) + (log_page_size, (u64), 1000) + (max_cache_data_size, (usize), 100 * 1024 * 1024) // 100 MB + (cache_tx_seq_ttl, (usize), 500) + + (rate_limit_retries, (u32), 100) + (timeout_retries, (u32), 100) + (initial_backoff, (u64), 500) + (recover_query_delay, (u64), 50) + + // rpc + (rpc_enabled, (bool), true) + (rpc_listen_address, (String), "127.0.0.1:5678".to_string()) + (rpc_chunks_per_segment, (usize), 1024) + (rpc_max_cache_file_size, (usize), 10*1024*1024) //10MB + + // chunk pool + (chunk_pool_write_window_size, (usize), 4) + (chunk_pool_max_cached_chunks_all, (usize), 4*1024*1024) // 1G + (chunk_pool_max_writings, (usize), 16) + (chunk_pool_expiration_time_secs, (u64), 300) // 5 minutes + + // db + (db_dir, (String), "db".to_string()) + + // misc + (log_config_file, (String), "log_config".to_string()) + (log_directory, (String), "log".to_string()) + + // mine + (mine_contract_address, (String), "".to_string()) + (miner_id, (Option), None) + (miner_key, (Option), None) + + // sync + (find_peer_timeout_secs, (u64), 30) + (enable_chunk_request, (bool), true) +} + +#[derive(Debug, Default, Deserialize)] +#[serde(default)] +pub struct ZgsConfig { + pub raw_conf: RawConfiguration, + + pub sync: sync::Config, +} + +impl Deref for ZgsConfig { + type Target = RawConfiguration; + + fn deref(&self) -> &Self::Target { + &self.raw_conf + } +} + +impl ZgsConfig { + pub fn parse(matches: &clap::ArgMatches) -> Result { + let config_file = match matches.get_one::("config") { + Some(file) => file.as_str(), + None => return Err("config file not specified".to_string()), + }; + + let mut config = config::Config::builder() + .add_source(config::File::with_name(config_file)) + .build() + .map_err(|e| format!("Failed to build config: {:?}", e))? + .try_deserialize::() + .map_err(|e| format!("Failed to deserialize config: {:?}", e))?; + + config.raw_conf = RawConfiguration::parse(matches)?; + + Ok(config) + } +} diff --git a/node/src/log.rs b/node/src/log.rs new file mode 100644 index 0000000..5f1b526 --- /dev/null +++ b/node/src/log.rs @@ -0,0 +1,61 @@ +use task_executor::TaskExecutor; +use tracing::Level; +use tracing_subscriber::EnvFilter; + +const LOG_RELOAD_PERIOD_SEC: u64 = 30; + +pub fn configure(log_level_file: &str, log_directory: &str, executor: TaskExecutor) { + let file_appender = tracing_appender::rolling::daily(log_directory, "zgs.log"); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + let builder = tracing_subscriber::fmt() + .with_max_level(Level::TRACE) + .with_env_filter(EnvFilter::default()) + .with_writer(non_blocking) + .with_ansi(false) + // .with_file(true) + // .with_line_number(true) + // .with_thread_names(true) + .with_filter_reloading(); + + let handle = builder.reload_handle(); + builder.init(); + + let level_file = log_level_file.to_string(); + + // load config synchronously + let mut config = std::fs::read_to_string(&level_file).unwrap_or_default(); + let _ = handle.reload(&config); + + // periodically check for config changes + executor.spawn( + async move { + // move the log writer guard so that it's not dropped. + let _moved_guard = guard; + let mut interval = + tokio::time::interval(std::time::Duration::from_secs(LOG_RELOAD_PERIOD_SEC)); + + loop { + interval.tick().await; + + let new_config = match tokio::fs::read_to_string(&level_file).await { + Ok(c) if c == config => continue, + Ok(c) => c, + Err(e) => { + println!("Unable to read log file {}: {:?}", level_file, e); + continue; + } + }; + + println!("Updating log config to {:?}", new_config); + + match handle.reload(&new_config) { + Ok(()) => config = new_config, + Err(e) => { + println!("Failed to load new config: {:?}", e); + } + } + } + }, + "log_reload", + ); +} diff --git a/node/src/main.rs b/node/src/main.rs new file mode 100644 index 0000000..dade5ee --- /dev/null +++ b/node/src/main.rs @@ -0,0 +1,101 @@ +#[macro_use] +extern crate tracing; + +mod cli; +mod client; +mod config; +mod log; + +use crate::config::ZgsConfig; +use client::{Client, ClientBuilder, RuntimeContext}; +use std::error::Error; + +async fn start_node(context: RuntimeContext, config: ZgsConfig) -> Result { + let network_config = config.network_config()?; + let storage_config = config.storage_config()?; + let rpc_config = config.rpc_config()?; + let log_sync_config = config.log_sync_config()?; + let miner_config = config.mine_config()?; + let router_config = config.router_config(&network_config)?; + let sync_config = config.sync_config(); + + ClientBuilder::default() + .with_runtime_context(context) + .with_rocksdb_store(&storage_config)? + .with_log_sync(log_sync_config) + .await? + .with_file_location_cache() + .with_network(&network_config) + .await? + .with_sync(sync_config) + .await? + .with_miner(miner_config) + .await? + .with_router(router_config)? + .with_rpc(rpc_config, config.chunk_pool_config()) + .await? + .build() +} + +fn main() -> Result<(), Box> { + // Only allow 64-bit targets for compilation, since there are many + // type conversions between `usize` and `u64`, or even use `usize` + // as file size or chunk index. + #[cfg(not(target_pointer_width = "64"))] + compile_error!("compilation is only allowed for 64-bit targets"); + + // enable backtraces + std::env::set_var("RUST_BACKTRACE", "1"); + + // runtime environment + let mut environment = client::EnvironmentBuilder::new() + .multi_threaded_tokio_runtime()? + .build()?; + + let context = environment.core_context(); + let executor = context.executor.clone(); + + // CLI, config, and logs + let matches = cli::cli_app().get_matches(); + let config = ZgsConfig::parse(&matches)?; + log::configure( + &config.log_config_file, + &config.log_directory, + executor.clone(), + ); + + // start services + executor.clone().spawn( + async move { + info!("Starting services..."); + if let Err(e) = start_node(context.clone(), config).await { + error!(reason = %e, "Failed to start zgs node"); + // Ignore the error since it always occurs during normal operation when + // shutting down. + let _ = + executor + .shutdown_sender() + .try_send(task_executor::ShutdownReason::Failure( + "Failed to start zgs node", + )); + } else { + info!("Services started"); + } + }, + "zgs_node", + ); + + // Block this thread until we get a ctrl-c or a task sends a shutdown signal. + let shutdown_reason = environment.block_until_shutdown_requested()?; + info!(reason = ?shutdown_reason, "Shutting down..."); + + environment.fire_signal(); + + // Shutdown the environment once all tasks have completed. + environment.shutdown_on_idle(); + + match shutdown_reason { + task_executor::ShutdownReason::Success(_) => Ok(()), + task_executor::ShutdownReason::Failure(msg) => Err(msg.to_string().into()), + } +} diff --git a/node/storage-async/Cargo.toml b/node/storage-async/Cargo.toml new file mode 100644 index 0000000..2d3b1c6 --- /dev/null +++ b/node/storage-async/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "storage-async" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +shared_types = { path = "../shared_types" } +storage = { path = "../storage" } +task_executor = { path = "../../common/task_executor" } +tokio = { version = "1.19.2", features = ["sync"] } +tracing = "0.1.35" \ No newline at end of file diff --git a/node/storage-async/src/lib.rs b/node/storage-async/src/lib.rs new file mode 100644 index 0000000..546b290 --- /dev/null +++ b/node/storage-async/src/lib.rs @@ -0,0 +1,91 @@ +#[macro_use] +extern crate tracing; + +use anyhow::bail; +use shared_types::{Chunk, ChunkArray, ChunkArrayWithProof, DataRoot, Transaction}; +use std::sync::Arc; +use storage::{error, error::Result, log_store::Store as LogStore, H256}; +use task_executor::TaskExecutor; +use tokio::sync::{oneshot, RwLock}; + +/// The name of the worker tokio tasks. +const WORKER_TASK_NAME: &str = "async_storage_worker"; + +macro_rules! delegate { + (fn $name:tt($($v:ident: $t:ty),*)) => { + delegate!($name($($v: $t),*) -> ()); + }; + + (fn $name:tt($($v:ident: $t:ty),*) -> $ret:ty) => { + pub async fn $name(&self, $($v: $t),*) -> $ret { + self.spawn(move |store| store.$name($($v),*)).await + } + }; +} + +#[derive(Clone)] +pub struct Store { + /// Log and transaction storage. + store: Arc>, + + /// Tokio executor for spawning worker tasks. + executor: TaskExecutor, +} + +impl Store { + pub fn new(store: Arc>, executor: TaskExecutor) -> Self { + Store { store, executor } + } + + delegate!(fn check_tx_completed(tx_seq: u64) -> Result); + delegate!(fn get_chunk_by_tx_and_index(tx_seq: u64, index: usize) -> Result>); + delegate!(fn get_chunks_by_tx_and_index_range(tx_seq: u64, index_start: usize, index_end: usize) -> Result>); + delegate!(fn get_chunks_with_proof_by_tx_and_index_range(tx_seq: u64, index_start: usize, index_end: usize) -> Result>); + delegate!(fn get_tx_by_seq_number(seq: u64) -> Result>); + delegate!(fn put_chunks(tx_seq: u64, chunks: ChunkArray) -> Result<()>); + delegate!(fn put_chunks_with_tx_hash(tx_seq: u64, tx_hash: H256, chunks: ChunkArray) -> Result); + delegate!(fn get_chunk_by_flow_index(index: u64, length: u64) -> Result>); + delegate!(fn finalize_tx(tx_seq: u64) -> Result<()>); + delegate!(fn finalize_tx_with_hash(tx_seq: u64, tx_hash: H256) -> Result); + + pub async fn get_tx_seq_by_data_root(&self, data_root: &DataRoot) -> Result> { + let root = *data_root; + self.spawn(move |store| store.get_tx_seq_by_data_root(&root)) + .await + } + + pub async fn get_tx_by_data_root(&self, data_root: &DataRoot) -> Result> { + let root = *data_root; + self.spawn(move |store| store.get_tx_by_data_root(&root)) + .await + } + + async fn spawn(&self, f: F) -> Result + where + F: FnOnce(&mut dyn LogStore) -> Result + Send + 'static, + T: Send + 'static, + { + let store = self.store.clone(); + let (tx, rx) = oneshot::channel(); + + self.executor.spawn( + async move { + // FIXME(zz): Not all functions need `write`. Refactor store usage. + let res = f(&mut *store.write().await); + + if tx.send(res).is_err() { + error!("Unable to complete async storage operation: the receiver dropped"); + } + }, + WORKER_TASK_NAME, + ); + + rx.await + .unwrap_or_else(|_| bail!(error::Error::Custom("Receiver error".to_string()))) + } + + // FIXME(zz): Refactor the lock and async call here. + pub fn get_store(&self) -> &RwLock { + self.store.as_ref() + } +} diff --git a/node/storage/Cargo.toml b/node/storage/Cargo.toml new file mode 100644 index 0000000..765f4d6 --- /dev/null +++ b/node/storage/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "storage" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "=1.0.58", features = ["backtrace"] } +append_merkle = {path = "../../common/append_merkle"} +zgs_spec = {path = "../../common/spec"} +zgs_seal = {path = "../../common/zgs_seal"} +eth2_ssz = "0.4.0" +eth2_ssz_derive = "0.3.0" +ethereum-types = "0.14" +hex = "0.4.3" +kvdb = "0.13.0" +kvdb-memorydb = "0.13.0" +kvdb-rocksdb = "0.19.0" +#merkle_light = {git = "https://github.com/sitano/merkle_light.git", rev = "fe31d4e" } +merkle_light = { path = "../../common/merkle_light" } +merkle_tree = { path = "../../common/merkle_tree"} +rayon = "1.5.3" +shared_types = { path = "../shared_types" } +tracing = "0.1.35" +typenum = "1.15.0" +bitmaps = "^3.2" +static_assertions = "1.1" +tiny-keccak = "*" +itertools = "0.10.5" + +[dev-dependencies] +tempdir = "0.3.7" +rand = "0.8.5" +hex-literal = "0.3.4" +criterion = "0.4" + +[[bench]] +name = "benchmark" +harness = false + +[profile.bench] +debug = true \ No newline at end of file diff --git a/node/storage/benches/benchmark.rs b/node/storage/benches/benchmark.rs new file mode 100644 index 0000000..8aae9f7 --- /dev/null +++ b/node/storage/benches/benchmark.rs @@ -0,0 +1,184 @@ +use std::{ + fs, + path::Path, + sync::{Arc, RwLock}, +}; + +use criterion::{criterion_group, criterion_main, Criterion}; +use rand::{random, Rng}; +use shared_types::{ChunkArray, Transaction, CHUNK_SIZE}; +use storage::{ + log_store::{ + log_manager::{sub_merkle_tree, tx_subtree_root_list_padded, LogConfig}, + Store, + }, + LogManager, +}; + +fn write_performance(c: &mut Criterion) { + if Path::new("db_write").exists() { + fs::remove_dir_all("db_write").unwrap(); + } + + let store: Arc> = Arc::new(RwLock::new( + LogManager::rocksdb(LogConfig::default(), "db_write") + .map_err(|e| format!("Unable to start RocksDB store: {:?}", e)) + .unwrap(), + )); + + let chunk_count = 2048; + let data_size = CHUNK_SIZE * chunk_count; + let (chunk_size_padded, _) = shared_types::compute_padded_chunk_size(data_size); + + let mut data_vec = vec![]; + let mut merkel_nodes_vec = vec![]; + let mut first_tree_size_vec = vec![]; + let mut merkel_root_vec = vec![]; + + for _ in 0..5000 { + let mut data = vec![0; data_size]; + for i in 0..data_size { + data[i] = random(); + } + + let merkel_nodes = tx_subtree_root_list_padded(&data[..]); + let first_tree_size = 1 << (merkel_nodes[0].0 - 1); + + let merkle = sub_merkle_tree(&data).unwrap(); + let merkel_root = merkle.root().into(); + + data_vec.push(data); + merkel_nodes_vec.push(merkel_nodes); + first_tree_size_vec.push(first_tree_size); + merkel_root_vec.push(merkel_root); + } + + let mut seq = 0; + let mut offset = 1; + + let mut group = c.benchmark_group("write performance"); + group.sample_size(10); + group.bench_function("write performance", move |b| { + b.iter(|| { + let first_tree_size = first_tree_size_vec[seq as usize]; + let data = data_vec[seq as usize].clone(); + let merkel_root = merkel_root_vec[seq as usize]; + let merkel_nodes = merkel_nodes_vec[seq as usize].clone(); + + let start_offset = if offset % first_tree_size == 0 { + offset + } else { + (offset / first_tree_size + 1) * first_tree_size + }; + + let chunks = ChunkArray { + data: data.to_vec(), + start_index: 0, + }; + + let tx = Transaction { + stream_ids: vec![], + size: data_size as u64, + data_merkle_root: merkel_root, + seq, + data: vec![], + start_entry_index: start_offset, + merkle_nodes: merkel_nodes, + }; + + store.write().unwrap().put_tx(tx).unwrap(); + store + .write() + .unwrap() + .put_chunks(seq, chunks.clone()) + .unwrap(); + store.write().unwrap().finalize_tx(seq).unwrap(); + + offset = start_offset + chunk_size_padded as u64; + seq += 1; + }) + }); +} + +fn read_performance(c: &mut Criterion) { + if Path::new("db_read").exists() { + fs::remove_dir_all("db_read").unwrap(); + } + + let store: Arc> = Arc::new(RwLock::new( + LogManager::rocksdb(LogConfig::default(), "db_read") + .map_err(|e| format!("Unable to start RocksDB store: {:?}", e)) + .unwrap(), + )); + + let tx_size = 1000; + let chunk_count = 4096; + let data_size = CHUNK_SIZE * chunk_count; + let mut offset = 1; + let (chunk_size_padded, _) = shared_types::compute_padded_chunk_size(data_size); + + for seq in 0..tx_size { + let mut data = vec![0; data_size]; + for i in 0..data_size { + data[i] = random(); + } + + let merkel_nodes = tx_subtree_root_list_padded(&data[..]); + let first_tree_size = 1 << (merkel_nodes[0].0 - 1); + + let merkle = sub_merkle_tree(&data).unwrap(); + let merkel_root = merkle.root().into(); + + let start_offset = if offset % first_tree_size == 0 { + offset + } else { + (offset / first_tree_size + 1) * first_tree_size + }; + + let chunks = ChunkArray { + data: data.to_vec(), + start_index: 0, + }; + + let tx = Transaction { + stream_ids: vec![], + size: data_size as u64, + data_merkle_root: merkel_root, + seq, + data: vec![], + start_entry_index: start_offset, + merkle_nodes: merkel_nodes, + }; + + store.write().unwrap().put_tx(tx).unwrap(); + store + .write() + .unwrap() + .put_chunks(seq, chunks.clone()) + .unwrap(); + store.write().unwrap().finalize_tx(seq).unwrap(); + + offset = start_offset + chunk_size_padded as u64; + } + + let mut rng = rand::thread_rng(); + + let mut group = c.benchmark_group("read performance"); + group.sample_size(100); + group.bench_function("read performance", move |b| { + b.iter(|| { + let tx_seq = rng.gen_range(0..tx_size); + let index_start = rng.gen_range(0..=chunk_count); + let index_end = rng.gen_range((index_start + 1)..=(chunk_count + 1)); + + store + .read() + .unwrap() + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, index_start, index_end) + .unwrap(); + }) + }); +} + +criterion_group!(benches, write_performance, read_performance); +criterion_main!(benches); diff --git a/node/storage/src/config.rs b/node/storage/src/config.rs new file mode 100644 index 0000000..1f537d3 --- /dev/null +++ b/node/storage/src/config.rs @@ -0,0 +1,6 @@ +use std::path::PathBuf; + +#[derive(Clone)] +pub struct Config { + pub db_dir: PathBuf, +} diff --git a/node/storage/src/error.rs b/node/storage/src/error.rs new file mode 100644 index 0000000..3a7082e --- /dev/null +++ b/node/storage/src/error.rs @@ -0,0 +1,42 @@ +use anyhow; +use ssz::DecodeError; +use std::error::Error as ErrorTrait; +use std::fmt::{Debug, Display, Formatter}; +use std::io::Error as IoError; + +pub type Result = anyhow::Result; + +#[derive(Debug)] +pub enum Error { + Io(IoError), + /// A partial chunk batch is written. + InvalidBatchBoundary, + ValueDecodingError(DecodeError), + Custom(String), +} + +impl From for Error { + fn from(e: IoError) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: DecodeError) -> Self { + Error::ValueDecodingError(e) + } +} + +impl From for Error { + fn from(e: anyhow::Error) -> Self { + Error::Custom(e.to_string()) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "StorageError: {:?}", self) + } +} + +impl ErrorTrait for Error {} diff --git a/node/storage/src/lib.rs b/node/storage/src/lib.rs new file mode 100644 index 0000000..dd5ad7b --- /dev/null +++ b/node/storage/src/lib.rs @@ -0,0 +1,32 @@ +use kvdb::KeyValueDB; + +pub mod config; +pub mod error; +pub mod log_store; + +pub use config::Config as StorageConfig; +pub use log_store::log_manager::LogManager; + +pub use ethereum_types::H256; + +pub trait ZgsKeyValueDB: KeyValueDB { + fn put(&self, col: u32, key: &[u8], value: &[u8]) -> std::io::Result<()> { + let mut tx = self.transaction(); + tx.put(col, key, value); + self.write(tx) + } + + fn delete(&self, col: u32, key: &[u8]) -> std::io::Result<()> { + let mut tx = self.transaction(); + tx.delete(col, key); + self.write(tx) + } + + fn delete_with_prefix(&self, col: u32, key_prefix: &[u8]) -> std::io::Result<()> { + let mut tx = self.transaction(); + tx.delete_prefix(col, key_prefix); + self.write(tx) + } +} + +impl ZgsKeyValueDB for T {} diff --git a/node/storage/src/log_store/config.rs b/node/storage/src/log_store/config.rs new file mode 100644 index 0000000..ace314a --- /dev/null +++ b/node/storage/src/log_store/config.rs @@ -0,0 +1,85 @@ +use anyhow::{anyhow, Result}; +use kvdb::{DBKey, DBOp}; +use ssz::{Decode, Encode}; + +use crate::LogManager; + +use super::log_manager::COL_MISC; + +pub trait Configurable { + fn get_config(&self, key: &[u8]) -> Result>>; + fn set_config(&self, key: &[u8], value: &[u8]) -> Result<()>; + fn remove_config(&self, key: &[u8]) -> Result<()>; + + fn exec_configs(&self, tx: ConfigTx) -> Result<()>; +} + +#[derive(Default)] +pub struct ConfigTx { + ops: Vec, +} + +impl ConfigTx { + pub fn append(&mut self, other: &mut Self) { + self.ops.append(&mut other.ops); + } + + pub fn set_config, T: Encode>(&mut self, key: &K, value: &T) { + self.ops.push(DBOp::Insert { + col: COL_MISC, + key: DBKey::from_slice(key.as_ref()), + value: value.as_ssz_bytes(), + }); + } + + pub fn remove_config>(&mut self, key: &K) { + self.ops.push(DBOp::Delete { + col: COL_MISC, + key: DBKey::from_slice(key.as_ref()), + }); + } +} + +pub trait ConfigurableExt: Configurable { + fn get_config_decoded, T: Decode>(&self, key: &K) -> Result> { + match self.get_config(key.as_ref())? { + Some(val) => Ok(Some( + T::from_ssz_bytes(&val).map_err(|e| anyhow!("SSZ decode error: {:?}", e))?, + )), + None => Ok(None), + } + } + + fn set_config_encoded, T: Encode>(&self, key: &K, value: &T) -> Result<()> { + self.set_config(key.as_ref(), &value.as_ssz_bytes()) + } + + fn remove_config_by_key>(&self, key: &K) -> Result<()> { + self.remove_config(key.as_ref()) + } +} + +impl ConfigurableExt for T {} + +impl Configurable for LogManager { + fn get_config(&self, key: &[u8]) -> Result>> { + Ok(self.db.get(COL_MISC, key)?) + } + + fn set_config(&self, key: &[u8], value: &[u8]) -> Result<()> { + self.db.put(COL_MISC, key, value)?; + Ok(()) + } + + fn remove_config(&self, key: &[u8]) -> Result<()> { + Ok(self.db.delete(COL_MISC, key)?) + } + + fn exec_configs(&self, tx: ConfigTx) -> Result<()> { + let mut db_tx = self.db.transaction(); + db_tx.ops = tx.ops; + self.db.write(db_tx)?; + + Ok(()) + } +} diff --git a/node/storage/src/log_store/flow_store.rs b/node/storage/src/log_store/flow_store.rs new file mode 100644 index 0000000..34fbcac --- /dev/null +++ b/node/storage/src/log_store/flow_store.rs @@ -0,0 +1,539 @@ +use super::load_chunk::EntryBatch; +use super::{MineLoadChunk, SealAnswer, SealTask}; +use crate::error::Error; +use crate::log_store::log_manager::{bytes_to_entries, COL_ENTRY_BATCH, COL_ENTRY_BATCH_ROOT}; +use crate::log_store::{FlowRead, FlowSeal, FlowWrite}; +use crate::{try_option, ZgsKeyValueDB}; +use anyhow::{anyhow, bail, Result}; +use append_merkle::{MerkleTreeInitialData, MerkleTreeRead}; +use itertools::Itertools; +use zgs_spec::{BYTES_PER_SECTOR, SEALS_PER_LOAD, SECTORS_PER_LOAD, SECTORS_PER_SEAL}; +use shared_types::{ChunkArray, DataRoot, FlowProof}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode as DeriveDecode, Encode as DeriveEncode}; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::fmt::Debug; +use std::sync::Arc; +use std::{cmp, mem}; +use tracing::{debug, error, trace}; + +pub struct FlowStore { + db: FlowDBStore, + // TODO(kevin): This is an in-memory cache for recording which chunks are ready for sealing. It should be persisted on disk. + to_seal_set: BTreeMap, + // Data sealing is an asynchronized process. + // The sealing service uses the version number to distinguish if revert happens during sealing. + to_seal_version: usize, + config: FlowConfig, +} + +impl FlowStore { + pub fn new(db: Arc, config: FlowConfig) -> Self { + Self { + db: FlowDBStore::new(db), + to_seal_set: Default::default(), + to_seal_version: 0, + config, + } + } + + pub fn put_batch_root_list(&self, root_map: BTreeMap) -> Result<()> { + self.db.put_batch_root_list(root_map) + } + + pub fn insert_subtree_list_for_batch( + &self, + batch_index: usize, + subtree_list: Vec<(usize, usize, DataRoot)>, + ) -> Result<()> { + let mut batch = self + .db + .get_entry_batch(batch_index as u64)? + .unwrap_or_else(|| EntryBatch::new(batch_index as u64)); + batch.set_subtree_list(subtree_list); + self.db.put_entry_raw(vec![(batch_index as u64, batch)])?; + + Ok(()) + } + + pub fn gen_proof_in_batch(&self, batch_index: usize, sector_index: usize) -> Result { + let batch = self + .db + .get_entry_batch(batch_index as u64)? + .ok_or_else(|| anyhow!("batch missing, index={}", batch_index))?; + let merkle = batch.to_merkle_tree(batch_index == 0)?.ok_or_else(|| { + anyhow!( + "batch data incomplete for building a merkle tree, index={}", + batch_index + ) + })?; + merkle.gen_proof(sector_index) + } +} + +#[derive(Clone, Debug)] +pub struct FlowConfig { + pub batch_size: usize, +} + +impl Default for FlowConfig { + fn default() -> Self { + Self { + batch_size: SECTORS_PER_LOAD, + } + } +} + +impl FlowRead for FlowStore { + /// Return `Ok(None)` if only partial data are available. + fn get_entries(&self, index_start: u64, index_end: u64) -> Result> { + if index_end <= index_start { + bail!( + "invalid entry index: start={} end={}", + index_start, + index_end + ); + } + let mut data = Vec::with_capacity((index_end - index_start) as usize * BYTES_PER_SECTOR); + for (start_entry_index, end_entry_index) in + batch_iter(index_start, index_end, self.config.batch_size) + { + let chunk_index = start_entry_index / self.config.batch_size as u64; + let mut offset = start_entry_index - chunk_index * self.config.batch_size as u64; + let mut length = end_entry_index - start_entry_index; + + // Tempfix: for first chunk, its offset is always 1 + if chunk_index == 0 && offset == 0 { + offset = 1; + length -= 1; + } + + let entry_batch = try_option!(self.db.get_entry_batch(chunk_index)?); + let mut entry_batch_data = + try_option!(entry_batch.get_unsealed_data(offset as usize, length as usize)); + data.append(&mut entry_batch_data); + } + Ok(Some(ChunkArray { + data, + start_index: index_start, + })) + } + + fn get_available_entries(&self, index_start: u64, index_end: u64) -> Result> { + // Both `index_start` and `index_end` are at the batch boundaries, so we do not need + // to check if the data is within range when we process each batch. + if index_end <= index_start + || index_start % self.config.batch_size as u64 != 0 + || index_end % self.config.batch_size as u64 != 0 + { + bail!( + "invalid entry index: start={} end={}", + index_start, + index_end + ); + } + let mut entry_list = Vec::::new(); + for (start_entry_index, _) in batch_iter(index_start, index_end, self.config.batch_size) { + let chunk_index = start_entry_index / self.config.batch_size as u64; + + if let Some(mut data_list) = self + .db + .get_entry_batch(chunk_index)? + .map(|b| b.into_data_list(start_entry_index)) + { + if data_list.is_empty() { + continue; + } + // This will not happen for now because we only get entries for the last chunk. + if let Some(last) = entry_list.last_mut() { + if last.start_index + bytes_to_entries(last.data.len() as u64) + == data_list[0].start_index + { + // Merge the first element with the previous one. + last.data.append(&mut data_list.remove(0).data); + } + } + for data in data_list { + entry_list.push(data); + } + } + } + Ok(entry_list) + } + + /// Return the list of all stored chunk roots. + fn get_chunk_root_list(&self) -> Result> { + self.db.get_batch_root_list() + } + + fn load_sealed_data(&self, chunk_index: u64) -> Result> { + let batch = try_option!(self.db.get_entry_batch(chunk_index)?); + let mut mine_chunk = MineLoadChunk::default(); + for (seal_index, (sealed, validity)) in mine_chunk + .loaded_chunk + .iter_mut() + .zip(mine_chunk.avalibilities.iter_mut()) + .enumerate() + { + if let Some(data) = batch.get_sealed_data(seal_index as u16) { + *validity = true; + *sealed = data; + } + } + Ok(Some(mine_chunk)) + } +} + +impl FlowWrite for FlowStore { + /// Return the roots of completed chunks. The order is guaranteed to be increasing + /// by chunk index. + fn append_entries(&mut self, data: ChunkArray) -> Result> { + trace!("append_entries: {} {}", data.start_index, data.data.len()); + if data.data.len() % BYTES_PER_SECTOR != 0 { + bail!("append_entries: invalid data size, len={}", data.data.len()); + } + let mut batch_list = Vec::new(); + for (start_entry_index, end_entry_index) in batch_iter( + data.start_index, + data.start_index + bytes_to_entries(data.data.len() as u64), + self.config.batch_size, + ) { + // TODO: Avoid mem-copy if possible. + let chunk = data + .sub_array(start_entry_index, end_entry_index) + .expect("in range"); + + let chunk_index = chunk.start_index / self.config.batch_size as u64; + + // TODO: Try to avoid loading from db if possible. + let mut batch = self + .db + .get_entry_batch(chunk_index)? + .unwrap_or_else(|| EntryBatch::new(chunk_index)); + let completed_seals = batch.insert_data( + (chunk.start_index % self.config.batch_size as u64) as usize, + chunk.data, + )?; + completed_seals.into_iter().for_each(|x| { + self.to_seal_set.insert( + chunk_index as usize * SEALS_PER_LOAD + x as usize, + self.to_seal_version, + ); + }); + + batch_list.push((chunk_index, batch)); + } + self.db.put_entry_batch_list(batch_list) + } + + fn truncate(&mut self, start_index: u64) -> crate::error::Result<()> { + let to_reseal = self.db.truncate(start_index, self.config.batch_size)?; + + self.to_seal_set + .split_off(&(start_index as usize / SECTORS_PER_SEAL)); + self.to_seal_version += 1; + + to_reseal.into_iter().for_each(|x| { + self.to_seal_set.insert(x, self.to_seal_version); + }); + Ok(()) + } +} + +impl FlowSeal for FlowStore { + fn pull_seal_chunk(&self, seal_index_max: usize) -> Result>> { + let mut to_seal_iter = self.to_seal_set.iter(); + let (&first_index, &first_version) = try_option!(to_seal_iter.next()); + if first_index >= seal_index_max { + return Ok(None); + } + + let mut tasks = Vec::with_capacity(SEALS_PER_LOAD); + + let batch_data = self + .db + .get_entry_batch((first_index / SEALS_PER_LOAD) as u64)? + .expect("Lost data chunk in to_seal_set"); + + for (&seal_index, &version) in + std::iter::once((&first_index, &first_version)).chain(to_seal_iter.filter(|(&x, _)| { + first_index / SEALS_PER_LOAD == x / SEALS_PER_LOAD && x < seal_index_max + })) + { + let seal_index_local = seal_index % SEALS_PER_LOAD; + let non_sealed_data = batch_data + .get_non_sealed_data(seal_index_local as u16) + .expect("Lost seal chunk in to_seal_set"); + tasks.push(SealTask { + seal_index: seal_index as u64, + version, + non_sealed_data, + }) + } + + Ok(Some(tasks)) + } + + fn submit_seal_result(&mut self, answers: Vec) -> Result<()> { + let is_consistent = |answer: &SealAnswer| { + self.to_seal_set + .get(&(answer.seal_index as usize)) + .map_or(false, |cur_ver| cur_ver == &answer.version) + }; + + let mut updated_chunk = vec![]; + let mut removed_seal_index = Vec::new(); + for (load_index, answers_in_chunk) in &answers + .into_iter() + .filter(is_consistent) + .group_by(|answer| answer.seal_index / SEALS_PER_LOAD as u64) + { + let mut batch_chunk = self + .db + .get_entry_batch(load_index)? + .expect("Can not find chunk data"); + for answer in answers_in_chunk { + removed_seal_index.push(answer.seal_index as usize); + batch_chunk.submit_seal_result(answer)?; + } + updated_chunk.push((load_index, batch_chunk)); + } + + debug!("Seal chunks: indices = {:?}", removed_seal_index); + + for idx in removed_seal_index.into_iter() { + self.to_seal_set.remove(&idx); + } + + self.db.put_entry_raw(updated_chunk)?; + + Ok(()) + } +} + +pub struct FlowDBStore { + kvdb: Arc, +} + +impl FlowDBStore { + pub fn new(kvdb: Arc) -> Self { + Self { kvdb } + } + + fn put_entry_batch_list( + &self, + batch_list: Vec<(u64, EntryBatch)>, + ) -> Result> { + let mut completed_batches = Vec::new(); + let mut tx = self.kvdb.transaction(); + for (batch_index, batch) in batch_list { + tx.put( + COL_ENTRY_BATCH, + &batch_index.to_be_bytes(), + &batch.as_ssz_bytes(), + ); + if let Some(root) = batch.build_root(batch_index == 0)? { + trace!("complete batch: index={}", batch_index); + tx.put( + COL_ENTRY_BATCH_ROOT, + // (batch_index, subtree_depth) + &encode_batch_root_key(batch_index as usize, 1), + root.as_bytes(), + ); + completed_batches.push((batch_index, root)); + } + } + self.kvdb.write(tx)?; + Ok(completed_batches) + } + + fn put_entry_raw(&self, batch_list: Vec<(u64, EntryBatch)>) -> Result<()> { + let mut tx = self.kvdb.transaction(); + for (batch_index, batch) in batch_list { + tx.put( + COL_ENTRY_BATCH, + &batch_index.to_be_bytes(), + &batch.as_ssz_bytes(), + ); + } + self.kvdb.write(tx)?; + Ok(()) + } + + fn get_entry_batch(&self, batch_index: u64) -> Result> { + let raw = try_option!(self.kvdb.get(COL_ENTRY_BATCH, &batch_index.to_be_bytes())?); + Ok(Some(EntryBatch::from_ssz_bytes(&raw).map_err(Error::from)?)) + } + + fn put_batch_root_list(&self, root_map: BTreeMap) -> Result<()> { + let mut tx = self.kvdb.transaction(); + for (batch_index, (root, subtree_depth)) in root_map { + tx.put( + COL_ENTRY_BATCH_ROOT, + &encode_batch_root_key(batch_index, subtree_depth), + root.as_bytes(), + ); + } + Ok(self.kvdb.write(tx)?) + } + + fn get_batch_root_list(&self) -> Result> { + let mut range_root = None; + // A list of `BatchRoot` that can reconstruct the whole merkle tree structure. + let mut root_list = Vec::new(); + // A list of leaf `(index, root_hash)` in the subtrees of some nodes in `root_list`, + // and they will be updated in the merkle tree with `fill_leaf` by the caller. + let mut leaf_list = Vec::new(); + let mut expected_index = 0; + for r in self.kvdb.iter(COL_ENTRY_BATCH_ROOT) { + let (index_bytes, root_bytes) = r?; + let (batch_index, subtree_depth) = decode_batch_root_key(index_bytes.as_ref())?; + debug!( + "load root depth={}, index expected={} get={}", + subtree_depth, expected_index, batch_index + ); + let root = DataRoot::from_slice(root_bytes.as_ref()); + if subtree_depth == 1 { + if range_root.is_none() { + // This is expected to be the next leaf. + if batch_index == expected_index { + root_list.push((1, root)); + expected_index += 1; + } else { + bail!( + "unexpected chunk leaf, expected={}, get={}", + expected_index, + batch_index + ); + } + } else { + match batch_index.cmp(&expected_index) { + Ordering::Less => { + // This leaf is within a subtree whose root is known. + leaf_list.push((batch_index, root)); + } + Ordering::Equal => { + // A subtree range ends. + range_root = None; + root_list.push((1, root)); + expected_index += 1; + } + Ordering::Greater => { + bail!( + "unexpected chunk leaf in range, expected={}, get={}, range={:?}", + expected_index, + batch_index, + range_root, + ); + } + } + } + } else if expected_index == batch_index { + range_root = Some(BatchRoot::Multiple((subtree_depth, root))); + root_list.push((subtree_depth, root)); + expected_index += 1 << (subtree_depth - 1); + } else { + bail!( + "unexpected range root: expected={} get={}", + expected_index, + batch_index + ); + } + } + Ok(MerkleTreeInitialData { + subtree_list: root_list, + known_leaves: leaf_list, + }) + } + + fn truncate(&self, start_index: u64, batch_size: usize) -> crate::error::Result> { + let mut tx = self.kvdb.transaction(); + let mut start_batch_index = start_index / batch_size as u64; + let first_batch_offset = start_index as usize % batch_size; + let mut index_to_reseal = Vec::new(); + if first_batch_offset != 0 { + if let Some(mut first_batch) = self.get_entry_batch(start_batch_index)? { + index_to_reseal = first_batch + .truncate(first_batch_offset) + .into_iter() + .map(|x| start_batch_index as usize * SEALS_PER_LOAD + x as usize) + .collect(); + if !first_batch.is_empty() { + tx.put( + COL_ENTRY_BATCH, + &start_batch_index.to_be_bytes(), + &first_batch.as_ssz_bytes(), + ); + } else { + tx.delete(COL_ENTRY_BATCH, &start_batch_index.to_be_bytes()); + } + } + + start_batch_index += 1; + } + // TODO: `kvdb` and `kvdb-rocksdb` does not support `seek_to_last` yet. + // We'll need to fork it or use another wrapper for a better performance in this. + let end = match self.kvdb.iter(COL_ENTRY_BATCH).last() { + Some(Ok((k, _))) => decode_batch_index(k.as_ref())?, + Some(Err(e)) => { + error!("truncate db error: e={:?}", e); + return Err(e.into()); + } + None => { + // The db has no data, so we can just return; + return Ok(index_to_reseal); + } + }; + for batch_index in start_batch_index as usize..=end { + tx.delete(COL_ENTRY_BATCH, &batch_index.to_be_bytes()); + tx.delete_prefix(COL_ENTRY_BATCH_ROOT, &batch_index.to_be_bytes()); + } + self.kvdb.write(tx)?; + Ok(index_to_reseal) + } +} + +#[derive(DeriveEncode, DeriveDecode, Clone, Debug)] +#[ssz(enum_behaviour = "union")] +pub enum BatchRoot { + Single(DataRoot), + Multiple((usize, DataRoot)), +} + +/// Return the batch boundaries `(batch_start_index, batch_end_index)` given the index range. +pub fn batch_iter(start: u64, end: u64, batch_size: usize) -> Vec<(u64, u64)> { + let mut list = Vec::new(); + for i in (start / batch_size as u64 * batch_size as u64..end).step_by(batch_size) { + let batch_start = cmp::max(start, i); + let batch_end = cmp::min(end, i + batch_size as u64); + list.push((batch_start, batch_end)); + } + list +} + +fn try_decode_usize(data: &[u8]) -> Result { + Ok(usize::from_be_bytes( + data.try_into().map_err(|e| anyhow!("{:?}", e))?, + )) +} + +fn decode_batch_index(data: &[u8]) -> Result { + try_decode_usize(data) +} + +/// For the same batch_index, we want to process the larger subtree_depth first in iteration. +fn encode_batch_root_key(batch_index: usize, subtree_depth: usize) -> Vec { + let mut key = batch_index.to_be_bytes().to_vec(); + key.extend_from_slice(&(usize::MAX - subtree_depth).to_be_bytes()); + key +} + +fn decode_batch_root_key(data: &[u8]) -> Result<(usize, usize)> { + if data.len() != mem::size_of::() * 2 { + bail!("invalid data length"); + } + let batch_index = try_decode_usize(&data[..mem::size_of::()])?; + let subtree_depth = usize::MAX - try_decode_usize(&data[mem::size_of::()..])?; + Ok((batch_index, subtree_depth)) +} diff --git a/node/storage/src/log_store/load_chunk/bitmap.rs b/node/storage/src/log_store/load_chunk/bitmap.rs new file mode 100644 index 0000000..0269428 --- /dev/null +++ b/node/storage/src/log_store/load_chunk/bitmap.rs @@ -0,0 +1,175 @@ +use std::ops::{Deref, DerefMut}; + +use ssz::{Decode, DecodeError, Encode}; + +use bitmaps::{Bitmap, Bits, BitsImpl}; + +#[derive(Default, Debug)] +pub struct WrappedBitmap(pub Bitmap) +where + BitsImpl<{ N }>: Bits; +type PrimitveInner = as Bits>::Store; + +impl Encode for WrappedBitmap +where + BitsImpl<{ N }>: Bits, + PrimitveInner<{ N }>: Encode, +{ + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.append(&mut self.0.into_value().as_ssz_bytes()) + } + + fn ssz_bytes_len(&self) -> usize { + ::ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + as Encode>::ssz_fixed_len() + } +} + +impl Decode for WrappedBitmap +where + BitsImpl<{ N }>: Bits, + PrimitveInner<{ N }>: Decode, +{ + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + as Decode>::ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + Ok(Self(Bitmap::<{ N }>::from_value( + PrimitveInner::from_ssz_bytes(bytes)?, + ))) + } +} + +impl Deref for WrappedBitmap +where + BitsImpl<{ N }>: Bits, +{ + type Target = Bitmap<{ N }>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WrappedBitmap +where + BitsImpl<{ N }>: Bits, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub trait TruncateBitmap { + fn truncate(&mut self, index: u16); +} + +impl TruncateBitmap for u16 { + fn truncate(&mut self, index: u16) { + let mask: u16 = (1 << index) - 1; + *self &= mask + } +} + +impl TruncateBitmap for u32 { + fn truncate(&mut self, index: u16) { + let mask: u32 = (1 << index) - 1; + *self &= mask + } +} + +impl TruncateBitmap for u64 { + fn truncate(&mut self, index: u16) { + let mask: u64 = (1 << index) - 1; + *self &= mask + } +} + +impl TruncateBitmap for u128 { + fn truncate(&mut self, index: u16) { + let mask: u128 = (1 << index) - 1; + *self &= mask + } +} + +impl TruncateBitmap for [u128; N] { + fn truncate(&mut self, index: u16) { + let blob_index = index as usize / u128::BITS as usize; + let bit_index = index as usize % u128::BITS as usize; + let mask: u128 = (1 << (bit_index as u128)) - 1; + self[blob_index] &= mask; + for blob in &mut self[(blob_index + 1)..N] { + *blob = 0; + } + } +} + +impl WrappedBitmap +where + BitsImpl<{ N }>: Bits, + PrimitveInner<{ N }>: TruncateBitmap, +{ + /// Set the position large or equal to `index` to false + pub fn truncate(&mut self, index: u16) { + let mut current = *self.as_value(); + TruncateBitmap::truncate(&mut current, index); + self.0 = Bitmap::<{ N }>::from_value(current) + } +} + +#[test] +fn bitmap_serde() { + let mut bitmap = WrappedBitmap::<64>::default(); + bitmap.set(10, true); + bitmap.set(29, true); + + let serialized = bitmap.as_ssz_bytes(); + let deserialized = WrappedBitmap::<64>::from_ssz_bytes(&serialized).unwrap(); + assert_eq!(bitmap.into_value(), deserialized.into_value()); +} + +#[test] +fn bitmap_truncate() { + let mut bitmap = WrappedBitmap::<64>::default(); + bitmap.set(10, true); + bitmap.set(29, true); + bitmap.set(30, true); + bitmap.set(55, true); + + bitmap.truncate(30); + + assert!(bitmap.get(10)); + assert!(bitmap.get(29)); + assert!(!bitmap.get(30)); + assert!(!bitmap.get(55)); +} + +#[test] +fn bitmap_big_truncate() { + let mut bitmap = WrappedBitmap::<300>::default(); + bitmap.set(110, true); + bitmap.set(129, true); + bitmap.set(130, true); + bitmap.set(155, true); + bitmap.set(299, true); + + bitmap.truncate(130); + + assert!(bitmap.get(110)); + assert!(bitmap.get(129)); + assert!(!bitmap.get(130)); + assert!(!bitmap.get(155)); + assert!(!bitmap.get(299)); +} diff --git a/node/storage/src/log_store/load_chunk/chunk_data.rs b/node/storage/src/log_store/load_chunk/chunk_data.rs new file mode 100644 index 0000000..8131607 --- /dev/null +++ b/node/storage/src/log_store/load_chunk/chunk_data.rs @@ -0,0 +1,497 @@ +use anyhow::{bail, Result}; +use zgs_spec::{BYTES_PER_LOAD, BYTES_PER_SECTOR, SECTORS_PER_LOAD, SECTORS_PER_SEAL}; +use shared_types::{bytes_to_chunks, DataRoot}; +use ssz_derive::{Decode, Encode}; +use std::fmt::{Debug, Formatter}; +use std::mem; +use tracing::error; + +pub enum EntryBatchData { + Complete(Vec), + /// All `PartialBatch`s are ordered based on `start_index`. + Incomplete(IncompleteData), +} + +#[derive(Default, Debug, Encode, Decode)] +pub struct IncompleteData { + pub subtrees: Vec, + pub known_data: Vec, +} + +impl IncompleteData { + fn truncate(&mut self, sector_index: usize) { + let partial_batch_truncate = match PartialBatch::find(&self.known_data, sector_index) { + Ok(x) => { + let p = &mut self.known_data[x]; + let truncated_byte = (sector_index - p.start_sector) * BYTES_PER_SECTOR; + p.data.truncate(truncated_byte); + if p.data.is_empty() { + x + } else { + x + 1 + } + } + Err(x) => x, + }; + self.known_data.truncate(partial_batch_truncate); + + let subtree_truncate = match Subtree::find(&self.subtrees, sector_index) { + Ok(x) => x, + Err(x) => x, + }; + self.subtrees.truncate(subtree_truncate); + } + + /// This is only called once when the batch is removed from the memory and fully stored in db. + pub fn set_subtree_list(&mut self, subtree_list: Vec) { + self.subtrees = subtree_list; + } + + pub fn get(&self, mut start_byte: usize, length_byte: usize) -> Option<&[u8]> { + let p = &self.known_data + [PartialBatch::find(&self.known_data, start_byte / BYTES_PER_SECTOR).ok()?]; + // Rebase the start_byte and end_byte w.r.t. to hit partial batch. + start_byte -= p.start_sector * BYTES_PER_SECTOR; + p.data.get(start_byte..(start_byte + length_byte)) + } +} + +#[derive(Default, Debug, Encode, Decode)] +pub struct Subtree { + pub start_sector: usize, + pub subtree_height: usize, + pub root: DataRoot, +} + +#[derive(PartialEq, Eq)] +pub struct PartialBatch { + /// Offset in this batch. + pub(super) start_sector: usize, + pub(super) data: Vec, +} + +impl Debug for PartialBatch { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "PartialBatch: start_offset={} data_len={}", + self.start_sector, + self.data.len() + ) + } +} + +trait DataRange: Sized { + fn start_sector(&self) -> usize; + fn size(&self) -> usize; + + fn end_sector(&self) -> usize { + self.start_sector() + self.size() + } + + fn find(data_list: &[Self], sector_index: usize) -> Result { + let possible_index = + match data_list.binary_search_by_key(§or_index, |x| x.start_sector()) { + Ok(x) => x, + Err(0) => { + return Err(0); + } + Err(x) => x - 1, + }; + if sector_index < data_list[possible_index].end_sector() { + Ok(possible_index) + } else { + Err(possible_index + 1) + } + } +} + +impl DataRange for PartialBatch { + fn start_sector(&self) -> usize { + self.start_sector + } + + fn size(&self) -> usize { + bytes_to_chunks(self.data.len()) + } +} + +impl DataRange for Subtree { + fn start_sector(&self) -> usize { + self.start_sector + } + + fn size(&self) -> usize { + 1 << (self.subtree_height - 1) + } +} + +impl EntryBatchData { + pub fn new() -> Self { + EntryBatchData::Incomplete(IncompleteData { + subtrees: vec![], + known_data: vec![], + }) + } + + pub fn is_empty(&self) -> bool { + matches!(self,EntryBatchData::Incomplete(x) if x.known_data.is_empty()) + } + + pub fn get(&self, start_byte: usize, length_byte: usize) -> Option<&[u8]> { + assert!(start_byte + length_byte <= BYTES_PER_LOAD); + + match self { + EntryBatchData::Complete(data) => data.get(start_byte..(start_byte + length_byte)), + EntryBatchData::Incomplete(data) => data.get(start_byte, length_byte), + } + } + + pub fn get_mut(&mut self, mut start_byte: usize, length_byte: usize) -> Option<&mut [u8]> { + assert!(start_byte + length_byte <= BYTES_PER_LOAD); + + match self { + EntryBatchData::Complete(data) => data.get_mut(start_byte..(start_byte + length_byte)), + EntryBatchData::Incomplete(data) => { + let index = + PartialBatch::find(&data.known_data, start_byte / BYTES_PER_SECTOR).ok()?; + let p = &mut data.known_data[index]; + + // Rebase the start_byte and end_byte w.r.t. to hit partial batch. + start_byte -= p.start_sector * BYTES_PER_SECTOR; + + p.data.get_mut(start_byte..(start_byte + length_byte)) + } + } + } + + pub fn truncate(&mut self, truncated_byte: usize) { + assert!(truncated_byte % BYTES_PER_SECTOR == 0); + *self = match self { + EntryBatchData::Complete(data) => { + data.truncate(truncated_byte); + let known_data = vec![PartialBatch { + start_sector: 0, + data: std::mem::take(data), + }]; + EntryBatchData::Incomplete(IncompleteData { + subtrees: vec![], + known_data, + }) + } + EntryBatchData::Incomplete(batch_list) => { + batch_list.truncate(truncated_byte / BYTES_PER_SECTOR); + EntryBatchData::Incomplete(std::mem::take(batch_list)) + } + }; + } + + pub fn insert_data(&mut self, start_byte: usize, mut data: Vec) -> Result> { + assert!(start_byte % BYTES_PER_SECTOR == 0); + assert!(data.len() % BYTES_PER_SECTOR == 0); + + if data.is_empty() || self.get(start_byte, data.len()) == Some(&data) { + // TODO(zz): This assumes the caller has processed chain reorg (truncate flow) before + // inserting new data, and the data of the same file are always inserted with the + // same pattern. + return Ok(vec![]); + } + + // Check if the entry is completed + let (list, subtree_list) = if let EntryBatchData::Incomplete(x) = self { + (&mut x.known_data, &mut x.subtrees) + } else { + bail!( + "overwriting a completed PoRA Chunk with conflict data, start_byte={}", + start_byte + ); + }; + + let start_sector = start_byte / BYTES_PER_SECTOR; + let end_sector = start_sector + data.len() / BYTES_PER_SECTOR; + let length_sector = data.len() / BYTES_PER_SECTOR; + + // Check if the entry is completed + let start_insert_position = match PartialBatch::find(list, start_sector) { + Ok(x) => { + bail!( + "start position overlapped with existing batch: start {}, len {}", + list[x].start_sector, + list[x].data.len() + ); + } + Err(x) => x, + }; + + let end_insert_position = match PartialBatch::find(list, end_sector - 1) { + Ok(x) => { + bail!( + "end position overlapped with existing batch: start {}, len {}", + list[x].start_sector, + list[x].data.len() + ); + } + Err(x) => x, + }; + + let position = if start_insert_position != end_insert_position { + bail!("data overlapped with existing batches"); + } else { + start_insert_position + }; + + let merge_prev = position != 0 && start_sector == list[position - 1].end_sector(); + let merge_next = position != list.len() && end_sector == list[position].start_sector; + + let updated_segment = match (merge_prev, merge_next) { + (false, false) => { + list.insert(position, PartialBatch { start_sector, data }); + &list[position] + } + (true, false) => { + list[position - 1].data.append(&mut data); + &list[position - 1] + } + (false, true) => { + data.append(&mut list[position].data); + list[position] = PartialBatch { start_sector, data }; + &list[position] + } + (true, true) => { + // Merge the new data with the two around partial batches to + // a single one. + list[position - 1].data.append(&mut data); + let mut next = list.remove(position); + list[position - 1].data.append(&mut next.data); + &list[position - 1] + } + }; + + // Remove completed subtrees. + let updated_end_sector = + updated_segment.start_sector + updated_segment.data.len() / BYTES_PER_SECTOR; + let start_subtree_index = match subtree_list + .binary_search_by_key(&updated_segment.start_sector, |tree| tree.start_sector) + { + Ok(x) => x, + Err(x) => x, + }; + for subtree_index in start_subtree_index..subtree_list.len() { + assert!(subtree_list[subtree_index].start_sector >= updated_segment.start_sector); + if subtree_list[subtree_index].end_sector() > updated_end_sector { + subtree_list.drain(start_subtree_index..subtree_index); + break; + } + } + + // Find which seal chunks are made intact by this submission. + // It will be notified to the sealer later. + let intact_seal_idxs = get_intact_sealing_index( + updated_segment.start_sector, + updated_segment.data.len() / BYTES_PER_SECTOR, + ); + + // TODO(zz): Use config here? + if list.len() == 1 + && list[0].start_sector == 0 + && bytes_to_chunks(list[0].data.len()) == SECTORS_PER_LOAD + { + // All data in this batch have been filled. + *self = EntryBatchData::Complete(mem::take(&mut list[0].data)); + } + + let ready_for_seal_idxs: Vec = get_covered_sealing_index(start_sector, length_sector) + .filter(|x| intact_seal_idxs.contains(x)) + .collect(); + + Ok(ready_for_seal_idxs) + } + + pub(super) fn available_range_entries(&self) -> Vec<(usize, usize)> { + match self { + EntryBatchData::Complete(data) => { + vec![(0, data.len() / BYTES_PER_SECTOR)] + } + EntryBatchData::Incomplete(batch_list) => batch_list + .known_data + .iter() + .map(|b| (b.start_sector, b.data.len() / BYTES_PER_SECTOR)) + .collect(), + } + } + + /// This is only called once when the batch is removed from the memory and fully stored in db. + pub fn set_subtree_list(&mut self, subtree_list: Vec<(usize, usize, DataRoot)>) { + let subtree_list: Vec = subtree_list + .into_iter() + .filter(|(start_sector, subtree_height, root)| { + self.get(*start_sector * BYTES_PER_SECTOR, (1 << (*subtree_height - 1)) * BYTES_PER_SECTOR) + .is_none() + // The first sector is regarded as known. + && *root != DataRoot::zero() + }) + .map(|(start_sector, subtree_height, root)| Subtree { + start_sector, + subtree_height, + root, + }) + .collect(); + match self { + EntryBatchData::Complete(_) => { + // This should not happen if the data in memory matches the data in DB. + if !subtree_list.is_empty() { + error!(?subtree_list, "store subtrees for a completed chunk"); + } + } + EntryBatchData::Incomplete(data) => data.set_subtree_list(subtree_list), + } + } + + pub fn get_subtree_list(&self) -> &[Subtree] { + match self { + EntryBatchData::Complete(_) => &[], + EntryBatchData::Incomplete(data) => &data.subtrees, + } + } +} + +fn get_intact_sealing_index(start_sector: usize, length_sector: usize) -> std::ops::Range { + // Inclusive + let start_index = ((start_sector + SECTORS_PER_SEAL - 1) / SECTORS_PER_SEAL) as u16; + // Exclusive + let end_index = ((start_sector + length_sector) / SECTORS_PER_SEAL) as u16; + start_index..end_index +} + +fn get_covered_sealing_index(start_sector: usize, length_sector: usize) -> std::ops::Range { + // Inclusive + let start_index = (start_sector / SECTORS_PER_SEAL) as u16; + // Exclusive + let end_index = + ((start_sector + length_sector + SECTORS_PER_SEAL - 1) / SECTORS_PER_SEAL) as u16; + start_index..end_index +} + +#[cfg(test)] +mod tests { + use crate::log_store::load_chunk::chunk_data::PartialBatch; + + use super::EntryBatchData; + use zgs_spec::{BYTES_PER_LOAD, BYTES_PER_SECTOR, SECTORS_PER_LOAD}; + use rand::{rngs::StdRng, RngCore, SeedableRng}; + + fn test_data() -> Vec { + let mut data = vec![0u8; BYTES_PER_LOAD]; + let mut random = StdRng::seed_from_u64(73); + random.fill_bytes(&mut data); + data + } + + #[test] + fn test_data_chunk_insert() { + let data = test_data(); + let mut chunk_batch = EntryBatchData::new(); + + for i in [2usize, 0, 1, 3].into_iter() { + chunk_batch + .insert_data( + BYTES_PER_LOAD / 4 * i, + data[(BYTES_PER_LOAD / 4) * i..(BYTES_PER_LOAD / 4) * (i + 1)].to_vec(), + ) + .unwrap(); + } + + assert!(matches!(chunk_batch, EntryBatchData::Complete(_))); + } + + #[test] + fn test_data_chunk_truncate() { + let data = test_data(); + let mut chunk_batch = EntryBatchData::new(); + + for i in [3, 1].into_iter() { + chunk_batch + .insert_data( + BYTES_PER_LOAD / 4 * i, + data[(BYTES_PER_LOAD / 4) * i..(BYTES_PER_LOAD / 4) * (i + 1)].to_vec(), + ) + .unwrap(); + } + + chunk_batch.truncate(BYTES_PER_LOAD / 4 * 3 + BYTES_PER_SECTOR); + + let chunks = if let EntryBatchData::Incomplete(chunks) = chunk_batch { + chunks + } else { + unreachable!(); + }; + + assert!(chunks.known_data.len() == 2); + assert!( + chunks.known_data[0] + == PartialBatch { + start_sector: SECTORS_PER_LOAD / 4, + data: data[(BYTES_PER_LOAD / 4)..(BYTES_PER_LOAD / 4) * 2].to_vec() + } + ); + assert!( + chunks.known_data[1] + == PartialBatch { + start_sector: SECTORS_PER_LOAD / 4 * 3, + data: data[BYTES_PER_LOAD / 4 * 3..BYTES_PER_LOAD / 4 * 3 + BYTES_PER_SECTOR] + .to_vec() + } + ); + } + + #[test] + fn test_data_chunk_get_slice() { + let data = test_data(); + let mut chunk_batch = EntryBatchData::new(); + + const N: usize = BYTES_PER_LOAD; + const B: usize = N / 16; + + // Skip batch 5,7,10,11 + for i in [3, 8, 12, 15, 6, 1, 4, 13, 0, 2, 9, 14].into_iter() { + chunk_batch + .insert_data(B * i, data[B * i..B * (i + 1)].to_vec()) + .unwrap(); + assert_eq!( + chunk_batch.get(B * i, B).unwrap(), + &data[B * i..B * (i + 1)] + ); + assert_eq!( + chunk_batch.get_mut(B * i, B).unwrap(), + &data[B * i..B * (i + 1)] + ); + } + + const S: usize = B / BYTES_PER_SECTOR; + assert_eq!( + chunk_batch.available_range_entries(), + vec![(0, 5 * S), (6 * S, S), (8 * S, 2 * S), (12 * S, 4 * S)] + ); + + assert_eq!(chunk_batch.get(B * 8, B * 2).unwrap(), &data[B * 8..B * 10]); + assert_eq!( + chunk_batch.get_mut(B * 8, B * 2).unwrap(), + &data[B * 8..B * 10] + ); + + assert_eq!(chunk_batch.get(0, B * 4).unwrap(), &data[0..B * 4]); + assert_eq!(chunk_batch.get_mut(0, B * 4).unwrap(), &data[0..B * 4]); + + assert!(chunk_batch.get(0, B * 5 + 32).is_none()); + assert!(chunk_batch.get_mut(0, B * 5 + 32).is_none()); + + assert!(chunk_batch.get(B * 7 - 32, B + 32).is_none()); + assert!(chunk_batch.get_mut(B * 7 - 32, B + 32).is_none()); + + assert!(chunk_batch.get(B * 7, B + 32).is_none()); + assert!(chunk_batch.get_mut(B * 7, B + 32).is_none()); + + assert!(chunk_batch.get(B * 12 - 32, B + 32).is_none()); + assert!(chunk_batch.get_mut(B * 12 - 32, B + 32).is_none()); + } +} diff --git a/node/storage/src/log_store/load_chunk/mod.rs b/node/storage/src/log_store/load_chunk/mod.rs new file mode 100644 index 0000000..7bc240f --- /dev/null +++ b/node/storage/src/log_store/load_chunk/mod.rs @@ -0,0 +1,425 @@ +mod bitmap; +mod chunk_data; +mod seal; +mod serde; + +use std::cmp::min; + +use anyhow::Result; +use ethereum_types::H256; +use ssz_derive::{Decode, Encode}; + +use crate::log_store::log_manager::data_to_merkle_leaves; +use crate::try_option; +use append_merkle::{Algorithm, MerkleTreeRead, Sha3Algorithm}; +use zgs_spec::{ + BYTES_PER_LOAD, BYTES_PER_SEAL, BYTES_PER_SECTOR, SEALS_PER_LOAD, SECTORS_PER_LOAD, + SECTORS_PER_SEAL, +}; +use shared_types::{ChunkArray, DataRoot, Merkle}; +use tracing::trace; + +use super::SealAnswer; +use chunk_data::EntryBatchData; +use seal::SealInfo; + +#[derive(Encode, Decode)] +pub struct EntryBatch { + seal: SealInfo, + // the inner data + data: EntryBatchData, +} + +impl EntryBatch { + pub fn new(load_index_global: u64) -> Self { + Self { + seal: SealInfo::new(load_index_global), + data: EntryBatchData::new(), + } + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } +} + +impl EntryBatch { + pub fn get_sealed_data(&self, seal_index: u16) -> Option<[u8; BYTES_PER_SEAL]> { + if self.seal.is_sealed(seal_index) { + let loaded_slice = self + .data + .get(seal_index as usize * BYTES_PER_SEAL, BYTES_PER_SEAL)?; + Some(loaded_slice.try_into().unwrap()) + } else { + None + } + } + + pub fn get_non_sealed_data(&self, seal_index: u16) -> Option<[u8; BYTES_PER_SEAL]> { + if !self.seal.is_sealed(seal_index) { + let loaded_slice = self + .data + .get(seal_index as usize * BYTES_PER_SEAL, BYTES_PER_SEAL)?; + Some(loaded_slice.try_into().unwrap()) + } else { + None + } + } + + /// Get unsealed data + pub fn get_unsealed_data(&self, start_sector: usize, length_sector: usize) -> Option> { + // If the start position is not aligned and is sealed, we need to load one more word (32 bytes) for unsealing + let advanced_by_one = if start_sector % SECTORS_PER_SEAL == 0 { + // If the start position is not aligned, it is no need to load one more word + false + } else { + // otherwise, it depends on if the given offset is seal + self.seal + .is_sealed((start_sector / SECTORS_PER_SEAL) as u16) + }; + + let start_byte = start_sector * BYTES_PER_SECTOR; + let length_byte = length_sector * BYTES_PER_SECTOR; + + // Load data slice with the word for unsealing + let (mut loaded_data, unseal_mask_seed) = if advanced_by_one { + let loaded_data_with_hint = self.data.get(start_byte - 32, length_byte + 32)?; + + // TODO (api stable): use `split_array_ref` instead when this api is stable. + let (unseal_mask_seed, loaded_data) = loaded_data_with_hint.split_at(32); + let unseal_mask_seed = <[u8; 32]>::try_from(unseal_mask_seed).unwrap(); + (loaded_data.to_vec(), Some(unseal_mask_seed)) + } else { + (self.data.get(start_byte, length_byte)?.to_vec(), None) + }; + + let incomplete_seal_chunk_length = (BYTES_PER_LOAD - start_byte) % BYTES_PER_SEAL; + + // Unseal the first incomplete sealing chunk (if exists) + if let Some(unseal_mask_seed) = unseal_mask_seed { + let data_to_unseal = if loaded_data.len() < incomplete_seal_chunk_length { + // The loaded data does not cross sealings + loaded_data.as_mut() + } else { + loaded_data[..incomplete_seal_chunk_length].as_mut() + }; + + zgs_seal::unseal_with_mask_seed(data_to_unseal, unseal_mask_seed); + } + + if loaded_data.len() > incomplete_seal_chunk_length { + let complete_chunks = &mut loaded_data[incomplete_seal_chunk_length..]; + let start_seal = (start_byte + incomplete_seal_chunk_length) / BYTES_PER_SEAL; + + for (seal_index, data_to_unseal) in complete_chunks + .chunks_mut(BYTES_PER_SEAL) + .enumerate() + .map(|(idx, chunk)| (start_seal + idx, chunk)) + { + self.seal.unseal(data_to_unseal, seal_index as u16); + } + } + + Some(loaded_data) + } + + /// Return `Error` if the new data overlaps with old data. + /// Convert `Incomplete` to `Completed` if the chunk is completed after the insertion. + pub fn insert_data(&mut self, offset: usize, data: Vec) -> Result> { + self.data.insert_data(offset * BYTES_PER_SECTOR, data) + } + + pub fn truncate(&mut self, truncated_sector: usize) -> Vec { + assert!(truncated_sector > 0 && truncated_sector < SECTORS_PER_LOAD); + + self.data.truncate(truncated_sector * BYTES_PER_SECTOR); + self.truncate_seal(truncated_sector) + } + + pub fn into_data_list(self, global_start_entry: u64) -> Vec { + self.data + .available_range_entries() + .into_iter() + .map(|(start_entry, length_entry)| ChunkArray { + data: self + .get_unsealed_data(start_entry, length_entry) + .unwrap() + .to_vec(), + start_index: global_start_entry + start_entry as u64, + }) + .collect() + } + + fn truncate_seal(&mut self, truncated_sector: usize) -> Vec { + let reverted_seal_index = (truncated_sector / SECTORS_PER_SEAL) as u16; + + let first_unseal_index = self.seal.truncated_seal_index(reverted_seal_index); + let last_unseal_index = ((truncated_sector - 1) / SECTORS_PER_SEAL) as u16; + + let mut to_reseal_set = Vec::with_capacity(SEALS_PER_LOAD); + + for unseal_index in first_unseal_index..=last_unseal_index { + if !self.seal.is_sealed(unseal_index) { + continue; + } + + let truncated_byte = truncated_sector * BYTES_PER_SECTOR; + let first_unseal_byte = unseal_index as usize * BYTES_PER_SEAL; + let length = min(truncated_byte - first_unseal_byte, BYTES_PER_SEAL); + let to_unseal = self + .data + .get_mut(first_unseal_byte, length) + .expect("Sealed chunk should be complete"); + self.seal.unseal(to_unseal, unseal_index); + + to_reseal_set.push(unseal_index) + } + + // truncate the bitmap + self.seal.truncate(reverted_seal_index); + + to_reseal_set + } + + pub fn build_root(&self, is_first_chunk: bool) -> Result> { + // Fast check if an incomplete chunk is a full chunk. + if let EntryBatchData::Incomplete(d) = &self.data { + if self.get_unsealed_data(SECTORS_PER_LOAD - 1, 1).is_none() { + if let Some(last_subtree) = d.subtrees.last() { + if last_subtree.start_sector + (1 << (last_subtree.subtree_height - 1)) + != SECTORS_PER_LOAD + { + return Ok(None); + } + } else { + return Ok(None); + } + } + } + Ok(Some( + *try_option!(self.to_merkle_tree(is_first_chunk)?).root(), + )) + } + + pub fn submit_seal_result(&mut self, answer: SealAnswer) -> Result<()> { + let local_seal_index = answer.seal_index as usize % SEALS_PER_LOAD; + assert!( + !self.seal.is_sealed(local_seal_index as u16), + "Duplicated sealing" + ); + assert_eq!( + answer.seal_index / SEALS_PER_LOAD as u64, + self.seal.load_index() + ); + + self.seal.set_seal_context( + answer.seal_context, + answer.context_end_seal, + answer.miner_id, + ); + let sealing_segment = self + .data + .get_mut(local_seal_index * BYTES_PER_SEAL, BYTES_PER_SEAL) + .expect("Sealing segment should exist"); + + sealing_segment.copy_from_slice(&answer.sealed_data); + self.seal.mark_sealed(local_seal_index as u16); + + Ok(()) + } + + /// This is only called once when the batch is removed from the memory and fully stored in db. + pub fn set_subtree_list(&mut self, subtree_list: Vec<(usize, usize, DataRoot)>) { + self.data.set_subtree_list(subtree_list) + } + + pub fn to_merkle_tree(&self, is_first_chunk: bool) -> Result> { + let initial_leaves = if is_first_chunk { + vec![H256::zero()] + } else { + vec![] + }; + let mut merkle = Merkle::new(initial_leaves, 0, None); + for subtree in self.data.get_subtree_list() { + trace!(?subtree, "get subtree, leaves={}", merkle.leaves()); + if subtree.start_sector != merkle.leaves() { + let leaf_data = try_option!( + self.get_unsealed_data(merkle.leaves(), subtree.start_sector - merkle.leaves()) + ); + merkle.append_list(data_to_merkle_leaves(&leaf_data).expect("aligned")); + } + merkle.append_subtree(subtree.subtree_height, subtree.root)?; + } + if merkle.leaves() != SECTORS_PER_LOAD { + let leaf_data = try_option!( + self.get_unsealed_data(merkle.leaves(), SECTORS_PER_LOAD - merkle.leaves()) + ); + merkle.append_list(data_to_merkle_leaves(&leaf_data).expect("aligned")); + } + // TODO(zz): Optimize. + for index in 0..merkle.leaves() { + if merkle.leaf_at(index)?.is_none() { + if let Some(leaf_data) = self.get_unsealed_data(index, 1) { + merkle.fill_leaf(index, Sha3Algorithm::leaf(&leaf_data)); + } + } + } + Ok(Some(merkle)) + } +} + +#[cfg(test)] +mod tests { + use super::{EntryBatch, SealAnswer}; + use ethereum_types::H256; + use zgs_spec::{ + BYTES_PER_SEAL, BYTES_PER_SECTOR, SEALS_PER_LOAD, SECTORS_PER_LOAD, SECTORS_PER_SEAL, + }; + const LOAD_INDEX: u64 = 1; + fn seal( + batch: &mut EntryBatch, + seal_index: u16, + context_digest: H256, + context_end_seal_local: u64, + ) { + let miner_id = H256([33u8; 32]); + let mut data = batch.get_non_sealed_data(seal_index).unwrap(); + zgs_seal::seal( + &mut data, + &miner_id, + &context_digest, + LOAD_INDEX * SECTORS_PER_LOAD as u64 + seal_index as u64 * SECTORS_PER_SEAL as u64, + ); + batch + .submit_seal_result(SealAnswer { + seal_index: LOAD_INDEX * SEALS_PER_LOAD as u64 + seal_index as u64, + version: 0, + sealed_data: data, + miner_id, + seal_context: context_digest, + context_end_seal: LOAD_INDEX * SEALS_PER_LOAD as u64 + context_end_seal_local, + }) + .unwrap(); + } + + #[test] + fn test_seal_single() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL]).unwrap(); + + const DIGEST: H256 = H256([22u8; 32]); + seal(&mut batch, 0, DIGEST, 1); + + assert_eq!( + batch.get_unsealed_data(0, SECTORS_PER_SEAL).unwrap(), + vec![11; SECTORS_PER_SEAL * BYTES_PER_SECTOR] + ); + assert_eq!( + batch.get_unsealed_data(1, SECTORS_PER_SEAL - 1).unwrap(), + vec![11; (SECTORS_PER_SEAL - 1) * BYTES_PER_SECTOR] + ); + } + + fn check_two_seals(batch: &EntryBatch) { + assert_eq!( + batch.get_unsealed_data(0, SECTORS_PER_SEAL).unwrap(), + vec![11; SECTORS_PER_SEAL * BYTES_PER_SECTOR] + ); + assert_eq!( + batch + .get_unsealed_data(SECTORS_PER_SEAL, SECTORS_PER_SEAL) + .unwrap(), + vec![11; SECTORS_PER_SEAL * BYTES_PER_SECTOR] + ); + assert_eq!( + batch.get_unsealed_data(1, SECTORS_PER_SEAL - 1).unwrap(), + vec![11; (SECTORS_PER_SEAL - 1) * BYTES_PER_SECTOR] + ); + assert_eq!( + batch.get_unsealed_data(1, SECTORS_PER_SEAL).unwrap(), + vec![11; SECTORS_PER_SEAL * BYTES_PER_SECTOR] + ); + assert_eq!( + batch + .get_unsealed_data(1, 2 * SECTORS_PER_SEAL - 1) + .unwrap(), + vec![11; (2 * SECTORS_PER_SEAL - 1) * BYTES_PER_SECTOR] + ); + } + + #[test] + fn test_seal_mono_context() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + const DIGEST: H256 = H256([22u8; 32]); + seal(&mut batch, 0, DIGEST, 2); + seal(&mut batch, 1, DIGEST, 2); + + check_two_seals(&batch); + } + + #[test] + fn test_seal_mono_context_reorder() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + const DIGEST: H256 = H256([22u8; 32]); + seal(&mut batch, 1, DIGEST, 2); + seal(&mut batch, 0, DIGEST, 2); + + check_two_seals(&batch); + } + + #[test] + fn test_seal_mono_context_partial() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + const DIGEST: H256 = H256([22u8; 32]); + seal(&mut batch, 1, DIGEST, 2); + + check_two_seals(&batch); + } + + #[test] + fn test_seal_hete_context() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + const DIGEST0: H256 = H256([22u8; 32]); + const DIGEST1: H256 = H256([33u8; 32]); + + seal(&mut batch, 0, DIGEST0, 1); + seal(&mut batch, 1, DIGEST1, 2); + + check_two_seals(&batch); + } + + #[test] + fn test_seal_hete_context_reord() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + const DIGEST0: H256 = H256([22u8; 32]); + const DIGEST1: H256 = H256([33u8; 32]); + + seal(&mut batch, 1, DIGEST1, 2); + seal(&mut batch, 0, DIGEST0, 1); + + check_two_seals(&batch); + } + + #[test] + fn test_seal_hete_context_partial() { + let mut batch = EntryBatch::new(LOAD_INDEX); + batch.insert_data(0, vec![11; BYTES_PER_SEAL * 2]).unwrap(); + + // const DIGEST0: H256 = H256([22u8; 32]); + const DIGEST1: H256 = H256([33u8; 32]); + + seal(&mut batch, 1, DIGEST1, 2); + + check_two_seals(&batch); + } +} diff --git a/node/storage/src/log_store/load_chunk/seal.rs b/node/storage/src/log_store/load_chunk/seal.rs new file mode 100644 index 0000000..f2c37d2 --- /dev/null +++ b/node/storage/src/log_store/load_chunk/seal.rs @@ -0,0 +1,303 @@ +use ethereum_types::H256; +use ssz_derive::{Decode as DeriveDecode, Encode as DeriveEncode}; +use static_assertions::const_assert; + +use zgs_seal; +use zgs_spec::{SEALS_PER_LOAD, SECTORS_PER_LOAD, SECTORS_PER_SEAL}; + +use super::bitmap::WrappedBitmap; + +#[derive(DeriveEncode, DeriveDecode)] +pub struct SealContextInfo { + /// The context digest for this seal group + context_digest: H256, + /// The end position (exclusive) indexed by sectors + end_seal_index: u16, +} + +type ChunkSealBitmap = WrappedBitmap; +const_assert!(SEALS_PER_LOAD <= u128::BITS as usize); + +#[derive(Default, DeriveEncode, DeriveDecode)] +pub struct SealInfo { + // a bitmap specify which sealing chunks have been sealed + bitmap: ChunkSealBitmap, + // the batch_offset (seal chunks) of the EntryBatch this seal info belongs to + load_index: u64, + // the miner Id for sealing this chunk, zero representing doesn't exists + miner_id: H256, + // seal context information, indexed by u16. Get a position has never been set is undefined behaviour. + seal_contexts: Vec, +} + +// Basic interfaces +impl SealInfo { + pub fn new(load_index: u64) -> Self { + Self { + load_index, + ..Default::default() + } + } + + pub fn is_sealed(&self, seal_index: u16) -> bool { + self.bitmap.get(seal_index as usize) + } + + pub fn mark_sealed(&mut self, seal_index: u16) { + self.bitmap.set(seal_index as usize, true); + } + + pub fn load_index(&self) -> u64 { + self.load_index + } + + pub fn global_seal_sector(&self, index: u16) -> u64 { + (self.load_index as usize * SECTORS_PER_LOAD + index as usize * SECTORS_PER_SEAL) as u64 + } +} + +// Interfaces for maintaining context info +impl SealInfo { + fn context_index(&self, seal_index: u16) -> usize { + match self + .seal_contexts + .binary_search_by_key(&(seal_index + 1), |x| x.end_seal_index) + { + Ok(x) | Err(x) => x, + } + } + + pub fn get_seal_context_digest(&self, seal_index: u16) -> Option { + self.seal_contexts + .get(self.context_index(seal_index)) + .map(|x| x.context_digest) + } + + pub fn set_seal_context( + &mut self, + context_digest: H256, + global_end_seal_index: u64, + miner_id: H256, + ) { + // 1. Check consistency of the miner id. + if self.miner_id.is_zero() { + self.miner_id = miner_id; + } else { + assert!( + self.miner_id == miner_id, + "miner_id setting is inconsistent with db" + ); + } + + // 2. Compute the local end_seal_index + let end_seal_index = global_end_seal_index - self.load_index * SEALS_PER_LOAD as u64; + let end_seal_index = std::cmp::min(end_seal_index as u16, SEALS_PER_LOAD as u16); + let new_context = SealContextInfo { + context_digest, + end_seal_index, + }; + + // 3. Update the seal context array by cases + let insert_position = self.context_index(end_seal_index - 1); + + if let Some(existing_context) = self.seal_contexts.get(insert_position) { + if existing_context.context_digest == new_context.context_digest { + // Case 1: the new context is consistent with existing contexts (nothing to do) + } else { + // Case 2: the new context should be inserted in the middle (may not happen) + self.seal_contexts.insert(insert_position, new_context); + } + } else { + // Case 3: the new context exceeds the upper bound of existing contexts + self.seal_contexts.push(new_context); + } + } +} + +impl SealInfo { + pub fn truncate(&mut self, reverted_seal_index: u16) { + // TODO (kevin): have issue in some cases + let truncated_context_index = self.context_index(reverted_seal_index); + let truncated_seal_index = self.truncated_seal_index(reverted_seal_index); + + self.bitmap.truncate(truncated_seal_index); + self.seal_contexts.truncate(truncated_context_index); + } + + pub fn truncated_seal_index(&self, reverted_seal_index: u16) -> u16 { + let truncated_context = self.context_index(reverted_seal_index); + if truncated_context == 0 { + 0 + } else { + self.seal_contexts + .get(truncated_context - 1) + .unwrap() + .end_seal_index + } + } +} + +impl SealInfo { + pub fn unseal(&self, data: &mut [u8], index: u16) { + if !self.is_sealed(index) { + return; + } + let seal_context = self + .get_seal_context_digest(index) + .expect("cannot unseal non-sealed data"); + zgs_seal::unseal( + data, + &self.miner_id, + &seal_context, + self.global_seal_sector(index), + ); + } + + #[cfg(test)] + pub fn seal(&self, data: &mut [u8], index: u16) { + if self.is_sealed(index) { + return; + } + let seal_context = self + .get_seal_context_digest(index) + .expect("cannot unseal non-sealed data"); + zgs_seal::seal( + data, + &self.miner_id, + &seal_context, + self.global_seal_sector(index), + ); + } +} + +#[cfg(test)] +mod tests { + use ethereum_types::H256; + use hex_literal::hex; + use zgs_seal; + use zgs_spec::BYTES_PER_SEAL; + use rand::{rngs::StdRng, RngCore, SeedableRng}; + + use super::{SealContextInfo, SealInfo}; + + const TEST_MINER_ID: H256 = H256(hex!( + "003d82782c78262bada18a22f5f982d2b43934d5541e236ca3781ddc8c911cb8" + )); + + #[test] + fn get_seal_context() { + let mut random = StdRng::seed_from_u64(149); + + let mut context1 = H256::default(); + let mut context2 = H256::default(); + let mut context3 = H256::default(); + random.fill_bytes(&mut context1.0); + random.fill_bytes(&mut context2.0); + random.fill_bytes(&mut context3.0); + + let mut sealer = SealInfo::new(0); + sealer.seal_contexts.push(SealContextInfo { + context_digest: context1, + end_seal_index: 2, + }); + sealer.seal_contexts.push(SealContextInfo { + context_digest: context2, + end_seal_index: 3, + }); + sealer.seal_contexts.push(SealContextInfo { + context_digest: context3, + end_seal_index: 6, + }); + + assert_eq!(sealer.get_seal_context_digest(0), Some(context1)); + assert_eq!(sealer.get_seal_context_digest(1), Some(context1)); + assert_eq!(sealer.get_seal_context_digest(2), Some(context2)); + assert_eq!(sealer.get_seal_context_digest(3), Some(context3)); + assert_eq!(sealer.get_seal_context_digest(4), Some(context3)); + assert_eq!(sealer.get_seal_context_digest(5), Some(context3)); + assert_eq!(sealer.get_seal_context_digest(6), None); + + assert_eq!(sealer.truncated_seal_index(0), 0); + assert_eq!(sealer.truncated_seal_index(1), 0); + assert_eq!(sealer.truncated_seal_index(2), 2); + assert_eq!(sealer.truncated_seal_index(3), 3); + assert_eq!(sealer.truncated_seal_index(4), 3); + assert_eq!(sealer.truncated_seal_index(5), 3); + assert_eq!(sealer.truncated_seal_index(6), 6); + } + + #[test] + fn unseal_chunks() { + let mut random = StdRng::seed_from_u64(137); + let mut unsealed_data = vec![0u8; BYTES_PER_SEAL * 10]; + random.fill_bytes(&mut unsealed_data); + let mut data = unsealed_data.clone(); + + let mut context1 = H256::default(); + let mut context2 = H256::default(); + let mut context3 = H256::default(); + random.fill_bytes(&mut context1.0); + random.fill_bytes(&mut context2.0); + random.fill_bytes(&mut context3.0); + + let mut sealer = SealInfo::new(100); + sealer.miner_id = TEST_MINER_ID; + + sealer.seal_contexts.push(SealContextInfo { + context_digest: context1, + end_seal_index: 2, + }); + sealer.seal_contexts.push(SealContextInfo { + context_digest: context2, + end_seal_index: 5, + }); + sealer.seal_contexts.push(SealContextInfo { + context_digest: context3, + end_seal_index: 10, + }); + + // skip seal 6, 3, 9 + for idx in [1, 7, 2, 5, 0, 8, 4].into_iter() { + sealer.seal( + &mut data[idx * BYTES_PER_SEAL..(idx + 1) * BYTES_PER_SEAL], + idx as u16, + ); + sealer.bitmap.set(idx, true); + } + + let partial_hint = &data[BYTES_PER_SEAL * 5 + 64..BYTES_PER_SEAL * 5 + 96]; + let mut tmp_data = data.clone(); + zgs_seal::unseal_with_mask_seed( + &mut tmp_data[BYTES_PER_SEAL * 5 + 96..BYTES_PER_SEAL * 6], + partial_hint, + ); + assert_eq!( + &tmp_data[BYTES_PER_SEAL * 5 + 96..BYTES_PER_SEAL * 6], + &unsealed_data[BYTES_PER_SEAL * 5 + 96..BYTES_PER_SEAL * 6] + ); + + let mut tmp_data = data.clone(); + sealer.unseal(&mut tmp_data[BYTES_PER_SEAL * 5..BYTES_PER_SEAL * 6], 5); + assert_eq!( + &tmp_data[BYTES_PER_SEAL * 5..BYTES_PER_SEAL * 6], + &unsealed_data[BYTES_PER_SEAL * 5..BYTES_PER_SEAL * 6] + ); + + let mut tmp_data = data.clone(); + sealer.unseal(&mut tmp_data[BYTES_PER_SEAL * 6..BYTES_PER_SEAL * 7], 6); + assert_eq!( + &tmp_data[BYTES_PER_SEAL * 6..BYTES_PER_SEAL * 7], + &unsealed_data[BYTES_PER_SEAL * 6..BYTES_PER_SEAL * 7] + ); + + let mut tmp_data = data.clone(); + sealer.unseal( + &mut tmp_data[BYTES_PER_SEAL * 7..BYTES_PER_SEAL * 7 + 96], + 7, + ); + assert_eq!( + &tmp_data[BYTES_PER_SEAL * 7..BYTES_PER_SEAL * 7 + 96], + &unsealed_data[BYTES_PER_SEAL * 7..BYTES_PER_SEAL * 7 + 96] + ); + } +} diff --git a/node/storage/src/log_store/load_chunk/serde.rs b/node/storage/src/log_store/load_chunk/serde.rs new file mode 100644 index 0000000..4c7fa50 --- /dev/null +++ b/node/storage/src/log_store/load_chunk/serde.rs @@ -0,0 +1,83 @@ +use super::{chunk_data::PartialBatch, EntryBatchData}; + +use crate::log_store::load_chunk::chunk_data::IncompleteData; +use ssz::{Decode, DecodeError, Encode}; +use std::mem; + +const COMPLETE_BATCH_TYPE: u8 = 0; +const INCOMPLETE_BATCH_TYPE: u8 = 1; + +impl Encode for EntryBatchData { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + match &self { + EntryBatchData::Complete(data) => { + buf.extend_from_slice(&[COMPLETE_BATCH_TYPE]); + buf.extend_from_slice(data.as_slice()); + } + EntryBatchData::Incomplete(data_list) => { + buf.extend_from_slice(&[INCOMPLETE_BATCH_TYPE]); + buf.extend_from_slice(&data_list.as_ssz_bytes()); + } + } + } + + fn ssz_bytes_len(&self) -> usize { + match &self { + EntryBatchData::Complete(data) => 1 + data.len(), + EntryBatchData::Incomplete(batch_list) => 1 + batch_list.ssz_bytes_len(), + } + } +} + +impl Decode for EntryBatchData { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> std::result::Result { + match *bytes.first().ok_or(DecodeError::ZeroLengthItem)? { + COMPLETE_BATCH_TYPE => Ok(EntryBatchData::Complete(bytes[1..].to_vec())), + INCOMPLETE_BATCH_TYPE => Ok(EntryBatchData::Incomplete( + IncompleteData::from_ssz_bytes(&bytes[1..])?, + )), + unknown => Err(DecodeError::BytesInvalid(format!( + "Unrecognized EntryBatchData indentifier {}", + unknown + ))), + } + } +} + +impl Encode for PartialBatch { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.start_sector.to_be_bytes()); + buf.extend_from_slice(&self.data); + } + + fn ssz_bytes_len(&self) -> usize { + 1 + self.data.len() + } +} + +impl Decode for PartialBatch { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> std::result::Result { + Ok(Self { + start_sector: usize::from_be_bytes( + bytes[..mem::size_of::()].try_into().unwrap(), + ), + data: bytes[mem::size_of::()..].to_vec(), + }) + } +} diff --git a/node/storage/src/log_store/log_manager.rs b/node/storage/src/log_store/log_manager.rs new file mode 100644 index 0000000..4f231ce --- /dev/null +++ b/node/storage/src/log_store/log_manager.rs @@ -0,0 +1,1077 @@ +use crate::log_store::flow_store::{batch_iter, FlowConfig, FlowStore}; +use crate::log_store::tx_store::TransactionStore; +use crate::log_store::{ + FlowRead, FlowWrite, LogStoreChunkRead, LogStoreChunkWrite, LogStoreRead, LogStoreWrite, +}; +use crate::{try_option, ZgsKeyValueDB}; +use anyhow::{anyhow, bail, Result}; +use append_merkle::{Algorithm, MerkleTreeRead, Sha3Algorithm}; +use ethereum_types::H256; +use kvdb_rocksdb::{Database, DatabaseConfig}; +use merkle_light::merkle::{log2_pow2, MerkleTree}; +use merkle_tree::RawLeafSha3Algorithm; +use rayon::iter::ParallelIterator; +use rayon::prelude::ParallelSlice; +use shared_types::{ + bytes_to_chunks, compute_padded_chunk_size, compute_segment_size, Chunk, ChunkArray, + ChunkArrayWithProof, ChunkWithProof, DataRoot, FlowProof, FlowRangeProof, Merkle, Transaction, +}; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::path::Path; +use std::sync::Arc; +use tracing::{debug, error, info, instrument, trace, warn}; + +use super::LogStoreInner; + +/// 256 Bytes +pub const ENTRY_SIZE: usize = 256; +/// 1024 Entries. +pub const PORA_CHUNK_SIZE: usize = 1024; + +pub const COL_TX: u32 = 0; +pub const COL_ENTRY_BATCH: u32 = 1; +pub const COL_TX_DATA_ROOT_INDEX: u32 = 2; +pub const COL_ENTRY_BATCH_ROOT: u32 = 3; +pub const COL_TX_COMPLETED: u32 = 4; +pub const COL_MISC: u32 = 5; +pub const COL_SEAL_CONTEXT: u32 = 6; +pub const COL_NUM: u32 = 7; + +pub struct LogManager { + pub(crate) db: Arc, + tx_store: TransactionStore, + flow_store: FlowStore, + // TODO(zz): Refactor the in-memory merkle and in-disk storage together. + pora_chunks_merkle: Merkle, + /// The in-memory structure of the sub merkle tree of the last chunk. + /// The size is always less than `PORA_CHUNK_SIZE`. + last_chunk_merkle: Merkle, +} + +#[derive(Clone, Default)] +pub struct LogConfig { + pub flow: FlowConfig, +} + +impl LogStoreInner for LogManager { + fn flow(&self) -> &dyn super::Flow { + &self.flow_store + } + + fn flow_mut(&mut self) -> &mut dyn super::Flow { + &mut self.flow_store + } +} + +impl LogStoreChunkWrite for LogManager { + fn put_chunks(&mut self, tx_seq: u64, chunks: ChunkArray) -> Result<()> { + let tx = self + .tx_store + .get_tx_by_seq_number(tx_seq)? + .ok_or_else(|| anyhow!("put chunks with missing tx: tx_seq={}", tx_seq))?; + let (chunks_for_proof, _) = compute_padded_chunk_size(tx.size as usize); + if chunks.start_index.saturating_mul(ENTRY_SIZE as u64) + chunks.data.len() as u64 + > (chunks_for_proof * ENTRY_SIZE) as u64 + { + bail!( + "put chunks with data out of tx range: tx_seq={} start_index={} data_len={}", + tx_seq, + chunks.start_index, + chunks.data.len() + ); + } + // TODO: Use another struct to avoid confusion. + let mut flow_entry_array = chunks; + flow_entry_array.start_index += tx.start_entry_index; + self.append_entries(flow_entry_array)?; + Ok(()) + } + + fn put_chunks_with_tx_hash( + &mut self, + tx_seq: u64, + tx_hash: H256, + chunks: ChunkArray, + ) -> Result { + let tx = self + .tx_store + .get_tx_by_seq_number(tx_seq)? + .ok_or_else(|| anyhow!("put chunks with missing tx: tx_seq={}", tx_seq))?; + if tx.hash() != tx_hash { + return Ok(false); + } + let (chunks_for_proof, _) = compute_padded_chunk_size(tx.size as usize); + if chunks.start_index.saturating_mul(ENTRY_SIZE as u64) + chunks.data.len() as u64 + > (chunks_for_proof * ENTRY_SIZE) as u64 + { + bail!( + "put chunks with data out of tx range: tx_seq={} start_index={} data_len={}", + tx_seq, + chunks.start_index, + chunks.data.len() + ); + } + // TODO: Use another struct to avoid confusion. + let mut flow_entry_array = chunks; + flow_entry_array.start_index += tx.start_entry_index; + self.append_entries(flow_entry_array)?; + Ok(true) + } + + fn remove_all_chunks(&self, _tx_seq: u64) -> crate::error::Result<()> { + todo!() + } +} + +impl LogStoreWrite for LogManager { + #[instrument(skip(self))] + /// Insert the tx and update the flow store if needed. + /// + /// We assumes that all transactions are inserted in order sequentially. + /// We always write the database in the following order: + /// 1. Insert the tx (the tx and the root to tx_seq map are inserted atomically). + /// 2. Update the flow store(pad data for alignment and copy data in `put_tx`, write data in + /// `put_chunks`, pad rear data in `finalize_tx`). + /// 3. Mark tx as finalized. + /// + /// Step 1 and 3 are both atomic operations. + /// * If a tx has been finalized, the data in flow must + /// have been updated correctly. + /// * If `put_tx` succeeds but not finalized, we rely on the upper layer + /// operations (client/auto-sync) to insert needed data (`put_chunks`) and trigger + /// finalization (`finalize_tx`). + /// * If `put_tx` fails in the middle, the tx is inserted but the flow is not updated correctly. + /// Only the last tx may have this case, so we rerun + /// `put_tx` for the last tx when we restart the node to ensure that it succeeds. + /// + fn put_tx(&mut self, tx: Transaction) -> Result<()> { + debug!("put_tx: tx={:?}", tx); + let expected_seq = self.next_tx_seq(); + if tx.seq != expected_seq { + if tx.seq + 1 == expected_seq && !self.check_tx_completed(tx.seq)? { + // special case for rerun the last tx during recovery. + debug!("recovery with tx_seq={}", tx.seq); + } else { + // This is not supposed to happen since we have checked the tx seq in log entry sync. + error!("tx unmatch, expected={} get={:?}", expected_seq, tx); + bail!("unexpected tx!"); + } + } + let maybe_same_data_tx_seq = self.tx_store.put_tx(tx.clone())?.first().cloned(); + // TODO(zz): Should we validate received tx? + self.append_subtree_list(tx.merkle_nodes.clone())?; + self.commit_merkle(tx.seq)?; + + if let Some(old_tx_seq) = maybe_same_data_tx_seq { + if self.check_tx_completed(old_tx_seq)? { + self.copy_tx_data(old_tx_seq, vec![tx.seq])?; + self.tx_store.finalize_tx(tx.seq)?; + } + } + Ok(()) + } + + fn finalize_tx(&mut self, tx_seq: u64) -> Result<()> { + let tx = self + .tx_store + .get_tx_by_seq_number(tx_seq)? + .ok_or_else(|| anyhow!("finalize_tx with tx missing: tx_seq={}", tx_seq))?; + + self.padding_rear_data(&tx)?; + + let tx_end_index = tx.start_entry_index + bytes_to_entries(tx.size); + // TODO: Check completeness without loading all data in memory. + // TODO: Should we double check the tx merkle root? + if self + .flow_store + .get_entries(tx.start_entry_index, tx_end_index)? + .is_some() + { + let same_root_seq_list = self + .tx_store + .get_tx_seq_list_by_data_root(&tx.data_merkle_root)?; + // Check if there are other same-root transaction not finalized. + if same_root_seq_list.first() == Some(&tx_seq) { + self.copy_tx_data(tx_seq, same_root_seq_list[1..].to_vec())?; + } + self.tx_store.finalize_tx(tx_seq)?; + Ok(()) + } else { + bail!("finalize tx with data missing: tx_seq={}", tx_seq) + } + } + + fn finalize_tx_with_hash(&mut self, tx_seq: u64, tx_hash: H256) -> crate::error::Result { + trace!( + "finalize_tx_with_hash: tx_seq={} tx_hash={:?}", + tx_seq, + tx_hash + ); + let tx = self + .tx_store + .get_tx_by_seq_number(tx_seq)? + .ok_or_else(|| anyhow!("finalize_tx with tx missing: tx_seq={}", tx_seq))?; + debug!("finalize_tx_with_hash: tx={:?}", tx); + if tx.hash() != tx_hash { + return Ok(false); + } + + self.padding_rear_data(&tx)?; + + let tx_end_index = tx.start_entry_index + bytes_to_entries(tx.size); + // TODO: Check completeness without loading all data in memory. + // TODO: Should we double check the tx merkle root? + if self + .flow_store + .get_entries(tx.start_entry_index, tx_end_index)? + .is_some() + { + self.tx_store.finalize_tx(tx_seq)?; + let same_root_seq_list = self + .tx_store + .get_tx_seq_list_by_data_root(&tx.data_merkle_root)?; + // Check if there are other same-root transaction not finalized. + if same_root_seq_list.first() == Some(&tx_seq) { + self.copy_tx_data(tx_seq, same_root_seq_list[1..].to_vec())?; + } + Ok(true) + } else { + bail!("finalize tx with data missing: tx_seq={}", tx_seq) + } + } + + fn put_sync_progress(&self, progress: (u64, H256)) -> Result<()> { + self.tx_store.put_progress(progress) + } + + /// Return the reverted Transactions in order. + /// `tx_seq == u64::MAX` is a special case for reverting all transactions. + fn revert_to(&mut self, tx_seq: u64) -> Result> { + // FIXME(zz): If this revert is triggered by chain reorg after restarts, this will fail. + self.revert_merkle_tree(tx_seq)?; + let start_index = self.last_chunk_start_index() * PORA_CHUNK_SIZE as u64 + + self.last_chunk_merkle.leaves() as u64; + self.flow_store.truncate(start_index)?; + let start = if tx_seq != u64::MAX { tx_seq + 1 } else { 0 }; + self.tx_store.remove_tx_after(start) + } +} + +impl LogStoreChunkRead for LogManager { + fn get_chunk_by_tx_and_index( + &self, + tx_seq: u64, + index: usize, + ) -> crate::error::Result> { + // TODO(zz): This is not needed? + let single_chunk_array = + try_option!(self.get_chunks_by_tx_and_index_range(tx_seq, index, index + 1)?); + Ok(Some(Chunk(single_chunk_array.data.as_slice().try_into()?))) + } + + fn get_chunks_by_tx_and_index_range( + &self, + tx_seq: u64, + index_start: usize, + index_end: usize, + ) -> crate::error::Result> { + let tx = try_option!(self.get_tx_by_seq_number(tx_seq)?); + + if index_end as u64 > bytes_to_entries(tx.size) { + bail!( + "end entry index exceeds tx size: end={} tx size={}", + index_start, + tx.size + ); + } + + let start_flow_index = tx.start_entry_index + index_start as u64; + let end_flow_index = tx.start_entry_index + index_end as u64; + // TODO: Use another struct. + // Set returned chunk start index as the offset in the tx data. + let mut tx_chunk = try_option!(self + .flow_store + .get_entries(start_flow_index, end_flow_index)?); + tx_chunk.start_index -= tx.start_entry_index; + Ok(Some(tx_chunk)) + } + + fn get_chunk_by_data_root_and_index( + &self, + _data_root: &DataRoot, + _index: usize, + ) -> crate::error::Result> { + todo!() + } + + fn get_chunks_by_data_root_and_index_range( + &self, + data_root: &DataRoot, + index_start: usize, + index_end: usize, + ) -> crate::error::Result> { + let tx_seq = try_option!(self.get_tx_seq_by_data_root(data_root)?); + self.get_chunks_by_tx_and_index_range(tx_seq, index_start, index_end) + } + + fn get_chunk_index_list(&self, _tx_seq: u64) -> crate::error::Result> { + todo!() + } + + fn get_chunk_by_flow_index( + &self, + index: u64, + length: u64, + ) -> crate::error::Result> { + let start_flow_index = index; + let end_flow_index = index + length; + self.flow_store + .get_entries(start_flow_index, end_flow_index) + } +} + +impl LogStoreRead for LogManager { + fn get_tx_by_seq_number(&self, seq: u64) -> crate::error::Result> { + self.tx_store.get_tx_by_seq_number(seq) + } + + fn get_tx_seq_by_data_root(&self, data_root: &DataRoot) -> crate::error::Result> { + self.tx_store.get_first_tx_seq_by_data_root(data_root) + } + + fn get_chunk_with_proof_by_tx_and_index( + &self, + tx_seq: u64, + index: usize, + ) -> crate::error::Result> { + // TODO(zz): Optimize for mining. + let single_chunk_array = try_option!(self.get_chunks_with_proof_by_tx_and_index_range( + tx_seq, + index, + index + 1 + )?); + Ok(Some(ChunkWithProof { + chunk: Chunk(single_chunk_array.chunks.data.as_slice().try_into()?), + proof: single_chunk_array.proof.left_proof, + })) + } + + fn get_chunks_with_proof_by_tx_and_index_range( + &self, + tx_seq: u64, + index_start: usize, + index_end: usize, + ) -> crate::error::Result> { + let tx = try_option!(self.tx_store.get_tx_by_seq_number(tx_seq)?); + let chunks = + try_option!(self.get_chunks_by_tx_and_index_range(tx_seq, index_start, index_end)?); + let left_proof = self.gen_proof(tx.start_entry_index + index_start as u64, None)?; + let right_proof = self.gen_proof(tx.start_entry_index + index_end as u64 - 1, None)?; + Ok(Some(ChunkArrayWithProof { + chunks, + proof: FlowRangeProof { + left_proof, + right_proof, + }, + })) + } + + fn check_tx_completed(&self, tx_seq: u64) -> crate::error::Result { + self.tx_store.check_tx_completed(tx_seq) + } + + fn validate_range_proof(&self, tx_seq: u64, data: &ChunkArrayWithProof) -> Result { + let tx = self + .get_tx_by_seq_number(tx_seq)? + .ok_or_else(|| anyhow!("tx missing"))?; + let leaves = data_to_merkle_leaves(&data.chunks.data)?; + data.proof.validate::( + &leaves, + (data.chunks.start_index + tx.start_entry_index) as usize, + )?; + Ok(self.pora_chunks_merkle.check_root(&data.proof.root())) + } + + fn get_sync_progress(&self) -> Result> { + self.tx_store.get_progress() + } + + fn next_tx_seq(&self) -> u64 { + self.tx_store.next_tx_seq() + } + + fn get_proof_at_root( + &self, + root: &DataRoot, + index: u64, + length: u64, + ) -> crate::error::Result { + let left_proof = self.gen_proof(index, Some(*root))?; + let right_proof = self.gen_proof(index + length - 1, Some(*root))?; + Ok(FlowRangeProof { + left_proof, + right_proof, + }) + } + + fn get_context(&self) -> crate::error::Result<(DataRoot, u64)> { + Ok(( + *self.pora_chunks_merkle.root(), + self.last_chunk_start_index() + self.last_chunk_merkle.leaves() as u64, + )) + } +} + +impl LogManager { + pub fn rocksdb(config: LogConfig, path: impl AsRef) -> Result { + let mut db_config = DatabaseConfig::with_columns(COL_NUM); + db_config.enable_statistics = true; + let db = Arc::new(Database::open(&db_config, path)?); + Self::new(db, config) + } + + pub fn memorydb(config: LogConfig) -> Result { + let db = Arc::new(kvdb_memorydb::create(COL_NUM)); + Self::new(db, config) + } + + fn new(db: Arc, config: LogConfig) -> Result { + let tx_store = TransactionStore::new(db.clone())?; + let flow_store = FlowStore::new(db.clone(), config.flow); + let mut initial_data = flow_store.get_chunk_root_list()?; + // If the last tx `put_tx` does not complete, we will revert it in `initial_data.subtree_list` + // first and call `put_tx` later. The known leaves in its data will be saved in `extra_leaves` + // and inserted later. + let mut extra_leaves = Vec::new(); + + let next_tx_seq = tx_store.next_tx_seq(); + let mut start_tx_seq = if next_tx_seq > 0 { + Some(next_tx_seq - 1) + } else { + None + }; + let mut last_tx_to_insert = None; + if let Some(last_tx_seq) = start_tx_seq { + if !tx_store.check_tx_completed(last_tx_seq)? { + // Last tx not finalized, we need to check if its `put_tx` is completed. + let last_tx = tx_store + .get_tx_by_seq_number(last_tx_seq)? + .expect("tx missing"); + let mut current_len = initial_data.leaves(); + let expected_len = (last_tx.start_entry_index + last_tx.num_entries() as u64) + / PORA_CHUNK_SIZE as u64; + match expected_len.cmp(&(current_len as u64)) { + Ordering::Less => { + bail!( + "Unexpected DB: merkle tree larger than the known data size,\ + expected={} get={}", + expected_len, + current_len + ); + } + Ordering::Equal => {} + Ordering::Greater => { + // Flow updates are not complete. + // For simplicity, we build the merkle tree for the previous tx and update + // the flow for the last tx again. + info!("revert last tx: last_tx={:?}", last_tx); + last_tx_to_insert = Some(last_tx); + if last_tx_seq == 0 { + start_tx_seq = None; + } else { + // truncate until we get the pora chunks merkle for the previous tx. + let previous_tx = tx_store + .get_tx_by_seq_number(last_tx_seq - 1)? + .expect("tx missing"); + let expected_len = ((previous_tx.start_entry_index + + previous_tx.num_entries() as u64) + / PORA_CHUNK_SIZE as u64) + as usize; + assert!(current_len > expected_len); + while let Some((subtree_depth, _)) = initial_data.subtree_list.pop() { + current_len -= 1 << (subtree_depth - 1); + if current_len == expected_len { + break; + } + } + assert_eq!(current_len, expected_len); + while let Some((index, h)) = initial_data.known_leaves.pop() { + if index < current_len { + initial_data.known_leaves.push((index, h)); + break; + } else { + extra_leaves.push((index, h)); + } + } + start_tx_seq = Some(last_tx_seq - 1); + }; + } + } + } + } + + let mut pora_chunks_merkle = + Merkle::new_with_subtrees(initial_data, log2_pow2(PORA_CHUNK_SIZE), start_tx_seq)?; + let last_chunk_merkle = match start_tx_seq { + Some(tx_seq) => { + tx_store.rebuild_last_chunk_merkle(pora_chunks_merkle.leaves(), tx_seq)? + } + // Initialize + None => Merkle::new_with_depth(vec![], log2_pow2(PORA_CHUNK_SIZE) + 1, None), + }; + + debug!( + "LogManager::new() with chunk_list_len={} start_tx_seq={:?} last_chunk={}", + pora_chunks_merkle.leaves(), + start_tx_seq, + last_chunk_merkle.leaves(), + ); + if last_chunk_merkle.leaves() != 0 { + pora_chunks_merkle.append(*last_chunk_merkle.root()); + // update the merkle root + pora_chunks_merkle.commit(start_tx_seq); + } + let mut log_manager = Self { + db, + tx_store, + flow_store, + pora_chunks_merkle, + last_chunk_merkle, + }; + + if let Some(tx) = last_tx_to_insert { + log_manager.put_tx(tx)?; + for (index, h) in extra_leaves { + if index < log_manager.pora_chunks_merkle.leaves() { + log_manager.pora_chunks_merkle.fill_leaf(index, h); + } else { + error!("out of range extra leaf: index={} hash={:?}", index, h); + } + } + } else { + assert!(extra_leaves.is_empty()); + } + log_manager.try_initialize()?; + Ok(log_manager) + } + + fn try_initialize(&mut self) -> Result<()> { + if self.pora_chunks_merkle.leaves() == 0 && self.last_chunk_merkle.leaves() == 0 { + self.last_chunk_merkle.append(H256::zero()); + self.pora_chunks_merkle + .update_last(*self.last_chunk_merkle.root()); + } else if self.last_chunk_merkle.leaves() != 0 { + let last_chunk_start_index = self.last_chunk_start_index(); + let last_chunk_data = self.flow_store.get_available_entries( + last_chunk_start_index, + last_chunk_start_index + PORA_CHUNK_SIZE as u64, + )?; + for e in last_chunk_data { + let start_index = e.start_index - last_chunk_start_index; + for i in 0..e.data.len() / ENTRY_SIZE { + let index = i + start_index as usize; + if index >= self.last_chunk_merkle.leaves() { + // We revert the merkle tree before truncate the flow store, + // so last_chunk_data may include data that should have been truncated. + break; + } + self.last_chunk_merkle.fill_leaf( + index, + Sha3Algorithm::leaf(&e.data[i * ENTRY_SIZE..(i + 1) * ENTRY_SIZE]), + ); + } + } + } + Ok(()) + } + + fn gen_proof(&self, flow_index: u64, maybe_root: Option) -> Result { + let chunk_index = flow_index / PORA_CHUNK_SIZE as u64; + let top_proof = match maybe_root { + None => self.pora_chunks_merkle.gen_proof(chunk_index as usize)?, + Some(root) => self + .pora_chunks_merkle + .at_root_version(&root)? + .gen_proof(chunk_index as usize)?, + }; + + // TODO(zz): Maybe we can decide that all proofs are at the PoRA chunk level, so + // we do not need to maintain the proof at the entry level below. + // Condition (self.last_chunk_merkle.leaves() == 0): When last chunk size is exactly PORA_CHUNK_SIZE, proof should be generated from flow data, as last_chunk_merkle.leaves() is zero at this time + // TODO(zz): In the current use cases, `maybe_root` is only `Some` for mining + // and `flow_index` must be within a complete PoRA chunk. For possible future usages, + // we'll need to find the flow length at the given root and load a partial chunk + // if `flow_index` is in the last chunk. + let sub_proof = if chunk_index as usize != self.pora_chunks_merkle.leaves() - 1 + || self.last_chunk_merkle.leaves() == 0 + { + self.flow_store + .gen_proof_in_batch(chunk_index as usize, flow_index as usize % PORA_CHUNK_SIZE)? + } else { + match maybe_root { + None => self + .last_chunk_merkle + .gen_proof(flow_index as usize % PORA_CHUNK_SIZE)?, + Some(root) => self + .last_chunk_merkle + .at_root_version(&root)? + .gen_proof(flow_index as usize % PORA_CHUNK_SIZE)?, + } + }; + entry_proof(&top_proof, &sub_proof) + } + + #[instrument(skip(self))] + fn append_subtree_list(&mut self, merkle_list: Vec<(usize, DataRoot)>) -> Result<()> { + if merkle_list.is_empty() { + return Ok(()); + } + + self.pad_tx(1 << (merkle_list[0].0 - 1))?; + + let mut batch_root_map = BTreeMap::new(); + for (subtree_depth, subtree_root) in merkle_list { + let subtree_size = 1 << (subtree_depth - 1); + if self.last_chunk_merkle.leaves() + subtree_size <= PORA_CHUNK_SIZE { + self.last_chunk_merkle + .append_subtree(subtree_depth, subtree_root)?; + if self.last_chunk_merkle.leaves() == subtree_size { + // `last_chunk_merkle` was empty, so this is a new leaf in the top_tree. + self.pora_chunks_merkle + .append_subtree(1, *self.last_chunk_merkle.root())?; + } else { + self.pora_chunks_merkle + .update_last(*self.last_chunk_merkle.root()); + } + if self.last_chunk_merkle.leaves() == PORA_CHUNK_SIZE { + batch_root_map.insert( + self.pora_chunks_merkle.leaves() - 1, + (*self.last_chunk_merkle.root(), 1), + ); + self.complete_last_chunk_merkle(self.pora_chunks_merkle.leaves() - 1)?; + } + } else { + // `last_chunk_merkle` has been padded here, so a subtree should not be across + // the chunks boundary. + assert_eq!(self.last_chunk_merkle.leaves(), 0); + assert!(subtree_size >= PORA_CHUNK_SIZE); + batch_root_map.insert( + self.pora_chunks_merkle.leaves(), + (subtree_root, subtree_depth - log2_pow2(PORA_CHUNK_SIZE)), + ); + self.pora_chunks_merkle + .append_subtree(subtree_depth - log2_pow2(PORA_CHUNK_SIZE), subtree_root)?; + } + } + self.flow_store.put_batch_root_list(batch_root_map)?; + Ok(()) + } + + #[instrument(skip(self))] + fn pad_tx(&mut self, first_subtree_size: u64) -> Result<()> { + // Check if we need to pad the flow. + let tx_start_flow_index = + self.last_chunk_start_index() + self.last_chunk_merkle.leaves() as u64; + let extra = tx_start_flow_index % first_subtree_size; + trace!( + "before pad_tx {} {}", + self.pora_chunks_merkle.leaves(), + self.last_chunk_merkle.leaves() + ); + if extra != 0 { + let pad_data = Self::padding((first_subtree_size - extra) as usize); + + // Update the in-memory merkle tree. + let mut root_map = BTreeMap::new(); + let last_chunk_pad = if self.last_chunk_merkle.leaves() == 0 { + 0 + } else { + (PORA_CHUNK_SIZE - self.last_chunk_merkle.leaves()) * ENTRY_SIZE + }; + + let mut completed_chunk_index = None; + if pad_data.len() < last_chunk_pad { + self.last_chunk_merkle + .append_list(data_to_merkle_leaves(&pad_data)?); + self.pora_chunks_merkle + .update_last(*self.last_chunk_merkle.root()); + } else { + if last_chunk_pad != 0 { + // Pad the last chunk. + self.last_chunk_merkle + .append_list(data_to_merkle_leaves(&pad_data[..last_chunk_pad])?); + self.pora_chunks_merkle + .update_last(*self.last_chunk_merkle.root()); + root_map.insert( + self.pora_chunks_merkle.leaves() - 1, + (*self.last_chunk_merkle.root(), 1), + ); + completed_chunk_index = Some(self.pora_chunks_merkle.leaves() - 1); + } + + // Pad with more complete chunks. + let mut start_index = last_chunk_pad / ENTRY_SIZE; + while pad_data.len() >= (start_index + PORA_CHUNK_SIZE) * ENTRY_SIZE { + let data = pad_data + [start_index * ENTRY_SIZE..(start_index + PORA_CHUNK_SIZE) * ENTRY_SIZE] + .to_vec(); + let root = *Merkle::new(data_to_merkle_leaves(&data)?, 0, None).root(); + self.pora_chunks_merkle.append(root); + root_map.insert(self.pora_chunks_merkle.leaves() - 1, (root, 1)); + start_index += PORA_CHUNK_SIZE; + } + assert_eq!(pad_data.len(), start_index * ENTRY_SIZE); + } + + // Update the root index. + self.flow_store.put_batch_root_list(root_map)?; + // Update the flow database. + // This should be called before `complete_last_chunk_merkle` so that we do not save + // subtrees with data known. + self.flow_store.append_entries(ChunkArray { + data: pad_data, + start_index: tx_start_flow_index, + })?; + if let Some(index) = completed_chunk_index { + self.complete_last_chunk_merkle(index)?; + } + } + trace!( + "after pad_tx {} {}", + self.pora_chunks_merkle.leaves(), + self.last_chunk_merkle.leaves() + ); + Ok(()) + } + + fn append_entries(&mut self, flow_entry_array: ChunkArray) -> Result<()> { + let last_chunk_start_index = self.last_chunk_start_index(); + if flow_entry_array.start_index + bytes_to_chunks(flow_entry_array.data.len()) as u64 + > last_chunk_start_index + { + // Update `last_chunk_merkle` with real data. + let (chunk_start_index, flow_entry_data_index) = if flow_entry_array.start_index + >= last_chunk_start_index + { + // flow_entry_array only fill last chunk + ( + (flow_entry_array.start_index - last_chunk_start_index) as usize, + 0, + ) + } else { + // flow_entry_array fill both last and last - 1 chunk + ( + 0, + (last_chunk_start_index - flow_entry_array.start_index) as usize * ENTRY_SIZE, + ) + }; + + // Since we always put tx before insert its data. Here `last_chunk_merkle` must + // have included the data range. + for (local_index, entry) in flow_entry_array.data[flow_entry_data_index..] + .chunks_exact(ENTRY_SIZE) + .enumerate() + { + self.last_chunk_merkle + .fill_leaf(chunk_start_index + local_index, Sha3Algorithm::leaf(entry)); + } + } + let chunk_roots = self.flow_store.append_entries(flow_entry_array)?; + for (chunk_index, chunk_root) in chunk_roots { + if chunk_index < self.pora_chunks_merkle.leaves() as u64 { + self.pora_chunks_merkle + .fill_leaf(chunk_index as usize, chunk_root); + } else { + // TODO(zz): This assumption may be false in the future. + unreachable!("We always insert tx nodes before put_chunks"); + } + } + Ok(()) + } + + // FIXME(zz): Implement padding. + pub fn padding(len: usize) -> Vec { + vec![0; len * ENTRY_SIZE] + } + + fn last_chunk_start_index(&self) -> u64 { + if self.pora_chunks_merkle.leaves() == 0 { + 0 + } else { + PORA_CHUNK_SIZE as u64 + * if self.last_chunk_merkle.leaves() == 0 { + // The last chunk is empty and its root hash is not in `pora_chunk_merkle`, + // so all chunks in `pora_chunk_merkle` is complete. + self.pora_chunks_merkle.leaves() + } else { + // The last chunk has data, so we need to exclude it from `pora_chunks_merkle`. + self.pora_chunks_merkle.leaves() - 1 + } as u64 + } + } + + #[instrument(skip(self))] + fn commit_merkle(&mut self, tx_seq: u64) -> Result<()> { + self.pora_chunks_merkle.commit(Some(tx_seq)); + self.last_chunk_merkle.commit(Some(tx_seq)); + Ok(()) + } + + fn revert_merkle_tree(&mut self, tx_seq: u64) -> Result<()> { + // Special case for reverting tx_seq == 0 + if tx_seq == u64::MAX { + self.pora_chunks_merkle.reset(); + self.last_chunk_merkle.reset(); + self.try_initialize()?; + return Ok(()); + } + let old_leaves = self.pora_chunks_merkle.leaves(); + self.pora_chunks_merkle.revert_to(tx_seq)?; + if old_leaves == self.pora_chunks_merkle.leaves() { + self.last_chunk_merkle.revert_to(tx_seq)?; + } else { + // We are reverting to a position before the current last_chunk. + self.last_chunk_merkle = self + .tx_store + .rebuild_last_chunk_merkle(self.pora_chunks_merkle.leaves() - 1, tx_seq)?; + self.try_initialize()?; + assert_eq!( + Some(*self.last_chunk_merkle.root()), + self.pora_chunks_merkle + .leaf_at(self.pora_chunks_merkle.leaves() - 1)? + ); + } + Ok(()) + } + + #[cfg(test)] + pub fn flow_store(&self) -> &FlowStore { + &self.flow_store + } + + fn padding_rear_data(&mut self, tx: &Transaction) -> Result<()> { + let (chunks, _) = compute_padded_chunk_size(tx.size as usize); + let (segments_for_proof, last_segment_size_for_proof) = + compute_segment_size(chunks, PORA_CHUNK_SIZE); + debug!( + "segments_for_proof: {}, last_segment_size_for_proof: {}", + segments_for_proof, last_segment_size_for_proof + ); + + let chunks_for_file = bytes_to_entries(tx.size) as usize; + let (mut segments_for_file, mut last_segment_size_for_file) = + compute_segment_size(chunks_for_file, PORA_CHUNK_SIZE); + debug!( + "segments_for_file: {}, last_segment_size_for_file: {}", + segments_for_file, last_segment_size_for_file + ); + + while segments_for_file <= segments_for_proof { + let padding_size = if segments_for_file == segments_for_proof { + (last_segment_size_for_proof - last_segment_size_for_file) * ENTRY_SIZE + } else { + (PORA_CHUNK_SIZE - last_segment_size_for_file) * ENTRY_SIZE + }; + + debug!("Padding size: {}", padding_size); + if padding_size > 0 { + // This tx hash is guaranteed to be consistent. + self.put_chunks_with_tx_hash( + tx.seq, + tx.hash(), + ChunkArray { + data: vec![0u8; padding_size], + start_index: ((segments_for_file - 1) * PORA_CHUNK_SIZE + + last_segment_size_for_file) + as u64, + }, + )?; + } + + last_segment_size_for_file = 0; + segments_for_file += 1; + } + + Ok(()) + } + + fn copy_tx_data(&mut self, from_tx_seq: u64, to_tx_seq_list: Vec) -> Result<()> { + // We have all the data need for this tx, so just copy them. + let old_tx = self + .get_tx_by_seq_number(from_tx_seq)? + .ok_or_else(|| anyhow!("from tx missing"))?; + let mut to_tx_offset_list = Vec::with_capacity(to_tx_seq_list.len()); + for seq in to_tx_seq_list { + // No need to copy data for completed tx. + if self.check_tx_completed(seq)? { + continue; + } + let tx = self + .get_tx_by_seq_number(seq)? + .ok_or_else(|| anyhow!("to tx missing"))?; + to_tx_offset_list.push((tx.seq, tx.start_entry_index - old_tx.start_entry_index)); + } + if to_tx_offset_list.is_empty() { + return Ok(()); + } + // copy data in batches + // TODO(zz): Do this asynchronously and keep atomicity. + for (batch_start, batch_end) in batch_iter( + old_tx.start_entry_index, + old_tx.start_entry_index + old_tx.num_entries() as u64, + PORA_CHUNK_SIZE, + ) { + let batch_data = self + .get_chunk_by_flow_index(batch_start, batch_end - batch_start)? + .ok_or_else(|| anyhow!("tx data missing"))?; + for (_, offset) in &to_tx_offset_list { + let mut data = batch_data.clone(); + data.start_index += offset; + self.append_entries(data)?; + } + } + // num_entries() includes the rear padding data, so no need for more padding. + + for (seq, _) in to_tx_offset_list { + self.tx_store.finalize_tx(seq)?; + } + Ok(()) + } + + /// Here we persist the subtrees with the incomplete data of the last chunk merkle so that + /// we can still provide proof for known data in it. + /// Another choice is to insert these subtrees earlier in `put_tx`. To insert them here can + /// batch them and avoid inserting for the subtrees with all data known. + fn complete_last_chunk_merkle(&mut self, index: usize) -> Result<()> { + let subtree_list = self.last_chunk_merkle.get_subtrees(); + self.last_chunk_merkle = + Merkle::new_with_depth(vec![], log2_pow2(PORA_CHUNK_SIZE) + 1, None); + + // Only insert non-leave subtrees. The leave data should have been available. + let mut to_insert_subtrees = Vec::new(); + let mut start_index = 0; + for (subtree_height, root) in subtree_list { + to_insert_subtrees.push((start_index, subtree_height, root)); + start_index += 1 << (subtree_height - 1); + } + self.flow_store + .insert_subtree_list_for_batch(index, to_insert_subtrees) + } +} + +/// This represents the subtree of a chunk or the whole data merkle tree. +pub type FileMerkleTree = MerkleTree<[u8; 32], RawLeafSha3Algorithm>; + +#[macro_export] +macro_rules! try_option { + ($r: ident) => { + match $r { + Some(v) => v, + None => return Ok(None), + } + }; + ($e: expr) => { + match $e { + Some(v) => v, + None => return Ok(None), + } + }; +} + +/// This should be called with input checked. +pub fn sub_merkle_tree(leaf_data: &[u8]) -> Result { + Ok(FileMerkleTree::new( + data_to_merkle_leaves(leaf_data)? + .into_iter() + .map(|h| h.0) + .collect::>(), + )) +} + +pub fn data_to_merkle_leaves(leaf_data: &[u8]) -> Result> { + if leaf_data.len() % ENTRY_SIZE != 0 { + bail!("merkle_tree: unmatch data size"); + } + // If the data size is small, using `rayon` would introduce more overhead. + let r = if leaf_data.len() >= ENTRY_SIZE * 8 { + leaf_data + .par_chunks_exact(ENTRY_SIZE) + .map(Sha3Algorithm::leaf) + .collect() + } else { + leaf_data + .chunks_exact(ENTRY_SIZE) + .map(Sha3Algorithm::leaf) + .collect() + }; + Ok(r) +} + +pub fn bytes_to_entries(size_bytes: u64) -> u64 { + if size_bytes % ENTRY_SIZE as u64 == 0 { + size_bytes / ENTRY_SIZE as u64 + } else { + size_bytes / ENTRY_SIZE as u64 + 1 + } +} + +fn entry_proof(top_proof: &FlowProof, sub_proof: &FlowProof) -> Result { + if top_proof.item() != sub_proof.root() { + bail!( + "top tree and sub tree mismatch: top_leaf={:?}, sub_root={:?}", + top_proof.item(), + sub_proof.root() + ); + } + let mut lemma = sub_proof.lemma().to_vec(); + let mut path = sub_proof.path().to_vec(); + assert!(lemma.pop().is_some()); + lemma.extend_from_slice(&top_proof.lemma()[1..]); + path.extend_from_slice(top_proof.path()); + Ok(FlowProof::new(lemma, path)) +} + +pub fn split_nodes(data_size: usize) -> Vec { + let (mut padded_chunks, chunks_next_pow2) = compute_padded_chunk_size(data_size); + let mut next_chunk_size = chunks_next_pow2; + + let mut nodes = vec![]; + while padded_chunks > 0 { + if padded_chunks >= next_chunk_size { + padded_chunks -= next_chunk_size; + nodes.push(next_chunk_size); + } + + next_chunk_size >>= 1; + } + + nodes +} + +pub fn tx_subtree_root_list_padded(data: &[u8]) -> Vec<(usize, DataRoot)> { + let mut root_list = Vec::new(); + let mut start_index = 0; + let nodes = split_nodes(data.len()); + + for &tree_size in nodes.iter() { + let end = start_index + tree_size * ENTRY_SIZE; + + let submerkle_root = if start_index >= data.len() { + sub_merkle_tree(&vec![0u8; tree_size * ENTRY_SIZE]) + .unwrap() + .root() + } else if end > data.len() { + let mut pad_data = data[start_index..].to_vec(); + pad_data.append(&mut vec![0u8; end - data.len()]); + sub_merkle_tree(&pad_data).unwrap().root() + } else { + sub_merkle_tree(&data[start_index..end]).unwrap().root() + }; + + root_list.push((log2_pow2(tree_size) + 1, submerkle_root.into())); + start_index = end; + } + + root_list +} diff --git a/node/storage/src/log_store/mod.rs b/node/storage/src/log_store/mod.rs new file mode 100644 index 0000000..a5a48a5 --- /dev/null +++ b/node/storage/src/log_store/mod.rs @@ -0,0 +1,227 @@ +use append_merkle::MerkleTreeInitialData; +use ethereum_types::H256; +use zgs_spec::{BYTES_PER_SEAL, SEALS_PER_LOAD}; +use shared_types::{ + Chunk, ChunkArray, ChunkArrayWithProof, ChunkWithProof, DataRoot, FlowRangeProof, Transaction, +}; + +use crate::error::Result; + +pub mod config; +mod flow_store; +mod load_chunk; +pub mod log_manager; +#[cfg(test)] +mod tests; +mod tx_store; + +/// The trait to read the transactions already appended to the log. +/// +/// Implementation Rationale: +/// If the stored chunk is large, we can store the proof together with the chunk. +pub trait LogStoreRead: LogStoreChunkRead { + /// Get a transaction by its global log sequence number. + fn get_tx_by_seq_number(&self, seq: u64) -> Result>; + + /// Get a transaction by the data root of its data. + fn get_tx_seq_by_data_root(&self, data_root: &DataRoot) -> Result>; + + fn get_tx_by_data_root(&self, data_root: &DataRoot) -> Result> { + match self.get_tx_seq_by_data_root(data_root)? { + Some(seq) => self.get_tx_by_seq_number(seq), + None => Ok(None), + } + } + + fn get_chunk_with_proof_by_tx_and_index( + &self, + tx_seq: u64, + index: usize, + ) -> Result>; + + fn get_chunks_with_proof_by_tx_and_index_range( + &self, + tx_seq: u64, + index_start: usize, + index_end: usize, + ) -> Result>; + + fn check_tx_completed(&self, tx_seq: u64) -> Result; + + fn next_tx_seq(&self) -> u64; + + fn get_sync_progress(&self) -> Result>; + + fn validate_range_proof(&self, tx_seq: u64, data: &ChunkArrayWithProof) -> Result; + + fn get_proof_at_root(&self, root: &DataRoot, index: u64, length: u64) + -> Result; + + /// Return flow root and length. + fn get_context(&self) -> Result<(DataRoot, u64)>; +} + +pub trait LogStoreChunkRead { + /// Get a data chunk by the transaction sequence number and the chunk offset in the transaction. + /// Accessing a single chunk is mostly used for mining. + fn get_chunk_by_tx_and_index(&self, tx_seq: u64, index: usize) -> Result>; + + /// Get a list of continuous chunks by the transaction sequence number and an index range (`index_end` excluded). + fn get_chunks_by_tx_and_index_range( + &self, + tx_seq: u64, + index_start: usize, + index_end: usize, + ) -> Result>; + + fn get_chunk_by_data_root_and_index( + &self, + data_root: &DataRoot, + index: usize, + ) -> Result>; + + fn get_chunks_by_data_root_and_index_range( + &self, + data_root: &DataRoot, + index_start: usize, + index_end: usize, + ) -> Result>; + + fn get_chunk_index_list(&self, tx_seq: u64) -> Result>; + + /// Accessing chunks by absolute flow index + fn get_chunk_by_flow_index(&self, index: u64, length: u64) -> Result>; +} + +pub trait LogStoreWrite: LogStoreChunkWrite { + /// Store a data entry metadata. + fn put_tx(&mut self, tx: Transaction) -> Result<()>; + + /// Finalize a transaction storage. + /// This will compute and the merkle tree, check the data root, and persist a part of the merkle + /// tree for future queries. + /// + /// This will return error if not all chunks are stored. But since this check can be expensive, + /// the caller is supposed to track chunk statuses and call this after storing all the chunks. + fn finalize_tx(&mut self, tx_seq: u64) -> Result<()>; + fn finalize_tx_with_hash(&mut self, tx_seq: u64, tx_hash: H256) -> Result; + + /// Store the progress of synced block number and its hash. + fn put_sync_progress(&self, progress: (u64, H256)) -> Result<()>; + + /// Revert the log state to a given tx seq. + /// This is needed when transactions are reverted because of chain reorg. + /// + /// Reverted transactions are returned in order. + fn revert_to(&mut self, tx_seq: u64) -> Result>; +} + +pub trait LogStoreChunkWrite { + /// Store data chunks of a data entry. + fn put_chunks(&mut self, tx_seq: u64, chunks: ChunkArray) -> Result<()>; + + fn put_chunks_with_tx_hash( + &mut self, + tx_seq: u64, + tx_hash: H256, + chunks: ChunkArray, + ) -> Result; + + /// Delete all chunks of a tx. + fn remove_all_chunks(&self, tx_seq: u64) -> Result<()>; +} + +pub trait LogChunkStore: LogStoreChunkRead + LogStoreChunkWrite + Send + Sync + 'static {} +impl LogChunkStore for T {} + +pub trait Store: + LogStoreRead + LogStoreWrite + LogStoreInner + config::Configurable + Send + Sync + 'static +{ +} +impl< + T: LogStoreRead + LogStoreWrite + LogStoreInner + config::Configurable + Send + Sync + 'static, + > Store for T +{ +} + +pub trait LogStoreInner { + fn flow(&self) -> &dyn Flow; + fn flow_mut(&mut self) -> &mut dyn Flow; +} + +pub struct MineLoadChunk { + pub loaded_chunk: [[u8; BYTES_PER_SEAL]; SEALS_PER_LOAD], + pub avalibilities: [bool; SEALS_PER_LOAD], +} + +impl Default for MineLoadChunk { + fn default() -> Self { + Self { + loaded_chunk: [[0u8; BYTES_PER_SEAL]; SEALS_PER_LOAD], + avalibilities: [false; SEALS_PER_LOAD], + } + } +} + +pub trait FlowRead { + /// Return the entries in the given range. If some data are missing, `Ok(None)` is returned. + fn get_entries(&self, index_start: u64, index_end: u64) -> Result>; + + /// Return the available entries in the given range. + /// The `ChunkArray` in the returned list are in order and they will not overlap or be adjacent. + /// + /// For simplicity, `index_start` and `index_end` must be at the batch boundaries. + fn get_available_entries(&self, index_start: u64, index_end: u64) -> Result>; + + fn get_chunk_root_list(&self) -> Result>; + + fn load_sealed_data(&self, chunk_index: u64) -> Result>; +} + +pub trait FlowWrite { + /// Append data to the flow. `start_index` is included in `ChunkArray`, so + /// it's possible to append arrays in any place. + /// Return the list of completed chunks. + fn append_entries(&mut self, data: ChunkArray) -> Result>; + + /// Remove all the entries after `start_index`. + /// This is used to remove deprecated data in case of chain reorg. + fn truncate(&mut self, start_index: u64) -> Result<()>; +} + +pub struct SealTask { + /// The index (in seal) of chunks + pub seal_index: u64, + /// An ephemeral version number to distinguish if revert happending + pub version: usize, + /// The data to be sealed + pub non_sealed_data: [u8; BYTES_PER_SEAL], +} + +#[derive(Debug)] +pub struct SealAnswer { + /// The index (in seal) of chunks + pub seal_index: u64, + /// An ephemeral version number to distinguish if revert happending + pub version: usize, + /// The data to be sealed + pub sealed_data: [u8; BYTES_PER_SEAL], + /// The miner Id + pub miner_id: H256, + /// The seal_context for this chunk + pub seal_context: H256, + pub context_end_seal: u64, +} + +pub trait FlowSeal { + /// Pull a seal chunk ready for sealing + /// Return the global index (in sector) and the data + fn pull_seal_chunk(&self, seal_index_max: usize) -> Result>>; + + /// Submit sealing result + + fn submit_seal_result(&mut self, answers: Vec) -> Result<()>; +} + +pub trait Flow: FlowRead + FlowWrite + FlowSeal {} +impl Flow for T {} diff --git a/node/storage/src/log_store/tests.rs b/node/storage/src/log_store/tests.rs new file mode 100644 index 0000000..8e3636b --- /dev/null +++ b/node/storage/src/log_store/tests.rs @@ -0,0 +1,207 @@ +use crate::log_store::log_manager::{ + data_to_merkle_leaves, sub_merkle_tree, tx_subtree_root_list_padded, LogConfig, LogManager, + PORA_CHUNK_SIZE, +}; +use crate::log_store::{LogStoreChunkRead, LogStoreChunkWrite, LogStoreRead, LogStoreWrite}; +use append_merkle::{Algorithm, AppendMerkleTree, MerkleTreeRead, Sha3Algorithm}; +use ethereum_types::H256; +use rand::random; +use shared_types::{compute_padded_chunk_size, ChunkArray, Transaction, CHUNK_SIZE}; +use std::cmp; + +#[test] +fn test_put_get() { + let config = LogConfig::default(); + let mut store = LogManager::memorydb(config.clone()).unwrap(); + let chunk_count = config.flow.batch_size + config.flow.batch_size / 2 - 1; + // Aligned with size. + let start_offset = 1024; + let data_size = CHUNK_SIZE * chunk_count; + let mut data = vec![0u8; data_size]; + for i in 0..chunk_count { + data[i * CHUNK_SIZE] = random(); + } + let (padded_chunks, _) = compute_padded_chunk_size(data_size); + let mut merkle = AppendMerkleTree::::new(vec![H256::zero()], 0, None); + merkle.append_list(data_to_merkle_leaves(&LogManager::padding(start_offset - 1)).unwrap()); + let mut data_padded = data.clone(); + data_padded.append(&mut vec![0u8; CHUNK_SIZE]); + merkle.append_list(data_to_merkle_leaves(&data_padded).unwrap()); + merkle.commit(Some(0)); + let tx_merkle = sub_merkle_tree(&data).unwrap(); + let tx = Transaction { + stream_ids: vec![], + size: data_size as u64, + data_merkle_root: tx_merkle.root().into(), + seq: 0, + data: vec![], + start_entry_index: start_offset as u64, + // TODO: This can come from `tx_merkle`. + merkle_nodes: tx_subtree_root_list_padded(&data), + }; + store.put_tx(tx.clone()).unwrap(); + for start_index in (0..chunk_count).step_by(PORA_CHUNK_SIZE) { + let end = cmp::min((start_index + PORA_CHUNK_SIZE) * CHUNK_SIZE, data.len()); + let chunk_array = ChunkArray { + data: data[start_index * CHUNK_SIZE..end].to_vec(), + start_index: start_index as u64, + }; + store.put_chunks(tx.seq, chunk_array.clone()).unwrap(); + } + store.finalize_tx(tx.seq).unwrap(); + + let chunk_array = ChunkArray { + data, + start_index: 0, + }; + assert_eq!(store.get_tx_by_seq_number(0).unwrap().unwrap(), tx); + for i in 0..chunk_count { + assert_eq!( + store.get_chunk_by_tx_and_index(tx.seq, i).unwrap().unwrap(), + chunk_array.chunk_at(i).unwrap() + ); + } + assert!(store + .get_chunk_by_tx_and_index(tx.seq, padded_chunks) + .is_err()); + + assert_eq!( + store + .get_chunks_by_tx_and_index_range(tx.seq, 0, chunk_count) + .unwrap() + .unwrap(), + chunk_array + ); + for i in 0..chunk_count { + let chunk_with_proof = store + .get_chunk_with_proof_by_tx_and_index(tx.seq, i) + .unwrap() + .unwrap(); + assert_eq!(chunk_with_proof.chunk, chunk_array.chunk_at(i).unwrap()); + assert_eq!( + chunk_with_proof.proof, + merkle.gen_proof(i + start_offset).unwrap() + ); + let r = chunk_with_proof.proof.validate::( + &Sha3Algorithm::leaf(&chunk_with_proof.chunk.0), + i + start_offset, + ); + assert!(r.is_ok(), "proof={:?} \n r={:?}", chunk_with_proof.proof, r); + assert!(merkle.check_root(&chunk_with_proof.proof.root())); + } + for i in (0..chunk_count).step_by(PORA_CHUNK_SIZE / 3) { + let end = std::cmp::min(i + PORA_CHUNK_SIZE, chunk_count); + let chunk_array_with_proof = store + .get_chunks_with_proof_by_tx_and_index_range(tx.seq, i, end) + .unwrap() + .unwrap(); + assert_eq!( + chunk_array_with_proof.chunks, + chunk_array.sub_array(i as u64, end as u64).unwrap() + ); + assert!(chunk_array_with_proof + .proof + .validate::( + &data_to_merkle_leaves(&chunk_array_with_proof.chunks.data).unwrap(), + i + start_offset + ) + .is_ok()); + } +} + +#[test] +fn test_root() { + for depth in 0..12 { + let n_chunk = 1 << depth; + let mut data = vec![0; n_chunk * CHUNK_SIZE]; + for i in 0..n_chunk { + data[i * CHUNK_SIZE] = random(); + } + let mt = sub_merkle_tree(&data).unwrap(); + println!("{:?} {}", mt.root(), hex::encode(mt.root())); + let append_mt = AppendMerkleTree::::new( + data_to_merkle_leaves(&data).unwrap(), + 0, + None, + ); + assert_eq!(mt.root(), append_mt.root().0); + } +} + +#[test] +fn test_multi_tx() { + let mut store = create_store(); + put_tx(&mut store, 3, 0); + put_tx(&mut store, 3, 1); + put_tx(&mut store, 5, 2); +} + +#[test] +fn test_revert() { + let mut store = create_store(); + put_tx(&mut store, 1, 0); + store.revert_to(0u64.wrapping_sub(1)).unwrap(); + put_tx(&mut store, 1, 0); + put_tx(&mut store, 1, 1); + store.revert_to(0).unwrap(); + put_tx(&mut store, 1, 1); + + // Test revert across the last chunk. + put_tx(&mut store, 1024 + 1, 2); + store.revert_to(1).unwrap(); + + // Test with chunk boundary within a tx. + put_tx(&mut store, 256, 2); + put_tx(&mut store, 512 + 1, 3); + put_tx(&mut store, 1, 4); + store.revert_to(1).unwrap(); + put_tx(&mut store, 1, 2); +} + +#[test] +fn test_put_tx() { + for i in 0..12 { + let chunk_count = 0xF << i; + let mut store = create_store(); + put_tx(&mut store, chunk_count, 0); + } +} + +fn create_store() -> LogManager { + let config = LogConfig::default(); + + LogManager::memorydb(config).unwrap() +} + +fn put_tx(store: &mut LogManager, chunk_count: usize, seq: u64) { + let data_size = CHUNK_SIZE * chunk_count; + let mut data = vec![0u8; data_size]; + for i in 0..chunk_count { + data[i * CHUNK_SIZE..(i * CHUNK_SIZE + 8)].copy_from_slice(&(seq + 1).to_be_bytes()); + } + let tx_merkle = sub_merkle_tree(&data).unwrap(); + let merkle_nodes = tx_subtree_root_list_padded(&data); + let flow_len = store.get_context().unwrap().1; + let first_subtree_size = 1 << (merkle_nodes.first().unwrap().0 - 1); + let start_entry_index = ((flow_len - 1) / first_subtree_size + 1) * first_subtree_size; + let tx = Transaction { + stream_ids: vec![], + size: data_size as u64, + data_merkle_root: tx_merkle.root().into(), + seq, + data: vec![], + start_entry_index, + // TODO: This can come from `tx_merkle`. + merkle_nodes, + }; + store.put_tx(tx.clone()).unwrap(); + for start_index in (0..chunk_count).step_by(PORA_CHUNK_SIZE) { + let end = cmp::min((start_index + PORA_CHUNK_SIZE) * CHUNK_SIZE, data.len()); + let chunk_array = ChunkArray { + data: data[start_index * CHUNK_SIZE..end].to_vec(), + start_index: start_index as u64, + }; + store.put_chunks(tx.seq, chunk_array.clone()).unwrap(); + } + store.finalize_tx(tx.seq).unwrap(); +} diff --git a/node/storage/src/log_store/tx_store.rs b/node/storage/src/log_store/tx_store.rs new file mode 100644 index 0000000..1da6731 --- /dev/null +++ b/node/storage/src/log_store/tx_store.rs @@ -0,0 +1,276 @@ +use crate::error::Error; +use crate::log_store::log_manager::{ + data_to_merkle_leaves, sub_merkle_tree, COL_MISC, COL_TX, COL_TX_COMPLETED, + COL_TX_DATA_ROOT_INDEX, ENTRY_SIZE, PORA_CHUNK_SIZE, +}; +use crate::{try_option, LogManager, ZgsKeyValueDB}; +use anyhow::{anyhow, Result}; +use append_merkle::{AppendMerkleTree, MerkleTreeRead, Sha3Algorithm}; +use ethereum_types::H256; +use merkle_light::merkle::log2_pow2; +use shared_types::{DataRoot, Transaction}; +use ssz::{Decode, Encode}; +use std::cmp; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; +use tracing::{error, instrument}; + +const LOG_SYNC_PROGRESS_KEY: &str = "log_sync_progress"; +const NEXT_TX_KEY: &str = "next_tx_seq"; + +pub struct TransactionStore { + kvdb: Arc, + /// This is always updated before writing the database to ensure no intermediate states. + next_tx_seq: AtomicU64, +} + +impl TransactionStore { + pub fn new(kvdb: Arc) -> Result { + let next_tx_seq = kvdb + .get(COL_TX, NEXT_TX_KEY.as_bytes())? + .map(|a| decode_tx_seq(&a)) + .unwrap_or(Ok(0))?; + Ok(Self { + kvdb, + next_tx_seq: AtomicU64::new(next_tx_seq), + }) + } + + #[instrument(skip(self))] + /// Return `Ok(Some(tx_seq))` if a previous transaction has the same tx root. + pub fn put_tx(&self, mut tx: Transaction) -> Result> { + let mut db_tx = self.kvdb.transaction(); + + if !tx.data.is_empty() { + tx.size = tx.data.len() as u64; + let mut padded_data = tx.data.clone(); + let extra = tx.data.len() % ENTRY_SIZE; + if extra != 0 { + padded_data.append(&mut vec![0u8; ENTRY_SIZE - extra]); + } + let data_root = sub_merkle_tree(&padded_data)?.root(); + tx.data_merkle_root = data_root.into(); + } + + db_tx.put(COL_TX, &tx.seq.to_be_bytes(), &tx.as_ssz_bytes()); + db_tx.put(COL_TX, NEXT_TX_KEY.as_bytes(), &(tx.seq + 1).to_be_bytes()); + let old_tx_seq_list = self.get_tx_seq_list_by_data_root(&tx.data_merkle_root)?; + // The list is sorted, and we always call `put_tx` in order. + assert!(old_tx_seq_list + .last() + .map(|last| *last < tx.seq) + .unwrap_or(true)); + let mut new_tx_seq_list = old_tx_seq_list.clone(); + new_tx_seq_list.push(tx.seq); + db_tx.put( + COL_TX_DATA_ROOT_INDEX, + tx.data_merkle_root.as_bytes(), + &new_tx_seq_list.as_ssz_bytes(), + ); + self.next_tx_seq.store(tx.seq + 1, Ordering::SeqCst); + self.kvdb.write(db_tx)?; + Ok(old_tx_seq_list) + } + + pub fn get_tx_by_seq_number(&self, seq: u64) -> Result> { + if seq >= self.next_tx_seq() { + return Ok(None); + } + let value = try_option!(self.kvdb.get(COL_TX, &seq.to_be_bytes())?); + let tx = Transaction::from_ssz_bytes(&value).map_err(Error::from)?; + Ok(Some(tx)) + } + + pub fn remove_tx_after(&self, min_seq: u64) -> Result> { + let mut removed_txs = Vec::new(); + let max_seq = self.next_tx_seq(); + let mut db_tx = self.kvdb.transaction(); + let mut modified_merkle_root_map = HashMap::new(); + for seq in min_seq..max_seq { + let Some(tx) = self.get_tx_by_seq_number(seq)? else { + error!(?seq, ?max_seq, "Transaction missing before the end"); + break; + }; + db_tx.delete(COL_TX, &seq.to_be_bytes()); + db_tx.delete(COL_TX_COMPLETED, &seq.to_be_bytes()); + // We only remove tx when the blockchain reorgs. + // If a tx is reverted, all data after it will also be reverted, so we call remove + // all indices after it. + let tx_seq_list = match modified_merkle_root_map.entry(tx.data_merkle_root) { + Entry::Occupied(e) => e.into_mut(), + Entry::Vacant(e) => { + e.insert(self.get_tx_seq_list_by_data_root(&tx.data_merkle_root)?) + } + }; + tx_seq_list.retain(|e| *e < seq); + removed_txs.push(tx); + } + for (merkle_root, tx_seq_list) in modified_merkle_root_map { + if tx_seq_list.is_empty() { + db_tx.delete(COL_TX_DATA_ROOT_INDEX, merkle_root.as_bytes()); + } else { + db_tx.put( + COL_TX_DATA_ROOT_INDEX, + merkle_root.as_bytes(), + &tx_seq_list.as_ssz_bytes(), + ); + } + } + db_tx.put(COL_TX, NEXT_TX_KEY.as_bytes(), &min_seq.to_be_bytes()); + self.next_tx_seq.store(min_seq, Ordering::SeqCst); + self.kvdb.write(db_tx)?; + Ok(removed_txs) + } + + pub fn get_tx_seq_list_by_data_root(&self, data_root: &DataRoot) -> Result> { + let value = match self + .kvdb + .get(COL_TX_DATA_ROOT_INDEX, data_root.as_bytes())? + { + Some(v) => v, + None => return Ok(Vec::new()), + }; + Ok(Vec::::from_ssz_bytes(&value).map_err(Error::from)?) + } + + pub fn get_first_tx_seq_by_data_root(&self, data_root: &DataRoot) -> Result> { + let value = try_option!(self + .kvdb + .get(COL_TX_DATA_ROOT_INDEX, data_root.as_bytes())?); + let seq_list = Vec::::from_ssz_bytes(&value).map_err(Error::from)?; + Ok(seq_list.first().cloned()) + } + + #[instrument(skip(self))] + pub fn finalize_tx(&self, tx_seq: u64) -> Result<()> { + Ok(self + .kvdb + .put(COL_TX_COMPLETED, &tx_seq.to_be_bytes(), &[0])?) + } + + pub fn check_tx_completed(&self, tx_seq: u64) -> Result { + Ok(self.kvdb.has_key(COL_TX_COMPLETED, &tx_seq.to_be_bytes())?) + } + + pub fn next_tx_seq(&self) -> u64 { + self.next_tx_seq.load(Ordering::SeqCst) + } + + #[instrument(skip(self))] + pub fn put_progress(&self, progress: (u64, H256)) -> Result<()> { + Ok(self.kvdb.put( + COL_MISC, + LOG_SYNC_PROGRESS_KEY.as_bytes(), + &progress.as_ssz_bytes(), + )?) + } + + #[instrument(skip(self))] + pub fn get_progress(&self) -> Result> { + Ok(Some( + <(u64, H256)>::from_ssz_bytes(&try_option!(self + .kvdb + .get(COL_MISC, LOG_SYNC_PROGRESS_KEY.as_bytes())?)) + .map_err(Error::from)?, + )) + } + + /// Build the merkle tree at `pora_chunk_index` with the data before (including) `tx_seq`. + /// This first rebuild the tree with the tx root nodes lists by repeatedly checking previous + /// until we reach the start of this chunk. + /// + /// Note that this can only be called with the last chunk after some transaction is committed, + /// otherwise the start of this chunk might be within some tx subtree and this will panic. + // TODO(zz): Fill the last chunk with data. + pub fn rebuild_last_chunk_merkle( + &self, + pora_chunk_index: usize, + mut tx_seq: u64, + ) -> Result> { + let last_chunk_start_index = pora_chunk_index as u64 * PORA_CHUNK_SIZE as u64; + let mut tx_list = Vec::new(); + // Find the first tx within the last chunk. + loop { + let tx = self.get_tx_by_seq_number(tx_seq)?.expect("tx not removed"); + match tx.start_entry_index.cmp(&last_chunk_start_index) { + cmp::Ordering::Greater => { + tx_list.push((tx_seq, tx.merkle_nodes)); + } + cmp::Ordering::Equal => { + tx_list.push((tx_seq, tx.merkle_nodes)); + break; + } + cmp::Ordering::Less => { + // The transaction data crosses a chunk, so we need to find the subtrees + // within the last chunk. + let mut start_index = tx.start_entry_index; + let mut first_index = None; + for (i, (depth, _)) in tx.merkle_nodes.iter().enumerate() { + start_index += 1 << (depth - 1); + if start_index == last_chunk_start_index { + first_index = Some(i + 1); + break; + } + } + // Some means some subtree ends at the chunk boundary. + // None means there are padding data between the tx data and the boundary, + // so no data belongs to the last chunk. + if let Some(first_index) = first_index { + if first_index != tx.merkle_nodes.len() { + tx_list.push((tx_seq, tx.merkle_nodes[first_index..].to_vec())); + } else { + // If the last subtree ends at the chunk boundary, we also do not need + // to add data of this tx to the last chunk. + // This is only possible if the last chunk is empty, because otherwise + // we should have entered the `Equal` condition before and + // have broken the loop. + assert!(tx_list.is_empty()); + } + } + break; + } + } + if tx_seq == 0 { + break; + } else { + tx_seq -= 1; + } + } + let mut merkle = if last_chunk_start_index == 0 { + // The first entry hash is initialized as zero. + AppendMerkleTree::::new_with_depth( + vec![H256::zero()], + log2_pow2(PORA_CHUNK_SIZE) + 1, + None, + ) + } else { + AppendMerkleTree::::new_with_depth( + vec![], + log2_pow2(PORA_CHUNK_SIZE) + 1, + None, + ) + }; + for (tx_seq, subtree_list) in tx_list.into_iter().rev() { + // Pad the tx. After the first subtree is padded, other subtrees should be aligned. + let first_subtree = 1 << (subtree_list[0].0 - 1); + if merkle.leaves() % first_subtree != 0 { + let pad_len = + cmp::min(first_subtree, PORA_CHUNK_SIZE) - (merkle.leaves() % first_subtree); + merkle.append_list(data_to_merkle_leaves(&LogManager::padding(pad_len))?); + } + // Since we are building the last merkle with a given last tx_seq, it's ensured + // that appending subtrees will not go beyond the max size. + merkle.append_subtree_list(subtree_list)?; + merkle.commit(Some(tx_seq)); + } + Ok(merkle) + } +} + +fn decode_tx_seq(data: &[u8]) -> Result { + Ok(u64::from_be_bytes( + data.try_into().map_err(|e| anyhow!("{:?}", e))?, + )) +} diff --git a/node/sync/Cargo.toml b/node/sync/Cargo.toml new file mode 100644 index 0000000..d2f800a --- /dev/null +++ b/node/sync/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "sync" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "1.0.58", features = ["backtrace"] } +append_merkle = { path = "../../common/append_merkle" } +channel = { path = "../../common/channel" } +file_location_cache = { path = "../file_location_cache" } +log_entry_sync = { path = "../log_entry_sync" } +network = { path = "../network" } +rand = "0.8.5" +shared_types = { path = "../shared_types" } +storage = { path = "../storage" } +storage-async = { path = "../storage-async" } +task_executor = { path = "../../common/task_executor" } +tokio = { version = "1.19.2", features = ["full"] } +tracing = "0.1.35" +eth2_ssz = "0.4.0" +serde = { version = "1.0.137", features = ["derive"] } +duration-str = "0.5.1" + +[dev-dependencies] +merkle_light = { path = "../../common/merkle_light" } + +[dependencies.libp2p] +version = "0.45.1" +default-features = false +features = ["websocket", "identify", "mplex", "yamux", "noise", "gossipsub", "dns-tokio", "tcp-tokio", "plaintext", "secp256k1"] \ No newline at end of file diff --git a/node/sync/src/auto_sync/manager.rs b/node/sync/src/auto_sync/manager.rs new file mode 100644 index 0000000..4f8e93c --- /dev/null +++ b/node/sync/src/auto_sync/manager.rs @@ -0,0 +1,667 @@ +use super::sync_store::SyncStore; +use crate::{controllers::SyncState, Config, SyncRequest, SyncResponse, SyncSender}; +use anyhow::{bail, Result}; +use log_entry_sync::LogSyncEvent; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; +use std::time::Duration; +use storage_async::Store; +use task_executor::TaskExecutor; +use tokio::sync::broadcast::{error::RecvError, Receiver}; +use tokio::time::sleep; + +const INTERVAL_CATCHUP: Duration = Duration::from_millis(1); +const INTERVAL: Duration = Duration::from_secs(1); +const INTERVAL_ERROR: Duration = Duration::from_secs(10); + +/// Manager to synchronize files among storage nodes automatically. +/// +/// Generally, most files could be synchronized among storage nodes. However, a few +/// files may be unavailable on all storage nodes, e.g. +/// +/// 1. File not uploaded by user in time. +/// 2. File removed due to blockchain reorg, and user do not upload again. +/// +/// So, there are 2 workers to synchronize files in parallel: +/// +/// 1. Synchronize announced files in sequence. If any file unavailable, store it into db. +/// 2. Synchronize the missed files in sequential synchronization. +#[derive(Clone)] +pub struct Manager { + config: Config, + + /// The next `tx_seq` to sync in sequence. + next_tx_seq: Arc, + + /// The maximum `tx_seq` to sync in sequence, `u64::MAX` means unlimited. + /// Generally, it is updated when file announcement received. + max_tx_seq: Arc, + + /// The last reverted transaction seq, `u64::MAX` means no tx reverted. + /// Generally, it is updated when transaction reverted. + reverted_tx_seq: Arc, + + store: Store, + sync_store: SyncStore, + + /// Used to interact with sync service for the current file in sync. + sync_send: SyncSender, +} + +impl Manager { + pub async fn new(store: Store, sync_send: SyncSender, config: Config) -> Result { + let sync_store = SyncStore::new(store.clone()); + + let (next_tx_seq, max_tx_seq) = sync_store.get_tx_seq_range().await?; + let next_tx_seq = next_tx_seq.unwrap_or(0); + let max_tx_seq = max_tx_seq.unwrap_or(u64::MAX); + + Ok(Self { + config, + next_tx_seq: Arc::new(AtomicU64::new(next_tx_seq)), + max_tx_seq: Arc::new(AtomicU64::new(max_tx_seq)), + reverted_tx_seq: Arc::new(AtomicU64::new(u64::MAX)), + store, + sync_store, + sync_send, + }) + } + + pub fn spwn(&self, executor: &TaskExecutor, receiver: Receiver) { + executor.spawn( + self.clone().monitor_reorg(receiver), + "sync_manager_reorg_monitor", + ); + + executor.spawn(self.clone().start_sync(), "sync_manager_sequential_syncer"); + + executor.spawn( + self.clone().start_sync_pending_txs(), + "sync_manager_pending_syncer", + ); + } + + fn set_reverted(&self, tx_seq: u64) -> bool { + if tx_seq >= self.reverted_tx_seq.load(Ordering::Relaxed) { + return false; + } + + self.reverted_tx_seq.store(tx_seq, Ordering::Relaxed); + + true + } + + fn handle_on_reorg(&self) -> Option { + let reverted_tx_seq = self.reverted_tx_seq.load(Ordering::Relaxed); + + // no reorg happened + if reverted_tx_seq == u64::MAX { + return None; + } + + self.reverted_tx_seq.store(u64::MAX, Ordering::Relaxed); + + // reorg happened, but no impact on file sync + let next_tx_seq = self.next_tx_seq.load(Ordering::Relaxed); + if reverted_tx_seq > next_tx_seq { + return None; + } + + // handles on reorg + info!(%reverted_tx_seq, %next_tx_seq, "Transaction reverted"); + + // re-sync files from the reverted tx seq + self.next_tx_seq.store(reverted_tx_seq, Ordering::Relaxed); + + Some(next_tx_seq) + } + + pub async fn update_on_announcement(&self, announced_tx_seq: u64) { + // new file announced + let max_tx_seq = self.max_tx_seq.load(Ordering::Relaxed); + if max_tx_seq == u64::MAX || announced_tx_seq > max_tx_seq { + match self.sync_store.set_max_tx_seq(announced_tx_seq).await { + Ok(()) => self.max_tx_seq.store(announced_tx_seq, Ordering::Relaxed), + Err(e) => error!(%e, "Failed to set max_tx_seq in store"), + }; + return; + } + + // already wait for sequential sync + if announced_tx_seq >= self.next_tx_seq.load(Ordering::Relaxed) { + return; + } + + // otherwise, mark tx as ready for sync + if let Err(e) = self.sync_store.upgrade_tx_to_ready(announced_tx_seq).await { + error!(%e, "Failed to promote announced tx to ready"); + } + } + + async fn move_forward(&self, pending: bool) -> Result { + let tx_seq = self.next_tx_seq.load(Ordering::Relaxed); + if tx_seq > self.max_tx_seq.load(Ordering::Relaxed) { + return Ok(false); + } + + // put the tx into pending list + if pending && self.sync_store.add_pending_tx(tx_seq).await? { + debug!(%tx_seq, "Pending tx added"); + } + + let next_tx_seq = tx_seq + 1; + self.sync_store.set_next_tx_seq(next_tx_seq).await?; + self.next_tx_seq.store(next_tx_seq, Ordering::Relaxed); + + Ok(true) + } + + /// Returns whether file sync in progress but no peers found + async fn sync_tx(&self, tx_seq: u64) -> Result { + // tx not available yet + if self.store.get_tx_by_seq_number(tx_seq).await?.is_none() { + return Ok(false); + } + + // get sync state to handle in advance + let state = match self + .sync_send + .request(SyncRequest::SyncStatus { tx_seq }) + .await? + { + SyncResponse::SyncStatus { status } => status, + _ => bail!("Invalid sync response type"), + }; + trace!(?tx_seq, ?state, "sync_tx tx status"); + + // notify service to sync file if not started or failed + if matches!(state, None | Some(SyncState::Failed { .. })) { + match self + .sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await? + { + SyncResponse::SyncFile { err } if err.is_empty() => return Ok(false), + SyncResponse::SyncFile { err } => bail!("Failed to sync file: {:?}", err), + _ => bail!("Invalid sync response type"), + } + } + + if matches!(state, Some(SyncState::FindingPeers { since, .. }) if since.elapsed() > self.config.find_peer_timeout) + { + // no peers found for a long time + self.terminate_file_sync(tx_seq, false).await; + Ok(true) + } else { + // otherwise, continue to wait for file sync that already in progress + Ok(false) + } + } + + async fn terminate_file_sync(&self, tx_seq: u64, is_reverted: bool) { + if let Err(err) = self + .sync_send + .request(SyncRequest::TerminateFileSync { + tx_seq, + is_reverted, + }) + .await + { + // just log and go ahead for any error, e.g. timeout + error!(%err, %tx_seq, "Failed to terminate file sync"); + } + } + + /// Starts to monitor reorg and handle on transaction reverted. + async fn monitor_reorg(self, mut receiver: Receiver) { + info!("Start to monitor reorg"); + + loop { + match receiver.recv().await { + Ok(LogSyncEvent::ReorgDetected { .. }) => {} + Ok(LogSyncEvent::Reverted { tx_seq }) => { + // requires to re-sync files since transaction and files removed in storage + self.set_reverted(tx_seq); + } + Ok(LogSyncEvent::TxSynced { .. }) => {} //No need to handle synced tx in reorg + Err(RecvError::Closed) => { + // program terminated + info!("Completed to monitor reorg"); + return; + } + Err(RecvError::Lagged(lagged)) => { + // Generally, such error should not happen since confirmed block + // reorg rarely happen, and the buffer size of broadcast channel + // is big enough. + error!(%lagged, "Failed to receive reverted tx (Lagged)"); + } + } + } + } + + /// Starts to synchronize files in sequence. + async fn start_sync(self) { + info!( + "Start to sync files periodically, next = {}, max = {}", + self.next_tx_seq.load(Ordering::Relaxed), + self.max_tx_seq.load(Ordering::Relaxed) + ); + + loop { + // handles reorg before file sync + if let Some(tx_seq) = self.handle_on_reorg() { + // request sync service to terminate the file sync immediately + self.terminate_file_sync(tx_seq, true).await; + } + + // sync file + let sync_result = self.sync_once().await; + let next_tx_seq = self.next_tx_seq.load(Ordering::Relaxed); + match sync_result { + Ok(true) => { + debug!(%next_tx_seq, "Completed to sync file"); + sleep(INTERVAL_CATCHUP).await; + } + Ok(false) => { + trace!(%next_tx_seq, "File in sync or log entry unavailable"); + sleep(INTERVAL).await; + } + Err(err) => { + warn!(%err, %next_tx_seq, "Failed to sync file"); + sleep(INTERVAL_ERROR).await; + } + } + } + } + + async fn sync_once(&self) -> Result { + // already sync to the latest file + let next_tx_seq = self.next_tx_seq.load(Ordering::Relaxed); + if next_tx_seq > self.max_tx_seq.load(Ordering::Relaxed) { + return Ok(false); + } + + // already finalized + if self.store.check_tx_completed(next_tx_seq).await? { + self.move_forward(false).await?; + return Ok(true); + } + + // try sync tx + let no_peer_timeout = self.sync_tx(next_tx_seq).await?; + + // put tx to pending list if no peers found for a long time + if no_peer_timeout { + self.move_forward(true).await?; + } + + Ok(no_peer_timeout) + } + + /// Starts to synchronize pending files that unavailable during sequential synchronization. + async fn start_sync_pending_txs(self) { + info!("Start to sync pending files"); + + let mut tx_seq = 0; + let mut next = true; + + loop { + if next { + match self.sync_store.random_tx().await { + Ok(Some(seq)) => tx_seq = seq, + Ok(None) => { + trace!("No pending file to sync"); + sleep(INTERVAL).await; + continue; + } + Err(err) => { + warn!(%err, "Failed to pick pending file to sync"); + sleep(INTERVAL_ERROR).await; + continue; + } + } + } + + match self.sync_pending_tx(tx_seq).await { + Ok(true) => { + debug!(%tx_seq, "Completed to sync pending file"); + sleep(INTERVAL_CATCHUP).await; + next = true; + } + Ok(false) => { + trace!(%tx_seq, "Pending file in sync or tx unavailable"); + sleep(INTERVAL).await; + next = false; + } + Err(err) => { + warn!(%err, %tx_seq, "Failed to sync pending file"); + sleep(INTERVAL_ERROR).await; + next = false; + } + } + } + } + + async fn sync_pending_tx(&self, tx_seq: u64) -> Result { + // already finalized + if self.store.check_tx_completed(tx_seq).await? { + self.sync_store.remove_tx(tx_seq).await?; + return Ok(true); + } + + // try sync tx + let no_peer_timeout = self.sync_tx(tx_seq).await?; + + // downgrade if no peers found for a long time + if no_peer_timeout && self.sync_store.downgrade_tx_to_pending(tx_seq).await? { + debug!(%tx_seq, "No peers found for pending file and downgraded"); + } + + Ok(no_peer_timeout) + } +} + +#[cfg(test)] +mod tests { + use std::{ + ops::Sub, + sync::atomic::Ordering, + time::{Duration, Instant}, + }; + + use channel::{test_util::TestReceiver, Channel}; + use tokio::sync::mpsc::error::TryRecvError; + + use crate::{ + auto_sync::sync_store::SyncStore, + controllers::SyncState, + test_util::{create_2_store, tests::TestStoreRuntime}, + Config, SyncMessage, SyncRequest, SyncResponse, + }; + + use super::Manager; + + async fn new_manager( + runtime: &TestStoreRuntime, + next_tx_seq: u64, + max_tx_seq: u64, + ) -> ( + Manager, + TestReceiver, + ) { + let sync_store = SyncStore::new(runtime.store.clone()); + sync_store.set_next_tx_seq(next_tx_seq).await.unwrap(); + if max_tx_seq < u64::MAX { + sync_store.set_max_tx_seq(max_tx_seq).await.unwrap(); + } + + let (sync_send, sync_recv) = Channel::unbounded(); + let manager = Manager::new(runtime.store.clone(), sync_send, Config::default()) + .await + .unwrap(); + (manager, sync_recv.into()) + } + + #[tokio::test] + async fn test_manager_init_values() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 12); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), u64::MAX); + } + + #[tokio::test] + async fn test_manager_set_reverted() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + // reverted to tx 5 + assert!(manager.set_reverted(5)); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), 5); + + // no effect if tx 6 reverted again + assert!(!manager.set_reverted(6)); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), 5); + + // overwrite tx 5 if tx 3 reverted + assert!(manager.set_reverted(3)); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), 3); + } + + #[tokio::test] + async fn test_manager_handle_reorg() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + // no effect if not reverted + assert_eq!(manager.handle_on_reorg(), None); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), u64::MAX); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + + // tx 5 reverted, but sync in future + assert!(manager.set_reverted(5)); + assert_eq!(manager.handle_on_reorg(), None); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), u64::MAX); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + + // tx 3 reverted, should terminate tx 4 and re-sync files since tx 3 + assert!(manager.set_reverted(3)); + assert_eq!(manager.handle_on_reorg(), Some(4)); + assert_eq!(manager.reverted_tx_seq.load(Ordering::Relaxed), u64::MAX); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 3); + } + + #[tokio::test] + async fn test_manager_update_on_announcement() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + // no effect if tx 10 announced + manager.update_on_announcement(10).await; + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 12); + + // `max_tx_seq` enlarged if tx 20 announced + manager.update_on_announcement(20).await; + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 20); + + // no effect if announced for a non-pending tx + manager.update_on_announcement(2).await; + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 4); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 20); + assert_eq!(manager.sync_store.random_tx().await.unwrap(), None); + + // pending tx upgraded if announcement received + assert!(manager.sync_store.add_pending_tx(1).await.unwrap()); + assert!(manager.sync_store.add_pending_tx(2).await.unwrap()); + manager.update_on_announcement(2).await; + assert_eq!(manager.sync_store.random_tx().await.unwrap(), Some(2)); + } + + #[tokio::test] + async fn test_manager_move_forward() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + // move forward from 4 to 5 + assert!(manager.move_forward(false).await.unwrap()); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 5); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 12); + assert_eq!( + manager.sync_store.get_tx_seq_range().await.unwrap(), + (Some(5), Some(12)) + ); + + // move forward and add tx 5 to pending list + assert!(manager.move_forward(true).await.unwrap()); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 6); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 12); + assert_eq!( + manager.sync_store.get_tx_seq_range().await.unwrap(), + (Some(6), Some(12)) + ); + assert_eq!(manager.sync_store.random_tx().await.unwrap(), Some(5)); + } + + #[tokio::test] + async fn test_manager_move_forward_failed() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 5, 5).await; + + // 5 -> 6 + assert!(manager.move_forward(false).await.unwrap()); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 6); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 5); + + // cannot move forward anymore + assert!(!manager.move_forward(false).await.unwrap()); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 6); + assert_eq!(manager.max_tx_seq.load(Ordering::Relaxed), 5); + assert_eq!( + manager.sync_store.get_tx_seq_range().await.unwrap(), + (Some(6), Some(5)) + ); + } + + #[tokio::test] + async fn test_manager_sync_tx_unavailable() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + assert!(!manager.sync_tx(4).await.unwrap()); + } + + #[tokio::test] + async fn test_manager_sync_tx_status_none() { + let (_, store, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, mut sync_recv) = new_manager(&runtime, 1, 5).await; + + let (_, sync_result) = tokio::join!( + sync_recv.expect_responses(vec![ + SyncResponse::SyncStatus { status: None }, + // cause to file sync started + SyncResponse::SyncFile { err: String::new() }, + ]), + manager.sync_tx(1) + ); + assert!(!sync_result.unwrap()); + assert!(matches!(sync_recv.try_recv(), Err(TryRecvError::Empty))); + } + + #[tokio::test] + async fn test_manager_sync_tx_in_progress() { + let (_, store, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, mut sync_recv) = new_manager(&runtime, 1, 5).await; + + let (_, sync_result) = tokio::join!( + // unnecessary to start file sync again + sync_recv.expect_response(SyncResponse::SyncStatus { + status: Some(SyncState::ConnectingPeers) + }), + manager.sync_tx(1) + ); + assert!(!sync_result.unwrap()); + assert!(matches!(sync_recv.try_recv(), Err(TryRecvError::Empty))); + } + + async fn expect_no_peer_found( + sync_recv: &mut TestReceiver, + ) { + let responses = vec![ + // no peers for file sync for a long time + SyncResponse::SyncStatus { + status: Some(SyncState::FindingPeers { + since: Instant::now().sub(Duration::from_secs(10000)), + updated: Instant::now(), + }), + }, + // required to terminate the file sync + SyncResponse::TerminateFileSync { count: 1 }, + ]; + sync_recv.expect_responses(responses).await + } + + #[tokio::test] + async fn test_manager_sync_tx_no_peer_found() { + let (_, store, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, mut sync_recv) = new_manager(&runtime, 1, 5).await; + + let (_, sync_result) = + tokio::join!(expect_no_peer_found(&mut sync_recv), manager.sync_tx(1)); + assert!(sync_result.unwrap()); + assert!(matches!(sync_recv.try_recv(), Err(TryRecvError::Empty))); + } + + #[tokio::test] + async fn test_manager_sync_once_already_latest() { + let runtime = TestStoreRuntime::default(); + let (manager, _sync_recv) = new_manager(&runtime, 6, 5).await; + + assert!(!manager.sync_once().await.unwrap()); + } + + #[tokio::test] + async fn test_manager_sync_once_finalized() { + let (_, store, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, _sync_recv) = new_manager(&runtime, 1, 5).await; + + assert!(manager.sync_once().await.unwrap()); + assert_eq!(manager.next_tx_seq.load(Ordering::Relaxed), 2); + assert_eq!(manager.sync_store.random_tx().await.unwrap(), None); + } + + #[tokio::test] + async fn test_manager_sync_once_no_peer_found() { + let (store, _, _, _) = create_2_store(vec![1314]); + let runtime = TestStoreRuntime::new(store); + let (manager, mut sync_recv) = new_manager(&runtime, 0, 5).await; + + let (_, sync_result) = + tokio::join!(expect_no_peer_found(&mut sync_recv), manager.sync_once(),); + assert!(sync_result.unwrap()); + assert!(matches!(sync_recv.try_recv(), Err(TryRecvError::Empty))); + assert_eq!(manager.sync_store.random_tx().await.unwrap(), Some(0)); + } + + #[tokio::test] + async fn test_manager_sync_pending_tx_finalized() { + let (_, store, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, _sync_recv) = new_manager(&runtime, 4, 12).await; + + assert!(manager.sync_store.add_pending_tx(0).await.unwrap()); + assert!(manager.sync_store.add_pending_tx(1).await.unwrap()); + + assert!(manager.sync_pending_tx(1).await.unwrap()); + assert_eq!(manager.sync_store.random_tx().await.unwrap(), Some(0)); + assert!(manager.sync_store.add_pending_tx(1).await.unwrap()); + } + + #[tokio::test] + async fn test_manager_sync_pending_tx_no_peer_found() { + let (store, _, _, _) = create_2_store(vec![1314, 1324]); + let runtime = TestStoreRuntime::new(store); + let (manager, mut sync_recv) = new_manager(&runtime, 4, 12).await; + + assert!(manager.sync_store.add_pending_tx(0).await.unwrap()); + assert!(manager.sync_store.add_pending_tx(1).await.unwrap()); + assert!(manager.sync_store.upgrade_tx_to_ready(1).await.unwrap()); + + let (_, sync_result) = tokio::join!( + expect_no_peer_found(&mut sync_recv), + manager.sync_pending_tx(1), + ); + assert!(sync_result.unwrap()); + assert!(matches!(sync_recv.try_recv(), Err(TryRecvError::Empty))); + assert!(manager.sync_store.upgrade_tx_to_ready(1).await.unwrap()); + } +} diff --git a/node/sync/src/auto_sync/mod.rs b/node/sync/src/auto_sync/mod.rs new file mode 100644 index 0000000..abf577c --- /dev/null +++ b/node/sync/src/auto_sync/mod.rs @@ -0,0 +1,5 @@ +mod manager; +mod sync_store; +mod tx_store; + +pub use manager::Manager as AutoSyncManager; diff --git a/node/sync/src/auto_sync/sync_store.rs b/node/sync/src/auto_sync/sync_store.rs new file mode 100644 index 0000000..7fa8bb5 --- /dev/null +++ b/node/sync/src/auto_sync/sync_store.rs @@ -0,0 +1,264 @@ +use super::tx_store::TxStore; +use anyhow::Result; +use std::ops::Deref; +use storage::log_store::config::{ConfigTx, ConfigurableExt}; +use storage_async::Store; + +const KEY_NEXT_TX_SEQ: &str = "sync.manager.next_tx_seq"; +const KEY_MAX_TX_SEQ: &str = "sync.manager.max_tx_seq"; + +#[derive(Clone)] +pub struct SyncStore { + store: Store, + + /// Pending transactions to sync with low priority. + pending_txs: TxStore, + + /// Ready transactions to sync with high priority since announcement + /// already received from other peers. + ready_txs: TxStore, +} + +impl SyncStore { + pub fn new(store: Store) -> Self { + Self { + store, + pending_txs: TxStore::new("pending"), + ready_txs: TxStore::new("ready"), + } + } + + pub async fn get_tx_seq_range(&self) -> Result<(Option, Option)> { + let store = self.store.get_store().read().await; + + // load next_tx_seq + let next_tx_seq = store.get_config_decoded(&KEY_NEXT_TX_SEQ)?; + + // load max_tx_seq + let max_tx_seq = store.get_config_decoded(&KEY_MAX_TX_SEQ)?; + + Ok((next_tx_seq, max_tx_seq)) + } + + pub async fn set_next_tx_seq(&self, tx_seq: u64) -> Result<()> { + self.store + .get_store() + .write() + .await + .set_config_encoded(&KEY_NEXT_TX_SEQ, &tx_seq) + } + + pub async fn set_max_tx_seq(&self, tx_seq: u64) -> Result<()> { + debug!(%tx_seq, "set_max_tx_seq"); + self.store + .get_store() + .write() + .await + .set_config_encoded(&KEY_MAX_TX_SEQ, &tx_seq) + } + + pub async fn add_pending_tx(&self, tx_seq: u64) -> Result { + let store = self.store.get_store().write().await; + + // already in ready queue + if self.ready_txs.has(store.deref(), tx_seq)? { + return Ok(false); + } + + // always add in pending queue + self.pending_txs.add(store.deref(), None, tx_seq) + } + + pub async fn upgrade_tx_to_ready(&self, tx_seq: u64) -> Result { + let store = self.store.get_store().write().await; + + let mut tx = ConfigTx::default(); + + // not in pending queue + if !self + .pending_txs + .remove(store.deref(), Some(&mut tx), tx_seq)? + { + return Ok(false); + } + + // move from pending to ready queue + let added = self.ready_txs.add(store.deref(), Some(&mut tx), tx_seq)?; + + store.exec_configs(tx)?; + + Ok(added) + } + + pub async fn downgrade_tx_to_pending(&self, tx_seq: u64) -> Result { + let store = self.store.get_store().write().await; + + let mut tx = ConfigTx::default(); + + // not in ready queue + if !self + .ready_txs + .remove(store.deref(), Some(&mut tx), tx_seq)? + { + return Ok(false); + } + + // move from ready to pending queue + let added = self.pending_txs.add(store.deref(), Some(&mut tx), tx_seq)?; + + store.exec_configs(tx)?; + + Ok(added) + } + + pub async fn random_tx(&self) -> Result> { + let store = self.store.get_store().read().await; + + // try to find a tx in ready queue with high priority + if let Some(val) = self.ready_txs.random(store.deref())? { + return Ok(Some(val)); + } + + // otherwise, find tx in pending queue + self.pending_txs.random(store.deref()) + } + + pub async fn remove_tx(&self, tx_seq: u64) -> Result { + let store = self.store.get_store().write().await; + + // removed in ready queue + if self.ready_txs.remove(store.deref(), None, tx_seq)? { + return Ok(true); + } + + // otherwise, try to remove in pending queue + self.pending_txs.remove(store.deref(), None, tx_seq) + } +} + +#[cfg(test)] +mod tests { + use crate::test_util::tests::TestStoreRuntime; + + use super::SyncStore; + + #[tokio::test] + async fn test_tx_seq_range() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // check values by default + assert_eq!(store.get_tx_seq_range().await.unwrap(), (None, None)); + + // update values + store.set_next_tx_seq(4).await.unwrap(); + store.set_max_tx_seq(12).await.unwrap(); + + // check values again + assert_eq!(store.get_tx_seq_range().await.unwrap(), (Some(4), Some(12))); + } + + #[tokio::test] + async fn test_add_pending_tx() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // add pending tx 3 + assert!(store.add_pending_tx(3).await.unwrap()); + + // cannot add pending tx 3 again + assert!(!store.add_pending_tx(3).await.unwrap()); + } + + #[tokio::test] + async fn test_upgrade_tx() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // cannot upgrade by default + assert!(!store.upgrade_tx_to_ready(3).await.unwrap()); + + // add pending tx 3 + assert!(store.add_pending_tx(3).await.unwrap()); + + // can upgrade to ready + assert!(store.upgrade_tx_to_ready(3).await.unwrap()); + + // cannot add pending tx 3 again event upgraded to ready + assert!(!store.add_pending_tx(3).await.unwrap()); + + // cannot upgrade again + assert!(!store.upgrade_tx_to_ready(3).await.unwrap()); + } + + #[tokio::test] + async fn test_downgrade_tx() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // cannot downgrade by default + assert!(!store.downgrade_tx_to_pending(3).await.unwrap()); + + // add pending tx 3 + assert!(store.add_pending_tx(3).await.unwrap()); + + // cannot downgrade for non-ready tx + assert!(!store.downgrade_tx_to_pending(3).await.unwrap()); + + // upgrade tx 3 to ready + assert!(store.upgrade_tx_to_ready(3).await.unwrap()); + + // can downgrade now + assert!(store.downgrade_tx_to_pending(3).await.unwrap()); + + // cannot downgrade now + assert!(!store.downgrade_tx_to_pending(3).await.unwrap()); + } + + #[tokio::test] + async fn test_random_tx() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // no tx by default + assert_eq!(store.random_tx().await.unwrap(), None); + + // add pending txs 1, 2, 3 + assert!(store.add_pending_tx(1).await.unwrap()); + assert!(store.add_pending_tx(2).await.unwrap()); + assert!(store.add_pending_tx(3).await.unwrap()); + let tx = store.random_tx().await.unwrap().unwrap(); + assert!((1..=3).contains(&tx)); + + // upgrade tx 1 to ready + assert!(store.upgrade_tx_to_ready(2).await.unwrap()); + assert_eq!(store.random_tx().await.unwrap(), Some(2)); + } + + #[tokio::test] + async fn test_remove_tx() { + let runtime = TestStoreRuntime::default(); + let store = SyncStore::new(runtime.store.clone()); + + // cannot remove by default + assert!(!store.remove_tx(1).await.unwrap()); + + // add pending tx 1, 2 + assert!(store.add_pending_tx(1).await.unwrap()); + assert!(store.add_pending_tx(2).await.unwrap()); + + // upgrade tx 1 to ready + assert!(store.upgrade_tx_to_ready(1).await.unwrap()); + assert_eq!(store.random_tx().await.unwrap(), Some(1)); + + // remove tx 1 + assert!(store.remove_tx(1).await.unwrap()); + assert_eq!(store.random_tx().await.unwrap(), Some(2)); + assert!(!store.remove_tx(1).await.unwrap()); + + // remove tx 2 + assert!(store.remove_tx(2).await.unwrap()); + assert_eq!(store.random_tx().await.unwrap(), None); + assert!(!store.remove_tx(2).await.unwrap()); + } +} diff --git a/node/sync/src/auto_sync/tx_store.rs b/node/sync/src/auto_sync/tx_store.rs new file mode 100644 index 0000000..3c0de45 --- /dev/null +++ b/node/sync/src/auto_sync/tx_store.rs @@ -0,0 +1,242 @@ +use anyhow::Result; +use rand::Rng; +use storage::log_store::config::{ConfigTx, ConfigurableExt}; +use storage::log_store::Store; + +/// TxStore is used to store pending transactions that to be synchronized in advance. +/// +/// Basically, this store maintains an enumerable map data structure for `tx_seq`. +#[derive(Clone)] +pub struct TxStore { + /// To allow multiple `TxStore` with different priority. + name: &'static str, + + /// DB key for `count` value. + key_count: String, +} + +impl TxStore { + pub fn new(name: &'static str) -> Self { + Self { + name, + key_count: format!("sync.manager.txs.{}.count", name), + } + } + + fn key_seq_to_index(&self, tx_seq: u64) -> String { + format!("sync.manager.txs.{}.seq2index.{}", self.name, tx_seq) + } + + fn key_index_to_seq(&self, index: usize) -> String { + format!("sync.manager.txs.{}.index2seq.{}", self.name, index) + } + + fn index_of(&self, store: &dyn Store, tx_seq: u64) -> Result> { + store.get_config_decoded(&self.key_seq_to_index(tx_seq)) + } + + fn at(&self, store: &dyn Store, index: usize) -> Result> { + store.get_config_decoded(&self.key_index_to_seq(index)) + } + + pub fn has(&self, store: &dyn Store, tx_seq: u64) -> Result { + self.index_of(store, tx_seq).map(|idx| idx.is_some()) + } + + pub fn count(&self, store: &dyn Store) -> Result { + store + .get_config_decoded(&self.key_count) + .map(|x| x.unwrap_or(0)) + } + + pub fn add( + &self, + store: &dyn Store, + db_tx: Option<&mut ConfigTx>, + tx_seq: u64, + ) -> Result { + // already exists + if self.has(store, tx_seq)? { + return Ok(false); + } + + let count = self.count(store)?; + + let mut tx = ConfigTx::default(); + tx.set_config(&self.key_index_to_seq(count), &tx_seq); + tx.set_config(&self.key_seq_to_index(tx_seq), &count); + tx.set_config(&self.key_count, &(count + 1)); + + if let Some(db_tx) = db_tx { + db_tx.append(&mut tx); + } else { + store.exec_configs(tx)?; + } + + Ok(true) + } + + pub fn random(&self, store: &dyn Store) -> Result> { + let count = self.count(store)?; + if count == 0 { + return Ok(None); + } + + let index = rand::thread_rng().gen_range(0..count); + let tx_seq = self.at(store, index)?.expect("data corruption"); + + Ok(Some(tx_seq)) + } + + pub fn remove( + &self, + store: &dyn Store, + db_tx: Option<&mut ConfigTx>, + tx_seq: u64, + ) -> Result { + let index = match self.index_of(store, tx_seq)? { + Some(val) => val, + None => return Ok(false), + }; + + let count = self.count(store)?; + assert!(count > 0, "data corruption"); + + let mut tx = ConfigTx::default(); + + // update `count` value + tx.set_config(&self.key_count, &(count - 1)); + + // remove `seq2index` index + tx.remove_config(&self.key_seq_to_index(tx_seq)); + + if index == count - 1 { + // remove `index2seq` index for the last element + tx.remove_config(&self.key_index_to_seq(index)); + } else { + // swap `back` to the `removed` slot + let last_tx = self.at(store, count - 1)?.expect("data corruption"); + + // update the `index2seq` for the removed element + tx.set_config(&self.key_index_to_seq(index), &last_tx); + + // remove the last slot + tx.remove_config(&self.key_index_to_seq(count - 1)); + + // update `seq2index` index for the last tx + tx.set_config(&self.key_seq_to_index(last_tx), &index); + } + + if let Some(db_tx) = db_tx { + db_tx.append(&mut tx); + } else { + store.exec_configs(tx)?; + } + + Ok(true) + } +} + +#[cfg(test)] +mod tests { + use crate::test_util::tests::TestStoreRuntime; + + use super::TxStore; + + #[test] + fn test_add() { + let store = TestStoreRuntime::new_store(); + let tx_store = TxStore::new("foo"); + + // count is 0 by default + assert_eq!(tx_store.count(&store).unwrap(), 0); + + // add 3 txs + assert!(tx_store.add(&store, None, 1).unwrap()); + assert!(tx_store.add(&store, None, 2).unwrap()); + assert!(tx_store.add(&store, None, 3).unwrap()); + + // cannot add again + assert!(!tx_store.add(&store, None, 1).unwrap()); + assert!(!tx_store.add(&store, None, 2).unwrap()); + assert!(!tx_store.add(&store, None, 3).unwrap()); + + // count is 3 after insertion + assert_eq!(tx_store.count(&store).unwrap(), 3); + + // check index of tx + assert_eq!(tx_store.index_of(&store, 1).unwrap(), Some(0)); + assert_eq!(tx_store.index_of(&store, 2).unwrap(), Some(1)); + assert_eq!(tx_store.index_of(&store, 3).unwrap(), Some(2)); + assert_eq!(tx_store.index_of(&store, 4).unwrap(), None); + + // check tx of index + assert_eq!(tx_store.at(&store, 0).unwrap(), Some(1)); + assert_eq!(tx_store.at(&store, 1).unwrap(), Some(2)); + assert_eq!(tx_store.at(&store, 2).unwrap(), Some(3)); + assert_eq!(tx_store.at(&store, 3).unwrap(), None); + } + + #[test] + fn test_random() { + let store = TestStoreRuntime::new_store(); + let tx_store = TxStore::new("foo"); + + assert_eq!(tx_store.random(&store).unwrap(), None); + + assert!(tx_store.add(&store, None, 1).unwrap()); + assert!(tx_store.add(&store, None, 2).unwrap()); + assert!(tx_store.add(&store, None, 3).unwrap()); + + let tx_seq = tx_store + .random(&store) + .unwrap() + .expect("should randomly pick one"); + assert!((1..=3).contains(&tx_seq)); + } + + #[test] + fn test_remove_tail() { + let store = TestStoreRuntime::new_store(); + let tx_store = TxStore::new("foo"); + + assert!(tx_store.add(&store, None, 1).unwrap()); + assert!(tx_store.add(&store, None, 2).unwrap()); + assert!(tx_store.add(&store, None, 3).unwrap()); + + assert!(tx_store.remove(&store, None, 3).unwrap()); + + assert_eq!(tx_store.count(&store).unwrap(), 2); + assert_eq!(tx_store.index_of(&store, 1).unwrap(), Some(0)); + assert_eq!(tx_store.index_of(&store, 2).unwrap(), Some(1)); + assert_eq!(tx_store.index_of(&store, 3).unwrap(), None); + + assert_eq!(tx_store.at(&store, 0).unwrap(), Some(1)); + assert_eq!(tx_store.at(&store, 1).unwrap(), Some(2)); + assert_eq!(tx_store.at(&store, 2).unwrap(), None); + } + + #[test] + fn test_remove_swap() { + let store = TestStoreRuntime::new_store(); + let tx_store = TxStore::new("foo"); + + assert!(tx_store.add(&store, None, 1).unwrap()); + assert!(tx_store.add(&store, None, 2).unwrap()); + assert!(tx_store.add(&store, None, 3).unwrap()); + assert!(tx_store.add(&store, None, 4).unwrap()); + + assert!(tx_store.remove(&store, None, 2).unwrap()); + + assert_eq!(tx_store.count(&store).unwrap(), 3); + assert_eq!(tx_store.index_of(&store, 1).unwrap(), Some(0)); + assert_eq!(tx_store.index_of(&store, 2).unwrap(), None); + assert_eq!(tx_store.index_of(&store, 3).unwrap(), Some(2)); + assert_eq!(tx_store.index_of(&store, 4).unwrap(), Some(1)); + + assert_eq!(tx_store.at(&store, 0).unwrap(), Some(1)); + assert_eq!(tx_store.at(&store, 1).unwrap(), Some(4)); + assert_eq!(tx_store.at(&store, 2).unwrap(), Some(3)); + assert_eq!(tx_store.at(&store, 3).unwrap(), None); + } +} diff --git a/node/sync/src/context.rs b/node/sync/src/context.rs new file mode 100644 index 0000000..9b62cd6 --- /dev/null +++ b/node/sync/src/context.rs @@ -0,0 +1,46 @@ +use network::{NetworkMessage, PeerAction, PeerId, PubsubMessage, ReportSource}; +use tokio::sync::mpsc; + +pub struct SyncNetworkContext { + network_send: mpsc::UnboundedSender, +} + +impl SyncNetworkContext { + pub fn new(network_send: mpsc::UnboundedSender) -> Self { + Self { network_send } + } + + /// Sends an arbitrary network message. + pub fn send(&self, msg: NetworkMessage) { + self.network_send.send(msg).unwrap_or_else(|_| { + warn!("Could not send message to the network service"); + }) + } + + /// Publishes a single message. + pub fn publish(&self, msg: PubsubMessage) { + self.send(NetworkMessage::Publish { + messages: vec![msg], + }); + } + + pub fn report_peer(&self, peer_id: PeerId, action: PeerAction, msg: &'static str) { + debug!(%peer_id, ?action, %msg, "Report peer"); + self.send(NetworkMessage::ReportPeer { + peer_id, + action, + source: ReportSource::SyncService, + msg, + }) + } + + pub fn ban_peer(&self, peer_id: PeerId, msg: &'static str) { + info!(%peer_id, %msg, "Ban peer"); + self.send(NetworkMessage::ReportPeer { + peer_id, + action: PeerAction::Fatal, + source: ReportSource::SyncService, + msg, + }) + } +} diff --git a/node/sync/src/controllers/mod.rs b/node/sync/src/controllers/mod.rs new file mode 100644 index 0000000..9eb4058 --- /dev/null +++ b/node/sync/src/controllers/mod.rs @@ -0,0 +1,16 @@ +mod peers; +mod serial; + +use serde::{Deserialize, Serialize}; + +pub use serial::{FailureReason, SerialSyncController, SyncState}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileSyncInfo { + pub elapsed_secs: u64, + pub peers: usize, + pub num_chunks: u64, + pub next_chunks: u64, + pub state: String, +} diff --git a/node/sync/src/controllers/peers.rs b/node/sync/src/controllers/peers.rs new file mode 100644 index 0000000..efac22c --- /dev/null +++ b/node/sync/src/controllers/peers.rs @@ -0,0 +1,306 @@ +use network::{Multiaddr, PeerId}; +use rand::seq::IteratorRandom; +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +const PEER_CONNECT_TIMEOUT: Duration = Duration::from_secs(5); +const PEER_DISCONNECT_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PeerState { + Found, + Connecting, + Connected, + Disconnecting, + Disconnected, +} + +struct PeerInfo { + /// The reported/connected address of the peer. + pub addr: Multiaddr, + + /// The current state of the peer. + pub state: PeerState, + + /// Timestamp of the last state change. + pub since: Instant, +} + +impl PeerInfo { + fn update_state(&mut self, new_state: PeerState) { + self.state = new_state; + self.since = Instant::now(); + } +} + +#[derive(Default)] +pub struct SyncPeers { + peers: HashMap, +} + +impl SyncPeers { + pub fn add_new_peer(&mut self, peer_id: PeerId, addr: Multiaddr) -> bool { + if self.peers.contains_key(&peer_id) { + return false; + } + + self.peers.insert( + peer_id, + PeerInfo { + addr, + state: PeerState::Found, + since: Instant::now(), + }, + ); + + true + } + + pub fn update_state( + &mut self, + peer_id: &PeerId, + from: PeerState, + to: PeerState, + ) -> Option { + let info = self.peers.get_mut(peer_id)?; + + if info.state == from { + info.update_state(to); + Some(true) + } else { + Some(false) + } + } + + pub fn update_state_force(&mut self, peer_id: &PeerId, state: PeerState) -> Option { + let info = self.peers.get_mut(peer_id)?; + let old_state = info.state; + info.state = state; + Some(old_state) + } + + pub fn peer_state(&self, peer_id: &PeerId) -> Option { + self.peers.get(peer_id).map(|info| info.state) + } + + pub fn random_peer(&self, state: PeerState) -> Option<(PeerId, Multiaddr)> { + self.peers + .iter() + .filter(|(_, info)| info.state == state) + .map(|(peer_id, info)| (*peer_id, info.addr.clone())) + .choose(&mut rand::thread_rng()) + } + + pub fn count(&self, states: &[PeerState]) -> usize { + self.peers + .values() + .filter(|info| states.contains(&info.state)) + .count() + } + + pub fn transition(&mut self) { + let mut bad_peers = vec![]; + + for (peer_id, info) in self.peers.iter_mut() { + match info.state { + PeerState::Found | PeerState::Connected => {} + + PeerState::Connecting => { + if info.since.elapsed() >= PEER_CONNECT_TIMEOUT { + info!(%peer_id, %info.addr, "Peer connection timeout"); + bad_peers.push(*peer_id); + } + } + + PeerState::Disconnecting => { + if info.since.elapsed() >= PEER_DISCONNECT_TIMEOUT { + info!(%peer_id, %info.addr, "Peer disconnect timeout"); + bad_peers.push(*peer_id); + } + } + + PeerState::Disconnected => bad_peers.push(*peer_id), + } + } + + for peer_id in bad_peers { + self.peers.remove(&peer_id); + } + } +} + +#[cfg(test)] +mod tests { + use libp2p::identity; + use std::collections::HashSet; + + use super::*; + + #[test] + fn test_add_new_peer() { + let mut sync_peers: SyncPeers = Default::default(); + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + assert!(sync_peers.add_new_peer(peer_id, addr.clone())); + assert!(!sync_peers.add_new_peer(peer_id, addr)); + } + + #[test] + fn test_update_state() { + let mut sync_peers: SyncPeers = Default::default(); + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + assert_eq!( + sync_peers.update_state(&peer_id, PeerState::Found, PeerState::Connecting), + None + ); + assert_eq!(sync_peers.peer_state(&peer_id), None); + + sync_peers.add_new_peer(peer_id, addr); + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Found)); + + assert_eq!( + sync_peers.update_state(&peer_id, PeerState::Found, PeerState::Connecting), + Some(true) + ); + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Connecting)); + + assert_eq!( + sync_peers.update_state(&peer_id, PeerState::Found, PeerState::Connected), + Some(false) + ); + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Connecting)); + } + + #[test] + fn test_update_state_force() { + let mut sync_peers: SyncPeers = Default::default(); + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + assert_eq!( + sync_peers.update_state_force(&peer_id, PeerState::Connecting), + None + ); + assert_eq!(sync_peers.peer_state(&peer_id), None); + + sync_peers.add_new_peer(peer_id, addr); + + assert_eq!( + sync_peers.update_state_force(&peer_id, PeerState::Connecting), + Some(PeerState::Found) + ); + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Connecting)); + } + + #[test] + fn test_random_peer() { + let count = 10; + let mut sync_peers: SyncPeers = Default::default(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + let mut peers_found = HashSet::new(); + let mut peers_connecting = HashSet::new(); + + for i in 0..count { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id, addr.clone()); + peers_found.insert(peer_id); + + assert_eq!(sync_peers.count(&[PeerState::Found]), i + 1); + assert_eq!(sync_peers.count(&[PeerState::Connecting]), 0); + assert_eq!( + sync_peers.count(&[PeerState::Found, PeerState::Connecting]), + i + 1 + ); + } + + for i in 0..count { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id, addr.clone()); + sync_peers.update_state_force(&peer_id, PeerState::Connecting); + peers_connecting.insert(peer_id); + + assert_eq!(sync_peers.count(&[PeerState::Found]), count); + assert_eq!(sync_peers.count(&[PeerState::Connecting]), i + 1); + assert_eq!( + sync_peers.count(&[PeerState::Found, PeerState::Connecting]), + count + i + 1 + ); + } + + // random pick + for _ in 0..30 { + let peer = sync_peers.random_peer(PeerState::Found).unwrap(); + assert!(peers_found.contains(&peer.0)); + assert_eq!(peer.1, addr); + let peer = sync_peers.random_peer(PeerState::Connecting).unwrap(); + assert!(peers_connecting.contains(&peer.0)); + assert_eq!(peer.1, addr); + assert!(sync_peers.random_peer(PeerState::Disconnected).is_none()); + } + } + + #[test] + fn test_transition() { + let mut sync_peers: SyncPeers = Default::default(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id, addr.clone()); + + let peer_id_connected = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id_connected, addr.clone()); + sync_peers.update_state_force(&peer_id_connected, PeerState::Connected); + + let peer_id_connecting = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id_connecting, addr.clone()); + sync_peers.update_state_force(&peer_id_connecting, PeerState::Connecting); + sync_peers.peers.get_mut(&peer_id_connecting).unwrap().since = + Instant::now() - PEER_CONNECT_TIMEOUT; + + let peer_id_disconnecting = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id_disconnecting, addr.clone()); + sync_peers.update_state_force(&peer_id_disconnecting, PeerState::Disconnecting); + sync_peers + .peers + .get_mut(&peer_id_disconnecting) + .unwrap() + .since = Instant::now() - PEER_DISCONNECT_TIMEOUT; + + let peer_id_disconnected = identity::Keypair::generate_ed25519().public().to_peer_id(); + sync_peers.add_new_peer(peer_id_disconnected, addr); + sync_peers.update_state_force(&peer_id_disconnected, PeerState::Disconnected); + + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Found)); + assert_eq!( + sync_peers.peer_state(&peer_id_connected), + Some(PeerState::Connected) + ); + assert_eq!( + sync_peers.peer_state(&peer_id_connecting), + Some(PeerState::Connecting) + ); + assert_eq!( + sync_peers.peer_state(&peer_id_disconnecting), + Some(PeerState::Disconnecting) + ); + assert_eq!( + sync_peers.peer_state(&peer_id_disconnected), + Some(PeerState::Disconnected) + ); + + sync_peers.transition(); + + assert_eq!(sync_peers.peer_state(&peer_id), Some(PeerState::Found)); + assert_eq!( + sync_peers.peer_state(&peer_id_connected), + Some(PeerState::Connected) + ); + assert_eq!(sync_peers.peer_state(&peer_id_connecting), None); + assert_eq!(sync_peers.peer_state(&peer_id_disconnecting), None); + assert_eq!(sync_peers.peer_state(&peer_id_disconnected), None); + } +} diff --git a/node/sync/src/controllers/serial.rs b/node/sync/src/controllers/serial.rs new file mode 100644 index 0000000..91d672c --- /dev/null +++ b/node/sync/src/controllers/serial.rs @@ -0,0 +1,1510 @@ +use crate::context::SyncNetworkContext; +use crate::controllers::peers::{PeerState, SyncPeers}; +use crate::controllers::FileSyncInfo; +use file_location_cache::FileLocationCache; +use libp2p::swarm::DialError; +use network::{ + multiaddr::Protocol, rpc::GetChunksRequest, types::FindFile, Multiaddr, NetworkMessage, + PeerAction, PeerId, PubsubMessage, SyncId as RequestId, +}; +use shared_types::{timestamp_now, ChunkArrayWithProof, TxID, CHUNK_SIZE}; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; +use storage_async::Store; + +const MAX_CHUNKS_TO_REQUEST: u64 = 2 * 1024; +const MAX_REQUEST_FAILURES: usize = 100; +const PEER_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); +const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(5); +const WAIT_OUTGOING_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); +const NEXT_REQUEST_WAIT_TIME: Duration = Duration::from_secs(3); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FailureReason { + DBError(String), + TxReverted(TxID), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SyncState { + Idle, + FindingPeers { + since: Instant, + updated: Instant, + }, + FoundPeers, + ConnectingPeers, + AwaitingOutgoingConnection { + since: Instant, + }, + AwaitingDownload { + since: Instant, + }, + Downloading { + peer_id: PeerId, + from_chunk: u64, + to_chunk: u64, + since: Instant, + }, + Completed, + Failed { + reason: FailureReason, + }, +} + +pub struct SerialSyncController { + // only used for log purpose + tx_seq: u64, + + /// The unique transaction ID. + tx_id: TxID, + + since: Instant, + + /// The size of the file to be synced. + num_chunks: u64, + + /// The next chunk id that we need to retrieve. + next_chunk: u64, + + /// Continuous RPC failures to request chunks. + failures: usize, + + /// Current state of this request. + state: SyncState, + + /// Sync peer manager. + peers: SyncPeers, + + /// A network context to contact the network service. + ctx: Arc, + + /// Log and transaction storage. + store: Store, + + /// Cache for storing and serving gossip messages. + file_location_cache: Arc, +} + +impl SerialSyncController { + pub fn new( + tx_id: TxID, + num_chunks: u64, + ctx: Arc, + store: Store, + file_location_cache: Arc, + ) -> Self { + SerialSyncController { + tx_seq: tx_id.seq, + tx_id, + since: Instant::now(), + num_chunks, + next_chunk: 0, + failures: 0, + state: SyncState::Idle, + peers: Default::default(), + ctx, + store, + file_location_cache, + } + } + + pub fn get_sync_info(&self) -> FileSyncInfo { + FileSyncInfo { + elapsed_secs: self.since.elapsed().as_secs(), + peers: self.peers.count(&[PeerState::Connected]), + num_chunks: self.num_chunks, + next_chunks: self.next_chunk, + state: format!("{:?}", self.state), + } + } + + pub fn get_status(&self) -> &SyncState { + &self.state + } + + /// Resets the status to re-sync file when failed. + pub fn reset(&mut self) { + self.next_chunk = 0; + self.failures = 0; + self.state = SyncState::Idle; + // remove disconnected peers + self.peers.transition(); + } + + fn try_find_peers(&mut self) { + info!(%self.tx_seq, "Finding peers"); + + // try from cache + let mut found_new_peer = false; + + for announcement in self.file_location_cache.get_all(self.tx_id) { + // make sure peer_id is part of the address + let peer_id: PeerId = announcement.peer_id.clone().into(); + let mut addr: Multiaddr = announcement.at.clone().into(); + addr.push(Protocol::P2p(peer_id.into())); + + found_new_peer = self.on_peer_found(peer_id, addr) || found_new_peer; + } + + if !found_new_peer { + self.ctx.publish(PubsubMessage::FindFile(FindFile { + tx_id: self.tx_id, + timestamp: timestamp_now(), + })); + } + + let now = Instant::now(); + + let (since, updated) = match self.state { + SyncState::FindingPeers { since, .. } => (since, now), + _ => (now, now), + }; + + self.state = SyncState::FindingPeers { since, updated }; + } + + fn try_connect(&mut self) { + // select a random peer + let (peer_id, address) = match self.peers.random_peer(PeerState::Found) { + Some((peer_id, address)) => (peer_id, address), + None => { + // peer may be disconnected by remote node and need to find peers again + warn!(%self.tx_seq, "No peers available to connect"); + self.state = SyncState::Idle; + return; + } + }; + + // connect to peer + info!(%peer_id, %address, "Attempting to connect to peer"); + self.ctx.send(NetworkMessage::DialPeer { address, peer_id }); + + self.peers + .update_state(&peer_id, PeerState::Found, PeerState::Connecting); + + self.state = SyncState::ConnectingPeers; + } + + fn try_request_next(&mut self) { + // select a random peer + let peer_id = match self.peers.random_peer(PeerState::Connected) { + Some((peer_id, _)) => peer_id, + None => { + warn!(%self.tx_seq, "No peers available to request chunks"); + self.state = SyncState::Idle; + return; + } + }; + + // request next chunk array + let from_chunk = self.next_chunk; + let to_chunk = std::cmp::min(from_chunk + MAX_CHUNKS_TO_REQUEST, self.num_chunks); + + let request_id = network::RequestId::Sync(RequestId::SerialSync { tx_id: self.tx_id }); + + let request = network::Request::GetChunks(GetChunksRequest { + tx_id: self.tx_id, + index_start: from_chunk, + index_end: to_chunk, + }); + + self.ctx.send(NetworkMessage::SendRequest { + peer_id, + request_id, + request, + }); + + self.state = SyncState::Downloading { + peer_id, + from_chunk, + to_chunk, + since: Instant::now(), + }; + } + + fn ban_peer(&mut self, peer_id: PeerId, reason: &'static str) { + self.ctx.ban_peer(peer_id, reason); + + self.peers + .update_state(&peer_id, PeerState::Connected, PeerState::Disconnecting); + } + + pub fn on_peer_found(&mut self, peer_id: PeerId, addr: Multiaddr) -> bool { + if self.peers.add_new_peer(peer_id, addr.clone()) { + info!(%self.tx_seq, %peer_id, %addr, "Found new peer"); + true + } else { + // e.g. multiple `AnnounceFile` messages propagated + debug!(%self.tx_seq, %peer_id, %addr, "Found an existing peer"); + false + } + } + + pub fn on_dail_failed(&mut self, peer_id: PeerId, err: &DialError) { + match err { + DialError::ConnectionLimit(_) => { + if let Some(true) = + self.peers + .update_state(&peer_id, PeerState::Connecting, PeerState::Found) + { + info!(%self.tx_seq, %peer_id, "Failed to dail peer due to outgoing connection limitation"); + self.state = SyncState::AwaitingOutgoingConnection { + since: Instant::now(), + }; + } + } + _ => { + if let Some(true) = self.peers.update_state( + &peer_id, + PeerState::Connecting, + PeerState::Disconnected, + ) { + info!(%self.tx_seq, %peer_id, "Failed to dail peer"); + self.state = SyncState::Idle; + } + } + } + } + + pub fn on_peer_connected(&mut self, peer_id: PeerId) { + if let Some(true) = + self.peers + .update_state(&peer_id, PeerState::Connecting, PeerState::Connected) + { + info!(%self.tx_seq, %peer_id, "Peer connected"); + } + } + + pub fn on_peer_disconnected(&mut self, peer_id: PeerId) { + match self + .peers + .update_state_force(&peer_id, PeerState::Disconnected) + { + Some(PeerState::Disconnecting) => info!(%self.tx_seq, %peer_id, "Peer disconnected"), + Some(old_state) => { + info!(%self.tx_seq, %peer_id, ?old_state, "Peer disconnected by remote"); + } + None => {} + } + } + + /// Handle the case that got an unexpected response: + /// 1. not in `Downloading` sync state. + /// 2. from unexpected peer. + fn handle_on_response_mismatch(&self, from_peer_id: PeerId) -> bool { + match self.state { + SyncState::Downloading { peer_id, .. } => { + if from_peer_id == peer_id { + return false; + } + + // got response from wrong peer + // this can happen if we get a response for a timeout request + warn!(%self.tx_seq, %from_peer_id, %peer_id, "Got response from unexpected peer"); + self.ctx.report_peer( + from_peer_id, + PeerAction::LowToleranceError, + "Peer id mismatch", + ); + true + } + _ => { + // FIXME(zz). Delayed response can enter this. + warn!(%self.tx_seq, %from_peer_id, ?self.state, "Got response in unexpected state"); + self.ctx.report_peer( + from_peer_id, + PeerAction::HighToleranceError, + "Sync state mismatch", + ); + true + } + } + } + + pub async fn on_response(&mut self, from_peer_id: PeerId, response: ChunkArrayWithProof) { + if self.handle_on_response_mismatch(from_peer_id) { + return; + } + + let (from_chunk, to_chunk) = match self.state { + SyncState::Downloading { + peer_id: _peer_id, + from_chunk, + to_chunk, + .. + } => (from_chunk, to_chunk), + _ => return, + }; + + debug_assert!(from_chunk < to_chunk, "Invalid chunk boundaries"); + + // invalid chunk array size: ban and re-request + let data_len = response.chunks.data.len(); + if data_len == 0 || data_len % CHUNK_SIZE > 0 { + warn!(%from_peer_id, %self.tx_seq, %data_len, "Invalid chunk response data length"); + self.ban_peer(from_peer_id, "Invalid chunk response data length"); + self.state = SyncState::Idle; + return; + } + + // invalid chunk range: ban and re-request + let start_index = response.chunks.start_index; + let end_index = start_index + (data_len / CHUNK_SIZE) as u64; + if start_index != from_chunk || end_index != to_chunk { + // FIXME(zz): Possible for relayed response. + warn!(%self.tx_seq, "Invalid chunk response range, expected={from_chunk}..{to_chunk}, actual={start_index}..{end_index}"); + // self.ban_peer(from_peer_id, "Invalid chunk response range"); + // self.state = SyncState::Idle; + return; + } + + // validate Merkle proofs + let validation_result = self + .store + .get_store() + .read() + .await + .validate_range_proof(self.tx_seq, &response); + + match validation_result { + Ok(true) => {} + Ok(false) => { + info!("Failed to validate chunks response due to no root found"); + self.state = SyncState::AwaitingDownload { + since: Instant::now() + NEXT_REQUEST_WAIT_TIME, + }; + return; + } + Err(err) => { + warn!(%err, "Failed to validate chunks response"); + self.ban_peer(from_peer_id, "Chunk array validation failed"); + self.state = SyncState::Idle; + return; + } + } + + self.failures = 0; + + // store in db + match self + .store + .put_chunks_with_tx_hash(self.tx_id.seq, self.tx_id.hash, response.chunks) + .await + { + Ok(true) => self.next_chunk = to_chunk, + Ok(false) => { + warn!(?self.tx_id, "Transaction reverted while storing chunks"); + self.state = SyncState::Failed { + reason: FailureReason::TxReverted(self.tx_id), + }; + return; + } + Err(err) => { + error!(%err, "Unexpected DB error while storing chunks"); + self.state = SyncState::Failed { + reason: FailureReason::DBError(err.to_string()), + }; + return; + } + } + + // prepare to download next + if self.next_chunk < self.num_chunks { + self.state = SyncState::Idle; + return; + } + + // finalize tx if all chunks downloaded + match self + .store + .finalize_tx_with_hash(self.tx_id.seq, self.tx_id.hash) + .await + { + Ok(true) => self.state = SyncState::Completed, + Ok(false) => { + warn!(?self.tx_id, "Transaction reverted during finalize_tx"); + self.state = SyncState::Failed { + reason: FailureReason::TxReverted(self.tx_id), + }; + } + Err(err) => { + error!(%err, "Unexpected error during finalize_tx"); + self.state = SyncState::Failed { + reason: FailureReason::DBError(err.to_string()), + }; + } + } + } + + pub fn on_request_failed(&mut self, peer_id: PeerId) { + if self.handle_on_response_mismatch(peer_id) { + return; + } + + self.handle_response_failure(peer_id, "RPC Error"); + } + + fn handle_response_failure(&mut self, peer_id: PeerId, reason: &'static str) { + info!(%peer_id, %self.tx_seq, %reason, "Chunks request failed"); + + // ban peer on too many failures + // FIXME(zz): If remote removes a file, we will also get failure here. + // self.ctx + // .report_peer(peer_id, PeerAction::HighToleranceError, reason); + + self.failures += 1; + + if self.failures <= MAX_REQUEST_FAILURES { + // try again + self.state = SyncState::AwaitingDownload { + since: Instant::now() + NEXT_REQUEST_WAIT_TIME, + }; + } else { + // ban and find new peer to download + self.ban_peer(peer_id, reason); + self.state = SyncState::Idle; + } + } + + pub fn transition(&mut self) { + use PeerState::*; + + // update peer connection states + self.peers.transition(); + + loop { + match self.state { + SyncState::Idle => { + if self.peers.count(&[Found, Connecting, Connected]) > 0 { + self.state = SyncState::FoundPeers; + } else { + self.try_find_peers(); + } + } + + SyncState::FindingPeers { updated, .. } => { + if self.peers.count(&[Found, Connecting, Connected]) > 0 { + self.state = SyncState::FoundPeers; + } else { + // storage node may not have the specific file when `FindFile` + // gossip message received. In this case, just broadcast the + // `FindFile` message again. + if updated.elapsed() >= PEER_REQUEST_TIMEOUT { + debug!(%self.tx_seq, "Peer request timeout"); + self.try_find_peers(); + } + + return; + } + } + + SyncState::FoundPeers => { + if self.peers.count(&[Connecting, Connected]) > 0 { + self.state = SyncState::ConnectingPeers; + } else { + self.try_connect(); + } + } + + SyncState::ConnectingPeers => { + if self.peers.count(&[Connected]) > 0 { + self.state = SyncState::AwaitingDownload { + since: Instant::now(), + }; + } else if self.peers.count(&[Connecting]) == 0 { + self.state = SyncState::Idle; + } else { + // peers.transition() will handle the case that peer connecting timeout + return; + } + } + + SyncState::AwaitingOutgoingConnection { since } => { + if since.elapsed() < WAIT_OUTGOING_CONNECTION_TIMEOUT { + return; + } + + self.state = SyncState::Idle; + } + + SyncState::AwaitingDownload { since } => { + if Instant::now() < since { + return; + } + + self.try_request_next(); + } + + SyncState::Downloading { peer_id, since, .. } => { + if !matches!(self.peers.peer_state(&peer_id), Some(PeerState::Connected)) { + // e.g. peer disconnected by remote node + self.state = SyncState::Idle; + } else if since.elapsed() >= DOWNLOAD_TIMEOUT { + self.handle_response_failure(peer_id, "RPC timeout"); + } else { + return; + } + } + + SyncState::Completed | SyncState::Failed { .. } => return, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::create_2_store; + use crate::test_util::tests::create_file_location_cache; + use libp2p::identity; + use network::{ReportSource, Request}; + use storage::log_store::log_manager::LogConfig; + use storage::log_store::log_manager::LogManager; + use storage::log_store::LogStoreRead; + use storage::H256; + use task_executor::{test_utils::TestRuntime, TaskExecutor}; + use tokio::sync::mpsc::{self, UnboundedReceiver}; + use tokio::sync::RwLock; + + #[test] + fn test_status() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, _) = create_default_controller(task_executor, None); + + assert_eq!(*controller.get_status(), SyncState::Idle); + controller.state = SyncState::Completed; + assert_eq!(*controller.get_status(), SyncState::Completed); + + controller.reset(); + assert_eq!(*controller.get_status(), SyncState::Idle); + } + + #[tokio::test] + async fn test_find_peers() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_default_controller(task_executor, None); + + assert_eq!(controller.peers.count(&[PeerState::Found]), 0); + + controller.try_find_peers(); + assert!(matches!( + *controller.get_status(), + SyncState::FindingPeers { .. } + )); + assert_eq!(controller.peers.count(&[PeerState::Found]), 1); + assert!(network_recv.try_recv().is_err()); + + controller.try_find_peers(); + assert_eq!(controller.peers.count(&[PeerState::Found]), 1); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::Publish { messages } => { + assert_eq!(messages.len(), 1); + + match &messages[0] { + PubsubMessage::FindFile(data) => { + assert_eq!(data.tx_id, controller.tx_id); + } + _ => { + panic!("Unexpected message type"); + } + } + } + _ => { + panic!("Unexpected message type"); + } + } + } + } + + #[tokio::test] + async fn test_find_peers_not_in_file_cache() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_default_controller(task_executor, None); + + controller.tx_seq = 1; + controller.tx_id = TxID { + seq: 1, + hash: H256::random(), + }; + controller.try_find_peers(); + + assert_eq!(controller.peers.count(&[PeerState::Found]), 0); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::Publish { messages } => { + assert_eq!(messages.len(), 1); + + match &messages[0] { + PubsubMessage::FindFile(data) => { + assert_eq!(data.tx_id, controller.tx_id); + } + _ => { + panic!("Unexpected message type"); + } + } + } + _ => { + panic!("Unexpected message type"); + } + } + } + + assert!(matches!( + *controller.get_status(), + SyncState::FindingPeers { .. } + )); + } + + #[tokio::test] + async fn test_connect_peers() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_default_controller(task_executor, None); + + controller.state = SyncState::FoundPeers; + controller.try_connect(); + assert_eq!(controller.state, SyncState::Idle); + assert!(network_recv.try_recv().is_err()); + + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + controller.peers.add_new_peer(new_peer_id, addr.clone()); + controller.try_connect(); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::DialPeer { address, peer_id } => { + assert_eq!(address, addr); + assert_eq!(peer_id, new_peer_id); + } + _ => { + panic!("Not expected message: NetworkMessage::DialPeer"); + } + } + } + + assert_eq!(controller.state, SyncState::ConnectingPeers); + } + + #[tokio::test] + async fn test_request_chunks() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_default_controller(task_executor, None); + + controller.state = SyncState::AwaitingDownload { + since: Instant::now(), + }; + controller.try_request_next(); + assert_eq!(controller.state, SyncState::Idle); + assert!(network_recv.try_recv().is_err()); + + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + + controller.peers.add_new_peer(new_peer_id, addr.clone()); + controller + .peers + .update_state_force(&new_peer_id, PeerState::Connected); + + controller.try_request_next(); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::SendRequest { + peer_id, + request_id, + request, + } => { + assert_eq!(peer_id, new_peer_id); + assert_eq!( + request, + Request::GetChunks(GetChunksRequest { + tx_id: controller.tx_id, + index_start: 0, + index_end: 123, + }) + ); + + match request_id { + network::RequestId::Sync(sync_id) => match sync_id { + network::SyncId::SerialSync { tx_id } => { + assert_eq!(tx_id, controller.tx_id); + } + }, + _ => { + panic!("Not expected message: network::RequestId::Sync"); + } + } + } + _ => { + panic!("Not expected message: NetworkMessage::SendRequest"); + } + } + } + + assert!(matches!( + *controller.get_status(), + SyncState::Downloading { .. } + )); + } + + #[tokio::test] + async fn test_ban_peer() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_default_controller(task_executor, None); + + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + controller.ban_peer(new_peer_id, "unit test"); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, new_peer_id); + match action { + PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "unit test"); + } + _ => { + panic!("Not received expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + async fn test_report_peer() { + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (controller, mut network_recv) = create_default_controller(task_executor, None); + + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + controller + .ctx + .report_peer(new_peer_id, PeerAction::MidToleranceError, "unit test"); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, new_peer_id); + match action { + PeerAction::MidToleranceError => {} + _ => { + panic!("PeerAction expect MidToleranceError"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "unit test"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[test] + fn test_peer_connected() { + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, _) = create_default_controller(task_executor, Some(new_peer_id)); + + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + controller.peers.add_new_peer(new_peer_id, addr); + + controller.on_peer_connected(new_peer_id); + assert_eq!( + controller.peers.peer_state(&new_peer_id), + Some(PeerState::Found) + ); + + controller + .peers + .update_state_force(&new_peer_id, PeerState::Connecting); + controller.on_peer_connected(new_peer_id); + assert_eq!( + controller.peers.peer_state(&new_peer_id), + Some(PeerState::Connected) + ); + } + + #[test] + fn test_peer_disconnected() { + let new_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, _) = create_default_controller(task_executor, Some(new_peer_id)); + + let addr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + controller.peers.add_new_peer(new_peer_id, addr); + + controller + .peers + .update_state_force(&new_peer_id, PeerState::Disconnecting); + controller.on_peer_disconnected(new_peer_id); + assert_eq!( + controller.peers.peer_state(&new_peer_id), + Some(PeerState::Disconnected) + ); + + controller + .peers + .update_state_force(&new_peer_id, PeerState::Found); + controller.on_peer_disconnected(new_peer_id); + assert_eq!( + controller.peers.peer_state(&new_peer_id), + Some(PeerState::Disconnected) + ); + + let new_peer_id_1 = identity::Keypair::generate_ed25519().public().to_peer_id(); + controller.on_peer_disconnected(new_peer_id_1); + assert_eq!(controller.peers.peer_state(&new_peer_id_1), None); + } + + // FIXME(zz): enable. + // #[tokio::test] + #[allow(unused)] + async fn test_response_mismatch_state_mismatch() { + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (controller, mut network_recv) = + create_default_controller(task_executor, Some(init_peer_id)); + + assert!(controller.handle_on_response_mismatch(init_peer_id)); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, init_peer_id); + match action { + PeerAction::HighToleranceError => {} + _ => { + panic!("PeerAction expect HighToleranceError"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "Sync state mismatch"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + async fn test_response_mismatch_peer_id_mismatch() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = + create_default_controller(task_executor, Some(peer_id)); + + let peer_id_1 = identity::Keypair::generate_ed25519().public().to_peer_id(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: 1, + since: Instant::now(), + }; + assert!(controller.handle_on_response_mismatch(peer_id_1)); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, peer_id_1); + match action { + PeerAction::LowToleranceError => {} + _ => { + panic!("PeerAction expect MidToleranceError"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "Peer id mismatch"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + #[should_panic(expected = "Invalid chunk boundaries")] + #[ignore = "only panic in debug mode"] + async fn test_response_panic() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, _) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: 0, + since: Instant::now(), + }; + controller.on_response(peer_id, chunks).await; + } + + #[tokio::test] + async fn test_response_chunk_len_invalid() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let mut chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: chunk_count as u64, + since: Instant::now(), + }; + + chunks.chunks.data = Vec::new(); + controller.on_response(peer_id, chunks).await; + assert_eq!(*controller.get_status(), SyncState::Idle); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, peer_id); + match action { + PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "Invalid chunk response data length"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + // FIXME(zz): enable. + // #[tokio::test] + #[allow(unused)] + async fn test_response_chunk_index_invalid() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 1, + to_chunk: chunk_count as u64, + since: Instant::now(), + }; + + controller.on_response(peer_id, chunks).await; + assert_eq!(*controller.get_status(), SyncState::Idle); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, peer_id); + match action { + PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "Invalid chunk response range"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + async fn test_response_validate_failed() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: chunk_count as u64, + since: Instant::now(), + }; + + controller.tx_seq = 1; + controller.tx_id = TxID { + seq: 1, + hash: H256::random(), + }; + + controller.on_response(peer_id, chunks).await; + assert_eq!(*controller.get_status(), SyncState::Idle); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, peer_id); + match action { + PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "Chunk array validation failed"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + // FIXME(zz): enable. + // #[tokio::test] + #[allow(unused)] + async fn test_response_put_failed() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (_, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + peer_store.clone(), + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: chunk_count as u64, + since: Instant::now(), + }; + + controller.on_response(peer_id, chunks).await; + match controller.get_status() { + SyncState::Failed { reason } => { + assert!(matches!(reason, FailureReason::DBError(..))); + } + _ => { + panic!("Not expected SyncState"); + } + } + + assert!(network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_response_finalize_failed() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 2049; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, 2048) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: 2048, + since: Instant::now(), + }; + + controller.num_chunks = 2048; + + controller.on_response(peer_id, chunks).await; + match controller.get_status() { + SyncState::Failed { reason } => { + assert!(matches!(reason, FailureReason::DBError(..))); + } + _ => { + panic!("Not expected SyncState"); + } + } + + assert!(network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_response_success() { + let peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let tx_seq = 0; + let chunk_count = 123; + let (store, peer_store, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(peer_id), + store, + txs[0].id(), + chunk_count, + ); + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range(tx_seq, 0, chunk_count) + .unwrap() + .unwrap(); + + controller.state = SyncState::Downloading { + peer_id, + from_chunk: 0, + to_chunk: chunk_count as u64, + since: Instant::now(), + }; + + controller.on_response(peer_id, chunks).await; + assert_eq!(*controller.get_status(), SyncState::Completed); + assert!(network_recv.try_recv().is_err()); + } + + // FIXME(zz): enable. + // #[tokio::test] + #[allow(unused)] + async fn test_handle_response_failure() { + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + + let chunk_count = 123; + let (store, _, txs, _) = create_2_store(vec![chunk_count]); + + let runtime = TestRuntime::default(); + let task_executor = runtime.task_executor.clone(); + let (mut controller, mut network_recv) = create_controller( + task_executor, + Some(init_peer_id), + store, + txs[0].id(), + chunk_count, + ); + + for i in 0..(MAX_REQUEST_FAILURES + 1) { + controller.handle_response_failure(init_peer_id, "unit test"); + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, init_peer_id); + match action { + PeerAction::LowToleranceError => {} + _ => { + panic!("PeerAction expect LowToleranceError"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "unit test"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + + assert_eq!(controller.failures, i + 1); + if i == MAX_REQUEST_FAILURES { + assert_eq!(*controller.get_status(), SyncState::Idle); + + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, init_peer_id); + match action { + PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + + assert_eq!(msg, "unit test"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } else { + assert!(matches!( + *controller.get_status(), + SyncState::AwaitingDownload { .. } + )); + } + } + } + + fn create_default_controller( + task_executor: TaskExecutor, + peer_id: Option, + ) -> (SerialSyncController, UnboundedReceiver) { + let tx_id = TxID { + seq: 0, + hash: H256::random(), + }; + let num_chunks = 123; + + let config = LogConfig::default(); + let store = Arc::new(RwLock::new(LogManager::memorydb(config).unwrap())); + + create_controller(task_executor, peer_id, store, tx_id, num_chunks) + } + + fn create_controller( + task_executor: TaskExecutor, + peer_id: Option, + store: Arc>, + tx_id: TxID, + num_chunks: usize, + ) -> (SerialSyncController, UnboundedReceiver) { + let (network_send, network_recv) = mpsc::unbounded_channel::(); + let ctx = Arc::new(SyncNetworkContext::new(network_send)); + + let peer_id = match peer_id { + Some(v) => v, + _ => identity::Keypair::generate_ed25519().public().to_peer_id(), + }; + + let file_location_cache = create_file_location_cache(peer_id, vec![tx_id]); + + let controller = SerialSyncController::new( + tx_id, + num_chunks as u64, + ctx, + Store::new(store, task_executor), + file_location_cache, + ); + + (controller, network_recv) + } +} diff --git a/node/sync/src/lib.rs b/node/sync/src/lib.rs new file mode 100644 index 0000000..dd5be31 --- /dev/null +++ b/node/sync/src/lib.rs @@ -0,0 +1,42 @@ +#[macro_use] +extern crate tracing; + +mod auto_sync; +mod context; +mod controllers; +mod service; +pub mod test_util; + +pub use controllers::FileSyncInfo; +use duration_str::deserialize_duration; +use serde::Deserialize; +pub use service::{SyncMessage, SyncReceiver, SyncRequest, SyncResponse, SyncSender, SyncService}; +use std::time::Duration; + +#[derive(Clone, Debug, Deserialize)] +#[serde(default)] +pub struct Config { + pub auto_sync_disabled: bool, + pub max_sync_files: usize, + #[serde(deserialize_with = "deserialize_duration")] + pub find_peer_timeout: Duration, + pub enable_chunk_request: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + auto_sync_disabled: false, + max_sync_files: 100, + find_peer_timeout: Duration::from_secs(30), + enable_chunk_request: false, + } + } +} + +impl Config { + pub fn disable_auto_sync(mut self) -> Self { + self.auto_sync_disabled = true; + self + } +} diff --git a/node/sync/src/service.rs b/node/sync/src/service.rs new file mode 100644 index 0000000..5a5a740 --- /dev/null +++ b/node/sync/src/service.rs @@ -0,0 +1,1576 @@ +use crate::auto_sync::AutoSyncManager; +use crate::context::SyncNetworkContext; +use crate::controllers::{FailureReason, FileSyncInfo, SerialSyncController, SyncState}; +use crate::Config; +use anyhow::{bail, Result}; +use file_location_cache::FileLocationCache; +use libp2p::swarm::DialError; +use log_entry_sync::LogSyncEvent; +use network::{ + rpc::GetChunksRequest, rpc::RPCResponseErrorCode, Multiaddr, NetworkMessage, PeerId, + PeerRequestId, SyncId as RequestId, +}; +use shared_types::{bytes_to_chunks, ChunkArrayWithProof, TxID}; +use std::{ + collections::{hash_map::Entry, HashMap}, + sync::Arc, +}; +use storage::error::Result as StorageResult; +use storage::log_store::Store as LogStore; +use storage_async::Store; +use tokio::sync::{broadcast, mpsc, RwLock}; + +const HEARTBEAT_INTERVAL_SEC: u64 = 5; + +pub type SyncSender = channel::Sender; +pub type SyncReceiver = channel::Receiver; + +#[derive(Debug)] +pub enum SyncMessage { + DailFailed { + peer_id: PeerId, + err: DialError, + }, + PeerConnected { + peer_id: PeerId, + }, + PeerDisconnected { + peer_id: PeerId, + }, + RequestChunks { + peer_id: PeerId, + request_id: PeerRequestId, + request: GetChunksRequest, + }, + ChunksResponse { + peer_id: PeerId, + request_id: RequestId, + response: ChunkArrayWithProof, + }, + RpcError { + peer_id: PeerId, + request_id: RequestId, + }, + AnnounceFileGossip { + tx_id: TxID, + peer_id: PeerId, + addr: Multiaddr, + }, +} + +#[derive(Debug)] +pub enum SyncRequest { + SyncStatus { tx_seq: u64 }, + SyncFile { tx_seq: u64 }, + FileSyncInfo { tx_seq: Option }, + TerminateFileSync { tx_seq: u64, is_reverted: bool }, +} + +#[derive(Debug)] +pub enum SyncResponse { + SyncStatus { status: Option }, + SyncFile { err: String }, + FileSyncInfo { result: HashMap }, + TerminateFileSync { count: usize }, +} + +pub struct SyncService { + config: Config, + + /// A receiving channel sent by the message processor thread. + msg_recv: channel::Receiver, + + /// A network context to contact the network service. + ctx: Arc, + + /// Log and transaction storage. + store: Store, + + /// Cache for storing and serving gossip messages. + file_location_cache: Arc, + + /// A collection of file sync controllers. + controllers: HashMap, + + /// Heartbeat interval for executing periodic tasks. + heartbeat: tokio::time::Interval, + + manager: AutoSyncManager, +} + +impl SyncService { + pub async fn spawn( + executor: task_executor::TaskExecutor, + network_send: mpsc::UnboundedSender, + store: Arc>, + file_location_cache: Arc, + event_recv: broadcast::Receiver, + ) -> Result { + Self::spawn_with_config( + Config::default(), + executor, + network_send, + store, + file_location_cache, + event_recv, + ) + .await + } + + pub async fn spawn_with_config( + config: Config, + executor: task_executor::TaskExecutor, + network_send: mpsc::UnboundedSender, + store: Arc>, + file_location_cache: Arc, + event_recv: broadcast::Receiver, + ) -> Result { + let (sync_send, sync_recv) = channel::Channel::unbounded(); + + let heartbeat = + tokio::time::interval(tokio::time::Duration::from_secs(HEARTBEAT_INTERVAL_SEC)); + + let store = Store::new(store, executor.clone()); + + let manager = + AutoSyncManager::new(store.clone(), sync_send.clone(), config.clone()).await?; + if !config.auto_sync_disabled { + manager.spwn(&executor, event_recv); + } + + let mut sync = SyncService { + config, + msg_recv: sync_recv, + ctx: Arc::new(SyncNetworkContext::new(network_send)), + store, + file_location_cache, + controllers: Default::default(), + heartbeat, + manager, + }; + + debug!("Starting sync service"); + executor.spawn(async move { Box::pin(sync.main()).await }, "sync"); + + Ok(sync_send) + } + + async fn main(&mut self) { + loop { + tokio::select! { + // received sync message + Some(msg) = self.msg_recv.recv() => { + match msg { + channel::Message::Notification(msg) => self.on_sync_msg(msg).await, + channel::Message::Request(req, sender) => self.on_sync_request(req, sender).await, + } + } + + // heartbeat + _ = self.heartbeat.tick() => self.on_heartbeat(), + } + } + } + + async fn on_sync_msg(&mut self, msg: SyncMessage) { + debug!("Sync received message {:?}", msg); + + match msg { + SyncMessage::DailFailed { peer_id, err } => { + self.on_dail_failed(peer_id, err); + } + SyncMessage::PeerConnected { peer_id } => { + self.on_peer_connected(peer_id); + } + + SyncMessage::PeerDisconnected { peer_id } => { + self.on_peer_disconnected(peer_id); + } + + SyncMessage::RequestChunks { + request_id, + peer_id, + request, + } => { + self.on_get_chunks_request(peer_id, request_id, request) + .await; + } + + SyncMessage::ChunksResponse { + peer_id, + request_id, + response, + } => { + self.on_chunks_response(peer_id, request_id, response).await; + } + + SyncMessage::RpcError { + peer_id, + request_id, + } => { + self.on_rpc_error(peer_id, request_id); + } + + SyncMessage::AnnounceFileGossip { + tx_id, + peer_id, + addr, + } => { + self.on_announce_file_gossip(tx_id, peer_id, addr).await; + } + } + } + + async fn on_sync_request( + &mut self, + req: SyncRequest, + sender: channel::ResponseSender, + ) { + match req { + SyncRequest::SyncStatus { tx_seq } => { + let status = self + .controllers + .get(&tx_seq) + .map(|c| c.get_status().clone()); + + let _ = sender.send(SyncResponse::SyncStatus { status }); + } + + SyncRequest::SyncFile { tx_seq } => { + if !self.controllers.contains_key(&tx_seq) + && self.controllers.len() >= self.config.max_sync_files + { + let _ = sender.send(SyncResponse::SyncFile { + err: format!( + "max sync file limitation reached: {:?}", + self.config.max_sync_files + ), + }); + return; + } + + let err = match self.on_start_sync_file(tx_seq, None).await { + Ok(()) => "".into(), + Err(err) => err.to_string(), + }; + + let _ = sender.send(SyncResponse::SyncFile { err }); + } + + SyncRequest::FileSyncInfo { tx_seq } => { + let mut result = HashMap::default(); + + match tx_seq { + Some(seq) => { + if let Some(controller) = self.controllers.get(&seq) { + result.insert(seq, controller.get_sync_info()); + } + } + None => { + for (seq, controller) in self.controllers.iter() { + result.insert(*seq, controller.get_sync_info()); + } + } + } + + let _ = sender.send(SyncResponse::FileSyncInfo { result }); + } + + SyncRequest::TerminateFileSync { + tx_seq, + is_reverted, + } => { + debug!(?tx_seq, "terminate file sync"); + let count = self.on_terminate_file_sync(tx_seq, is_reverted); + let _ = sender.send(SyncResponse::TerminateFileSync { count }); + } + } + } + + fn on_dail_failed(&mut self, peer_id: PeerId, err: DialError) { + info!(%peer_id, "Dail to peer failed"); + + for controller in self.controllers.values_mut() { + controller.on_dail_failed(peer_id, &err); + controller.transition(); + } + } + + fn on_peer_connected(&mut self, peer_id: PeerId) { + info!(%peer_id, "Peer connected"); + + for controller in self.controllers.values_mut() { + controller.on_peer_connected(peer_id); + controller.transition(); + } + } + + fn on_peer_disconnected(&mut self, peer_id: PeerId) { + info!(%peer_id, "Peer disconnected"); + + for controller in self.controllers.values_mut() { + controller.on_peer_disconnected(peer_id); + controller.transition(); + } + } + + async fn on_get_chunks_request( + &mut self, + peer_id: PeerId, + request_id: PeerRequestId, + request: GetChunksRequest, + ) { + info!(?request, %peer_id, ?request_id, "Received GetChunks request"); + + if let Err(err) = self + .handle_chunks_request_with_db_err(peer_id, request_id, request) + .await + { + error!(%err, "Failed to handle chunks request due to db error"); + self.ctx.send(NetworkMessage::SendErrorResponse { + peer_id, + id: request_id, + error: RPCResponseErrorCode::ServerError, + reason: "DB error".into(), + }); + } + } + + async fn handle_chunks_request_with_db_err( + &mut self, + peer_id: PeerId, + request_id: PeerRequestId, + request: GetChunksRequest, + ) -> StorageResult<()> { + // ban peer for invalid chunk index range + if request.index_start >= request.index_end { + self.ctx.ban_peer(peer_id, "Invalid chunk indices"); + return Ok(()); + } + + // ban peer if invalid tx requested + let tx = match self.store.get_tx_by_seq_number(request.tx_id.seq).await? { + Some(tx) => tx, + None => { + self.ctx.ban_peer(peer_id, "Tx not found"); + return Ok(()); + } + }; + + // Transaction may be reverted during file sync + if tx.id() != request.tx_id { + self.ctx.send(NetworkMessage::SendErrorResponse { + peer_id, + error: RPCResponseErrorCode::InvalidRequest, + reason: "Tx not found (Reverted)".into(), + id: request_id, + }); + } + + // ban peer if chunk index out of bound + let num_chunks = bytes_to_chunks(tx.size as usize); + if request.index_end as usize > num_chunks { + self.ctx.ban_peer(peer_id, "Chunk index out of bound"); + return Ok(()); + } + + // file may be removed, but remote peer still find one from the file location cache + let finalized = self.store.check_tx_completed(request.tx_id.seq).await?; + if !finalized { + info!(%request.tx_id.seq, "Failed to handle chunks request due to tx not finalized"); + // FIXME(zz): If remote removes a file, we will also get failure here. + // self.ctx + // .report_peer(peer_id, PeerAction::HighToleranceError, "Tx not finalized"); + self.ctx.send(NetworkMessage::SendErrorResponse { + peer_id, + error: RPCResponseErrorCode::InvalidRequest, + reason: "Tx not finalized".into(), + id: request_id, + }); + return Ok(()); + } + + let result = self + .store + .get_chunks_with_proof_by_tx_and_index_range( + request.tx_id.seq, + request.index_start as usize, + request.index_end as usize, + ) + .await?; + + match result { + Some(chunks) => { + self.ctx.send(NetworkMessage::SendResponse { + peer_id, + id: request_id, + response: network::Response::Chunks(chunks), + }); + } + None => { + // file may be removed during downloading + warn!(%request.tx_id.seq, "Failed to handle chunks request due to chunks not found"); + self.ctx.send(NetworkMessage::SendErrorResponse { + peer_id, + error: RPCResponseErrorCode::InvalidRequest, + reason: "Chunks not found".into(), + id: request_id, + }); + } + } + + Ok(()) + } + + async fn on_chunks_response( + &mut self, + peer_id: PeerId, + request_id: RequestId, + response: ChunkArrayWithProof, + ) { + info!(%response.chunks, %peer_id, ?request_id, "Received chunks response"); + + let tx_seq = match request_id { + RequestId::SerialSync { tx_id } => tx_id.seq, + }; + + match self.controllers.get_mut(&tx_seq) { + Some(controller) => { + controller.on_response(peer_id, response).await; + controller.transition(); + } + None => { + warn!("Received chunks response for non-existent controller tx_seq={tx_seq}"); + } + } + } + + fn on_rpc_error(&mut self, peer_id: PeerId, request_id: RequestId) { + info!(%peer_id, ?request_id, "Received RPC error"); + + let tx_seq = match request_id { + RequestId::SerialSync { tx_id } => tx_id.seq, + }; + + match self.controllers.get_mut(&tx_seq) { + Some(controller) => { + controller.on_request_failed(peer_id); + controller.transition(); + } + None => { + warn!("Received rpc error for non-existent controller tx_seq={tx_seq}"); + } + } + } + + async fn on_start_sync_file( + &mut self, + tx_seq: u64, + maybe_peer: Option<(PeerId, Multiaddr)>, + ) -> Result<()> { + info!(%tx_seq, "Start to sync file"); + if !self.config.enable_chunk_request { + return Ok(()); + } + + // remove failed entry if caused by tx reverted, so as to re-sync + // file with latest tx_id. + let mut tx_reverted = false; + if let Some(controller) = self.controllers.get(&tx_seq) { + if let SyncState::Failed { + reason: FailureReason::TxReverted(..), + } = controller.get_status() + { + tx_reverted = true; + } + } + + if tx_reverted { + self.controllers.remove(&tx_seq); + } + + let controller = match self.controllers.entry(tx_seq) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let tx = match self.store.get_tx_by_seq_number(tx_seq).await? { + Some(tx) => tx, + None => bail!("transaction not found"), + }; + + let num_chunks = match usize::try_from(tx.size) { + Ok(size) => bytes_to_chunks(size), + Err(_) => { + error!(%tx_seq, "Unexpected transaction size: {}", tx.size); + bail!("Unexpected transaction size"); + } + }; + + // file already exists + if self.store.check_tx_completed(tx_seq).await? { + bail!("File already exists"); + } + + entry.insert(SerialSyncController::new( + tx.id(), + num_chunks as u64, + self.ctx.clone(), + self.store.clone(), + self.file_location_cache.clone(), + )) + } + }; + + // trigger retry after failure + if let SyncState::Failed { .. } = controller.get_status() { + controller.reset(); + } + + if let Some((peer_id, addr)) = maybe_peer { + controller.on_peer_found(peer_id, addr); + } + + controller.transition(); + + Ok(()) + } + + async fn on_announce_file_gossip(&mut self, tx_id: TxID, peer_id: PeerId, addr: Multiaddr) { + let tx_seq = tx_id.seq; + info!(%tx_seq, %peer_id, %addr, "Received AnnounceFile gossip"); + + self.manager.update_on_announcement(tx_seq).await; + + // File already in sync + if let Some(controller) = self.controllers.get_mut(&tx_seq) { + controller.on_peer_found(peer_id, addr); + controller.transition(); + return; + } + + // File already exists and ignore the AnnounceFile message + match self.store.check_tx_completed(tx_seq).await { + Ok(true) => return, + Ok(false) => {} + Err(err) => { + error!(%tx_seq, %err, "Failed to check if file finalized"); + return; + } + } + + // Now, always sync files among all nodes + if let Err(err) = self.on_start_sync_file(tx_seq, Some((peer_id, addr))).await { + // FIXME(zz): This is possible for tx missing. Is it expected? + error!(%tx_seq, %err, "Failed to sync file"); + } + } + + /// Terminate file sync of `min_tx_seq`. + /// If `is_reverted` is `true` (means confirmed transactions reverted), + /// also terminate `tx_seq` greater than `min_tx_seq` + /// + /// Note, this function should be as fast as possible to avoid + /// message lagged in channel. + fn on_terminate_file_sync(&mut self, min_tx_seq: u64, is_reverted: bool) -> usize { + let mut to_terminate = vec![]; + + if is_reverted { + for (tx_seq, _) in self.controllers.iter() { + if *tx_seq >= min_tx_seq { + to_terminate.push(*tx_seq); + } + } + } else { + to_terminate.push(min_tx_seq); + } + + for tx_seq in to_terminate.iter() { + self.controllers.remove(tx_seq); + } + + to_terminate.len() + } + + fn on_heartbeat(&mut self) { + let mut completed = vec![]; + + for (&tx_seq, controller) in self.controllers.iter_mut() { + controller.transition(); + + if let SyncState::Completed = controller.get_status() { + completed.push(tx_seq); + } + } + + for tx_seq in completed { + self.controllers.remove(&tx_seq); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::create_2_store; + use crate::test_util::tests::create_file_location_cache; + use libp2p::identity; + use network::discovery::ConnectionId; + use network::rpc::SubstreamId; + use network::ReportSource; + use shared_types::ChunkArray; + use shared_types::Transaction; + use std::thread; + use std::time::Duration; + use std::time::Instant; + use storage::log_store::log_manager::LogConfig; + use storage::log_store::log_manager::LogManager; + use storage::log_store::LogStoreRead; + use storage::H256; + use task_executor::test_utils::TestRuntime; + use tokio::sync::mpsc::UnboundedReceiver; + use tokio::sync::mpsc::UnboundedSender; + + struct TestSyncRuntime { + runtime: TestRuntime, + + chunk_count: usize, + store: Arc>, + peer_store: Arc>, + txs: Vec, + init_data: Vec, + + init_peer_id: PeerId, + file_location_cache: Arc, + + network_send: UnboundedSender, + network_recv: UnboundedReceiver, + event_send: broadcast::Sender, + } + + impl Default for TestSyncRuntime { + fn default() -> Self { + TestSyncRuntime::new(vec![1535], 1) + } + } + + impl TestSyncRuntime { + fn new(chunk_counts: Vec, seq_size: usize) -> Self { + let chunk_count = chunk_counts[0]; + let (store, peer_store, txs, data) = create_2_store(chunk_counts); + let init_data = data[0].clone(); + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let (network_send, network_recv) = mpsc::unbounded_channel::(); + let (event_send, _) = broadcast::channel(16); + + let tx_ids = txs.iter().take(seq_size).map(|tx| tx.id()).collect(); + + Self { + runtime: TestRuntime::default(), + chunk_count, + store, + peer_store, + txs, + init_data, + init_peer_id, + file_location_cache: create_file_location_cache(init_peer_id, tx_ids), + network_send, + network_recv, + event_send, + } + } + + async fn spawn_sync_service(&self, with_peer_store: bool) -> SyncSender { + let store = if with_peer_store { + self.peer_store.clone() + } else { + self.store.clone() + }; + + SyncService::spawn_with_config( + Config::default().disable_auto_sync(), + self.runtime.task_executor.clone(), + self.network_send.clone(), + store, + self.file_location_cache.clone(), + self.event_send.subscribe(), + ) + .await + .unwrap() + } + } + + #[tokio::test] + async fn test_peer_connected_not_in_controller() { + let runtime = TestRuntime::default(); + + let chunk_count = 1535; + let (_, store, txs, _) = create_2_store(vec![chunk_count]); + let store = Store::new(store, runtime.task_executor.clone()); + + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let file_location_cache: Arc = + create_file_location_cache(init_peer_id, vec![txs[0].id()]); + + let (network_send, mut network_recv) = mpsc::unbounded_channel::(); + let (sync_send, sync_recv) = channel::Channel::unbounded(); + + let heartbeat = tokio::time::interval(Duration::from_secs(HEARTBEAT_INTERVAL_SEC)); + let manager = AutoSyncManager::new(store.clone(), sync_send, Config::default()) + .await + .unwrap(); + + let mut sync = SyncService { + config: Config::default().disable_auto_sync(), + msg_recv: sync_recv, + ctx: Arc::new(SyncNetworkContext::new(network_send)), + store, + file_location_cache, + controllers: Default::default(), + heartbeat, + manager, + }; + + sync.on_peer_connected(init_peer_id); + assert!(network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_peer_disconnected_not_in_controller() { + let runtime = TestRuntime::default(); + + let chunk_count = 1535; + let (_, store, txs, _) = create_2_store(vec![chunk_count]); + let store = Store::new(store, runtime.task_executor.clone()); + + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let file_location_cache: Arc = + create_file_location_cache(init_peer_id, vec![txs[0].id()]); + + let (network_send, mut network_recv) = mpsc::unbounded_channel::(); + let (sync_send, sync_recv) = channel::Channel::unbounded(); + + let heartbeat = tokio::time::interval(Duration::from_secs(HEARTBEAT_INTERVAL_SEC)); + let manager = AutoSyncManager::new(store.clone(), sync_send, Config::default()) + .await + .unwrap(); + + let mut sync = SyncService { + config: Config::default().disable_auto_sync(), + msg_recv: sync_recv, + ctx: Arc::new(SyncNetworkContext::new(network_send)), + store, + file_location_cache, + controllers: Default::default(), + heartbeat, + manager, + }; + + sync.on_peer_disconnected(init_peer_id); + assert!(network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_request_chunks() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let request = GetChunksRequest { + tx_id: runtime.txs[0].id(), + index_start: 0, + index_end: runtime.chunk_count as u64, + }; + + sync_send + .notify(SyncMessage::RequestChunks { + request_id: (ConnectionId::new(0), SubstreamId(0)), + peer_id: runtime.init_peer_id, + request, + }) + .unwrap(); + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::SendResponse { + peer_id, + response, + id, + } => match response { + network::Response::Chunks(response) => { + assert_eq!(peer_id, runtime.init_peer_id); + assert_eq!(id.0, ConnectionId::new(0)); + assert_eq!(id.1 .0, 0); + + let data = runtime.init_data.clone(); + let chunk_array = ChunkArray { + data, + start_index: 0, + }; + + assert_eq!( + response.chunks, + chunk_array + .sub_array(0, runtime.chunk_count as u64) + .unwrap() + ); + + runtime + .peer_store + .read() + .await + .validate_range_proof(0, &response) + .expect("validate proof"); + } + _ => { + panic!("Not expected message: Response::Chunks"); + } + }, + _ => { + panic!("Not expected message: NetworkMessage::SendResponse"); + } + } + } + } + + #[tokio::test] + async fn test_request_chunks_invalid_indices() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let request = GetChunksRequest { + tx_id: runtime.txs[0].id(), + index_start: 0, + index_end: 0_u64, + }; + + sync_send + .notify(SyncMessage::RequestChunks { + request_id: (ConnectionId::new(0), SubstreamId(0)), + peer_id: runtime.init_peer_id, + request, + }) + .unwrap(); + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + match action { + network::PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + assert_eq!(msg, "Invalid chunk indices"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + async fn test_request_chunks_tx_not_exist() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let request = GetChunksRequest { + tx_id: TxID { + seq: 1, + hash: H256::random(), + }, + index_start: 0, + index_end: runtime.chunk_count as u64, + }; + + sync_send + .notify(SyncMessage::RequestChunks { + request_id: (ConnectionId::new(0), SubstreamId(0)), + peer_id: runtime.init_peer_id, + request, + }) + .unwrap(); + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + match action { + network::PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + assert_eq!(msg, "Tx not found"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + #[tokio::test] + async fn test_request_chunks_index_out_bound() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let request = GetChunksRequest { + tx_id: runtime.txs[0].id(), + index_start: 0, + index_end: runtime.chunk_count as u64 + 1, + }; + + sync_send + .notify(SyncMessage::RequestChunks { + request_id: (ConnectionId::new(0), SubstreamId(0)), + peer_id: runtime.init_peer_id, + request, + }) + .unwrap(); + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + match action { + network::PeerAction::Fatal => {} + _ => { + panic!("PeerAction expect Fatal"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + assert_eq!(msg, "Chunk index out of bound"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + } + + // FIXME(zz): enable. + // #[tokio::test] + #[allow(unused)] + async fn test_request_chunks_tx_not_finalized() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(false).await; + + let request = GetChunksRequest { + tx_id: runtime.txs[0].id(), + index_start: 0, + index_end: runtime.chunk_count as u64, + }; + + sync_send + .notify(SyncMessage::RequestChunks { + request_id: (ConnectionId::new(0), SubstreamId(0)), + peer_id: runtime.init_peer_id, + request, + }) + .unwrap(); + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + match action { + network::PeerAction::MidToleranceError => {} + _ => { + panic!("PeerAction expect MidToleranceError"); + } + } + + match source { + ReportSource::SyncService => {} + _ => { + panic!("ReportSource expect SyncService"); + } + } + assert_eq!(msg, "Tx not finalized"); + } + _ => { + panic!("Not expected message: NetworkMessage::ReportPeer"); + } + } + } + + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::SendErrorResponse { + peer_id, + id, + error, + reason, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + assert_eq!(id.1 .0, 0); + assert_eq!(error, RPCResponseErrorCode::InvalidRequest); + assert_eq!(reason, "Tx not finalized".to_string()); + } + _ => { + panic!("Not expected message: NetworkMessage::SendResponse"); + } + } + } + } + + #[tokio::test] + // #[traced_test] + async fn test_sync_file_tx_not_exist() { + let runtime = TestRuntime::default(); + + let config = LogConfig::default(); + + let store = Arc::new(RwLock::new(LogManager::memorydb(config.clone()).unwrap())); + + let init_peer_id = identity::Keypair::generate_ed25519().public().to_peer_id(); + let file_location_cache: Arc = + create_file_location_cache(init_peer_id, vec![]); + + let (network_send, mut network_recv) = mpsc::unbounded_channel::(); + let (_event_send, event_recv) = broadcast::channel(16); + let sync_send = SyncService::spawn_with_config( + Config::default().disable_auto_sync(), + runtime.task_executor.clone(), + network_send, + store.clone(), + file_location_cache, + event_recv, + ) + .await + .unwrap(); + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + thread::sleep(Duration::from_millis(1000)); + assert_eq!( + store.read().await.get_tx_by_seq_number(tx_seq).unwrap(), + None + ); + assert!(network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_sync_file_exist_in_store() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + thread::sleep(Duration::from_millis(1000)); + assert!(runtime + .peer_store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + assert!(runtime.network_recv.try_recv().is_err()); + } + + async fn wait_for_tx_finalized(store: Arc>, tx_seq: u64) { + let deadline = Instant::now() + Duration::from_millis(5000); + while !store.read().await.check_tx_completed(tx_seq).unwrap() { + if Instant::now() >= deadline { + panic!("Failed to wait tx completed"); + } + + thread::sleep(Duration::from_millis(300)); + } + } + + #[tokio::test] + async fn test_sync_file_success() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(false).await; + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + + assert!(!matches!( + sync_send + .request(SyncRequest::SyncStatus { tx_seq }) + .await + .unwrap(), + SyncResponse::SyncStatus { status } if status == Some(SyncState::Completed) + )); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + + // test heartbeat + let deadline = Instant::now() + Duration::from_secs(HEARTBEAT_INTERVAL_SEC + 1); + while !matches!(sync_send + .request(SyncRequest::SyncStatus { tx_seq }) + .await + .unwrap(), + SyncResponse::SyncStatus {status} if status.is_none() + ) { + if Instant::now() >= deadline { + panic!("Failed to wait heartbeat"); + } + + thread::sleep(Duration::from_millis(300)); + } + } + + #[tokio::test] + async fn test_sync_file_special_size() { + test_sync_file(1).await; + test_sync_file(511).await; + test_sync_file(512).await; + test_sync_file(513).await; + test_sync_file(514).await; + test_sync_file(1023).await; + test_sync_file(1024).await; + test_sync_file(1025).await; + test_sync_file(2047).await; + test_sync_file(2048).await; + } + + #[tokio::test] + async fn test_sync_file_exceed_max_chunks_to_request() { + let mut runtime = TestSyncRuntime::new(vec![2049], 1); + let sync_send = runtime.spawn_sync_service(false).await; + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + 2048, + ) + .await; + + assert!(!matches!( + sync_send + .request(SyncRequest::SyncStatus { tx_seq }) + .await + .unwrap(), + SyncResponse::SyncStatus { status } if status == Some(SyncState::Completed) + )); + + // next batch + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 2048, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + } + + #[tokio::test] + async fn test_sync_file_multi_files() { + let mut runtime = TestSyncRuntime::new(vec![1535, 1535, 1535], 3); + let sync_send = runtime.spawn_sync_service(false).await; + + // second file + let tx_seq = 1u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + assert!(!runtime.store.read().await.check_tx_completed(0).unwrap()); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store.clone(), tx_seq).await; + + assert!(!runtime.store.read().await.check_tx_completed(0).unwrap()); + + // first file + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + + sync_send + .notify(SyncMessage::PeerDisconnected { + peer_id: runtime.init_peer_id, + }) + .unwrap(); + + thread::sleep(Duration::from_millis(1000)); + assert!(runtime.network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_rpc_error() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + sync_send + .notify(SyncMessage::RpcError { + request_id: network::SyncId::SerialSync { + tx_id: runtime.txs[0].id(), + }, + peer_id: runtime.init_peer_id, + }) + .unwrap(); + + thread::sleep(Duration::from_millis(1000)); + assert!(runtime.network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_announce_file() { + let mut runtime = TestSyncRuntime::new(vec![1535], 0); + let sync_send = runtime.spawn_sync_service(false).await; + + let tx_seq = 0u64; + let address: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + sync_send + .notify(SyncMessage::AnnounceFileGossip { + tx_id: runtime.txs[tx_seq as usize].id(), + peer_id: runtime.init_peer_id, + addr: address, + }) + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + } + + #[tokio::test] + async fn test_announce_file_in_sync() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(false).await; + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + let address: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + sync_send + .notify(SyncMessage::AnnounceFileGossip { + tx_id: runtime.txs[tx_seq as usize].id(), + peer_id: runtime.init_peer_id, + addr: address, + }) + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + runtime.chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + } + + #[tokio::test] + async fn test_announce_file_already_in_store() { + let mut runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(true).await; + + let tx_seq = 0u64; + let address: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse().unwrap(); + sync_send + .notify(SyncMessage::AnnounceFileGossip { + tx_id: runtime.txs[tx_seq as usize].id(), + peer_id: runtime.init_peer_id, + addr: address, + }) + .unwrap(); + + thread::sleep(Duration::from_millis(1000)); + assert!(runtime.network_recv.try_recv().is_err()); + } + + #[tokio::test] + async fn test_sync_status_unknown() { + let runtime = TestSyncRuntime::default(); + let sync_send = runtime.spawn_sync_service(false).await; + + assert!(matches!( + sync_send + .request(SyncRequest::SyncStatus { tx_seq: 0 }) + .await + .unwrap(), + SyncResponse::SyncStatus { status } if status.is_none() + )); + } + + async fn receive_dial(runtime: &mut TestSyncRuntime, sync_send: &SyncSender) { + if let Some(msg) = runtime.network_recv.recv().await { + match msg { + NetworkMessage::DialPeer { + address: _, + peer_id, + } => { + assert_eq!(peer_id, runtime.init_peer_id); + + sync_send + .notify(SyncMessage::PeerConnected { peer_id }) + .unwrap(); + } + _ => { + panic!("Not expected message: NetworkMessage::DialPeer"); + } + } + } + } + + async fn test_sync_file(chunk_count: usize) { + let mut runtime = TestSyncRuntime::new(vec![chunk_count], 1); + let sync_send = runtime.spawn_sync_service(false).await; + + let tx_seq = 0u64; + sync_send + .request(SyncRequest::SyncFile { tx_seq }) + .await + .unwrap(); + + receive_dial(&mut runtime, &sync_send).await; + + assert!(!runtime + .store + .read() + .await + .check_tx_completed(tx_seq) + .unwrap()); + + assert!(!matches!( + sync_send + .request(SyncRequest::SyncStatus { tx_seq }) + .await + .unwrap(), + SyncResponse::SyncStatus { status } if status == Some(SyncState::Completed) )); + + receive_chunk_request( + &mut runtime.network_recv, + &sync_send, + runtime.peer_store.clone(), + runtime.init_peer_id, + tx_seq, + 0, + chunk_count as u64, + ) + .await; + + wait_for_tx_finalized(runtime.store, tx_seq).await; + } + + async fn receive_chunk_request( + network_recv: &mut UnboundedReceiver, + sync_send: &SyncSender, + peer_store: Arc>, + init_peer_id: PeerId, + tx_seq: u64, + index_start: u64, + index_end: u64, + ) { + if let Some(msg) = network_recv.recv().await { + match msg { + NetworkMessage::SendRequest { + peer_id, + request, + request_id, + } => { + assert_eq!(peer_id, init_peer_id); + + let req = match request { + network::Request::GetChunks(req) => { + assert_eq!(req.tx_id.seq, tx_seq); + assert_eq!(req.index_start, index_start); + assert_eq!(req.index_end, index_end); + + req + } + _ => { + panic!("Not expected message network::Request::GetChunks"); + } + }; + + let sync_id = match request_id { + network::RequestId::Sync(sync_id) => sync_id, + _ => unreachable!("All Chunks responses belong to sync"), + }; + + let chunks = peer_store + .read() + .await + .get_chunks_with_proof_by_tx_and_index_range( + tx_seq, + req.index_start as usize, + req.index_end as usize, + ) + .unwrap() + .unwrap(); + + sync_send + .notify(SyncMessage::ChunksResponse { + peer_id, + request_id: sync_id, + response: chunks, + }) + .unwrap(); + } + _ => { + panic!("Not expected message: NetworkMessage::SendRequest"); + } + } + } + } +} diff --git a/node/sync/src/test_util.rs b/node/sync/src/test_util.rs new file mode 100644 index 0000000..15b46a6 --- /dev/null +++ b/node/sync/src/test_util.rs @@ -0,0 +1,150 @@ +use rand::random; +use shared_types::{compute_padded_chunk_size, ChunkArray, Transaction, CHUNK_SIZE}; +use std::{cmp, sync::Arc}; +use storage::{ + log_store::{ + log_manager::{sub_merkle_tree, tx_subtree_root_list_padded, LogConfig, PORA_CHUNK_SIZE}, + LogStoreChunkWrite, LogStoreWrite, + }, + LogManager, +}; +use tokio::sync::RwLock; + +/// Creates stores for local node and peers with initialized transaction of specified chunk count. +/// The first store is for local node, and data not stored. The second store is for peers, and all +/// transactions are finalized for file sync. +#[allow(clippy::type_complexity)] +pub fn create_2_store( + chunk_count: Vec, +) -> ( + Arc>, + Arc>, + Vec, + Vec>, +) { + let config = LogConfig::default(); + let mut store = LogManager::memorydb(config.clone()).unwrap(); + let mut peer_store = LogManager::memorydb(config).unwrap(); + + let mut offset = 1; + let mut txs = vec![]; + let mut data = vec![]; + + for (tx_seq, chunks) in chunk_count.iter().enumerate() { + let ret = generate_data(*chunks, &mut store, &mut peer_store, tx_seq as u64, offset); + txs.push(ret.0); + data.push(ret.1); + offset = ret.2; + } + + ( + Arc::new(RwLock::new(store)), + Arc::new(RwLock::new(peer_store)), + txs, + data, + ) +} + +fn generate_data( + chunk_count: usize, + store: &mut LogManager, + peer_store: &mut LogManager, + seq: u64, + offset: u64, +) -> (Transaction, Vec, u64) { + let data_size = CHUNK_SIZE * chunk_count; + let mut data = vec![0u8; data_size]; + + for i in 0..chunk_count { + data[i * CHUNK_SIZE] = random(); + } + + let merkel_nodes = tx_subtree_root_list_padded(&data); + let first_tree_size = 1 << (merkel_nodes[0].0 - 1); + let start_offset = if offset % first_tree_size == 0 { + offset + } else { + (offset / first_tree_size + 1) * first_tree_size + }; + + let merkle = sub_merkle_tree(&data).unwrap(); + let tx = Transaction { + stream_ids: vec![], + size: data_size as u64, + data_merkle_root: merkle.root().into(), + seq, + data: vec![], + start_entry_index: start_offset, + merkle_nodes: merkel_nodes, + }; + store.put_tx(tx.clone()).unwrap(); + peer_store.put_tx(tx.clone()).unwrap(); + for start_index in (0..chunk_count).step_by(PORA_CHUNK_SIZE) { + let end = cmp::min((start_index + PORA_CHUNK_SIZE) * CHUNK_SIZE, data.len()); + let chunk_array = ChunkArray { + data: data[start_index * CHUNK_SIZE..end].to_vec(), + start_index: start_index as u64, + }; + peer_store.put_chunks(tx.seq, chunk_array.clone()).unwrap(); + } + peer_store.finalize_tx(tx.seq).unwrap(); + + let (padded_chunk_count, _) = compute_padded_chunk_size(data_size); + (tx, data, start_offset + padded_chunk_count as u64) +} + +#[cfg(test)] +pub mod tests { + use file_location_cache::{test_util::AnnounceFileBuilder, FileLocationCache}; + use libp2p::PeerId; + use shared_types::TxID; + use std::sync::Arc; + use storage::{ + log_store::{log_manager::LogConfig, Store as LogStore}, + LogManager, + }; + use storage_async::Store; + use task_executor::test_utils::TestRuntime; + use tokio::sync::RwLock; + + pub struct TestStoreRuntime { + pub runtime: TestRuntime, + pub store: Store, + } + + impl Default for TestStoreRuntime { + fn default() -> Self { + let store = Arc::new(RwLock::new(Self::new_store())); + Self::new(store) + } + } + + impl TestStoreRuntime { + pub fn new_store() -> impl LogStore { + LogManager::memorydb(LogConfig::default()).unwrap() + } + + pub fn new(store: Arc>) -> TestStoreRuntime { + let runtime = TestRuntime::default(); + let executor = runtime.task_executor.clone(); + Self { + runtime, + store: Store::new(store, executor), + } + } + } + + pub fn create_file_location_cache(peer_id: PeerId, txs: Vec) -> Arc { + let cache = FileLocationCache::default(); + + for tx_id in txs { + let announcement = AnnounceFileBuilder::default() + .with_tx_id(tx_id) + .with_peer_id(peer_id) + .build(); + cache.insert(announcement); + } + + Arc::new(cache) + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1899b27 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +jsonrpcclient +pyyaml +pysha3 +coincurve +eth-utils +py-ecc +web3 +eth_tester +rtoml \ No newline at end of file diff --git a/run/config.toml b/run/config.toml new file mode 100644 index 0000000..293bb69 --- /dev/null +++ b/run/config.toml @@ -0,0 +1,32 @@ +network_dir = "network" +network_listen_address = "0.0.0.0" +# network_enr_address = "" +# network_enr_tcp_port = 1234 +# network_enr_udp_port = 1234 +network_libp2p_port = 1234 +network_discovery_port = 1234 +network_target_peers = 3 +network_boot_nodes = ["/ip4/141.148.134.20/udp/1234/p2p/16Uiu2HAmPxGNWu9eVAQPJww79J32pTJLKGcpjRMb4Qb8xxKkyuG1","/ip4/129.146.156.125/udp/1234/p2p/16Uiu2HAm93Hd5azfhkGBbkx1zero3nYHvfjQYM2NtiW4R3r5bE2g"] +network_libp2p_nodes = [] +network_private = false +network_disable_discovery = false + +db_dir = "db" + +rpc_enabled = true +rpc_listen_address = "0.0.0.0:5678" + +log_config_file = "log_config" + +blockchain_rpc_endpoint = "https://evmtestnet.confluxrpc.com" + +log_contract_address = "0xcb7afff6f048ec1fb033aa7b495ed0dd932c8656" +mine_contract_address = "0x0d0d21020d04621479489f9ebde9a0373efbb838" + +log_sync_start_block_number = 134253180 +log_page_size = 999 + +# [sync] +# auto_sync_disabled = false +# max_sync_files = 8 +# find_peer_timeout = "30s" diff --git a/run/log_config b/run/log_config new file mode 100644 index 0000000..b2b5d0d --- /dev/null +++ b/run/log_config @@ -0,0 +1 @@ +debug \ No newline at end of file diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..5e3a425 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +1.73.0 diff --git a/tests/cache_test.py b/tests/cache_test.py new file mode 100755 index 0000000..c56fa14 --- /dev/null +++ b/tests/cache_test.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data, data_to_segments +from utility.utils import wait_until + + +class ExampleTest(TestFramework): + def run_test(self): + client = self.nodes[0] + + chunk_data = b"\x01" * 256 * 1025 + submissions, data_root = create_submission(chunk_data) + + segments = data_to_segments(chunk_data) + client.zgs_upload_segment(segments[0]) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + wait_until(lambda: client.zgs_get_file_info(data_root)["isCached"]) + client.zgs_upload_segment(segments[1]) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + +if __name__ == "__main__": + ExampleTest().main() diff --git a/tests/ccov.sh b/tests/ccov.sh new file mode 100755 index 0000000..74ab4b7 --- /dev/null +++ b/tests/ccov.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -euo pipefail +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )" + +cd $ROOT_DIR +echo "If you are running this script for the first time, please clean previous +debug build first by running \`rm -rf target/debug\`." + +# Install dependencies +cargo install grcov + +# Build binary and run unit tests with code coverage. +export CARGO_INCREMENTAL=0 +export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests" +export RUSTC_BOOTSTRAP=1 + +cargo build +cargo test --all + +# Run python integration tests. +export ZGS="`pwd`/target/debug/zgs_node" +./tests/test_all.py + +# Generate code coverage data +if [ -d "ccov" ] +then + rm -dr ccov +fi + +mkdir ccov +zip -0 ccov/ccov.zip `find . \( -name "*.gc*" \) -print` +grcov ccov/ccov.zip -s . -t html --llvm --branch --ignore-not-existing --ignore "/*" \ +--ignore "*target/debug/build/libp2p-*" \ +--ignore "*target/debug/build/clang-sys*" \ +--ignore "*target/debug/build/librocksdb-sys*" \ +--ignore "*target/debug/build/solang*" -o ccov + +echo "Code coverage result is saved to directory 'ccov'. +You can open 'ccov/index.html' with a web brower to start." + diff --git a/tests/cli_submission_test.py b/tests/cli_submission_test.py new file mode 100755 index 0000000..8f1c5b5 --- /dev/null +++ b/tests/cli_submission_test.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +import base64 +import random +import tempfile + +from config.node_config import GENESIS_ACCOUNT +from utility.submission import ENTRY_SIZE, bytes_to_entries +from utility.utils import ( + assert_equal, + wait_until, +) +from test_framework.test_framework import TestFramework + + +class CliSubmissionTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 2 + + def run_test(self): + data_size = [ + 2, + 255, + 256, + 257, + 1023, + 1024, + 1025, + 256 * 1023, + 256 * 1024, + 256 * 1025, + 256 * 2048, + 256 * 16385, + 256 * 1024 * 64, + ] + + for i, v in enumerate(data_size): + self.__test_upload_file_with_cli( + v, i + 1, False if v >= 256 * 1024 * 64 else True + ) + + def __test_upload_file_with_cli(self, size, submission_index, rand_data=True): + node_idx = random.randint(0, self.num_nodes - 1) + self.log.info("node index: %d, file size: %d", node_idx, size) + + file_to_upload = tempfile.NamedTemporaryFile(dir=self.root_dir, delete=False) + data = random.randbytes(size) if rand_data else b"\x10" * size + + file_to_upload.write(data) + file_to_upload.close() + + root = self._upload_file_use_cli( + self.blockchain_nodes[0].rpc_url, + self.contract.address(), + GENESIS_ACCOUNT.key, + self.nodes[node_idx].rpc_url, + file_to_upload, + ) + + self.log.info("root: %s", root) + wait_until(lambda: self.contract.num_submissions() == submission_index) + + client = self.nodes[node_idx] + wait_until(lambda: client.zgs_get_file_info(root) is not None) + wait_until(lambda: client.zgs_get_file_info(root)["finalized"]) + + num_of_entris = bytes_to_entries(size) + if num_of_entris > 1: + start_idx = random.randint(0, num_of_entris - 2) + end_idx = min( + random.randint(start_idx + 1, num_of_entris - 1), start_idx + ENTRY_SIZE + ) + + assert_equal( + client.zgs_download_segment(root, start_idx, end_idx), + base64.b64encode( + data[start_idx * ENTRY_SIZE : end_idx * ENTRY_SIZE] + ).decode("utf-8"), + ) + + for i in range(0, self.num_nodes): + if node_idx == i: + continue + + self.log.info("wait node %d", i) + self.nodes[i].admin_start_sync_file(submission_index - 1) + wait_until( + lambda: self.nodes[i].sycn_status_is_completed_or_unknown( + submission_index - 1 + ) + ) + + wait_until(lambda: self.nodes[i].zgs_get_file_info(root) is not None) + wait_until(lambda: self.nodes[i].zgs_get_file_info(root)["finalized"]) + + # start_idx = random.randint(0, num_of_entris - 1) + # end_idx = min( + # random.randint(start_idx + 1, num_of_entris), start_idx + ENTRY_SIZE + # ) + + assert_equal( + client.zgs_download_segment(root, 0, 1), + self.nodes[i].zgs_download_segment(root, 0, 1), + ) + + +if __name__ == "__main__": + CliSubmissionTest().main() diff --git a/tests/config/MockToken.json b/tests/config/MockToken.json new file mode 100644 index 0000000..5d82c15 --- /dev/null +++ b/tests/config/MockToken.json @@ -0,0 +1,286 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "MockToken", + "sourceName": "contracts/token/MockToken.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x608034620003ac576040906001600160401b039080830182811182821017620002ac5783526007815260209166135bd8dad554d160ca1b8383015283519184830183811083821117620002ac578552600391828452621554d160ea1b85850152815192818411620002ac578054936001938486811c96168015620003a1575b888710146200038b578190601f9687811162000335575b508890878311600114620002ce57600092620002c2575b505060001982841b1c191690841b1781555b8451918211620002ac5760049485548481811c91168015620002a1575b888210146200028c5785811162000241575b508690858411600114620001d657938394918492600095620001ca575b50501b92600019911b1c19161782555b33156200018b57505062000130600254620003b1565b6002553360005260008152816000206200014b8154620003b1565b905560007fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef835192633b9aca0084523393a3516108bc9081620003da8239f35b60649284519262461bcd60e51b845283015260248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152fd5b0151935038806200010a565b9190601f198416928760005284896000209460005b8b898383106200022957505050106200020e575b50505050811b0182556200011a565b01519060f884600019921b161c1916905538808080620001ff565b868601518955909701969485019488935001620001eb565b86600052876000208680860160051c8201928a871062000282575b0160051c019085905b82811062000275575050620000ed565b6000815501859062000265565b925081926200025c565b602287634e487b7160e01b6000525260246000fd5b90607f1690620000db565b634e487b7160e01b600052604160045260246000fd5b015190503880620000ac565b90869350601f19831691856000528a6000209260005b8c8282106200031e575050841162000305575b505050811b018155620000be565b015160001983861b60f8161c19169055388080620002f7565b8385015186558a97909501949384019301620002e4565b90915083600052886000208780850160051c8201928b861062000381575b918891869594930160051c01915b8281106200037157505062000095565b6000815585945088910162000361565b9250819262000353565b634e487b7160e01b600052602260045260246000fd5b95607f16956200007e565b600080fd5b90633b9aca008201809211620003c357565b634e487b7160e01b600052601160045260246000fdfe608060408181526004918236101561001657600080fd5b600092833560e01c91826306fdde031461049657508163095ea7b31461046c57816318160ddd1461044d57816323b872dd14610383578163313ce56714610367578163395093511461031757816370a08231146102e057816395d89b41146101c1578163a457c2d71461011957508063a9059cbb146100e95763dd62ed3e1461009e57600080fd5b346100e557806003193601126100e557806020926100ba6105bb565b6100c26105d6565b6001600160a01b0391821683526001865283832091168252845220549051908152f35b5080fd5b50346100e557806003193601126100e5576020906101126101086105bb565b602435903361060f565b5160018152f35b905082346101be57826003193601126101be576101346105bb565b918360243592338152600160205281812060018060a01b038616825260205220549082821061016d576020856101128585038733610784565b608490602086519162461bcd60e51b8352820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b6064820152fd5b80fd5b8383346100e557816003193601126100e557805190828454600181811c908083169283156102d6575b60209384841081146102c3578388529081156102a75750600114610252575b505050829003601f01601f191682019267ffffffffffffffff84118385101761023f575082918261023b925282610572565b0390f35b634e487b7160e01b815260418552602490fd5b8787529192508591837f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5b8385106102935750505050830101858080610209565b80548886018301529301928490820161027d565b60ff1916878501525050151560051b8401019050858080610209565b634e487b7160e01b895260228a52602489fd5b91607f16916101ea565b5050346100e55760203660031901126100e55760209181906001600160a01b036103086105bb565b16815280845220549051908152f35b5050346100e557806003193601126100e55761011260209261036061033a6105bb565b338352600186528483206001600160a01b038216845286529184902054602435906105ec565b9033610784565b5050346100e557816003193601126100e5576020905160128152f35b839150346100e55760603660031901126100e55761039f6105bb565b6103a76105d6565b91846044359460018060a01b0384168152600160205281812033825260205220549060001982036103e1575b60208661011287878761060f565b84821061040a57509183916103ff6020969561011295033383610784565b9193948193506103d3565b606490602087519162461bcd60e51b8352820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152fd5b5050346100e557816003193601126100e5576020906002549051908152f35b5050346100e557806003193601126100e55760209061011261048c6105bb565b6024359033610784565b8490843461056e578260031936011261056e5782600354600181811c90808316928315610564575b60209384841081146102c3578388529081156102a7575060011461050e57505050829003601f01601f191682019267ffffffffffffffff84118385101761023f575082918261023b925282610572565b600387529192508591837fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b8385106105505750505050830101858080610209565b80548886018301529301928490820161053a565b91607f16916104be565b8280fd5b6020808252825181830181905290939260005b8281106105a757505060409293506000838284010152601f8019910116010190565b818101860151848201604001528501610585565b600435906001600160a01b03821682036105d157565b600080fd5b602435906001600160a01b03821682036105d157565b919082018092116105f957565b634e487b7160e01b600052601160045260246000fd5b6001600160a01b0390811691821561073157169182156106e05760008281528060205260408120549180831061068c57604082827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9587602096528286520382822055868152206106818282546105ec565b9055604051908152a3565b60405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608490fd5b60405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608490fd5b60405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608490fd5b6001600160a01b0390811691821561083557169182156107e55760207f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925918360005260018252604060002085600052825280604060002055604051908152a3565b60405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608490fd5b60405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fdfea26469706673582212200239d5fdc8ba06d4c9278a25c561798906e419c37123e597870f8ad34e28da2864736f6c63430008100033", + "deployedBytecode": "0x608060408181526004918236101561001657600080fd5b600092833560e01c91826306fdde031461049657508163095ea7b31461046c57816318160ddd1461044d57816323b872dd14610383578163313ce56714610367578163395093511461031757816370a08231146102e057816395d89b41146101c1578163a457c2d71461011957508063a9059cbb146100e95763dd62ed3e1461009e57600080fd5b346100e557806003193601126100e557806020926100ba6105bb565b6100c26105d6565b6001600160a01b0391821683526001865283832091168252845220549051908152f35b5080fd5b50346100e557806003193601126100e5576020906101126101086105bb565b602435903361060f565b5160018152f35b905082346101be57826003193601126101be576101346105bb565b918360243592338152600160205281812060018060a01b038616825260205220549082821061016d576020856101128585038733610784565b608490602086519162461bcd60e51b8352820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b6064820152fd5b80fd5b8383346100e557816003193601126100e557805190828454600181811c908083169283156102d6575b60209384841081146102c3578388529081156102a75750600114610252575b505050829003601f01601f191682019267ffffffffffffffff84118385101761023f575082918261023b925282610572565b0390f35b634e487b7160e01b815260418552602490fd5b8787529192508591837f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5b8385106102935750505050830101858080610209565b80548886018301529301928490820161027d565b60ff1916878501525050151560051b8401019050858080610209565b634e487b7160e01b895260228a52602489fd5b91607f16916101ea565b5050346100e55760203660031901126100e55760209181906001600160a01b036103086105bb565b16815280845220549051908152f35b5050346100e557806003193601126100e55761011260209261036061033a6105bb565b338352600186528483206001600160a01b038216845286529184902054602435906105ec565b9033610784565b5050346100e557816003193601126100e5576020905160128152f35b839150346100e55760603660031901126100e55761039f6105bb565b6103a76105d6565b91846044359460018060a01b0384168152600160205281812033825260205220549060001982036103e1575b60208661011287878761060f565b84821061040a57509183916103ff6020969561011295033383610784565b9193948193506103d3565b606490602087519162461bcd60e51b8352820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152fd5b5050346100e557816003193601126100e5576020906002549051908152f35b5050346100e557806003193601126100e55760209061011261048c6105bb565b6024359033610784565b8490843461056e578260031936011261056e5782600354600181811c90808316928315610564575b60209384841081146102c3578388529081156102a7575060011461050e57505050829003601f01601f191682019267ffffffffffffffff84118385101761023f575082918261023b925282610572565b600387529192508591837fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b8385106105505750505050830101858080610209565b80548886018301529301928490820161053a565b91607f16916104be565b8280fd5b6020808252825181830181905290939260005b8281106105a757505060409293506000838284010152601f8019910116010190565b818101860151848201604001528501610585565b600435906001600160a01b03821682036105d157565b600080fd5b602435906001600160a01b03821682036105d157565b919082018092116105f957565b634e487b7160e01b600052601160045260246000fd5b6001600160a01b0390811691821561073157169182156106e05760008281528060205260408120549180831061068c57604082827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9587602096528286520382822055868152206106818282546105ec565b9055604051908152a3565b60405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608490fd5b60405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608490fd5b60405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608490fd5b6001600160a01b0390811691821561083557169182156107e55760207f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925918360005260018252604060002085600052825280604060002055604051908152a3565b60405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608490fd5b60405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fdfea26469706673582212200239d5fdc8ba06d4c9278a25c561798906e419c37123e597870f8ad34e28da2864736f6c63430008100033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/tests/config/bsc.toml b/tests/config/bsc.toml new file mode 100644 index 0000000..2b5c92b --- /dev/null +++ b/tests/config/bsc.toml @@ -0,0 +1,101 @@ +[Eth] +SyncMode = "snap" +DisablePeerTxBroadcast = false +EthDiscoveryURLs = [] +SnapDiscoveryURLs = [] +TrustDiscoveryURLs = [] +NoPruning = false +NoPrefetch = false +DirectBroadcast = false +DisableSnapProtocol = false +DisableDiffProtocol = false +EnableTrustProtocol = false +DiffSync = false +RangeLimit = false +TxLookupLimit = 2350000 +LightPeers = 100 +UltraLightFraction = 75 +DatabaseCache = 512 +DatabaseFreezer = "" +DatabaseDiff = "" +TrieCleanCache = 154 +TrieCleanCacheJournal = "triecache" +TrieCleanCacheRejournal = 3600000000000 +TrieDirtyCache = 256 +TrieTimeout = 3600000000000 +SnapshotCache = 102 +TriesInMemory = 128 +TriesVerifyMode = "local" +Preimages = false +PersistDiff = false +DiffBlock = 86400 +PruneAncientData = false +EnablePreimageRecording = false +EWASMInterpreter = "" +EVMInterpreter = "" +RPCGasCap = 50000000 +RPCEVMTimeout = 5000000000 +RPCTxFeeCap = 1e+00 + +[Eth.Miner] +DelayLeftOver = 50000000 +GasFloor = 0 +GasCeil = 8000000 +GasPrice = 1000000000 +Recommit = 3000000000 +Noverify = false + +[Eth.Ethash] +CacheDir = "ethash" +CachesInMem = 2 +CachesOnDisk = 3 +CachesLockMmap = false +DatasetDir = "" +DatasetsInMem = 1 +DatasetsOnDisk = 2 +DatasetsLockMmap = false +PowMode = 2 +NotifyFull = false + +[Eth.TxPool] +Locals = [] +NoLocals = false +Journal = "transactions.rlp" +Rejournal = 3600000000000 +PriceLimit = 1 +PriceBump = 10 +AccountSlots = 16 +GlobalSlots = 5120 +AccountQueue = 64 +GlobalQueue = 1024 +Lifetime = 10800000000000 +ReannounceTime = 315360000000000000 + +[Eth.GPO] +Blocks = 20 +Percentile = 60 +MaxHeaderHistory = 0 +MaxBlockHistory = 0 +MaxPrice = 100000000000 +IgnorePrice = 4 +OracleThreshold = 1000 + +[Node] +IPCPath = "geth.ipc" +HTTPHost = "127.0.0.1" +HTTPVirtualHosts = ["localhost"] +HTTPModules = ["personal", "eth", "net", "web3", "admin", "txpool", "miner"] +GraphQLVirtualHosts = ["localhost"] + +[Node.P2P] +MaxPeers = 50 +NoDiscovery = false +StaticNodes = [] +VerifyNodes = [] +TrustedNodes = [] +EnableMsgEvents = false + +[Node.HTTPTimeouts] +ReadTimeout = 30000000000 +WriteTimeout = 30000000000 +IdleTimeout = 120000000000 \ No newline at end of file diff --git a/tests/config/genesis.json b/tests/config/genesis.json new file mode 100644 index 0000000..7112690 --- /dev/null +++ b/tests/config/genesis.json @@ -0,0 +1,32 @@ +{ + "config":{ + "chainId":2001, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "ramanujanBlock": 0, + "nielsBlock": 0 + }, + "nonce":"0x0000000000000061", + "timestamp":"0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "gasLimit":"0x8000000", + "difficulty":"0x100", + "mixhash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase":"0x3333333333333333333333333333331234567890", + "alloc":{ + "fbe45681Ac6C53D5a40475F7526baC1FE7590fb8":{ + "balance":"400000000000000000000000000" + }, + "0e768D12395C8ABFDEdF7b1aEB0Dd1D27d5E2A7F":{ + "balance":"400000000000000000000000000" + } + } + } \ No newline at end of file diff --git a/tests/config/node_config.py b/tests/config/node_config.py new file mode 100644 index 0000000..4e02373 --- /dev/null +++ b/tests/config/node_config.py @@ -0,0 +1,50 @@ +from web3 import Web3 + +ZGS_CONFIG = { + "log_config_file": "log_config", +} + +BSC_CONFIG = dict( + NetworkId=1000, + HTTPPort=8545, + HTTPHost="127.0.0.1", + Etherbase="0x7df9a875a174b3bc565e6424a0050ebc1b2d1d82", + DataDir="test/local_ethereum_blockchain/node1", + Port=30303, + Verbosity=5, +) + +CONFLUX_CONFIG = dict( + mode="dev", + chain_id=10, + jsonrpc_http_eth_port=8545, + tcp_port=32323, + log_level="debug", + log_file="./conflux.log", + public_address="127.0.0.1", + poll_lifetime_in_seconds=60, + dev_allow_phase_change_without_peer="true", + dev_block_interval_ms=200, +) + +BLOCK_SIZE_LIMIT = 200 * 1024 +GENESIS_PRIV_KEY = "46b9e861b63d3509c88b7817275a30d22d62c8cd8fa6486ddee35ef0d8e0495f" +MINER_ID = "308a6e102a5829ba35e4ba1da0473c3e8bd45f5d3ffb91e31adb43f25463dddb" +GENESIS_ACCOUNT = Web3().eth.account.from_key(GENESIS_PRIV_KEY) +TX_PARAMS = { + "gasPrice": 10_000_000_000, + "gas": 10_000_000, + "from": GENESIS_ACCOUNT.address, + "gas": 2_000_000, +} + +GENESIS_PRIV_KEY1 = "9a6d3ba2b0c7514b16a006ee605055d71b9edfad183aeb2d9790e9d4ccced471" +GENESIS_ACCOUNT1 = Web3().eth.account.from_key(GENESIS_PRIV_KEY1) +TX_PARAMS1 = { + "gasPrice": 10_000_000_000, + "gas": 10_000_000, + "from": GENESIS_ACCOUNT1.address, +} + +NO_SEAL_FLAG = 0x1 +NO_MERKLE_PROOF_FLAG = 0x2 diff --git a/tests/crash_test.py b/tests/crash_test.py new file mode 100644 index 0000000..ed0bfc1 --- /dev/null +++ b/tests/crash_test.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + + +class CrashTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 4 + + def run_test(self): + # 1. all nodes are running + chunk_data = b"\x01" * 256 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root) is not None) + + segment = submit_data(self.nodes[0], chunk_data) + self.log.info("segment: %s", segment) + + for i in range(self.num_nodes): + self.log.info("wait for node: %s", i) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root)["finalized"] + ) + + # 2: first node runnging, other nodes killed + self.log.info("kill node") + # kill node to simulate node crash + for i in range(1, self.num_nodes): + self.nodes[i].stop(kill=True) + + chunk_data = b"\x02" * 256 * 1024 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 2) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root) is not None) + + segment = submit_data(self.nodes[0], chunk_data) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root)["finalized"]) + + self.start_storage_node(1) + self.nodes[1].wait_for_rpc_connection() + self.nodes[1].admin_start_sync_file(1) + wait_until(lambda: self.nodes[1].zgs_get_file_info(data_root) is not None) + wait_until(lambda: self.nodes[1].zgs_get_file_info(data_root)["finalized"]) + + for i in range(2, self.num_nodes): + self.start_storage_node(i) + self.nodes[i].wait_for_rpc_connection() + self.nodes[i].admin_start_sync_file(1) + + self.nodes[i].stop(kill=True) + self.start_storage_node(i) + self.nodes[i].wait_for_rpc_connection() + self.nodes[i].admin_start_sync_file(1) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root)["finalized"] + ) + + # 4: node[1..] synced contract entries and killed + self.log.info("kill node 0") + self.nodes[0].stop(kill=True) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + + chunk_data = b"\x03" * 256 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 3) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root) is not None) + + for i in range(1, self.num_nodes): + self.nodes[i].stop(kill=True) + + segment = submit_data(self.nodes[0], chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root)["finalized"]) + + for i in range(1, self.num_nodes): + self.log.info("wait for node: %s", i) + self.start_storage_node(i) + self.nodes[i].wait_for_rpc_connection() + self.nodes[i].admin_start_sync_file(2) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root)["finalized"] + ) + + # 5: node[1..] synced contract entries and killed, sync disorder + self.nodes[0].stop(kill=True) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + + chunk_data = b"\x04" * 256 * 1561 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 4) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root) is not None) + + for i in range(1, 2): + self.nodes[i].stop(kill=True) + + chunk_data1 = b"\x05" * 256 + submissions1, data_root1 = create_submission(chunk_data1) + self.contract.submit(submissions1) + wait_until(lambda: self.contract.num_submissions() == 5) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root1) is not None) + + for i in range(2, self.num_nodes): + self.nodes[i].stop(kill=True) + + segment = submit_data(self.nodes[0], chunk_data) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root)["finalized"]) + + segment = submit_data(self.nodes[0], chunk_data1) + wait_until(lambda: self.nodes[0].zgs_get_file_info(data_root1)["finalized"]) + + for i in range(1, self.num_nodes): + self.log.info("wait for node: %s", i) + self.start_storage_node(i) + self.nodes[i].wait_for_rpc_connection() + self.nodes[i].admin_start_sync_file(4) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root1) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root1)["finalized"] + ) + + self.nodes[i].admin_start_sync_file(3) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root)["finalized"] + ) + + +if __name__ == "__main__": + CrashTest().main() diff --git a/tests/dep_pip3.sh b/tests/dep_pip3.sh new file mode 100755 index 0000000..8686bcd --- /dev/null +++ b/tests/dep_pip3.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +function install() { + if [ "`pip3 show ${1%%=*}`" = "" ]; then + pip3 install $1 + fi +} + +install jsonrpcclient +install pyyaml +install pysha3 +install coincurve +install eth_utils +install py-ecc +install web3 +install eth_tester \ No newline at end of file diff --git a/tests/example_test.py b/tests/example_test.py new file mode 100755 index 0000000..25fe3d1 --- /dev/null +++ b/tests/example_test.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + + +class ExampleTest(TestFramework): + def run_test(self): + client = self.nodes[0] + + chunk_data = b"\x00" * 256 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + segment = submit_data(client, chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + +if __name__ == "__main__": + ExampleTest().main() diff --git a/tests/fuzz_test.py b/tests/fuzz_test.py new file mode 100644 index 0000000..0c17fe7 --- /dev/null +++ b/tests/fuzz_test.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +import random +import threading + +from test_framework.test_framework import TestFramework +from config.node_config import TX_PARAMS, TX_PARAMS1 +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + +SAMLL_SIZE = 350 +LARGE_SIZE = 200 +RADMON_SIZE = 300 + + +class FuzzTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 4 + + def run_test(self): + lock = threading.Lock() + nodes_index = [i for i in range(self.num_nodes)] + + account_lock = threading.Lock() + accounts = [TX_PARAMS, TX_PARAMS1] + + def run_chunk_size(sizes, nodes, contract, log): + data_roots = [] + for i, v in enumerate(sizes): + log.info("submission %s, data size: %s", i, v) + + chunk_data = random.randbytes(v) + submissions, data_root = create_submission(chunk_data) + + account_lock.acquire() + account_idx = random.randint(0, len(accounts) - 1) + contract.submit(submissions, tx_prarams=accounts[account_idx]) + account_lock.release() + + lock.acquire() + client_idx = random.randint(0, len(nodes_index) - 1) + idx = nodes_index[client_idx] + nodes_index.pop(client_idx) + lock.release() + + log.info("submit data via client %s", idx) + wait_until( + lambda: nodes[idx].zgs_get_file_info(data_root) is not None + ) + segment = submit_data(nodes[idx], chunk_data) + wait_until( + lambda: nodes[idx].zgs_get_file_info(data_root)["finalized"] + ) + + lock.acquire() + nodes_index.append(idx) + lock.release() + + data_roots.append(data_root) + + for data_root in data_roots: + for idx in range(len(nodes)): + wait_until( + lambda: nodes[idx].zgs_get_file_info(data_root) is not None + ) + + def wait_finalized(): + ret = nodes[idx].zgs_get_file_info(data_root) + if ret["finalized"]: + return True + else: + nodes[idx].admin_start_sync_file(ret['tx']['seq']) + return False + + wait_until( + lambda: wait_finalized(), timeout = 180 + ) + + def run_small_chunk_size(nodes, contract, log): + sizes = [i for i in range(1, SAMLL_SIZE + 1)] + random.shuffle(sizes) + + run_chunk_size(sizes, nodes, contract, log) + + def run_large_chunk_size(nodes, contract, log): + sizes = [i for i in range(256 * 1024 * 256 - LARGE_SIZE, 256 * 1024 * 256 )] + random.shuffle(sizes) + + run_chunk_size(sizes, nodes, contract, log) + + def run_random_chunk_size(nodes, contract, log): + sizes = [] + for i in range(RADMON_SIZE): + sizes.append(random.randint(1, 256 * 1024 * 256)) + + run_chunk_size(sizes, nodes, contract, log) + + t1 = threading.Thread( + target=run_small_chunk_size, args=(self.nodes, self.contract, self.log) + ) + t2 = threading.Thread( + target=run_large_chunk_size, args=(self.nodes, self.contract, self.log) + ) + t3 = threading.Thread( + target=run_random_chunk_size, args=(self.nodes, self.contract, self.log) + ) + + t1.start() + t2.start() + t3.start() + + t1.join() + t2.join() + t3.join() + + +if __name__ == "__main__": + FuzzTest().main() diff --git a/tests/mine_test.py b/tests/mine_test.py new file mode 100755 index 0000000..481727d --- /dev/null +++ b/tests/mine_test.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +from test_framework.blockchain_node import BlockChainNodeType +from test_framework.test_framework import TestFramework +from config.node_config import MINER_ID, GENESIS_PRIV_KEY +from test_framework.blockchain_node import BlockChainNodeType +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + + +class MineTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 1 + self.zgs_node_configs[0] = { + "miner_id": MINER_ID, + "miner_key": GENESIS_PRIV_KEY, + } + + def submit_data(self, item, size): + submissions_before = self.contract.num_submissions() + client = self.nodes[0] + chunk_data = item * 256 * size + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == submissions_before + 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + segment = submit_data(client, chunk_data) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + def run_test(self): + blockchain = self.blockchain_nodes[0] + + self.log.info("flow address: %s", self.contract.address()) + self.log.info("mine address: %s", self.mine_contract.address()) + + quality = int(2**256 / 40960) + self.mine_contract.set_quality(quality) + + self.log.info("Submit the first data chunk") + self.submit_data(b"\x11", 2000) + + self.log.info("Wait for the first mine context release") + wait_until(lambda: int(blockchain.eth_blockNumber(), 16) > 100, timeout=180) + + self.log.info("Wait for the first mine answer") + wait_until(lambda: self.mine_contract.last_mined_epoch() == 1) + + self.log.info("Wait for the second mine context release") + wait_until(lambda: int(blockchain.eth_blockNumber(), 16) > 200, timeout=180) + + self.log.info("Wait for the second mine answer") + wait_until(lambda: self.mine_contract.last_mined_epoch() == 2) + + self.nodes[0].miner_stop() + self.log.info("Wait for the third mine context release") + wait_until(lambda: int(blockchain.eth_blockNumber(), 16) > 307, timeout=180) + self.log.info("Submit the second data chunk") + self.submit_data(b"\x22", 2000) + # Now the storage node should have the latest flow, but the mining context is using an old one. + self.nodes[0].miner_start() + + self.log.info("Wait for the third mine answer") + wait_until(lambda: self.mine_contract.last_mined_epoch() == 3) + + +if __name__ == "__main__": + MineTest(blockchain_node_type=BlockChainNodeType.BSC).main() diff --git a/tests/parallel_submission_test.py b/tests/parallel_submission_test.py new file mode 100644 index 0000000..8128ade --- /dev/null +++ b/tests/parallel_submission_test.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +import random +import threading + +from utility.submission import create_submission, submit_data +from utility.utils import ( + wait_until, +) +from test_framework.test_framework import TestFramework + + +class ParallelSubmissionTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 2 + + def run_test(self): + size = 245759 + thread_count = 16 + + chunks = self.generate_data(size, thread_count) + for submission, _, _ in chunks: + self.log.info("submission: %s", submission) + self.contract.submit(submission) + + wait_until(lambda: self.contract.num_submissions() == thread_count) + + client = self.nodes[0] + for _, data_root, _ in chunks: + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + def submit(client, chunk_data): + submit_data(client, chunk_data) + + threads = [] + for _, _, chunk_data in chunks: + t = threading.Thread(target=submit, args=(client, chunk_data)) + threads.append(t) + + for t in threads[::-1]: + t.start() + + for t in threads: + t.join() + + for _, data_root, _ in chunks: + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + def generate_data(self, size, num): + res = [] + for _ in range(num): + chunk_data = random.randbytes(size) + submission = create_submission(chunk_data) + res.append((submission[0], submission[1], chunk_data)) + + return res + + +if __name__ == "__main__": + ParallelSubmissionTest().main() diff --git a/tests/partial_submission_test.py b/tests/partial_submission_test.py new file mode 100755 index 0000000..e2fb3df --- /dev/null +++ b/tests/partial_submission_test.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data, data_to_segments +from utility.utils import wait_until + + +class ExampleTest(TestFramework): + def run_test(self): + client = self.nodes[0] + + chunk_data = b"\x01" * 256 * 1025 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + segments = data_to_segments(chunk_data) + client.zgs_upload_segment(segments[1]) + segment = client.zgs_download_segment(data_root, 1024, 1025) + print(segment) + print(client.rpc.zgs_downloadSegmentWithProof([data_root, 1])) + + +if __name__ == "__main__": + ExampleTest().main() diff --git a/tests/random_test.py b/tests/random_test.py new file mode 100755 index 0000000..c3eea3c --- /dev/null +++ b/tests/random_test.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +import random + +from test_framework.blockchain_node import BlockChainNodeType +from test_framework.test_framework import TestFramework +from utility.submission import create_submission +from utility.submission import submit_data +from utility.utils import ( + assert_equal, + wait_until, +) + + +class RandomTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 4 + for i in range(self.num_nodes): + self.zgs_node_configs[i] = {"find_peer_timeout_secs": 1, "confirmation_block_count": 1} + + def run_test(self): + max_size = 256 * 1024 * 64 + small_max_size = 256 * 64 + small_ratio = 0.9 + no_data_ratio = 0.2 + crash_ratio = 0.2 + clean_ratio = 0.3 + + file_list = [] + # The number of files is set to a larger number for local tests. + tx_count = 20 + for i in range(tx_count): + chosen_node = random.randint(0, self.num_nodes - 1) + if random.random() <= small_ratio: + size = random.randint(0, small_max_size) + else: + size = random.randint(0, max_size) + no_data = random.random() <= no_data_ratio + self.log.info(f"choose {chosen_node}, seq={i}, size={size}, no_data={no_data}") + + client = self.nodes[chosen_node] + chunk_data = random.randbytes(size) + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == i + 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None, timeout=120) + if not no_data: + submit_data(client, chunk_data) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + # Wait until the tx is sent out. + for node_index in range(len(self.nodes)): + if node_index != chosen_node: + self.log.debug(f"check {node_index}") + wait_until(lambda: self.nodes[node_index].zgs_get_file_info(data_root) is not None, timeout=120) + wait_until(lambda: self.nodes[node_index].zgs_get_file_info(data_root)["finalized"], timeout=300) + # TODO(zz): This is a temp solution to trigger auto sync after all nodes started. + if i >= tx_count - 2: + continue + file_list.append((data_root, no_data)) + + if random.random() <= crash_ratio: + # TODO(zz): node 0 is the boot node. + # If it's crashed and cleaned, it will not be connected by others now. + chosen_crash = random.randint(1, self.num_nodes - 1) + clean = random.random() <= clean_ratio + self.log.info(f"crash {chosen_crash}, clean={clean}") + self.stop_storage_node(chosen_crash, clean) + self.start_storage_node(chosen_crash) + self.nodes[chosen_crash].wait_for_rpc_connection() + + for i in range(tx_count): + for node in self.nodes: + status = node.zgs_get_file_info_by_tx_seq(i) + self.log.info(f"check tx seq {i}, {node.index}, status={status}") + for data_root, no_data in file_list: + if not no_data: + for node in self.nodes: + self.log.debug(f"check {data_root}, {node.index}") + wait_until(lambda: node.zgs_get_file_info(data_root)["finalized"], timeout=300) + + +if __name__ == "__main__": + RandomTest(blockchain_node_type=BlockChainNodeType.Conflux).main() diff --git a/tests/recovery_test.py b/tests/recovery_test.py new file mode 100755 index 0000000..fe23c44 --- /dev/null +++ b/tests/recovery_test.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + + +class RecoveryTest(TestFramework): + def run_test(self): + client = self.nodes[0] + + chunk_data = b"\x01" * 256 * 5 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + segment = submit_data(client, chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + self.stop_storage_node(0) + chunk_data = b"\x02" * 256 * 7 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 2) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + segment = submit_data(client, chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + self.stop_storage_node(0) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + # Test with larger data. + chunk_data = b"\x03" * 256 * 1024 * 19 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 3) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + self.stop_storage_node(0) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + submit_data(client, chunk_data) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + self.stop_storage_node(0) + self.start_storage_node(0) + self.nodes[0].wait_for_rpc_connection() + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + +if __name__ == "__main__": + RecoveryTest().main() diff --git a/tests/revert_test.py b/tests/revert_test.py new file mode 100755 index 0000000..f6fad01 --- /dev/null +++ b/tests/revert_test.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +from test_framework.test_framework import TestFramework +from test_framework.blockchain_node import BlockChainNodeType +from test_framework.conflux_node import connect_nodes, disconnect_nodes, sync_blocks +from config.node_config import CONFLUX_CONFIG, TX_PARAMS1 +from utility.submission import create_submission, submit_data +from utility.utils import wait_until + + +class RevertTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 1 + + del CONFLUX_CONFIG["dev_block_interval_ms"] + + def run_test(self): + blockchain_client1 = self.blockchain_nodes[0] + blockchain_client2 = self.blockchain_nodes[1] + + self.log.info("Node 1 epoch {}".format(blockchain_client1.cfx_epochNumber())) + self.log.info("Node 2 epoch {}".format(blockchain_client2.cfx_epochNumber())) + + disconnect_nodes(self.blockchain_nodes, 0, 1) + blockchain_client1.generate_empty_blocks(5) + + self.log.info("Node 1 epoch {}".format(blockchain_client1.cfx_epochNumber())) + self.log.info("Node 2 epoch {}".format(blockchain_client2.cfx_epochNumber())) + + client = self.nodes[0] + chunk_data = b"\x00" * 256 + submissions, data_root = create_submission(chunk_data) + + tx_hash = self.contract.submit(submissions, 0) + self.log.info("tx 1 hash: {}".format(tx_hash.hex())) + wait_until(lambda: self.contract.num_submissions() == 1) + assert client.zgs_get_file_info(data_root) is None + # Generate blocks for confirmation + blockchain_client1.generate_empty_blocks(12) + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + + segment = submit_data(client, chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + self.log.info("Node 1 epoch {}".format(blockchain_client1.cfx_epochNumber())) + self.log.info("Node 2 epoch {}".format(blockchain_client2.cfx_epochNumber())) + + self.log.info("===== submit tx to second node =====") + chunk_data = b"\x10" * 256 + submissions, data_root1 = create_submission(chunk_data) + tx_hash = self.contract.submit(submissions, 1, tx_prarams=TX_PARAMS1) + self.log.info("tx 2 hash: {}".format(tx_hash.hex())) + wait_until(lambda: self.contract.num_submissions(1) == 1) + + blockchain_client2.generate_empty_blocks(30) + + self.log.info("Node 1 epoch {}".format(blockchain_client1.cfx_epochNumber())) + self.log.info("Node 2 epoch {}".format(blockchain_client2.cfx_epochNumber())) + + connect_nodes(self.blockchain_nodes, 0, 1) + sync_blocks(self.blockchain_nodes[0:2]) + + self.log.info("Node 1 epoch {}".format(blockchain_client1.cfx_epochNumber())) + self.log.info("Node 2 epoch {}".format(blockchain_client2.cfx_epochNumber())) + + wait_until(lambda: client.zgs_get_file_info(data_root) is None) + wait_until(lambda: client.zgs_get_file_info(data_root1) is not None) + wait_until(lambda: not client.zgs_get_file_info(data_root1)["finalized"]) + + segment = submit_data(client, chunk_data) + self.log.info("segment: %s", segment) + wait_until(lambda: client.zgs_get_file_info(data_root1)["finalized"]) + + +if __name__ == "__main__": + RevertTest(blockchain_node_type=BlockChainNodeType.Conflux).main() diff --git a/tests/rpc_test.py b/tests/rpc_test.py new file mode 100755 index 0000000..f6edc6b --- /dev/null +++ b/tests/rpc_test.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +import tempfile + +from config.node_config import GENESIS_ACCOUNT +from test_framework.test_framework import TestFramework +from utility.submission import create_submission, submit_data +from utility.utils import ( + assert_equal, + wait_until, +) + + +class RpcTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 2 + + def run_test(self): + client1 = self.nodes[0] + client2 = self.nodes[1] + + chunk_data = b"\x00" * 256 + submissions, data_root = create_submission(chunk_data) + self.contract.submit(submissions) + wait_until(lambda: self.contract.num_submissions() == 1) + wait_until(lambda: self.contract.num_submissions(1) == 1) + assert_equal(self.contract.num_submissions(), self.contract.num_submissions(1)) + + wait_until(lambda: client1.zgs_get_file_info(data_root) is not None) + assert_equal(client1.zgs_get_file_info(data_root)["finalized"], False) + + wait_until(lambda: client2.zgs_get_file_info(data_root) is not None) + assert_equal(client2.zgs_get_file_info(data_root)["finalized"], False) + + segment = submit_data(client1, chunk_data) + self.log.info("segment: %s", segment) + + wait_until(lambda: client1.zgs_get_file_info(data_root)["finalized"]) + assert_equal( + client1.zgs_download_segment(data_root, 0, 1), segment[0]["data"] + ) + + client2.admin_start_sync_file(0) + wait_until(lambda: client2.sycn_status_is_completed_or_unknown(0)) + + wait_until(lambda: client2.zgs_get_file_info(data_root)["finalized"]) + assert_equal( + client2.zgs_download_segment(data_root, 0, 1), segment[0]["data"] + ) + + self.__test_upload_file_with_cli(client1) + + client2.shutdown() + wait_until(lambda: client1.zgs_get_status() == 0) + + def __test_upload_file_with_cli(self, client1): + # Test with uploading files with cli + n_files = 1 + for size in [2, 1023, 2051]: + self.log.debug("file size: %d", size) + file_to_upload = tempfile.NamedTemporaryFile( + dir=self.root_dir, delete=False + ) + file_to_upload.write(b"\x10" * 256 * size) + file_to_upload.close() + + root = self._upload_file_use_cli( + self.blockchain_nodes[0].rpc_url, + self.contract.address(), + GENESIS_ACCOUNT.key, + self.nodes[0].rpc_url, + file_to_upload, + ) + + n_files += 1 + wait_until(lambda: self.contract.num_submissions() == n_files) + + wait_until(lambda: client1.zgs_get_file_info(root) is not None) + wait_until(lambda: client1.zgs_get_file_info(root)["finalized"]) + + for i in range(1, self.num_nodes): + self.log.info("wait node %d", i) + self.nodes[i].admin_start_sync_file(n_files - 1) + wait_until( + lambda: self.nodes[i].sycn_status_is_completed_or_unknown( + n_files - 1 + ) + ) + + wait_until(lambda: self.nodes[i].zgs_get_file_info(root) is not None) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(root)["finalized"] + ) + + assert_equal( + client1.zgs_download_segment(root, 0, 2), + self.nodes[i].zgs_download_segment(root, 0, 2), + ) + + +if __name__ == "__main__": + RpcTest().main() diff --git a/tests/same_root_test.py b/tests/same_root_test.py new file mode 100755 index 0000000..f7c293a --- /dev/null +++ b/tests/same_root_test.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import base64 +import random +from test_framework.test_framework import TestFramework +from utility.submission import ENTRY_SIZE, submit_data +from utility.submission import create_submission +from utility.utils import ( + assert_equal, + wait_until, +) + + +class SubmissionTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 1 + + def run_test(self): + data_size = [ + 2, + 256, + 256 * 1023 + 1, + 256 * 1024 * 256, + ] + same_root_tx_count = 2 + + next_tx_seq = 0 + for i, v in enumerate(data_size): + chunk_data = random.randbytes(v) + # Send tx before uploading data + for _ in range(same_root_tx_count): + self.submit_tx_for_data(chunk_data, next_tx_seq) + next_tx_seq += 1 + + # Send tx and upload data. + self.submit_tx_for_data(chunk_data, next_tx_seq) + self.submit_data(chunk_data) + next_tx_seq += 1 + # Check if all transactions are finalized + for tx_offset in range(same_root_tx_count + 1): + tx_seq = next_tx_seq - 1 - tx_offset + status = self.nodes[0].zgs_get_file_info_by_tx_seq(tx_seq) + assert status["finalized"] + + # Send tx after uploading data + for _ in range(same_root_tx_count): + self.submit_tx_for_data(chunk_data, next_tx_seq, data_finalized=True) + next_tx_seq += 1 + + def submit_tx_for_data(self, chunk_data, tx_seq, data_finalized=False, node_idx=0): + submissions, data_root = create_submission(chunk_data) + self.log.info("data root: %s, submissions: %s", data_root, submissions) + self.contract.submit(submissions) + + wait_until(lambda: self.contract.num_submissions() == tx_seq + 1) + + client = self.nodes[node_idx] + wait_until(lambda: client.zgs_get_file_info_by_tx_seq(tx_seq) is not None) + assert_equal(client.zgs_get_file_info_by_tx_seq(tx_seq)["finalized"], data_finalized) + + def submit_data(self, chunk_data, node_idx=0): + _, data_root = create_submission(chunk_data) + client = self.nodes[node_idx] + segments = submit_data(client, chunk_data) + self.log.debug( + "segments: %s", [(s["root"], s["index"], s["proof"]) for s in segments] + ) + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + +if __name__ == "__main__": + SubmissionTest().main() diff --git a/tests/submission_test.py b/tests/submission_test.py new file mode 100755 index 0000000..376f13f --- /dev/null +++ b/tests/submission_test.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +import base64 +import random +from test_framework.test_framework import TestFramework +from utility.submission import ENTRY_SIZE, submit_data +from utility.submission import create_submission +from utility.utils import ( + assert_equal, + wait_until, +) + + +class SubmissionTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 3 + + def run_test(self): + data_size = [ + 2, + 255, + 256, + 257, + 1023, + 1024, + 1025, + 256 * 1023, + 256 * 1023 + 1, + 256 * 1024, + 256 * 1024 + 1, + 256 * 1025, + 256 * 2048 - 1, + 256 * 2048, + 256 * 16385, + 256 * 1024 * 256, + ] + + for i, v in enumerate(data_size): + self.submission_data(v, i + 1, False if v >= 256 * 1024 * 256 else True) + + def submission_data(self, size, submission_index, rand_data=True): + node_idx = random.randint(0, self.num_nodes - 1) + self.log.info("node index: %d, file size: %d", node_idx, size) + chunk_data = random.randbytes(size) if rand_data else b"\x10" * size + + submissions, data_root = create_submission(chunk_data) + self.log.info("data root: %s, submissions: %s", data_root, submissions) + self.contract.submit(submissions) + + wait_until(lambda: self.contract.num_submissions() == submission_index) + + client = self.nodes[node_idx] + wait_until(lambda: client.zgs_get_file_info(data_root) is not None) + assert_equal(client.zgs_get_file_info(data_root)["finalized"], False) + + segments = submit_data(client, chunk_data) + self.log.info( + "segments: %s", [(s["root"], s["index"], s["proof"]) for s in segments] + ) + + wait_until(lambda: client.zgs_get_file_info(data_root)["finalized"]) + + first_entry = base64.b64decode(segments[0]["data"].encode("utf-8"))[ + 0:ENTRY_SIZE + ] + + assert_equal( + base64.b64decode( + client.zgs_download_segment(data_root, 0, 1).encode("utf-8") + ), + first_entry, + ) + + for i in range(0, self.num_nodes): + if node_idx == i: + continue + + self.nodes[i].admin_start_sync_file(submission_index - 1) + wait_until( + lambda: self.nodes[i].sycn_status_is_completed_or_unknown( + submission_index - 1 + ) + ) + + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root) is not None + ) + wait_until( + lambda: self.nodes[i].zgs_get_file_info(data_root)["finalized"] + ) + + assert_equal( + base64.b64decode( + self.nodes[i] + .zgs_download_segment(data_root, 0, 1) + .encode("utf-8") + ), + first_entry, + ) + + +if __name__ == "__main__": + SubmissionTest().main() diff --git a/tests/sync_test.py b/tests/sync_test.py new file mode 100755 index 0000000..781927b --- /dev/null +++ b/tests/sync_test.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +import random + +from test_framework.test_framework import TestFramework +from utility.submission import create_submission +from utility.submission import submit_data +from utility.utils import ( + assert_equal, + wait_until, +) + + +class SyncTest(TestFramework): + def setup_params(self): + self.num_blockchain_nodes = 2 + self.num_nodes = 2 + + def run_test(self): + client1 = self.nodes[0] + client2 = self.nodes[1] + + self.stop_storage_node(1) + + size = 256 * 1024 + chunk_data = random.randbytes(size) + + submissions, data_root = create_submission(chunk_data) + self.log.info("data root: %s, submissions: %s", data_root, submissions) + self.contract.submit(submissions) + + wait_until(lambda: self.contract.num_submissions() == 1) + + wait_until(lambda: client1.zgs_get_file_info(data_root) is not None) + assert_equal(client1.zgs_get_file_info(data_root)["finalized"], False) + + segments = submit_data(client1, chunk_data) + self.log.info( + "segments: %s", [(s["root"], s["index"], s["proof"]) for s in segments] + ) + + wait_until(lambda: client1.zgs_get_file_info(data_root)["finalized"]) + + self.start_storage_node(1) + self.nodes[1].wait_for_rpc_connection() + + client2.admin_start_sync_file(0) + wait_until(lambda: client2.sycn_status_is_completed_or_unknown(0)) + + wait_until(lambda: client2.zgs_get_file_info(data_root)["finalized"]) + assert_equal( + client2.zgs_download_segment(data_root, 0, 1), + client1.zgs_download_segment(data_root, 0, 1), + ) + + +if __name__ == "__main__": + SyncTest().main() diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100755 index 0000000..2507a82 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +import argparse +import os +import shutil +import stat +import subprocess +import sys + +from concurrent.futures import ProcessPoolExecutor + +from utility.utils import is_windows_platform + +PORT_MIN = 11000 +PORT_MAX = 65535 +PORT_RANGE = 500 + +__file_path__ = os.path.dirname(os.path.realpath(__file__)) + + +def run_single_test(py, script, test_dir, index, port_min, port_max): + try: + # Make sure python thinks it can write unicode to its stdout + "\u2713".encode("utf_8").decode(sys.stdout.encoding) + TICK = "✓ " + CROSS = "✖ " + CIRCLE = "○ " + except UnicodeDecodeError: + TICK = "P " + CROSS = "x " + CIRCLE = "o " + + BOLD, BLUE, RED, GREY = ("", ""), ("", ""), ("", ""), ("", "") + if os.name == "posix": + # primitive formatting on supported + # terminal via ANSI escape sequences: + BOLD = ("\033[0m", "\033[1m") + BLUE = ("\033[0m", "\033[0;34m") + RED = ("\033[0m", "\033[0;31m") + GREY = ("\033[0m", "\033[1;30m") + print("Running " + script) + port_min = port_min + (index * PORT_RANGE) % (port_max - port_min) + color = BLUE + glyph = TICK + try: + subprocess.check_output( + args=[py, script, "--randomseed=1", f"--port-min={port_min}"], + stdin=None, + cwd=test_dir, + ) + except subprocess.CalledProcessError as err: + color = RED + glyph = CROSS + print(color[1] + glyph + " Testcase " + script + color[0]) + print("Output of " + script + "\n" + err.output.decode("utf-8")) + raise err + print(color[1] + glyph + " Testcase " + script + color[0]) + + +def run(): + dir_name = os.path.join(__file_path__, "tmp") + if not os.path.exists(dir_name): + os.makedirs(dir_name, exist_ok=True) + + conflux_path = os.path.join(dir_name, "conflux") + if not os.path.exists(conflux_path): + build_conflux(conflux_path) + + parser = argparse.ArgumentParser(usage="%(prog)s [options]") + parser.add_argument( + "--max-workers", + dest="max_workers", + default=5, + type=int, + ) + parser.add_argument( + "--port-max", + dest="port_max", + default=PORT_MAX, + type=int, + ) + parser.add_argument( + "--port-min", + dest="port_min", + default=PORT_MIN, + type=int, + ) + + options = parser.parse_args() + + TEST_SCRIPTS = [] + + test_dir = os.path.dirname(os.path.realpath(__file__)) + test_subdirs = [ + "", # include test_dir itself + ] + + slow_tests = {} + long_manual_tests = {"fuzz_test.py"} + + for subdir in test_subdirs: + subdir_path = os.path.join(test_dir, subdir) + for file in os.listdir(subdir_path): + if file.endswith("_test.py"): + rel_path = os.path.join(subdir, file) + if rel_path not in slow_tests and rel_path not in long_manual_tests: + TEST_SCRIPTS.append(rel_path) + + executor = ProcessPoolExecutor(max_workers=options.max_workers) + test_results = [] + + py = "python3" + if hasattr(sys, "getwindowsversion"): + py = "python" + + i = 0 + # Start slow tests first to avoid waiting for long-tail jobs + for script in slow_tests: + f = executor.submit( + run_single_test, py, script, test_dir, i, options.port_min, options.port_max + ) + test_results.append((script, f)) + i += 1 + for script in TEST_SCRIPTS: + f = executor.submit( + run_single_test, py, script, test_dir, i, options.port_min, options.port_max + ) + test_results.append((script, f)) + i += 1 + + failed = set() + for script, f in test_results: + try: + f.result() + except subprocess.CalledProcessError as err: + print("CalledProcessError " + repr(err)) + failed.add(script) + + if len(failed) > 0: + print("The following test fails: ") + for c in failed: + print(c) + sys.exit(1) + + +def build_conflux(conflux_path): + destination_path = os.path.join(__file_path__, "tmp", "conflux_tmp") + if os.path.exists(destination_path): + shutil.rmtree(destination_path) + + clone_command = "git clone https://github.com/Conflux-Chain/conflux-rust.git" + clone_with_path = clone_command + " " + destination_path + os.system(clone_with_path) + + origin_path = os.getcwd() + os.chdir(destination_path) + os.system("cargo build --release --bin conflux") + + path = os.path.join(destination_path, "target", "release", "conflux") + shutil.copyfile(path, conflux_path) + + if not is_windows_platform(): + st = os.stat(conflux_path) + os.chmod(conflux_path, st.st_mode | stat.S_IEXEC) + + os.chdir(origin_path) + + +if __name__ == "__main__": + run() diff --git a/tests/test_framework/blockchain_node.py b/tests/test_framework/blockchain_node.py new file mode 100644 index 0000000..bcbb5ca --- /dev/null +++ b/tests/test_framework/blockchain_node.py @@ -0,0 +1,326 @@ +import json +import os +import subprocess +import tempfile +import time + +from eth_utils import decode_hex +from web3 import Web3, HTTPProvider +from web3.middleware import construct_sign_and_send_raw_middleware +from enum import Enum, unique +from config.node_config import ( + GENESIS_PRIV_KEY, + GENESIS_PRIV_KEY1, + TX_PARAMS, + MINER_ID, + NO_MERKLE_PROOF_FLAG, + NO_SEAL_FLAG, + TX_PARAMS1, +) +from utility.simple_rpc_proxy import SimpleRpcProxy +from utility.utils import ( + initialize_config, + wait_until, +) + + +@unique +class BlockChainNodeType(Enum): + Conflux = 0 + BSC = 1 + + +@unique +class NodeType(Enum): + BlockChain = 0 + Zgs = 1 + + +class FailedToStartError(Exception): + """Raised when a node fails to start correctly.""" + + +class TestNode: + def __init__( + self, node_type, index, data_dir, rpc_url, binary, config, log, rpc_timeout=10 + ): + self.node_type = node_type + self.index = index + self.data_dir = data_dir + self.rpc_url = rpc_url + self.config = config + self.rpc_timeout = rpc_timeout + self.process = None + self.stdout = None + self.stderr = None + self.config_file = os.path.join(self.data_dir, "config.toml") + self.args = [binary, "--config", self.config_file] + self.running = False + self.rpc_connected = False + self.rpc = None + self.log = log + + def __del__(self): + if self.process: + self.process.terminate() + + def __getattr__(self, name): + """Dispatches any unrecognised messages to the RPC connection.""" + assert self.rpc_connected and self.rpc is not None, self._node_msg( + "Error: no RPC connection" + ) + return getattr(self.rpc, name) + + def _node_msg(self, msg: str) -> str: + """Return a modified msg that identifies this node by its index as a debugging aid.""" + return "[node %s %d] %s" % (self.node_type, self.index, msg) + + def _raise_assertion_error(self, msg: str): + """Raise an AssertionError with msg modified to identify this node.""" + raise AssertionError(self._node_msg(msg)) + + def setup_config(self): + os.mkdir(self.data_dir) + initialize_config(self.config_file, self.config) + + def start(self, redirect_stderr=False): + my_env = os.environ.copy() + if self.stdout is None: + self.stdout = tempfile.NamedTemporaryFile( + dir=self.data_dir, prefix="stdout", delete=False + ) + if self.stderr is None: + self.stderr = tempfile.NamedTemporaryFile( + dir=self.data_dir, prefix="stderr", delete=False + ) + + if redirect_stderr: + self.process = subprocess.Popen( + self.args, + stdout=self.stdout, + stderr=self.stdout, + cwd=self.data_dir, + env=my_env, + ) + else: + self.process = subprocess.Popen( + self.args, + stdout=self.stdout, + stderr=self.stderr, + cwd=self.data_dir, + env=my_env, + ) + self.running = True + + def wait_for_rpc_connection(self): + raise NotImplementedError + + def _wait_for_rpc_connection(self, check): + """Sets up an RPC connection to the node process. Returns False if unable to connect.""" + # Poll at a rate of four times per second + poll_per_s = 4 + for _ in range(poll_per_s * self.rpc_timeout): + if self.process.poll() is not None: + raise FailedToStartError( + self._node_msg( + "exited with status {} during initialization".format( + self.process.returncode + ) + ) + ) + rpc = SimpleRpcProxy(self.rpc_url, timeout=self.rpc_timeout) + if check(rpc): + self.rpc_connected = True + self.rpc = rpc + return + time.sleep(1.0 / poll_per_s) + self._raise_assertion_error( + "failed to get RPC proxy: index = {}, rpc_url = {}".format( + self.index, self.rpc_url + ) + ) + + def stop(self, expected_stderr="", kill=False, wait=True): + """Stop the node.""" + if not self.running: + return + if kill: + self.process.kill() + else: + self.process.terminate() + if wait: + self.wait_until_stopped() + # Check that stderr is as expected + self.stderr.seek(0) + stderr = self.stderr.read().decode("utf-8").strip() + # TODO: Check how to avoid `pthread lock: Invalid argument`. + if stderr != expected_stderr and stderr != "pthread lock: Invalid argument": + # print process status for debug + if self.return_code is None: + self.log.info("Process is still running") + else: + self.log.info( + "Process has terminated with code {}".format(self.return_code) + ) + + raise AssertionError( + "Unexpected stderr {} != {} from node={}{}".format( + stderr, expected_stderr, self.node_type, self.index + ) + ) + + self.stdout.close() + self.stderr.close() + self.stdout = None + self.stderr = None + + def is_node_stopped(self): + """Checks whether the node has stopped. + + Returns True if the node has stopped. False otherwise. + This method is responsible for freeing resources (self.process).""" + if not self.running: + return True + return_code = self.process.poll() + if return_code is None: + return False + + # process has stopped. Assert that it didn't return an error code. + # assert return_code == 0, self._node_msg( + # "Node returned non-zero exit code (%d) when stopping" % return_code + # ) + self.running = False + self.process = None + self.rpc = None + self.log.debug("Node stopped") + self.return_code = return_code + return True + + def wait_until_stopped(self, timeout=20): + wait_until(self.is_node_stopped, timeout=timeout) + + +class BlockchainNode(TestNode): + def __init__( + self, + index, + data_dir, + rpc_url, + binary, + local_conf, + contract_path, + token_contract_path, + mine_contract_path, + log, + blockchain_node_type, + rpc_timeout=10, + ): + self.contract_path = contract_path + self.token_contract_path = token_contract_path + self.mine_contract_path = mine_contract_path + + self.blockchain_node_type = blockchain_node_type + + super().__init__( + NodeType.BlockChain, + index, + data_dir, + rpc_url, + binary, + local_conf, + log, + rpc_timeout, + ) + + def wait_for_rpc_connection(self): + self._wait_for_rpc_connection(lambda rpc: rpc.eth_syncing() is False) + + def wait_for_start_mining(self): + self._wait_for_rpc_connection(lambda rpc: int(rpc.eth_blockNumber(), 16) > 0) + + def wait_for_transaction_receipt(self, w3, tx_hash, timeout=120, parent_hash=None): + return w3.eth.wait_for_transaction_receipt(tx_hash, timeout) + + def setup_contract(self): + w3 = Web3(HTTPProvider(self.rpc_url)) + + account1 = w3.eth.account.from_key(GENESIS_PRIV_KEY) + account2 = w3.eth.account.from_key(GENESIS_PRIV_KEY1) + w3.middleware_onion.add( + construct_sign_and_send_raw_middleware([account1, account2]) + ) + # account = w3.eth.account.from_key(GENESIS_PRIV_KEY1) + # w3.middleware_onion.add(construct_sign_and_send_raw_middleware(account)) + + def deploy_contract(path, args=None): + if args is None: + args = [] + contract_interface = json.load(open(path, "r")) + contract = w3.eth.contract( + abi=contract_interface["abi"], + bytecode=contract_interface["bytecode"], + ) + tx_params = TX_PARAMS.copy() + del tx_params["gas"] + tx_hash = contract.constructor(*args).transact(tx_params) + tx_receipt = self.wait_for_transaction_receipt(w3, tx_hash) + contract = w3.eth.contract( + address=tx_receipt.contractAddress, + abi=contract_interface["abi"], + ) + return contract, tx_hash + + self.log.debug("Start deploy contracts") + token_contract, _ = deploy_contract(self.token_contract_path) + self.log.debug("ERC20 deployed") + flow_contract, flow_contract_hash = deploy_contract( + self.contract_path, [token_contract.address] + ) + self.log.debug("Flow deployed") + mine_contract, _ = deploy_contract( + self.mine_contract_path, + [flow_contract.address, 0], + ) + self.log.debug("Mine deployed") + self.log.info("All contracts deployed") + + tx_hash = token_contract.functions.approve( + flow_contract.address, int(1e9) + ).transact(TX_PARAMS) + self.wait_for_transaction_receipt(w3, tx_hash) + + # setup second account + amount = int(1e8) + tx_hash = token_contract.functions.transfer(account2.address, amount).transact( + TX_PARAMS + ) + self.wait_for_transaction_receipt(w3, tx_hash) + + tx_hash = token_contract.functions.approve( + flow_contract.address, amount + ).transact(TX_PARAMS1) + self.wait_for_transaction_receipt(w3, tx_hash) + + tx_hash = mine_contract.functions.setMiner(decode_hex(MINER_ID)).transact(TX_PARAMS) + self.wait_for_transaction_receipt(w3, tx_hash) + + return flow_contract, flow_contract_hash, mine_contract + + def get_contract(self, contract_address): + w3 = Web3(HTTPProvider(self.rpc_url)) + + account1 = w3.eth.account.from_key(GENESIS_PRIV_KEY) + account2 = w3.eth.account.from_key(GENESIS_PRIV_KEY1) + w3.middleware_onion.add( + construct_sign_and_send_raw_middleware([account1, account2]) + ) + + contract_interface = json.load(open(self.contract_path, "r")) + return w3.eth.contract(address=contract_address, abi=contract_interface["abi"]) + + def wait_for_transaction(self, tx_hash): + w3 = Web3(HTTPProvider(self.rpc_url)) + w3.eth.wait_for_transaction_receipt(tx_hash) + + def start(self): + super().start(self.blockchain_node_type == BlockChainNodeType.BSC) diff --git a/tests/test_framework/bsc_node.py b/tests/test_framework/bsc_node.py new file mode 100644 index 0000000..886ce80 --- /dev/null +++ b/tests/test_framework/bsc_node.py @@ -0,0 +1,164 @@ +import os +import platform +import requests +import shutil +import stat + +from config.node_config import BSC_CONFIG +from eth_utils import encode_hex +from utility.signature_utils import ec_random_keys, priv_to_addr +from test_framework.blockchain_node import BlockChainNodeType, BlockchainNode +from utility.utils import ( + blockchain_p2p_port, + blockchain_rpc_port, + is_windows_platform, + wait_until, +) + +__file_path__ = os.path.dirname(os.path.realpath(__file__)) + + +class BSCNode(BlockchainNode): + def __init__( + self, + index, + root_dir, + binary, + updated_config, + contract_path, + token_contract_path, + mine_contract_path, + log, + rpc_timeout=10, + ): + local_conf = BSC_CONFIG.copy() + indexed_config = { + "HTTPPort": blockchain_rpc_port(index), + "Port": blockchain_p2p_port(index), + } + # Set configs for this specific node. + local_conf.update(indexed_config) + # Overwrite with personalized configs. + local_conf.update(updated_config) + data_dir = os.path.join(root_dir, "blockchain_node" + str(index)) + rpc_url = "http://" + local_conf["HTTPHost"] + ":" + str(local_conf["HTTPPort"]) + + self.genesis_config = os.path.join( + __file_path__, "..", "config", "genesis.json" + ) + self.binary = binary + + if not os.path.exists(self.binary): + log.info("binary does not exist") + dir_name = os.path.dirname(self.binary) + if not os.path.exists(dir_name): + os.makedirs(dir_name, exist_ok=True) + + try: + with open(f"{self.binary}", "xb") as f: + self.__try_download_node(f, log) + except FileExistsError: + log.info("Binary is alrady under downloading") + + wait_until(lambda: os.access(f"{self.binary}", os.X_OK), timeout=120) + + self.node_id = encode_hex(priv_to_addr(ec_random_keys()[0])) + + super().__init__( + index, + data_dir, + rpc_url, + binary, + local_conf, + contract_path, + token_contract_path, + mine_contract_path, + log, + BlockChainNodeType.BSC, + rpc_timeout, + ) + + def __try_download_node(self, f, log): + url = "https://api.github.com/repos/{}/{}/releases/latest".format( + "bnb-chain", "bsc" + ) + req = requests.get(url) + if req.ok: + asset_name = self.__get_asset_name() + + url = "" + for asset in req.json()["assets"]: + if asset["name"].lower() == asset_name: + url = asset["browser_download_url"] + break + + if url: + log.info("Try to download geth from %s", url) + f.write(requests.get(url).content) + f.close() + + if not is_windows_platform(): + st = os.stat(self.binary) + os.chmod(self.binary, st.st_mode | stat.S_IEXEC) + else: + log.info("Request failed with %s", req) + + def __get_asset_name(self): + sys = platform.system().lower() + if sys == "linux": + return "geth_linux" + elif sys == "windows": + return "geth_windows.exe" + elif sys == "darwin": + return "geth_mac" + else: + raise RuntimeError("Unable to recognize platform") + + def start(self): + self.args = [ + self.binary, + "--datadir", + self.data_dir, + "init", + self.genesis_config, + ] + super().start() + + wait_until(lambda: self.process.poll() is not None) + ret = self.process.poll() + assert ret == 0, "BSC init should be successful" + + self.log.info("BSC node%d init finished with return code %d", self.index, ret) + + config_file = os.path.join(__file_path__, "..", "config", "bsc.toml") + target = os.path.join(self.data_dir, "bsc.toml") + shutil.copyfile(config_file, target) + + self.args = [ + self.binary, + "--datadir", + self.data_dir, + "--port", + str(self.config["Port"]), + "--http", + "--http.api", + "personal,eth,net,web3,admin,txpool,miner", + "--http.port", + str(self.config["HTTPPort"]), + # "--syncmode", + # "full", + # "--mine", + "--miner.threads", + "1", + "--miner.etherbase", + self.node_id, + "--networkid", + str(self.config["NetworkId"]), + "--verbosity", + str(self.config["Verbosity"]), + "--config", + "bsc.toml", + ] + + self.log.info("Start BSC node %d", self.index) + super().start() diff --git a/tests/test_framework/conflux_node.py b/tests/test_framework/conflux_node.py new file mode 100644 index 0000000..3be502a --- /dev/null +++ b/tests/test_framework/conflux_node.py @@ -0,0 +1,335 @@ +import os +import random +import threading +import time + +from config.node_config import BLOCK_SIZE_LIMIT, CONFLUX_CONFIG +import eth_utils +from test_framework.blockchain_node import BlockChainNodeType, BlockchainNode +from utility.signature_utils import ( + encode_int32, + get_nodeid, + sha3, +) +from utility.simple_rpc_proxy import SimpleRpcProxy +from utility.utils import ( + blockchain_p2p_port, + blockchain_rpc_port, + blockchain_rpc_port_core, + wait_until, +) +from web3.exceptions import TransactionNotFound + + +class ConfluxNode(BlockchainNode): + def __init__( + self, + index, + root_dir, + binary, + updated_config, + contract_path, + token_contract_path, + mine_contract_path, + log, + rpc_timeout=10, + ): + local_conf = CONFLUX_CONFIG.copy() + indexed_config = { + "jsonrpc_http_eth_port": blockchain_rpc_port(index), + "jsonrpc_local_http_port": blockchain_rpc_port_core(index), + "tcp_port": blockchain_p2p_port(index), + } + # Set configs for this specific node. + local_conf.update(indexed_config) + # Overwrite with personalized configs. + local_conf.update(updated_config) + data_dir = os.path.join(root_dir, "blockchain_node" + str(index)) + rpc_url = ( + "http://" + + local_conf["public_address"] + + ":" + + str(local_conf["jsonrpc_http_eth_port"]) + ) + self.ip = local_conf["public_address"] + self.port = str(local_conf["tcp_port"]) + + if "dev_block_interval_ms" in local_conf: + self.auto_mining = True + else: + self.auto_mining = False + + # setup core space rpc + core_space_rpc_url = ( + "http://" + + local_conf["public_address"] + + ":" + + str(local_conf["jsonrpc_local_http_port"]) + ) + + self.core_space_rpc = SimpleRpcProxy(core_space_rpc_url, timeout=rpc_timeout) + + super().__init__( + index, + data_dir, + rpc_url, + binary, + local_conf, + contract_path, + token_contract_path, + mine_contract_path, + log, + BlockChainNodeType.Conflux, + rpc_timeout, + ) + + def __getattr__(self, name): + """Dispatches any unrecognised messages to the RPC connection.""" + assert self.rpc_connected and self.rpc is not None, self._node_msg( + "Error: no RPC connection" + ) + if name.startswith("eth_") or name.startswith("parity_"): + return getattr(self.rpc, name) + else: + return getattr(self.core_space_rpc, name) + + def wait_for_transaction_receipt(self, w3, tx_hash, timeout=120, parent_hash=None): + if self.auto_mining: + return super().wait_for_transaction_receipt(w3, tx_hash, timeout) + else: + time_end = time.time() + timeout + while time.time() < time_end: + try: + tx_receipt = w3.eth.get_transaction_receipt(tx_hash) + except TransactionNotFound: + tx_receipt = None + if parent_hash: + parent_hash = self.generatefixedblock( + parent_hash, [], 1, False, None, None + ) + else: + self.generateoneblock(1, BLOCK_SIZE_LIMIT) + time.sleep(0.5) + + if tx_receipt is not None: + return tx_receipt + + raise TransactionNotFound + + def wait_for_nodeid(self): + pubkey, x, y = get_nodeid(self) + self.key = eth_utils.encode_hex(pubkey) + addr_tmp = bytearray(sha3(encode_int32(x) + encode_int32(y))[12:]) + addr_tmp[0] &= 0x0F + addr_tmp[0] |= 0x10 + self.addr = addr_tmp + self.log.debug("Get node {} nodeid {}".format(self.index, self.key)) + + def best_block_hash(self): + return self.core_space_rpc.cfx_getBestBlockHash() + + def cfx_epochNumber(self, epoch_number=None): + return self.core_space_rpc.cfx_epochNumber([epoch_number]) + + def getblockcount(self): + return self.core_space_rpc.getblockcount() + + def addnode(self, key, peer_addr): + return self.core_space_rpc.addnode([key, peer_addr]) + + def removenode(self, key, peer_addr): + return self.core_space_rpc.removenode([key, peer_addr]) + + def addlatency(self, node_id, latency_ms): + return self.core_space_rpc.addlatency([node_id, latency_ms]) + + def getnodeid(self, challenge): + return self.core_space_rpc.getnodeid([challenge]) + + def generate_empty_blocks(self, num_blocks): + return self.core_space_rpc.generate_empty_blocks([num_blocks]) + + def generateoneblock(self, num_txs, block_size_limit): + return self.core_space_rpc.generateoneblock([num_txs, block_size_limit]) + + def generatefixedblock( + self, parent_hash, referee, num_txs, adaptive, difficulty, pos_reference + ): + return self.core_space_rpc.generatefixedblock( + [parent_hash, referee, num_txs, adaptive, difficulty, pos_reference] + ) + + +def check_handshake(from_connection, target_node_id): + """ + Check whether node 'from_connection' has already + added node 'target_node_id' into its peer set. + """ + peers = from_connection.getpeerinfo() + for peer in peers: + if peer["nodeid"] == target_node_id and len(peer["protocols"]) > 0: + return True + return False + + +def get_peer_addr(connection): + return "{}:{}".format(connection.ip, connection.port) + + +def connect_nodes(nodes, a, node_num, timeout=60): + """ + Let node[a] connect to node[node_num] + """ + from_connection = nodes[a] + to_connection = nodes[node_num] + key = nodes[node_num].key + peer_addr = get_peer_addr(to_connection) + from_connection.addnode(key, peer_addr) + # poll until hello handshake complete to avoid race conditions + # with transaction relaying + wait_until( + lambda: check_handshake(from_connection, to_connection.key), timeout=timeout + ) + + +def sync_blocks(rpc_connections, *, sync_count=True, wait=1, timeout=60): + """ + Wait until everybody has the same tip. + + sync_blocks needs to be called with an rpc_connections set that has least + one node already synced to the latest, stable tip, otherwise there's a + chance it might return before all nodes are stably synced. + """ + stop_time = time.time() + timeout + while time.time() <= stop_time: + best_hash = [x.best_block_hash() for x in rpc_connections] + block_count = [x.getblockcount() for x in rpc_connections] + if best_hash.count(best_hash[0]) == len(rpc_connections) and ( + not sync_count or block_count.count(block_count[0]) == len(rpc_connections) + ): + return + time.sleep(wait) + raise AssertionError( + "Block sync timed out:{}".format( + "".join("\n {!r}".format(b) for b in best_hash + block_count) + ) + ) + + +def disconnect_nodes(nodes, from_connection, node_num): + try: + nodes[from_connection].removenode( + nodes[node_num].key, get_peer_addr(nodes[node_num]) + ) + nodes[node_num].removenode( + nodes[from_connection].key, get_peer_addr(nodes[from_connection]) + ) + except Exception as e: + # If this node is disconnected between calculating the peer id + # and issuing the disconnect, don't worry about it. + # This avoids a race condition if we're mass-disconnecting peers. + if e.error["code"] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED + raise + + # wait to disconnect + wait_until( + lambda: [ + peer + for peer in nodes[from_connection].getpeerinfo() + if peer["nodeid"] == nodes[node_num].key + ] + == [], + timeout=5, + ) + wait_until( + lambda: [ + peer + for peer in nodes[node_num].getpeerinfo() + if peer["nodeid"] == nodes[from_connection].key + ] + == [], + timeout=5, + ) + + +def connect_sample_nodes( + nodes, log, sample=3, latency_min=0, latency_max=300, timeout=30 +): + """ + Establish connections among nodes with each node having 'sample' outgoing peers. + It first lets all the nodes link as a loop, then randomly pick 'sample-1' + outgoing peers for each node. + """ + peer = [[] for _ in nodes] + latencies = [{} for _ in nodes] + threads = [] + num_nodes = len(nodes) + sample = min(num_nodes - 1, sample) + + for i in range(num_nodes): + # make sure all nodes are reachable + next = (i + 1) % num_nodes + peer[i].append(next) + lat = random.randint(latency_min, latency_max) + latencies[i][next] = lat + latencies[next][i] = lat + + for _ in range(sample - 1): + while True: + p = random.randint(0, num_nodes - 1) + if p not in peer[i] and not p == i: + peer[i].append(p) + lat = random.randint(latency_min, latency_max) + latencies[i][p] = lat + latencies[p][i] = lat + break + + for i in range(num_nodes): + t = ConnectThread(nodes, i, peer[i], latencies, log, min_peers=sample) + t.start() + threads.append(t) + + for t in threads: + t.join(timeout) + assert ( + not t.is_alive() + ), "Node[{}] connect to other nodes timeout in {} seconds".format(t.a, timeout) + assert not t.failed, "connect_sample_nodes failed." + + +class ConnectThread(threading.Thread): + def __init__(self, nodes, a, peers, latencies, log, min_peers=3, daemon=True): + threading.Thread.__init__(self, daemon=daemon) + self.nodes = nodes + self.a = a + self.peers = peers + self.latencies = latencies + self.log = log + self.min_peers = min_peers + self.failed = False + + def run(self): + try: + while True: + for i in range(len(self.peers)): + p = self.peers[i] + connect_nodes(self.nodes, self.a, p) + for p in self.latencies[self.a]: + self.nodes[self.a].addlatency( + self.nodes[p].key, self.latencies[self.a][p] + ) + if len(self.nodes[self.a].getpeerinfo()) >= self.min_peers: + break + else: + time.sleep(1) + except Exception as e: + node = self.nodes[self.a] + self.log.error( + "Node " + + str(self.a) + + " fails to be connected to " + + str(self.peers) + + ", ip={}, index={}".format(node.ip, node.index) + ) + self.log.error(e) + self.failed = True diff --git a/tests/test_framework/contract_proxy.py b/tests/test_framework/contract_proxy.py new file mode 100644 index 0000000..52c350a --- /dev/null +++ b/tests/test_framework/contract_proxy.py @@ -0,0 +1,66 @@ +from gettext import npgettext +from config.node_config import TX_PARAMS +from utility.utils import assert_equal + + +class ContractProxy: + def __init__(self, contract, blockchain_nodes): + self.contract = contract + self.contract_address = contract.address + self.blockchain_nodes = blockchain_nodes + + def _get_contract(self, node_idx=0): + return ( + self.contract + if node_idx == 0 + else self.blockchain_nodes[node_idx].get_contract(self.contract_address) + ) + + def _call(self, fn_name, node_idx, **args): + assert node_idx < len(self.blockchain_nodes) + + contract = self._get_contract(node_idx) + return getattr(contract.functions, fn_name)(**args).call() + + def _send(self, fn_name, node_idx, **args): + assert node_idx < len(self.blockchain_nodes) + + contract = self._get_contract(node_idx) + return getattr(contract.functions, fn_name)(**args).transact(TX_PARAMS) + + def address(self): + return self.contract_address + + +class FlowContractProxy(ContractProxy): + def submit( + self, submission_nodes, node_idx=0, tx_prarams=TX_PARAMS, parent_hash=None + ): + assert node_idx < len(self.blockchain_nodes) + + contract = self._get_contract(node_idx) + tx_hash = contract.functions.submit(submission_nodes).transact(tx_prarams) + receipt = self.blockchain_nodes[node_idx].wait_for_transaction_receipt( + contract.w3, tx_hash, parent_hash=parent_hash + ) + if receipt["status"] != 1: + print(receipt) + assert_equal(receipt["status"], 1) + return tx_hash + + def num_submissions(self, node_idx=0): + return self._call("numSubmissions", node_idx) + + def first_block(self, node_idx=0): + return self._call("firstBlock", node_idx) + + def epoch(self, node_idx=0): + return self._call("epoch", node_idx) + + +class MineContractProxy(ContractProxy): + def last_mined_epoch(self, node_idx=0): + return self._call("lastMinedEpoch", node_idx) + + def set_quality(self, quality, node_idx=0): + return self._send("setQuality", node_idx, _targetQuality=quality) diff --git a/tests/test_framework/test_framework.py b/tests/test_framework/test_framework.py new file mode 100644 index 0000000..7348639 --- /dev/null +++ b/tests/test_framework/test_framework.py @@ -0,0 +1,461 @@ +import argparse +from enum import Enum +import logging +import os +import pdb +import random +import shutil +import subprocess +import sys +import tempfile +import time +import traceback + +from eth_utils import encode_hex +from test_framework.bsc_node import BSCNode +from test_framework.contract_proxy import FlowContractProxy, MineContractProxy +from test_framework.zgs_node import ZgsNode +from test_framework.blockchain_node import BlockChainNodeType +from test_framework.conflux_node import ConfluxNode, connect_sample_nodes, sync_blocks + +from utility.utils import PortMin, is_windows_platform, wait_until + +__file_path__ = os.path.dirname(os.path.realpath(__file__)) + + +class TestStatus(Enum): + PASSED = 1 + FAILED = 2 + + +TEST_EXIT_PASSED = 0 +TEST_EXIT_FAILED = 1 + + +class TestFramework: + def __init__(self, blockchain_node_type=BlockChainNodeType.Conflux): + self.num_blockchain_nodes = None + self.num_nodes = None + self.blockchain_nodes = [] + self.nodes = [] + self.contract = None + self.blockchain_node_configs = {} + self.zgs_node_configs = {} + self.blockchain_node_type = blockchain_node_type + + def __setup_blockchain_node(self): + for i in range(self.num_blockchain_nodes): + if i in self.blockchain_node_configs: + updated_config = self.blockchain_node_configs[i] + else: + updated_config = {} + + node = None + if self.blockchain_node_type == BlockChainNodeType.BSC: + node = BSCNode( + i, + self.root_dir, + self.blockchain_binary, + updated_config, + self.contract_path, + self.token_contract_path, + self.mine_contract_path, + self.log, + 60, + ) + elif self.blockchain_node_type == BlockChainNodeType.Conflux: + node = ConfluxNode( + i, + self.root_dir, + self.blockchain_binary, + updated_config, + self.contract_path, + self.token_contract_path, + self.mine_contract_path, + self.log, + ) + else: + raise NotImplementedError + + self.blockchain_nodes.append(node) + node.setup_config() + node.start() + + # wait node to start to avoid NewConnectionError + time.sleep(1) + for node in self.blockchain_nodes: + node.wait_for_rpc_connection() + + if self.blockchain_node_type == BlockChainNodeType.BSC: + enodes = set( + [node.admin_nodeInfo()["enode"] for node in self.blockchain_nodes[1:]] + ) + for enode in enodes: + self.blockchain_nodes[0].admin_addPeer([enode]) + + # mine + self.blockchain_nodes[0].miner_start([1]) + + def wait_for_peer(): + peers = self.blockchain_nodes[0].admin_peers() + for peer in peers: + if peer["enode"] in enodes: + enodes.remove(peer["enode"]) + + if enodes: + for enode in enodes: + self.blockchain_nodes[0].admin_addPeer([enode]) + return False + + return True + + wait_until(lambda: wait_for_peer()) + + for node in self.blockchain_nodes: + node.wait_for_start_mining() + elif self.blockchain_node_type == BlockChainNodeType.Conflux: + for node in self.blockchain_nodes: + node.wait_for_nodeid() + + # make nodes full connected + if self.num_blockchain_nodes > 1: + connect_sample_nodes(self.blockchain_nodes, self.log) + sync_blocks(self.blockchain_nodes) + + contract, tx_hash, mine_contract = self.blockchain_nodes[0].setup_contract() + self.contract = FlowContractProxy(contract, self.blockchain_nodes) + self.mine_contract = MineContractProxy(mine_contract, self.blockchain_nodes) + + for node in self.blockchain_nodes[1:]: + node.wait_for_transaction(tx_hash) + + def __setup_zgs_node(self): + for i in range(self.num_nodes): + if i in self.zgs_node_configs: + updated_config = self.zgs_node_configs[i] + else: + updated_config = {} + + assert os.path.exists(self.zgs_binary), ( + "%s should be exist" % self.zgs_binary + ) + node = ZgsNode( + i, + self.root_dir, + self.zgs_binary, + updated_config, + self.contract.address(), + self.mine_contract.address(), + self.log, + ) + self.nodes.append(node) + node.setup_config() + # wait firt node start for connection + if i > 0: + time.sleep(1) + node.start() + + time.sleep(1) + for node in self.nodes: + node.wait_for_rpc_connection() + + def __parse_arguments(self): + parser = argparse.ArgumentParser(usage="%(prog)s [options]") + + parser.add_argument( + "--conflux-binary", + dest="conflux", + default=os.path.join( + __file_path__, + "../tmp/conflux" + (".exe" if is_windows_platform() else ""), + ), + type=str, + ) + + parser.add_argument( + "--bsc-binary", + dest="bsc", + default=os.path.join( + __file_path__, + "../tmp/geth" + (".exe" if is_windows_platform() else ""), + ), + type=str, + ) + + parser.add_argument( + "--zerog-storage-binary", + dest="zerog_storage", + default=os.getenv( + "ZGS", + default=os.path.join( + __file_path__, + "../../target/release/zgs_node" + + (".exe" if is_windows_platform() else ""), + ), + ), + type=str, + ) + + parser.add_argument( + "--zerog-storage-client", + dest="cli", + default=os.path.join( + __file_path__, + "../../target/zerog-storage-client" + + (".exe" if is_windows_platform() else ""), + ), + type=str, + ) + + parser.add_argument( + "--contract-path", + dest="contract", + default=os.path.join( + __file_path__, + "../../zerog-storage-contracts/artifacts/contracts/dataFlow/Flow.sol/Flow.json", + ), + type=str, + ) + + parser.add_argument( + "--token-contract-path", + dest="token_contract", + default=os.path.join( + __file_path__, + "../config/MockToken.json", + ), + type=str, + ) + + parser.add_argument( + "--mine-contract-path", + dest="mine_contract", + default=os.path.join( + __file_path__, + "../../zerog-storage-contracts/artifacts/contracts/test/PoraMineTest.sol/PoraMineTest.json", + ), + type=str, + ) + + parser.add_argument( + "-l", + "--loglevel", + dest="loglevel", + default="INFO", + help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console. Note that logs at all levels are always written to the test_framework.log file in the temporary test directory.", + ) + + parser.add_argument( + "--tmpdir", dest="tmpdir", help="Root directory for datadirs" + ) + + parser.add_argument( + "--randomseed", dest="random_seed", type=int, help="Set a random seed" + ) + + parser.add_argument("--port-min", dest="port_min", default=11000, type=int) + + parser.add_argument( + "--pdbonfailure", + dest="pdbonfailure", + default=False, + action="store_true", + help="Attach a python debugger if test fails", + ) + + self.options = parser.parse_args() + + def __start_logging(self): + # Add logger and logging handlers + self.log = logging.getLogger("TestFramework") + self.log.setLevel(logging.DEBUG) + + # Create file handler to log all messages + fh = logging.FileHandler( + self.options.tmpdir + "/test_framework.log", encoding="utf-8" + ) + fh.setLevel(logging.DEBUG) + + # Create console handler to log messages to stderr. By default this logs only error messages, but can be configured with --loglevel. + ch = logging.StreamHandler(sys.stdout) + # User can provide log level as a number or string (eg DEBUG). loglevel was caught as a string, so try to convert it to an int + ll = ( + int(self.options.loglevel) + if self.options.loglevel.isdigit() + else self.options.loglevel.upper() + ) + ch.setLevel(ll) + + # Format logs the same as bitcoind's debug.log with microprecision (so log files can be concatenated and sorted) + formatter = logging.Formatter( + fmt="%(asctime)s.%(msecs)03d000Z %(name)s (%(levelname)s): %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + ) + formatter.converter = time.gmtime + fh.setFormatter(formatter) + ch.setFormatter(formatter) + + # add the handlers to the logger + self.log.addHandler(fh) + self.log.addHandler(ch) + + def _upload_file_use_cli( + self, + blockchain_node_rpc_url, + contract_address, + key, + ionion_node_rpc_url, + file_to_upload, + ): + assert os.path.exists(self.cli_binary), "%s should be exist" % self.cli_binary + upload_args = [ + self.cli_binary, + "upload", + "--url", + blockchain_node_rpc_url, + "--contract", + contract_address, + "--key", + encode_hex(key), + "--node", + ionion_node_rpc_url, + "--log-level", + "debug", + "--gas-limit", + "10000000", + "--file", + ] + + proc = subprocess.Popen( + upload_args + [file_to_upload.name], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + proc.wait() + + root = None + lines = proc.stdout.readlines() + for line in lines: + self.log.debug("line: %s", line) + if "root" in line: + index = line.find("root=") + root = line[index + 5 : -1] + self.log.info("root: %s", root) + + assert proc.returncode == 0, "%s upload file failed, log: %s" % (self.cli_binary, lines) + self.log.info("file uploaded") + + return root + + def setup_params(self): + self.num_blockchain_nodes = 1 + self.num_nodes = 1 + + def setup_nodes(self): + self.__setup_blockchain_node() + self.__setup_zgs_node() + + def stop_nodes(self): + # stop ionion nodes first + for node in self.nodes: + node.stop() + + for node in self.blockchain_nodes: + node.stop() + + def stop_storage_node(self, index, clean=False): + self.nodes[index].stop() + if clean: + self.nodes[index].clean_data() + + + def start_storage_node(self, index): + self.nodes[index].start() + + def run_test(self): + raise NotImplementedError + + def main(self): + self.__parse_arguments() + PortMin.n = self.options.port_min + + # Set up temp directory and start logging + if self.options.tmpdir: + self.options.tmpdir = os.path.abspath(self.options.tmpdir) + os.makedirs(self.options.tmpdir, exist_ok=True) + else: + self.options.tmpdir = os.getenv( + "ZGS_TESTS_LOG_DIR", default=tempfile.mkdtemp(prefix="zgs_test_") + ) + + self.root_dir = self.options.tmpdir + + self.__start_logging() + self.log.info("Root dir: %s", self.root_dir) + + if self.blockchain_node_type == BlockChainNodeType.Conflux: + self.blockchain_binary = self.options.conflux + else: + self.blockchain_binary = self.options.bsc + + self.zgs_binary = self.options.zerog_storage + self.cli_binary = self.options.cli + self.contract_path = self.options.contract + self.token_contract_path = self.options.token_contract + self.mine_contract_path = self.options.mine_contract + + assert os.path.exists(self.contract_path), ( + "%s should be exist" % self.contract_path + ) + assert os.path.exists(self.token_contract_path), ( + "%s should be exist" % self.token_contract_path + ) + assert os.path.exists(self.mine_contract_path), ( + "%s should be exist" % self.mine_contract_path + ) + + if self.options.random_seed is not None: + random.seed(self.options.random_seed) + + success = TestStatus.FAILED + try: + self.setup_params() + self.setup_nodes() + self.run_test() + success = TestStatus.PASSED + except AssertionError as e: + self.log.exception("Assertion failed %s", repr(e)) + except KeyboardInterrupt as e: + self.log.warning("Exiting after keyboard interrupt %s", repr(e)) + except Exception as e: + self.log.error("Test exception %s %s", repr(e), traceback.format_exc()) + self.log.error(f"Test data are not deleted: {self.root_dir}") + + if success == TestStatus.FAILED and self.options.pdbonfailure: + print("Testcase failed. Attaching python debugger. Enter ? for help") + pdb.set_trace() + + if success == TestStatus.PASSED: + self.log.info("Tests successful") + exit_code = TEST_EXIT_PASSED + else: + self.log.error( + "Test failed. Test logging available at %s/test_framework.log", + self.options.tmpdir, + ) + exit_code = TEST_EXIT_FAILED + + self.stop_nodes() + + if success == TestStatus.PASSED: + self.log.info("Test success") + shutil.rmtree(self.root_dir) + + handlers = self.log.handlers[:] + for handler in handlers: + self.log.removeHandler(handler) + handler.close() + logging.shutdown() + + sys.exit(exit_code) diff --git a/tests/test_framework/zgs_node.py b/tests/test_framework/zgs_node.py new file mode 100644 index 0000000..a8307a7 --- /dev/null +++ b/tests/test_framework/zgs_node.py @@ -0,0 +1,110 @@ +import os +import shutil +import sys + +from config.node_config import ZGS_CONFIG +from test_framework.blockchain_node import NodeType, TestNode +from config.node_config import MINER_ID +from utility.utils import ( + initialize_toml_config, + p2p_port, + rpc_port, + blockchain_rpc_port, +) + + +class ZgsNode(TestNode): + def __init__( + self, + index, + root_dir, + binary, + updated_config, + log_contract_address, + mine_contract_address, + log, + rpc_timeout=10, + libp2p_nodes=None, + ): + local_conf = ZGS_CONFIG.copy() + if libp2p_nodes is None: + if index == 0: + libp2p_nodes = [] + else: + libp2p_nodes = [] + for i in range(index): + libp2p_nodes.append(f"/ip4/127.0.0.1/tcp/{p2p_port(i)}") + + indexed_config = { + "network_libp2p_port": p2p_port(index), + "network_discovery_port": p2p_port(index), + "rpc_listen_address": f"127.0.0.1:{rpc_port(index)}", + "network_libp2p_nodes": libp2p_nodes, + "log_contract_address": log_contract_address, + "mine_contract_address": mine_contract_address, + "blockchain_rpc_endpoint": f"http://127.0.0.1:{blockchain_rpc_port(0)}", + } + # Set configs for this specific node. + local_conf.update(indexed_config) + # Overwrite with personalized configs. + local_conf.update(updated_config) + data_dir = os.path.join(root_dir, "zgs_node" + str(index)) + rpc_url = "http://" + local_conf["rpc_listen_address"] + super().__init__( + NodeType.Zgs, + index, + data_dir, + rpc_url, + binary, + local_conf, + log, + rpc_timeout, + ) + + def setup_config(self): + os.mkdir(self.data_dir) + log_config_path = os.path.join(self.data_dir, self.config["log_config_file"]) + with open(log_config_path, "w") as f: + f.write("debug") + + initialize_toml_config(self.config_file, self.config) + + def wait_for_rpc_connection(self): + self._wait_for_rpc_connection(lambda rpc: rpc.zgs_getStatus() is not None) + + def start(self): + self.log.info("Start zerog_storage node %d", self.index) + super().start() + + # rpc + def zgs_get_status(self): + return self.rpc.zgs_getStatus()["connectedPeers"] + + def zgs_upload_segment(self, segment): + return self.rpc.zgs_uploadSegment([segment]) + + def zgs_download_segment(self, data_root, start_index, end_index): + return self.rpc.zgs_downloadSegment([data_root, start_index, end_index]) + + def zgs_get_file_info(self, data_root): + return self.rpc.zgs_getFileInfo([data_root]) + + def zgs_get_file_info_by_tx_seq(self, tx_seq): + return self.rpc.zgs_getFileInfoByTxSeq([tx_seq]) + + def shutdown(self): + self.rpc.admin_shutdown() + self.wait_until_stopped() + + def admin_start_sync_file(self, tx_seq): + return self.rpc.admin_startSyncFile([tx_seq]) + + def admin_get_sync_status(self, tx_seq): + return self.rpc.admin_getSyncStatus([tx_seq]) + + def sycn_status_is_completed_or_unknown(self, tx_seq): + status = self.rpc.admin_getSyncStatus([tx_seq]) + return status == "Completed" or status == "unknown" + + def clean_data(self): + shutil.rmtree(os.path.join(self.data_dir, "db")) diff --git a/tests/utility/merkle_tree.py b/tests/utility/merkle_tree.py new file mode 100644 index 0000000..3906aac --- /dev/null +++ b/tests/utility/merkle_tree.py @@ -0,0 +1,238 @@ +import sha3 + +from math import log2 + + +def decompose(num): + powers = [] + while num > 0: + power = int(log2(num)) + powers += [power] + num -= 1 << power + return powers + + +def add_0x_prefix(val): + return "0x" + val + + +class Hasher: + def __init__(self, algorithm="keccak_256", encoding="utf-8", security=False): + self.algorithm = algorithm + self.security = security + self.encoding = encoding + + if security: + self.prefix00 = "\x00".encode(encoding) + self.prefix01 = "\x01".encode(encoding) + else: + self.prefix00 = bytes() + self.prefix01 = bytes() + + def _hasher(self): + if self.algorithm == "keccak_256": + return sha3.keccak_256() + else: + raise NotImplementedError + + def hash_data(self, data): + buff = self.prefix00 + ( + data if isinstance(data, bytes) else data.encode(self.encoding) + ) + + hasher = self._hasher() + hasher.update(buff) + return hasher.hexdigest().encode(self.encoding) + + def hash_pair(self, left, right): + buff = ( + self.prefix01 + + bytes.fromhex(left.decode("utf-8")) + + bytes.fromhex(right.decode("utf-8")) + ) + + hasher = self._hasher() + hasher.update(buff) + return hasher.hexdigest().encode(self.encoding) + + +class Node: + + __slots__ = ("__value", "__parent", "__left", "__right") + + def __init__(self, value, parent=None, left=None, right=None): + self.__value = value + self.__parent = parent + self.__left = left + self.__right = right + + if left: + left.__parent = self + if right: + right.__parent = self + + @property + def value(self): + return self.__value + + @property + def left(self): + return self.__left + + @property + def right(self): + return self.__right + + @property + def parent(self): + return self.__parent + + def set_left(self, node): + self.__left = node + + def set_right(self, node): + self.__right = node + + def set_parent(self, node): + self.__parent = node + + def is_left_child(self): + parent = self.__parent + if not parent: + return False + + return self == parent.left + + def is_right_child(self): + parent = self.__parent + if not parent: + return False + + return self == parent.right + + def is_leaf(self): + return isinstance(self, Leaf) + + @classmethod + def from_children(cls, left, right, hasher): + digest = hasher.hash_pair(left.__value, right.__value) + return cls(value=digest, left=left, right=right, parent=None) + + def ancestor(self, degree): + if degree == 0: + return self + + if not self.__parent: + return + + return self.__parent.ancestor(degree - 1) + + def recalculate_hash(self, hasher): + self.__value = hasher.hash_pair(self.left.value, self.right.value) + + +class Leaf(Node): + def __init__(self, value, leaf=None): + super().__init__(value) + + @classmethod + def from_data(cls, data, hasher): + return cls(hasher.hash_data(data), leaf=None) + + +class MerkleTree: + def __init__(self, encoding="utf-8"): + self.__root = None + self.__leaves = [] + self.encoding = encoding + self.hasher = Hasher(encoding=encoding) + + def __bool__(self): + return len(self.__leaves) != 0 + + def encrypt(self, data): + leaf = Leaf.from_data(data, self.hasher) + self.add_leaf(leaf) + + def add_leaf(self, leaf): + if self: + subroot = self.get_last_subroot() + self._append_leaf(leaf) + + if not subroot.parent: + # Increase height by one + self.__root = Node.from_children(subroot, leaf, self.hasher) + else: + parent = subroot.parent + + # Create bifurcation node + new_node = Node.from_children(subroot, leaf, self.hasher) + + # Interject bifurcation node + parent.set_right(new_node) + new_node.set_parent(parent) + + # Recalculate hashes only at the rightmost branch of the tree + curr = parent + while curr: + curr.recalculate_hash(self.hasher) + curr = curr.parent + else: + self._append_leaf(leaf) + self.__root = leaf + + def get_last_subroot(self): + if not self.__leaves: + raise ValueError + + last_power = decompose(len(self.__leaves))[-1] + return self.get_tail().ancestor(degree=last_power) + + def get_tail(self): + return self.__leaves[-1] + + def _append_leaf(self, leaf): + self.__leaves.append(leaf) + + def get_root_hash(self): + if not self.__root: + return + + return self.__root.value + + def decode_value(self, val): + return val.decode(self.encoding) + + def proof_at(self, i): + if i < 0 or i >= len(self.__leaves): + raise IndexError + + if len(self.__leaves) == 1: + return { + "lemma": [add_0x_prefix(self.decode_value(self.get_root_hash()))], + "path": [], + } + + proof = {"lemma": [], "path": []} + proof["lemma"].append(add_0x_prefix(self.decode_value(self.__leaves[i].value))) + + current = self.__leaves[i] + while current != self.__root: + if current.parent != None and current.parent.left == current: + # add right + proof["lemma"].append( + add_0x_prefix(self.decode_value(current.parent.right.value)) + ) + proof["path"].append(True) + else: + # add left + proof["lemma"].append( + add_0x_prefix(self.decode_value(current.parent.left.value)) + ) + proof["path"].append(False) + + current = current.parent + + # add root + proof["lemma"].append(add_0x_prefix(self.decode_value(self.get_root_hash()))) + return proof diff --git a/tests/utility/signature_utils.py b/tests/utility/signature_utils.py new file mode 100644 index 0000000..84d21d7 --- /dev/null +++ b/tests/utility/signature_utils.py @@ -0,0 +1,615 @@ +import coincurve +import random +import rlp +import sha3 as _sha3 + +from eth_utils import decode_hex, int_to_big_endian, big_endian_to_int +from eth_utils import encode_hex as encode_hex_0x +from py_ecc.secp256k1 import privtopub, ecdsa_raw_sign, ecdsa_raw_recover +from rlp.sedes import big_endian_int, BigEndianInt, Binary +from rlp.utils import ALL_BYTES + + +def sha3_256(x): + return _sha3.keccak_256(x).digest() + + +class Memoize: + def __init__(self, fn): + self.fn = fn + self.memo = {} + + def __call__(self, *args): + if args not in self.memo: + self.memo[args] = self.fn(*args) + return self.memo[args] + + +TT256 = 2**256 +TT256M1 = 2**256 - 1 +TT255 = 2**255 +SECP256K1P = 2**256 - 4294968273 + + +def is_numeric(x): + return isinstance(x, int) + + +def is_string(x): + return isinstance(x, bytes) + + +def to_string(value): + if isinstance(value, bytes): + return value + if isinstance(value, str): + return bytes(value, "utf-8") + if isinstance(value, int): + return bytes(str(value), "utf-8") + + +def int_to_bytes(value): + if isinstance(value, bytes): + return value + return int_to_big_endian(value) + + +def to_string_for_regexp(value): + return str(to_string(value), "utf-8") + + +unicode = str + + +def bytearray_to_bytestr(value): + return bytes(value) + + +def encode_int32(v): + return v.to_bytes(32, byteorder="big") + + +def bytes_to_int(value): + return int.from_bytes(value, byteorder="big") + + +def str_to_bytes(value): + if isinstance(value, bytearray): + value = bytes(value) + if isinstance(value, bytes): + return value + return bytes(value, "utf-8") + + +def ascii_chr(n): + return ALL_BYTES[n] + + +def encode_hex(n): + if isinstance(n, str): + return encode_hex(n.encode("ascii")) + return encode_hex_0x(n)[2:] + + +def ecrecover_to_pub(rawhash, v, r, s): + if coincurve and hasattr(coincurve, "PublicKey"): + try: + pk = coincurve.PublicKey.from_signature_and_message( + zpad(bytearray_to_bytestr(int_to_32bytearray(r)), 32) + + zpad(bytearray_to_bytestr(int_to_32bytearray(s)), 32) + + ascii_chr(v - 27), + rawhash, + hasher=None, + ) + pub = pk.format(compressed=False)[1:] + x, y = pk.point() + except BaseException: + x, y = 0, 0 + pub = b"\x00" * 64 + else: + result = ecdsa_raw_recover(rawhash, (v, r, s)) + if result: + x, y = result + pub = encode_int32(x) + encode_int32(y) + else: + raise ValueError("Invalid VRS") + assert len(pub) == 64 + return pub, x, y + + +def ecsign(rawhash, key): + if coincurve and hasattr(coincurve, "PrivateKey"): + pk = coincurve.PrivateKey(key) + signature = pk.sign_recoverable(rawhash, hasher=None) + v = safe_ord(signature[64]) + 27 + r = big_endian_to_int(signature[0:32]) + s = big_endian_to_int(signature[32:64]) + else: + v, r, s = ecdsa_raw_sign(rawhash, key) + return v, r, s + + +def ec_random_keys(): + priv_key = random.randint(0, 2**256).to_bytes(32, "big") + pub_key = privtopub(priv_key) + return priv_key, pub_key + + +def convert_to_nodeid(signature, challenge): + r = big_endian_to_int(signature[:32]) + s = big_endian_to_int(signature[32:64]) + v = big_endian_to_int(signature[64:]) + 27 + signed = int_to_bytes(challenge) + h_signed = sha3_256(signed) + return ecrecover_to_pub(h_signed, v, r, s) + + +def get_nodeid(node): + challenge = random.randint(0, 2**32 - 1) + signature = node.getnodeid(list(int_to_bytes(challenge))) + return convert_to_nodeid(signature, challenge) + + +def mk_contract_address(sender, nonce): + return sha3(rlp.encode([normalize_address(sender), nonce]))[12:] + + +def mk_metropolis_contract_address(sender, initcode): + return sha3(normalize_address(sender) + initcode)[12:] + + +def safe_ord(value): + if isinstance(value, int): + return value + else: + return ord(value) + + +# decorator + + +def debug(label): + def deb(f): + def inner(*args, **kwargs): + i = random.randrange(1000000) + print(label, i, "start", args) + x = f(*args, **kwargs) + print(label, i, "end", x) + return x + + return inner + + return deb + + +def flatten(li): + o = [] + for l in li: + o.extend(l) + return o + + +def bytearray_to_int(arr): + o = 0 + for a in arr: + o = (o << 8) + a + return o + + +def int_to_32bytearray(i): + o = [0] * 32 + for x in range(32): + o[31 - x] = i & 0xFF + i >>= 8 + return o + + +# sha3_count = [0] + + +def sha3(seed): + return sha3_256(to_string(seed)) + + +assert ( + encode_hex(sha3(b"")) + == "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" +) +assert ( + encode_hex(sha3(b"\x00" * 256)) + == "d397b3b043d87fcd6fad1291ff0bfd16401c274896d8c63a923727f077b8e0b5" +) + + +@Memoize +def priv_to_addr(k): + k = normalize_key(k) + x, y = privtopub(k) + addr = bytearray(sha3(encode_int32(x) + encode_int32(y))[12:]) + addr[0] &= 0x0F + addr[0] |= 0x10 + return bytes(addr) + + +def priv_to_pub(k): + k = normalize_key(k) + x, y = privtopub(k) + return bytes(encode_int32(x) + encode_int32(y)) + + +def pub_to_addr(k): + x = big_endian_to_int(decode_hex(k[2:34])) + y = big_endian_to_int(decode_hex(k[34:66])) + addr = sha3(encode_int32(x) + encode_int32(y))[12:] + addr[0] &= 0x0F + addr[0] |= 0x10 + return bytes(addr) + + +def checksum_encode(addr): # Takes a 20-byte binary address as input + addr = normalize_address(addr) + o = "" + v = big_endian_to_int(sha3(encode_hex(addr))) + for i, c in enumerate(encode_hex(addr)): + if c in "0123456789": + o += c + else: + o += c.upper() if (v & (2 ** (255 - 4 * i))) else c.lower() + return "0x" + o + + +def check_checksum(addr): + return checksum_encode(normalize_address(addr)) == addr + + +def normalize_address(x, allow_blank=False): + if is_numeric(x): + return int_to_addr(x) + if allow_blank and x in {"", b""}: + return b"" + if len(x) in (42, 50) and x[:2] in {"0x", b"0x"}: + x = x[2:] + if len(x) in (40, 48): + x = decode_hex(x) + if len(x) == 24: + assert len(x) == 24 and sha3(x[:20])[:4] == x[-4:] + x = x[:20] + if len(x) != 20: + raise Exception("Invalid address format: %r" % x) + return x + + +def normalize_key(key): + if is_numeric(key): + o = encode_int32(key) + elif len(key) == 32: + o = key + elif len(key) == 64: + o = decode_hex(key) + elif len(key) == 66 and key[:2] == "0x": + o = decode_hex(key[2:]) + else: + raise Exception("Invalid key format: %r" % key) + if o == b"\x00" * 32: + raise Exception("Zero privkey invalid") + return o + + +def zpad(x, l): + """Left zero pad value `x` at least to length `l`. + + >>> zpad('', 1) + '\x00' + >>> zpad('\xca\xfe', 4) + '\x00\x00\xca\xfe' + >>> zpad('\xff', 1) + '\xff' + >>> zpad('\xca\xfe', 2) + '\xca\xfe' + """ + return b"\x00" * max(0, l - len(x)) + x + + +def rzpad(value, total_length): + """Right zero pad value `x` at least to length `l`. + + >>> zpad('', 1) + '\x00' + >>> zpad('\xca\xfe', 4) + '\xca\xfe\x00\x00' + >>> zpad('\xff', 1) + '\xff' + >>> zpad('\xca\xfe', 2) + '\xca\xfe' + """ + return value + b"\x00" * max(0, total_length - len(value)) + + +def int_to_addr(x): + o = [b""] * 20 + for i in range(20): + o[19 - i] = ascii_chr(x & 0xFF) + x >>= 8 + return b"".join(o) + + +def coerce_addr_to_bin(x): + if is_numeric(x): + return encode_hex(zpad(big_endian_int.serialize(x), 20)) + elif len(x) == 40 or len(x) == 0: + return decode_hex(x) + else: + return zpad(x, 20)[-20:] + + +def coerce_addr_to_hex(x): + if is_numeric(x): + return encode_hex(zpad(big_endian_int.serialize(x), 20)) + elif len(x) == 40 or len(x) == 0: + return x + else: + return encode_hex(zpad(x, 20)[-20:]) + + +def coerce_to_int(x): + if is_numeric(x): + return x + elif len(x) == 40: + return big_endian_to_int(decode_hex(x)) + else: + return big_endian_to_int(x) + + +def coerce_to_bytes(x): + if is_numeric(x): + return big_endian_int.serialize(x) + elif len(x) == 40: + return decode_hex(x) + else: + return x + + +def parse_int_or_hex(s): + if is_numeric(s): + return s + elif s[:2] in (b"0x", "0x"): + s = to_string(s) + tail = (b"0" if len(s) % 2 else b"") + s[2:] + return big_endian_to_int(decode_hex(tail)) + else: + return int(s) + + +def ceil32(x): + return x if x % 32 == 0 else x + 32 - (x % 32) + + +def to_signed(i): + return i if i < TT255 else i - TT256 + + +def sha3rlp(x): + return sha3(rlp.encode(x)) + + +# Format encoders/decoders for bin, addr, int + + +def decode_bin(v): + """decodes a bytearray from serialization""" + if not is_string(v): + raise Exception("Value must be binary, not RLP array") + return v + + +def decode_addr(v): + """decodes an address from serialization""" + if len(v) not in [0, 20]: + raise Exception("Serialized addresses must be empty or 20 bytes long!") + return encode_hex(v) + + +def decode_int(v): + """decodes and integer from serialization""" + if len(v) > 0 and (v[0] == b"\x00" or v[0] == 0): + raise Exception("No leading zero bytes allowed for integers") + return big_endian_to_int(v) + + +def decode_int256(v): + return big_endian_to_int(v) + + +def encode_bin(v): + """encodes a bytearray into serialization""" + return v + + +def encode_root(v): + """encodes a trie root into serialization""" + return v + + +def encode_int(v): + """encodes an integer into serialization""" + if not is_numeric(v) or v < 0 or v >= TT256: + raise Exception("Integer invalid or out of range: %r" % v) + return int_to_big_endian(v) + + +def encode_int256(v): + return zpad(int_to_big_endian(v), 256) + + +def scan_bin(v): + if v[:2] in ("0x", b"0x"): + return decode_hex(v[2:]) + else: + return decode_hex(v) + + +def scan_int(v): + if v[:2] in ("0x", b"0x"): + return big_endian_to_int(decode_hex(v[2:])) + else: + return int(v) + + +# Decoding from RLP serialization +decoders = { + "bin": decode_bin, + "addr": decode_addr, + "int": decode_int, + "int256b": decode_int256, +} + +# Encoding to RLP serialization +encoders = { + "bin": encode_bin, + "int": encode_int, + "trie_root": encode_root, + "int256b": encode_int256, +} + +# Encoding to printable format +printers = { + "bin": lambda v: "0x" + encode_hex(v), + "addr": lambda v: v, + "int": lambda v: to_string(v), + "trie_root": lambda v: encode_hex(v), + "int256b": lambda x: encode_hex(zpad(encode_int256(x), 256)), +} + +# Decoding from printable format +scanners = { + "bin": scan_bin, + "addr": lambda x: x[2:] if x[:2] in (b"0x", "0x") else x, + "int": scan_int, + "trie_root": lambda x: scan_bin, + "int256b": lambda x: big_endian_to_int(decode_hex(x)), +} + + +def int_to_hex(x): + o = encode_hex(encode_int(x)) + return "0x" + (o[1:] if (len(o) > 0 and o[0] == b"0") else o) + + +def remove_0x_head(s): + return s[2:] if s[:2] in (b"0x", "0x") else s + + +def parse_as_bin(s): + return decode_hex(s[2:] if s[:2] == "0x" else s) + + +def parse_as_int(s): + return s if is_numeric(s) else int("0" + s[2:], 16) if s[:2] == "0x" else int(s) + + +def print_func_call(ignore_first_arg=False, max_call_number=100): + """utility function to facilitate debug, it will print input args before + function call, and print return value after function call + + usage: + + @print_func_call + def some_func_to_be_debu(): + pass + + :param ignore_first_arg: whether print the first arg or not. + useful when ignore the `self` parameter of an object method call + """ + from functools import wraps + + def display(x): + x = to_string(x) + try: + x.decode("ascii") + except BaseException: + return "NON_PRINTABLE" + return x + + local = {"call_number": 0} + + def inner(f): + @wraps(f) + def wrapper(*args, **kwargs): + local["call_number"] += 1 + tmp_args = args[1:] if ignore_first_arg and len(args) else args + this_call_number = local["call_number"] + print( + ( + "{0}#{1} args: {2}, {3}".format( + f.__name__, + this_call_number, + ", ".join([display(x) for x in tmp_args]), + ", ".join( + display(key) + "=" + to_string(value) + for key, value in kwargs.items() + ), + ) + ) + ) + res = f(*args, **kwargs) + print( + ( + "{0}#{1} return: {2}".format( + f.__name__, this_call_number, display(res) + ) + ) + ) + + if local["call_number"] > 100: + raise Exception("Touch max call number!") + return res + + return wrapper + + return inner + + +def dump_state(trie): + res = "" + for k, v in list(trie.to_dict().items()): + res += "%r:%r\n" % (encode_hex(k), encode_hex(v)) + return res + + +class Denoms: + def __init__(self): + self.wei = 1 + self.babbage = 10**3 + self.ada = 10**3 + self.kwei = 10**3 + self.lovelace = 10**6 + self.mwei = 10**6 + self.shannon = 10**9 + self.gwei = 10**9 + self.szabo = 10**12 + self.finney = 10**15 + self.mether = 10**15 + self.ether = 10**18 + self.turing = 2**256 - 1 + + +denoms = Denoms() + +address = Binary.fixed_length(20, allow_empty=True) +int20 = BigEndianInt(20) +int32 = BigEndianInt(32) +int256 = BigEndianInt(256) +hash32 = Binary.fixed_length(32) +hash20 = Binary.fixed_length(20) +trie_root = Binary.fixed_length(32, allow_empty=True) + + +class bcolors: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKGREEN = "\033[92m" + WARNING = "\033[91m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" diff --git a/tests/utility/simple_rpc_proxy.py b/tests/utility/simple_rpc_proxy.py new file mode 100644 index 0000000..26db293 --- /dev/null +++ b/tests/utility/simple_rpc_proxy.py @@ -0,0 +1,31 @@ +from jsonrpcclient import request, parse, Ok +import requests + + +class SimpleRpcProxy: + def __init__(self, url, timeout=60): + self.url = url + self.timeout = timeout + + def __getattr__(self, name): + return RpcCaller(self.url, name, self.timeout) + + +class RpcCaller: + def __init__(self, url, method, timeout): + self.url = url + self.method = method + self.timeout = timeout + + def __call__(self, *args, **argsn): + r = request(self.method, *args) + try: + response = requests.post(self.url, json=r, timeout=self.timeout) + parsed = parse(response.json()) + if isinstance(parsed, Ok): + return parsed.result + else: + print(parsed) + except Exception as ex: + print(ex) + return None diff --git a/tests/utility/submission.py b/tests/utility/submission.py new file mode 100644 index 0000000..bcb363b --- /dev/null +++ b/tests/utility/submission.py @@ -0,0 +1,228 @@ +import base64 + +from eth_utils import encode_hex, decode_hex +from math import log2 +from utility.merkle_tree import add_0x_prefix, Leaf, MerkleTree + + +ENTRY_SIZE = 256 +PORA_CHUNK_SIZE = 1024 + + +def log2_pow2(n): + return int(log2(((n ^ (n - 1)) >> 1) + 1)) + + +def next_pow2(input): + x = input + x -= 1 + x |= x >> 16 + x |= x >> 8 + x |= x >> 4 + x |= x >> 2 + x |= x >> 1 + x += 1 + + return x + + +def bytes_to_entries(size_bytes): + if size_bytes % ENTRY_SIZE == 0: + return size_bytes // ENTRY_SIZE + else: + return size_bytes // ENTRY_SIZE + 1 + + +def create_submission(data): + submission = [] + submission.append(len(data)) + submission.append(b"") + submission.append([]) + + offset = 0 + nodes = [] + for chunks in split_nodes(len(data)): + node_hash = create_node(data, offset, chunks) + nodes.append(node_hash) + + height = int(log2(chunks)) + submission[2].append([decode_hex(node_hash.decode("utf-8")), height]) + offset += chunks * ENTRY_SIZE + + root_hash = nodes[-1] + for i in range(len(nodes) - 2, -1, -1): + tree = MerkleTree() + tree.add_leaf(Leaf(nodes[i])) + tree.add_leaf(Leaf(root_hash)) + root_hash = tree.get_root_hash() + + return submission, add_0x_prefix(root_hash.decode("utf-8")) + + +def split_nodes(data_len): + nodes = [] + + chunks = bytes_to_entries(data_len) + padded_chunks, chunks_next_pow2 = compute_padded_size(chunks) + next_chunk_size = chunks_next_pow2 + + while padded_chunks > 0: + if padded_chunks >= next_chunk_size: + padded_chunks -= next_chunk_size + nodes.append(next_chunk_size) + + next_chunk_size >>= 1 + + return nodes + + +def compute_padded_size(chunk_len): + chunks_next_pow2 = next_pow2(chunk_len) + + if chunks_next_pow2 == chunk_len: + return chunks_next_pow2, chunks_next_pow2 + + min_chunk = 1 if chunks_next_pow2 < 16 else chunks_next_pow2 // 16 + padded_chunks = ((chunk_len - 1) // min_chunk + 1) * min_chunk + + return padded_chunks, chunks_next_pow2 + + +def create_node(data, offset, chunks): + batch = chunks + if chunks > PORA_CHUNK_SIZE: + batch = PORA_CHUNK_SIZE + + return create_segment_node(data, offset, ENTRY_SIZE * batch, ENTRY_SIZE * chunks) + + +def create_segment_node(data, offset, batch, size): + tree = MerkleTree() + i = offset + n = len(data) + while i < offset + size: + start = i + end = min(offset + size, i + batch) + + if start >= n: + tree.add_leaf(Leaf(segment_root(b"\x00" * (end - start)))) + elif end > n: + tree.add_leaf(Leaf(segment_root(data[start:] + b"\x00" * (end - n)))) + else: + tree.add_leaf(Leaf(segment_root(data[start:end]))) + + i += batch + + return tree.get_root_hash() + + +def segment_root(chunks): + data_len = len(chunks) + if data_len == 0: + return b"\x00" * 32 + + tree = MerkleTree() + for i in range(0, data_len, ENTRY_SIZE): + tree.encrypt(chunks[i : i + ENTRY_SIZE]) + + return tree.get_root_hash() + + +def generate_merkle_tree(data): + chunks = bytes_to_entries(len(data)) + padded_chunks, _ = compute_padded_size(chunks) + + tree = MerkleTree() + for i in range(padded_chunks): + if i * ENTRY_SIZE > len(data): + tree.encrypt(b"\x00" * ENTRY_SIZE) + elif (i + 1) * ENTRY_SIZE > len(data): + tree.encrypt( + data[i * ENTRY_SIZE :] + b"\x00" * ((i + 1) * ENTRY_SIZE - len(data)) + ) + else: + tree.encrypt(data[i * ENTRY_SIZE : (i + 1) * ENTRY_SIZE]) + + return tree + + +def generate_merkle_tree_by_batch(data): + chunks = bytes_to_entries(len(data)) + padded_chunks, _ = compute_padded_size(chunks) + + tree = MerkleTree() + for i in range(0, padded_chunks, PORA_CHUNK_SIZE): + if i * ENTRY_SIZE >= len(data): + tree.add_leaf( + Leaf( + segment_root( + b"\x00" * ENTRY_SIZE * min(PORA_CHUNK_SIZE, padded_chunks - i) + ) + ) + ) + elif (i + PORA_CHUNK_SIZE) * ENTRY_SIZE > len(data): + tree.add_leaf( + Leaf( + segment_root( + data[i * ENTRY_SIZE :] + + b"\x00" + * ( + min(padded_chunks, i + PORA_CHUNK_SIZE) * ENTRY_SIZE + - len(data) + ) + ) + ) + ) + else: + tree.add_leaf( + Leaf( + segment_root( + data[i * ENTRY_SIZE : (i + PORA_CHUNK_SIZE) * ENTRY_SIZE] + ) + ) + ) + + return tree, add_0x_prefix(tree.decode_value(tree.get_root_hash())) + + +def submit_data(client, data): + segments = data_to_segments(data) + for segment in segments: + client.zgs_upload_segment(segment) + return segments + + +def data_to_segments(data): + tree, root_hash = generate_merkle_tree_by_batch(data) + chunks = bytes_to_entries(len(data)) + + segments = [] + idx = 0 + while idx * PORA_CHUNK_SIZE < chunks: + proof = tree.proof_at(idx) + + tmp = ( + data[ + idx + * ENTRY_SIZE + * PORA_CHUNK_SIZE : (idx + 1) + * ENTRY_SIZE + * PORA_CHUNK_SIZE + ] + if len(data) >= (idx + 1) * PORA_CHUNK_SIZE * ENTRY_SIZE + else data[idx * ENTRY_SIZE * PORA_CHUNK_SIZE :] + + b"\x00" * (chunks * ENTRY_SIZE - len(data)) + ) + + segment = { + "root": root_hash, + "data": base64.b64encode(tmp).decode("utf-8"), + "index": idx, + "proof": proof, + "fileSize": len(data), + } + + segments.append(segment) + idx += 1 + + return segments \ No newline at end of file diff --git a/tests/utility/utils.py b/tests/utility/utils.py new file mode 100644 index 0000000..95d3084 --- /dev/null +++ b/tests/utility/utils.py @@ -0,0 +1,130 @@ +import base64 +import inspect +import os +import platform +import rtoml +import time + +from config.node_config import ZGS_CONFIG +from eth_utils import encode_hex + + +class PortMin: + # Must be initialized with a unique integer for each process + n = 11000 + + +MAX_NODES = 100 + + +def p2p_port(n): + assert n <= MAX_NODES + return PortMin.n + n + + +def rpc_port(n): + return PortMin.n + MAX_NODES + n + + +def blockchain_p2p_port(n): + return PortMin.n + 2 * MAX_NODES + n + + +def blockchain_rpc_port(n): + return PortMin.n + 3 * MAX_NODES + n + + +def blockchain_rpc_port_core(n): + return PortMin.n + 4 * MAX_NODES + n + + +def wait_until(predicate, *, attempts=float("inf"), timeout=float("inf"), lock=None): + if attempts == float("inf") and timeout == float("inf"): + timeout = 60 + attempt = 0 + time_end = time.time() + timeout + + while attempt < attempts and time.time() < time_end: + if lock: + with lock: + if predicate(): + return + else: + if predicate(): + return + attempt += 1 + time.sleep(0.5) + + # Print the cause of the timeout + predicate_source = inspect.getsourcelines(predicate) + if attempt >= attempts: + raise AssertionError( + "Predicate {} not true after {} attempts".format(predicate_source, attempts) + ) + elif time.time() >= time_end: + raise AssertionError( + "Predicate {} not true after {} seconds".format(predicate_source, timeout) + ) + raise RuntimeError("Unreachable") + + +def is_windows_platform(): + return platform.system().lower() == "windows" + + +def initialize_config(config_path, config_parameters): + with open(config_path, "w") as f: + for k in config_parameters: + value = config_parameters[k] + if isinstance(value, str) and not ( + value.startswith('"') or value.startswith("'") + ): + if value == "true" or value == "false": + value = f"{value}" + else: + value = f'"{value}"' + + f.write(f"{k}={value}\n") + + +def initialize_toml_config(config_path, config_parameters): + with open(config_path, "w") as f: + rtoml.dump(config_parameters, f) + + +def create_proof_and_segment(chunk_data, data_root, index=0): + proof = { + "lemma": [data_root], + "path": [], + } + + segment = { + "root": data_root, + "data": base64.b64encode(chunk_data).decode("utf-8"), + "index": index, + "proof": proof, + } + + return proof, segment + + +def assert_equal(thing1, thing2, *args): + if thing1 != thing2 or any(thing1 != arg for arg in args): + raise AssertionError( + "not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args) + ) + + +def assert_ne(thing1, thing2): + if thing1 == thing2: + raise AssertionError("not(%s)" % " != ".join([thing1, thing2])) + + +def assert_greater_than(thing1, thing2): + if thing1 <= thing2: + raise AssertionError("%s <= %s" % (str(thing1), str(thing2))) + + +def assert_greater_than_or_equal(thing1, thing2): + if thing1 < thing2: + raise AssertionError("%s < %s" % (str(thing1), str(thing2))) diff --git a/version-meld/discv5/.cargo-ok b/version-meld/discv5/.cargo-ok new file mode 100644 index 0000000..b5754e2 --- /dev/null +++ b/version-meld/discv5/.cargo-ok @@ -0,0 +1 @@ +ok \ No newline at end of file diff --git a/version-meld/discv5/.cargo_vcs_info.json b/version-meld/discv5/.cargo_vcs_info.json new file mode 100644 index 0000000..5ce3272 --- /dev/null +++ b/version-meld/discv5/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "00b4b1b1987d443b8f3f25647a0cfe9c5d5f29db" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/version-meld/discv5/Cargo.lock b/version-meld/discv5/Cargo.lock new file mode 100644 index 0000000..9d63d70 --- /dev/null +++ b/version-meld/discv5/Cargo.lock @@ -0,0 +1,2102 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "ctr", + "opaque-debug", +] + +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991984e3fd003e7ba02eb724f87a0f997b78677c46c0e91f8424ad7394c9886a" +dependencies = [ + "getrandom 0.2.3", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "asn1_der" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22d1f4b888c298a027c99dc9048015fac177587de20fc30232a057dfbe24a21" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bumpalo" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "colored" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] +name = "const-oid" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279bc8fc53f788a75c7804af68237d1fce02cde1e275a886a4b320604dc2aeda" + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +dependencies = [ + "generic-array", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] +name = "der" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eeb9d92785d1facb50567852ce75d0858630630e7eabea59cf7eb7474051087" +dependencies = [ + "const-oid", + "typenum", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +dependencies = [ + "block-buffer 0.10.0", + "crypto-common", + "generic-array", +] + +[[package]] +name = "discv5" +version = "0.1.0-beta.13" +dependencies = [ + "aes", + "aes-gcm", + "arrayvec", + "digest 0.10.1", + "enr", + "env_logger 0.9.0", + "fnv", + "futures", + "hashlink", + "hex", + "hex-literal", + "hkdf", + "lazy_static", + "libp2p-core", + "lru", + "parking_lot", + "quickcheck", + "rand 0.7.3", + "rand 0.8.4", + "rand_core 0.6.3", + "rand_xorshift", + "rlp", + "sha2", + "simple_logger", + "smallvec", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "tracing-subscriber", + "uint", + "zeroize", +] + +[[package]] +name = "ecdsa" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34d33b390ab82f2e1481e331dbd0530895640179d2128ef9a79cc690b78d1eba" +dependencies = [ + "der", + "elliptic-curve", + "hmac 0.11.0", + "signature", +] + +[[package]] +name = "ed25519" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4620d40f6d2601794401d6dd95a5cf69b6c157852539470eeda433a99b3c0efc" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2", + "zeroize", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "elliptic-curve" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13e9b0c3c4170dcc2a12783746c4205d98e18957f57854251eea3f9750fe005" +dependencies = [ + "bitvec", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.3", + "subtle", + "zeroize", +] + +[[package]] +name = "enr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809869a1328bfb586b48c9c0f87761c47c41793a85bcb06f66074a87cafc1bcd" +dependencies = [ + "base64", + "bs58", + "bytes", + "ed25519-dalek", + "hex", + "k256", + "log", + "rand 0.8.4", + "rlp", + "serde", + "sha3", + "zeroize", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "ff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a4d941a5b7c2a75222e2d44fcdf634a67133d9db31e177ae5ff6ecda852bfe" +dependencies = [ + "bitvec", + "rand_core 0.6.3", + "subtle", +] + +[[package]] +name = "fixedbitset" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398ea4fabe40b9b0d885340a2a991a44c8a645624075ad966d21f88688e2b69e" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "futures" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" + +[[package]] +name = "futures-executor" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" + +[[package]] +name = "futures-macro" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" + +[[package]] +name = "futures-task" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] +name = "futures-util" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "group" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3c1e8b4f1ca07e6605ea1be903a5f6956aec5c8a67fd44d56076631675ed8" +dependencies = [ + "ff", + "rand_core 0.6.3", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac 0.11.0", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest 0.9.0", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3e8e491ed22bc161583a1c77e42313672c483eba6bd9d7afec0f1131d0b9ce" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "keccak" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" + +[[package]] +name = "libp2p-core" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef22d9bba1e8bcb7ec300073e6802943fe8abb8190431842262b5f1c30abba1" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "either", + "fnv", + "futures", + "futures-timer", + "lazy_static", + "libsecp256k1", + "log", + "multiaddr", + "multihash", + "multistream-select", + "parking_lot", + "pin-project 1.0.8", + "prost", + "prost-build", + "rand 0.8.4", + "ring", + "rw-stream-sink", + "sha2", + "smallvec", + "thiserror", + "unsigned-varint", + "void", + "zeroize", +] + +[[package]] +name = "libsecp256k1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0452aac8bab02242429380e9b2f94ea20cea2b37e2c1777a1358799bbe97f37" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.4", + "serde", + "sha2", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lru" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469898e909a1774d844793b347135a0cd344ca2f69d082013ecb8061a2229a3a" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mio" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "multiaddr" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ee4ea82141951ac6379f964f71b20876d43712bea8faf6dd1a375e08a46499" +dependencies = [ + "arrayref", + "bs58", + "byteorder", + "data-encoding", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + +[[package]] +name = "multihash" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "752a61cd890ff691b4411423d23816d5866dd5621e4d1c5687a53b94b5a979d8" +dependencies = [ + "digest 0.9.0", + "generic-array", + "multihash-derive", + "sha2", + "unsigned-varint", +] + +[[package]] +name = "multihash-derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424f6e86263cd5294cbd7f1e95746b95aca0e0d66bff31e5a40d6baa87b4aa99" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "multistream-select" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d91ec0a2440aaff5f78ec35631a7027d50386c6163aa975f7caa0d5da4b6ff8" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project 1.0.8", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "petgraph" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918192b5c59119d51e0cd221f4d49dde9112824ba717369e903c97d076083d0f" +dependencies = [ + "pin-project-internal 0.4.28", +] + +[[package]] +name = "pin-project" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" +dependencies = [ + "pin-project-internal 1.0.8", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be26700300be6d9d23264c73211d8190e755b6b5ca7a1b28230025511b52a5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c2f795bc591cb3384cb64082a578b89207ac92bb89c9d98c1ea2ace7cd8110" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "prost" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" +dependencies = [ + "bytes", + "heck", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" +dependencies = [ + "bytes", + "prost", +] + +[[package]] +name = "quickcheck" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" +dependencies = [ + "env_logger 0.7.1", + "log", + "rand 0.7.3", + "rand_core 0.5.1", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rlp" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rw-stream-sink" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da5fcb054c46f5a5dff833b129285a93d3f0179531735e6c866e8cc307d2020" +dependencies = [ + "futures", + "pin-project 0.4.28", + "static_assertions", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335" +dependencies = [ + "digest 0.9.0", + "rand_core 0.6.3", +] + +[[package]] +name = "simple_logger" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45b60258a35dc3cb8a16890b8fd6723349bfa458d7960e25e633f1b1c19d7b5e" +dependencies = [ + "atty", + "colored", + "log", + "time", + "winapi", +] + +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dae7e047abc519c96350e9484a96c6bf1492348af912fd3446dd2dc323f6268" +dependencies = [ + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand 0.8.4", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" +dependencies = [ + "itoa", + "libc", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + +[[package]] +name = "tinyvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245da694cc7fc4729f3f418b304cb57789f1bed2a78c575407ab8a23f53cb4d3" +dependencies = [ + "ansi_term", + "lazy_static", + "matchers", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + +[[package]] +name = "uint" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "unsigned-varint" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f8d425fafb8cd76bc3f22aace4af471d3156301d7508f2107e98fbeae10bc7f" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +dependencies = [ + "either", + "lazy_static", + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "zeroize" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdff2024a851a322b08f179173ae2ba620445aef1e838f0c196820eade4ae0c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/version-meld/discv5/Cargo.toml b/version-meld/discv5/Cargo.toml new file mode 100644 index 0000000..40ea4b5 --- /dev/null +++ b/version-meld/discv5/Cargo.toml @@ -0,0 +1,139 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "discv5" +version = "0.1.0-beta.13" +authors = ["Age Manning "] +exclude = [".gitignore", ".github/*"] +description = "Implementation of the p2p discv5 discovery protocol" +readme = "./README.md" +keywords = ["peer-to-peer", "libp2p", "networking", "discovery", "discv5"] +categories = ["network-programming", "asynchronous"] +license = "MIT" +repository = "https://github.com/sigp/discv5" +[dependencies.aes] +version = "0.7.5" +features = ["ctr"] + +[dependencies.aes-gcm] +version = "0.9.4" + +[dependencies.arrayvec] +version = "0.7.2" + +[dependencies.digest] +version = "0.10.1" + +[dependencies.enr] +version = "0.6.2" +features = ["k256", "ed25519"] + +[dependencies.fnv] +version = "1.0.7" + +[dependencies.futures] +version = "0.3.19" + +[dependencies.hashlink] +version = "0.7.0" + +[dependencies.hex] +version = "0.4.3" + +[dependencies.hkdf] +version = "0.12" + +[dependencies.lazy_static] +version = "1.4.0" + +[dependencies.libp2p-core] +version = "0.30.0" +optional = true + +[dependencies.lru] +version = "0.7.1" + +[dependencies.parking_lot] +version = "0.11.2" + +[dependencies.rand] +version = "0.8.4" +package = "rand" + +[dependencies.rlp] +version = "0.5.1" + +[dependencies.sha2] +version = "0.10" + +[dependencies.smallvec] +version = "1.7.0" + +[dependencies.tokio] +version = "1.15.0" +features = ["net", "sync", "macros", "rt"] + +[dependencies.tokio-stream] +version = "0.1.8" + +[dependencies.tokio-util] +version = "0.6.9" +features = ["time"] + +[dependencies.tracing] +version = "0.1.29" +features = ["log"] + +[dependencies.tracing-subscriber] +version = "0.3.3" +features = ["env-filter"] + +[dependencies.uint] +version = "0.9.1" +default-features = false + +[dependencies.zeroize] +version = "1.4.3" +features = ["zeroize_derive"] +[dev-dependencies.env_logger] +version = "0.9.0" + +[dev-dependencies.hex-literal] +version = "0.3.4" + +[dev-dependencies.quickcheck] +version = "0.9.2" + +[dev-dependencies.rand_07] +version = "0.7" +package = "rand" + +[dev-dependencies.rand_core] +version = "0.6.3" + +[dev-dependencies.rand_xorshift] +version = "0.3.0" + +[dev-dependencies.simple_logger] +version = "1.16.0" + +[dev-dependencies.tokio] +version = "1.15.0" +features = ["full"] + +[dev-dependencies.tokio-util] +version = "0.6.9" +features = ["time"] + +[features] +libp2p = ["libp2p-core"] diff --git a/version-meld/discv5/Cargo.toml.orig b/version-meld/discv5/Cargo.toml.orig new file mode 100644 index 0000000..58e6a31 --- /dev/null +++ b/version-meld/discv5/Cargo.toml.orig @@ -0,0 +1,53 @@ +[package] +name = "discv5" +authors = ["Age Manning "] +edition = "2018" +version = "0.1.0-beta.13" +description = "Implementation of the p2p discv5 discovery protocol" +license = "MIT" +repository = "https://github.com/sigp/discv5" +readme = "./README.md" +keywords = ["peer-to-peer", "libp2p", "networking", "discovery", "discv5"] +categories = ["network-programming", "asynchronous"] +exclude = [".gitignore", ".github/*"] + +[dependencies] +enr = { version = "0.5.1", features = ["k256", "ed25519"] } +tokio = { version = "1.15.0", features = ["net", "sync", "macros", "rt"] } +tokio-stream = "0.1.8" +tokio-util = { version = "0.6.9", features = ["time"] } +libp2p-core = { version = "0.30.0", optional = true } +zeroize = { version = "1.4.3", features = ["zeroize_derive"] } +futures = "0.3.19" +uint = { version = "0.9.1", default-features = false } +rlp = "0.5.1" +sha2 = "0.9.5" +hkdf = "0.11.0" +hex = "0.4.3" +fnv = "1.0.7" +arrayvec = "0.7.2" +digest = "0.10.1" +rand = { version = "0.8.4", package = "rand" } +smallvec = "1.7.0" +parking_lot = "0.11.2" +lazy_static = "1.4.0" +aes = { version = "0.7.5", features = ["ctr"] } +aes-gcm = "0.9.4" +tracing = { version = "0.1.29", features = ["log"] } +tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } +lru = "0.7.1" +hashlink = "0.7.0" + +[dev-dependencies] +rand_07 = { package = "rand", version = "0.7" } +quickcheck = "0.9.2" +env_logger = "0.9.0" +hex-literal = "0.3.4" +simple_logger = "1.16.0" +tokio-util = { version = "0.6.9", features = ["time"] } +tokio = { version = "1.15.0", features = ["full"] } +rand_xorshift = "0.3.0" +rand_core = "0.6.3" + +[features] +libp2p = ["libp2p-core"] diff --git a/version-meld/discv5/LICENSE b/version-meld/discv5/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/version-meld/discv5/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/version-meld/discv5/README.md b/version-meld/discv5/README.md new file mode 100644 index 0000000..21934ee --- /dev/null +++ b/version-meld/discv5/README.md @@ -0,0 +1,75 @@ +discv5 +============ + +[![Build Status]][Build Link] [![Doc Status]][Doc Link] [![Crates +Status]][Crates Link] + +[Build Status]: https://github.com/sigp/discv5/workflows/build/badge.svg?branch=master +[Build Link]: https://github.com/sigp/discv5/actions +[Doc Status]: https://docs.rs/discv5/badge.svg +[Doc Link]: https://docs.rs/discv5 +[Crates Status]: https://img.shields.io/crates/v/discv5.svg +[Crates Link]: https://crates.io/crates/discv5 + +[Documentation at docs.rs](https://docs.rs/discv5) + + +# Overview + +This is a rust implementation of the [Discovery v5](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md) +peer discovery protocol. + +Discovery v5 is a protocol designed for encrypted peer discovery and topic advertisement. Each peer/node +on the network is identified via it's `ENR` ([Ethereum Node +Record](https://eips.ethereum.org/EIPS/eip-778)), which is essentially a signed key-value store +containing the node's public key and optionally IP address and port. + +Discv5 employs a kademlia-like routing table to store and manage discovered peers and topics. The +protocol allows for external IP discovery in NAT environments through regular PING/PONG's with +discovered nodes. Nodes return the external IP address that they have received and a simple +majority is chosen as our external IP address. If an external IP address is updated, this is +produced as an event to notify the swarm (if one is used for this behaviour). + +For a simple CLI discovery service see [discv5-cli](https://github.com/AgeManning/discv5-cli) + +# Usage + +A simple example of creating this service is as follows: + +```rust + use discv5::{enr, enr::{CombinedKey, NodeId}, TokioExecutor, Discv5, Discv5ConfigBuilder}; + use std::net::SocketAddr; + + // listening address and port + let listen_addr = "0.0.0.0:9000".parse::().unwrap(); + + // construct a local ENR + let enr_key = CombinedKey::generate_secp256k1(); + let enr = enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); + + // build the tokio executor + let mut runtime = tokio::runtime::Builder::new_multi_thread() + .thread_name("Discv5-example") + .enable_all() + .build() + .unwrap(); + + // default configuration + let config = Discv5ConfigBuilder::new().build(); + + // construct the discv5 server + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + + // In order to bootstrap the routing table an external ENR should be added + // This can be done via add_enr. I.e.: + // discv5.add_enr() + + // start the discv5 server + runtime.block_on(discv5.start(listen_addr)); + + // run a find_node query + runtime.block_on(async { + let found_nodes = discv5.find_node(NodeId::random()).await.unwrap(); + println!("Found nodes: {:?}", found_nodes); + }); +``` diff --git a/version-meld/discv5/examples/custom_executor.rs b/version-meld/discv5/examples/custom_executor.rs new file mode 100644 index 0000000..9d48c8b --- /dev/null +++ b/version-meld/discv5/examples/custom_executor.rs @@ -0,0 +1,82 @@ +//! Demonstrates how to run a basic Discovery v5 Service with a custom tokio executor. +//! +//! Discv5 requires a Tokio executor with all features. A custom exuector can be passed via the +//! configuration parameters. If none is passed, it will use the current runtime that build the +//! `Discv5` struct. +//! +//! To run this example simply run: +//! ``` +//! $ cargo run --example custom_executor +//! ``` + +use discv5::{enr, enr::CombinedKey, Discv5, Discv5ConfigBuilder, Discv5Event}; +use std::net::SocketAddr; + +fn main() { + // allows detailed logging with the RUST_LOG env variable + let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new("info")) + .unwrap(); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter_layer) + .try_init(); + + // listening address and port + let listen_addr = "0.0.0.0:9000".parse::().unwrap(); + + let enr_key = CombinedKey::generate_secp256k1(); + // construct a local ENR + let enr = enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); + + // build the tokio executor + let runtime = tokio::runtime::Builder::new_multi_thread() + .thread_name("Discv5-example") + .enable_all() + .build() + .unwrap(); + + // default configuration - uses the current executor + let config = Discv5ConfigBuilder::new().build(); + + // construct the discv5 server + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + + // if we know of another peer's ENR, add it known peers + if let Some(base64_enr) = std::env::args().nth(1) { + match base64_enr.parse::>() { + Ok(enr) => { + println!( + "ENR Read. ip: {:?}, udp_port {:?}, tcp_port: {:?}", + enr.ip(), + enr.udp(), + enr.tcp() + ); + if let Err(e) = discv5.add_enr(enr) { + println!("ENR was not added: {}", e); + } + } + Err(e) => panic!("Decoding ENR failed: {}", e), + } + } + + runtime.block_on(async { + // start the discv5 service + discv5.start(listen_addr).await.unwrap(); + println!("Server started"); + + // get an event stream + let mut event_stream = discv5.event_stream().await.unwrap(); + + loop { + match event_stream.recv().await { + Some(Discv5Event::SocketUpdated(addr)) => { + println!("Nodes ENR socket address has been updated to: {:?}", addr); + } + Some(Discv5Event::Discovered(enr)) => { + println!("A peer has been discovered: {}", enr.node_id()); + } + _ => {} + } + } + }) +} diff --git a/version-meld/discv5/examples/find_nodes.rs b/version-meld/discv5/examples/find_nodes.rs new file mode 100644 index 0000000..16430c1 --- /dev/null +++ b/version-meld/discv5/examples/find_nodes.rs @@ -0,0 +1,167 @@ +//! Demonstrates how to run a basic Discovery v5 Service. +//! +//! This example creates a discv5 service which searches for peers every 60 seconds. On +//! creation, the local ENR created for this service is displayed in base64. This can be used to +//! allow other instances to connect and join the network. The service can be stopped by pressing +//! Ctrl-C. +//! +//! To add peers to the network, create multiple instances of this service adding the ENR of a +//! participating node in the command line. The nodes should discover each other over a period of +//! time. (It is probabilistic that nodes to find each other on any given query). +//! +//! A single instance listening on a UDP socket `0.0.0.0:9000` (with an ENR that has an empty IP +//! and UDP port) can be created via: +//! +//! ``` +//! sh cargo run --example find_nodes +//! ``` +//! +//! As the associated ENR has no IP/Port it is not displayed, as it cannot be used to connect to. +//! +//! An ENR IP address (to allow another nodes to dial this service), port and ENR node can also be +//! passed as command line options. Therefore, a second instance, in a new terminal, can be run on +//! port 9001 and connected to another node with a valid ENR: +//! +//! ``` +//! sh cargo run --example find_nodes -- 127.0.0.1 9001 +//! ``` +//! Here `127.0.0.1` represents the external IP address that others may connect to this node on. The +//! `9001` represents the external port and the port to listen on. The `` is the base64 +//! ENR given from executing the first node with an IP and port +//! given in the CLI. +//! `` is a boolean (`true` or `false`) specifying if a new key should be generated. +//! These steps can be repeated to add further nodes to the test network. +//! +//! The parameters are optional. +//! +//! For a simple CLI discovery service see [discv5-cli](https://github.com/AgeManning/discv5-cli) + +use discv5::{ + enr, + enr::{k256, CombinedKey}, + Discv5, Discv5ConfigBuilder, +}; +use std::{ + net::{Ipv4Addr, SocketAddr}, + time::Duration, +}; + +#[tokio::main] +async fn main() { + let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new("info")) + .unwrap(); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter_layer) + .try_init(); + + // if there is an address specified use it + let address = std::env::args() + .nth(1) + .map(|addr| addr.parse::().unwrap()); + + let port = { + if let Some(udp_port) = std::env::args().nth(2) { + udp_port.parse().unwrap() + } else { + 9000 + } + }; + + // A fixed key for testing + let raw_key = + hex::decode("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291").unwrap(); + let secret_key = k256::ecdsa::SigningKey::from_bytes(&raw_key).unwrap(); + let mut enr_key = CombinedKey::from(secret_key); + + // use a random key if specified + if let Some(generate_key) = std::env::args().nth(3) { + if generate_key.parse::().unwrap() { + enr_key = CombinedKey::generate_secp256k1(); + } + } + + // construct a local ENR + let enr = { + let mut builder = enr::EnrBuilder::new("v4"); + // if an IP was specified, use it + if let Some(external_address) = address { + builder.ip(external_address.into()); + } + // if a port was specified, use it + if std::env::args().nth(2).is_some() { + builder.udp(port); + } + builder.build(&enr_key).unwrap() + }; + + // if the ENR is useful print it + println!("Node Id: {}", enr.node_id()); + if enr.udp_socket().is_some() { + println!("Base64 ENR: {}", enr.to_base64()); + println!("IP: {}, UDP_PORT:{}", enr.ip().unwrap(), enr.udp().unwrap()); + } else { + println!("ENR is not printed as no IP:PORT was specified"); + } + + // default configuration with packet filtering + // let config = Discv5ConfigBuilder::new().enable_packet_filter().build(); + // default configuration without packet filtering + let config = Discv5ConfigBuilder::new().build(); + + // the address to listen on + let socket_addr = SocketAddr::new("0.0.0.0".parse().expect("valid ip"), port); + + // construct the discv5 server + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + + // if we know of another peer's ENR, add it known peers + if let Some(base64_enr) = std::env::args().nth(4) { + match base64_enr.parse::>() { + Ok(enr) => { + println!( + "ENR Read. ip: {:?}, udp_port {:?}, tcp_port: {:?}", + enr.ip(), + enr.udp(), + enr.tcp() + ); + if let Err(e) = discv5.add_enr(enr) { + println!("ENR was not added: {}", e); + } + } + Err(e) => panic!("Decoding ENR failed: {}", e), + } + } + + // start the discv5 service + discv5.start(socket_addr).await.unwrap(); + + // construct a 30 second interval to search for new peers. + let mut query_interval = tokio::time::interval(Duration::from_secs(60)); + + loop { + tokio::select! { + _ = query_interval.tick() => { + // pick a random node target + let target_random_node_id = enr::NodeId::random(); + // get metrics + let metrics = discv5.metrics(); + let connected_peers = discv5.connected_peers(); + println!("Connected peers: {}, Active sessions: {}, Unsolicited requests/s: {:.2}", connected_peers, metrics.active_sessions, metrics.unsolicited_requests_per_second); + println!("Searching for peers..."); + // execute a FINDNODE query + match discv5.find_node(target_random_node_id).await { + Err(e) => println!("Find Node result failed: {:?}", e), + Ok(v) => { + // found a list of ENR's print their NodeIds + let node_ids = v.iter().map(|enr| enr.node_id()).collect::>(); + println!("Nodes found: {}", node_ids.len()); + for node_id in node_ids { + println!("Node: {}", node_id); + } + } + } + } + } + } +} diff --git a/version-meld/discv5/examples/request_enr.rs b/version-meld/discv5/examples/request_enr.rs new file mode 100644 index 0000000..dc010c8 --- /dev/null +++ b/version-meld/discv5/examples/request_enr.rs @@ -0,0 +1,65 @@ +//! Creates a Discv5 Server and requests an ENR of a node given a multiaddr. +//! +//! This is a simple example of how one may connect to a Discv5 peer using a multiaddr/multiaddr +//! string. +//! +//! To run this example execute the following command from the root directory: +//! ```bash +//! $ cargo run --example request_enr +//! ``` +//! +//! The value should be the string form of a multiaddr including the p2p protocol. +//! Currently only secp256k1 and ed25519 keys are supported. +//! +//! This requires the "libp2p" feature. +#[cfg(feature = "libp2p")] +use discv5::{enr, enr::CombinedKey, Discv5, Discv5Config}; +#[cfg(feature = "libp2p")] +use std::net::SocketAddr; + +#[cfg(not(feature = "libp2p"))] +fn main() {} + +#[cfg(feature = "libp2p")] +#[tokio::main] +async fn main() { + let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new("info")) + .unwrap(); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter_layer) + .try_init(); + + // listening address and port + let listen_addr = "0.0.0.0:9000".parse::().unwrap(); + + // generate a new enr key + let enr_key = CombinedKey::generate_secp256k1(); + // construct a local ENR + let enr = enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); + + // default discv5 configuration + let config = Discv5Config::default(); + + let multiaddr = std::env::args() + .nth(1) + .expect("A multiaddr must be supplied"); + + // construct the discv5 server + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + + // start the discv5 service + discv5.start(listen_addr).await.unwrap(); + + // search for the ENR + match discv5.request_enr(multiaddr).await { + Ok(enr) => { + println!("ENR Found:"); + println!("Base64:{}", enr.to_base64()); + println!("{}", enr); + } + Err(e) => { + println!("Error:{:?}", e); + } + } +} diff --git a/version-meld/discv5/examples/simple_server.rs b/version-meld/discv5/examples/simple_server.rs new file mode 100644 index 0000000..b61bec4 --- /dev/null +++ b/version-meld/discv5/examples/simple_server.rs @@ -0,0 +1,109 @@ +//! Demonstrates how to run a basic Discovery v5 Service. +//! +//! This example simply starts a discovery server and listens to events that the server emits. +//! +//! +//! It can be bootstrapped to a DHT by providing an ENR to add to its DHT. +//! +//! To run this example simply run: +//! ``` +//! $ cargo run --example simple_server -- +//! ``` + +use discv5::{enr, enr::CombinedKey, Discv5, Discv5Config, Discv5Event}; +use std::net::{Ipv4Addr, SocketAddr}; + +#[tokio::main] +async fn main() { + // allows detailed logging with the RUST_LOG env variable + let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new("info")) + .unwrap(); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter_layer) + .try_init(); + + // if there is an address specified use it + let address = std::env::args() + .nth(1) + .map(|addr| addr.parse::().unwrap()); + + let port = { + if let Some(udp_port) = std::env::args().nth(2) { + udp_port.parse().unwrap() + } else { + 9000 + } + }; + + // listening address and port + let listen_addr = "0.0.0.0:9000".parse::().unwrap(); + + let enr_key = CombinedKey::generate_secp256k1(); + + // construct a local ENR + let enr = { + let mut builder = enr::EnrBuilder::new("v4"); + // if an IP was specified, use it + if let Some(external_address) = address { + builder.ip(external_address.into()); + } + // if a port was specified, use it + if std::env::args().nth(2).is_some() { + builder.udp(port); + } + builder.build(&enr_key).unwrap() + }; + + // if the ENR is useful print it + println!("Node Id: {}", enr.node_id()); + if enr.udp_socket().is_some() { + println!("Base64 ENR: {}", enr.to_base64()); + println!("IP: {}, UDP_PORT:{}", enr.ip().unwrap(), enr.udp().unwrap()); + } else { + println!("ENR is not printed as no IP:PORT was specified"); + } + + // default configuration + let config = Discv5Config::default(); + + // construct the discv5 server + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + + // if we know of another peer's ENR, add it known peers + if let Some(base64_enr) = std::env::args().nth(3) { + match base64_enr.parse::>() { + Ok(enr) => { + println!( + "ENR Read. ip: {:?}, udp_port {:?}, tcp_port: {:?}", + enr.ip(), + enr.udp(), + enr.tcp() + ); + if let Err(e) = discv5.add_enr(enr) { + println!("ENR was not added: {}", e); + } + } + Err(e) => panic!("Decoding ENR failed: {}", e), + } + } + + // start the discv5 service + discv5.start(listen_addr).await.unwrap(); + println!("Server started"); + + // get an event stream + let mut event_stream = discv5.event_stream().await.unwrap(); + + loop { + match event_stream.recv().await { + Some(Discv5Event::SocketUpdated(addr)) => { + println!("Nodes ENR socket address has been updated to: {:?}", addr); + } + Some(Discv5Event::Discovered(enr)) => { + println!("A peer has been discovered: {}", enr.node_id()); + } + _ => {} + } + } +} diff --git a/version-meld/discv5/src/config.rs b/version-meld/discv5/src/config.rs new file mode 100644 index 0000000..264e885 --- /dev/null +++ b/version-meld/discv5/src/config.rs @@ -0,0 +1,339 @@ +use crate::{ + kbucket::MAX_NODES_PER_BUCKET, Enr, Executor, PermitBanList, RateLimiter, RateLimiterBuilder, +}; +///! A set of configuration parameters to tune the discovery protocol. +use std::time::Duration; + +/// Configuration parameters that define the performance of the gossipsub network. +#[derive(Clone)] +pub struct Discv5Config { + /// Whether to enable the incoming packet filter. Default: false. + pub enable_packet_filter: bool, + + /// The request timeout for each UDP request. Default: 2 seconds. + pub request_timeout: Duration, + + /// The interval over which votes are remembered when determining our external IP. A lower + /// interval will respond faster to IP changes. Default is 30 seconds. + pub vote_duration: Duration, + + /// The timeout after which a `QueryPeer` in an ongoing query is marked unresponsive. + /// Unresponsive peers don't count towards the parallelism limits for a query. + /// Hence, we may potentially end up making more requests to good peers. Default: 2 seconds. + pub query_peer_timeout: Duration, + + /// The timeout for an entire query. Any peers discovered for this query are returned. Default 60 seconds. + pub query_timeout: Duration, + + /// The number of retries for each UDP request. Default: 1. + pub request_retries: u8, + + /// The session timeout for each node. Default: 1 day. + pub session_timeout: Duration, + + /// The maximum number of established sessions to maintain. Default: 1000. + pub session_cache_capacity: usize, + + /// Updates the local ENR IP and port based on PONG responses from peers. Default: true. + pub enr_update: bool, + + /// The maximum number of nodes we return to a find nodes request. The default is 16. + pub max_nodes_response: usize, + + /// The minimum number of peer's who agree on an external IP port before updating the + /// local ENR. Default: 10. + pub enr_peer_update_min: usize, + + /// The number of peers to request in parallel in a single query. Default: 3. + pub query_parallelism: usize, + + /// Limits the number of IP addresses from the same + /// /24 subnet in the kbuckets table. This is to mitigate eclipse attacks. Default: false. + pub ip_limit: bool, + + /// Sets a maximum limit to the number of incoming nodes (nodes that have dialed us) to exist per-bucket. This cannot be larger + /// than the bucket size (16). By default this is disabled (set to the maximum bucket size, 16). + pub incoming_bucket_limit: usize, + + /// A filter used to decide whether to insert nodes into our local routing table. Nodes can be + /// excluded if they do not pass this filter. The default is to accept all nodes. + pub table_filter: fn(&Enr) -> bool, + + /// The time between pings to ensure connectivity amongst connected nodes. Default: 300 + /// seconds. + pub ping_interval: Duration, + + /// Reports all discovered ENR's when traversing the DHT to the event stream. Default true. + pub report_discovered_peers: bool, + + /// A set of configuration parameters for setting inbound request rate limits. See + /// [`RateLimiterBuilder`] for options. This is only functional if the packet filter is + /// enabled via the `enable_packet_filter` option. See the `Default` implementation for + /// default values. If set to None, inbound requests are not filtered. + pub filter_rate_limiter: Option, + + /// The maximum number of node-ids allowed per IP address before the IP address gets banned. + /// Having this set to None, disables this feature. Default value is 10. This is only + /// applicable if the `enable_packet_filter` option is set. + pub filter_max_nodes_per_ip: Option, + + /// The maximum number of nodes that can be banned by a single IP before that IP gets banned. + /// The default is 5. This is only + /// applicable if the `enable_packet_filter` option is set. + pub filter_max_bans_per_ip: Option, + + /// A set of lists that permit or ban IP's or NodeIds from the server. See + /// `crate::PermitBanList`. + pub permit_ban_list: PermitBanList, + + /// Set the default duration for which nodes are banned for. This timeouts are checked every 5 minutes, + /// so the precision will be to the nearest 5 minutes. If set to `None`, bans from the filter + /// will last indefinitely. Default is 1 hour. + pub ban_duration: Option, + + /// A custom executor which can spawn the discv5 tasks. This must be a tokio runtime, with + /// timing support. By default, the executor that created the discv5 struct will be used. + pub executor: Option>, +} + +impl Default for Discv5Config { + fn default() -> Self { + // This is only applicable if enable_packet_filter is set. + let filter_rate_limiter = Some( + RateLimiterBuilder::new() + .total_n_every(10, Duration::from_secs(1)) // Allow bursts, average 10 per second + .node_n_every(8, Duration::from_secs(1)) // Allow bursts, average 8 per second + .ip_n_every(9, Duration::from_secs(1)) // Allow bursts, average 9 per second + .build() + .expect("The total rate limit has been specified"), + ); + + Self { + enable_packet_filter: false, + request_timeout: Duration::from_secs(1), + vote_duration: Duration::from_secs(30), + query_peer_timeout: Duration::from_secs(2), + query_timeout: Duration::from_secs(60), + request_retries: 1, + session_timeout: Duration::from_secs(86400), + session_cache_capacity: 1000, + enr_update: true, + max_nodes_response: 16, + enr_peer_update_min: 10, + query_parallelism: 3, + ip_limit: false, + incoming_bucket_limit: MAX_NODES_PER_BUCKET, + table_filter: |_| true, + ping_interval: Duration::from_secs(300), + report_discovered_peers: true, + filter_rate_limiter, + filter_max_nodes_per_ip: Some(10), + filter_max_bans_per_ip: Some(5), + permit_ban_list: PermitBanList::default(), + ban_duration: Some(Duration::from_secs(3600)), // 1 hour + executor: None, + } + } +} + +#[derive(Debug, Default)] +pub struct Discv5ConfigBuilder { + config: Discv5Config, +} + +impl Discv5ConfigBuilder { + // set default values + pub fn new() -> Self { + Discv5ConfigBuilder::default() + } + + /// Whether to enable the incoming packet filter. + pub fn enable_packet_filter(&mut self) -> &mut Self { + self.config.enable_packet_filter = true; + self + } + + /// The request timeout for each UDP request. + pub fn request_timeout(&mut self, timeout: Duration) -> &mut Self { + self.config.request_timeout = timeout; + self + } + + /// The interval over which votes are remembered when determining our external IP. A lower + /// interval will respond faster to IP changes. Default is 30 seconds. + pub fn vote_duration(&mut self, vote_duration: Duration) -> &mut Self { + self.config.vote_duration = vote_duration; + self + } + + /// The timeout after which a `QueryPeer` in an ongoing query is marked unresponsive. + /// Unresponsive peers don't count towards the parallelism limits for a query. + /// Hence, we may potentially end up making more requests to good peers. + pub fn query_peer_timeout(&mut self, timeout: Duration) -> &mut Self { + self.config.query_peer_timeout = timeout; + self + } + + /// The timeout for an entire query. Any peers discovered before this timeout are returned. + pub fn query_timeout(&mut self, timeout: Duration) -> &mut Self { + self.config.query_timeout = timeout; + self + } + + /// The number of retries for each UDP request. + pub fn request_retries(&mut self, retries: u8) -> &mut Self { + self.config.request_retries = retries; + self + } + + /// The session timeout for each node. + pub fn session_timeout(&mut self, timeout: Duration) -> &mut Self { + self.config.session_timeout = timeout; + self + } + + /// The maximum number of established sessions to maintain. + pub fn session_cache_capacity(&mut self, capacity: usize) -> &mut Self { + self.config.session_cache_capacity = capacity; + self + } + + /// Disables the auto-update of the local ENR IP and port based on PONG responses from peers. + pub fn disable_enr_update(&mut self) -> &mut Self { + self.config.enr_update = false; + self + } + + /// The maximum number of nodes we response to a find nodes request. + pub fn max_nodes_response(&mut self, max: usize) -> &mut Self { + self.config.max_nodes_response = max; + self + } + + /// The minimum number of peer's who agree on an external IP port before updating the + /// local ENR. + pub fn enr_peer_update_min(&mut self, min: usize) -> &mut Self { + if min < 2 { + panic!("Setting enr_peer_update_min to a value less than 2 will cause issues with discovery with peers behind NAT"); + } + self.config.enr_peer_update_min = min; + self + } + + /// The number of peers to request in parallel in a single query. + pub fn query_parallelism(&mut self, parallelism: usize) -> &mut Self { + self.config.query_parallelism = parallelism; + self + } + + /// Limits the number of IP addresses from the same + /// /24 subnet in the kbuckets table. This is to mitigate eclipse attacks. + pub fn ip_limit(&mut self) -> &mut Self { + self.config.ip_limit = true; + self + } + + /// Sets a maximum limit to the number of incoming nodes (nodes that have dialed us) to exist per-bucket. This cannot be larger + /// than the bucket size (16). By default, half of every bucket (8 positions) is the largest number of nodes that we accept that dial us. + pub fn incoming_bucket_limit(&mut self, limit: usize) -> &mut Self { + self.config.incoming_bucket_limit = limit; + self + } + + /// A filter used to decide whether to insert nodes into our local routing table. Nodes can be + /// excluded if they do not pass this filter. + pub fn table_filter(&mut self, filter: fn(&Enr) -> bool) -> &mut Self { + self.config.table_filter = filter; + self + } + + /// The time between pings to ensure connectivity amongst connected nodes. + pub fn ping_interval(&mut self, interval: Duration) -> &mut Self { + self.config.ping_interval = interval; + self + } + + /// Disables reporting of discovered peers through the event stream. + pub fn disable_report_discovered_peers(&mut self) -> &mut Self { + self.config.report_discovered_peers = false; + self + } + + /// A rate limiter for limiting inbound requests. + pub fn filter_rate_limiter(&mut self, rate_limiter: Option) -> &mut Self { + self.config.filter_rate_limiter = rate_limiter; + self + } + + /// If the filter is enabled, sets the maximum number of nodes per IP before banning + /// the IP. + pub fn filter_max_nodes_per_ip(&mut self, max_nodes_per_ip: Option) -> &mut Self { + self.config.filter_max_nodes_per_ip = max_nodes_per_ip; + self + } + + /// The maximum number of times nodes from a single IP can be banned, before the IP itself + /// gets banned. + pub fn filter_max_bans_per_ip(&mut self, max_bans_per_ip: Option) -> &mut Self { + self.config.filter_max_bans_per_ip = max_bans_per_ip; + self + } + + /// A set of lists that permit or ban IP's or NodeIds from the server. See + /// `crate::PermitBanList`. + pub fn permit_ban_list(&mut self, list: PermitBanList) -> &mut Self { + self.config.permit_ban_list = list; + self + } + + /// Set the default duration for which nodes are banned for. This timeouts are checked every 5 minutes, + /// so the precision will be to the nearest 5 minutes. If set to `None`, bans from the filter + /// will last indefinitely. Default is 1 hour. + pub fn ban_duration(&mut self, ban_duration: Option) -> &mut Self { + self.config.ban_duration = ban_duration; + self + } + + /// A custom executor which can spawn the discv5 tasks. This must be a tokio runtime, with + /// timing support. + pub fn executor(&mut self, executor: Box) -> &mut Self { + self.config.executor = Some(executor); + self + } + + pub fn build(&mut self) -> Discv5Config { + // If an executor is not provided, assume a current tokio runtime is running. + if self.config.executor.is_none() { + self.config.executor = Some(Box::new(crate::executor::TokioExecutor::default())); + }; + + assert!(self.config.incoming_bucket_limit <= MAX_NODES_PER_BUCKET); + + self.config.clone() + } +} + +impl std::fmt::Debug for Discv5Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut builder = f.debug_struct("Discv5Config"); + let _ = builder.field("filter_enabled", &self.enable_packet_filter); + let _ = builder.field("request_timeout", &self.request_timeout); + let _ = builder.field("vote_duration", &self.vote_duration); + let _ = builder.field("query_timeout", &self.query_timeout); + let _ = builder.field("query_peer_timeout", &self.query_peer_timeout); + let _ = builder.field("request_retries", &self.request_retries); + let _ = builder.field("session_timeout", &self.session_timeout); + let _ = builder.field("session_cache_capacity", &self.session_cache_capacity); + let _ = builder.field("enr_update", &self.enr_update); + let _ = builder.field("query_parallelism", &self.query_parallelism); + let _ = builder.field("report_discovered_peers", &self.report_discovered_peers); + let _ = builder.field("ip_limit", &self.ip_limit); + let _ = builder.field("filter_max_nodes_per_ip", &self.filter_max_nodes_per_ip); + let _ = builder.field("filter_max_bans_per_ip", &self.filter_max_bans_per_ip); + let _ = builder.field("ip_limit", &self.ip_limit); + let _ = builder.field("incoming_bucket_limit", &self.incoming_bucket_limit); + let _ = builder.field("ping_interval", &self.ping_interval); + let _ = builder.field("ban_duration", &self.ban_duration); + builder.finish() + } +} diff --git a/version-meld/discv5/src/discv5.rs b/version-meld/discv5/src/discv5.rs new file mode 100644 index 0000000..0605219 --- /dev/null +++ b/version-meld/discv5/src/discv5.rs @@ -0,0 +1,592 @@ +//! The Discovery v5 protocol. See the module level docs for further details. +//! +//! This provides the main struct for running and interfacing with a discovery v5 server. +//! +//! A [`Discv5`] struct needs to be created either with an [`crate::executor::Executor`] specified in the +//! [`Discv5Config`] via the [`crate::Discv5ConfigBuilder`] or in the presence of a tokio runtime that has +//! timing and io enabled. +//! +//! Once a [`Discv5`] struct has been created the service is started by running the [`Discv5::start`] +//! functions with a UDP socket. This will start a discv5 server in the background listening on the +//! specified UDP socket. +//! +//! The server can be shutdown using the [`Discv5::shutdown`] function. + +use crate::{ + error::{Discv5Error, QueryError, RequestError}, + kbucket::{ + self, ConnectionDirection, ConnectionState, FailureReason, InsertResult, KBucketsTable, + NodeStatus, UpdateResult, + }, + node_info::NodeContact, + service::{QueryKind, Service, ServiceRequest, TalkRequest}, + Discv5Config, Enr, +}; +use enr::{CombinedKey, EnrError, EnrKey, NodeId}; +use parking_lot::RwLock; +use std::{ + future::Future, + net::SocketAddr, + sync::Arc, + time::{Duration, Instant}, +}; +use tokio::sync::{mpsc, oneshot}; +use tracing::{debug, warn}; + +#[cfg(feature = "libp2p")] +use {libp2p_core::Multiaddr, std::convert::TryFrom}; + +// Create lazy static variable for the global permit/ban list +use crate::metrics::{Metrics, METRICS}; +lazy_static! { + pub static ref PERMIT_BAN_LIST: RwLock = + RwLock::new(crate::PermitBanList::default()); +} + +mod test; + +/// Events that can be produced by the `Discv5` event stream. +#[derive(Debug)] +pub enum Discv5Event { + /// A node has been discovered from a FINDNODES request. + /// + /// The ENR of the node is returned. Various properties can be derived from the ENR. + /// This happen spontaneously through queries as nodes return ENR's. These ENR's are not + /// guaranteed to be live or contactable. + Discovered(Enr), + /// A new ENR was added to the routing table. + EnrAdded { enr: Enr, replaced: Option }, + /// A new node has been added to the routing table. + NodeInserted { + node_id: NodeId, + replaced: Option, + }, + /// Our local ENR IP address has been updated. + SocketUpdated(SocketAddr), + /// A node has initiated a talk request. + TalkRequest(TalkRequest), +} + +/// The main Discv5 Service struct. This provides the user-level API for performing queries and +/// interacting with the underlying service. +pub struct Discv5 { + config: Discv5Config, + /// The channel to make requests from the main service. + service_channel: Option>, + /// The exit channel to shutdown the underlying service. + service_exit: Option>, + /// The routing table of the discv5 service. + kbuckets: Arc>>, + /// The local ENR of the server. + local_enr: Arc>, + /// The key associated with the local ENR, required for updating the local ENR. + enr_key: Arc>, +} + +impl Discv5 { + pub fn new( + local_enr: Enr, + enr_key: CombinedKey, + mut config: Discv5Config, + ) -> Result { + // ensure the keypair matches the one that signed the enr. + if local_enr.public_key() != enr_key.public() { + return Err("Provided keypair does not match the provided ENR"); + } + + // If an executor is not provided, assume a current tokio runtime is running. If not panic. + if config.executor.is_none() { + config.executor = Some(Box::new(crate::executor::TokioExecutor::default())); + }; + + // NOTE: Currently we don't expose custom filter support in the configuration. Users can + // optionally use the IP filter via the ip_limit configuration parameter. In the future, we + // may expose this functionality to the users if there is demand for it. + let (table_filter, bucket_filter) = if config.ip_limit { + ( + Some(Box::new(kbucket::IpTableFilter) as Box>), + Some(Box::new(kbucket::IpBucketFilter) as Box>), + ) + } else { + (None, None) + }; + + let local_enr = Arc::new(RwLock::new(local_enr)); + let enr_key = Arc::new(RwLock::new(enr_key)); + let kbuckets = Arc::new(RwLock::new(KBucketsTable::new( + local_enr.read().node_id().into(), + Duration::from_secs(60), + config.incoming_bucket_limit, + table_filter, + bucket_filter, + ))); + + // Update the PermitBan list based on initial configuration + *PERMIT_BAN_LIST.write() = config.permit_ban_list.clone(); + + Ok(Discv5 { + config, + service_channel: None, + service_exit: None, + kbuckets, + local_enr, + enr_key, + }) + } + + /// Starts the required tasks and begins listening on a given UDP SocketAddr. + pub async fn start(&mut self, listen_socket: SocketAddr) -> Result<(), Discv5Error> { + if self.service_channel.is_some() { + warn!("Service is already started"); + return Err(Discv5Error::ServiceAlreadyStarted); + } + + // create the main service + let (service_exit, service_channel) = Service::spawn( + self.local_enr.clone(), + self.enr_key.clone(), + self.kbuckets.clone(), + self.config.clone(), + listen_socket, + ) + .await?; + self.service_exit = Some(service_exit); + self.service_channel = Some(service_channel); + Ok(()) + } + + /// Terminates the service. + pub fn shutdown(&mut self) { + if let Some(exit) = self.service_exit.take() { + if exit.send(()).is_err() { + debug!("Discv5 service already shutdown"); + } + self.service_channel = None; + } else { + debug!("Service is already shutdown"); + } + } + + /// Adds a known ENR of a peer participating in Service to the + /// routing table. + /// + /// This allows pre-populating the Kademlia routing table with known + /// addresses, so that they can be used immediately in following DHT + /// operations involving one of these peers, without having to dial + /// them upfront. + pub fn add_enr(&self, enr: Enr) -> Result<(), &'static str> { + // only add ENR's that have a valid udp socket. + if enr.udp_socket().is_none() { + warn!("ENR attempted to be added without a UDP socket has been ignored"); + return Err("ENR has no UDP socket to connect to"); + } + + if !(self.config.table_filter)(&enr) { + warn!("ENR attempted to be added which is banned by the configuration table filter."); + return Err("ENR banned by table filter"); + } + + let key = kbucket::Key::from(enr.node_id()); + + match self.kbuckets.write().insert_or_update( + &key, + enr, + NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Incoming, + }, + ) { + InsertResult::Inserted + | InsertResult::Pending { .. } + | InsertResult::StatusUpdated { .. } + | InsertResult::ValueUpdated + | InsertResult::Updated { .. } + | InsertResult::UpdatedPending => Ok(()), + InsertResult::Failed(FailureReason::BucketFull) => Err("Table full"), + InsertResult::Failed(FailureReason::BucketFilter) => Err("Failed bucket filter"), + InsertResult::Failed(FailureReason::TableFilter) => Err("Failed table filter"), + InsertResult::Failed(FailureReason::InvalidSelfUpdate) => Err("Invalid self update"), + InsertResult::Failed(_) => Err("Failed to insert ENR"), + } + } + + /// Removes a `node_id` from the routing table. + /// + /// This allows applications, for whatever reason, to remove nodes from the local routing + /// table. Returns `true` if the node was in the table and `false` otherwise. + pub fn remove_node(&self, node_id: &NodeId) -> bool { + let key = &kbucket::Key::from(*node_id); + self.kbuckets.write().remove(key) + } + + /// Returns a vector of closest nodes by the given distances. + pub fn nodes_by_distance(&self, mut distances: Vec) -> Vec { + let mut nodes_to_send = Vec::new(); + distances.sort_unstable(); + distances.dedup(); + + if let Some(0) = distances.first() { + // if the distance is 0 send our local ENR + nodes_to_send.push(self.local_enr.read().clone()); + distances.remove(0); + } + + if !distances.is_empty() { + let mut kbuckets = self.kbuckets.write(); + for node in kbuckets + .nodes_by_distances(distances.as_slice(), self.config.max_nodes_response) + .into_iter() + .map(|entry| entry.node.value.clone()) + { + nodes_to_send.push(node); + } + } + nodes_to_send + } + + /// Mark a node in the routing table as `Disconnnected`. + /// + /// A `Disconnected` node will be present in the routing table and will be only + /// used if there are no other `Connected` peers in the bucket. + /// Returns `true` if node was in table and `false` otherwise. + pub fn disconnect_node(&self, node_id: &NodeId) -> bool { + let key = &kbucket::Key::from(*node_id); + !matches!( + self.kbuckets + .write() + .update_node_status(key, ConnectionState::Disconnected, None), + UpdateResult::Failed(_) + ) + } + + /// Returns the number of connected peers that exist in the routing table. + pub fn connected_peers(&self) -> usize { + self.kbuckets + .write() + .iter() + .filter(|entry| entry.status.is_connected()) + .count() + } + + /// Gets the metrics associated with the Server + pub fn metrics(&self) -> Metrics { + Metrics::from(&METRICS) + } + + /// Exposes the raw reference to the underlying internal metrics. + pub fn raw_metrics() -> &'static METRICS { + &METRICS + } + + /// Returns the local ENR of the node. + pub fn local_enr(&self) -> Enr { + self.local_enr.read().clone() + } + + /// Returns an ENR if one is known for the given NodeId. + pub fn find_enr(&self, node_id: &NodeId) -> Option { + // check if we know this node id in our routing table + let key = kbucket::Key::from(*node_id); + if let kbucket::Entry::Present(entry, _) = self.kbuckets.write().entry(&key) { + return Some(entry.value().clone()); + } + None + } + + /// Bans a node from the server. This will remove the node from the routing table if it exists + /// and block all incoming packets from the node until the timeout specified. Setting the + /// timeout to `None` creates a permanent ban. + pub fn ban_node(&self, node_id: &NodeId, duration_of_ban: Option) { + let time_to_unban = duration_of_ban.map(|v| Instant::now() + v); + self.remove_node(node_id); + PERMIT_BAN_LIST + .write() + .ban_nodes + .insert(*node_id, time_to_unban); + } + + /// Removes a banned node from the banned list. + pub fn ban_node_remove(&self, node_id: &NodeId) { + PERMIT_BAN_LIST.write().ban_nodes.remove(node_id); + } + + /// Permits a node, allowing the node to bypass the packet filter. + pub fn permit_node(&self, node_id: &NodeId) { + PERMIT_BAN_LIST.write().permit_nodes.insert(*node_id); + } + + /// Removes a node from the permit list. + pub fn permit_node_remove(&self, node_id: &NodeId) { + PERMIT_BAN_LIST.write().permit_nodes.remove(node_id); + } + + /// Bans an IP from the server. This will block all incoming packets from the IP. + pub fn ban_ip(&self, ip: std::net::IpAddr, duration_of_ban: Option) { + let time_to_unban = duration_of_ban.map(|v| Instant::now() + v); + PERMIT_BAN_LIST.write().ban_ips.insert(ip, time_to_unban); + } + + /// Removes a banned IP from the banned list. + pub fn ban_ip_remove(&self, ip: &std::net::IpAddr) { + PERMIT_BAN_LIST.write().ban_ips.remove(ip); + } + + /// Permits an IP, allowing the all packets from the IP to bypass the packet filter. + pub fn permit_ip(&self, ip: std::net::IpAddr) { + PERMIT_BAN_LIST.write().permit_ips.insert(ip); + } + + /// Removes an IP from the permit list. + pub fn permit_ip_remove(&self, ip: &std::net::IpAddr) { + PERMIT_BAN_LIST.write().permit_ips.remove(ip); + } + + /// Updates the local ENR TCP/UDP socket. + pub fn update_local_enr_socket(&self, socket_addr: SocketAddr, is_tcp: bool) -> bool { + let local_socket = self.local_enr.read().udp_socket(); + if local_socket != Some(socket_addr) { + if is_tcp { + self.local_enr + .write() + .set_tcp_socket(socket_addr, &self.enr_key.read()) + .is_ok() + } else { + self.local_enr + .write() + .set_udp_socket(socket_addr, &self.enr_key.read()) + .is_ok() + } + } else { + false + } + } + + /// Allows application layer to insert an arbitrary field into the local ENR. + pub fn enr_insert(&self, key: &str, value: &[u8]) -> Result>, EnrError> { + self.local_enr + .write() + .insert(key, value, &self.enr_key.read()) + .map(|v| v.map(|v| v.to_vec())) + } + + /// Returns an iterator over all ENR node IDs of nodes currently contained in the routing table. + pub fn table_entries_id(&self) -> Vec { + self.kbuckets + .write() + .iter() + .map(|entry| *entry.node.key.preimage()) + .collect() + } + + /// Returns an iterator over all the ENR's of nodes currently contained in the routing table. + pub fn table_entries_enr(&self) -> Vec { + self.kbuckets + .write() + .iter() + .map(|entry| entry.node.value.clone()) + .collect() + } + + /// Returns an iterator over all the entries in the routing table. + pub fn table_entries(&self) -> Vec<(NodeId, Enr, NodeStatus)> { + self.kbuckets + .write() + .iter() + .map(|entry| { + ( + *entry.node.key.preimage(), + entry.node.value.clone(), + entry.status, + ) + }) + .collect() + } + + /// Requests the ENR of a node corresponding to multiaddr or multi-addr string. + /// + /// Only `ed25519` and `secp256k1` key types are currently supported. + /// + /// Note: The async syntax is forgone here in order to create `'static` futures, where the + /// underlying sending channel is cloned. + #[cfg(feature = "libp2p")] + #[cfg_attr(docsrs, doc(cfg(feature = "libp2p")))] + pub fn request_enr( + &self, + multiaddr: impl std::convert::TryInto + 'static, + ) -> impl Future> + 'static { + let channel = self.clone_channel(); + + async move { + let channel = channel.map_err(|_| RequestError::ServiceNotStarted)?; + // Sanitize the multiaddr + + // The multiaddr must support the udp protocol and be of an appropriate key type. + // The conversion logic is contained in the `TryFrom` implementation of a + // `NodeContact`. + let multiaddr: Multiaddr = multiaddr.try_into().map_err(|_| { + RequestError::InvalidMultiaddr("Could not convert to multiaddr".into()) + })?; + let node_contact: NodeContact = NodeContact::try_from(multiaddr) + .map_err(|e| RequestError::InvalidMultiaddr(e.into()))?; + + let (callback_send, callback_recv) = oneshot::channel(); + + let event = ServiceRequest::FindEnr(node_contact, callback_send); + channel + .send(event) + .await + .map_err(|_| RequestError::ChannelFailed("Service channel closed".into()))?; + callback_recv + .await + .map_err(|e| RequestError::ChannelFailed(e.to_string()))? + } + } + + /// Request a TALK message from a node, identified via the ENR. + pub fn talk_req( + &self, + enr: Enr, + protocol: Vec, + request: Vec, + ) -> impl Future, RequestError>> + 'static { + // convert the ENR to a node_contact. + let node_contact = NodeContact::from(enr); + + // the service will verify if this node is contactable, we just send it and + // await a response. + let (callback_send, callback_recv) = oneshot::channel(); + let channel = self.clone_channel(); + + async move { + let channel = channel.map_err(|_| RequestError::ServiceNotStarted)?; + + let event = ServiceRequest::Talk(node_contact, protocol, request, callback_send); + + // send the request + channel + .send(event) + .await + .map_err(|_| RequestError::ChannelFailed("Service channel closed".into()))?; + // await the response + callback_recv + .await + .map_err(|e| RequestError::ChannelFailed(e.to_string()))? + } + } + + /// Runs an iterative `FIND_NODE` request. + /// + /// This will return peers containing contactable nodes of the DHT closest to the + /// requested `NodeId`. + /// + /// Note: The async syntax is forgone here in order to create `'static` futures, where the + /// underlying sending channel is cloned. + pub fn find_node( + &self, + target_node: NodeId, + ) -> impl Future, QueryError>> + 'static { + let channel = self.clone_channel(); + + async move { + let channel = channel.map_err(|_| QueryError::ServiceNotStarted)?; + let (callback_send, callback_recv) = oneshot::channel(); + + let query_kind = QueryKind::FindNode { target_node }; + + let event = ServiceRequest::StartQuery(query_kind, callback_send); + channel + .send(event) + .await + .map_err(|_| QueryError::ChannelFailed("Service channel closed".into()))?; + + callback_recv + .await + .map_err(|e| QueryError::ChannelFailed(e.to_string())) + } + } + + /// Starts a `FIND_NODE` request. + /// + /// This will return less than or equal to `num_nodes` ENRs which satisfy the + /// `predicate`. + /// + /// The predicate is a boxed function that takes an ENR reference and returns a boolean + /// indicating if the record is applicable to the query or not. + /// + /// Note: The async syntax is forgone here in order to create `'static` futures, where the + /// underlying sending channel is cloned. + /// + /// ### Example + /// ```ignore + /// let predicate = Box::new(|enr: &Enr| enr.ip().is_some()); + /// let target = NodeId::random(); + /// let result = discv5.find_node_predicate(target, predicate, 5).await; + /// ``` + pub fn find_node_predicate( + &self, + target_node: NodeId, + predicate: Box bool + Send>, + target_peer_no: usize, + ) -> impl Future, QueryError>> + 'static { + let channel = self.clone_channel(); + + async move { + let channel = channel.map_err(|_| QueryError::ServiceNotStarted)?; + let (callback_send, callback_recv) = oneshot::channel(); + + let query_kind = QueryKind::Predicate { + target_node, + predicate, + target_peer_no, + }; + + let event = ServiceRequest::StartQuery(query_kind, callback_send); + channel + .send(event) + .await + .map_err(|_| QueryError::ChannelFailed("Service channel closed".into()))?; + + callback_recv + .await + .map_err(|e| QueryError::ChannelFailed(e.to_string())) + } + } + + /// Creates an event stream channel which can be polled to receive Discv5 events. + pub fn event_stream( + &self, + ) -> impl Future, Discv5Error>> + 'static { + let channel = self.clone_channel(); + + async move { + let channel = channel?; + + let (callback_send, callback_recv) = oneshot::channel(); + + let event = ServiceRequest::RequestEventStream(callback_send); + channel + .send(event) + .await + .map_err(|_| Discv5Error::ServiceChannelClosed)?; + + callback_recv + .await + .map_err(|_| Discv5Error::ServiceChannelClosed) + } + } + + /// Internal helper function to send events to the Service. + fn clone_channel(&self) -> Result, Discv5Error> { + if let Some(channel) = self.service_channel.as_ref() { + Ok(channel.clone()) + } else { + Err(Discv5Error::ServiceNotStarted) + } + } +} + +impl Drop for Discv5 { + fn drop(&mut self) { + self.shutdown(); + } +} diff --git a/version-meld/discv5/src/discv5/test.rs b/version-meld/discv5/src/discv5/test.rs new file mode 100644 index 0000000..55e13e5 --- /dev/null +++ b/version-meld/discv5/src/discv5/test.rs @@ -0,0 +1,482 @@ +#![cfg(test)] + +use crate::{kbucket, Discv5, *}; +use enr::{k256, CombinedKey, Enr, EnrBuilder, EnrKey, NodeId}; +use rand_core::{RngCore, SeedableRng}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr}, +}; + +fn init() { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); +} + +fn update_enr(discv5: &mut Discv5, key: &str, value: &[u8]) -> bool { + discv5.enr_insert(key, value).is_ok() +} + +async fn build_nodes(n: usize, base_port: u16) -> Vec { + let mut nodes = Vec::new(); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + + for port in base_port..base_port + n as u16 { + let enr_key = CombinedKey::generate_secp256k1(); + let config = Discv5Config::default(); + + let enr = EnrBuilder::new("v4") + .ip(ip) + .udp(port) + .build(&enr_key) + .unwrap(); + // transport for building a swarm + let socket_addr = enr.udp_socket().unwrap(); + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + discv5.start(socket_addr).await.unwrap(); + nodes.push(discv5); + } + nodes +} + +/// Build `n` swarms using passed keypairs. +async fn build_nodes_from_keypairs(keys: Vec, base_port: u16) -> Vec { + let mut nodes = Vec::new(); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + + for (i, enr_key) in keys.into_iter().enumerate() { + let port = base_port + i as u16; + + let config = Discv5ConfigBuilder::new().build(); + let enr = EnrBuilder::new("v4") + .ip(ip) + .udp(port) + .build(&enr_key) + .unwrap(); + + let socket_addr = enr.udp_socket().unwrap(); + let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); + discv5.start(socket_addr).await.unwrap(); + nodes.push(discv5); + } + nodes +} + +/// Generate `n` deterministic keypairs from a given seed. +fn generate_deterministic_keypair(n: usize, seed: u64) -> Vec { + let mut keypairs = Vec::new(); + for i in 0..n { + let sk = { + let rng = &mut rand_xorshift::XorShiftRng::seed_from_u64(seed + i as u64); + let mut b = [0; 32]; + loop { + // until a value is given within the curve order + rng.fill_bytes(&mut b); + if let Ok(k) = k256::ecdsa::SigningKey::from_bytes(&b) { + break k; + } + } + }; + let kp = CombinedKey::from(sk); + keypairs.push(kp); + } + keypairs +} + +fn get_distance(node1: NodeId, node2: NodeId) -> Option { + let node1: kbucket::Key = node1.into(); + node1.log2_distance(&node2.into()) +} + +// Simple searching function to find seeds that give node ids for a range of testing and different +// topologies +#[allow(dead_code)] +fn find_seed_same_bucket() { + let mut seed = 1; + 'main: loop { + if seed % 1000 == 0 { + println!("Seed: {}", seed); + } + + let keys = generate_deterministic_keypair(11, seed); + + let node_ids = keys + .into_iter() + .map(|k| NodeId::from(k.public())) + .collect::>(); + + let local = node_ids[0]; + + for &id in node_ids[1..].iter() { + let distance = get_distance(local, id); + if distance != Some(256) { + seed += 1; + continue 'main; + } + } + break; + } + println!("Found Seed: {}", seed); +} + +#[allow(dead_code)] +fn find_seed_spread_bucket() { + let mut buckets; + let mut seed = 1; + loop { + seed += 1; + + let keys = generate_deterministic_keypair(11, seed); + + let node_ids = keys + .into_iter() + .map(|k| NodeId::from(k.public())) + .collect::>(); + + let local = node_ids[0]; + + buckets = HashMap::new(); + + for &id in node_ids[1..].iter() { + let distance = get_distance(local, id); + if let Some(distance) = distance { + *buckets.entry(distance).or_insert_with(|| 0) += 1; + } + } + if buckets.values().find(|v| **v > 2) == None { + break; + } + if seed % 1000 == 0 { + println!("Seed: {}", seed); + } + } + println!("Found Seed: {}", seed); + for (k, v) in buckets.iter() { + println!("{}, {}", k, v); + } +} + +/// This is a smaller version of the star topology test designed to debug issues with queries. +#[tokio::test] +async fn test_discovery_three_peers() { + init(); + let total_nodes = 3; + // Seed is chosen such that all nodes are in the 256th bucket of bootstrap + let seed = 1652; + // Generate `num_nodes` + bootstrap_node and target_node keypairs from given seed + let keypairs = generate_deterministic_keypair(total_nodes + 2, seed); + let mut nodes = build_nodes_from_keypairs(keypairs, 11200).await; + // Last node is bootstrap node in a star topology + let bootstrap_node = nodes.remove(0); + // target_node is not polled. + let target_node = nodes.pop().unwrap(); + println!("Bootstrap node: {}", bootstrap_node.local_enr().node_id()); + println!("Target node: {}", target_node.local_enr().node_id()); + let key: kbucket::Key = target_node.local_enr().node_id().into(); + let distance = key + .log2_distance(&bootstrap_node.local_enr().node_id().into()) + .unwrap(); + println!( + "Distance of target_node {} relative to bootstrap {}: {}", + target_node.local_enr().node_id(), + bootstrap_node.local_enr().node_id(), + distance + ); + for node in nodes.iter_mut() { + let key: kbucket::Key = node.local_enr().node_id().into(); + let distance = key + .log2_distance(&bootstrap_node.local_enr().node_id().into()) + .unwrap(); + println!( + "Distance of node {} relative to bootstrap {}: {}", + node.local_enr().node_id(), + bootstrap_node.local_enr().node_id(), + distance + ); + node.add_enr(bootstrap_node.local_enr().clone()).unwrap(); + bootstrap_node.add_enr(node.local_enr().clone()).unwrap(); + } + + // Start a FINDNODE query of target + let target_random_node_id = target_node.local_enr().node_id(); + nodes.push(bootstrap_node); + let result_nodes = nodes + .first_mut() + .unwrap() + .find_node(target_random_node_id) + .await + .unwrap(); + println!( + "Query found {} peers, Total peers {}", + result_nodes.len(), + total_nodes + ); + assert!(result_nodes.len() == total_nodes); +} + +/// Test for a star topology with `num_nodes` connected to a `bootstrap_node` +/// FINDNODE request is sent from any of the `num_nodes` nodes to a `target_node` +/// which isn't part of the swarm. +/// The seed for the keypair generation is chosen such that all `num_nodes` nodes +/// and the `target_node` are in the 256th k-bucket of the bootstrap node. +/// This ensures that all nodes are found in a single FINDNODE query. +#[tokio::test] +async fn test_discovery_star_topology() { + init(); + let total_nodes = 10; + // Seed is chosen such that all nodes are in the 256th bucket of bootstrap + let seed = 1652; + // Generate `num_nodes` + bootstrap_node and target_node keypairs from given seed + let keypairs = generate_deterministic_keypair(total_nodes + 2, seed); + let mut nodes = build_nodes_from_keypairs(keypairs, 11000).await; + // Last node is bootstrap node in a star topology + let bootstrap_node = nodes.remove(0); + // target_node is not polled. + let target_node = nodes.pop().unwrap(); + println!("Bootstrap node: {}", bootstrap_node.local_enr().node_id()); + let key: kbucket::Key = target_node.local_enr().node_id().into(); + let distance = key + .log2_distance(&bootstrap_node.local_enr().node_id().into()) + .unwrap(); + println!("Target node: {}", target_node.local_enr().node_id()); + println!( + "Distance of target_node {} relative to bootstrap {}: {}", + target_node.local_enr().node_id(), + bootstrap_node.local_enr().node_id(), + distance + ); + for node in nodes.iter_mut() { + let key: kbucket::Key = node.local_enr().node_id().into(); + let distance = key + .log2_distance(&bootstrap_node.local_enr().node_id().into()) + .unwrap(); + println!( + "Distance of node {} relative to bootstrap node {}: {}", + node.local_enr().node_id(), + bootstrap_node.local_enr().node_id(), + distance + ); + node.add_enr(bootstrap_node.local_enr().clone()).unwrap(); + bootstrap_node.add_enr(node.local_enr().clone()).unwrap(); + } + // Start a FINDNODE query of target + let target_random_node_id = target_node.local_enr().node_id(); + nodes.push(bootstrap_node); + let result_nodes = nodes + .first_mut() + .unwrap() + .find_node(target_random_node_id) + .await + .unwrap(); + println!( + "Query found {} peers, Total peers {}", + result_nodes.len(), + total_nodes + ); + assert!(result_nodes.len() == total_nodes); +} + +#[tokio::test] +async fn test_findnode_query() { + init(); + // build a collection of 8 nodes + let total_nodes = 8; + let mut nodes = build_nodes(total_nodes, 30000).await; + let node_enrs: Vec> = nodes.iter().map(|n| n.local_enr()).collect(); + + // link the nodes together + for (node, previous_node_enr) in nodes.iter_mut().skip(1).zip(node_enrs.clone()) { + let key: kbucket::Key = node.local_enr().node_id().into(); + let distance = key + .log2_distance(&previous_node_enr.node_id().into()) + .unwrap(); + println!("Distance of node relative to next: {}", distance); + node.add_enr(previous_node_enr).unwrap(); + } + + // pick a random node target + let target_random_node_id = NodeId::random(); + + // start a query on the last node + let found_nodes = nodes + .last_mut() + .unwrap() + .find_node(target_random_node_id) + .await + .unwrap(); + + // build expectations + let expected_node_ids: Vec = node_enrs + .iter() + .map(|enr| enr.node_id()) + .take(total_nodes - 1) + .collect(); + + // NOTE: The number of peers found is statistical, as we only ask + // peers for specific buckets, there is a chance our node doesn't + // exist if the first few buckets asked for. + println!( + "Query with found {} peers. Total peers were: {}", + found_nodes.len(), + expected_node_ids.len() + ); + assert!(found_nodes.len() <= expected_node_ids.len()); +} + +#[tokio::test] +async fn test_predicate_search() { + init(); + let total_nodes = 10; + // Seed is chosen such that all nodes are in the 256th bucket of bootstrap + let seed = 1652; + // Generate `num_nodes` + bootstrap_node and target_node keypairs from given seed + let keypairs = generate_deterministic_keypair(total_nodes + 2, seed); + let mut nodes = build_nodes_from_keypairs(keypairs, 12000).await; + // Last node is bootstrap node in a star topology + let bootstrap_node = nodes.remove(0); + // target_node is not polled. + let target_node = nodes.pop().unwrap(); + + // Update `num_nodes` with the required attnet value + let num_nodes = total_nodes / 2; + let required_attnet_value = vec![1, 0, 0, 0]; + let unwanted_attnet_value = vec![0, 0, 0, 0]; + println!("Bootstrap node: {}", bootstrap_node.local_enr().node_id()); + println!("Target node: {}", target_node.local_enr().node_id()); + + for (i, swarm) in nodes.iter_mut().enumerate() { + let key: kbucket::Key = swarm.local_enr().node_id().into(); + let distance = key + .log2_distance(&bootstrap_node.local_enr().node_id().into()) + .unwrap(); + println!( + "Distance of node {} relative to node {}: {}", + swarm.local_enr().node_id(), + bootstrap_node.local_enr().node_id(), + distance + ); + swarm.add_enr(bootstrap_node.local_enr().clone()).unwrap(); + if i % 2 == 0 { + update_enr(swarm, "attnets", &unwanted_attnet_value); + } else { + update_enr(swarm, "attnets", &required_attnet_value); + } + bootstrap_node.add_enr(swarm.local_enr().clone()).unwrap(); + } + + // Predicate function for filtering enrs + let predicate = move |enr: &Enr| { + if let Some(v) = enr.get("attnets") { + v == required_attnet_value.as_slice() + } else { + false + } + }; + nodes.push(bootstrap_node); + + // Start a find enr predicate query + let target_random_node_id = target_node.local_enr().node_id(); + let found_nodes = nodes + .first_mut() + .unwrap() + .find_node_predicate(target_random_node_id, Box::new(predicate), total_nodes) + .await + .unwrap(); + + println!( + "Query found {} peers. Total peers were: {}", + found_nodes.len(), + total_nodes, + ); + println!("Nodes expected to pass predicate search {}", num_nodes); + assert!(found_nodes.len() == num_nodes); +} + +// The kbuckets table can have maximum 10 nodes in the same /24 subnet across all buckets +#[tokio::test] +async fn test_table_limits() { + // this seed generates 12 node id's that are distributed accross buckets such that no more than + // 2 exist in a single bucket. + let mut keypairs = generate_deterministic_keypair(12, 9487); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + let enr_key: CombinedKey = keypairs.remove(0); + let config = Discv5ConfigBuilder::new().ip_limit().build(); + let enr = EnrBuilder::new("v4") + .ip(ip) + .udp(9050) + .build(&enr_key) + .unwrap(); + + // let socket_addr = enr.udp_socket().unwrap(); + let discv5: Discv5 = Discv5::new(enr, enr_key, config).unwrap(); + let table_limit: usize = 10; + // Generate `table_limit + 2` nodes in the same subnet. + let enrs: Vec> = (1..=table_limit + 1) + .map(|i| { + let ip: IpAddr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, i as u8)); + let enr_key: CombinedKey = keypairs.remove(0); + EnrBuilder::new("v4") + .ip(ip) + .udp(9050 + i as u16) + .build(&enr_key) + .unwrap() + }) + .collect(); + for enr in enrs { + let _ = discv5.add_enr(enr.clone()); // we expect some of these to fail the filter. + } + // Number of entries should be `table_limit`, i.e one node got restricted + assert_eq!(discv5.kbuckets.read().iter_ref().count(), table_limit); +} + +// Each bucket can have maximum 2 nodes in the same /24 subnet +#[tokio::test] +async fn test_bucket_limits() { + let enr_key = CombinedKey::generate_secp256k1(); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + let enr = EnrBuilder::new("v4") + .ip(ip) + .udp(9500) + .build(&enr_key) + .unwrap(); + let bucket_limit: usize = 2; + // Generate `bucket_limit + 1` keypairs that go in `enr` node's 256th bucket. + let keys = { + let mut keys = Vec::new(); + for _ in 0..bucket_limit + 1 { + loop { + let key = CombinedKey::generate_secp256k1(); + let enr_new = EnrBuilder::new("v4").build(&key).unwrap(); + let node_key: kbucket::Key = enr.node_id().into(); + let distance = node_key.log2_distance(&enr_new.node_id().into()).unwrap(); + if distance == 256 { + keys.push(key); + break; + } + } + } + keys + }; + // Generate `bucket_limit + 1` nodes in the same subnet. + let enrs: Vec> = (1..=bucket_limit + 1) + .map(|i| { + let kp = &keys[i - 1]; + let ip: IpAddr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, i as u8)); + EnrBuilder::new("v4") + .ip(ip) + .udp(9500 + i as u16) + .build(kp) + .unwrap() + }) + .collect(); + + let config = Discv5ConfigBuilder::new().ip_limit().build(); + let discv5 = Discv5::new(enr, enr_key, config).unwrap(); + for enr in enrs { + let _ = discv5.add_enr(enr.clone()); // we expect some of these to fail based on the filter. + } + + // Number of entries should be equal to `bucket_limit`. + assert_eq!(discv5.kbuckets.read().iter_ref().count(), bucket_limit); +} diff --git a/version-meld/discv5/src/error.rs b/version-meld/discv5/src/error.rs new file mode 100644 index 0000000..26f0757 --- /dev/null +++ b/version-meld/discv5/src/error.rs @@ -0,0 +1,146 @@ +use crate::handler::Challenge; +use rlp::DecoderError; +use std::fmt; + +#[derive(Debug)] +/// A general error that is used throughout the Discv5 library. +pub enum Discv5Error { + /// An invalid ENR was received. + InvalidEnr, + /// The public key type is known. + UnknownPublicKey, + /// The ENR key used is not supported. + KeyTypeNotSupported(&'static str), + /// Failed to derive an ephemeral public key. + KeyDerivationFailed, + /// The remote's public key was invalid. + InvalidRemotePublicKey, + /// The secret key does not match the provided ENR. + InvalidSecretKey, + /// An invalid signature was received for a challenge. + InvalidChallengeSignature(Challenge), + /// The Service channel has been closed early. + ServiceChannelClosed, + /// The discv5 service is not running. + ServiceNotStarted, + /// The service has is already running. + ServiceAlreadyStarted, + /// A session could not be established with the remote. + SessionNotEstablished, + /// An RLP decoding error occurred. + RLPError(DecoderError), + /// Failed to encrypt a message. + EncryptionFail(String), + /// Failed to decrypt a message. + DecryptionFailed(String), + /// The custom error has occurred. + Custom(&'static str), + /// A generic dynamic error occurred. + Error(String), + /// An IO error occurred. + Io(std::io::Error), +} + +impl From for Discv5Error { + fn from(err: std::io::Error) -> Discv5Error { + Discv5Error::Io(err) + } +} + +#[derive(Debug, Clone, PartialEq)] +/// Types of packet errors. +pub enum PacketError { + /// The packet type is unknown. + UnknownPacket, + /// The packet size was larger than expected. + TooLarge, + /// The packet size was smaller than expected. + TooSmall, + /// The NodeId sent was invalid. + InvalidNodeId, + /// The header has an invalid length. + HeaderLengthInvalid(usize), + /// The header could not be decrypted. + HeaderDecryptionFailed, + /// The authdata size is too large. + InvalidAuthDataSize, + /// The handshake is of an invalid version. + InvalidVersion(u16), + /// The ENR sent was invalid. + InvalidEnr(DecoderError), +} + +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum ResponseError { + /// The channel used to send the response has already been closed. + ChannelClosed, +} + +impl fmt::Display for ResponseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ResponseError::ChannelClosed => { + write!(f, "response channel has already been closed") + } + } + } +} + +impl std::error::Error for ResponseError {} + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestError { + /// The request timed out. + Timeout, + /// The discovery service has not been started. + ServiceNotStarted, + /// The request was sent to ourselves. + SelfRequest, + /// The channel to the underlying threads failed. + ChannelFailed(String), + /// An invalid ENR was provided. + InvalidEnr(String), + /// The remote's ENR is invalid. + InvalidRemoteEnr, + /// The remote returned and invalid packet. + InvalidRemotePacket, + /// Failed attempting to encrypt the request. + EncryptionFailed(String), + /// The multiaddr provided is invalid. + InvalidMultiaddr(String), + /// Failure generating random numbers during request. + EntropyFailure(&'static str), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum QueryError { + /// The discv5 service is not currently running. + ServiceNotStarted, + /// The channel to the underlying threads failed. + ChannelFailed(String), + /// The ENR provided was invalid. + InvalidEnr(String), + /// Encrypting the message failed. + EncryptionFailed(String), + /// The multiaddr provided was invalid. + InvalidMultiaddr(String), +} + +impl fmt::Display for Discv5Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl fmt::Display for RequestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl fmt::Display for QueryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/version-meld/discv5/src/executor.rs b/version-meld/discv5/src/executor.rs new file mode 100644 index 0000000..f3b6532 --- /dev/null +++ b/version-meld/discv5/src/executor.rs @@ -0,0 +1,42 @@ +///! A simple trait to allow generic executors or wrappers for spawning the discv5 tasks. +use std::future::Future; +use std::pin::Pin; + +pub trait Executor: ExecutorClone { + /// Run the given future in the background until it ends. + fn spawn(&self, future: Pin + Send>>); +} + +pub trait ExecutorClone { + fn clone_box(&self) -> Box; +} + +impl ExecutorClone for T +where + T: 'static + Executor + Clone + Send + Sync, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_box() + } +} + +#[derive(Clone)] +pub struct TokioExecutor; + +impl Executor for TokioExecutor { + fn spawn(&self, future: Pin + Send>>) { + tokio::task::spawn(future); + } +} + +impl Default for TokioExecutor { + fn default() -> Self { + TokioExecutor + } +} diff --git a/version-meld/discv5/src/handler/crypto/ecdh.rs b/version-meld/discv5/src/handler/crypto/ecdh.rs new file mode 100644 index 0000000..bf78000 --- /dev/null +++ b/version-meld/discv5/src/handler/crypto/ecdh.rs @@ -0,0 +1,23 @@ +//! Implements the static ecdh algorithm required by discv5 in terms of the `k256` library. +use super::k256::{ + self, + ecdsa::{SigningKey, VerifyingKey}, + elliptic_curve::sec1::ToEncodedPoint, +}; + +pub fn ecdh(public_key: &VerifyingKey, secret_key: &SigningKey) -> Vec { + k256::PublicKey::from_affine( + (&k256::PublicKey::from_sec1_bytes(public_key.to_bytes().as_ref()) + .unwrap() + .to_projective() + * k256::SecretKey::from_be_bytes(secret_key.to_bytes().as_ref()) + .unwrap() + .to_nonzero_scalar() + .as_ref()) + .to_affine(), + ) + .unwrap() + .to_encoded_point(true) + .as_bytes() + .to_vec() +} diff --git a/version-meld/discv5/src/handler/crypto/mod.rs b/version-meld/discv5/src/handler/crypto/mod.rs new file mode 100644 index 0000000..d6c4544 --- /dev/null +++ b/version-meld/discv5/src/handler/crypto/mod.rs @@ -0,0 +1,405 @@ +//! Implementation for generating session keys in the Discv5 protocol. +//! Currently, Diffie-Hellman key agreement is performed with known public key types. Session keys +//! are then derived using the HKDF (SHA2-256) key derivation function. +//! +//! There is no abstraction in this module as the specification explicitly defines a singular +//! encryption and key-derivation algorithms. Future versions may abstract some of these to allow +//! for different algorithms. +use crate::{ + error::Discv5Error, + node_info::NodeContact, + packet::{ChallengeData, MessageNonce}, +}; +use aes_gcm::{ + aead::{generic_array::GenericArray, Aead, NewAead, Payload}, + Aes128Gcm, +}; +use ecdh::ecdh; +use enr::{ + k256::{ + self, + ecdsa::{ + signature::{DigestSigner, DigestVerifier, Signature as _}, + Signature, + }, + }, + CombinedKey, CombinedPublicKey, NodeId, +}; +use hkdf::Hkdf; +use sha2::{Digest, Sha256}; +use std::convert::TryFrom; + +mod ecdh; + +const NODE_ID_LENGTH: usize = 32; +const INFO_LENGTH: usize = 26 + 2 * NODE_ID_LENGTH; +const KEY_LENGTH: usize = 16; +const KEY_AGREEMENT_STRING: &str = "discovery v5 key agreement"; +const ID_SIGNATURE_TEXT: &str = "discovery v5 identity proof"; + +type Key = [u8; KEY_LENGTH]; + +/* Session key generation */ + +/// Generates session and auth-response keys for a nonce and remote ENR. This currently only +/// supports Secp256k1 signed ENR's. This returns four keys; initiator key, responder key, auth +/// response key and the ephemeral public key. +pub(crate) fn generate_session_keys( + local_id: &NodeId, + contact: &NodeContact, + challenge_data: &ChallengeData, +) -> Result<(Key, Key, Vec), Discv5Error> { + let (secret, ephem_pk) = { + match contact.public_key() { + CombinedPublicKey::Secp256k1(remote_pk) => { + let ephem_sk = k256::ecdsa::SigningKey::random(rand::thread_rng()); + let secret = ecdh(&remote_pk, &ephem_sk); + let ephem_pk = ephem_sk.verifying_key(); + (secret, ephem_pk.to_bytes().to_vec()) + } + CombinedPublicKey::Ed25519(_) => { + return Err(Discv5Error::KeyTypeNotSupported("Ed25519")) + } + } + }; + + let (initiator_key, recipient_key) = + derive_key(&secret, local_id, &contact.node_id(), challenge_data)?; + + Ok((initiator_key, recipient_key, ephem_pk)) +} + +fn derive_key( + secret: &[u8], + first_id: &NodeId, + second_id: &NodeId, + challenge_data: &ChallengeData, +) -> Result<(Key, Key), Discv5Error> { + let mut info = [0u8; INFO_LENGTH]; + info[0..26].copy_from_slice(KEY_AGREEMENT_STRING.as_bytes()); + info[26..26 + NODE_ID_LENGTH].copy_from_slice(&first_id.raw()); + info[26 + NODE_ID_LENGTH..].copy_from_slice(&second_id.raw()); + + let hk = Hkdf::::new(Some(challenge_data.as_ref()), secret); + + let mut okm = [0u8; 2 * KEY_LENGTH]; + hk.expand(&info, &mut okm) + .map_err(|_| Discv5Error::KeyDerivationFailed)?; + + let mut initiator_key: Key = Default::default(); + let mut recipient_key: Key = Default::default(); + initiator_key.copy_from_slice(&okm[0..KEY_LENGTH]); + recipient_key.copy_from_slice(&okm[KEY_LENGTH..2 * KEY_LENGTH]); + + Ok((initiator_key, recipient_key)) +} + +/// Derives the session keys for a public key type that matches the local keypair. +pub(crate) fn derive_keys_from_pubkey( + local_key: &CombinedKey, + local_id: &NodeId, + remote_id: &NodeId, + challenge_data: &ChallengeData, + ephem_pubkey: &[u8], +) -> Result<(Key, Key), Discv5Error> { + let secret = { + match local_key { + CombinedKey::Secp256k1(key) => { + // convert remote pubkey into secp256k1 public key + // the key type should match our own node record + let remote_pubkey = k256::ecdsa::VerifyingKey::from_sec1_bytes(ephem_pubkey) + .map_err(|_| Discv5Error::InvalidRemotePublicKey)?; + ecdh(&remote_pubkey, key) + } + CombinedKey::Ed25519(_) => return Err(Discv5Error::KeyTypeNotSupported("Ed25519")), + } + }; + + derive_key(&secret, remote_id, local_id, challenge_data) +} + +/* Nonce Signing */ + +/// Generates a signature of a nonce given a keypair. This prefixes the `NONCE_PREFIX` to the +/// signature. +pub(crate) fn sign_nonce( + signing_key: &CombinedKey, + challenge_data: &ChallengeData, + ephem_pubkey: &[u8], + dst_id: &NodeId, +) -> Result, Discv5Error> { + let signing_message = generate_signing_nonce(challenge_data, ephem_pubkey, dst_id); + + match signing_key { + CombinedKey::Secp256k1(key) => { + let message = Sha256::new().chain_update(signing_message); + let signature: Signature = key + .try_sign_digest(message) + .map_err(|e| Discv5Error::Error(format!("Failed to sign message: {}", e)))?; + Ok(signature.as_bytes().to_vec()) + } + CombinedKey::Ed25519(_) => Err(Discv5Error::KeyTypeNotSupported("Ed25519")), + } +} + +/// Verifies the authentication header nonce. +pub(crate) fn verify_authentication_nonce( + remote_pubkey: &CombinedPublicKey, + remote_ephem_pubkey: &[u8], + challenge_data: &ChallengeData, + dst_id: &NodeId, + sig: &[u8], +) -> bool { + let signing_nonce = generate_signing_nonce(challenge_data, remote_ephem_pubkey, dst_id); + + match remote_pubkey { + CombinedPublicKey::Secp256k1(key) => { + if let Ok(sig) = k256::ecdsa::Signature::try_from(sig) { + return key + .verify_digest(Sha256::new().chain_update(signing_nonce), &sig) + .is_ok(); + } + false + } + CombinedPublicKey::Ed25519(_) => { + // key not yet supported + false + } + } +} + +/// Builds the signature for a given challenge data. +/// +/// This takes the SHA256 hash of the nonce. +fn generate_signing_nonce( + challenge_data: &ChallengeData, + ephem_pubkey: &[u8], + dst_id: &NodeId, +) -> Vec { + let mut data = ID_SIGNATURE_TEXT.as_bytes().to_vec(); + data.extend_from_slice(challenge_data.as_ref()); + data.extend_from_slice(ephem_pubkey); + data.extend_from_slice(&dst_id.raw().to_vec()); + data +} + +/* Decryption related functions */ + +/// Decrypt messages that are post-fixed with an authenticated MAC. +pub(crate) fn decrypt_message( + key: &Key, + message_nonce: MessageNonce, + msg: &[u8], + aad: &[u8], +) -> Result, Discv5Error> { + if msg.len() < 16 { + return Err(Discv5Error::DecryptionFailed( + "Message not long enough to contain a MAC".into(), + )); + } + + let aead = Aes128Gcm::new(GenericArray::from_slice(key)); + let payload = Payload { msg, aad }; + aead.decrypt(GenericArray::from_slice(&message_nonce), payload) + .map_err(|e| Discv5Error::DecryptionFailed(e.to_string())) +} + +/* Encryption related functions */ + +/// A wrapper around the underlying default AES_GCM implementation. This may be abstracted in the +/// future. +pub(crate) fn encrypt_message( + key: &Key, + message_nonce: MessageNonce, + msg: &[u8], + aad: &[u8], +) -> Result, Discv5Error> { + let aead = Aes128Gcm::new(GenericArray::from_slice(key)); + let payload = Payload { msg, aad }; + aead.encrypt(GenericArray::from_slice(&message_nonce), payload) + .map_err(|e| Discv5Error::DecryptionFailed(e.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use enr::{CombinedKey, EnrBuilder, EnrKey}; + use std::convert::TryInto; + + fn hex_decode(x: &'static str) -> Vec { + hex::decode(x).unwrap() + } + + fn node_key_1() -> CombinedKey { + CombinedKey::secp256k1_from_bytes(&mut hex_decode( + "eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f", + )) + .unwrap() + } + + fn node_key_2() -> CombinedKey { + CombinedKey::secp256k1_from_bytes(&mut hex_decode( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628", + )) + .unwrap() + } + /* This section provides a series of reference tests for the encoding of packets */ + + #[test] + fn ref_test_ecdh() { + let remote_pubkey = + hex::decode("039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231") + .unwrap(); + let local_secret_key = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + let expected_secret = + hex::decode("033b11a2a1f214567e1537ce5e509ffd9b21373247f2a3ff6841f4976f53165e7e") + .unwrap(); + + let remote_pk = k256::ecdsa::VerifyingKey::from_sec1_bytes(&remote_pubkey).unwrap(); + let local_sk = k256::ecdsa::SigningKey::from_bytes(&local_secret_key).unwrap(); + + let secret = ecdh(&remote_pk, &local_sk); + assert_eq!(secret, expected_secret); + } + + #[test] + fn ref_key_derivation() { + let ephem_key = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + let dest_pubkey = + hex::decode("0317931e6e0840220642f230037d285d122bc59063221ef3226b1f403ddc69ca91") + .unwrap(); + + let remote_pk = k256::ecdsa::VerifyingKey::from_sec1_bytes(&dest_pubkey).unwrap(); + let local_sk = k256::ecdsa::SigningKey::from_bytes(&ephem_key).unwrap(); + + let secret = ecdh(&remote_pk, &local_sk); + + let first_node_id: NodeId = node_key_1().public().into(); + let second_node_id: NodeId = node_key_2().public().into(); + + let challenge_data: ChallengeData = hex::decode("000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000").unwrap().as_slice().try_into().unwrap(); + + let expected_first_key = hex::decode("dccc82d81bd610f4f76d3ebe97a40571").unwrap(); + let expected_second_key = hex::decode("ac74bb8773749920b0d3a8881c173ec5").unwrap(); + + let (first_key, second_key) = + derive_key(&secret, &first_node_id, &second_node_id, &challenge_data).unwrap(); + + assert_eq!(first_key.to_vec(), expected_first_key); + assert_eq!(second_key.to_vec(), expected_second_key); + } + + #[test] + fn ref_nonce_signing() { + let ephemeral_pubkey = + hex::decode("039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231") + .unwrap(); + let local_secret_key = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + let dst_id: NodeId = node_key_2().public().into(); + + println!("{}", dst_id); + + let expected_sig = hex::decode("94852a1e2318c4e5e9d422c98eaf19d1d90d876b29cd06ca7cb7546d0fff7b484fe86c09a064fe72bdbef73ba8e9c34df0cd2b53e9d65528c2c7f336d5dfc6e6").unwrap(); + + let challenge_data = ChallengeData::try_from(hex::decode("000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000").unwrap().as_slice()).unwrap(); + let key = k256::ecdsa::SigningKey::from_bytes(&local_secret_key).unwrap(); + let sig = sign_nonce(&key.into(), &challenge_data, &ephemeral_pubkey, &dst_id).unwrap(); + + assert_eq!(sig, expected_sig); + } + + #[test] + fn ref_encryption() { + let key_bytes = hex::decode("9f2d77db7004bf8a1a85107ac686990b").unwrap(); + let nonce_bytes = hex::decode("27b5af763c446acd2749fe8e").unwrap(); + let pt = hex::decode("01c20101").unwrap(); + let ad = hex::decode("93a7400fa0d6a694ebc24d5cf570f65d04215b6ac00757875e3f3a5f42107903") + .unwrap(); + let expected_ciphertext = hex::decode("a5d12a2d94b8ccb3ba55558229867dc13bfa3648").unwrap(); + + let mut key = [0u8; 16]; + key.copy_from_slice(&key_bytes); + let mut nonce = [0u8; 12]; + nonce.copy_from_slice(&nonce_bytes); + + let ciphertext = encrypt_message(&key, nonce, &pt, &ad).unwrap(); + + assert_eq!(ciphertext, expected_ciphertext); + } + + /* This section provides functionality testing */ + + #[test] + fn derive_symmetric_keys() { + let node1_key = CombinedKey::generate_secp256k1(); + let node2_key = CombinedKey::generate_secp256k1(); + + let node1_enr = EnrBuilder::new("v4").build(&node1_key).unwrap(); + let node2_enr = EnrBuilder::new("v4").build(&node2_key).unwrap(); + + let challenge_data = vec![1; 63]; + let challenge_data = ChallengeData::try_from(challenge_data.as_slice()).unwrap(); + + let (key1, key2, pk) = generate_session_keys( + &node1_enr.node_id(), + &node2_enr.clone().into(), + &challenge_data, + ) + .unwrap(); + let (key4, key5) = derive_keys_from_pubkey( + &node2_key, + &node2_enr.node_id(), + &node1_enr.node_id(), + &challenge_data, + &pk, + ) + .unwrap(); + + assert_eq!(key1, key4); + assert_eq!(key2, key5); + } + + #[test] + fn encrypt_decrypt() { + // aad + let aad: [u8; 12] = rand::random(); + let msg: Vec = vec![1, 2, 3, 4, 5, 6]; + let key: Key = rand::random(); + let nonce: MessageNonce = rand::random(); + + let cipher = encrypt_message(&key, nonce, &msg, &aad).unwrap(); + let plain_text = decrypt_message(&key, nonce, &cipher, &aad).unwrap(); + + assert_eq!(plain_text, msg); + } + + #[test] + fn decrypt_ref_test_ping() { + let dst_id: NodeId = node_key_2().public().into(); + let encoded_ref_packet = hex::decode("00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc").unwrap(); + let (_packet, auth_data) = + crate::packet::Packet::decode(&dst_id, &encoded_ref_packet).unwrap(); + + let ciphertext = hex::decode("b84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc").unwrap(); + let read_key = hex::decode("00000000000000000000000000000000").unwrap(); + let mut key = [0u8; 16]; + key.copy_from_slice(&read_key); + let byte_nonce = hex::decode("ffffffffffffffffffffffff").unwrap(); + let mut nonce = [0u8; 12]; + nonce.copy_from_slice(&byte_nonce); + + let message = decrypt_message(&key, nonce, &ciphertext, &auth_data).unwrap(); + dbg!(&message); + dbg!(hex::encode(&message)); + let rpc = crate::rpc::Message::decode(&message).unwrap(); + + println!("{}", rpc); + } +} diff --git a/version-meld/discv5/src/handler/hashmap_delay.rs b/version-meld/discv5/src/handler/hashmap_delay.rs new file mode 100644 index 0000000..3f369d9 --- /dev/null +++ b/version-meld/discv5/src/handler/hashmap_delay.rs @@ -0,0 +1,170 @@ +//! A simple hashmap object coupled with a `delay_queue` which has entries that expire after a +//! fixed time. +//! +//! A `HashMapDelay` implements `Stream` which removes expired items from the map. + +/// The default delay for entries, in seconds. This is only used when `insert()` is used to add +/// entries. +const DEFAULT_DELAY: u64 = 30; + +use futures::prelude::*; +use std::{ + collections::HashMap, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; +use tokio_util::time::delay_queue::{self, DelayQueue}; + +pub struct HashMapDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// The given entries. + entries: HashMap>, + /// A queue holding the timeouts of each entry. + expirations: DelayQueue, + /// The default expiration timeout of an entry. + default_entry_timeout: Duration, +} + +/// A wrapping around entries that adds the link to the entry's expiration, via a `delay_queue` key. +struct MapEntry { + /// The expiration key for the entry. + key: delay_queue::Key, + /// The actual entry. + value: V, +} + +impl Default for HashMapDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + fn default() -> Self { + HashMapDelay::new(Duration::from_secs(DEFAULT_DELAY)) + } +} + +impl HashMapDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// Creates a new instance of `HashMapDelay`. + pub fn new(default_entry_timeout: Duration) -> Self { + HashMapDelay { + entries: HashMap::new(), + expirations: DelayQueue::new(), + default_entry_timeout, + } + } + + /// Insert an entry into the mapping. Entries will expire after the `default_entry_timeout`. + pub fn insert(&mut self, key: K, value: V) { + self.insert_at(key, value, self.default_entry_timeout); + } + + /// Inserts an entry that will expire at a given instant. + pub fn insert_at(&mut self, key: K, value: V, entry_duration: Duration) { + if self.contains_key(&key) { + // update the timeout + self.update_timeout(&key, value, entry_duration); + } else { + let delay_key = self.expirations.insert(key.clone(), entry_duration); + let entry = MapEntry { + key: delay_key, + value, + }; + self.entries.insert(key, entry); + } + } + + /// Updates the timeout for a given key. Returns true if the key existed, false otherwise. + /// + /// Panics if the duration is too far in the future. + pub fn update_timeout(&mut self, key: &K, value: V, timeout: Duration) -> bool { + if let Some(entry) = self.entries.get_mut(key) { + entry.value = value; + self.expirations.reset(&entry.key, timeout); + true + } else { + false + } + } + + /// Gets a reference to an entry if it exists. + /// + /// Returns None if the entry does not exist. + pub fn get(&self, key: &K) -> Option<&V> { + self.entries.get(key).map(|entry| &entry.value) + } + + /// Gets a mutable reference to an entry if it exists. + /// + /// Returns None if the entry does not exist. + pub fn _get_mut(&mut self, key: &K) -> Option<&mut V> { + self.entries.get_mut(key).map(|entry| &mut entry.value) + } + + /// Returns true if the key exists, false otherwise. + pub fn contains_key(&self, key: &K) -> bool { + self.entries.contains_key(key) + } + + /// Returns the length of the mapping. + pub fn _len(&self) -> usize { + self.entries.len() + } + + /// Removes a key from the map returning the value associated with the key that was in the map. + /// + /// Return None if the key was not in the map. + pub fn remove(&mut self, key: &K) -> Option { + if let Some(entry) = self.entries.remove(key) { + self.expirations.remove(&entry.key); + return Some(entry.value); + } + None + } + + /// Retains only the elements specified by the predicate. + /// + /// In other words, remove all pairs `(k, v)` such that `f(&k,&mut v)` returns false. + pub fn _retain bool>(&mut self, mut f: F) { + let expiration = &mut self.expirations; + self.entries.retain(|key, entry| { + let result = f(key, &mut entry.value); + if !result { + expiration.remove(&entry.key); + } + result + }) + } + + /// Removes all entries from the map. + pub fn _clear(&mut self) { + self.entries.clear(); + self.expirations.clear(); + } +} + +impl Stream for HashMapDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, + V: Unpin, +{ + type Item = Result<(K, V), String>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.expirations.poll_expired(cx) { + Poll::Ready(Some(Ok(key))) => match self.entries.remove(key.get_ref()) { + Some(entry) => Poll::Ready(Some(Ok((key.into_inner(), entry.value)))), + None => Poll::Ready(Some(Err("Value no longer exists in expirations".into()))), + }, + Poll::Ready(Some(Err(e))) => { + Poll::Ready(Some(Err(format!("delay queue error: {:?}", e)))) + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/version-meld/discv5/src/handler/mod.rs b/version-meld/discv5/src/handler/mod.rs new file mode 100644 index 0000000..2aebd31 --- /dev/null +++ b/version-meld/discv5/src/handler/mod.rs @@ -0,0 +1,1143 @@ +//! Session and packet handling for the Discv5 Discovery service. +//! +//! The [`Handler`] is responsible for establishing and maintaining sessions with +//! connected/discovered nodes. Each node, identified by it's [`NodeId`] is associated with a +//! `Session`. This service drives the handshakes for establishing the sessions and associated +//! logic for sending/requesting initial connections/ENR's to/from unknown peers. +//! +//! The [`Handler`] also manages the timeouts for each request and reports back RPC failures, +//! and received messages. Messages are encrypted and decrypted using the +//! associated `Session` for each node. +//! +//! An ongoing established connection is abstractly represented by a `Session`. A node that provides an ENR with an +//! IP address/port that doesn't match the source, is considered invalid. A node that doesn't know +//! their external contactable addresses should set their ENR IP field to `None`. +//! +//! The Handler also routinely checks the timeouts for banned nodes and removes them from the +//! banned list once their ban expires. +//! +//! # Usage +//! +//! Interacting with a handler is done via channels. A Handler is spawned using the [`Handler::spawn`] +//! function. This returns an exit channel, a sending and receiving channel respectively. If the +//! exit channel is dropped or fired, the handler task gets shutdown. +//! +//! Requests to the handler can be made via the sending channel using a [`HandlerRequest`]. +//! Responses come by the receiving channel in the form of a [`HandlerResponse`]. +use crate::{ + config::Discv5Config, + discv5::PERMIT_BAN_LIST, + error::{Discv5Error, RequestError}, + packet::{ChallengeData, IdNonce, MessageNonce, Packet, PacketKind}, + rpc::{Message, Request, RequestBody, RequestId, Response, ResponseBody}, + socket, + socket::{FilterConfig, Socket}, + Enr, +}; +use enr::{CombinedKey, NodeId}; +use futures::prelude::*; +use parking_lot::RwLock; +use std::{ + collections::HashMap, + convert::TryFrom, + default::Default, + net::SocketAddr, + sync::{atomic::Ordering, Arc}, + time::{Duration, Instant}, +}; +use tokio::sync::{mpsc, oneshot}; +use tracing::{debug, error, trace, warn}; + +mod crypto; +mod hashmap_delay; +mod session; +mod tests; + +pub use crate::node_info::{NodeAddress, NodeContact}; + +use crate::metrics::METRICS; + +use crate::lru_time_cache::LruTimeCache; +use hashmap_delay::HashMapDelay; +use session::Session; + +// The time interval to check banned peer timeouts and unban peers when the timeout has elapsed (in +// seconds). +const BANNED_NODES_CHECK: u64 = 300; // Check every 5 minutes. + +/// Events sent to the handler to be executed. +#[derive(Debug, Clone, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum HandlerRequest { + /// Sends a `Request` to a `NodeContact`. A `NodeContact` is an abstract type + /// that allows for either an ENR to be sent or a `Raw` type which represents an `SocketAddr`, + /// `PublicKey` and `NodeId`. This type can be created from MultiAddrs and MultiAddr strings + /// for some keys. + /// + /// This permits us to send messages to nodes without knowing their ENR. In this case their ENR + /// will be requested during the handshake. + /// + /// A Request is flagged and permits responses through the packet filter. + /// + /// Note: To update an ENR for an unknown node, we request a FINDNODE with distance 0 to the + /// `NodeContact` we know of. + Request(NodeContact, Box), + + /// Send a response to a received request to a particular node. + /// + /// The handler does not keep state of requests, so the application layer must send the + /// response back to the `NodeAddress` from which the request was received. + Response(NodeAddress, Box), + + /// A Random packet has been received and we have requested the application layer to inform + /// us what the highest known ENR is for this node. + /// The `WhoAreYouRef` is sent out in the `HandlerResponse::WhoAreYou` event and should + /// be returned here to submit the application's response. + WhoAreYou(WhoAreYouRef, Option), +} + +/// The outputs provided by the `Handler`. +#[derive(Debug, Clone, PartialEq)] +pub enum HandlerResponse { + /// A session has been established with a node. + /// + /// A session is only considered established once we have received a signed ENR from the + /// node and received messages from it's `SocketAddr` matching it's ENR fields. + Established(Enr, ConnectionDirection), + + /// A Request has been received. + Request(NodeAddress, Box), + + /// A Response has been received. + Response(NodeAddress, Box), + + /// An unknown source has requested information from us. Return the reference with the known + /// ENR of this node (if known). See the `HandlerRequest::WhoAreYou` variant. + WhoAreYou(WhoAreYouRef), + + /// An RPC request failed. + /// + /// This returns the request ID and an error indicating why the request failed. + RequestFailed(RequestId, RequestError), +} + +/// How we connected to the node. +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum ConnectionDirection { + /// The node contacted us. + Incoming, + /// We contacted the node. + Outgoing, +} + +/// A reference for the application layer to send back when the handler requests any known +/// ENR for the NodeContact. +#[derive(Debug, Clone, PartialEq)] +pub struct WhoAreYouRef(pub NodeAddress, MessageNonce); + +#[derive(Debug)] +/// A Challenge (WHOAREYOU) object used to handle and send WHOAREYOU requests. +pub struct Challenge { + /// The challenge data received from the node. + data: ChallengeData, + /// The remote's ENR if we know it. We can receive a challenge from an unknown node. + remote_enr: Option, +} + +/// A request to a node that we are waiting for a response. +#[derive(Debug)] +pub(crate) struct RequestCall { + contact: NodeContact, + /// The raw discv5 packet sent. + packet: Packet, + /// The unencrypted message. Required if need to re-encrypt and re-send. + request: Request, + /// Handshakes attempted. + handshake_sent: bool, + /// The number of times this request has been re-sent. + retries: u8, + /// If we receive a Nodes Response with a total greater than 1. This keeps track of the + /// remaining responses expected. + remaining_responses: Option, + /// Signifies if we are initiating the session with a random packet. This is only used to + /// determine the connection direction of the session. + initiating_session: bool, +} + +impl RequestCall { + fn new( + contact: NodeContact, + packet: Packet, + request: Request, + initiating_session: bool, + ) -> Self { + RequestCall { + contact, + packet, + request, + handshake_sent: false, + retries: 1, + remaining_responses: None, + initiating_session, + } + } + + fn id(&self) -> &RequestId { + &self.request.id + } +} + +/// Process to handle handshakes and sessions established from raw RPC communications between nodes. +pub struct Handler { + /// Configuration for the discv5 service. + request_retries: u8, + /// The local node id to save unnecessary read locks on the ENR. The NodeID should not change + /// during the operation of the server. + node_id: NodeId, + /// The local ENR. + enr: Arc>, + /// The key to sign the ENR and set up encrypted communication with peers. + key: Arc>, + /// Pending raw requests. A list of raw messages we are awaiting a response from the remote. + active_requests: HashMapDelay, + // WHOAREYOU messages do not include the source node id. We therefore maintain another + // mapping of active_requests via message_nonce. This allows us to match WHOAREYOU + // requests with active requests sent. + /// A mapping of all pending active raw requests message nonces to their NodeAddress. + active_requests_nonce_mapping: HashMap, + /// The expected responses by SocketAddr which allows packets to pass the underlying filter. + filter_expected_responses: Arc>>, + /// Requests awaiting a handshake completion. + pending_requests: HashMap>, + /// Currently in-progress handshakes with peers. + active_challenges: LruTimeCache, + /// Established sessions with peers. + sessions: LruTimeCache, + /// The channel that receives requests from the application layer. + inbound_channel: mpsc::UnboundedReceiver, + /// The channel to send responses to the application layer. + outbound_channel: mpsc::Sender, + /// The listening socket to filter out any attempted requests to self. + listen_socket: SocketAddr, + /// The discovery v5 UDP socket tasks. + socket: Socket, + /// Exit channel to shutdown the handler. + exit: oneshot::Receiver<()>, +} + +type HandlerReturn = ( + oneshot::Sender<()>, + mpsc::UnboundedSender, + mpsc::Receiver, +); +impl Handler { + /// A new Session service which instantiates the UDP socket send/recv tasks. + pub async fn spawn( + enr: Arc>, + key: Arc>, + listen_socket: SocketAddr, + config: Discv5Config, + ) -> Result { + let (exit_sender, exit) = oneshot::channel(); + // create the channels to send/receive messages from the application + let (inbound_send, inbound_channel) = mpsc::unbounded_channel(); + let (outbound_channel, outbound_recv) = mpsc::channel(50); + + // Creates a SocketConfig to pass to the underlying UDP socket tasks. + + // Lets the underlying filter know that we are expecting a packet from this source. + let filter_expected_responses = Arc::new(RwLock::new(HashMap::new())); + + // The local node id + let node_id = enr.read().node_id(); + + // enable the packet filter if required + + let filter_config = FilterConfig { + enabled: config.enable_packet_filter, + rate_limiter: config.filter_rate_limiter.clone(), + max_nodes_per_ip: config.filter_max_nodes_per_ip, + max_bans_per_ip: config.filter_max_bans_per_ip, + }; + + let socket_config = socket::SocketConfig { + executor: config.executor.clone().expect("Executor must exist"), + socket_addr: listen_socket, + filter_config, + local_node_id: node_id, + expected_responses: filter_expected_responses.clone(), + ban_duration: config.ban_duration, + }; + + // Attempt to bind to the socket before spinning up the send/recv tasks. + let socket = socket::Socket::new_socket(&socket_config.socket_addr).await?; + + config + .executor + .clone() + .expect("Executor must be present") + .spawn(Box::pin(async move { + let socket = match socket::Socket::new(socket, socket_config) { + Ok(v) => v, + Err(e) => { + error!("Could not bind UDP socket. {}", e); + return; + } + }; + + let mut handler = Handler { + request_retries: config.request_retries, + node_id, + enr, + key, + active_requests: HashMapDelay::new(config.request_timeout), + active_requests_nonce_mapping: HashMap::new(), + pending_requests: HashMap::new(), + filter_expected_responses, + sessions: LruTimeCache::new( + config.session_timeout, + Some(config.session_cache_capacity), + ), + active_challenges: LruTimeCache::new(config.request_timeout * 2, None), + inbound_channel, + outbound_channel, + listen_socket, + socket, + exit, + }; + debug!("Handler Starting"); + handler.start().await; + })); + + Ok((exit_sender, inbound_send, outbound_recv)) + } + + /// The main execution loop for the handler. + async fn start(&mut self) { + let mut banned_nodes_check = tokio::time::interval(Duration::from_secs(BANNED_NODES_CHECK)); + + loop { + tokio::select! { + Some(handler_request) = self.inbound_channel.recv() => { + match handler_request { + HandlerRequest::Request(contact, request) => { + let id = request.id.clone(); + if let Err(request_error) = self.send_request(contact, *request).await { + // If the sending failed report to the application + let _ = self.outbound_channel.send(HandlerResponse::RequestFailed(id, request_error)).await; + } + } + HandlerRequest::Response(dst, response) => self.send_response(dst, *response).await, + HandlerRequest::WhoAreYou(wru_ref, enr) => self.send_challenge(wru_ref, enr).await, + } + } + Some(inbound_packet) = self.socket.recv.recv() => { + self.process_inbound_packet(inbound_packet).await; + } + Some(Ok((node_address, pending_request))) = self.active_requests.next() => { + self.handle_request_timeout(node_address, pending_request).await; + } + _ = banned_nodes_check.tick() => self.unban_nodes_check(), // Unban nodes that are past the timeout + _ = &mut self.exit => { + return; + } + } + } + } + + /// Processes an inbound decoded packet. + async fn process_inbound_packet(&mut self, inbound_packet: socket::InboundPacket) { + let message_nonce = inbound_packet.header.message_nonce; + match inbound_packet.header.kind { + PacketKind::WhoAreYou { enr_seq, .. } => { + let challenge_data = + ChallengeData::try_from(inbound_packet.authenticated_data.as_slice()) + .expect("Must be correct size"); + self.handle_challenge( + inbound_packet.src_address, + message_nonce, + enr_seq, + challenge_data, + ) + .await + } + PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + } => { + let node_address = NodeAddress { + socket_addr: inbound_packet.src_address, + node_id: src_id, + }; + self.handle_auth_message( + node_address, + message_nonce, + &id_nonce_sig, + &ephem_pubkey, + enr_record, + &inbound_packet.message, + &inbound_packet.authenticated_data, // This is required for authenticated data in decryption. + ) + .await + } + PacketKind::Message { src_id } => { + let node_address = NodeAddress { + socket_addr: inbound_packet.src_address, + node_id: src_id, + }; + self.handle_message( + node_address, + message_nonce, + &inbound_packet.message, + &inbound_packet.authenticated_data, + ) + .await + } + } + } + + fn remove_expected_response(&mut self, socket_addr: SocketAddr) { + if let std::collections::hash_map::Entry::Occupied(mut entry) = + self.filter_expected_responses.write().entry(socket_addr) + { + let count = entry.get_mut(); + *count = count.saturating_sub(1); + if count == &0 { + entry.remove(); + } + } + } + + fn add_expected_response(&mut self, socket_addr: SocketAddr) { + *self + .filter_expected_responses + .write() + .entry(socket_addr) + .or_default() += 1; + } + + /// A request has timed out. + async fn handle_request_timeout( + &mut self, + node_address: NodeAddress, + mut request_call: RequestCall, + ) { + if request_call.retries >= self.request_retries { + trace!("Request timed out with {}", node_address); + // Remove the request from the awaiting packet_filter + // Remove the associated nonce mapping. + self.active_requests_nonce_mapping + .remove(request_call.packet.message_nonce()); + self.remove_expected_response(node_address.socket_addr); + // The request has timed out. We keep any established session for future use. + self.fail_request(request_call, RequestError::Timeout, false) + .await; + } else { + // increment the request retry count and restart the timeout + trace!( + "Resending message: {} to {}", + request_call.request, + node_address + ); + self.send(node_address.clone(), request_call.packet.clone()) + .await; + request_call.retries += 1; + self.active_requests.insert(node_address, request_call); + } + } + + /// Sends a `Request` to a node. + async fn send_request( + &mut self, + contact: NodeContact, + request: Request, + ) -> Result<(), RequestError> { + let node_address = contact + .node_address() + .map_err(|e| RequestError::InvalidEnr(e.into()))?; + + if node_address.socket_addr == self.listen_socket { + debug!("Filtered request to self"); + return Err(RequestError::SelfRequest); + } + + // If there is already an active request for this node, add to pending requests + if self.active_requests.get(&node_address).is_some() { + trace!("Request queued for node: {}", node_address); + self.pending_requests + .entry(node_address) + .or_insert_with(Vec::new) + .push((contact, request)); + return Ok(()); + } + + let (packet, initiating_session) = { + if let Some(session) = self.sessions.get_mut(&node_address) { + // Encrypt the message and send + let packet = session + .encrypt_message(self.node_id, &request.clone().encode()) + .map_err(|e| RequestError::EncryptionFailed(format!("{:?}", e)))?; + (packet, false) + } else { + // No session exists, start a new handshake + trace!( + "Starting session. Sending random packet to: {}", + node_address + ); + let packet = + Packet::new_random(&self.node_id).map_err(RequestError::EntropyFailure)?; + // We are initiating a new session + (packet, true) + } + }; + + let call = RequestCall::new(contact, packet.clone(), request, initiating_session); + // let the filter know we are expecting a response + self.add_expected_response(node_address.socket_addr); + let nonce = *packet.message_nonce(); + self.send(node_address.clone(), packet).await; + + self.active_requests_nonce_mapping + .insert(nonce, node_address.clone()); + self.active_requests.insert(node_address, call); + Ok(()) + } + + /// Sends an RPC Response. + async fn send_response(&mut self, node_address: NodeAddress, response: Response) { + // Check for an established session + if let Some(session) = self.sessions.get_mut(&node_address) { + // Encrypt the message and send + let packet = match session.encrypt_message(self.node_id, &response.encode()) { + Ok(packet) => packet, + Err(e) => { + warn!("Could not encrypt response: {:?}", e); + return; + } + }; + self.send(node_address, packet).await; + } else { + // Either the session is being established or has expired. We simply drop the + // response in this case. + warn!( + "Session is not established. Dropping response {} for node: {}", + response, node_address.node_id + ); + } + } + + /// This is called in response to a `HandlerResponse::WhoAreYou` event. The applications finds the + /// highest known ENR for a node then we respond to the node with a WHOAREYOU packet. + async fn send_challenge(&mut self, wru_ref: WhoAreYouRef, remote_enr: Option) { + let node_address = wru_ref.0; + let message_nonce = wru_ref.1; + + if self.active_challenges.peek(&node_address).is_some() { + warn!("WHOAREYOU already sent. {}", node_address); + return; + } + + // Ignore this request if the session is already established + if self.sessions.get(&node_address).is_some() { + trace!( + "Session already established. WHOAREYOU not sent to {}", + node_address + ); + return; + } + + // It could be the case we have sent an ENR with an active request, however we consider + // these independent as this is in response to an unknown packet. If the ENR it not in our + // table (remote_enr is None) then we re-request the ENR to keep the session up to date. + + // send the challenge + let enr_seq = remote_enr.clone().map_or_else(|| 0, |enr| enr.seq()); + let id_nonce: IdNonce = rand::random(); + let packet = Packet::new_whoareyou(message_nonce, id_nonce, enr_seq); + let challenge_data = ChallengeData::try_from(packet.authenticated_data().as_slice()) + .expect("Must be the correct challenge size"); + debug!("Sending WHOAREYOU to {}", node_address); + self.send(node_address.clone(), packet).await; + self.active_challenges.insert( + node_address, + Challenge { + data: challenge_data, + remote_enr, + }, + ); + } + + /* Packet Handling */ + + /// Handles a WHOAREYOU packet that was received from the network. + async fn handle_challenge( + &mut self, + src_address: SocketAddr, + request_nonce: MessageNonce, + enr_seq: u64, + challenge_data: ChallengeData, + ) { + // Check that this challenge matches a known active request. + // If this message passes all the requisite checks, a request call is returned. + let mut request_call = { + // Check for an active request + let node_address = match self.active_requests_nonce_mapping.remove(&request_nonce) { + Some(addr) => addr, + None => { + trace!("Received a WHOAREYOU packet that references an unknown or expired request. Source {}, message_nonce {}", src_address, hex::encode(request_nonce)); + return; + } + }; + + // Verify that the src_addresses match + if node_address.socket_addr != src_address { + trace!("Received a WHOAREYOU packet for a message with a non-expected source. Source {}, expected_source: {} message_nonce {}", src_address, node_address.socket_addr, hex::encode(request_nonce)); + // add the mapping back + self.active_requests_nonce_mapping + .insert(request_nonce, node_address); + return; + } + + // Obtain the request from the mapping. This must exist, otherwise there is a + // serious coding error. The active_requests_nonce_mapping and active_requests + // mappings should be 1 to 1. + + match self.active_requests.remove(&node_address) { + Some(request_call) => request_call, + None => { + error!("Active request mappings are not in sync. Message_id {}, node_address {} doesn't exist in active request mapping", hex::encode(request_nonce), node_address); + // NOTE: Both mappings are removed in this case. + return; + } + } + }; + + // double check the message nonces match + if request_call.packet.message_nonce() != &request_nonce { + // This could theoretically happen if a peer uses the same node id across + // different connections. + warn!("Received a WHOAREYOU from a non expected source. Source: {}, message_nonce {} , expected_nonce: {}", request_call.contact, hex::encode(request_call.packet.message_nonce()), hex::encode(request_nonce)); + // NOTE: Both mappings are removed in this case. + return; + } + + trace!( + "Received a WHOAREYOU packet response. Source: {}", + request_call.contact + ); + + // We do not allow multiple WHOAREYOU packets for a single challenge request. If we have + // already sent a WHOAREYOU ourselves, we drop sessions who send us a WHOAREYOU in + // response. + if request_call.handshake_sent { + warn!( + "Authentication response already sent. Dropping session. Node: {}", + request_call.contact + ); + self.fail_request(request_call, RequestError::InvalidRemotePacket, true) + .await; + return; + } + + // Encrypt the message with an auth header and respond + + // First if a new version of our ENR is requested, obtain it for the header + let updated_enr = if enr_seq < self.enr.read().seq() { + Some(self.enr.read().clone()) + } else { + None + }; + + // Generate a new session and authentication packet + let (auth_packet, mut session) = match Session::encrypt_with_header( + &request_call.contact, + self.key.clone(), + updated_enr, + &self.node_id, + &challenge_data, + &(request_call.request.clone().encode()), + ) { + Ok(v) => v, + Err(e) => { + error!("Could not generate a session. Error: {:?}", e); + self.fail_request(request_call, RequestError::InvalidRemotePacket, true) + .await; + return; + } + }; + + // There are two quirks with an established session at this point. + // 1. We may not know the ENR if we dialed this node with a NodeContact::Raw. In this case + // we need to set up a request to find the ENR and wait for a response before we + // officially call this node established. + // 2. The challenge here could be to an already established session. If so, we need to + // update the existing session to attempt to decrypt future messages with the new keys + // and update the keys internally upon successful decryption. + // + // We handle both of these cases here. + + // Check if we know the ENR, if not request it and flag the session as awaiting an ENR. + // + // All sent requests must have an associated node_id. Therefore the following + // must not panic. + let node_address = request_call + .contact + .node_address() + .expect("All sent requests must have a node address"); + match request_call.contact.clone() { + NodeContact::Enr(enr) => { + // NOTE: Here we decide if the session is outgoing or ingoing. The condition for an + // outgoing session is that we originally sent a RANDOM packet (signifying we did + // not have a session for a request) and the packet is not a PING (we are not + // trying to update an old session that may have expired. + let connection_direction = { + match (&request_call.initiating_session, &request_call.request.body) { + (true, RequestBody::Ping { .. }) => ConnectionDirection::Incoming, + (true, _) => ConnectionDirection::Outgoing, + (false, _) => ConnectionDirection::Incoming, + } + }; + + // We already know the ENR. Send the handshake response packet + trace!("Sending Authentication response to node: {}", node_address); + request_call.packet = auth_packet.clone(); + request_call.handshake_sent = true; + request_call.initiating_session = false; + // Reinsert the request_call + self.insert_active_request(request_call); + // Send the actual packet to the send task. + self.send(node_address.clone(), auth_packet).await; + + // Notify the application that the session has been established + self.outbound_channel + .send(HandlerResponse::Established(*enr, connection_direction)) + .await + .unwrap_or_else(|e| warn!("Error with sending channel: {}", e)); + } + NodeContact::Raw { .. } => { + // Don't know the ENR. Establish the session, but request an ENR also + + // Send the Auth response + let contact = request_call.contact.clone(); + trace!( + "Sending Authentication response to node: {}", + request_call + .contact + .node_address() + .expect("Sanitized contact") + ); + request_call.packet = auth_packet.clone(); + request_call.handshake_sent = true; + // Reinsert the request_call + self.insert_active_request(request_call); + self.send(node_address.clone(), auth_packet).await; + + let id = RequestId::random(); + let request = Request { + id: id.clone(), + body: RequestBody::FindNode { distances: vec![0] }, + }; + + session.awaiting_enr = Some(id); + let _ = self.send_request(contact, request).await; + } + } + self.new_session(node_address, session); + } + + /// Verifies a Node ENR to it's observed address. If it fails, any associated session is also + /// considered failed. If it succeeds, we notify the application. + fn verify_enr(&self, enr: &Enr, node_address: &NodeAddress) -> bool { + // If the ENR does not match the observed IP addresses, we consider the Session + // failed. + enr.node_id() == node_address.node_id + && (enr.udp_socket().is_none() || enr.udp_socket() == Some(node_address.socket_addr)) + } + + /// Handle a message that contains an authentication header. + #[allow(clippy::too_many_arguments)] + async fn handle_auth_message( + &mut self, + node_address: NodeAddress, + message_nonce: MessageNonce, + id_nonce_sig: &[u8], + ephem_pubkey: &[u8], + enr_record: Option, + message: &[u8], + authenticated_data: &[u8], + ) { + // Needs to match an outgoing challenge packet (so we have the required nonce to be signed). If it doesn't we drop the packet. + // This will lead to future outgoing challenges if they proceed to send further encrypted + // packets. + trace!( + "Received an Authentication header message from: {}", + node_address + ); + + if let Some(challenge) = self.active_challenges.remove(&node_address) { + match Session::establish_from_challenge( + self.key.clone(), + &self.node_id, + &node_address.node_id, + challenge, + id_nonce_sig, + ephem_pubkey, + enr_record, + ) { + Ok((session, enr)) => { + // Receiving an AuthResponse must give us an up-to-date view of the node ENR. + // Verify the ENR is valid + if self.verify_enr(&enr, &node_address) { + // Session is valid + // Notify the application + // The session established here are from WHOAREYOU packets that we sent. + // This occurs when a node established a connection with us. + let _ = self + .outbound_channel + .send(HandlerResponse::Established( + enr, + ConnectionDirection::Incoming, + )) + .await; + self.new_session(node_address.clone(), session); + self.handle_message( + node_address, + message_nonce, + message, + authenticated_data, + ) + .await; + } else { + // IP's or NodeAddress don't match. Drop the session. + warn!( + "Session has invalid ENR. Enr socket: {:?}, {}", + enr.udp_socket(), + node_address + ); + self.fail_session(&node_address, RequestError::InvalidRemoteEnr, true) + .await; + } + } + Err(Discv5Error::InvalidChallengeSignature(challenge)) => { + warn!( + "Authentication header contained invalid signature. Ignoring packet from: {}", + node_address + ); + // insert back the challenge + self.active_challenges.insert(node_address, challenge); + } + Err(e) => { + warn!( + "Invalid Authentication header. Dropping session. Error: {:?}", + e + ); + self.fail_session(&node_address, RequestError::InvalidRemotePacket, true) + .await; + } + } + } else { + warn!( + "Received an authenticated header without a matching WHOAREYOU request. {}", + node_address + ); + } + } + + async fn send_next_request(&mut self, node_address: NodeAddress) { + // ensure we are not over writing any existing requests + + if self.active_requests.get(&node_address).is_none() { + if let std::collections::hash_map::Entry::Occupied(mut entry) = + self.pending_requests.entry(node_address) + { + // If it exists, there must be a request here + let request = entry.get_mut().remove(0); + if entry.get().is_empty() { + entry.remove(); + } + trace!("Sending next awaiting message. Node: {}", request.0); + let _ = self.send_request(request.0, request.1).await; + } + } + } + + /// Handle a standard message that does not contain an authentication header. + #[allow(clippy::single_match)] + async fn handle_message( + &mut self, + node_address: NodeAddress, + message_nonce: MessageNonce, + message: &[u8], + authenticated_data: &[u8], + ) { + // check if we have an available session + if let Some(session) = self.sessions.get_mut(&node_address) { + // attempt to decrypt and process the message. + let message = match session.decrypt_message(message_nonce, message, authenticated_data) + { + Ok(m) => match Message::decode(&m) { + Ok(p) => p, + Err(e) => { + warn!("Failed to decode message. Error: {:?}, {}", e, node_address); + return; + } + }, + Err(e) => { + // We have a session, but the message could not be decrypted. It is likely the node + // sending this message has dropped their session. In this case, this message is a + // Random packet and we should reply with a WHOAREYOU. + // This means we need to drop the current session and re-establish. + trace!("Decryption failed. Error {}", e); + debug!( + "Message from node: {} is not encrypted with known session keys.", + node_address + ); + self.fail_session(&node_address, RequestError::InvalidRemotePacket, true) + .await; + // If we haven't already sent a WhoAreYou, + // spawn a WHOAREYOU event to check for highest known ENR + // Update the cache time and remove expired entries. + if self.active_challenges.peek(&node_address).is_none() { + let whoareyou_ref = WhoAreYouRef(node_address, message_nonce); + let _ = self + .outbound_channel + .send(HandlerResponse::WhoAreYou(whoareyou_ref)) + .await; + } else { + trace!("WHOAREYOU packet already sent: {}", node_address); + } + return; + } + }; + + trace!("Received message from: {}", node_address); + + // Remove any associated request from pending_request + match message { + Message::Request(request) => { + // report the request to the application + let _ = self + .outbound_channel + .send(HandlerResponse::Request(node_address, Box::new(request))) + .await; + } + Message::Response(response) => { + // Sessions could be awaiting an ENR response. Check if this response matches + // these + if let Some(request_id) = session.awaiting_enr.as_ref() { + if &response.id == request_id { + session.awaiting_enr = None; + match response.body { + ResponseBody::Nodes { mut nodes, .. } => { + // Received the requested ENR + if let Some(enr) = nodes.pop() { + if self.verify_enr(&enr, &node_address) { + // Notify the application + // This can occur when we try to dial a node without an + // ENR. In this case we have attempted to establish the + // connection, so this is an outgoing connection. + let _ = self + .outbound_channel + .send(HandlerResponse::Established( + enr, + ConnectionDirection::Outgoing, + )) + .await; + return; + } + } + } + _ => {} + } + debug!("Session failed invalid ENR response"); + self.fail_session(&node_address, RequestError::InvalidRemoteEnr, true) + .await; + return; + } + } + // Handle standard responses + self.handle_response(node_address, response).await; + } + } + } else { + // no session exists + trace!("Received a message without a session. {}", node_address); + trace!("Requesting a WHOAREYOU packet to be sent."); + // spawn a WHOAREYOU event to check for highest known ENR + let whoareyou_ref = WhoAreYouRef(node_address, message_nonce); + let _ = self + .outbound_channel + .send(HandlerResponse::WhoAreYou(whoareyou_ref)) + .await; + } + } + + /// Handles a response to a request. Re-inserts the request call if the response is a multiple + /// Nodes response. + async fn handle_response(&mut self, node_address: NodeAddress, response: Response) { + // Find a matching request, if any + if let Some(mut request_call) = self.active_requests.remove(&node_address) { + if request_call.id() != &response.id { + trace!( + "Received an RPC Response to an unknown request. Likely late response. {}", + node_address + ); + // add the request back and reset the timer + self.active_requests.insert(node_address, request_call); + return; + } + + // The response matches a request + + // Check to see if this is a Nodes response, in which case we may require to wait for + // extra responses + if let ResponseBody::Nodes { total, .. } = response.body { + if total > 1 { + // This is a multi-response Nodes response + if let Some(remaining_responses) = request_call.remaining_responses.as_mut() { + *remaining_responses -= 1; + if remaining_responses != &0 { + // more responses remaining, add back the request and send the response + // add back the request and send the response + self.active_requests + .insert(node_address.clone(), request_call); + let _ = self + .outbound_channel + .send(HandlerResponse::Response(node_address, Box::new(response))) + .await; + return; + } + } else { + // This is the first instance + request_call.remaining_responses = Some(total - 1); + // add back the request and send the response + self.active_requests + .insert(node_address.clone(), request_call); + let _ = self + .outbound_channel + .send(HandlerResponse::Response(node_address, Box::new(response))) + .await; + return; + } + } + } + + // Remove the associated nonce mapping. + self.active_requests_nonce_mapping + .remove(request_call.packet.message_nonce()); + // Remove the expected response + self.remove_expected_response(node_address.socket_addr); + + // The request matches report the response + let _ = self + .outbound_channel + .send(HandlerResponse::Response( + node_address.clone(), + Box::new(response), + )) + .await; + self.send_next_request(node_address).await; + } else { + // This is likely a late response and we have already failed the request. These get + // dropped here. + trace!("Late response from node: {}", node_address); + } + } + + /// Inserts a request and associated auth_tag mapping. + fn insert_active_request(&mut self, request_call: RequestCall) { + let node_address = request_call + .contact + .node_address() + .expect("Can only add requests with a valid destination"); + // adds the mapping of message nonce to node address + self.active_requests_nonce_mapping + .insert(*request_call.packet.message_nonce(), node_address.clone()); + self.active_requests.insert(node_address, request_call); + } + + fn new_session(&mut self, node_address: NodeAddress, session: Session) { + if let Some(current_session) = self.sessions.get_mut(&node_address) { + current_session.update(session); + } else { + self.sessions.insert(node_address, session); + METRICS + .active_sessions + .store(self.sessions.len(), Ordering::Relaxed); + } + } + + /// A request has failed. + async fn fail_request( + &mut self, + request_call: RequestCall, + error: RequestError, + remove_session: bool, + ) { + // The Request has expired, remove the session. + // Remove the associated nonce mapping. + self.active_requests_nonce_mapping + .remove(request_call.packet.message_nonce()); + // Fail the current request + let request_id = request_call.request.id; + let _ = self + .outbound_channel + .send(HandlerResponse::RequestFailed(request_id, error.clone())) + .await; + + let node_address = request_call + .contact + .node_address() + .expect("All Request calls have been sanitized"); + self.fail_session(&node_address, error, remove_session) + .await; + } + + /// Removes a session and updates associated metrics and fields. + async fn fail_session( + &mut self, + node_address: &NodeAddress, + error: RequestError, + remove_session: bool, + ) { + if remove_session { + self.sessions.remove(node_address); + METRICS + .active_sessions + .store(self.sessions.len(), Ordering::Relaxed); + } + for request in self + .pending_requests + .remove(node_address) + .unwrap_or_else(Vec::new) + { + let _ = self + .outbound_channel + .send(HandlerResponse::RequestFailed(request.1.id, error.clone())) + .await; + } + } + + /// Sends a packet to the send handler to be encoded and sent. + async fn send(&mut self, node_address: NodeAddress, packet: Packet) { + let outbound_packet = socket::OutboundPacket { + node_address, + packet, + }; + let _ = self.socket.send.send(outbound_packet).await; + } + + /// Check if any banned nodes have served their time and unban them. + fn unban_nodes_check(&self) { + PERMIT_BAN_LIST + .write() + .ban_ips + .retain(|_, time| time.is_none() || Some(Instant::now()) < *time); + PERMIT_BAN_LIST + .write() + .ban_nodes + .retain(|_, time| time.is_none() || Some(Instant::now()) < *time); + } +} diff --git a/version-meld/discv5/src/handler/session.rs b/version-meld/discv5/src/handler/session.rs new file mode 100644 index 0000000..c7c3ec3 --- /dev/null +++ b/version-meld/discv5/src/handler/session.rs @@ -0,0 +1,253 @@ +use super::*; +use crate::{ + node_info::NodeContact, + packet::{ChallengeData, Packet, PacketHeader, PacketKind, MESSAGE_NONCE_LENGTH}, +}; +use enr::{CombinedKey, NodeId}; +use zeroize::Zeroize; + +#[derive(Zeroize, PartialEq)] +pub(crate) struct Keys { + /// The encryption key. + encryption_key: [u8; 16], + /// The decryption key. + decryption_key: [u8; 16], +} + +/// A Session containing the encryption/decryption keys. These are kept individually for a given +/// node. +pub(crate) struct Session { + /// The current keys used to encrypt/decrypt messages. + keys: Keys, + /// If a new handshake is being established, these keys can be tried to determine if this new + /// set of keys is canon. + awaiting_keys: Option, + /// If we contacted this node without an ENR, i.e. via a multiaddr, during the session + /// establishment we request the nodes ENR. Once the ENR is received and verified, this session + /// becomes established. + /// + /// This field holds the request_id associated with the ENR request. + pub awaiting_enr: Option, + /// Number of messages sent. Used to ensure the nonce used in message encryption is always + /// unique. + counter: u32, +} + +impl Session { + pub fn new(keys: Keys) -> Self { + Session { + keys, + awaiting_keys: None, + awaiting_enr: None, + counter: 0, + } + } + + /// A new session has been established. Update this session based on the new session. + pub fn update(&mut self, new_session: Session) { + // Await the new sessions keys + self.awaiting_keys = Some(new_session.keys); + self.awaiting_enr = new_session.awaiting_enr; + } + + /// Uses the current `Session` to encrypt a message. Encrypt packets with the current session + /// key if we are awaiting a response from AuthMessage. + pub(crate) fn encrypt_message( + &mut self, + src_id: NodeId, + message: &[u8], + ) -> Result { + self.counter += 1; + + // If the message nonce length is ever set below 4 bytes this will explode. The packet + // size constants shouldn't be modified. + let random_nonce: [u8; MESSAGE_NONCE_LENGTH - 4] = rand::random(); + let mut message_nonce: MessageNonce = [0u8; crate::packet::MESSAGE_NONCE_LENGTH]; + message_nonce[..4].copy_from_slice(&self.counter.to_be_bytes()); + message_nonce[4..].copy_from_slice(&random_nonce); + + // the authenticated data is the IV concatenated with the packet header + let iv: u128 = rand::random(); + let header = PacketHeader { + message_nonce, + kind: PacketKind::Message { src_id }, + }; + + let mut authenticated_data = iv.to_be_bytes().to_vec(); + authenticated_data.extend_from_slice(&header.encode()); + + let cipher = crypto::encrypt_message( + &self.keys.encryption_key, + message_nonce, + message, + &authenticated_data, + )?; + + // construct a packet from the header and the cipher text + Ok(Packet { + iv, + header, + message: cipher, + }) + } + + /// Decrypts an encrypted message. If a Session is already established, the original decryption + /// keys are tried first, upon failure, the new keys are attempted. If the new keys succeed, + /// the session keys are updated along with the Session state. + pub(crate) fn decrypt_message( + &mut self, + message_nonce: MessageNonce, + message: &[u8], + aad: &[u8], + ) -> Result, Discv5Error> { + // try with the new keys + if let Some(new_keys) = self.awaiting_keys.take() { + let result = + crypto::decrypt_message(&new_keys.decryption_key, message_nonce, message, aad); + if result.is_ok() { + self.keys = new_keys; + return result; + } + } + // if it failed try with the old keys + crypto::decrypt_message(&self.keys.decryption_key, message_nonce, message, aad) + } + + /* Session Helper Functions */ + + /// Generates session keys from an authentication header. If the IP of the ENR does not match the + /// source IP address, we consider this session untrusted. The output returns a boolean which + /// specifies if the Session is trusted or not. + pub(crate) fn establish_from_challenge( + local_key: Arc>, + local_id: &NodeId, + remote_id: &NodeId, + challenge: Challenge, + id_nonce_sig: &[u8], + ephem_pubkey: &[u8], + enr_record: Option, + ) -> Result<(Session, Enr), Discv5Error> { + // check and verify a potential ENR update + + // Duplicate code here to avoid cloning an ENR + let remote_public_key = { + let enr = match (enr_record.as_ref(), challenge.remote_enr.as_ref()) { + (Some(new_enr), Some(known_enr)) => { + if new_enr.seq() > known_enr.seq() { + new_enr + } else { + known_enr + } + } + (Some(new_enr), None) => new_enr, + (None, Some(known_enr)) => known_enr, + (None, None) => { + warn!( + "Peer did not respond with their ENR. Session could not be established. Node: {}", + remote_id + ); + return Err(Discv5Error::SessionNotEstablished); + } + }; + enr.public_key() + }; + + // verify the auth header nonce + if !crypto::verify_authentication_nonce( + &remote_public_key, + ephem_pubkey, + &challenge.data, + local_id, + id_nonce_sig, + ) { + return Err(Discv5Error::InvalidChallengeSignature(challenge)); + } + + // The keys are derived after the message has been verified to prevent potential extra work + // for invalid messages. + + // generate session keys + let (decryption_key, encryption_key) = crypto::derive_keys_from_pubkey( + &local_key.read(), + local_id, + remote_id, + &challenge.data, + ephem_pubkey, + )?; + + let keys = Keys { + encryption_key, + decryption_key, + }; + + // Takes ownership of the provided ENRs - Slightly annoying code duplication, but avoids + // cloning ENRs + let session_enr = match (enr_record, challenge.remote_enr) { + (Some(new_enr), Some(known_enr)) => { + if new_enr.seq() > known_enr.seq() { + new_enr + } else { + known_enr + } + } + (Some(new_enr), None) => new_enr, + (None, Some(known_enr)) => known_enr, + (None, None) => unreachable!("Checked in the first match above"), + }; + + Ok((Session::new(keys), session_enr)) + } + + /// Encrypts a message and produces an AuthMessage. + pub(crate) fn encrypt_with_header( + remote_contact: &NodeContact, + local_key: Arc>, + updated_enr: Option, + local_node_id: &NodeId, + challenge_data: &ChallengeData, + message: &[u8], + ) -> Result<(Packet, Session), Discv5Error> { + // generate the session keys + let (encryption_key, decryption_key, ephem_pubkey) = + crypto::generate_session_keys(local_node_id, remote_contact, challenge_data)?; + + let keys = Keys { + encryption_key, + decryption_key, + }; + + // construct the nonce signature + let sig = crypto::sign_nonce( + &local_key.read(), + challenge_data, + &ephem_pubkey, + &remote_contact.node_id(), + ) + .map_err(|_| Discv5Error::Custom("Could not sign WHOAREYOU nonce"))?; + + // build an authentication packet + let message_nonce: MessageNonce = rand::random(); + let mut packet = Packet::new_authheader( + *local_node_id, + message_nonce, + sig, + ephem_pubkey, + updated_enr, + ); + + // Create the authenticated data for the new packet. + + let mut authenticated_data = packet.iv.to_be_bytes().to_vec(); + authenticated_data.extend_from_slice(&packet.header.encode()); + + // encrypt the message + let message_ciphertext = + crypto::encrypt_message(&encryption_key, message_nonce, message, &authenticated_data)?; + + packet.message = message_ciphertext; + + let session = Session::new(keys); + + Ok((packet, session)) + } +} diff --git a/version-meld/discv5/src/handler/tests.rs b/version-meld/discv5/src/handler/tests.rs new file mode 100644 index 0000000..2045285 --- /dev/null +++ b/version-meld/discv5/src/handler/tests.rs @@ -0,0 +1,219 @@ +#![cfg(test)] +use super::*; +use crate::{ + rpc::{Request, Response}, + Discv5ConfigBuilder, +}; +use enr::EnrBuilder; +use std::{net::IpAddr, time::Duration}; +use tokio::time::sleep; + +fn init() { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); +} + +macro_rules! arc_rw { + ( $x: expr ) => { + Arc::new(RwLock::new($x)) + }; +} + +#[tokio::test] +// Tests the construction and sending of a simple message +async fn simple_session_message() { + init(); + + let sender_port = 5000; + let receiver_port = 5001; + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + + let key1 = CombinedKey::generate_secp256k1(); + let key2 = CombinedKey::generate_secp256k1(); + + let config = Discv5ConfigBuilder::new().enable_packet_filter().build(); + + let sender_enr = EnrBuilder::new("v4") + .ip(ip) + .udp(sender_port) + .build(&key1) + .unwrap(); + let receiver_enr = EnrBuilder::new("v4") + .ip(ip) + .udp(receiver_port) + .build(&key2) + .unwrap(); + + let (_exit_send, sender_send, _sender_recv) = Handler::spawn( + arc_rw!(sender_enr.clone()), + arc_rw!(key1), + sender_enr.udp_socket().unwrap(), + config.clone(), + ) + .await + .unwrap(); + + let (_exit_recv, recv_send, mut receiver_recv) = Handler::spawn( + arc_rw!(receiver_enr.clone()), + arc_rw!(key2), + receiver_enr.udp_socket().unwrap(), + config, + ) + .await + .unwrap(); + + let send_message = Box::new(Request { + id: RequestId(vec![1]), + body: RequestBody::Ping { enr_seq: 1 }, + }); + + let _ = sender_send.send(HandlerRequest::Request( + receiver_enr.into(), + send_message.clone(), + )); + + let receiver = async move { + loop { + if let Some(message) = receiver_recv.recv().await { + match message { + HandlerResponse::WhoAreYou(wru_ref) => { + let _ = recv_send + .send(HandlerRequest::WhoAreYou(wru_ref, Some(sender_enr.clone()))); + } + HandlerResponse::Request(_, request) => { + assert_eq!(request, send_message); + return; + } + _ => {} + } + } + } + }; + + tokio::select! { + _ = receiver => {} + _ = sleep(Duration::from_millis(500)) => { + panic!("Test timed out"); + } + } +} + +#[tokio::test] +// Tests sending multiple messages on an encrypted session +async fn multiple_messages() { + init(); + let sender_port = 5002; + let receiver_port = 5003; + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + let key1 = CombinedKey::generate_secp256k1(); + let key2 = CombinedKey::generate_secp256k1(); + + let config = Discv5ConfigBuilder::new().build(); + let sender_enr = EnrBuilder::new("v4") + .ip(ip) + .udp(sender_port) + .build(&key1) + .unwrap(); + let receiver_enr = EnrBuilder::new("v4") + .ip(ip) + .udp(receiver_port) + .build(&key2) + .unwrap(); + + let (_exit_send, sender_handler, mut sender_handler_recv) = Handler::spawn( + arc_rw!(sender_enr.clone()), + arc_rw!(key1), + sender_enr.udp_socket().unwrap(), + config.clone(), + ) + .await + .unwrap(); + + let (_exit_recv, recv_send, mut receiver_handler) = Handler::spawn( + arc_rw!(receiver_enr.clone()), + arc_rw!(key2), + receiver_enr.udp_socket().unwrap(), + config, + ) + .await + .unwrap(); + + let send_message = Box::new(Request { + id: RequestId(vec![1]), + body: RequestBody::Ping { enr_seq: 1 }, + }); + + // sender to send the first message then await for the session to be established + let _ = sender_handler.send(HandlerRequest::Request( + receiver_enr.clone().into(), + send_message.clone(), + )); + + let pong_response = Response { + id: RequestId(vec![1]), + body: ResponseBody::Pong { + enr_seq: 1, + ip, + port: sender_port, + }, + }; + + let messages_to_send = 5usize; + + let mut message_count = 0usize; + let recv_send_message = send_message.clone(); + + let sender = async move { + loop { + match sender_handler_recv.recv().await { + Some(HandlerResponse::Established(_, _)) => { + // now the session is established, send the rest of the messages + for _ in 0..messages_to_send - 1 { + let _ = sender_handler.send(HandlerRequest::Request( + receiver_enr.clone().into(), + send_message.clone(), + )); + } + } + _ => continue, + }; + } + }; + + let receiver = async move { + loop { + match receiver_handler.recv().await { + Some(HandlerResponse::WhoAreYou(wru_ref)) => { + let _ = recv_send + .send(HandlerRequest::WhoAreYou(wru_ref, Some(sender_enr.clone()))); + } + Some(HandlerResponse::Request(addr, request)) => { + assert_eq!(request, recv_send_message); + message_count += 1; + // required to send a pong response to establish the session + let _ = recv_send.send(HandlerRequest::Response( + addr, + Box::new(pong_response.clone()), + )); + if message_count == messages_to_send { + return; + } + } + _ => { + continue; + } + } + } + }; + + let sleep_future = sleep(Duration::from_millis(100)); + + tokio::select! { + _ = sender => {} + _ = receiver => {} + _ = sleep_future => { + panic!("Test timed out"); + } + } +} diff --git a/version-meld/discv5/src/kbucket.rs b/version-meld/discv5/src/kbucket.rs new file mode 100644 index 0000000..eb308ed --- /dev/null +++ b/version-meld/discv5/src/kbucket.rs @@ -0,0 +1,1027 @@ +// Copyright 2018 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +//! Implementation of a Kademlia routing table as used by a single peer +//! participating in a Kademlia DHT. +//! +//! The entry point for the API of this module is a [`KBucketsTable`]. +//! +//! ## Pending Insertions +//! +//! When the bucket associated with the `Key` of an inserted entry is full +//! but contains disconnected nodes, it accepts a [`PendingEntry`]. +//! Pending entries are inserted lazily when their timeout is found to be expired +//! upon querying the `KBucketsTable`. When that happens, the `KBucketsTable` records +//! an [`AppliedPending`] result which must be consumed by calling [`take_applied_pending`] +//! regularly and / or after performing lookup operations like [`entry`] and [`closest_keys`]. +//! +//! [`entry`]: KBucketsTable::entry +//! [`closest_keys`]: KBucketsTable::closest_keys +//! [`take_applied_pending`]: KBucketsTable::take_applied_pending + +// [Implementation Notes] +// +// 1. Routing Table Layout +// +// The routing table is currently implemented as a fixed-size "array" of +// buckets, ordered by increasing distance relative to a local key +// that identifies the local peer. This is an often-used, simplified +// implementation that approximates the properties of the b-tree (or prefix tree) +// implementation described in the full paper [0], whereby buckets are split on-demand. +// This should be treated as an implementation detail, however, so that the +// implementation may change in the future without breaking the API. +// +// 2. Replacement Cache +// +// In this implementation, the "replacement cache" for unresponsive peers +// consists of a single entry per bucket. Furthermore, this implementation is +// currently tailored to connection-oriented transports, meaning that the +// "LRU"-based ordering of entries in a bucket is actually based on the last reported +// connection status of the corresponding peers, from least-recently (dis)connected to +// most-recently (dis)connected, and controlled through the `Entry` API. As a result, +// the nodes in the buckets are not reordered as a result of RPC activity, but only as a +// result of nodes being marked as connected or disconnected. In particular, +// if a bucket is full and contains only entries for peers that are considered +// connected, no pending entry is accepted. See the `bucket` submodule for +// further details. +// +// [0]: https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf + +mod bucket; +mod entry; +mod filter; +mod key; + +pub use entry::*; + +pub use crate::handler::ConnectionDirection; +use arrayvec::{self, ArrayVec}; +use bucket::KBucket; +pub use bucket::{ + ConnectionState, FailureReason, InsertResult as BucketInsertResult, UpdateResult, + MAX_NODES_PER_BUCKET, +}; +pub use filter::{Filter, IpBucketFilter, IpTableFilter}; +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; + +/// Maximum number of k-buckets. +const NUM_BUCKETS: usize = 256; + +/// Closest Iterator Output Value +pub struct ClosestValue { + pub key: Key, + pub value: TVal, +} + +impl AsRef> for ClosestValue { + fn as_ref(&self) -> &Key { + &self.key + } +} + +/// A key that can be returned from the `closest_keys` function, which indicates if the key matches the +/// predicate or not. +pub struct PredicateKey { + pub key: Key, + pub predicate_match: bool, +} + +impl From> for Key { + fn from(key: PredicateKey) -> Self { + key.key + } +} + +impl From> for PredicateKey { + fn from(value: PredicateValue) -> Self { + PredicateKey { + key: value.key, + predicate_match: value.predicate_match, + } + } +} + +/// A value being returned from a predicate closest iterator. +pub struct PredicateValue { + pub key: Key, + pub predicate_match: bool, + pub value: TVal, +} + +impl AsRef> for PredicateValue { + fn as_ref(&self) -> &Key { + &self.key + } +} + +impl From> for Key { + fn from(key: PredicateValue) -> Self { + key.key + } +} + +/// A `KBucketsTable` represents a Kademlia routing table. +#[derive(Clone)] +pub struct KBucketsTable { + /// The key identifying the local peer that owns the routing table. + local_key: Key, + /// The buckets comprising the routing table. + buckets: Vec>, + /// The list of evicted entries that have been replaced with pending + /// entries since the last call to [`KBucketsTable::take_applied_pending`]. + applied_pending: VecDeque>, + /// Filter to be applied at the table level when adding/updating a node. + table_filter: Option>>, +} + +#[must_use] +#[derive(Debug, Clone)] +/// Informs if the record was inserted. +pub enum InsertResult { + /// The node didn't exist and the new record was inserted. + Inserted, + /// The node was inserted into a pending state. + Pending { + /// The key of the least-recently connected entry that is currently considered + /// disconnected and whose corresponding peer should be checked for connectivity + /// in order to prevent it from being evicted. If connectivity to the peer is + /// re-established, the corresponding entry should be updated with + /// [`bucket::ConnectionState::Connected`]. + disconnected: Key, + }, + /// The node existed and the status was updated. + StatusUpdated { + // Returns true if the status updated promoted a disconnected node to a connected node. + promoted_to_connected: bool, + }, + /// The node existed and the value was updated. + ValueUpdated, + /// Both the status and value were updated. + Updated { + // Returns true if the status updated promoted a disconnected node to a connected node. + promoted_to_connected: bool, + }, + /// The pending slot was updated. + UpdatedPending, + /// The record failed to be inserted. This can happen to not passing table/bucket filters or + /// the bucket was full. + Failed(FailureReason), +} + +/// A (type-safe) index into a `KBucketsTable`, i.e. a non-negative integer in the +/// interval `[0, NUM_BUCKETS)`. +#[derive(Copy, Clone)] +struct BucketIndex(usize); + +impl BucketIndex { + /// Creates a new `BucketIndex` for a `Distance`. + /// + /// The given distance is interpreted as the distance from a `local_key` of + /// a `KBucketsTable`. If the distance is zero, `None` is returned, in + /// recognition of the fact that the only key with distance `0` to a + /// `local_key` is the `local_key` itself, which does not belong in any + /// bucket. + fn new(d: &Distance) -> Option { + (NUM_BUCKETS - d.0.leading_zeros() as usize) + .checked_sub(1) + .map(BucketIndex) + } + + /// Gets the index value as an unsigned integer. + fn get(self) -> usize { + self.0 + } +} + +impl KBucketsTable +where + TNodeId: Clone, + TVal: Eq, +{ + /// Creates a new, empty Kademlia routing table with entries partitioned + /// into buckets as per the Kademlia protocol. + /// + /// The given `pending_timeout` specifies the duration after creation of + /// a [`PendingEntry`] after which it becomes eligible for insertion into + /// a full bucket, replacing the least-recently (dis)connected node. + /// + /// A filter can be applied that limits entries into a bucket based on the buckets contents. + /// Entries that fail the filter, will not be inserted. + pub fn new( + local_key: Key, + pending_timeout: Duration, + max_incoming_per_bucket: usize, + table_filter: Option>>, + bucket_filter: Option>>, + ) -> Self { + KBucketsTable { + local_key, + buckets: (0..NUM_BUCKETS) + .map(|_| { + KBucket::new( + pending_timeout, + max_incoming_per_bucket, + bucket_filter.clone(), + ) + }) + .collect(), + applied_pending: VecDeque::new(), + table_filter, + } + } + + // Updates a node's status if it exists in the table. + // This checks all table and bucket filters before performing the update. + pub fn update_node_status( + &mut self, + key: &Key, + state: ConnectionState, + direction: Option, + ) -> UpdateResult { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + + bucket.update_status(key, state, direction) + } else { + UpdateResult::NotModified // The key refers to our current node. + } + } + + /// Updates a node's value if it exists in the table. + /// + /// Optionally the connection state can be modified. + pub fn update_node( + &mut self, + key: &Key, + value: TVal, + state: Option, + ) -> UpdateResult { + // Apply the table filter + let mut passed_table_filter = true; + if let Some(table_filter) = self.table_filter.as_ref() { + // Check if the value is a duplicate before applying the table filter (optimisation). + let duplicate = { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(node) = bucket.get(key) { + node.value == value + } else { + false + } + } else { + false + } + }; + + // If the Value is new, check the table filter + if !duplicate && !table_filter.filter(&value, &mut self.table_iter()) { + passed_table_filter = false; + } + } + + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + + if !passed_table_filter { + bucket.remove(key); + return UpdateResult::Failed(FailureReason::TableFilter); + } + + let update_result = bucket.update_value(key, value); + + if let UpdateResult::Failed(_) = &update_result { + return update_result; + } + + // If we need to update the connection state, update it here. + let status_result = if let Some(state) = state { + bucket.update_status(key, state, None) + } else { + UpdateResult::NotModified + }; + + // Return an appropriate value + match (&update_result, &status_result) { + (_, UpdateResult::Failed(_)) => status_result, + (UpdateResult::Failed(_), _) => update_result, + (_, UpdateResult::UpdatedAndPromoted) => UpdateResult::UpdatedAndPromoted, + (UpdateResult::UpdatedPending, _) => UpdateResult::UpdatedPending, + (_, UpdateResult::UpdatedPending) => UpdateResult::UpdatedPending, + (UpdateResult::NotModified, UpdateResult::NotModified) => UpdateResult::NotModified, + (_, _) => UpdateResult::Updated, + } + } else { + UpdateResult::NotModified // The key refers to our current node. + } + } + + // Attempts to insert or update + pub fn insert_or_update( + &mut self, + key: &Key, + value: TVal, + status: NodeStatus, + ) -> InsertResult { + // Check the table filter + let mut passed_table_filter = true; + if let Some(table_filter) = self.table_filter.as_ref() { + // Check if the value is a duplicate before applying the table filter (optimisation). + let duplicate = { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(node) = bucket.get(key) { + node.value == value + } else { + false + } + } else { + false + } + }; + + if !duplicate && !table_filter.filter(&value, &mut self.table_iter()) { + passed_table_filter = false; + } + } + + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + + if !passed_table_filter { + bucket.remove(key); + return InsertResult::Failed(FailureReason::TableFilter); + } + + // If the node doesn't exist, insert it + if bucket.position(key).is_none() { + let node = Node { + key: key.clone(), + value, + status, + }; + match bucket.insert(node) { + bucket::InsertResult::NodeExists => unreachable!("Node must exist"), + bucket::InsertResult::Full => InsertResult::Failed(FailureReason::BucketFull), + bucket::InsertResult::TooManyIncoming => { + InsertResult::Failed(FailureReason::TooManyIncoming) + } + bucket::InsertResult::FailedFilter => { + InsertResult::Failed(FailureReason::BucketFilter) + } + bucket::InsertResult::Pending { disconnected } => { + InsertResult::Pending { disconnected } + } + bucket::InsertResult::Inserted => InsertResult::Inserted, + } + } else { + // The node exists in the bucket + // Attempt to update the status + let update_status = bucket.update_status(key, status.state, Some(status.direction)); + + if update_status.failed() { + // The node was removed from the table + return InsertResult::Failed(FailureReason::TooManyIncoming); + } + // Attempt to update the value + let update_value = bucket.update_value(key, value); + + match (update_value, update_status) { + (UpdateResult::Updated { .. }, UpdateResult::Updated) => { + InsertResult::Updated { + promoted_to_connected: false, + } + } + (UpdateResult::Updated { .. }, UpdateResult::UpdatedAndPromoted) => { + InsertResult::Updated { + promoted_to_connected: true, + } + } + (UpdateResult::Updated { .. }, UpdateResult::NotModified) + | (UpdateResult::Updated { .. }, UpdateResult::UpdatedPending) => { + InsertResult::ValueUpdated + } + (UpdateResult::NotModified, UpdateResult::Updated) => { + InsertResult::StatusUpdated { + promoted_to_connected: false, + } + } + (UpdateResult::NotModified, UpdateResult::UpdatedAndPromoted) => { + InsertResult::StatusUpdated { + promoted_to_connected: true, + } + } + (UpdateResult::NotModified, UpdateResult::NotModified) => { + InsertResult::Updated { + promoted_to_connected: false, + } + } + (UpdateResult::UpdatedPending, _) | (_, UpdateResult::UpdatedPending) => { + InsertResult::UpdatedPending + } + (UpdateResult::Failed(reason), _) => InsertResult::Failed(reason), + (_, UpdateResult::Failed(_)) => unreachable!("Status failure handled earlier."), + (UpdateResult::UpdatedAndPromoted, _) => { + unreachable!("Value update cannot promote a connection.") + } + } + } + } else { + // Cannot insert our local entry. + InsertResult::Failed(FailureReason::InvalidSelfUpdate) + } + } + + /// Removes a node from the routing table. Returns `true` of the node existed. + pub fn remove(&mut self, key: &Key) -> bool { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + bucket.remove(key) + } else { + false + } + } + + /// Returns an `Entry` for the given key, representing the state of the entry + /// in the routing table. + /// NOTE: This must be used with caution. Modifying values manually can bypass the internal + /// table filters and ingoing/outgoing limits. + pub fn entry<'a>(&'a mut self, key: &'a Key) -> Entry<'a, TNodeId, TVal> { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &mut self.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + Entry::new(bucket, key) + } else { + Entry::SelfEntry + } + } + + /// Returns an iterator over all the entries in the routing table. + pub fn iter(&mut self) -> impl Iterator> { + let applied_pending = &mut self.applied_pending; + self.buckets.iter_mut().flat_map(move |table| { + if let Some(applied) = table.apply_pending() { + applied_pending.push_back(applied) + } + table.iter().map(move |n| EntryRefView { + node: NodeRefView { + key: &n.key, + value: &n.value, + }, + status: n.status, + }) + }) + } + + /// Returns an iterator over all the entries in the routing table to give to a table filter. + /// + /// This differs from the regular iterator as it doesn't take ownership of self and doesn't try + /// to apply any pending nodes. + fn table_iter(&self) -> impl Iterator { + self.buckets + .iter() + .flat_map(move |table| table.iter().map(|n| &n.value)) + } + + /// Returns an iterator over all the entries in the routing table. + /// Does not add pending node to kbucket to get an iterator which + /// takes a reference instead of a mutable reference. + pub fn iter_ref(&self) -> impl Iterator> { + self.buckets.iter().flat_map(move |table| { + table.iter().map(move |n| EntryRefView { + node: NodeRefView { + key: &n.key, + value: &n.value, + }, + status: n.status, + }) + }) + } + + /// Consumes the next applied pending entry, if any. + /// + /// When an entry is attempted to be inserted and the respective bucket is full, + /// it may be recorded as pending insertion after a timeout, see [`InsertResult::Pending`]. + /// + /// If the oldest currently disconnected entry in the respective bucket does not change + /// its status until the timeout of pending entry expires, it is evicted and + /// the pending entry inserted instead. These insertions of pending entries + /// happens lazily, whenever the `KBucketsTable` is accessed, and the corresponding + /// buckets are updated accordingly. The fact that a pending entry was applied is + /// recorded in the `KBucketsTable` in the form of `AppliedPending` results, which must be + /// consumed by calling this function. + pub fn take_applied_pending(&mut self) -> Option> { + self.applied_pending.pop_front() + } + + /// Returns an iterator over the keys that are contained in a kbucket, specified by a log2 distance. + pub fn nodes_by_distances( + &mut self, + log2_distances: &[u64], + max_nodes: usize, + ) -> Vec> { + // Filter log2 distances to only include those in the closed interval [1, 256] + let distances = log2_distances + .iter() + .filter_map(|&d| { + if d > 0 && d <= (NUM_BUCKETS as u64) { + Some(d) + } else { + None + } + }) + .collect::>(); + + // Apply pending nodes + for distance in &distances { + // The log2 distance ranges from 1-256 and is always 1 more than the bucket index. For this + // reason we subtract 1 from log2 distance to get the correct bucket index. + let bucket = &mut self.buckets[(distance - 1) as usize]; + if let Some(applied) = bucket.apply_pending() { + self.applied_pending.push_back(applied) + } + } + + // Find the matching nodes + let mut matching_nodes = Vec::new(); + + // Note we search via distance in order + for distance in distances { + let bucket = &self.buckets[(distance - 1) as usize]; + for node in bucket.iter().map(|n| { + let node = NodeRefView { + key: &n.key, + value: &n.value, + }; + EntryRefView { + node, + status: n.status, + } + }) { + matching_nodes.push(node); + // Exit early if we have found enough nodes + if matching_nodes.len() >= max_nodes { + return matching_nodes; + } + } + } + matching_nodes + } + + /// Returns an iterator over the keys closest to `target`, ordered by + /// increasing distance. + pub fn closest_keys<'a, T>( + &'a mut self, + target: &'a Key, + ) -> impl Iterator> + 'a + where + T: Clone, + { + let distance = self.local_key.distance(target); + ClosestIter { + target, + iter: None, + table: self, + buckets_iter: ClosestBucketsIter::new(distance), + fmap: |b: &KBucket| -> ArrayVec<_, MAX_NODES_PER_BUCKET> { + b.iter().map(|n| n.key.clone()).collect() + }, + } + } + + /// Returns an iterator over the keys closest to `target`, ordered by + /// increasing distance. + pub fn closest_values<'a, T>( + &'a mut self, + target: &'a Key, + ) -> impl Iterator> + 'a + where + T: Clone, + TVal: Clone, + { + let distance = self.local_key.distance(target); + ClosestIter { + target, + iter: None, + table: self, + buckets_iter: ClosestBucketsIter::new(distance), + fmap: |b: &KBucket| -> ArrayVec<_, MAX_NODES_PER_BUCKET> { + b.iter() + .map(|n| ClosestValue { + key: n.key.clone(), + value: n.value.clone(), + }) + .collect() + }, + } + } + + /// Returns an iterator over the keys closest to `target`, ordered by + /// increasing distance specifying which keys agree with a value predicate. + pub fn closest_values_predicate<'a, T, F>( + &'a mut self, + target: &'a Key, + predicate: F, + ) -> impl Iterator> + 'a + where + T: Clone, + F: Fn(&TVal) -> bool + 'a, + TVal: Clone, + { + let distance = self.local_key.distance(target); + ClosestIter { + target, + iter: None, + table: self, + buckets_iter: ClosestBucketsIter::new(distance), + fmap: move |b: &KBucket| -> ArrayVec<_, MAX_NODES_PER_BUCKET> { + b.iter() + .map(|n| PredicateValue { + key: n.key.clone(), + predicate_match: predicate(&n.value), + value: n.value.clone(), + }) + .collect() + }, + } + } + + /// Returns a reference to a bucket given the key. Returns None if bucket does not exist. + pub fn get_bucket<'a>(&'a self, key: &Key) -> Option<&'a KBucket> { + let index = BucketIndex::new(&self.local_key.distance(key)); + if let Some(i) = index { + let bucket = &self.buckets[i.get()]; + Some(bucket) + } else { + None + } + } +} + +/// An iterator over (some projection of) the closest entries in a +/// `KBucketsTable` w.r.t. some target `Key`. +struct ClosestIter<'a, TTarget, TNodeId, TVal: Eq, TMap, TOut> { + /// A reference to the target key whose distance to the local key determines + /// the order in which the buckets are traversed. The resulting + /// array from projecting the entries of each bucket using `fmap` is + /// sorted according to the distance to the target. + target: &'a Key, + /// A reference to all buckets of the `KBucketsTable`. + table: &'a mut KBucketsTable, + /// The iterator over the bucket indices in the order determined by the + /// distance of the local key to the target. + buckets_iter: ClosestBucketsIter, + /// The iterator over the entries in the currently traversed bucket. + iter: Option>, + /// The projection function / mapping applied on each bucket as + /// it is encountered, producing the next `iter`ator. + fmap: TMap, +} + +/// An iterator over the bucket indices, in the order determined by the `Distance` of +/// a target from the `local_key`, such that the entries in the buckets are incrementally +/// further away from the target, starting with the bucket covering the target. +struct ClosestBucketsIter { + /// The distance to the `local_key`. + distance: Distance, + /// The current state of the iterator. + state: ClosestBucketsIterState, +} + +/// Operating states of a `ClosestBucketsIter`. +enum ClosestBucketsIterState { + /// The starting state of the iterator yields the first bucket index and + /// then transitions to `ZoomIn`. + Start(BucketIndex), + /// The iterator "zooms in" to to yield the next bucket containing nodes that + /// are incrementally closer to the local node but further from the `target`. + /// These buckets are identified by a `1` in the corresponding bit position + /// of the distance bit string. When bucket `0` is reached, the iterator + /// transitions to `ZoomOut`. + ZoomIn(BucketIndex), + /// Once bucket `0` has been reached, the iterator starts "zooming out" + /// to buckets containing nodes that are incrementally further away from + /// both the local key and the target. These are identified by a `0` in + /// the corresponding bit position of the distance bit string. When bucket + /// `255` is reached, the iterator transitions to state `Done`. + ZoomOut(BucketIndex), + /// The iterator is in this state once it has visited all buckets. + Done, +} + +impl ClosestBucketsIter { + fn new(distance: Distance) -> Self { + let state = match BucketIndex::new(&distance) { + Some(i) => ClosestBucketsIterState::Start(i), + None => ClosestBucketsIterState::Done, + }; + Self { distance, state } + } + + fn next_in(&self, i: BucketIndex) -> Option { + (0..i.get()).rev().find_map(|i| { + if self.distance.0.bit(i) { + Some(BucketIndex(i)) + } else { + None + } + }) + } + + fn next_out(&self, i: BucketIndex) -> Option { + (i.get() + 1..NUM_BUCKETS).find_map(|i| { + if !self.distance.0.bit(i) { + Some(BucketIndex(i)) + } else { + None + } + }) + } +} + +impl Iterator for ClosestBucketsIter { + type Item = BucketIndex; + + fn next(&mut self) -> Option { + match self.state { + ClosestBucketsIterState::Start(i) => { + self.state = ClosestBucketsIterState::ZoomIn(i); + Some(i) + } + ClosestBucketsIterState::ZoomIn(i) => { + if let Some(i) = self.next_in(i) { + self.state = ClosestBucketsIterState::ZoomIn(i); + Some(i) + } else { + let i = BucketIndex(0); + self.state = ClosestBucketsIterState::ZoomOut(i); + Some(i) + } + } + ClosestBucketsIterState::ZoomOut(i) => { + if let Some(i) = self.next_out(i) { + self.state = ClosestBucketsIterState::ZoomOut(i); + Some(i) + } else { + self.state = ClosestBucketsIterState::Done; + None + } + } + ClosestBucketsIterState::Done => None, + } + } +} + +impl Iterator + for ClosestIter<'_, TTarget, TNodeId, TVal, TMap, TOut> +where + TNodeId: Clone, + TVal: Eq, + TMap: Fn(&KBucket) -> ArrayVec, + TOut: AsRef>, +{ + type Item = TOut; + + fn next(&mut self) -> Option { + loop { + match &mut self.iter { + Some(iter) => match iter.next() { + Some(k) => return Some(k), + None => self.iter = None, + }, + None => { + if let Some(i) = self.buckets_iter.next() { + let bucket = &mut self.table.buckets[i.get()]; + if let Some(applied) = bucket.apply_pending() { + self.table.applied_pending.push_back(applied) + } + let mut v = (self.fmap)(bucket); + v.sort_by(|a, b| { + self.target + .distance(a.as_ref()) + .cmp(&self.target.distance(b.as_ref())) + }); + self.iter = Some(v.into_iter()); + } else { + return None; + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{bucket::InsertResult as BucketInsertResult, *}; + use enr::NodeId; + + fn connected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Connected, + direction: ConnectionDirection::Outgoing, + } + } + + fn disconnected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Outgoing, + } + } + + #[test] + fn basic_closest() { + let local_key = Key::from(NodeId::random()); + let other_id = Key::from(NodeId::random()); + + let mut table = KBucketsTable::<_, ()>::new( + local_key, + Duration::from_secs(5), + MAX_NODES_PER_BUCKET, + None, + None, + ); + if let Entry::Absent(entry) = table.entry(&other_id) { + match entry.insert((), connected_state()) { + BucketInsertResult::Inserted => (), + _ => panic!(), + } + } else { + panic!() + } + + let res = table.closest_keys(&other_id).collect::>(); + assert_eq!(res.len(), 1); + assert_eq!(res[0], other_id); + } + + #[test] + fn update_local_id_fails() { + let local_key = Key::from(NodeId::random()); + let mut table = KBucketsTable::<_, ()>::new( + local_key.clone(), + Duration::from_secs(5), + MAX_NODES_PER_BUCKET, + None, + None, + ); + match table.entry(&local_key) { + Entry::SelfEntry => (), + _ => panic!(), + } + } + + #[test] + fn closest() { + let local_key = Key::from(NodeId::random()); + let mut table = KBucketsTable::<_, ()>::new( + local_key, + Duration::from_secs(5), + MAX_NODES_PER_BUCKET, + None, + None, + ); + let mut count = 0; + loop { + if count == 100 { + break; + } + let key = Key::from(NodeId::random()); + if let Entry::Absent(e) = table.entry(&key) { + match e.insert((), connected_state()) { + BucketInsertResult::Inserted => count += 1, + _ => continue, + } + } else { + panic!("entry exists") + } + } + + let mut expected_keys: Vec<_> = table + .buckets + .iter() + .flat_map(|t| t.iter().map(|n| n.key.clone())) + .collect(); + + for _ in 0..10 { + let target_key = Key::from(NodeId::random()); + let keys = table.closest_keys(&target_key).collect::>(); + // The list of keys is expected to match the result of a full-table scan. + expected_keys.sort_by_key(|k| k.distance(&target_key)); + assert_eq!(keys, expected_keys); + } + } + + #[test] + fn applied_pending() { + let local_key = Key::from(NodeId::random()); + let mut table = KBucketsTable::<_, ()>::new( + local_key.clone(), + Duration::from_millis(1), + MAX_NODES_PER_BUCKET, + None, + None, + ); + let expected_applied; + let full_bucket_index; + loop { + let key = Key::from(NodeId::random()); + if let Entry::Absent(e) = table.entry(&key) { + match e.insert((), disconnected_state()) { + BucketInsertResult::Full => { + if let Entry::Absent(e) = table.entry(&key) { + match e.insert((), connected_state()) { + BucketInsertResult::Pending { disconnected } => { + expected_applied = AppliedPending { + inserted: key.clone(), + evicted: Some(Node { + key: disconnected, + value: (), + status: disconnected_state(), + }), + }; + full_bucket_index = BucketIndex::new(&key.distance(&local_key)); + break; + } + _ => panic!(), + } + } else { + panic!() + } + } + _ => continue, + } + } else { + panic!("entry exists") + } + } + + // Expire the timeout for the pending entry on the full bucket.` + let full_bucket = &mut table.buckets[full_bucket_index.unwrap().get()]; + let elapsed = Instant::now() - Duration::from_secs(1); + full_bucket.pending_mut().unwrap().set_ready_at(elapsed); + + match table.entry(&expected_applied.inserted) { + Entry::Present( + _, + NodeStatus { + state: ConnectionState::Connected, + direction: _direction, + }, + ) => {} + x => panic!("Unexpected entry: {:?}", x), + } + + match table.entry(&expected_applied.evicted.as_ref().unwrap().key) { + Entry::Absent(_) => {} + x => panic!("Unexpected entry: {:?}", x), + } + + assert_eq!(Some(expected_applied), table.take_applied_pending()); + assert_eq!(None, table.take_applied_pending()); + } +} diff --git a/version-meld/discv5/src/kbucket/bucket.rs b/version-meld/discv5/src/kbucket/bucket.rs new file mode 100644 index 0000000..94d12e1 --- /dev/null +++ b/version-meld/discv5/src/kbucket/bucket.rs @@ -0,0 +1,1439 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +//! The internal API for a single `KBucket` in a `KBucketsTable`. +//! +//! > **Note**: Uniqueness of entries w.r.t. a `Key` in a `KBucket` is not +//! > checked in this module. This is an invariant that must hold across all +//! > buckets in a `KBucketsTable` and hence is enforced by the public API +//! > of the `KBucketsTable` and in particular the public `Entry` API. + +#![allow(dead_code)] + +use super::*; +use tracing::{debug, error}; + +/// Maximum number of nodes in a bucket, i.e. the (fixed) `k` parameter. +pub const MAX_NODES_PER_BUCKET: usize = 16; + +/// A `PendingNode` is a `Node` that is pending insertion into a `KBucket`. +#[derive(Debug, Clone)] +pub struct PendingNode { + /// The pending node to insert. + node: Node, + + /// The instant at which the pending node is eligible for insertion into a bucket. + replace: Instant, +} + +/// The status of a node in a bucket. +/// +/// The status of a node in a bucket together with the time of the +/// last status change determines the position of the node in a +/// bucket. +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub struct NodeStatus { + /// The direction (incoming or outgoing) for the node. If in the disconnected state, this + /// represents the last connection status. + pub direction: ConnectionDirection, + /// The connection state, connected or disconnected. + pub state: ConnectionState, +} + +/// The connection state of a node. +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum ConnectionState { + /// The node is connected. + Connected, + /// The node is considered disconnected. + Disconnected, +} + +impl NodeStatus { + pub fn is_connected(&self) -> bool { + match self.state { + ConnectionState::Connected => true, + ConnectionState::Disconnected => false, + } + } + + pub fn is_incoming(&self) -> bool { + match self.direction { + ConnectionDirection::Outgoing => false, + ConnectionDirection::Incoming => true, + } + } +} + +impl PendingNode { + pub fn status(&self) -> NodeStatus { + self.node.status + } + + pub fn value_mut(&mut self) -> &mut TVal { + &mut self.node.value + } + + pub fn set_ready_at(&mut self, t: Instant) { + self.replace = t; + } +} + +/// A `Node` in a bucket, representing a peer participating +/// in the Kademlia DHT together with an associated value (e.g. contact +/// information). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Node { + /// The key of the node, identifying the peer. + pub key: Key, + /// The associated value. + pub value: TVal, + /// The status of the node. + pub status: NodeStatus, +} + +/// The position of a node in a `KBucket`, i.e. a non-negative integer +/// in the range `[0, MAX_NODES_PER_BUCKET)`. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Position(usize); + +/// A `KBucket` is a list of up to `MAX_NODES_PER_BUCKET` `Key`s and associated values, +/// ordered from least-recently connected to most-recently connected. +#[derive(Clone)] +pub struct KBucket { + /// The nodes contained in the bucket. + nodes: ArrayVec, MAX_NODES_PER_BUCKET>, + + /// The position (index) in `nodes` that marks the first connected node. + /// + /// Since the entries in `nodes` are ordered from least-recently connected to + /// most-recently connected, all entries above this index are also considered + /// connected, i.e. the range `[0, first_connected_pos)` marks the sub-list of entries + /// that are considered disconnected and the range + /// `[first_connected_pos, MAX_NODES_PER_BUCKET)` marks sub-list of entries that are + /// considered connected. + /// + /// `None` indicates that there are no connected entries in the bucket, i.e. + /// the bucket is either empty, or contains only entries for peers that are + /// considered disconnected. + first_connected_pos: Option, + + /// A node that is pending to be inserted into a full bucket, should the + /// least-recently connected (and currently disconnected) node not be + /// marked as connected within `unresponsive_timeout`. + pending: Option>, + + /// The timeout window before a new pending node is eligible for insertion, + /// if the least-recently connected node is not updated as being connected + /// in the meantime. + pending_timeout: Duration, + + /// An optional filter that filters new entries given an iterator over current entries in + /// the bucket. + filter: Option>>, + + /// The maximum number of incoming connections allowed per bucket. Setting this to + /// MAX_NODES_PER_BUCKET means there is no restriction on incoming nodes. + max_incoming: usize, +} + +/// The result of inserting an entry into a bucket. +#[must_use] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InsertResult { + /// The entry has been successfully inserted. + Inserted, + /// The entry is pending insertion because the relevant bucket is currently full. + /// The entry is inserted after a timeout elapsed, if the status of the + /// least-recently connected (and currently disconnected) node in the bucket + /// is not updated before the timeout expires. + Pending { + /// The key of the least-recently connected entry that is currently considered + /// disconnected and whose corresponding peer should be checked for connectivity + /// in order to prevent it from being evicted. If connectivity to the peer is + /// re-established, the corresponding entry should be updated with a connected status. + disconnected: Key, + }, + /// The attempted entry failed to pass the filter. + FailedFilter, + /// There were too many incoming nodes for this bucket. + TooManyIncoming, + /// The entry was not inserted because the relevant bucket is full. + Full, + /// The entry already exists. + NodeExists, +} + +/// The result of performing an update on a kbucket/table. +#[must_use] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UpdateResult { + /// The node was updated successfully, + Updated, + /// The update promoted the node to a connected state from a disconnected state. + UpdatedAndPromoted, + /// The pending entry was updated. + UpdatedPending, + /// The update removed the node because it would violate the incoming peers condition. + Failed(FailureReason), + /// There were no changes made to the value of the node. + NotModified, +} + +impl UpdateResult { + // The update failed. + pub fn failed(&self) -> bool { + matches!(self, UpdateResult::Failed(_)) + } +} + +/// A reason for failing to update or insert a node into the bucket. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FailureReason { + /// Too many incoming nodes already in the bucket. + TooManyIncoming, + /// The node didn't pass the bucket filter. + BucketFilter, + /// The node didn't pass the table filter. + TableFilter, + /// The node didn't exist. + KeyNonExistant, + /// The bucket was full. + BucketFull, + /// Cannot update self, + InvalidSelfUpdate, +} + +/// The result of applying a pending node to a bucket, possibly +/// replacing an existing node. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AppliedPending { + /// The key of the inserted pending node. + pub inserted: Key, + /// The node that has been evicted from the bucket to make room for the + /// pending node, if any. + pub evicted: Option>, +} + +impl KBucket +where + TNodeId: Clone, + TVal: Eq, +{ + /// Creates a new `KBucket` with the given timeout for pending entries. + pub fn new( + pending_timeout: Duration, + max_incoming: usize, + filter: Option>>, + ) -> Self { + KBucket { + nodes: ArrayVec::new(), + first_connected_pos: None, + pending: None, + pending_timeout, + filter, + max_incoming, + } + } + + /// Returns a reference to the pending node of the bucket, if there is any. + pub fn pending(&self) -> Option<&PendingNode> { + self.pending.as_ref() + } + + /// Returns a mutable reference to the pending node of the bucket, if there is any. + pub fn pending_mut(&mut self) -> Option<&mut PendingNode> { + self.pending.as_mut() + } + + /// Returns a reference to the pending node of the bucket, if there is any + /// with a matching key. + pub fn as_pending(&self, key: &Key) -> Option<&PendingNode> { + self.pending().filter(|p| &p.node.key == key) + } + + /// Returns an iterator over the nodes in the bucket, together with their status. + pub fn iter(&self) -> impl Iterator> { + self.nodes.iter() + } + + /// Inserts the pending node into the bucket, if its timeout has elapsed, + /// replacing the least-recently connected node. + /// + /// If a pending node has been inserted, its key is returned together with + /// the node that was replaced. `None` indicates that the nodes in the + /// bucket remained unchanged. + pub fn apply_pending(&mut self) -> Option> { + if let Some(pending) = self.pending.take() { + if pending.replace <= Instant::now() { + // Check if the bucket is full + if self.nodes.is_full() { + // Apply bucket filters + + if self.nodes[0].status.is_connected() { + // The bucket is full with connected nodes. Drop the pending node. + return None; + } + // Check the custom filter + if let Some(filter) = self.filter.as_ref() { + if !filter.filter( + &pending.node.value, + &mut self.iter().map(|node| &node.value), + ) { + // The pending node doesn't satisfy the bucket filter. Drop the pending + // node. + return None; + } + } + // Check the incoming node restriction + if pending.status().is_connected() && pending.status().is_incoming() { + // Make sure this doesn't violate the incoming conditions + if self.is_max_incoming() { + // The pending node doesn't satisfy the incoming/outgoing limits. Drop + // the pending node. + return None; + } + } + + // The pending node will be inserted. + let inserted = pending.node.key.clone(); + // A connected pending node goes at the end of the list for + // the connected peers, removing the least-recently connected. + if pending.status().is_connected() { + let evicted = Some(self.nodes.remove(0)); + self.first_connected_pos = self + .first_connected_pos + .map_or_else(|| Some(self.nodes.len()), |p| p.checked_sub(1)); + self.nodes.push(pending.node); + return Some(AppliedPending { inserted, evicted }); + } + // A disconnected pending node goes at the end of the list + // for the disconnected peers. + else if let Some(p) = self.first_connected_pos { + if let Some(insert_pos) = p.checked_sub(1) { + let evicted = Some(self.nodes.remove(0)); + self.nodes.insert(insert_pos, pending.node); + return Some(AppliedPending { inserted, evicted }); + } + } else { + // All nodes are disconnected. Insert the new node as the most + // recently disconnected, removing the least-recently disconnected. + let evicted = Some(self.nodes.remove(0)); + self.nodes.push(pending.node); + return Some(AppliedPending { inserted, evicted }); + } + } else { + // There is room in the bucket, so just insert the pending node. + let inserted = pending.node.key.clone(); + match self.insert(pending.node) { + InsertResult::Inserted => { + return Some(AppliedPending { + inserted, + evicted: None, + }) + } + InsertResult::Full => unreachable!("Bucket cannot be full"), + InsertResult::Pending { .. } | InsertResult::NodeExists => { + error!("Bucket is not full or double node") + } + InsertResult::FailedFilter => debug!("Pending node failed filter"), + InsertResult::TooManyIncoming => { + debug!("Pending node failed incoming filter") + } + } + } + } else { + self.pending = Some(pending); + } + } + + None + } + + /// Updates the status of the pending node, if any. + pub fn update_pending(&mut self, status: NodeStatus) { + if let Some(pending) = &mut self.pending { + pending.node.status = status + } + } + + /// Updates the status of the node referred to by the given key, if it is + /// in the bucket. If the node is not in the bucket, or the update would violate a bucket + /// filter or incoming limits, returns an update result indicating the outcome. + /// An optional connection state can be given. If this is omitted the connection state will not + /// be modified. + pub fn update_status( + &mut self, + key: &Key, + state: ConnectionState, + direction: Option, + ) -> UpdateResult { + // Remove the node from its current position and then reinsert it + // with the desired status, which puts it at the end of either the + // prefix list of disconnected nodes or the suffix list of connected + // nodes (i.e. most-recently disconnected or most-recently connected, + // respectively). + if let Some(pos) = self.position(key) { + // Remove the node from its current position. + let mut node = self.nodes.remove(pos.0); + let old_status = node.status; + node.status.state = state; + if let Some(direction) = direction { + node.status.direction = direction; + } + + // Flag indicating if this update modified the entry. + let not_modified = old_status == node.status; + // Flag indicating we are upgrading to a connected status + let is_connected = matches!(state, ConnectionState::Connected); + + // Adjust `first_connected_pos` accordingly. + match old_status.state { + ConnectionState::Connected => { + if self.first_connected_pos.map_or(false, |p| p == pos.0) + && pos.0 == self.nodes.len() + { + // It was the last connected node. + self.first_connected_pos = None + } + } + ConnectionState::Disconnected => { + self.first_connected_pos = + self.first_connected_pos.and_then(|p| p.checked_sub(1)) + } + } + // If the least-recently connected node re-establishes its + // connected status, drop the pending node. + if pos == Position(0) && is_connected { + self.pending = None + } + // Reinsert the node with the desired status. + match self.insert(node) { + InsertResult::Inserted => { + if not_modified { + UpdateResult::NotModified + } else if !old_status.is_connected() && is_connected { + // This means the status was updated from a disconnected state to connected + // state + UpdateResult::UpdatedAndPromoted + } else { + UpdateResult::Updated + } + } + InsertResult::TooManyIncoming => { + UpdateResult::Failed(FailureReason::TooManyIncoming) + } + // Node could not be inserted. None of these should be possible. + InsertResult::FailedFilter => { + // If the filter is non-deterministic, potentially a re-insertion of the same + // node can fail the filter. + UpdateResult::Failed(FailureReason::BucketFilter) + } + InsertResult::NodeExists => { + unreachable!("The node was removed and shouldn't already exist") + } + InsertResult::Full => { + unreachable!("The node was removed so the bucket cannot be full") + } + InsertResult::Pending { .. } => { + unreachable!("The node was removed so can't be added as pending") + } + } + } else if let Some(pending) = &mut self.pending { + if &pending.node.key == key { + pending.node.status.state = state; + if let Some(direction) = direction { + pending.node.status.direction = direction; + } + UpdateResult::UpdatedPending + } else { + UpdateResult::Failed(FailureReason::KeyNonExistant) + } + } else { + UpdateResult::Failed(FailureReason::KeyNonExistant) + } + } + + /// Updates the value of the node referred to by the given key, if it is + /// in the bucket. If the node is not in the bucket, or the update would violate a bucket + /// filter or incoming limits, returns false and removes the node from the bucket. + /// NOTE: This does not update the position of the node in the table. It node will be removed + /// if it fails the filter however. + pub fn update_value(&mut self, key: &Key, value: TVal) -> UpdateResult { + // Remove the node from its current position, check the filter and add it back in. + if let Some(Position(pos)) = self.position(key) { + // Remove the node from its current position. + let mut node = self.nodes.remove(pos); + if node.value == value { + self.nodes.insert(pos, node); + UpdateResult::NotModified + } else { + // Check bucket filter + if let Some(filter) = self.filter.as_ref() { + if !filter.filter(&value, &mut self.iter().map(|node| &node.value)) { + // Node is removed, update the `first_connected_pos` accordingly. + self.update_first_connected_pos_for_removal(pos); + + return UpdateResult::Failed(FailureReason::BucketFilter); + } + } + node.value = value; + self.nodes.insert(pos, node); + UpdateResult::Updated + } + } else if let Some(pending) = &mut self.pending { + if &pending.node.key == key { + pending.node.value = value; + UpdateResult::UpdatedPending + } else { + UpdateResult::Failed(FailureReason::KeyNonExistant) + } + } else { + UpdateResult::Failed(FailureReason::KeyNonExistant) + } + } + + /// Inserts a new node into the bucket with the given status. + /// + /// The status of the node to insert determines the result as follows: + /// + /// * [`ConnectionState::Connected`] for both directions: If the bucket is full and either all nodes are connected + /// or there is already a pending node, insertion fails with [`InsertResult::Full`]. + /// If the bucket is full but at least one node is disconnected and there is no pending + /// node, the new node is inserted as pending, yielding [`InsertResult::Pending`]. + /// Otherwise the bucket has free slots and the new node is added to the end of the + /// bucket as the most-recently connected node. + /// + /// * [`ConnectionState::Disconnected`]: If the bucket is full, insertion fails with + /// [`InsertResult::Full`]. Otherwise the bucket has free slots and the new node + /// is inserted at the position preceding the first connected node, + /// i.e. as the most-recently disconnected node. If there are no connected nodes, + /// the new node is added as the last element of the bucket. + /// + /// The insert can fail if a provided bucket filter does not pass. If a node is attempted + /// to be inserted that doesn't pass the bucket filter, [`InsertResult::FailedFilter`] will be + /// returned. Similarly, if the inserted node would violate the `max_incoming` value, the + /// result will return [`InsertResult::TooManyIncoming`]. + pub fn insert(&mut self, node: Node) -> InsertResult { + // Prevent inserting duplicate nodes. + if self.position(&node.key).is_some() { + return InsertResult::NodeExists; + } + + // check bucket filter + if let Some(filter) = self.filter.as_ref() { + if !filter.filter(&node.value, &mut self.iter().map(|node| &node.value)) { + return InsertResult::FailedFilter; + } + } + + let inserting_pending = self + .pending + .as_ref() + .map(|pending| pending.node.key == node.key) + .unwrap_or_default(); + + let insert_result = match node.status.state { + ConnectionState::Connected => { + if node.status.is_incoming() { + // check the maximum counter + if self.is_max_incoming() { + return InsertResult::TooManyIncoming; + } + } + if self.nodes.is_full() { + if self.first_connected_pos == Some(0) || self.pending.is_some() { + return InsertResult::Full; + } else { + self.pending = Some(PendingNode { + node, + replace: Instant::now() + self.pending_timeout, + }); + return InsertResult::Pending { + disconnected: self.nodes[0].key.clone(), + }; + } + } + + let pos = self.nodes.len(); + self.first_connected_pos = self.first_connected_pos.or(Some(pos)); + self.nodes.push(node); + InsertResult::Inserted + } + ConnectionState::Disconnected => { + if self.nodes.is_full() { + return InsertResult::Full; + } + + if let Some(ref mut first_connected_pos) = self.first_connected_pos { + self.nodes.insert(*first_connected_pos, node); + *first_connected_pos += 1; + } else { + self.nodes.push(node); + } + InsertResult::Inserted + } + }; + + // If we inserted the node, make sure there is no pending node of the same key. This can + // happen when a pending node is inserted, a node gets removed from the bucket, freeing up + // space and then re-inserted here. + if matches!(insert_result, InsertResult::Inserted) && inserting_pending { + self.pending = None + } + insert_result + } + + /// Removes a node from the bucket. + pub fn remove(&mut self, key: &Key) -> bool { + if let Some(Position(position)) = self.position(key) { + self.nodes.remove(position); + self.update_first_connected_pos_for_removal(position); + self.apply_pending(); + true + } else { + false + } + } + + /// Gets the number of entries currently in the bucket. + pub fn num_entries(&self) -> usize { + self.nodes.len() + } + + /// Gets the number of entries in the bucket that are considered connected. + pub fn num_connected(&self) -> usize { + self.first_connected_pos.map_or(0, |i| self.nodes.len() - i) + } + + /// Gets the number of entries in the bucket that are considered disconnected. + pub fn num_disconnected(&self) -> usize { + self.nodes.len() - self.num_connected() + } + + /// Gets the position of an node in the bucket. + pub fn position(&self, key: &Key) -> Option { + self.nodes.iter().position(|p| &p.key == key).map(Position) + } + + /// Returns the state of the node at the given position. + pub fn status(&self, pos: Position) -> NodeStatus { + if let Some(node) = self.nodes.get(pos.0) { + node.status + } else { + // If the node isn't in the bucket, return the worst kind of state. + NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Incoming, + } + } + } + + /// Gets a mutable reference to the node identified by the given key. + /// + /// Returns `None` if the given key does not refer to an node in the + /// bucket. + fn get_mut(&mut self, key: &Key) -> Option<&mut Node> { + self.nodes.iter_mut().find(move |p| &p.key == key) + } + + /// Gets a reference to the node identified by the given key. + /// + /// Returns `None` if the given key does not refer to an node in the + /// bucket. + pub fn get(&self, key: &Key) -> Option<&Node> { + self.nodes.iter().find(move |p| &p.key == key) + } + + /// Returns whether the bucket has reached its maximum capacity of incoming nodes. This is used + /// to determine if new nodes can be added to the bucket or not. + fn is_max_incoming(&self) -> bool { + self.nodes + .iter() + .filter(|node| node.status.is_connected() && node.status.is_incoming()) + .count() + >= self.max_incoming + } + + /// Update the `first_connected_pos` for the removal of a node at position `removed_pos`. + /// + /// This function should be called *after* removing the node. It has the ability to destroy + /// the bucket's internal consistency invariants if misused. + fn update_first_connected_pos_for_removal(&mut self, removed_pos: usize) { + self.first_connected_pos = self.first_connected_pos.and_then(|fcp| { + if removed_pos < fcp { + // Remove node is before the first connected position, decrement it. + Some(fcp - 1) + } else { + // FCP is unchanged, unless there are no nodes following the removed node. + Some(fcp).filter(|_| fcp < self.nodes.len()) + } + }); + } +} + +impl std::fmt::Debug + for KBucket +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut builder = f.debug_struct("KBucket"); + let _ = builder.field("nodes", &self.nodes); + let _ = builder.field("first_connected_pos", &self.first_connected_pos); + let _ = builder.field("pending", &self.pending); + let _ = builder.field("pending_timeout", &self.pending_timeout); + let _ = builder.field("filter", &self.filter.is_some()); + let _ = builder.field("max_incoming", &self.max_incoming); + builder.finish() + } +} + +impl std::fmt::Display for ConnectionDirection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + ConnectionDirection::Incoming => write!(f, "Incoming"), + ConnectionDirection::Outgoing => write!(f, "Outgoing"), + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use enr::NodeId; + use quickcheck::*; + use rand_07::Rng; + use std::{ + collections::{HashSet, VecDeque}, + hash::Hash, + }; + + fn connected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Connected, + direction: ConnectionDirection::Outgoing, + } + } + + fn disconnected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Outgoing, + } + } + + pub fn arbitrary_node_id(g: &mut G) -> NodeId { + let mut node_id = [0u8; 32]; + g.fill_bytes(&mut node_id); + NodeId::new(&node_id) + } + + impl KBucket + where + V: Eq + std::fmt::Debug, + { + /// Check invariants that must hold on the `KBucket`. + fn check_invariants(&self) { + self.check_first_connected_pos(); + self.check_status_ordering(); + self.check_max_incoming_nodes(); + } + + /// Check that the cached `first_connected_pos` field matches the list of nodes. + fn check_first_connected_pos(&self) { + let first_connected_pos = self + .nodes + .iter() + .position(|node| node.status.is_connected()); + assert_eq!(self.first_connected_pos, first_connected_pos); + } + + /// Check that disconnected nodes are listed first, follow by connected nodes. + fn check_status_ordering(&self) { + let first_connected_pos = self.first_connected_pos.unwrap_or(self.nodes.len()); + assert!(self.nodes[..first_connected_pos] + .iter() + .all(|n| !n.status.is_connected())); + assert!(self.nodes[first_connected_pos..] + .iter() + .all(|n| n.status.is_connected())); + } + + /// Check that the limit on incoming connections is respected. + fn check_max_incoming_nodes(&self) { + let number_of_incoming_nodes = self + .nodes + .iter() + .filter(|n| n.status.is_connected() && n.status.is_incoming()) + .count(); + assert!(number_of_incoming_nodes <= self.max_incoming); + } + } + + impl Arbitrary for KBucket + where + V: Arbitrary + Eq, + { + fn arbitrary(g: &mut G) -> KBucket { + let timeout = Duration::from_secs(g.gen_range(1, g.size() as u64)); + let mut bucket = KBucket::::new(timeout, MAX_NODES_PER_BUCKET, None); + let num_nodes = g.gen_range(1, MAX_NODES_PER_BUCKET + 1); + for _ in 0..num_nodes { + loop { + let node = Node::arbitrary(g); + match bucket.insert(node) { + InsertResult::Inserted => break, + InsertResult::TooManyIncoming => {} + _ => panic!(), + } + } + } + bucket + } + } + + impl Arbitrary for Node + where + V: Arbitrary + Eq, + { + fn arbitrary(g: &mut G) -> Self { + let key = Key::from(arbitrary_node_id(g)); + Node { + key: key.clone(), + value: V::arbitrary(g), + status: NodeStatus::arbitrary(g), + } + } + } + + impl Arbitrary for NodeStatus { + fn arbitrary(g: &mut G) -> NodeStatus { + match g.gen_range(1, 4) { + 1 => NodeStatus { + direction: ConnectionDirection::Incoming, + state: ConnectionState::Connected, + }, + 2 => NodeStatus { + direction: ConnectionDirection::Outgoing, + state: ConnectionState::Connected, + }, + 3 => NodeStatus { + direction: ConnectionDirection::Incoming, + state: ConnectionState::Disconnected, + }, + 4 => NodeStatus { + direction: ConnectionDirection::Outgoing, + state: ConnectionState::Disconnected, + }, + x => unreachable!("Should not generate numbers out of this range {}", x), + } + } + } + + impl Arbitrary for Position { + fn arbitrary(g: &mut G) -> Position { + Position(g.gen_range(0, MAX_NODES_PER_BUCKET)) + } + } + + // Fill a bucket with random nodes with the given status. + fn fill_bucket(bucket: &mut KBucket, status: NodeStatus) { + let num_entries_start = bucket.num_entries(); + for i in 0..MAX_NODES_PER_BUCKET - num_entries_start { + let key = Key::from(NodeId::random()); + let node = Node { + key, + value: (), + status, + }; + assert_eq!(InsertResult::Inserted, bucket.insert(node)); + assert_eq!(bucket.num_entries(), num_entries_start + i + 1); + } + } + + /// Filter for testing that returns true if the value is in `self.set`. + #[derive(Debug, Clone)] + pub struct SetFilter { + set: HashSet, + } + + impl Filter for SetFilter + where + T: Clone + Hash + Eq + Send + Sync + 'static, + { + fn filter(&self, value: &T, _: &mut dyn Iterator) -> bool { + self.set.contains(value) + } + } + + /// Enum encoding mutable method calls on KBucket, implements Arbitrary. + #[derive(Debug, Clone)] + pub enum Action + where + TVal: Eq, + { + Insert(Node), + Remove(usize), + UpdatePending(NodeStatus), + ApplyPending, + UpdateStatus(usize, NodeStatus), + UpdateValue(usize, TVal), + } + + impl Arbitrary for Action + where + V: Arbitrary + Eq, + { + fn arbitrary(g: &mut G) -> Self { + match g.gen_range(0, 6) { + 0 => Action::Insert(<_>::arbitrary(g)), + 1 => Action::Remove(<_>::arbitrary(g)), + 2 => Action::UpdatePending(<_>::arbitrary(g)), + 3 => Action::ApplyPending, + 4 => Action::UpdateStatus(<_>::arbitrary(g), <_>::arbitrary(g)), + 5 => Action::UpdateValue(<_>::arbitrary(g), <_>::arbitrary(g)), + _ => panic!("wrong number of action variants"), + } + } + } + + impl KBucket + where + V: Eq + std::fmt::Debug, + { + fn apply_action(&mut self, action: Action) { + match action { + Action::Insert(node) => { + let _ = self.insert(node); + } + Action::Remove(pos) => { + if let Some(key) = self.key_of_pos(pos) { + self.remove(&key); + } + } + Action::UpdatePending(status) => { + self.update_pending(status); + } + Action::ApplyPending => { + self.apply_pending(); + } + Action::UpdateStatus(pos, status) => { + if let Some(key) = self.key_of_pos(pos) { + let _ = self.update_status(&key, status.state, Some(status.direction)); + } + } + Action::UpdateValue(pos, value) => { + if let Some(key) = self.key_of_pos(pos) { + let _ = self.update_value(&key, value); + } + } + } + } + + fn key_of_pos(&self, pos: usize) -> Option> { + let num_nodes = self.num_entries(); + if num_nodes > 0 { + let pos = pos % num_nodes; + let key = self.nodes[pos].key.clone(); + Some(key) + } else { + None + } + } + } + + #[test] + fn ordering() { + fn prop(status: Vec) -> bool { + let mut bucket = + KBucket::::new(Duration::from_secs(1), MAX_NODES_PER_BUCKET, None); + + // The expected lists of connected and disconnected nodes. + let mut connected = VecDeque::new(); + let mut disconnected = VecDeque::new(); + + // Fill the bucket, thereby populating the expected lists in insertion order. + for status in status { + let key = Key::from(NodeId::random()); + let node = Node { + key: key.clone(), + value: (), + status, + }; + let full = bucket.num_entries() == MAX_NODES_PER_BUCKET; + if let InsertResult::Inserted = bucket.insert(node) { + let vec = if status.is_connected() { + &mut connected + } else { + &mut disconnected + }; + if full { + vec.pop_front(); + } + vec.push_back((status, key.clone())); + } + } + + // Get all nodes from the bucket, together with their status. + let mut nodes = bucket + .iter() + .map(|n| (n.status, n.key.clone())) + .collect::>(); + + // Split the list of nodes at the first connected node. + let first_connected_pos = nodes.iter().position(|(status, _)| status.is_connected()); + assert_eq!(bucket.first_connected_pos, first_connected_pos); + let tail = first_connected_pos.map_or(Vec::new(), |p| nodes.split_off(p)); + + // All nodes before the first connected node must be disconnected and + // in insertion order. Similarly, all remaining nodes must be connected + // and in insertion order. + disconnected == nodes && connected == tail + } + + quickcheck(prop as fn(_) -> _); + } + + #[test] + fn full_bucket() { + let mut bucket = + KBucket::::new(Duration::from_secs(1), MAX_NODES_PER_BUCKET, None); + + let disconnected_status = NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Outgoing, + }; + // Fill the bucket with disconnected nodes. + fill_bucket(&mut bucket, disconnected_status); + + // Trying to insert another disconnected node fails. + let key = Key::from(NodeId::random()); + let node = Node { + key, + value: (), + status: disconnected_status, + }; + match bucket.insert(node) { + InsertResult::Full => {} + x => panic!("{:?}", x), + } + + // One-by-one fill the bucket with connected nodes, replacing the disconnected ones. + for i in 0..MAX_NODES_PER_BUCKET { + let first = bucket.iter().next().unwrap(); + let first_disconnected = first.clone(); + assert_eq!(first.status, disconnected_status); + + // Add a connected node, which is expected to be pending, scheduled to + // replace the first (i.e. least-recently connected) node. + let key = Key::from(NodeId::random()); + let node = Node { + key: key.clone(), + value: (), + status: connected_state(), + }; + match bucket.insert(node.clone()) { + InsertResult::Pending { disconnected } => { + assert_eq!(disconnected, first_disconnected.key) + } + x => panic!("{:?}", x), + } + + // Trying to insert another connected node fails. + match bucket.insert(node.clone()) { + InsertResult::Full => {} + x => panic!("{:?}", x), + } + + assert!(bucket.pending().is_some()); + + // Apply the pending node. + let pending = bucket.pending_mut().expect("No pending node."); + pending.set_ready_at(Instant::now() - Duration::from_secs(1)); + let result = bucket.apply_pending(); + assert_eq!( + result, + Some(AppliedPending { + inserted: key.clone(), + evicted: Some(first_disconnected) + }) + ); + assert_eq!( + Some(connected_state()), + bucket.iter().map(|v| v.status).last() + ); + assert!(bucket.pending().is_none()); + assert_eq!( + Some(MAX_NODES_PER_BUCKET - (i + 1)), + bucket.first_connected_pos + ); + } + + assert!(bucket.pending().is_none()); + assert_eq!(MAX_NODES_PER_BUCKET, bucket.num_entries()); + + // Trying to insert another connected node fails. + let key = Key::from(NodeId::random()); + let node = Node { + key, + value: (), + status: connected_state(), + }; + match bucket.insert(node) { + InsertResult::Full => {} + x => panic!("{:?}", x), + } + } + + #[test] + fn full_bucket_discard_pending() { + let mut bucket = + KBucket::::new(Duration::from_secs(1), MAX_NODES_PER_BUCKET, None); + fill_bucket(&mut bucket, disconnected_state()); + let first = bucket.iter().next().unwrap(); + let first_disconnected = first.clone(); + + // Add a connected pending node. + let key = Key::from(NodeId::random()); + let node = Node { + key: key.clone(), + value: (), + status: connected_state(), + }; + if let InsertResult::Pending { disconnected } = bucket.insert(node) { + assert_eq!(&disconnected, &first_disconnected.key); + } else { + panic!() + } + assert!(bucket.pending().is_some()); + + // Update the status of the first disconnected node to be connected. + let _ = bucket.update_status(&first_disconnected.key, ConnectionState::Connected, None); + + // The pending node has been discarded. + assert!(bucket.pending().is_none()); + assert!(bucket.iter().all(|n| n.key != key)); + + // The initially disconnected node is now the most-recently connected. + assert_eq!( + Some((&first_disconnected.key, connected_state())), + bucket.iter().map(|v| (&v.key, v.status)).last() + ); + assert_eq!( + bucket.position(&first_disconnected.key).map(|p| p.0), + bucket.first_connected_pos + ); + assert_eq!(1, bucket.num_connected()); + assert_eq!(MAX_NODES_PER_BUCKET - 1, bucket.num_disconnected()); + } + + /// No duplicate nodes can be inserted via the apply_pending function. + #[test] + fn full_bucket_applied_no_duplicates() { + // First fill the bucket with connected nodes. + let mut bucket = + KBucket::::new(Duration::from_secs(1), MAX_NODES_PER_BUCKET, None); + fill_bucket(&mut bucket, connected_state()); + + let first = bucket.iter().next().unwrap().clone(); + + let third = bucket.iter().nth(2).unwrap().clone(); + + // Set the first connected node as disconnected + + assert_eq!( + bucket.update_status(&first.key, ConnectionState::Disconnected, None), + UpdateResult::Updated + ); + + // Add a connected pending node. + let key = Key::from(NodeId::random()); + let node = Node { + key: key.clone(), + value: (), + status: connected_state(), + }; + + // Add a pending node + if let InsertResult::Pending { disconnected } = bucket.insert(node.clone()) { + assert_eq!(&disconnected, &first.key); + } else { + panic!() + } + assert!(bucket.pending().is_some()); + + // A misc node gets dropped, because it may not pass a filter when updating its connection + // status. + bucket.remove(&third.key); + + // The pending nodes status gets updated + // Apply pending gets called within kbuckets, so we mimic here. + // The pending time hasn't elapsed so nothing should occur. + assert_eq!(bucket.apply_pending(), None); + assert_eq!(bucket.insert(node.clone()), InsertResult::Inserted); + assert!(bucket.pending.is_none()); + + // Speed up the pending time + if let Some(pending) = bucket.pending.as_mut() { + pending.replace = Instant::now() - Duration::from_secs(1); + } + + // At some later time apply pending + assert_eq!(bucket.apply_pending(), None); + // And try and update the status of the pending node + assert_eq!( + bucket.update_status(&node.key, ConnectionState::Connected, None), + UpdateResult::NotModified + ); + } + + #[test] + fn bucket_update_status() { + fn prop(mut bucket: KBucket, pos: Position, status: NodeStatus) -> bool { + let num_nodes = bucket.num_entries(); + + // Capture position and key of the random node to update. + let pos = pos.0 % num_nodes; + let key = bucket.nodes[pos].key.clone(); + + // Record the (ordered) list of status of all nodes in the bucket. + let mut expected = bucket + .iter() + .map(|n| (n.key.clone(), n.status)) + .collect::>(); + + // Update the node in the bucket. + let _ = bucket.update_status(&key, status.state, Some(status.direction)); + + // Check that the bucket now contains the node with the new status, + // preserving the status and relative order of all other nodes. + let expected_pos = if status.is_connected() { + num_nodes - 1 + } else { + bucket.first_connected_pos.unwrap_or(num_nodes) - 1 + }; + expected.remove(pos); + expected.insert(expected_pos, (key, status)); + let actual = bucket + .iter() + .map(|n| (n.key.clone(), n.status)) + .collect::>(); + expected == actual + } + + quickcheck(prop as fn(_, _, _) -> _); + } + + #[test] + fn bucket_update_value_with_filtering() { + fn prop( + mut bucket: KBucket, + pos: Position, + value: u8, + value_matches_filter: bool, + ) -> bool { + // Initialise filter. + let filter = SetFilter { + set: value_matches_filter.then(|| value).into_iter().collect(), + }; + bucket.filter = Some(Box::new(filter)); + + let num_nodes = bucket.num_entries(); + + // Capture position and key of the random node to update. + let pos = pos.0 % num_nodes; + let key = bucket.nodes[pos].key.clone(); + + // Record the (ordered) list of values of all nodes in the bucket. + let mut expected = bucket + .iter() + .map(|n| (n.key.clone(), n.value)) + .collect::>(); + + // Update the node in the bucket. + let _ = bucket.update_value(&key, value); + + bucket.check_invariants(); + + // Check that the bucket now contains the node with the new value, or that the node + // has been removed. + if value_matches_filter || expected[pos].1 == value { + expected[pos].1 = value; + } else { + expected.remove(pos); + } + let actual = bucket + .iter() + .map(|n| (n.key.clone(), n.value)) + .collect::>(); + expected == actual + } + + quickcheck(prop as fn(_, _, _, _) -> _); + } + + /// Hammer a bucket with random mutations to ensure invariants are always maintained. + #[test] + fn random_actions_with_filtering() { + fn prop( + initial_nodes: Vec>, + pending_timeout_millis: u64, + max_incoming: usize, + filter_set: HashSet, + actions: Vec>, + ) -> bool { + let filter = SetFilter { set: filter_set }; + let pending_timeout = Duration::from_millis(pending_timeout_millis); + let mut kbucket = + KBucket::::new(pending_timeout, max_incoming, Some(Box::new(filter))); + + for node in initial_nodes { + let _ = kbucket.insert(node); + } + + for action in actions { + kbucket.apply_action(action); + kbucket.check_invariants(); + } + true + } + + quickcheck(prop as fn(_, _, _, _, _) -> _); + } + + #[test] + fn table_update_status_connection() { + let max_incoming = 7; + let mut bucket = KBucket::::new(Duration::from_secs(1), max_incoming, None); + + let mut incoming_connected = 0; + let mut keys = Vec::new(); + for _ in 0..MAX_NODES_PER_BUCKET { + let key = Key::from(NodeId::random()); + keys.push(key.clone()); + incoming_connected += 1; + let direction = if incoming_connected <= max_incoming { + ConnectionDirection::Incoming + } else { + ConnectionDirection::Outgoing + }; + let status = NodeStatus { + state: ConnectionState::Connected, + direction, + }; + let node = Node { + key: key.clone(), + value: (), + status, + }; + assert_eq!(InsertResult::Inserted, bucket.insert(node)); + } + + // Bucket is full + // Attempt to modify a new state + let result = bucket.update_status( + &keys[max_incoming], + ConnectionState::Disconnected, + Some(ConnectionDirection::Incoming), + ); + assert_eq!(result, UpdateResult::Updated); + let result = bucket.update_status( + &keys[max_incoming], + ConnectionState::Connected, + Some(ConnectionDirection::Outgoing), + ); + assert_eq!(result, UpdateResult::UpdatedAndPromoted); + let result = bucket.update_status( + &keys[max_incoming], + ConnectionState::Connected, + Some(ConnectionDirection::Outgoing), + ); + assert_eq!(result, UpdateResult::NotModified); + let result = bucket.update_status( + &keys[max_incoming], + ConnectionState::Connected, + Some(ConnectionDirection::Incoming), + ); + assert_eq!(result, UpdateResult::Failed(FailureReason::TooManyIncoming)); + } + + #[test] + fn bucket_max_incoming_nodes() { + fn prop(status: Vec) -> bool { + let max_incoming_nodes = 5; + let mut bucket = + KBucket::::new(Duration::from_secs(1), max_incoming_nodes, None); + + // The expected lists of connected and disconnected nodes. + let mut connected = VecDeque::new(); + let mut disconnected = VecDeque::new(); + + // Fill the bucket, thereby populating the expected lists in insertion order. + for status in status { + let key = Key::from(NodeId::random()); + let node = Node { + key: key.clone(), + value: (), + status, + }; + let full = bucket.num_entries() == MAX_NODES_PER_BUCKET; + match bucket.insert(node) { + InsertResult::Inserted => { + let vec = if status.is_connected() { + &mut connected + } else { + &mut disconnected + }; + if full { + vec.pop_front(); + } + vec.push_back((status, key.clone())); + } + InsertResult::FailedFilter => break, + _ => {} + } + } + + // Check all invariants. + bucket.check_invariants(); + + // Get all nodes from the bucket, together with their status. + let mut nodes = bucket + .iter() + .map(|n| (n.status, n.key.clone())) + .collect::>(); + + // Split the list of nodes at the first connected node. + let tail = bucket + .first_connected_pos + .map_or(Vec::new(), |p| nodes.split_off(p)); + + // All nodes before the first connected node must be disconnected and + // in insertion order. Similarly, all remaining nodes must be connected + // and in insertion order. + // The number of incoming nodes does not exceed the maximum limit. + disconnected == nodes && connected == tail + } + + quickcheck(prop as fn(_) -> _); + } +} diff --git a/version-meld/discv5/src/kbucket/entry.rs b/version-meld/discv5/src/kbucket/entry.rs new file mode 100644 index 0000000..e978613 --- /dev/null +++ b/version-meld/discv5/src/kbucket/entry.rs @@ -0,0 +1,198 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +//! The `Entry` API for querying and modifying the entries of a `KBucketsTable` +//! representing the nodes participating in the Kademlia DHT. + +pub use super::{ + bucket::{ + AppliedPending, ConnectionState, InsertResult, Node, NodeStatus, MAX_NODES_PER_BUCKET, + }, + key::*, + ConnectionDirection, +}; + +use super::*; + +/// An immutable by-reference view of a bucket entry. +pub struct EntryRefView<'a, TPeerId, TVal: Eq> { + /// The node represented by the entry. + pub node: NodeRefView<'a, TPeerId, TVal>, + /// The status of the node identified by the key. + pub status: NodeStatus, +} + +/// An immutable by-reference view of a `Node`. +pub struct NodeRefView<'a, TPeerId, TVal: Eq> { + pub key: &'a Key, + pub value: &'a TVal, +} + +/// A cloned, immutable view of an entry that is either present in a bucket +/// or pending insertion. +#[derive(Clone, Debug)] +pub struct EntryView { + /// The node represented by the entry. + pub node: Node, + /// The status of the node. + pub status: NodeStatus, +} + +impl AsRef> for EntryView { + fn as_ref(&self) -> &Key { + &self.node.key + } +} + +/// A reference into a single entry of a `KBucketsTable`. +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +pub enum Entry<'a, TPeerId, TVal: Eq> { + /// The entry is present in a bucket. + Present(PresentEntry<'a, TPeerId, TVal>, NodeStatus), + /// The entry is pending insertion in a bucket. + Pending(PendingEntry<'a, TPeerId, TVal>, NodeStatus), + /// The entry is absent and may be inserted. + Absent(AbsentEntry<'a, TPeerId, TVal>), + /// The entry represents the local node. + SelfEntry, +} + +/// The internal representation of the different states of an `Entry`, +/// referencing the associated key and bucket. +#[derive(Debug)] +struct EntryRef<'a, TPeerId, TVal: Eq> { + bucket: &'a mut KBucket, + key: &'a Key, +} + +impl<'a, TPeerId, TVal> Entry<'a, TPeerId, TVal> +where + TPeerId: Clone, + TVal: Eq, +{ + /// Creates a new `Entry` for a `Key`, encapsulating access to a bucket. + pub(super) fn new(bucket: &'a mut KBucket, key: &'a Key) -> Self { + if let Some(pos) = bucket.position(key) { + let status = bucket.status(pos); + Entry::Present(PresentEntry::new(bucket, key), status) + } else if let Some(pending) = bucket.as_pending(key) { + let status = pending.status(); + Entry::Pending(PendingEntry::new(bucket, key), status) + } else { + Entry::Absent(AbsentEntry::new(bucket, key)) + } + } +} + +/// An entry present in a bucket. +#[derive(Debug)] +pub struct PresentEntry<'a, TPeerId, TVal: Eq>(EntryRef<'a, TPeerId, TVal>); + +impl<'a, TPeerId, TVal> PresentEntry<'a, TPeerId, TVal> +where + TPeerId: Clone, + TVal: Eq, +{ + fn new(bucket: &'a mut KBucket, key: &'a Key) -> Self { + PresentEntry(EntryRef { bucket, key }) + } + + /// Returns the value associated with the key. + pub fn value(&self) -> &TVal { + &self + .0 + .bucket + .get(self.0.key) + .expect("We can only build a ConnectedEntry if the entry is in the bucket; QED") + .value + } + + /// Sets the status of the entry. + pub fn update(self, state: ConnectionState, direction: Option) -> Self { + let _ = self.0.bucket.update_status(self.0.key, state, direction); + Self::new(self.0.bucket, self.0.key) + } + + /// Removes the entry from the table. + pub fn remove(self) { + self.0.bucket.remove(self.0.key); + } +} + +/// An entry waiting for a slot to be available in a bucket. +#[derive(Debug)] +pub struct PendingEntry<'a, TPeerId, TVal: Eq>(EntryRef<'a, TPeerId, TVal>); + +impl<'a, TPeerId, TVal: Eq> PendingEntry<'a, TPeerId, TVal> +where + TPeerId: Clone, + TVal: Eq, +{ + fn new(bucket: &'a mut KBucket, key: &'a Key) -> Self { + PendingEntry(EntryRef { bucket, key }) + } + + /// Returns the value associated with the key. + pub fn value(&mut self) -> &mut TVal { + self.0 + .bucket + .pending_mut() + .expect("We can only build a ConnectedPendingEntry if the entry is pending; QED") + .value_mut() + } + + /// Updates the status of the pending entry. + pub fn update(self, status: NodeStatus) -> PendingEntry<'a, TPeerId, TVal> { + self.0.bucket.update_pending(status); + PendingEntry::new(self.0.bucket, self.0.key) + } + + /// Removes the entry from the table. + pub fn remove(self) { + self.0.bucket.remove(self.0.key); + } +} + +/// An entry that is not present in any bucket. +#[derive(Debug)] +pub struct AbsentEntry<'a, TPeerId, TVal: Eq>(EntryRef<'a, TPeerId, TVal>); + +impl<'a, TPeerId, TVal> AbsentEntry<'a, TPeerId, TVal> +where + TPeerId: Clone, + TVal: Eq, +{ + fn new(bucket: &'a mut KBucket, key: &'a Key) -> Self { + AbsentEntry(EntryRef { bucket, key }) + } + + /// Attempts to insert the entry into a bucket. + pub fn insert(self, value: TVal, status: NodeStatus) -> InsertResult { + self.0.bucket.insert(Node { + key: self.0.key.clone(), + value, + status, + }) + } +} diff --git a/version-meld/discv5/src/kbucket/filter.rs b/version-meld/discv5/src/kbucket/filter.rs new file mode 100644 index 0000000..36fe7fa --- /dev/null +++ b/version-meld/discv5/src/kbucket/filter.rs @@ -0,0 +1,96 @@ +//! Provides a trait that can be implemented to apply a filter to a table or bucket. + +use crate::Enr; + +pub trait Filter: FilterClone + Send + Sync { + fn filter( + &self, + value_to_be_inserted: &TVal, + other_vals: &mut dyn Iterator, + ) -> bool; +} + +/// Allow the trait objects to be cloneable. +pub trait FilterClone { + fn clone_box(&self) -> Box>; +} + +impl FilterClone for T +where + T: 'static + Filter + Clone, +{ + fn clone_box(&self) -> Box> { + Box::new(self.clone()) + } +} + +impl Clone for Box> { + fn clone(&self) -> Box> { + self.clone_box() + } +} + +// Implementation of an IP filter for buckets and for tables + +/// Number of permitted nodes in the same /24 subnet per table. +const MAX_NODES_PER_SUBNET_TABLE: usize = 10; +/// The number of nodes permitted in the same /24 subnet per bucket. +const MAX_NODES_PER_SUBNET_BUCKET: usize = 2; + +#[derive(Clone)] +pub struct IpTableFilter; + +impl Filter for IpTableFilter { + fn filter( + &self, + value_to_be_inserted: &Enr, + other_vals: &mut dyn Iterator, + ) -> bool { + ip_filter(value_to_be_inserted, other_vals, MAX_NODES_PER_SUBNET_TABLE) + } +} + +#[derive(Clone)] +pub struct IpBucketFilter; + +impl Filter for IpBucketFilter { + fn filter( + &self, + value_to_be_inserted: &Enr, + other_vals: &mut dyn Iterator, + ) -> bool { + ip_filter( + value_to_be_inserted, + other_vals, + MAX_NODES_PER_SUBNET_BUCKET, + ) + } +} + +fn ip_filter( + value_to_be_inserted: &Enr, + other_vals: &mut dyn Iterator, + limit: usize, +) -> bool { + if let Some(ip) = value_to_be_inserted.ip() { + let mut count = 0; + for enr in other_vals { + // Ignore duplicates + if enr == value_to_be_inserted { + continue; + } + + // Count the same /24 subnet + if let Some(other_ip) = enr.ip() { + if other_ip.octets()[0..3] == ip.octets()[0..3] { + count += 1; + } + } + if count >= limit { + return false; + } + } + } + // No IP, so no restrictions + true +} diff --git a/version-meld/discv5/src/kbucket/key.rs b/version-meld/discv5/src/kbucket/key.rs new file mode 100644 index 0000000..f27765a --- /dev/null +++ b/version-meld/discv5/src/kbucket/key.rs @@ -0,0 +1,168 @@ +// Copyright 2018 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +#![allow(clippy::all)] + +use enr::NodeId; +use sha2::digest::generic_array::{typenum::U32, GenericArray}; +use uint::construct_uint; + +construct_uint! { + /// 256-bit unsigned integer. + pub(super) struct U256(4); +} + +/// A `Key` is a cryptographic hash, identifying both the nodes participating in +/// the Kademlia DHT, as well as records stored in the DHT. +/// +/// The set of all `Key`s defines the Kademlia keyspace. +/// +/// `Key`s have an XOR metric as defined in the Kademlia paper, i.e. the bitwise XOR of +/// the hash digests, interpreted as an integer. See [`Key::distance`]. +/// +/// A `Key` preserves the preimage of type `T` of the hash function. See [`Key::preimage`]. +#[derive(Clone, Debug)] +pub struct Key { + preimage: T, + hash: GenericArray, +} + +impl PartialEq for Key { + fn eq(&self, other: &Key) -> bool { + self.hash == other.hash + } +} + +impl Eq for Key {} + +impl AsRef> for Key { + fn as_ref(&self) -> &Key { + self + } +} + +impl Key { + /// Construct a new `Key` by providing the raw 32 byte hash. + pub fn new_raw(preimage: T, hash: GenericArray) -> Key { + Key { preimage, hash } + } + + /// Borrows the preimage of the key. + pub fn preimage(&self) -> &T { + &self.preimage + } + + /// Converts the key into its preimage. + pub fn into_preimage(self) -> T { + self.preimage + } + + /// Computes the distance of the keys according to the XOR metric. + pub fn distance(&self, other: &Key) -> Distance { + let a = U256::from(self.hash.as_slice()); + let b = U256::from(other.hash.as_slice()); + Distance(a ^ b) + } + + // Used in the FINDNODE query outside of the k-bucket implementation. + /// Computes the integer log-2 distance between two keys, assuming a 256-bit + /// key. The output returns None if the key's are identical. The range is 1-256. + pub fn log2_distance(&self, other: &Key) -> Option { + let xor_dist = self.distance(other); + let log_dist = u64::from(256 - xor_dist.0.leading_zeros()); + if log_dist == 0 { + None + } else { + Some(log_dist) + } + } +} + +impl From for Key { + fn from(node_id: NodeId) -> Self { + Key { + preimage: node_id, + hash: *GenericArray::from_slice(&node_id.raw()), + } + } +} + +/// A distance between two `Key`s. +#[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Debug)] +pub struct Distance(pub(super) U256); + +#[cfg(test)] +mod tests { + use super::*; + use crate::kbucket::bucket::tests::arbitrary_node_id; + use quickcheck::*; + + impl Arbitrary for Key { + fn arbitrary(g: &mut G) -> Key { + Key::from(arbitrary_node_id(g)) + } + } + + #[test] + fn identity() { + fn prop(a: Key) -> bool { + a.distance(&a) == Distance::default() + } + quickcheck(prop as fn(_) -> _) + } + + #[test] + fn symmetry() { + fn prop(a: Key, b: Key) -> bool { + a.distance(&b) == b.distance(&a) + } + quickcheck(prop as fn(_, _) -> _) + } + + #[test] + fn triangle_inequality() { + fn prop(a: Key, b: Key, c: Key) -> TestResult { + let ab = a.distance(&b); + let bc = b.distance(&c); + let (ab_plus_bc, overflow) = ab.0.overflowing_add(bc.0); + if overflow { + TestResult::discard() + } else { + TestResult::from_bool(a.distance(&c) <= Distance(ab_plus_bc)) + } + } + quickcheck(prop as fn(_, _, _) -> _) + } + + #[test] + fn unidirectionality() { + fn prop(a: Key, b: Key) -> bool { + let d = a.distance(&b); + (0..100).all(|_| { + let c = Key::from(NodeId::random()); + a.distance(&c) != d || b == c + }) + } + quickcheck(prop as fn(_, _) -> _) + } +} diff --git a/version-meld/discv5/src/lib.rs b/version-meld/discv5/src/lib.rs new file mode 100644 index 0000000..1aac813 --- /dev/null +++ b/version-meld/discv5/src/lib.rs @@ -0,0 +1,135 @@ +#![warn(rust_2018_idioms)] +#![deny(rustdoc::broken_intra_doc_links)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::needless_doctest_main)] +//! An implementation of [Discovery V5](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md). +//! +//! # Overview +//! +//! Discovery v5 is a protocol designed for encrypted peer discovery and topic advertisement. Each peer/node +//! on the network is identified via it's ENR ([Ethereum Name +//! Record](https://eips.ethereum.org/EIPS/eip-778)), which is essentially a signed key-value store +//! containing the node's public key and optionally IP address and port. +//! +//! Discv5 employs a kademlia-like routing table to store and manage discovered peers and topics. The +//! protocol allows for external IP discovery in NAT environments through regular PING/PONG's with +//! discovered nodes. Nodes return the external IP address that they have received and a simple +//! majority is chosen as our external IP address. If an external IP address is updated, this is +//! produced as an event to notify the swarm (if one is used for this behaviour). +//! +//! For a simple CLI discovery service see [discv5-cli](https://github.com/AgeManning/discv5-cli) +//! +//! This protocol is split into four main sections/layers: +//! +//! * Socket - The [`socket`] module is responsible for opening the underlying UDP socket. It +//! creates individual tasks for sending/encoding and receiving/decoding packets from the UDP +//! socket. +//! * Handler - The protocol's communication is encrypted with `AES_GCM`. All node communication +//! undergoes a handshake, which results in a [`Session`]. [`Session`]'s are established when +//! needed and get dropped after a timeout. This section manages the creation and maintenance of +//! sessions between nodes and the encryption/decryption of packets from the socket. It is realised by the [`handler::Handler`] struct and it runs in its own task. +//! * Service - This section contains the protocol-level logic. In particular it manages the +//! routing table of known ENR's, topic registration/advertisement and performs various queries +//! such as peer discovery. This section is realised by the [`Service`] struct. This also runs in +//! it's own thread. +//! * Application - This section is the user-facing API which can start/stop the underlying +//! tasks, initiate queries and obtain metrics about the underlying server. +//! +//! ## Event Stream +//! +//! The [`Discv5`] struct provides access to an event-stream which allows the user to listen to +//! [`Discv5Event`] that get generated from the underlying server. The stream can be obtained +//! from the [`Discv5::event_stream()`] function. +//! +//! ## Runtimes +//! +//! Discv5 requires a tokio runtime with timing and io enabled. An explicit runtime can be given +//! via the configuration. See the [`Discv5ConfigBuilder`] for further details. Such a runtime +//! must implement the [`Executor`] trait. +//! +//! If an explicit runtime is not provided via the configuration parameters, it is assumed that +//! a tokio runtime is present when creating the [`Discv5`] struct. The struct will use the +//! existing runtime for spawning the underlying server tasks. If a runtime is not present, the +//! creation of the [`Discv5`] struct will panic. +//! +//! # Usage +//! +//! A simple example of creating this service is as follows: +//! +//! ```rust +//! use discv5::{enr, enr::{CombinedKey, NodeId}, TokioExecutor, Discv5, Discv5ConfigBuilder}; +//! use std::net::SocketAddr; +//! +//! // listening address and port +//! let listen_addr = "0.0.0.0:9000".parse::().unwrap(); +//! +//! // construct a local ENR +//! let enr_key = CombinedKey::generate_secp256k1(); +//! let enr = enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); +//! +//! // build the tokio executor +//! let mut runtime = tokio::runtime::Builder::new_multi_thread() +//! .thread_name("Discv5-example") +//! .enable_all() +//! .build() +//! .unwrap(); +//! +//! // default configuration +//! let config = Discv5ConfigBuilder::new().build(); +//! +//! // construct the discv5 server +//! let mut discv5 = Discv5::new(enr, enr_key, config).unwrap(); +//! +//! // In order to bootstrap the routing table an external ENR should be added +//! // This can be done via add_enr. I.e.: +//! // discv5.add_enr() +//! +//! // start the discv5 server +//! runtime.block_on(discv5.start(listen_addr)); +//! +//! // run a find_node query +//! runtime.block_on(async { +//! let found_nodes = discv5.find_node(NodeId::random()).await.unwrap(); +//! println!("Found nodes: {:?}", found_nodes); +//! }); +//! ``` +//! +//! [`Discv5`]: struct.Discv5.html +//! [`Discv5Event`]: enum.Discv5Event.html +//! [`Discv5Config`]: config/struct.Discv5Config.html +//! [`Discv5ConfigBuilder`]: config/struct.Discv5ConfigBuilder.html +//! [Packet]: packet/enum.Packet.html +//! [`Service`]: service/struct.Service.html +//! [`Session`]: session/struct.Session.html + +mod config; +mod discv5; +mod error; +mod executor; +pub mod handler; +pub mod kbucket; +mod lru_time_cache; +pub mod metrics; +mod node_info; +pub mod packet; +pub mod permit_ban; +mod query_pool; +pub mod rpc; +pub mod service; +pub mod socket; + +#[macro_use] +extern crate lazy_static; + +pub type Enr = enr::Enr; + +pub use crate::discv5::{Discv5, Discv5Event}; +pub use config::{Discv5Config, Discv5ConfigBuilder}; +pub use error::{Discv5Error, QueryError, RequestError, ResponseError}; +pub use executor::{Executor, TokioExecutor}; +pub use kbucket::{ConnectionDirection, ConnectionState, Key}; +pub use permit_ban::PermitBanList; +pub use service::TalkRequest; +pub use socket::{RateLimiter, RateLimiterBuilder}; +// re-export the ENR crate +pub use enr; diff --git a/version-meld/discv5/src/lru_time_cache.rs b/version-meld/discv5/src/lru_time_cache.rs new file mode 100644 index 0000000..67606c3 --- /dev/null +++ b/version-meld/discv5/src/lru_time_cache.rs @@ -0,0 +1,253 @@ +use hashlink::LinkedHashMap; +use std::{ + hash::Hash, + time::{Duration, Instant}, +}; + +pub struct LruTimeCache { + map: LinkedHashMap, + /// The time elements remain in the cache. + ttl: Duration, + /// The max size of the cache. + capacity: usize, +} + +impl LruTimeCache { + pub fn new(ttl: Duration, capacity: Option) -> LruTimeCache { + let capacity = if let Some(cap) = capacity { + cap + } else { + usize::MAX + }; + LruTimeCache { + map: LinkedHashMap::new(), + ttl, + capacity, + } + } + + /// Inserts a key-value pair into the cache. + pub fn insert(&mut self, key: K, value: V) { + let now = Instant::now(); + self.map.insert(key, (value, now)); + + if self.map.len() > self.capacity { + self.map.pop_front(); + } + } + + /// Retrieves a reference to the value stored under `key`, or `None` if the key doesn't exist. + /// Also removes expired elements and updates the time. + pub fn get(&mut self, key: &K) -> Option<&V> { + self.get_mut(key).map(|value| &*value) + } + + /// Retrieves a mutable reference to the value stored under `key`, or `None` if the key doesn't exist. + /// Also removes expired elements and updates the time. + pub fn get_mut(&mut self, key: &K) -> Option<&mut V> { + let now = Instant::now(); + self.remove_expired_values(now); + + match self.map.raw_entry_mut().from_key(key) { + hashlink::linked_hash_map::RawEntryMut::Occupied(mut occupied) => { + occupied.get_mut().1 = now; + occupied.to_back(); + Some(&mut occupied.into_mut().0) + } + hashlink::linked_hash_map::RawEntryMut::Vacant(_) => None, + } + } + + /// Returns a reference to the value with the given `key`, if present and not expired, without + /// updating the timestamp. + pub fn peek(&self, key: &K) -> Option<&V> { + if let Some((value, time)) = self.map.get(key) { + return if *time + self.ttl >= Instant::now() { + Some(value) + } else { + None + }; + } + + None + } + + /// Returns the size of the cache, i.e. the number of cached non-expired key-value pairs. + pub fn len(&mut self) -> usize { + self.remove_expired_values(Instant::now()); + self.map.len() + } + + /// Removes a key-value pair from the cache, returning the value at the key if the key + /// was previously in the map. + pub fn remove(&mut self, key: &K) -> Option { + self.map.remove(key).map(|v| v.0) + } + + /// Removes expired items from the cache. + fn remove_expired_values(&mut self, now: Instant) { + let mut expired_keys = vec![]; + + for (key, (_, time)) in self.map.iter_mut() { + if *time + self.ttl >= now { + break; + } + expired_keys.push(key.clone()); + } + + for k in expired_keys { + self.map.remove(&k); + } + } +} + +#[cfg(test)] +mod tests { + use crate::lru_time_cache::LruTimeCache; + use std::time::Duration; + + #[test] + fn insert() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), None); + + cache.insert(1, 10); + cache.insert(2, 20); + cache.insert(3, 30); + + assert_eq!(Some(&10), cache.get(&1)); + assert_eq!(Some(&20), cache.get(&2)); + assert_eq!(Some(&30), cache.get(&3)); + } + + #[test] + fn capacity() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), Some(2)); + + cache.insert(1, 10); + cache.insert(2, 20); + assert_eq!(2, cache.len()); + + cache.insert(3, 30); + assert_eq!(2, cache.len()); + assert_eq!(Some(&20), cache.get(&2)); + assert_eq!(Some(&30), cache.get(&3)); + } + + #[test] + fn get() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), Some(2)); + + cache.insert(1, 10); + cache.insert(2, 20); + assert_eq!(Some(&10), cache.get(&1)); + + cache.insert(3, 30); + // `1` is alive as `get()` updates the timestamp. + assert_eq!(Some(&10), cache.get(&1)); + // `2` is removed as `2` is oldest at the time `3` was inserted. + assert_eq!(None, cache.get(&2)); + } + + #[test] + fn get_mut() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), None); + + cache.insert(1, 10); + let v = cache.get_mut(&1).expect("should have value"); + *v = 100; + + assert_eq!(Some(&100), cache.get(&1)); + } + + #[test] + fn peek() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), Some(2)); + + cache.insert(1, 10); + cache.insert(2, 20); + assert_eq!(Some(&10), cache.peek(&1)); + + cache.insert(3, 30); + // `1` is removed as `peek()` does not update the time. + assert_eq!(None, cache.peek(&1)); + assert_eq!(Some(&20), cache.get(&2)); + } + + #[test] + fn len() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), None); + + assert_eq!(0, cache.len()); + + cache.insert(1, 10); + cache.insert(2, 20); + cache.insert(3, 30); + assert_eq!(3, cache.len()); + } + + #[test] + fn remove() { + let mut cache = LruTimeCache::new(Duration::from_secs(10), None); + + cache.insert(1, 10); + assert_eq!(Some(10), cache.remove(&1)); + assert_eq!(None, cache.get(&1)); + assert_eq!(None, cache.remove(&1)); + } + + mod ttl { + use crate::lru_time_cache::LruTimeCache; + use std::{thread::sleep, time::Duration}; + + const TTL: Duration = Duration::from_millis(100); + + #[test] + fn get() { + let mut cache = LruTimeCache::new(TTL, None); + cache.insert(1, 10); + assert_eq!(Some(&10), cache.get(&1)); + + sleep(TTL); + assert_eq!(None, cache.get(&1)); + } + + #[test] + fn peek() { + let mut cache = LruTimeCache::new(TTL, None); + cache.insert(1, 10); + assert_eq!(Some(&10), cache.peek(&1)); + + sleep(TTL); + assert_eq!(None, cache.peek(&1)); + } + + #[test] + fn len() { + let mut cache = LruTimeCache::new(TTL, None); + cache.insert(1, 10); + assert_eq!(1, cache.len()); + + sleep(TTL); + assert_eq!(0, cache.len()); + } + + #[test] + fn ttl() { + let mut cache = LruTimeCache::new(TTL, None); + cache.insert(1, 10); + sleep(TTL / 4); + cache.insert(2, 20); + sleep(TTL / 4); + cache.insert(3, 30); + sleep(TTL / 4); + cache.insert(4, 40); + sleep(TTL / 4); + + assert_eq!(3, cache.len()); + assert_eq!(None, cache.get(&1)); + assert_eq!(Some(&20), cache.get(&2)); + assert_eq!(Some(&30), cache.get(&3)); + assert_eq!(Some(&40), cache.get(&4)); + } + } +} diff --git a/version-meld/discv5/src/metrics.rs b/version-meld/discv5/src/metrics.rs new file mode 100644 index 0000000..2e6f2fc --- /dev/null +++ b/version-meld/discv5/src/metrics.rs @@ -0,0 +1,72 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +lazy_static! { + pub static ref METRICS: InternalMetrics = InternalMetrics::default(); +} + +/// A collection of metrics used throughout the server. +pub struct InternalMetrics { + /// The number of active UDP sessions that are currently established. + pub active_sessions: AtomicUsize, + /// The number of seconds to store received packets to taking a moving average over. + pub moving_window: u64, + /// The number of unsolicited requests received per moving window. + pub unsolicited_requests_per_window: AtomicUsize, + /// The number of bytes sent. + pub bytes_sent: AtomicUsize, + /// The number of bytes received. + pub bytes_recv: AtomicUsize, +} + +impl Default for InternalMetrics { + fn default() -> Self { + InternalMetrics { + moving_window: 5, + active_sessions: AtomicUsize::new(0), + unsolicited_requests_per_window: AtomicUsize::new(0), + bytes_sent: AtomicUsize::new(0), + bytes_recv: AtomicUsize::new(0), + } + } +} + +impl InternalMetrics { + pub fn add_recv_bytes(&self, bytes: usize) { + let current_bytes_recv = self.bytes_recv.load(Ordering::Relaxed); + self.bytes_recv + .store(current_bytes_recv.saturating_add(bytes), Ordering::Relaxed); + } + + pub fn add_sent_bytes(&self, bytes: usize) { + let current_bytes_sent = self.bytes_sent.load(Ordering::Relaxed); + self.bytes_sent + .store(current_bytes_sent.saturating_add(bytes), Ordering::Relaxed); + } +} + +#[derive(Clone, Debug)] +/// The publicly accessible metrics that can be obtained from the Discv5 server. +pub struct Metrics { + /// The number of active UDP sessions that are currently established. + pub active_sessions: usize, + /// The number of unsolicited requests received per second (averaged over a moving window). + pub unsolicited_requests_per_second: f64, + /// The number of bytes sent. + pub bytes_sent: usize, + /// The number of bytes received. + pub bytes_recv: usize, +} + +impl From<&METRICS> for Metrics { + fn from(internal_metrics: &METRICS) -> Self { + Metrics { + active_sessions: internal_metrics.active_sessions.load(Ordering::Relaxed), + unsolicited_requests_per_second: internal_metrics + .unsolicited_requests_per_window + .load(Ordering::Relaxed) as f64 + / internal_metrics.moving_window as f64, + bytes_sent: internal_metrics.bytes_sent.load(Ordering::Relaxed), + bytes_recv: internal_metrics.bytes_recv.load(Ordering::Relaxed), + } + } +} diff --git a/version-meld/discv5/src/node_info.rs b/version-meld/discv5/src/node_info.rs new file mode 100644 index 0000000..3bf7010 --- /dev/null +++ b/version-meld/discv5/src/node_info.rs @@ -0,0 +1,190 @@ +use super::*; +use crate::Enr; +use enr::{CombinedPublicKey, NodeId}; +use std::net::SocketAddr; + +#[cfg(feature = "libp2p")] +use libp2p_core::{identity::PublicKey, multiaddr::Protocol, multihash, Multiaddr}; + +/// This type relaxes the requirement of having an ENR to connect to a node, to allow for unsigned +/// connection types, such as multiaddrs. +#[derive(Debug, Clone, PartialEq)] +pub enum NodeContact { + /// We know the ENR of the node we are contacting. + Enr(Box), + /// We don't have an ENR, but have enough information to start a handshake. + /// + /// The handshake will request the ENR at the first opportunity. + /// The public key can be derived from multiaddr's whose keys can be inlined. The `TryFrom` + /// implementation for `String` and `MultiAddr`. This is gated behind the `libp2p` feature. + Raw { + /// An ENR compatible public key, required for handshaking with peers. + public_key: Box, + /// The socket address and `NodeId` of the peer to connect to. + node_address: Box, + }, +} + +impl NodeContact { + pub fn node_id(&self) -> NodeId { + match self { + NodeContact::Enr(enr) => enr.node_id(), + NodeContact::Raw { node_address, .. } => node_address.node_id, + } + } + + pub fn seq_no(&self) -> Option { + match self { + NodeContact::Enr(enr) => Some(enr.seq()), + _ => None, + } + } + + pub fn public_key(&self) -> CombinedPublicKey { + match self { + NodeContact::Enr(ref enr) => enr.public_key(), + NodeContact::Raw { public_key, .. } => *public_key.clone(), + } + } + + pub fn is_enr(&self) -> bool { + matches!(self, NodeContact::Enr(_)) + } + + pub fn udp_socket(&self) -> Result { + match self { + NodeContact::Enr(enr) => enr + .udp_socket() + .ok_or("ENR does not contain an IP and UDP port"), + NodeContact::Raw { node_address, .. } => Ok(node_address.socket_addr), + } + } + + pub fn node_address(&self) -> Result { + let socket_addr = self.udp_socket()?; + let node_id = self.node_id(); + Ok(NodeAddress { + socket_addr, + node_id, + }) + } +} + +impl From for NodeContact { + fn from(enr: Enr) -> Self { + NodeContact::Enr(Box::new(enr)) + } +} + +#[cfg(feature = "libp2p")] +impl std::convert::TryFrom for NodeContact { + type Error = &'static str; + + fn try_from(multiaddr: Multiaddr) -> Result { + // The multiaddr must contain either the ip4 or ip6 protocols, the UDP protocol and the P2P + // protocol with either secp256k1 or ed25519 keys. + + // perform a single pass and try to fill all required protocols from the multiaddr + let mut ip_addr = None; + let mut udp_port = None; + let mut p2p = None; + + for protocol in multiaddr.into_iter() { + match protocol { + Protocol::Udp(port) => udp_port = Some(port), + Protocol::Ip4(addr) => ip_addr = Some(addr.into()), + Protocol::Ip6(addr) => ip_addr = Some(addr.into()), + Protocol::P2p(multihash) => p2p = Some(multihash), + _ => {} + } + } + + let udp_port = udp_port.ok_or("A UDP port must be specified in the multiaddr")?; + let ip_addr = ip_addr.ok_or("An IP address must be specified in the multiaddr")?; + let multihash = p2p.ok_or("The p2p protocol must be specified in the multiaddr")?; + + // verify the correct key type + if multihash.code() != u64::from(multihash::Code::Identity) { + return Err("The key type is unsupported"); + } + + let public_key: CombinedPublicKey = + match PublicKey::from_protobuf_encoding(&multihash.to_bytes()[2..]) + .map_err(|_| "Invalid public key")? + { + PublicKey::Secp256k1(pk) => { + // TODO: Remove libp2p dep to avoid conversion here + enr::k256::ecdsa::VerifyingKey::from_sec1_bytes(&pk.encode_uncompressed()) + .expect("Libp2p key conversion, always valid") + .into() + } + PublicKey::Ed25519(pk) => enr::ed25519_dalek::PublicKey::from_bytes(&pk.encode()) + .expect("Libp2p key conversion, always valid") + .into(), + _ => return Err("The key type is not supported"), + }; + + Ok(NodeContact::Raw { + public_key: Box::new(public_key.clone()), + node_address: Box::new(NodeAddress { + socket_addr: SocketAddr::new(ip_addr, udp_port), + node_id: public_key.into(), + }), + }) + } +} + +impl std::fmt::Display for NodeContact { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NodeContact::Enr(enr) => { + write!(f, "Node: {}, addr: {:?}", enr.node_id(), enr.udp_socket()) + } + NodeContact::Raw { node_address, .. } => write!(f, "{}", node_address), + } + } +} + +/// A representation of an unsigned contactable node. +#[derive(PartialEq, Hash, Eq, Clone, Debug)] +pub struct NodeAddress { + /// The destination socket address. + pub socket_addr: SocketAddr, + /// The destination Node Id. + pub node_id: NodeId, +} + +impl Ord for NodeAddress { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let ord = self.node_id.raw().cmp(&other.node_id.raw()); + if ord != std::cmp::Ordering::Equal { + return ord; + } + let ord = self.socket_addr.ip().cmp(&other.socket_addr.ip()); + if ord != std::cmp::Ordering::Equal { + return ord; + } + self.socket_addr.port().cmp(&other.socket_addr.port()) + } +} + +impl PartialOrd for NodeAddress { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl NodeAddress { + pub fn new(socket_addr: SocketAddr, node_id: NodeId) -> Self { + Self { + socket_addr, + node_id, + } + } +} + +impl std::fmt::Display for NodeAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Node: {}, addr: {:?}", self.node_id, self.socket_addr) + } +} diff --git a/version-meld/discv5/src/packet/mod.rs b/version-meld/discv5/src/packet/mod.rs new file mode 100644 index 0000000..240618c --- /dev/null +++ b/version-meld/discv5/src/packet/mod.rs @@ -0,0 +1,899 @@ +//! This module defines the raw UDP message packets for Discovery v5.1. +//! +//! The [discv5 wire specification](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md) provides further information on UDP message packets as implemented in this module. +//! +//! A [`Packet`] defines all raw UDP message variants and implements the encoding/decoding +//! logic. +//! +//! Note, that all message encryption/decryption is handled outside of this module. +//! +//! [`Packet`]: enum.Packet.html + +use crate::{error::PacketError, Enr}; +use aes::{ + cipher::{generic_array::GenericArray, NewCipher, StreamCipher}, + Aes128Ctr, +}; +use enr::NodeId; +use rand::Rng; +use std::convert::TryInto; +use zeroize::Zeroize; + +/// The packet IV length (u128). +pub const IV_LENGTH: usize = 16; +/// The length of the static header. (6 byte protocol id, 2 bytes version, 1 byte kind, 12 byte +/// message nonce and a 2 byte authdata-size). +pub const STATIC_HEADER_LENGTH: usize = 23; +/// The message nonce length (in bytes). +pub const MESSAGE_NONCE_LENGTH: usize = 12; +/// The Id nonce length (in bytes). +pub const ID_NONCE_LENGTH: usize = 16; + +/// Protocol ID sent with each message. +const PROTOCOL_ID: &str = "discv5"; +/// The version sent with each handshake. +const VERSION: u16 = 0x0001; + +pub(crate) const MAX_PACKET_SIZE: usize = 1280; +// The smallest packet must be at least this large +// The 24 is the smallest auth_data that can be sent (it is by a WHOAREYOU packet) +const MIN_PACKET_SIZE: usize = IV_LENGTH + STATIC_HEADER_LENGTH + 24; + +/// Message Nonce (12 bytes). +pub type MessageNonce = [u8; MESSAGE_NONCE_LENGTH]; +/// The nonce sent in a WHOAREYOU packet. +pub type IdNonce = [u8; ID_NONCE_LENGTH]; + +// This is the WHOAREYOU authenticated data. +pub struct ChallengeData([u8; 63]); + +impl std::fmt::Debug for ChallengeData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(&self.0)) + } +} + +impl std::convert::TryFrom<&[u8]> for ChallengeData { + type Error = (); + + fn try_from(data: &[u8]) -> Result { + if data.len() != 63 { + return Err(()); + } + let mut result = [0; 63]; + result.copy_from_slice(data); + Ok(ChallengeData(result)) + } +} + +impl AsRef<[u8]> for ChallengeData { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Packet { + /// Random data unique to the packet. + pub iv: u128, + /// Protocol header. + pub header: PacketHeader, + /// The message contents itself. + pub message: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PacketHeader { + /// The nonce of the associated message + pub message_nonce: MessageNonce, + /// The type of packet this is. + pub kind: PacketKind, +} + +impl PacketHeader { + // Encodes the header to bytes to be included into the `masked-header` of the Packet Encoding. + pub fn encode(&self) -> Vec { + let auth_data = self.kind.encode(); + let mut buf = Vec::with_capacity(auth_data.len() + STATIC_HEADER_LENGTH); + buf.extend_from_slice(PROTOCOL_ID.as_bytes()); + buf.extend_from_slice(&VERSION.to_be_bytes()); + let kind: u8 = (&self.kind).into(); + buf.extend_from_slice(&kind.to_be_bytes()); + buf.extend_from_slice(&self.message_nonce); + buf.extend_from_slice(&(auth_data.len() as u16).to_be_bytes()); + buf.extend_from_slice(&auth_data); + buf + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum PacketKind { + /// An ordinary message. + Message { + /// The sending NodeId. + src_id: NodeId, + }, + /// A WHOAREYOU packet. + WhoAreYou { + /// The ID Nonce to be verified. + id_nonce: IdNonce, + /// The local node's current ENR sequence number. + enr_seq: u64, + }, + /// A handshake message. + Handshake { + /// The sending NodeId + src_id: NodeId, + /// Id-nonce signature that matches the WHOAREYOU request. + id_nonce_sig: Vec, + /// The ephemeral public key of the handshake. + ephem_pubkey: Vec, + /// The ENR record of the node if the WHOAREYOU request is out-dated. + enr_record: Option, + }, +} + +impl From<&PacketKind> for u8 { + fn from(kind: &PacketKind) -> Self { + match kind { + PacketKind::Message { .. } => 0, + PacketKind::WhoAreYou { .. } => 1, + PacketKind::Handshake { .. } => 2, + } + } +} + +impl PacketKind { + /// Encodes the packet type into its corresponding auth_data. + pub fn encode(&self) -> Vec { + match self { + PacketKind::Message { src_id } => src_id.raw().to_vec(), + PacketKind::WhoAreYou { id_nonce, enr_seq } => { + let mut auth_data = Vec::with_capacity(24); + auth_data.extend_from_slice(id_nonce); + auth_data.extend_from_slice(&enr_seq.to_be_bytes()); + debug_assert_eq!(auth_data.len(), 24); + auth_data + } + PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + } => { + let sig_size = id_nonce_sig.len(); + let pubkey_size = ephem_pubkey.len(); + let node_record = enr_record.as_ref().map(rlp::encode); + let expected_len = 34 + + sig_size + + pubkey_size + + node_record.as_ref().map(|x| x.len()).unwrap_or_default(); + + let mut auth_data = Vec::with_capacity(expected_len); + auth_data.extend_from_slice(&src_id.raw()); + auth_data.extend_from_slice(&(sig_size as u8).to_be_bytes()); + auth_data.extend_from_slice(&(pubkey_size as u8).to_be_bytes()); + auth_data.extend_from_slice(id_nonce_sig); + auth_data.extend_from_slice(ephem_pubkey); + if let Some(node_record) = node_record { + auth_data.extend_from_slice(&node_record); + } + debug_assert_eq!(auth_data.len(), expected_len); + auth_data + } + } + } + + pub fn is_whoareyou(&self) -> bool { + matches!(self, PacketKind::WhoAreYou { .. }) + } + + /// Decodes auth data, given the kind byte. + pub fn decode(kind: u8, auth_data: &[u8]) -> Result { + match kind { + 0 => { + // Decoding a message packet + // This should only contain a 32 byte NodeId. + if auth_data.len() != 32 { + return Err(PacketError::InvalidAuthDataSize); + } + + let src_id = NodeId::parse(auth_data).map_err(|_| PacketError::InvalidNodeId)?; + Ok(PacketKind::Message { src_id }) + } + 1 => { + // Decoding a WHOAREYOU packet authdata + // This must be 24 bytes long. + if auth_data.len() != 24 { + return Err(PacketError::InvalidAuthDataSize); + } + let id_nonce: IdNonce = auth_data[..ID_NONCE_LENGTH] + .try_into() + .expect("ID_NONCE_LENGTH must be the correct size"); + let enr_seq = u64::from_be_bytes( + auth_data[ID_NONCE_LENGTH..] + .try_into() + .expect("The length of the authdata must be 52 bytes"), + ); + + Ok(PacketKind::WhoAreYou { id_nonce, enr_seq }) + } + 2 => { + // Decoding a Handshake packet + // Start by decoding the header + // Length must contain 2 bytes of lengths and the src id (32 bytes) + if auth_data.len() < 34 { + // The auth_data header is too short + return Err(PacketError::InvalidAuthDataSize); + } + + // decode the src_id + let src_id = + NodeId::parse(&auth_data[..32]).map_err(|_| PacketError::InvalidNodeId)?; + + // decode the lengths + let sig_size = auth_data[32] as usize; + let eph_key_size = auth_data[32 + 1] as usize; + + let total_size = sig_size + eph_key_size; + + // verify the auth data length + if auth_data.len() < 34 + total_size { + return Err(PacketError::InvalidAuthDataSize); + } + + let remaining_data = &auth_data[32 + 2..]; + + let id_nonce_sig = remaining_data[0..sig_size as usize].to_vec(); + let ephem_pubkey = remaining_data[sig_size as usize..total_size].to_vec(); + + let enr_record = if remaining_data.len() > total_size { + Some( + rlp::decode::(&remaining_data[total_size..]) + .map_err(PacketError::InvalidEnr)?, + ) + } else { + None + }; + + Ok(PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }) + } + _ => Err(PacketError::UnknownPacket), + } + } +} + +/// The implementation of creating, encoding and decoding raw packets in the discv5.1 system. +// +// NOTE: We perform the encryption and decryption when we are encoding/decoding as this is +// performed in its own task in practice. The Handler can create the messages without the overhead +// of encryption/decryption and send them off to the send/recv tasks to perform the +// encryption/decryption. +impl Packet { + /// Creates an ordinary message packet. + pub fn new_message(src_id: NodeId, message_nonce: MessageNonce, ciphertext: Vec) -> Self { + let iv: u128 = rand::random(); + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Message { src_id }, + }; + + Packet { + iv, + header, + message: ciphertext, + } + } + + pub fn new_whoareyou(request_nonce: MessageNonce, id_nonce: IdNonce, enr_seq: u64) -> Self { + let iv: u128 = rand::random(); + + let header = PacketHeader { + message_nonce: request_nonce, + kind: PacketKind::WhoAreYou { id_nonce, enr_seq }, + }; + + Packet { + iv, + header, + message: Vec::new(), + } + } + + pub fn new_authheader( + src_id: NodeId, + message_nonce: MessageNonce, + id_nonce_sig: Vec, + ephem_pubkey: Vec, + enr_record: Option, + ) -> Self { + let iv: u128 = rand::random(); + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }, + }; + + Packet { + iv, + header, + message: Vec::new(), + } + } + + /// Generates a Packet::Random given a `tag`. + pub fn new_random(src_id: &NodeId) -> Result { + let mut ciphertext = [0u8; 44]; + rand::thread_rng() + .try_fill(&mut ciphertext[..]) + .map_err(|_| "PRNG failed")?; + + let message_nonce: MessageNonce = rand::random(); + + Ok(Self::new_message( + *src_id, + message_nonce, + ciphertext.to_vec(), + )) + } + + /// Returns true if the packet is a WHOAREYOU packet. + pub fn is_whoareyou(&self) -> bool { + match &self.header.kind { + PacketKind::WhoAreYou { .. } => true, + PacketKind::Message { .. } | PacketKind::Handshake { .. } => false, + } + } + + /// Non-challenge (WHOAREYOU) packets contain the src_id of the node. This function returns the + /// src_id in this case. + pub fn src_id(&self) -> Option { + match self.header.kind { + PacketKind::Message { src_id } => Some(src_id), + PacketKind::WhoAreYou { .. } => None, + PacketKind::Handshake { src_id, .. } => Some(src_id), + } + } + + /// Returns the message nonce if one exists. + pub fn message_nonce(&self) -> &MessageNonce { + &self.header.message_nonce + } + + /// Generates the authenticated data for this packet. + pub fn authenticated_data(&self) -> Vec { + let mut authenticated_data = self.iv.to_be_bytes().to_vec(); + authenticated_data.extend_from_slice(&self.header.encode()); + authenticated_data + } + + /// Encodes a packet to bytes and performs the AES-CTR encryption. + pub fn encode(self, dst_id: &NodeId) -> Vec { + let header = self.encrypt_header(dst_id); + let mut buf = Vec::with_capacity(IV_LENGTH + header.len() + self.message.len()); + buf.extend_from_slice(&self.iv.to_be_bytes()); + buf.extend_from_slice(&header); + buf.extend_from_slice(&self.message); + buf + } + + /// Creates the masked header of a packet performing the required AES-CTR encryption. + fn encrypt_header(&self, dst_id: &NodeId) -> Vec { + let mut header_bytes = self.header.encode(); + + /* Encryption is done inline + * + * This was split into its own library, but brought back to allow re-use of the cipher when + * performing decryption + */ + let mut key = GenericArray::clone_from_slice(&dst_id.raw()[..16]); + let mut nonce = GenericArray::clone_from_slice(&self.iv.to_be_bytes()); + + let mut cipher = Aes128Ctr::new(&key, &nonce); + cipher.apply_keystream(&mut header_bytes); + key.zeroize(); + nonce.zeroize(); + header_bytes + } + + /// Decodes a packet (data) given our local source id (src_key). + /// + /// This also returns the authenticated data for further decryption in the handler. + pub fn decode(src_id: &NodeId, data: &[u8]) -> Result<(Self, Vec), PacketError> { + if data.len() > MAX_PACKET_SIZE { + return Err(PacketError::TooLarge); + } + if data.len() < MIN_PACKET_SIZE { + return Err(PacketError::TooSmall); + } + + // attempt to decrypt the static header + let iv = data[..IV_LENGTH].to_vec(); + + /* Decryption is done inline + * + * This was split into its own library, but brought back to allow re-use of the cipher when + * performing the decryption + */ + let key = GenericArray::clone_from_slice(&src_id.raw()[..16]); + let nonce = GenericArray::clone_from_slice(&iv); + let mut cipher = Aes128Ctr::new(&key, &nonce); + + // Take the static header content + let mut static_header = data[IV_LENGTH..IV_LENGTH + STATIC_HEADER_LENGTH].to_vec(); + cipher.apply_keystream(&mut static_header); + + // double check the size + if static_header.len() != STATIC_HEADER_LENGTH { + return Err(PacketError::HeaderLengthInvalid(static_header.len())); + } + + // Check the protocol id + if &static_header[..6] != PROTOCOL_ID.as_bytes() { + return Err(PacketError::HeaderDecryptionFailed); + } + + // Check the version matches + let version = u16::from_be_bytes( + static_header[6..8] + .try_into() + .expect("Must be correct size"), + ); + if version != VERSION { + return Err(PacketError::InvalidVersion(version)); + } + + let flag = static_header[8]; + + // Obtain the message nonce + let message_nonce: MessageNonce = static_header[9..9 + MESSAGE_NONCE_LENGTH] + .try_into() + .expect("Must be correct size"); + + // The decryption was successful, decrypt the remaining header + let auth_data_size = u16::from_be_bytes( + static_header[STATIC_HEADER_LENGTH - 2..] + .try_into() + .expect("Can only be 2 bytes in size"), + ); + + let remaining_data = data[IV_LENGTH + STATIC_HEADER_LENGTH..].to_vec(); + if auth_data_size as usize > remaining_data.len() { + return Err(PacketError::InvalidAuthDataSize); + } + + let mut auth_data = data[IV_LENGTH + STATIC_HEADER_LENGTH + ..IV_LENGTH + STATIC_HEADER_LENGTH + auth_data_size as usize] + .to_vec(); + cipher.apply_keystream(&mut auth_data); + + let kind = PacketKind::decode(flag, &auth_data)?; + + let header = PacketHeader { + message_nonce, + kind, + }; + + // Any remaining bytes are message data + let message = data[IV_LENGTH + STATIC_HEADER_LENGTH + auth_data_size as usize..].to_vec(); + + if !message.is_empty() && header.kind.is_whoareyou() { + // do not allow extra bytes being sent in WHOAREYOU messages + return Err(PacketError::UnknownPacket); + } + + // build the authenticated data + let mut authenticated_data = iv.to_vec(); + authenticated_data.extend_from_slice(&static_header); + authenticated_data.extend_from_slice(&auth_data); + + let packet = Packet { + iv: u128::from_be_bytes(iv[..].try_into().expect("IV_LENGTH must be 16 bytes")), + header, + message, + }; + + Ok((packet, authenticated_data)) + } +} + +impl std::fmt::Display for Packet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Packet {{ iv: {}, header: {}, message {} }}", + hex::encode(self.iv.to_be_bytes()), + self.header.to_string(), + hex::encode(&self.message) + ) + } +} + +impl std::fmt::Display for PacketHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "PacketHeader {{ message_nonce: {}, kind: {} }}", + hex::encode(self.message_nonce), + self.kind.to_string() + ) + } +} + +impl std::fmt::Display for PacketKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PacketKind::Message { src_id } => write!(f, "Message {{ src_id: {} }}", src_id), + PacketKind::WhoAreYou { id_nonce, enr_seq } => write!( + f, + "WhoAreYou {{ id_nonce: {}, enr_seq: {} }}", + hex::encode(id_nonce), + enr_seq + ), + PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + } => write!( + f, + "Handshake {{ src_id : {}, id_nonce_sig: {}, ephem_pubkey: {}, enr_record {:?}", + hex::encode(src_id.raw()), + hex::encode(id_nonce_sig), + hex::encode(ephem_pubkey), + enr_record + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use enr::{CombinedKey, EnrKey}; + + fn init_log() { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); + } + + fn hex_decode(x: &'static str) -> Vec { + hex::decode(x).unwrap() + } + + fn node_key_1() -> CombinedKey { + CombinedKey::secp256k1_from_bytes(&mut hex_decode( + "eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f", + )) + .unwrap() + } + + fn node_key_2() -> CombinedKey { + CombinedKey::secp256k1_from_bytes(&mut hex_decode( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628", + )) + .unwrap() + } + + #[test] + fn packet_encode_random() { + init_log(); + let node_id_a: NodeId = node_key_1().public().into(); + let node_id_b: NodeId = node_key_2().public().into(); + + let expected_result = hex::decode("0000000000000000000000000000000b4f3ab1857252f96f758330a846b5d3d4a954d738dfcd6d1ed118ecc1d54f9b20fbf2be28db87805b23193e03c455d73d63ac71dfa91ffa010101010101010101010101").unwrap(); + let iv = 11u128; + let message_nonce = [12u8; MESSAGE_NONCE_LENGTH]; + let header = PacketHeader { + message_nonce, + kind: PacketKind::Message { src_id: node_id_a }, + }; + let message = [1u8; 12].to_vec(); + let packet = Packet { + iv, + header, + message, + }; + + let encoded = packet.encode(&node_id_b); + dbg!(hex::encode(&encoded)); + assert_eq!(expected_result, encoded); + } + + #[test] + fn packet_ref_test_encode_whoareyou() { + init_log(); + // reference input + let dst_id: NodeId = node_key_2().public().into(); + let request_nonce: MessageNonce = hex_decode("0102030405060708090a0b0c")[..] + .try_into() + .unwrap(); + let id_nonce: IdNonce = hex_decode("0102030405060708090a0b0c0d0e0f10")[..] + .try_into() + .unwrap(); + let enr_seq = 0u64; + let iv = 0u128; + + // expected hex output + let expected_output = hex::decode("00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d").unwrap(); + + let header = PacketHeader { + message_nonce: request_nonce, + kind: PacketKind::WhoAreYou { id_nonce, enr_seq }, + }; + + let packet = Packet { + iv, + header, + message: Vec::new(), + }; + + assert_eq!(packet.encode(&dst_id), expected_output); + } + + #[test] + fn packet_encode_handshake() { + init_log(); + // reference input + let src_id = NodeId::parse(&[3; 32]).unwrap(); + let dst_id = NodeId::parse(&[4; 32]).unwrap(); + let message_nonce: MessageNonce = [52u8; MESSAGE_NONCE_LENGTH]; + let id_nonce_sig = vec![5u8; 64]; + let ephem_pubkey = vec![6u8; 33]; + let enr_record = None; + let iv = 0u128; + + let expected_output = hex::decode("0000000000000000000000000000000035a14bcdb844ae25f36070f07e0b25e765ed72b4d69c99d5fe5a8d438a4b5b518dfead9d80200875c23e31d0acda6f1b2a6124a70e3dc1f2b8b0770f24d8da18605ff3f5b60b090c61515093a88ef4c02186f7d1b5c9a88fdb8cfae239f13e451758751561b439d8044e27cecdf646f2aa1c9ecbd5faf37eb67a4f6337f4b2a885391e631f72deb808c63bf0b0faed23d7117f7a2e1f98c28bd0").unwrap(); + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }, + }; + + let packet = Packet { + iv, + header, + message: Vec::new(), + }; + let encoded = packet.encode(&dst_id); + assert_eq!(encoded, expected_output); + } + + #[test] + fn packet_encode_handshake_enr() { + // reference input + let node_key_1 = node_key_1(); + let src_id: NodeId = node_key_1.public().into(); + let dst_id = NodeId::parse(&[4; 32]).unwrap(); + let message_nonce: MessageNonce = [52u8; MESSAGE_NONCE_LENGTH]; + let id_nonce_sig = vec![5u8; 64]; + let ephem_pubkey = vec![6u8; 33]; + let enr_record: Option = Some("enr:-IS4QHXuNmr1vGEGVGDcy_sG2BZ7a3A7mbKS812BK_9rToQiF1Lfknsi5o0xKLnGJbTzBssJCzMcIj8SOiu1O9dnfZEBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMT0UIR4Ch7I2GhYViQqbUhIIBUbQoleuTP-Wz1NJksuYN0Y3CCIyg".parse().unwrap()); + let iv = 0u128; + + let expected_output = hex::decode("0000000000000000000000000000000035a14bcdb844ae25f36070f07e0b25e765ed72b4d69d137c57dd97a97dd558d1d8e6e6b6fed699e55bb02b47d25562e0a6486ff2aba179f2b8b0770f24d8da18605ff3f5b60b090c61515093a88ef4c02186f7d1b5c9a88fdb8cfae239f13e451758751561b439d8044e27cecdf646f2aa1c9ecbd5faf37eb67a4f6337f4b2a885391e631f72deb808c63bf0b0faed23d7117f7a2e1f98c28bd0e908ce8b51cc89e592ed2efa671b8efd49e1ce8fd567fdb06ed308267d31f6bd75827812d21e8aa5a6c025e69b67faea57a15c1c9324d16938c4ebe71dba0bd5d7b00bb6de3e846ed37ef13a9d2e271f25233f5d97bbb026223dbe6595210f6a11cbee54589a0c0c20c7bb7c4c5bea46553480e1b7d4e83b2dd8305aac3b15fd9b1a1e13fda0").unwrap(); + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }, + }; + + let packet = Packet { + iv, + header, + message: Vec::new(), + }; + let encoded = packet.encode(&dst_id); + assert_eq!(encoded, expected_output); + } + + #[test] + fn packet_ref_test_encode_message() { + // reference input + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + let iv = 0u128; + + let message_nonce: MessageNonce = [52u8; MESSAGE_NONCE_LENGTH]; + let header = PacketHeader { + message_nonce, + kind: PacketKind::Message { src_id }, + }; + let ciphertext = vec![23; 12]; + + let expected_output = hex::decode("00000000000000000000000000000000088b3d43427746493294faf2af68559e215d0bce6652be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08da171717171717171717171717").unwrap(); + + let packet = Packet { + iv, + header, + message: ciphertext, + }; + let encoded = packet.encode(&dst_id); + assert_eq!(encoded, expected_output); + } + + /* This section provides functionality testing of the packets */ + #[test] + fn packet_encode_decode_random() { + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + + let packet = Packet::new_random(&src_id).unwrap(); + + let encoded_packet = packet.clone().encode(&dst_id); + let (decoded_packet, _authenticated_data) = + Packet::decode(&dst_id, &encoded_packet).unwrap(); + + assert_eq!(decoded_packet, packet); + } + + #[test] + fn packet_encode_decode_whoareyou() { + let dst_id: NodeId = node_key_2().public().into(); + + let message_nonce: MessageNonce = rand::random(); + let id_nonce: IdNonce = rand::random(); + let enr_seq: u64 = rand::random(); + + let packet = Packet::new_whoareyou(message_nonce, id_nonce, enr_seq); + + let encoded_packet = packet.clone().encode(&dst_id); + let (decoded_packet, _authenticated_data) = + Packet::decode(&dst_id, &encoded_packet).unwrap(); + + assert_eq!(decoded_packet, packet); + } + + #[test] + fn encode_decode_auth_packet() { + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + + let message_nonce: MessageNonce = rand::random(); + let id_nonce_sig = vec![13; 64]; + let pubkey = vec![11; 33]; + let enr_record = None; + + let packet = + Packet::new_authheader(src_id, message_nonce, id_nonce_sig, pubkey, enr_record); + + let encoded_packet = packet.clone().encode(&dst_id); + let (decoded_packet, _authenticated_data) = + Packet::decode(&dst_id, &encoded_packet).unwrap(); + + assert_eq!(decoded_packet, packet); + } + + #[test] + fn packet_decode_ref_ping() { + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + let message_nonce: MessageNonce = hex_decode("ffffffffffffffffffffffff")[..] + .try_into() + .unwrap(); + let iv = 0u128; + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Message { src_id }, + }; + let ciphertext = hex_decode("b84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc"); + let expected_packet = Packet { + iv, + header, + message: ciphertext, + }; + + let encoded_ref_packet = hex::decode("00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc").unwrap(); + + let (packet, _auth_data) = Packet::decode(&dst_id, &encoded_ref_packet).unwrap(); + assert_eq!(packet, expected_packet); + } + + #[test] + fn packet_decode_ref_ping_handshake() { + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + let message_nonce: MessageNonce = hex_decode("ffffffffffffffffffffffff")[..] + .try_into() + .unwrap(); + let id_nonce_sig = hex_decode("c0a04b36f276172afc66a62848eb0769800c670c4edbefab8f26785e7fda6b56506a3f27ca72a75b106edd392a2cbf8a69272f5c1785c36d1de9d98a0894b2db"); + let ephem_pubkey = + hex_decode("039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5"); + let enr_record = None; + let iv = 0u128; + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }, + }; + + let message = hex_decode("f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d839cf8"); + let expected_packet = Packet { + iv, + header, + message, + }; + + let decoded_ref_packet = hex::decode("00000000000000000000000000000000088b3d4342774649305f313964a39e55ea96c005ad521d8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08da4bb252012b2cba3f4f374a90a75cff91f142fa9be3e0a5f3ef268ccb9065aeecfd67a999e7fdc137e062b2ec4a0eb92947f0d9a74bfbf44dfba776b21301f8b65efd5796706adff216ab862a9186875f9494150c4ae06fa4d1f0396c93f215fa4ef524f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d839cf8").unwrap(); + + let (packet, _auth_data) = Packet::decode(&dst_id, &decoded_ref_packet).unwrap(); + assert_eq!(packet, expected_packet); + } + + #[test] + fn packet_decode_ref_ping_handshake_enr() { + let src_id: NodeId = node_key_1().public().into(); + let dst_id: NodeId = node_key_2().public().into(); + let message_nonce: MessageNonce = hex_decode("ffffffffffffffffffffffff")[..] + .try_into() + .unwrap(); + let id_nonce_sig = hex_decode("a439e69918e3f53f555d8ca4838fbe8abeab56aa55b056a2ac4d49c157ee719240a93f56c9fccfe7742722a92b3f2dfa27a5452f5aca8adeeab8c4d5d87df555"); + let ephem_pubkey = + hex_decode("039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5"); + let enr_record = Some("enr:-H24QBfhsHORjaMtZAZCx2LA4ngWmOSXH4qzmnd0atrYPwHnb_yHTFkkgIu-fFCJCILCuKASh6CwgxLR1ToX1Rf16ycBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMT0UIR4Ch7I2GhYViQqbUhIIBUbQoleuTP-Wz1NJksuQ".parse::().unwrap()); + let iv = 0u128; + + let header = PacketHeader { + message_nonce, + kind: PacketKind::Handshake { + src_id, + id_nonce_sig, + ephem_pubkey, + enr_record, + }, + }; + + let message = hex_decode("08d65093ccab5aa596a34d7511401987662d8cf62b139471"); + let expected_packet = Packet { + iv, + header, + message, + }; + + let encoded_ref_packet = hex::decode("00000000000000000000000000000000088b3d4342774649305f313964a39e55ea96c005ad539c8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08da4bb23698868350aaad22e3ab8dd034f548a1c43cd246be98562fafa0a1fa86d8e7a3b95ae78cc2b988ded6a5b59eb83ad58097252188b902b21481e30e5e285f19735796706adff216ab862a9186875f9494150c4ae06fa4d1f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb12a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e4539717307a0208cd208d65093ccab5aa596a34d7511401987662d8cf62b139471").unwrap(); + + let (packet, _auth_data) = Packet::decode(&dst_id, &encoded_ref_packet).unwrap(); + assert_eq!(packet, expected_packet); + } + + #[test] + fn packet_decode_invalid_packet_size() { + let src_id: NodeId = node_key_1().public().into(); + + let data = [0; MAX_PACKET_SIZE + 1]; + let result = Packet::decode(&src_id, &data); + assert_eq!(result, Err(PacketError::TooLarge)); + + let data = [0; MIN_PACKET_SIZE - 1]; + let result = Packet::decode(&src_id, &data); + assert_eq!(result, Err(PacketError::TooSmall)); + } +} diff --git a/version-meld/discv5/src/permit_ban.rs b/version-meld/discv5/src/permit_ban.rs new file mode 100644 index 0000000..32af5a6 --- /dev/null +++ b/version-meld/discv5/src/permit_ban.rs @@ -0,0 +1,27 @@ +use crate::node_info::NodeAddress; +use enr::NodeId; +use std::{ + collections::{HashMap, HashSet}, + net::IpAddr, + time::Instant, +}; + +#[derive(Debug, Clone, Default)] +pub struct PermitBanList { + /// A set of IPs which pass all filters. + pub permit_ips: HashSet, + /// A set of IPs whose packets get dropped instantly. + pub ban_ips: HashMap>, + /// A set of NodeIds which pass all filters. + pub permit_nodes: HashSet, + /// A set of NodeIds whose packets get dropped instantly. + pub ban_nodes: HashMap>, +} + +impl PermitBanList { + pub fn ban(&mut self, node_address: NodeAddress, time_to_unban: Option) { + self.ban_ips + .insert(node_address.socket_addr.ip(), time_to_unban); + self.ban_nodes.insert(node_address.node_id, time_to_unban); + } +} diff --git a/version-meld/discv5/src/query_pool.rs b/version-meld/discv5/src/query_pool.rs new file mode 100644 index 0000000..ad3ba46 --- /dev/null +++ b/version-meld/discv5/src/query_pool.rs @@ -0,0 +1,293 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +mod peers; + +pub use peers::QueryState; +pub(crate) use peers::{ + closest::{FindNodeQuery, FindNodeQueryConfig}, + predicate::{PredicateQuery, PredicateQueryConfig}, +}; + +use crate::kbucket::{Key, PredicateKey}; +use fnv::FnvHashMap; +use std::time::{Duration, Instant}; + +pub trait TargetKey { + fn key(&self) -> Key; +} + +/// A `QueryPool` provides an aggregate state machine for driving `Query`s to completion. +/// +/// Internally, a `Query` is in turn driven by an underlying `QueryPeerIter` +/// that determines the peer selection strategy, i.e. the order in which the +/// peers involved in the query should be contacted. +pub struct QueryPool { + next_id: usize, + query_timeout: Duration, + queries: FnvHashMap>, +} + +/// The observable states emitted by [`QueryPool::poll`]. +#[allow(clippy::type_complexity)] +pub enum QueryPoolState<'a, TTarget, TNodeId, TResult> { + /// The pool is idle, i.e. there are no queries to process. + Idle, + /// At least one query is waiting for results. `Some(request)` indicates + /// that a new request is now being waited on. + Waiting(Option<(&'a mut Query, TNodeId)>), + /// A query has finished. + Finished(Query), + /// A query has timed out. + Timeout(Query), +} + +impl QueryPool +where + TTarget: TargetKey, + TNodeId: Into> + Eq + Clone, + TResult: Into + Clone, +{ + /// Creates a new `QueryPool` with the given configuration. + pub fn new(query_timeout: Duration) -> Self { + QueryPool { + next_id: 0, + query_timeout, + queries: Default::default(), + } + } + + /// Returns an iterator over the queries in the pool. + pub fn iter(&self) -> impl Iterator> { + self.queries.values() + } + + /// Adds a query to the pool that iterates towards the closest peers to the target. + pub fn add_findnode_query( + &mut self, + config: FindNodeQueryConfig, + target: TTarget, + peers: I, + ) -> QueryId + where + I: IntoIterator>, + { + let target_key = target.key(); + let findnode_query = FindNodeQuery::with_config(config, target_key, peers); + let peer_iter = QueryPeerIter::FindNode(findnode_query); + self.add(peer_iter, target) + } + + /// Adds a query to the pool that returns peers that satisfy a predicate. + pub(crate) fn add_predicate_query( + &mut self, + config: PredicateQueryConfig, + target: TTarget, + peers: I, + predicate: impl Fn(&TResult) -> bool + Send + 'static, + ) -> QueryId + where + I: IntoIterator>, + { + let target_key = target.key(); + let predicate_query = PredicateQuery::with_config(config, target_key, peers, predicate); + let peer_iter = QueryPeerIter::Predicate(predicate_query); + self.add(peer_iter, target) + } + + fn add(&mut self, peer_iter: QueryPeerIter, target: TTarget) -> QueryId { + let id = QueryId(self.next_id); + self.next_id = self.next_id.wrapping_add(1); + let query = Query::new(id, peer_iter, target); + self.queries.insert(id, query); + id + } + + /// Returns a mutable reference to a query with the given ID, if it is in the pool. + pub fn get_mut(&mut self, id: QueryId) -> Option<&mut Query> { + self.queries.get_mut(&id) + } + + /// Polls the pool to advance the queries. + pub fn poll(&mut self) -> QueryPoolState<'_, TTarget, TNodeId, TResult> { + let now = Instant::now(); + let mut finished = None; + let mut waiting = None; + let mut timeout = None; + + for (&query_id, query) in self.queries.iter_mut() { + query.started = query.started.or(Some(now)); + match query.next(now) { + QueryState::Finished => { + finished = Some(query_id); + break; + } + QueryState::Waiting(Some(return_peer)) => { + waiting = Some((query_id, return_peer)); + break; + } + QueryState::Waiting(None) | QueryState::WaitingAtCapacity => { + let elapsed = now - query.started.unwrap_or(now); + if elapsed >= self.query_timeout { + timeout = Some(query_id); + break; + } + } + } + } + + if let Some((query_id, return_peer)) = waiting { + let query = self.queries.get_mut(&query_id).expect("s.a."); + return QueryPoolState::Waiting(Some((query, return_peer))); + } + + if let Some(query_id) = finished { + let query = self.queries.remove(&query_id).expect("s.a."); + return QueryPoolState::Finished(query); + } + + if let Some(query_id) = timeout { + let query = self.queries.remove(&query_id).expect("s.a."); + return QueryPoolState::Timeout(query); + } + + if self.queries.is_empty() { + QueryPoolState::Idle + } else { + QueryPoolState::Waiting(None) + } + } +} + +/// Unique identifier for an active query. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub struct QueryId(pub usize); + +impl std::ops::Deref for QueryId { + type Target = usize; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// A query in a `QueryPool`. +pub struct Query { + /// The unique ID of the query. + id: QueryId, + /// The peer iterator that drives the query state. + peer_iter: QueryPeerIter, + /// The instant when the query started (i.e. began waiting for the first + /// result from a peer). + started: Option, + /// Target we are looking for. + target: TTarget, +} + +/// The peer selection strategies that can be used by queries. +enum QueryPeerIter { + FindNode(FindNodeQuery), + Predicate(PredicateQuery), +} + +impl Query +where + TTarget: TargetKey, + TNodeId: Into> + Eq + Clone, + TResult: Into + Clone, +{ + /// Creates a new query without starting it. + fn new(id: QueryId, peer_iter: QueryPeerIter, target: TTarget) -> Self { + Query { + id, + peer_iter, + target, + started: None, + } + } + + /// Gets the unique ID of the query. + pub fn id(&self) -> QueryId { + self.id + } + + /// Informs the query that the attempt to contact `peer` failed. + pub fn on_failure(&mut self, peer: &TNodeId) { + match &mut self.peer_iter { + QueryPeerIter::FindNode(iter) => iter.on_failure(peer), + QueryPeerIter::Predicate(iter) => iter.on_failure(peer), + } + } + + /// Informs the query that the attempt to contact `peer` succeeded, + /// possibly resulting in new peers that should be incorporated into + /// the query, if applicable. + pub fn on_success<'a>(&mut self, peer: &TNodeId, new_peers: &'a [TResult]) + where + &'a TResult: Into, + { + match &mut self.peer_iter { + QueryPeerIter::FindNode(iter) => { + iter.on_success(peer, new_peers.iter().map(|result| result.into()).collect()) + } + QueryPeerIter::Predicate(iter) => iter.on_success(peer, new_peers), + } + } + + /// Advances the state of the underlying peer iterator. + fn next(&mut self, now: Instant) -> QueryState { + match &mut self.peer_iter { + QueryPeerIter::FindNode(iter) => iter.next(now), + QueryPeerIter::Predicate(iter) => iter.next(now), + } + } + + /// Consumes the query, producing the final `QueryResult`. + pub fn into_result(self) -> QueryResult> { + let peers = match self.peer_iter { + QueryPeerIter::FindNode(iter) => iter.into_result(), + QueryPeerIter::Predicate(iter) => iter.into_result(), + }; + QueryResult { + target: self.target, + closest_peers: peers.into_iter(), + } + } + + /// Returns a reference to the query `target`. + pub fn target(&self) -> &TTarget { + &self.target + } + + /// Returns a mutable reference to the query `target`. + pub fn target_mut(&mut self) -> &mut TTarget { + &mut self.target + } +} + +/// The result of a `Query`. +pub struct QueryResult { + /// The target of the query. + pub target: TTarget, + /// The closest peers to the target found by the query. + pub closest_peers: TClosest, +} diff --git a/version-meld/discv5/src/query_pool/peers.rs b/version-meld/discv5/src/query_pool/peers.rs new file mode 100644 index 0000000..9a9da5b --- /dev/null +++ b/version-meld/discv5/src/query_pool/peers.rs @@ -0,0 +1,68 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// The basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p + +//! Peer selection strategies for queries in the form of iterator-like state machines. +//! +//! Using a peer iterator in a query involves performing the following steps +//! repeatedly and in an alternating fashion: +//! +//! 1. Calling `next` to observe the next state of the iterator and determine +//! what to do, which is to either issue new requests to peers or continue +//! waiting for responses. +//! +//! 2. When responses are received or requests fail, providing input to the +//! iterator via the `on_success` and `on_failure` callbacks, +//! respectively, followed by repeating step (1). +//! +//! When a call to `next` returns [`Finished`], no more peers can be obtained +//! from the iterator and the results can be obtained from `into_result`. +//! +//! A peer iterator can be finished prematurely at any time through `finish`. +//! +//! [`Finished`]: QueryState::Finished + +pub mod closest; +pub mod predicate; + +/// The state of the query reported by [`closest::FindNodeQuery::next`] or +/// [`predicate::PredicateQuery::next`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum QueryState { + /// The query is waiting for results. + /// + /// `Some(peer)` indicates that the query is now waiting for a result + /// from `peer`, in addition to any other peers for which the query is already + /// waiting for results. + /// + /// `None` indicates that the query is waiting for results and there is no + /// new peer to contact, despite the query not being at capacity w.r.t. + /// the permitted parallelism. + Waiting(Option), + + /// The query is waiting for results and is at capacity w.r.t. the + /// permitted parallelism. + WaitingAtCapacity, + + /// The query finished. + Finished, +} diff --git a/version-meld/discv5/src/query_pool/peers/closest.rs b/version-meld/discv5/src/query_pool/peers/closest.rs new file mode 100644 index 0000000..fd64efa --- /dev/null +++ b/version-meld/discv5/src/query_pool/peers/closest.rs @@ -0,0 +1,707 @@ +// Copyright 2018 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// This basis of this file has been taken from the rust-libp2p codebase: +// https://github.com/libp2p/rust-libp2p +// +use super::*; +use crate::{ + config::Discv5Config, + kbucket::{Distance, Key, MAX_NODES_PER_BUCKET}, +}; +use std::{ + collections::btree_map::{BTreeMap, Entry}, + time::{Duration, Instant}, +}; + +#[derive(Debug, Clone)] +pub struct FindNodeQuery { + /// The target key we are looking for + target_key: Key, + + /// The current state of progress of the query. + progress: QueryProgress, + + /// The closest peers to the target, ordered by increasing distance. + closest_peers: BTreeMap>, + + /// The number of peers for which the query is currently waiting for results. + num_waiting: usize, + + /// The configuration of the query. + config: FindNodeQueryConfig, +} + +/// Configuration for a `Query`. +#[derive(Debug, Clone)] +pub struct FindNodeQueryConfig { + /// Allowed level of parallelism. + /// + /// The `α` parameter in the Kademlia paper. The maximum number of peers that a query + /// is allowed to wait for in parallel while iterating towards the closest + /// nodes to a target. Defaults to `3`. + pub parallelism: usize, + + /// Number of results to produce. + /// + /// The number of closest peers that a query must obtain successful results + /// for before it terminates. Defaults to the maximum number of entries in a + /// single k-bucket, i.e. the `k` parameter in the Kademlia paper. + pub num_results: usize, + + /// The timeout for a single peer. + /// + /// If a successful result is not reported for a peer within this timeout + /// window, the iterator considers the peer unresponsive and will not wait for + /// the peer when evaluating the termination conditions, until and unless a + /// result is delivered. Defaults to `10` seconds. + pub peer_timeout: Duration, +} + +impl FindNodeQueryConfig { + pub fn new_from_config(config: &Discv5Config) -> Self { + Self { + parallelism: config.query_parallelism, + num_results: MAX_NODES_PER_BUCKET, + peer_timeout: config.query_peer_timeout, + } + } +} + +impl FindNodeQuery +where + TNodeId: Into> + Eq + Clone, +{ + /// Creates a new query with the given configuration. + pub fn with_config( + config: FindNodeQueryConfig, + target_key: Key, + known_closest_peers: I, + ) -> Self + where + I: IntoIterator>, + { + // Initialise the closest peers to begin the query with. + let closest_peers = known_closest_peers + .into_iter() + .map(|key| { + let key: Key = key; + let distance = key.distance(&target_key); + let state = QueryPeerState::NotContacted; + (distance, QueryPeer::new(key, state)) + }) + .take(config.num_results) + .collect(); + + // The query initially makes progress by iterating towards the target. + let progress = QueryProgress::Iterating { no_progress: 0 }; + + FindNodeQuery { + config, + target_key, + progress, + closest_peers, + num_waiting: 0, + } + } + + /// Callback for delivering the result of a successful request to a peer + /// that the query is waiting on. + /// + /// Delivering results of requests back to the query allows the query to make + /// progress. The query is said to make progress either when the given + /// `closer_peers` contain a peer closer to the target than any peer seen so far, + /// or when the query did not yet accumulate `num_results` closest peers and + /// `closer_peers` contains a new peer, regardless of its distance to the target. + /// + /// After calling this function, `next` should eventually be called again + /// to advance the state of the query. + /// + /// If the query is finished, the query is not currently waiting for a + /// result from `peer`, or a result for `peer` has already been reported, + /// calling this function has no effect. + pub fn on_success(&mut self, node_id: &TNodeId, closer_peers: Vec) { + if let QueryProgress::Finished = self.progress { + return; + } + + let key: Key = node_id.clone().into(); + let distance = key.distance(&self.target_key); + + // Mark the peer's progress, the total nodes it has returned and it's current iteration. + // If the node returned peers, mark it as succeeded. + match self.closest_peers.entry(distance) { + Entry::Vacant(..) => return, + Entry::Occupied(mut e) => match e.get().state { + QueryPeerState::Waiting(..) => { + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + let peer = e.get_mut(); + peer.peers_returned += closer_peers.len(); + // mark the peer as succeeded + peer.state = QueryPeerState::Succeeded; + } + QueryPeerState::Unresponsive => { + let peer = e.get_mut(); + peer.peers_returned += closer_peers.len(); + // mark the peer as succeeded + peer.state = QueryPeerState::Succeeded; + } + QueryPeerState::NotContacted + | QueryPeerState::Failed + | QueryPeerState::Succeeded => return, + }, + } + + let mut progress = false; + let num_closest = self.closest_peers.len(); + + // Incorporate the reported closer peers into the query. + for peer in closer_peers { + let key: Key = peer.into(); + let distance = self.target_key.distance(&key); + let peer = QueryPeer::new(key, QueryPeerState::NotContacted); + self.closest_peers.entry(distance).or_insert(peer); + // The query makes progress if the new peer is either closer to the target + // than any peer seen so far (i.e. is the first entry), or the query did + // not yet accumulate enough closest peers. + progress = self.closest_peers.keys().next() == Some(&distance) + || num_closest < self.config.num_results; + } + + // Update the query progress. + self.progress = match self.progress { + QueryProgress::Iterating { no_progress } => { + let no_progress = if progress { 0 } else { no_progress + 1 }; + if no_progress >= self.config.parallelism { + QueryProgress::Stalled + } else { + QueryProgress::Iterating { no_progress } + } + } + QueryProgress::Stalled => { + if progress { + QueryProgress::Iterating { no_progress: 0 } + } else { + QueryProgress::Stalled + } + } + QueryProgress::Finished => QueryProgress::Finished, + } + } + + /// Callback for informing the query about a failed request to a peer + /// that the query is waiting on. + /// + /// After calling this function, `next` should eventually be called again + /// to advance the state of the query. + /// + /// If the query is finished, the query is not currently waiting for a + /// result from `peer`, or a result for `peer` has already been reported, + /// calling this function has no effect. + pub fn on_failure(&mut self, peer: &TNodeId) { + if let QueryProgress::Finished = self.progress { + return; + } + + let key: Key = peer.clone().into(); + let distance = key.distance(&self.target_key); + + match self.closest_peers.entry(distance) { + Entry::Vacant(_) => {} + Entry::Occupied(mut e) => match e.get().state { + QueryPeerState::Waiting(..) => { + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + e.get_mut().state = QueryPeerState::Failed + } + QueryPeerState::Unresponsive => e.get_mut().state = QueryPeerState::Failed, + _ => {} + }, + } + } + + /// Advances the state of the query, potentially getting a new peer to contact. + /// + /// See [`QueryState`]. + pub fn next(&mut self, now: Instant) -> QueryState { + if let QueryProgress::Finished = self.progress { + return QueryState::Finished; + } + + // Count the number of peers that returned a result. If there is a + // request in progress to one of the `num_results` closest peers, the + // counter is set to `None` as the query can only finish once + // `num_results` closest peers have responded (or there are no more + // peers to contact, see `active_counter`). + let mut result_counter = Some(0); + + // Check if the query is at capacity w.r.t. the allowed parallelism. + let at_capacity = self.at_capacity(); + + for peer in self.closest_peers.values_mut() { + match peer.state { + QueryPeerState::NotContacted => { + // This peer is waiting to be reiterated. + if !at_capacity { + let timeout = now + self.config.peer_timeout; + peer.state = QueryPeerState::Waiting(timeout); + self.num_waiting += 1; + let peer = peer.key.preimage().clone(); + return QueryState::Waiting(Some(peer)); + } else { + return QueryState::WaitingAtCapacity; + } + } + + QueryPeerState::Waiting(timeout) => { + if now >= timeout { + // Peers that don't respond within timeout are set to `Failed`. + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + peer.state = QueryPeerState::Unresponsive; + } else if at_capacity { + // The query is still waiting for a result from a peer and is + // at capacity w.r.t. the maximum number of peers being waited on. + return QueryState::WaitingAtCapacity; + } else { + // The query is still waiting for a result from a peer and the + // `result_counter` did not yet reach `num_results`. Therefore + // the query is not yet done, regardless of already successful + // queries to peers farther from the target. + result_counter = None; + } + } + + QueryPeerState::Succeeded => { + if let Some(ref mut cnt) = result_counter { + *cnt += 1; + // If `num_results` successful results have been delivered for the + // closest peers, the query is done. + if *cnt >= self.config.num_results { + self.progress = QueryProgress::Finished; + return QueryState::Finished; + } + } + } + + QueryPeerState::Failed | QueryPeerState::Unresponsive => { + // Skip over unresponsive or failed peers. + } + } + } + + if self.num_waiting > 0 { + // The query is still waiting for results and not at capacity w.r.t. + // the allowed parallelism, but there are no new peers to contact + // at the moment. + QueryState::Waiting(None) + } else { + // The query is finished because all available peers have been contacted + // and the query is not waiting for any more results. + self.progress = QueryProgress::Finished; + QueryState::Finished + } + } + + /// Consumes the query, returning the target and the closest peers. + pub fn into_result(self) -> Vec { + self.closest_peers + .into_iter() + .filter_map(|(_, peer)| { + if let QueryPeerState::Succeeded = peer.state { + Some(peer.key.into_preimage()) + } else { + None + } + }) + .take(self.config.num_results) + .collect() + } + + /// Checks if the query is at capacity w.r.t. the permitted parallelism. + /// + /// While the query is stalled, up to `num_results` parallel requests + /// are allowed. This is a slightly more permissive variant of the + /// requirement that the initiator "resends the FIND_NODE to all of the + /// k closest nodes it has not already queried". + fn at_capacity(&self) -> bool { + match self.progress { + QueryProgress::Stalled => self.num_waiting >= self.config.num_results, + QueryProgress::Iterating { .. } => self.num_waiting >= self.config.parallelism, + QueryProgress::Finished => true, + } + } +} + +/// Stage of the query. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum QueryProgress { + /// The query is making progress by iterating towards `num_results` closest + /// peers to the target with a maximum of `parallelism` peers for which the + /// query is waiting for results at a time. + /// + /// > **Note**: When the query switches back to `Iterating` after being + /// > `Stalled`, it may temporarily be waiting for more than `parallelism` + /// > results from peers, with new peers only being considered once + /// > the number pending results drops below `parallelism`. + Iterating { + /// The number of consecutive results that did not yield a peer closer + /// to the target. When this number reaches `parallelism` and no new + /// peer was discovered or at least `num_results` peers are known to + /// the query, it is considered `Stalled`. + no_progress: usize, + }, + + /// A query is stalled when it did not make progress after `parallelism` + /// consecutive successful results (see `on_success`). + /// + /// While the query is stalled, the maximum allowed parallelism for pending + /// results is increased to `num_results` in an attempt to finish the query. + /// If the query can make progress again upon receiving the remaining + /// results, it switches back to `Iterating`. Otherwise it will be finished. + Stalled, + + /// The query is finished. + /// + /// A query finishes either when it has collected `num_results` results + /// from the closest peers (not counting those that failed or are unresponsive) + /// or because the query ran out of peers that have not yet delivered + /// results (or failed). + Finished, +} + +/// Representation of a peer in the context of a query. +#[derive(Debug, Clone)] +struct QueryPeer { + /// The `KBucket` key used to identify the peer. + key: Key, + + /// The number of peers that have been returned by this peer. + peers_returned: usize, + + /// The current query state of this peer. + state: QueryPeerState, +} + +impl QueryPeer { + pub fn new(key: Key, state: QueryPeerState) -> Self { + QueryPeer { + key, + peers_returned: 0, + state, + } + } +} + +/// The state of `QueryPeer` in the context of a query. +#[derive(Debug, Copy, Clone)] +enum QueryPeerState { + /// The peer has not yet been contacted. + /// + /// This is the starting state for every peer known to, or discovered by, a query. + NotContacted, + + /// The query is waiting for a result from the peer. + Waiting(Instant), + + /// A result was not delivered for the peer within the configured timeout. + /// + /// The peer is not taken into account for the termination conditions + /// of the iterator until and unless it responds. + Unresponsive, + + /// Obtaining a result from the peer has failed. + /// + /// This is a final state, reached as a result of a call to `on_failure`. + Failed, + + /// A successful result from the peer has been delivered. + /// + /// This is a final state, reached as a result of a call to `on_success`. + Succeeded, +} + +#[cfg(test)] +mod tests { + use super::*; + use enr::NodeId; + use quickcheck::*; + use rand_07::{thread_rng, Rng}; + use std::time::Duration; + + type TestQuery = FindNodeQuery; + + fn random_nodes(n: usize) -> impl Iterator + Clone { + (0..n).map(|_| NodeId::random()) + } + + fn random_query(g: &mut G) -> TestQuery { + let known_closest_peers = random_nodes(g.gen_range(1, 60)).map(Key::from); + let target = NodeId::random(); + let config = FindNodeQueryConfig { + parallelism: g.gen_range(1, 10), + num_results: g.gen_range(1, 25), + peer_timeout: Duration::from_secs(g.gen_range(10, 30)), + }; + FindNodeQuery::with_config(config, target.into(), known_closest_peers) + } + + fn sorted(target: &Key, peers: &[Key]) -> bool { + peers + .windows(2) + .all(|w| w[0].distance(&target) < w[1].distance(&target)) + } + + impl Arbitrary for TestQuery { + fn arbitrary(g: &mut G) -> TestQuery { + random_query(g) + } + } + + #[test] + fn new_query() { + let query = random_query(&mut thread_rng()); + let target = query.target_key.clone(); + + let (keys, states): (Vec<_>, Vec<_>) = query + .closest_peers + .values() + .map(|e| (e.key.clone(), &e.state)) + .unzip(); + + let none_contacted = states + .iter() + .all(|s| matches!(s, QueryPeerState::NotContacted)); + + assert!(none_contacted, "Unexpected peer state in new query."); + assert!( + sorted(&target, &keys), + "Closest peers in new query not sorted by distance to target." + ); + assert_eq!( + query.num_waiting, 0, + "Unexpected peers in progress in new query." + ); + assert!( + query.into_result().is_empty(), + "Unexpected closest peers in new query" + ); + } + + #[test] + fn termination_and_parallelism() { + fn prop(mut query: TestQuery) { + let now = Instant::now(); + let mut rng = thread_rng(); + + let mut expected = query + .closest_peers + .values() + .map(|e| e.key.clone()) + .collect::>(); + let num_known = expected.len(); + let max_parallelism = usize::min(query.config.parallelism, num_known); + + let target = query.target_key.clone(); + let mut remaining; + let mut num_failures = 0; + + 'finished: loop { + if expected.is_empty() { + break; + } + // Split off the next up to `parallelism` expected peers. + else if expected.len() < max_parallelism { + remaining = Vec::new(); + } else { + remaining = expected.split_off(max_parallelism); + } + + // Advance the query for maximum parallelism. + for k in expected.iter() { + match query.next(now) { + QueryState::Finished => break 'finished, + QueryState::Waiting(Some(p)) => assert_eq!(&p, k.preimage()), + QueryState::Waiting(None) => panic!("Expected another peer."), + QueryState::WaitingAtCapacity => panic!("Unexpectedly reached capacity."), + } + } + let num_waiting = query.num_waiting; + assert_eq!(num_waiting, expected.len()); + + // Check the bounded parallelism. + if query.at_capacity() { + assert_eq!(query.next(now), QueryState::WaitingAtCapacity) + } + + // Report results back to the query with a random number of "closer" + // peers or an error, thus finishing the "in-flight requests". + for (i, k) in expected.iter().enumerate() { + if rng.gen_bool(0.75) { + let num_closer = rng.gen_range(0, query.config.num_results + 1); + let closer_peers = random_nodes(num_closer).collect::>(); + // let _: () = remaining; + remaining.extend(closer_peers.iter().map(|x| Key::from(*x))); + query.on_success(k.preimage(), closer_peers); + } else { + num_failures += 1; + query.on_failure(k.preimage()); + } + assert_eq!(query.num_waiting, num_waiting - (i + 1)); + } + + // Re-sort the remaining expected peers for the next "round". + remaining.sort_by_key(|k| target.distance(&k)); + + expected = remaining + } + + // The query must be finished. + assert_eq!(query.next(now), QueryState::Finished); + assert_eq!(query.progress, QueryProgress::Finished); + + // Determine if all peers have been contacted by the query. This _must_ be + // the case if the query finished with fewer than the requested number + // of results. + let all_contacted = query.closest_peers.values().all(|e| { + !matches!( + e.state, + QueryPeerState::NotContacted | QueryPeerState::Waiting { .. } + ) + }); + + let target_key = query.target_key.clone(); + let num_results = query.config.num_results; + let result = query.into_result(); + let closest = result.into_iter().map(Key::from).collect::>(); + + // assert_eq!(result.target, target); + assert!(sorted(&target_key, &closest)); + + if closest.len() < num_results { + // The query returned fewer results than requested. Therefore + // either the initial number of known peers must have been + // less than the desired number of results, or there must + // have been failures. + assert!(num_known < num_results || num_failures > 0); + // All peers must have been contacted. + assert!(all_contacted, "Not all peers have been contacted."); + } else { + assert_eq!(num_results, closest.len(), "Too many results."); + } + } + + QuickCheck::new().tests(10).quickcheck(prop as fn(_) -> _) + } + + #[test] + fn no_duplicates() { + fn prop(mut query: TestQuery) -> bool { + let now = Instant::now(); + let closer: Vec = random_nodes(1).collect(); + + // A first peer reports a "closer" peer. + let peer1 = if let QueryState::Waiting(Some(p)) = query.next(now) { + p + } else { + panic!("No peer."); + }; + query.on_success(&peer1, closer.clone()); + // Duplicate result from the same peer. + query.on_success(&peer1, closer.clone()); + + // If there is a second peer, let it also report the same "closer" peer. + match query.next(now) { + QueryState::Waiting(Some(p)) => { + let peer2 = p; + query.on_success(&peer2, closer.clone()) + } + QueryState::Finished => {} + _ => panic!("Unexpectedly query state."), + }; + + // The "closer" peer must only be in the query once. + let n = query + .closest_peers + .values() + .filter(|e| e.key.preimage() == &closer[0]) + .count(); + assert_eq!(n, 1); + + true + } + + QuickCheck::new().tests(10).quickcheck(prop as fn(_) -> _) + } + + #[test] + fn timeout() { + fn prop(mut query: TestQuery) -> bool { + let mut now = Instant::now(); + let peer = query + .closest_peers + .values() + .next() + .unwrap() + .key + .clone() + .into_preimage(); + // Poll the query for the first peer to be in progress. + match query.next(now) { + QueryState::Waiting(Some(id)) => assert_eq!(id, peer), + _ => panic!(), + } + + // Artificially advance the clock. + now += query.config.peer_timeout; + + // Advancing the query again should mark the first peer as unresponsive. + let _ = query.next(now); + match &query.closest_peers.values().next().unwrap() { + QueryPeer { + key, + state: QueryPeerState::Unresponsive, + .. + } => { + assert_eq!(key.preimage(), &peer); + } + QueryPeer { state, .. } => panic!("Unexpected peer state: {:?}", state), + } + + let finished = query.progress == QueryProgress::Finished; + query.on_success(&peer, Vec::::new()); + let closest = query.into_result(); + + if finished { + // Delivering results when the query already finished must have + // no effect. + assert_eq!(Vec::::new(), closest); + } else { + // Unresponsive peers can still deliver results while the iterator + // is not finished. + assert_eq!(vec![peer], closest) + } + true + } + + QuickCheck::new().tests(10).quickcheck(prop as fn(_) -> _) + } +} diff --git a/version-meld/discv5/src/query_pool/peers/predicate.rs b/version-meld/discv5/src/query_pool/peers/predicate.rs new file mode 100644 index 0000000..4768a1c --- /dev/null +++ b/version-meld/discv5/src/query_pool/peers/predicate.rs @@ -0,0 +1,438 @@ +use super::*; +use crate::{ + config::Discv5Config, + kbucket::{Distance, Key, PredicateKey, MAX_NODES_PER_BUCKET}, +}; +use std::{ + collections::btree_map::{BTreeMap, Entry}, + time::{Duration, Instant}, +}; + +pub(crate) struct PredicateQuery { + /// The target key we are looking for + target_key: Key, + + /// The current state of progress of the query. + progress: QueryProgress, + + /// The closest peers to the target, ordered by increasing distance. + closest_peers: BTreeMap>, + + /// The number of peers for which the query is currently waiting for results. + num_waiting: usize, + + /// The predicate function to be applied to filter the ENR's found during the search. + predicate: Box bool + Send + 'static>, + + /// The configuration of the query. + config: PredicateQueryConfig, +} + +/// Configuration for a `Query`. +#[derive(Debug, Clone)] +pub(crate) struct PredicateQueryConfig { + /// Allowed level of parallelism. + /// + /// The `α` parameter in the Kademlia paper. The maximum number of peers that a query + /// is allowed to wait for in parallel while iterating towards the closest + /// nodes to a target. Defaults to `3`. + pub(crate) parallelism: usize, + + /// Number of results to produce. + /// + /// The number of closest peers that a query must obtain successful results + /// for before it terminates. Defaults to the maximum number of entries in a + /// single k-bucket, i.e. the `k` parameter in the Kademlia paper. + pub(crate) num_results: usize, + + /// The timeout for a single peer. + /// + /// If a successful result is not reported for a peer within this timeout + /// window, the iterator considers the peer unresponsive and will not wait for + /// the peer when evaluating the termination conditions, until and unless a + /// result is delivered. Defaults to `10` seconds. + pub(crate) peer_timeout: Duration, +} + +impl PredicateQueryConfig { + pub(crate) fn new_from_config(config: &Discv5Config) -> Self { + Self { + parallelism: config.query_parallelism, + num_results: MAX_NODES_PER_BUCKET, + peer_timeout: config.query_peer_timeout, + } + } +} + +impl PredicateQuery +where + TNodeId: Into> + Eq + Clone, + TResult: Into + Clone, +{ + /// Creates a new query with the given configuration. + pub fn with_config( + config: PredicateQueryConfig, + target_key: Key, + known_closest_peers: I, + predicate: impl Fn(&TResult) -> bool + Send + 'static, + ) -> Self + where + I: IntoIterator>, + { + // Initialise the closest peers to begin the query with. + let closest_peers = known_closest_peers + .into_iter() + .map(|key| { + let predicate_match = key.predicate_match; + let key: Key = key.into(); + let distance = key.distance(&target_key); + let state = QueryPeerState::NotContacted; + + (distance, QueryPeer::new(key, state, predicate_match)) + }) + .take(config.num_results) + .collect(); + + // The query initially makes progress by iterating towards the target. + let progress = QueryProgress::Iterating { no_progress: 0 }; + + PredicateQuery { + config, + target_key, + progress, + closest_peers, + num_waiting: 0, + predicate: Box::new(predicate), + } + } + + /// Callback for delivering the result of a successful request to a peer + /// that the query is waiting on. + /// + /// Delivering results of requests back to the query allows the query to make + /// progress. The query is said to make progress either when the given + /// `closer_peers` contain a peer closer to the target than any peer seen so far, + /// or when the query did not yet accumulate `num_results` closest peers and + /// `closer_peers` contains a new peer, regardless of its distance to the target. + /// + /// After calling this function, `next` should eventually be called again + /// to advance the state of the query. + /// + /// If the query is finished, the query is not currently waiting for a + /// result from `peer`, or a result for `peer` has already been reported, + /// calling this function has no effect. + pub fn on_success<'a>(&mut self, node_id: &TNodeId, closer_peers: &'a [TResult]) + where + &'a TResult: Into, + { + if let QueryProgress::Finished = self.progress { + return; + } + + let key: Key = node_id.clone().into(); + let distance = key.distance(&self.target_key); + + // Mark the peer's progress, the total nodes it has returned and it's current iteration. + // If the node returned peers, mark it as succeeded. + match self.closest_peers.entry(distance) { + Entry::Vacant(..) => return, + Entry::Occupied(mut e) => match e.get().state { + QueryPeerState::Waiting(..) => { + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + let peer = e.get_mut(); + peer.peers_returned += closer_peers.len(); + // mark the peer as succeeded + peer.state = QueryPeerState::Succeeded; + } + QueryPeerState::Unresponsive => { + let peer = e.get_mut(); + peer.peers_returned += closer_peers.len(); + // mark the peer as succeeded + peer.state = QueryPeerState::Succeeded; + } + QueryPeerState::NotContacted + | QueryPeerState::Failed + | QueryPeerState::Succeeded => return, + }, + } + + let mut progress = false; + let num_closest = self.closest_peers.len(); + + // Incorporate the reported closer peers into the query. + for result in closer_peers { + // If ENR satisfies the predicate, add to list of peers that satisfies predicate + let predicate_match = (self.predicate)(result); + let key: TNodeId = result.into(); + let key: Key = key.into(); + let distance = self.target_key.distance(&key); + let peer = QueryPeer::new(key, QueryPeerState::NotContacted, predicate_match); + self.closest_peers.entry(distance).or_insert(peer); + // The query makes progress if the new peer is either closer to the target + // than any peer seen so far (i.e. is the first entry), or the query did + // not yet accumulate enough closest peers. + progress = self.closest_peers.keys().next() == Some(&distance) + || num_closest < self.config.num_results; + } + + // Update the query progress. + self.progress = match self.progress { + QueryProgress::Iterating { no_progress } => { + let no_progress = if progress { 0 } else { no_progress + 1 }; + if no_progress >= self.config.parallelism { + QueryProgress::Stalled + } else { + QueryProgress::Iterating { no_progress } + } + } + QueryProgress::Stalled => { + if progress { + QueryProgress::Iterating { no_progress: 0 } + } else { + QueryProgress::Stalled + } + } + QueryProgress::Finished => QueryProgress::Finished, + } + } + + /// Callback for informing the query about a failed request to a peer + /// that the query is waiting on. + /// + /// After calling this function, `next` should eventually be called again + /// to advance the state of the query. + /// + /// If the query is finished, the query is not currently waiting for a + /// result from `peer`, or a result for `peer` has already been reported, + /// calling this function has no effect. + pub fn on_failure(&mut self, peer: &TNodeId) { + if let QueryProgress::Finished = self.progress { + return; + } + + let key: Key = peer.clone().into(); + let distance = key.distance(&self.target_key); + + if let Entry::Occupied(mut e) = self.closest_peers.entry(distance) { + if let QueryPeerState::Waiting(..) = e.get().state { + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + e.get_mut().state = QueryPeerState::Failed + } + } + } + + /// Advances the state of the query, potentially getting a new peer to contact. + /// + /// See [`QueryState`]. + pub fn next(&mut self, now: Instant) -> QueryState { + if let QueryProgress::Finished = self.progress { + return QueryState::Finished; + } + + // Count the number of peers that returned a result. If there is a + // request in progress to one of the `num_results` closest peers, the + // counter is set to `None` as the query can only finish once + // `num_results` closest peers have responded (or there are no more + // peers to contact, see `active_counter`). + let mut result_counter = Some(0); + + // Check if the query is at capacity w.r.t. the allowed parallelism. + let at_capacity = self.at_capacity(); + + for peer in self.closest_peers.values_mut() { + match peer.state { + QueryPeerState::NotContacted => { + // This peer is waiting to be reiterated. + if !at_capacity { + let timeout = now + self.config.peer_timeout; + peer.state = QueryPeerState::Waiting(timeout); + self.num_waiting += 1; + let return_peer = peer.key.preimage().clone(); + return QueryState::Waiting(Some(return_peer)); + } else { + return QueryState::WaitingAtCapacity; + } + } + + QueryPeerState::Waiting(timeout) => { + if now >= timeout { + // Peers that don't respond within timeout are set to `Failed`. + debug_assert!(self.num_waiting > 0); + self.num_waiting -= 1; + peer.state = QueryPeerState::Unresponsive; + } else if at_capacity { + // The query is still waiting for a result from a peer and is + // at capacity w.r.t. the maximum number of peers being waited on. + return QueryState::WaitingAtCapacity; + } else { + // The query is still waiting for a result from a peer and the + // `result_counter` did not yet reach `num_results`. Therefore + // the query is not yet done, regardless of already successful + // queries to peers farther from the target. + // Only count predicate peers. + if peer.predicate_match { + result_counter = None; + } + } + } + + QueryPeerState::Succeeded => { + if let Some(ref mut cnt) = result_counter { + if peer.predicate_match { + *cnt += 1; + // If `num_results` successful results have been delivered for the + // closest peers, the query is done. + if *cnt >= self.config.num_results { + self.progress = QueryProgress::Finished; + return QueryState::Finished; + } + } + } + } + + QueryPeerState::Failed | QueryPeerState::Unresponsive => { + // Skip over unresponsive or failed peers. + } + } + } + + if self.num_waiting > 0 { + // The query is still waiting for results and not at capacity w.r.t. + // the allowed parallelism, but there are no new peers to contact + // at the moment. + QueryState::Waiting(None) + } else { + // The query is finished because all available peers have been contacted + // and the query is not waiting for any more results. + self.progress = QueryProgress::Finished; + QueryState::Finished + } + } + + /// Consumes the query, returning the peers who match the predicate. + pub fn into_result(self) -> Vec { + self.closest_peers + .into_iter() + .filter_map(|(_, peer)| { + if let QueryPeerState::Succeeded = peer.state { + if peer.predicate_match { + Some(peer.key.into_preimage()) + } else { + None + } + } else { + None + } + }) + .take(self.config.num_results) + .collect() + } + + /// Checks if the query is at capacity w.r.t. the permitted parallelism. + /// + /// While the query is stalled, up to `num_results` parallel requests + /// are allowed. This is a slightly more permissive variant of the + /// requirement that the initiator "resends the FIND_NODE to all of the + /// k closest nodes it has not already queried". + fn at_capacity(&self) -> bool { + match self.progress { + QueryProgress::Stalled => self.num_waiting >= self.config.num_results, + QueryProgress::Iterating { .. } => self.num_waiting >= self.config.parallelism, + QueryProgress::Finished => true, + } + } +} + +/// Stage of the query. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum QueryProgress { + /// The query is making progress by iterating towards `num_results` closest + /// peers to the target with a maximum of `parallelism` peers for which the + /// query is waiting for results at a time. + /// + /// > **Note**: When the query switches back to `Iterating` after being + /// > `Stalled`, it may temporarily be waiting for more than `parallelism` + /// > results from peers, with new peers only being considered once + /// > the number pending results drops below `parallelism`. + Iterating { + /// The number of consecutive results that did not yield a peer closer + /// to the target. When this number reaches `parallelism` and no new + /// peer was discovered or at least `num_results` peers are known to + /// the query, it is considered `Stalled`. + no_progress: usize, + }, + + /// A query is stalled when it did not make progress after `parallelism` + /// consecutive successful results (see `on_success`). + /// + /// While the query is stalled, the maximum allowed parallelism for pending + /// results is increased to `num_results` in an attempt to finish the query. + /// If the query can make progress again upon receiving the remaining + /// results, it switches back to `Iterating`. Otherwise it will be finished. + Stalled, + + /// The query is finished. + /// + /// A query finishes either when it has collected `num_results` results + /// from the closest peers (not counting those that failed or are unresponsive) + /// or because the query ran out of peers that have not yet delivered + /// results (or failed). + Finished, +} + +/// Representation of a peer in the context of a query. +#[derive(Debug, Clone)] +struct QueryPeer { + /// The `KBucket` key used to identify the peer. + key: Key, + + /// The number of peers that have been returned by this peer. + peers_returned: usize, + + /// Whether the peer has matched the predicate or not. + predicate_match: bool, + + /// The current query state of this peer. + state: QueryPeerState, +} + +impl QueryPeer { + pub fn new(key: Key, state: QueryPeerState, predicate_match: bool) -> Self { + QueryPeer { + key, + peers_returned: 0, + predicate_match, + state, + } + } +} + +/// The state of `QueryPeer` in the context of a query. +#[derive(Debug, Copy, Clone)] +enum QueryPeerState { + /// The peer has not yet been contacted. + /// + /// This is the starting state for every peer known to, or discovered by, a query. + NotContacted, + + /// The query is waiting for a result from the peer. + Waiting(Instant), + + /// A result was not delivered for the peer within the configured timeout. + /// + /// The peer is not taken into account for the termination conditions + /// of the iterator until and unless it responds. + Unresponsive, + + /// Obtaining a result from the peer has failed. + /// + /// This is a final state, reached as a result of a call to `on_failure`. + Failed, + + /// A successful result from the peer has been delivered. + /// + /// This is a final state, reached as a result of a call to `on_success`. + Succeeded, +} diff --git a/version-meld/discv5/src/rpc.rs b/version-meld/discv5/src/rpc.rs new file mode 100644 index 0000000..120578a --- /dev/null +++ b/version-meld/discv5/src/rpc.rs @@ -0,0 +1,1018 @@ +use enr::{CombinedKey, Enr}; +use rlp::{DecoderError, RlpStream}; +use std::net::{IpAddr, Ipv6Addr}; +use tracing::{debug, warn}; + +type TopicHash = [u8; 32]; + +/// Type to manage the request IDs. +#[derive(Debug, Clone, PartialEq, Hash, Eq)] +pub struct RequestId(pub Vec); + +impl From for Vec { + fn from(id: RequestId) -> Self { + id.0 + } +} + +impl RequestId { + /// Decodes the ID from a raw bytes. + pub fn decode(data: Vec) -> Result { + if data.len() > 8 { + return Err(DecoderError::Custom("Invalid ID length")); + } + Ok(RequestId(data)) + } + + pub fn random() -> Self { + let rand: u64 = rand::random(); + RequestId(rand.to_be_bytes().to_vec()) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +#[derive(Debug, Clone, PartialEq)] +/// A combined type representing requests and responses. +pub enum Message { + /// A request, which contains its [`RequestId`]. + Request(Request), + /// A Response, which contains the [`RequestId`] of its associated request. + Response(Response), +} + +#[derive(Debug, Clone, PartialEq)] +/// A request sent between nodes. +pub struct Request { + /// The [`RequestId`] of the request. + pub id: RequestId, + /// The body of the request. + pub body: RequestBody, +} + +#[derive(Debug, Clone, PartialEq)] +/// A response sent in response to a [`Request`] +pub struct Response { + /// The [`RequestId`] of the request that triggered this response. + pub id: RequestId, + /// The body of this response. + pub body: ResponseBody, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestBody { + /// A PING request. + Ping { + /// Our current ENR sequence number. + enr_seq: u64, + }, + /// A FINDNODE request. + FindNode { + /// The distance(s) of peers we expect to be returned in the response. + distances: Vec, + }, + /// A Talk request. + Talk { + /// The protocol requesting. + protocol: Vec, + /// The request. + request: Vec, + }, + /// A REGISTERTOPIC request. + RegisterTopic { + topic: Vec, + enr: crate::Enr, + ticket: Vec, + }, + /// A TOPICQUERY request. + TopicQuery { topic: TopicHash }, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseBody { + /// A PONG response. + Pong { + /// The current ENR sequence number of the responder. + enr_seq: u64, + /// Our external IP address as observed by the responder. + ip: IpAddr, + /// Our external UDP port as observed by the responder. + port: u16, + }, + /// A NODES response. + Nodes { + /// The total number of responses that make up this response. + total: u64, + /// A list of ENR's returned by the responder. + nodes: Vec>, + }, + /// The TALK response. + Talk { + /// The response for the talk. + response: Vec, + }, + Ticket { + ticket: Vec, + wait_time: u64, + }, + RegisterConfirmation { + topic: Vec, + }, +} + +impl Request { + pub fn msg_type(&self) -> u8 { + match self.body { + RequestBody::Ping { .. } => 1, + RequestBody::FindNode { .. } => 3, + RequestBody::Talk { .. } => 5, + RequestBody::RegisterTopic { .. } => 7, + RequestBody::TopicQuery { .. } => 10, + } + } + + /// Encodes a Message to RLP-encoded bytes. + pub fn encode(self) -> Vec { + let mut buf = Vec::with_capacity(10); + let msg_type = self.msg_type(); + buf.push(msg_type); + let id = &self.id; + match self.body { + RequestBody::Ping { enr_seq } => { + let mut s = RlpStream::new(); + s.begin_list(2); + s.append(&id.as_bytes()); + s.append(&enr_seq); + buf.extend_from_slice(&s.out()); + buf + } + RequestBody::FindNode { distances } => { + let mut s = RlpStream::new(); + s.begin_list(2); + s.append(&id.as_bytes()); + s.begin_list(distances.len()); + for distance in distances { + s.append(&distance); + } + buf.extend_from_slice(&s.out()); + buf + } + RequestBody::Talk { protocol, request } => { + let mut s = RlpStream::new(); + s.begin_list(3); + s.append(&id.as_bytes()); + s.append(&protocol); + s.append(&request); + buf.extend_from_slice(&s.out()); + buf + } + RequestBody::RegisterTopic { topic, enr, ticket } => { + let mut s = RlpStream::new(); + s.begin_list(4); + s.append(&id.as_bytes()); + s.append(&topic); + s.append(&enr); + s.append(&ticket); + buf.extend_from_slice(&s.out()); + buf + } + RequestBody::TopicQuery { topic } => { + let mut s = RlpStream::new(); + s.begin_list(2); + s.append(&id.as_bytes()); + s.append(&(&topic as &[u8])); + buf.extend_from_slice(&s.out()); + buf + } + } + } +} + +impl Response { + pub fn msg_type(&self) -> u8 { + match &self.body { + ResponseBody::Pong { .. } => 2, + ResponseBody::Nodes { .. } => 4, + ResponseBody::Talk { .. } => 6, + ResponseBody::Ticket { .. } => 8, + ResponseBody::RegisterConfirmation { .. } => 9, + } + } + + /// Determines if the response is a valid response to the given request. + pub fn match_request(&self, req: &RequestBody) -> bool { + match self.body { + ResponseBody::Pong { .. } => matches!(req, RequestBody::Ping { .. }), + ResponseBody::Nodes { .. } => { + matches!( + req, + RequestBody::FindNode { .. } | RequestBody::TopicQuery { .. } + ) + } + ResponseBody::Talk { .. } => matches!(req, RequestBody::Talk { .. }), + ResponseBody::Ticket { .. } => matches!(req, RequestBody::RegisterTopic { .. }), + ResponseBody::RegisterConfirmation { .. } => { + matches!(req, RequestBody::RegisterTopic { .. }) + } + } + } + + /// Encodes a Message to RLP-encoded bytes. + pub fn encode(self) -> Vec { + let mut buf = Vec::with_capacity(10); + let msg_type = self.msg_type(); + buf.push(msg_type); + let id = &self.id; + match self.body { + ResponseBody::Pong { enr_seq, ip, port } => { + let mut s = RlpStream::new(); + s.begin_list(4); + s.append(&id.as_bytes()); + s.append(&enr_seq); + match ip { + IpAddr::V4(addr) => s.append(&(&addr.octets() as &[u8])), + IpAddr::V6(addr) => s.append(&(&addr.octets() as &[u8])), + }; + s.append(&port); + buf.extend_from_slice(&s.out()); + buf + } + ResponseBody::Nodes { total, nodes } => { + let mut s = RlpStream::new(); + s.begin_list(3); + s.append(&id.as_bytes()); + s.append(&total); + + if nodes.is_empty() { + s.begin_list(0); + } else { + s.begin_list(nodes.len()); + for node in nodes { + s.append(&node); + } + } + buf.extend_from_slice(&s.out()); + buf + } + ResponseBody::Talk { response } => { + let mut s = RlpStream::new(); + s.begin_list(2); + s.append(&id.as_bytes()); + s.append(&response); + buf.extend_from_slice(&s.out()); + buf + } + ResponseBody::Ticket { ticket, wait_time } => { + let mut s = RlpStream::new(); + s.begin_list(3); + s.append(&id.as_bytes()); + s.append(&ticket); + s.append(&wait_time); + buf.extend_from_slice(&s.out()); + buf + } + ResponseBody::RegisterConfirmation { topic } => { + let mut s = RlpStream::new(); + s.begin_list(2); + s.append(&id.as_bytes()); + s.append(&topic); + buf.extend_from_slice(&s.out()); + buf + } + } + } +} + +impl std::fmt::Display for RequestId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(&self.0)) + } +} + +impl std::fmt::Display for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Message::Request(request) => write!(f, "{}", request), + Message::Response(response) => write!(f, "{}", response), + } + } +} + +impl std::fmt::Display for Response { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Response: id: {}: {}", self.id, self.body) + } +} + +impl std::fmt::Display for ResponseBody { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResponseBody::Pong { enr_seq, ip, port } => write!( + f, + "PONG: Enr-seq: {}, Ip: {:?}, Port: {}", + enr_seq, ip, port + ), + ResponseBody::Nodes { total, nodes } => { + let _ = write!(f, "NODES: total: {}, Nodes: [", total); + let mut first = true; + for id in nodes { + if !first { + write!(f, ", {}", id)?; + } else { + write!(f, "{}", id)?; + } + first = false; + } + + write!(f, "]") + } + ResponseBody::Talk { response } => { + write!(f, "Response: Response {}", hex::encode(response)) + } + ResponseBody::Ticket { ticket, wait_time } => { + write!(f, "TICKET: Ticket: {:?}, Wait time: {}", ticket, wait_time) + } + ResponseBody::RegisterConfirmation { topic } => { + write!(f, "REGTOPIC: Registered: {}", hex::encode(topic)) + } + } + } +} + +impl std::fmt::Display for Request { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Request: id: {}: {}", self.id, self.body) + } +} + +impl std::fmt::Display for RequestBody { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RequestBody::Ping { enr_seq } => write!(f, "PING: enr_seq: {}", enr_seq), + RequestBody::FindNode { distances } => { + write!(f, "FINDNODE Request: distance: {:?}", distances) + } + RequestBody::Talk { protocol, request } => write!( + f, + "TALK: protocol: {}, request: {}", + hex::encode(protocol), + hex::encode(request) + ), + RequestBody::TopicQuery { topic } => write!(f, "TOPICQUERY: topic: {:?}", topic), + RequestBody::RegisterTopic { topic, enr, ticket } => write!( + f, + "RegisterTopic: topic: {}, enr: {}, ticket: {}", + hex::encode(topic), + enr.to_base64(), + hex::encode(ticket) + ), + } + } +} +#[allow(dead_code)] +impl Message { + pub fn encode(self) -> Vec { + match self { + Self::Request(request) => request.encode(), + Self::Response(response) => response.encode(), + } + } + + pub fn decode(data: &[u8]) -> Result { + if data.len() < 3 { + return Err(DecoderError::RlpIsTooShort); + } + + let msg_type = data[0]; + + let rlp = rlp::Rlp::new(&data[1..]); + + let list_len = rlp.item_count().and_then(|size| { + if size < 2 { + Err(DecoderError::RlpIncorrectListLen) + } else { + Ok(size) + } + })?; + + let id = RequestId::decode(rlp.val_at::>(0)?)?; + + let message = match msg_type { + 1 => { + // PingRequest + if list_len != 2 { + debug!( + "Ping Request has an invalid RLP list length. Expected 2, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + Message::Request(Request { + id, + body: RequestBody::Ping { + enr_seq: rlp.val_at::(1)?, + }, + }) + } + 2 => { + // PingResponse + if list_len != 4 { + debug!( + "Ping Response has an invalid RLP list length. Expected 4, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + let ip_bytes = rlp.val_at::>(2)?; + let ip = match ip_bytes.len() { + 4 => { + let mut ip = [0u8; 4]; + ip.copy_from_slice(&ip_bytes); + IpAddr::from(ip) + } + 16 => { + let mut ip = [0u8; 16]; + ip.copy_from_slice(&ip_bytes); + let ipv6 = Ipv6Addr::from(ip); + // If the ipv6 is ipv4 compatible/mapped, simply return the ipv4. + if let Some(ipv4) = ipv6.to_ipv4() { + IpAddr::V4(ipv4) + } else { + IpAddr::V6(ipv6) + } + } + _ => { + debug!("Ping Response has incorrect byte length for IP"); + return Err(DecoderError::RlpIncorrectListLen); + } + }; + let port = rlp.val_at::(3)?; + Message::Response(Response { + id, + body: ResponseBody::Pong { + enr_seq: rlp.val_at::(1)?, + ip, + port, + }, + }) + } + 3 => { + // FindNodeRequest + if list_len != 2 { + debug!( + "FindNode Request has an invalid RLP list length. Expected 2, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + let distances = rlp.list_at::(1)?; + + if distances.len() > 10 { + warn!( + "Rejected FindNode request asking for too many buckets {}, maximum 10", + distances.len() + ); + return Err(DecoderError::Custom("FINDNODE request too large")); + } + for distance in distances.iter() { + if distance > &256u64 { + warn!( + "Rejected FindNode request asking for unknown distance {}, maximum 256", + distance + ); + return Err(DecoderError::Custom("FINDNODE request distance invalid")); + } + } + + Message::Request(Request { + id, + body: RequestBody::FindNode { distances }, + }) + } + 4 => { + // NodesResponse + if list_len != 3 { + debug!( + "Nodes Response has an invalid RLP list length. Expected 3, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + + let nodes = { + let enr_list_rlp = rlp.at(2)?; + if enr_list_rlp.is_empty() { + // no records + vec![] + } else { + enr_list_rlp.as_list::>()? + } + }; + Message::Response(Response { + id, + body: ResponseBody::Nodes { + total: rlp.val_at::(1)?, + nodes, + }, + }) + } + 5 => { + // Talk Request + if list_len != 3 { + debug!( + "Talk Request has an invalid RLP list length. Expected 3, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + let protocol = rlp.val_at::>(1)?; + let request = rlp.val_at::>(2)?; + Message::Request(Request { + id, + body: RequestBody::Talk { protocol, request }, + }) + } + 6 => { + // Talk Response + if list_len != 2 { + debug!( + "Talk Response has an invalid RLP list length. Expected 2, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + let response = rlp.val_at::>(1)?; + Message::Response(Response { + id, + body: ResponseBody::Talk { response }, + }) + } + _ => { + return Err(DecoderError::Custom("Unknown RPC message type")); + } /* + * All other RPC messages are currently not supported as per the 5.1 specification. + + 7 => { + // RegisterTopicRequest + if list_len != 2 { + debug!("RegisterTopic Request has an invalid RLP list length. Expected 2, found {}", list_len); + return Err(DecoderError::RlpIncorrectListLen); + } + let ticket = rlp.val_at::>(1)?; + Message::Request(Request { + id, + body: RequestBody::RegisterTopic { ticket }, + }) + } + 8 => { + // RegisterTopicResponse + if list_len != 2 { + debug!("RegisterTopic Response has an invalid RLP list length. Expected 2, found {}", list_len); + return Err(DecoderError::RlpIncorrectListLen); + } + Message::Response(Response { + id, + body: ResponseBody::RegisterTopic { + registered: rlp.val_at::(1)?, + }, + }) + } + 9 => { + // TopicQueryRequest + if list_len != 2 { + debug!( + "TopicQuery Request has an invalid RLP list length. Expected 2, found {}", + list_len + ); + return Err(DecoderError::RlpIncorrectListLen); + } + let topic = { + let topic_bytes = rlp.val_at::>(1)?; + if topic_bytes.len() > 32 { + debug!("Ticket Request has a topic greater than 32 bytes"); + return Err(DecoderError::RlpIsTooBig); + } + let mut topic = [0u8; 32]; + topic[32 - topic_bytes.len()..].copy_from_slice(&topic_bytes); + topic + }; + Message::Request(Request { + id, + body: RequestBody::TopicQuery { topic }, + }) + } + */ + }; + + Ok(message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use enr::EnrBuilder; + + #[test] + fn ref_test_encode_request_ping() { + // reference input + let id = RequestId(vec![1]); + let enr_seq = 1; + let message = Message::Request(Request { + id, + body: RequestBody::Ping { enr_seq }, + }); + + // expected hex output + let expected_output = hex::decode("01c20101").unwrap(); + + dbg!(hex::encode(message.clone().encode())); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_request_findnode() { + // reference input + let id = RequestId(vec![1]); + let distances = vec![256]; + let message = Message::Request(Request { + id, + body: RequestBody::FindNode { distances }, + }); + + // expected hex output + let expected_output = hex::decode("03c501c3820100").unwrap(); + dbg!(hex::encode(message.clone().encode())); + + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_response_ping() { + // reference input + let id = RequestId(vec![1]); + let enr_seq = 1; + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + let port = 5000; + let message = Message::Response(Response { + id, + body: ResponseBody::Pong { enr_seq, ip, port }, + }); + + // expected hex output + let expected_output = hex::decode("02ca0101847f000001821388").unwrap(); + + dbg!(hex::encode(message.clone().encode())); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_response_nodes_empty() { + // reference input + let id = RequestId(vec![1]); + let total = 1; + + // expected hex output + let expected_output = hex::decode("04c30101c0").unwrap(); + + let message = Message::Response(Response { + id, + body: ResponseBody::Nodes { + total, + nodes: vec![], + }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_response_nodes() { + // reference input + let id = RequestId(vec![1]); + let total = 1; + + let enr = "-HW4QCjfjuCfSmIJHxqLYfGKrSz-Pq3G81DVJwd_muvFYJiIOkf0bGtJu7kZVCOPnhSTMneyvR4MRbF3G5TNB4wy2ssBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg".parse::>().unwrap(); + // expected hex output + let expected_output = hex::decode("04f87b0101f877f875b84028df8ee09f4a62091f1a8b61f18aad2cfe3eadc6f350d527077f9aebc56098883a47f46c6b49bbb91954238f9e14933277b2bd1e0c45b1771b94cd078c32dacb0182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138").unwrap(); + + let message = Message::Response(Response { + id, + body: ResponseBody::Nodes { + total, + nodes: vec![enr], + }, + }); + dbg!(hex::encode(message.clone().encode())); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_response_nodes_multiple() { + // reference input + let id = RequestId(vec![1]); + let total = 1; + let enr = "enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxaagKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg".parse::>().unwrap(); + + let enr2 = "enr:-HW4QNfxw543Ypf4HXKXdYxkyzfcxcO-6p9X986WldfVpnVTQX1xlTnWrktEWUbeTZnmgOuAY_KUhbVV1Ft98WoYUBMBgmlkgnY0iXNlY3AyNTZrMaEDDiy3QkHAxPyOgWbxp5oF1bDdlYE6dLCUUp8xfVw50jU".parse::>().unwrap(); + + // expected hex output + let expected_output = hex::decode("04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235").unwrap(); + + let message = Message::Response(Response { + id, + body: ResponseBody::Nodes { + total, + nodes: vec![enr, enr2], + }, + }); + dbg!(hex::encode(message.clone().encode())); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_decode_response_nodes_multiple() { + let input = hex::decode("04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235").unwrap(); + + let expected_enr1 = "enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxaagKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAPMljNMTg".parse::>().unwrap(); + let expected_enr2 = "enr:-HW4QNfxw543Ypf4HXKXdYxkyzfcxcO-6p9X986WldfVpnVTQX1xlTnWrktEWUbeTZnmgOuAY_KUhbVV1Ft98WoYUBMBgmlkgnY0iXNlY3AyNTZrMaEDDiy3QkHAxPyOgWbxp5oF1bDdlYE6dLCUUp8xfVw50jU".parse::>().unwrap(); + + let decoded = Message::decode(&input).unwrap(); + + match decoded { + Message::Response(response) => match response.body { + ResponseBody::Nodes { total, nodes } => { + assert_eq!(total, 1); + assert_eq!(nodes[0], expected_enr1); + assert_eq!(nodes[1], expected_enr2); + } + _ => panic!("Invalid decoding"), + }, + _ => panic!("Invalid decoding"), + } + } + + #[test] + fn encode_decode_ping_request() { + let id = RequestId(vec![1]); + let request = Message::Request(Request { + id, + body: RequestBody::Ping { enr_seq: 15 }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(&encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_ping_response() { + let id = RequestId(vec![1]); + let request = Message::Response(Response { + id, + body: ResponseBody::Pong { + enr_seq: 15, + ip: "127.0.0.1".parse().unwrap(), + port: 80, + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(&encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_find_node_request() { + let id = RequestId(vec![1]); + let request = Message::Request(Request { + id, + body: RequestBody::FindNode { + distances: vec![12], + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(&encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_nodes_response() { + let key = CombinedKey::generate_secp256k1(); + let enr1 = EnrBuilder::new("v4") + .ip("127.0.0.1".parse().unwrap()) + .udp(500) + .build(&key) + .unwrap(); + let enr2 = EnrBuilder::new("v4") + .ip("10.0.0.1".parse().unwrap()) + .tcp4(8080) + .build(&key) + .unwrap(); + let enr3 = EnrBuilder::new("v4") + .ip("10.4.5.6".parse().unwrap()) + .build(&key) + .unwrap(); + + let enr_list = vec![enr1, enr2, enr3]; + let id = RequestId(vec![1]); + let request = Message::Response(Response { + id, + body: ResponseBody::Nodes { + total: 1, + nodes: enr_list, + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(&encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_ticket_request() { + let id = RequestId(vec![1]); + let request = Message::Request(Request { + id, + body: RequestBody::Talk { + protocol: vec![17u8; 32], + request: vec![1, 2, 3], + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(&encoded).unwrap(); + + assert_eq!(request, decoded); + } + + /* + * These RPC messages are not in use yet + * + #[test] + fn ref_test_encode_request_ticket() { + // reference input + let id = 1; + let hash_bytes = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + // expected hex output + let expected_output = + hex::decode("05e201a0fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + let mut topic_hash = [0; 32]; + topic_hash.copy_from_slice(&hash_bytes); + + let message = Message::Request(Request { + id, + body: RequestBody::Ticket { topic: topic_hash }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_request_register_topic() { + // reference input + let id = 1; + let ticket = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + // expected hex output + let expected_output = + hex::decode("07e201a0fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + let message = Message::Request(Request { + id, + body: RequestBody::RegisterTopic { ticket }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_request_topic_query() { + // reference input + let id = 1; + let hash_bytes = + hex::decode("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + // expected hex output + let expected_output = + hex::decode("09e201a0fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736") + .unwrap(); + + let mut topic_hash = [0; 32]; + topic_hash.copy_from_slice(&hash_bytes); + + let message = Message::Request(Request { + id, + body: RequestBody::TopicQuery { topic: topic_hash }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn ref_test_encode_response_register_topic() { + // reference input + let id = 1; + let registered = true; + + // expected hex output + let expected_output = hex::decode("08c20101").unwrap(); + let message = Message::Response(Response { + id, + body: ResponseBody::RegisterTopic { registered }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn encode_decode_register_topic_request() { + let request = Message::Request(Request { + id: 1, + body: RequestBody::RegisterTopic { + topic: vec![1,2,3], + ticket: vec![1, 2, 3, 4, 5], + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_register_topic_response() { + let request = Message::Response(Response { + id: 0, + body: ResponseBody::RegisterTopic { registered: true }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn encode_decode_topic_query_request() { + let request = Message::Request(Request { + id: 1, + body: RequestBody::TopicQuery { topic: [17u8; 32] }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(encoded).unwrap(); + + assert_eq!(request, decoded); + } + + #[test] + fn ref_test_encode_response_ticket() { + // reference input + let id = 1; + let ticket = [0; 32].to_vec(); // all 0's + let wait_time = 5; + + // expected hex output + let expected_output = hex::decode( + "06e301a0000000000000000000000000000000000000000000000000000000000000000005", + ) + .unwrap(); + + let message = Message::Response(Response { + id, + body: ResponseBody::Ticket { ticket, wait_time }, + }); + assert_eq!(message.encode(), expected_output); + } + + #[test] + fn encode_decode_ticket_response() { + let request = Message::Response(Response { + id: 0, + body: ResponseBody::Ticket { + ticket: vec![1, 2, 3, 4, 5], + wait_time: 5, + }, + }); + + let encoded = request.clone().encode(); + let decoded = Message::decode(encoded).unwrap(); + + assert_eq!(request, decoded); + } + + */ +} diff --git a/version-meld/discv5/src/service.rs b/version-meld/discv5/src/service.rs new file mode 100644 index 0000000..7d5d6e0 --- /dev/null +++ b/version-meld/discv5/src/service.rs @@ -0,0 +1,1434 @@ +//! The Discovery v5 protocol. See `lib.rs` for further details. +//! +//! Note: Discovered ENR's are not automatically added to the routing table. Only established +//! sessions get added, ensuring only valid ENRs are added. Manual additions can be made using the +//! `add_enr()` function. +//! +//! Response to queries return `PeerId`. Only the trusted (a session has been established with) +//! `PeerId`'s are returned, as ENR's for these `PeerId`'s are stored in the routing table and as +//! such should have an address to connect to. Untrusted `PeerId`'s can be obtained from the +//! `Service::Discovered` event, which is fired as peers get discovered. +//! +//! Note that although the ENR crate does support Ed25519 keys, these are currently not +//! supported as the ECDH procedure isn't specified in the specification. Therefore, only +//! secp256k1 keys are supported currently. + +use self::{ + ip_vote::IpVote, + query_info::{QueryInfo, QueryType}, +}; +use crate::{ + error::{RequestError, ResponseError}, + handler::{Handler, HandlerRequest, HandlerResponse}, + kbucket::{ + self, ConnectionDirection, ConnectionState, FailureReason, InsertResult, KBucketsTable, + NodeStatus, UpdateResult, + }, + node_info::{NodeAddress, NodeContact}, + packet::MAX_PACKET_SIZE, + query_pool::{ + FindNodeQueryConfig, PredicateQueryConfig, QueryId, QueryPool, QueryPoolState, TargetKey, + }, + rpc, Discv5Config, Discv5Event, Enr, +}; +use enr::{CombinedKey, NodeId}; +use fnv::FnvHashMap; +use futures::prelude::*; +use hashset_delay::HashSetDelay; +use parking_lot::RwLock; +use rpc::*; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, task::Poll, time::Instant}; +use tokio::sync::{mpsc, oneshot}; +use tracing::{debug, error, info, trace, warn}; + +mod hashset_delay; +mod ip_vote; +mod query_info; +mod test; + +/// Request type for Protocols using `TalkReq` message. +/// +/// Automatically responds with an empty body on drop if +/// [`TalkRequest::respond`] is not called. +#[derive(Debug)] +pub struct TalkRequest { + id: RequestId, + node_address: NodeAddress, + protocol: Vec, + body: Vec, + sender: Option>, +} + +impl Drop for TalkRequest { + fn drop(&mut self) { + let sender = match self.sender.take() { + Some(s) => s, + None => return, + }; + + let response = Response { + id: self.id.clone(), + body: ResponseBody::Talk { response: vec![] }, + }; + + debug!("Sending empty TALK response to {}", self.node_address); + let _ = sender.send(HandlerRequest::Response( + self.node_address.clone(), + Box::new(response), + )); + } +} + +impl TalkRequest { + pub fn id(&self) -> &RequestId { + &self.id + } + + pub fn node_id(&self) -> &NodeId { + &self.node_address.node_id + } + + pub fn protocol(&self) -> &[u8] { + &self.protocol + } + + pub fn body(&self) -> &[u8] { + &self.body + } + + pub fn respond(mut self, response: Vec) -> Result<(), ResponseError> { + debug!("Sending TALK response to {}", self.node_address); + + let response = Response { + id: self.id.clone(), + body: ResponseBody::Talk { response }, + }; + + self.sender + .take() + .unwrap() + .send(HandlerRequest::Response( + self.node_address.clone(), + Box::new(response), + )) + .map_err(|_| ResponseError::ChannelClosed)?; + + Ok(()) + } +} + +/// The number of distances (buckets) we simultaneously request from each peer. +pub(crate) const DISTANCES_TO_REQUEST_PER_PEER: usize = 3; + +/// The types of requests to send to the Discv5 service. +pub enum ServiceRequest { + /// A request to start a query. There are two types of queries: + /// - A FindNode Query - Searches for peers using a random target. + /// - A Predicate Query - Searches for peers closest to a random target that match a specified + /// predicate. + StartQuery(QueryKind, oneshot::Sender>), + /// Find the ENR of a node given its multiaddr. + FindEnr(NodeContact, oneshot::Sender>), + /// The TALK discv5 RPC function. + Talk( + NodeContact, + Vec, + Vec, + oneshot::Sender, RequestError>>, + ), + /// Sets up an event stream where the discv5 server will return various events such as + /// discovered nodes as it traverses the DHT. + RequestEventStream(oneshot::Sender>), +} + +use crate::discv5::PERMIT_BAN_LIST; + +pub struct Service { + /// Configuration parameters. + config: Discv5Config, + + /// The local ENR of the server. + local_enr: Arc>, + + /// The key associated with the local ENR. + enr_key: Arc>, + + /// Storage of the ENR record for each node. + kbuckets: Arc>>, + + /// All the iterative queries we are currently performing. + queries: QueryPool, + + /// RPC requests that have been sent and are awaiting a response. Some requests are linked to a + /// query. + active_requests: FnvHashMap, + + /// Keeps track of the number of responses received from a NODES response. + active_nodes_responses: HashMap, + + /// A map of votes nodes have made about our external IP address. We accept the majority. + ip_votes: Option, + + /// The channel to send messages to the handler. + handler_send: mpsc::UnboundedSender, + + /// The channel to receive messages from the handler. + handler_recv: mpsc::Receiver, + + /// The exit channel to shutdown the handler. + handler_exit: Option>, + + /// The channel of messages sent by the controlling discv5 wrapper. + discv5_recv: mpsc::Receiver, + + /// The exit channel for the service. + exit: oneshot::Receiver<()>, + + /// A queue of peers that require regular ping to check connectivity. + peers_to_ping: HashSetDelay, + + /// A channel that the service emits events on. + event_stream: Option>, +} + +/// Active RPC request awaiting a response from the handler. +struct ActiveRequest { + /// The address the request was sent to. + pub contact: NodeContact, + /// The request that was sent. + pub request_body: RequestBody, + /// The query ID if the request was related to a query. + pub query_id: Option, + /// Channel callback if this request was from a user level request. + pub callback: Option, +} + +/// The kinds of responses we can send back to the discv5 layer. +pub enum CallbackResponse { + /// A response to a requested ENR. + Enr(oneshot::Sender>), + /// A response from a TALK request + Talk(oneshot::Sender, RequestError>>), +} + +/// For multiple responses to a FindNodes request, this keeps track of the request count +/// and the nodes that have been received. +struct NodesResponse { + /// The response count. + count: usize, + /// The filtered nodes that have been received. + received_nodes: Vec, +} + +impl Default for NodesResponse { + fn default() -> Self { + NodesResponse { + count: 1, + received_nodes: Vec::new(), + } + } +} + +impl Service { + /// Builds the `Service` main struct. + /// + /// `local_enr` is the `ENR` representing the local node. This contains node identifying information, such + /// as IP addresses and ports which we wish to broadcast to other nodes via this discovery + /// mechanism. + pub async fn spawn( + local_enr: Arc>, + enr_key: Arc>, + kbuckets: Arc>>, + config: Discv5Config, + listen_socket: SocketAddr, + ) -> Result<(oneshot::Sender<()>, mpsc::Sender), std::io::Error> { + // process behaviour-level configuration parameters + let ip_votes = if config.enr_update { + Some(IpVote::new( + config.enr_peer_update_min, + config.vote_duration, + )) + } else { + None + }; + + // build the session service + let (handler_exit, handler_send, handler_recv) = Handler::spawn( + local_enr.clone(), + enr_key.clone(), + listen_socket, + config.clone(), + ) + .await?; + + // create the required channels + let (discv5_send, discv5_recv) = mpsc::channel(30); + let (exit_send, exit) = oneshot::channel(); + + config + .executor + .clone() + .expect("Executor must be present") + .spawn(Box::pin(async move { + let mut service = Service { + local_enr, + enr_key, + kbuckets, + queries: QueryPool::new(config.query_timeout), + active_requests: Default::default(), + active_nodes_responses: HashMap::new(), + ip_votes, + handler_send, + handler_recv, + handler_exit: Some(handler_exit), + peers_to_ping: HashSetDelay::new(config.ping_interval), + discv5_recv, + event_stream: None, + exit, + config: config.clone(), + }; + + info!("Discv5 Service started"); + service.start().await; + })); + + Ok((exit_send, discv5_send)) + } + + /// The main execution loop of the discv5 serviced. + async fn start(&mut self) { + loop { + tokio::select! { + _ = &mut self.exit => { + if let Some(exit) = self.handler_exit.take() { + let _ = exit.send(()); + info!("Discv5 Service shutdown"); + } + return; + } + Some(service_request) = self.discv5_recv.recv() => { + match service_request { + ServiceRequest::StartQuery(query, callback) => { + match query { + QueryKind::FindNode { target_node } => { + self.start_findnode_query(target_node, callback); + } + QueryKind::Predicate { target_node, target_peer_no, predicate } => { + self.start_predicate_query(target_node, target_peer_no, predicate, callback); + } + } + } + ServiceRequest::FindEnr(node_contact, callback) => { + self.request_enr(node_contact, Some(callback)); + } + ServiceRequest::Talk(node_contact, protocol, request, callback) => { + self.talk_request(node_contact, protocol, request, callback); + } + ServiceRequest::RequestEventStream(callback) => { + // the channel size needs to be large to handle many discovered peers + // if we are reporting them on the event stream. + let channel_size = if self.config.report_discovered_peers { 100 } else { 30 }; + let (event_stream, event_stream_recv) = mpsc::channel(channel_size); + self.event_stream = Some(event_stream); + if callback.send(event_stream_recv).is_err() { + error!("Failed to return the event stream channel"); + } + } + } + } + Some(event) = self.handler_recv.recv() => { + match event { + HandlerResponse::Established(enr, direction) => { + self.inject_session_established(enr,direction); + } + HandlerResponse::Request(node_address, request) => { + self.handle_rpc_request(node_address, *request); + } + HandlerResponse::Response(node_address, response) => { + self.handle_rpc_response(node_address, *response); + } + HandlerResponse::WhoAreYou(whoareyou_ref) => { + // check what our latest known ENR is for this node. + if let Some(known_enr) = self.find_enr(&whoareyou_ref.0.node_id) { + let _ = self.handler_send.send(HandlerRequest::WhoAreYou(whoareyou_ref, Some(known_enr))); + } else { + // do not know of this peer + debug!("NodeId unknown, requesting ENR. {}", whoareyou_ref.0); + let _ = self.handler_send.send(HandlerRequest::WhoAreYou(whoareyou_ref, None)); + } + } + HandlerResponse::RequestFailed(request_id, error) => { + if let RequestError::Timeout = error { + debug!("RPC Request timed out. id: {}", request_id); + } else { + warn!("RPC Request failed: id: {}, error {:?}", request_id, error); + } + self.rpc_failure(request_id, error); + } + } + } + event = Service::bucket_maintenance_poll(&self.kbuckets) => { + self.send_event(event); + } + query_event = Service::query_event_poll(&mut self.queries) => { + match query_event { + QueryEvent::Waiting(query_id, node_id, request_body) => { + self.send_rpc_query(query_id, node_id, request_body); + } + // Note: Currently the distinction between a timed-out query and a finished + // query is superfluous, however it may be useful in future versions. + QueryEvent::Finished(query) | QueryEvent::TimedOut(query) => { + let id = query.id(); + let mut result = query.into_result(); + // obtain the ENR's for the resulting nodes + let mut found_enrs = Vec::new(); + for node_id in result.closest_peers.into_iter() { + if let Some(position) = result.target.untrusted_enrs.iter().position(|enr| enr.node_id() == node_id) { + let enr = result.target.untrusted_enrs.swap_remove(position); + found_enrs.push(enr); + } else if let Some(enr) = self.find_enr(&node_id) { + // look up from the routing table + found_enrs.push(enr); + } + else { + warn!("ENR not present in queries results"); + } + } + if result.target.callback.send(found_enrs).is_err() { + warn!("Callback dropped for query {}. Results dropped", *id); + } + } + } + } + Some(Ok(node_id)) = self.peers_to_ping.next() => { + // If the node is in the routing table, Ping it and re-queue the node. + let key = kbucket::Key::from(node_id); + let enr = { + if let kbucket::Entry::Present(entry, _) = self.kbuckets.write().entry(&key) { + // The peer is in the routing table, ping it and re-queue the ping + self.peers_to_ping.insert(node_id); + Some(entry.value().clone()) + } else { None } + }; + + if let Some(enr) = enr { + self.send_ping(enr); + } + } + } + } + } + + /// Internal function that starts a query. + fn start_findnode_query(&mut self, target_node: NodeId, callback: oneshot::Sender>) { + let mut target = QueryInfo { + query_type: QueryType::FindNode(target_node), + untrusted_enrs: Default::default(), + distances_to_request: DISTANCES_TO_REQUEST_PER_PEER, + callback, + }; + + let target_key: kbucket::Key = target.key(); + let mut known_closest_peers = Vec::new(); + { + let mut kbuckets = self.kbuckets.write(); + for closest in kbuckets.closest_values(&target_key) { + // Add the known ENR's to the untrusted list + target.untrusted_enrs.push(closest.value); + // Add the key to the list for the query + known_closest_peers.push(closest.key); + } + } + + if known_closest_peers.is_empty() { + warn!("No known_closest_peers found. Return empty result without sending query."); + if target.callback.send(vec![]).is_err() { + warn!("Failed to callback"); + } + } else { + let query_config = FindNodeQueryConfig::new_from_config(&self.config); + self.queries + .add_findnode_query(query_config, target, known_closest_peers); + } + } + + /// Internal function that starts a query. + fn start_predicate_query( + &mut self, + target_node: NodeId, + num_nodes: usize, + predicate: Box bool + Send>, + callback: oneshot::Sender>, + ) { + let mut target = QueryInfo { + query_type: QueryType::FindNode(target_node), + untrusted_enrs: Default::default(), + distances_to_request: DISTANCES_TO_REQUEST_PER_PEER, + callback, + }; + + let target_key: kbucket::Key = target.key(); + + // Map the TableEntry to an ENR. + let kbucket_predicate = |e: &Enr| predicate(e); + + let mut known_closest_peers = Vec::>::new(); + { + let mut kbuckets = self.kbuckets.write(); + for closest in kbuckets.closest_values_predicate(&target_key, &kbucket_predicate) { + // Add the known ENR's to the untrusted list + target.untrusted_enrs.push(closest.value.clone()); + // Add the key to the list for the query + known_closest_peers.push(closest.into()); + } + }; + + if known_closest_peers.is_empty() { + warn!("No known_closest_peers found. Return empty result without sending query."); + if target.callback.send(vec![]).is_err() { + warn!("Failed to callback"); + } + } else { + let mut query_config = PredicateQueryConfig::new_from_config(&self.config); + query_config.num_results = num_nodes; + self.queries + .add_predicate_query(query_config, target, known_closest_peers, predicate); + } + } + + /// Returns an ENR if one is known for the given NodeId. + pub fn find_enr(&self, node_id: &NodeId) -> Option { + // check if we know this node id in our routing table + let key = kbucket::Key::from(*node_id); + if let kbucket::Entry::Present(entry, _) = self.kbuckets.write().entry(&key) { + return Some(entry.value().clone()); + } + // check the untrusted addresses for ongoing queries + for query in self.queries.iter() { + if let Some(enr) = query + .target() + .untrusted_enrs + .iter() + .find(|v| v.node_id() == *node_id) + { + return Some(enr.clone()); + } + } + None + } + + /// Processes an RPC request from a peer. Requests respond to the received socket address, + /// rather than the IP of the known ENR. + fn handle_rpc_request(&mut self, node_address: NodeAddress, req: Request) { + let id = req.id; + match req.body { + RequestBody::FindNode { distances } => { + self.send_nodes_response(node_address, id, distances); + } + RequestBody::Ping { enr_seq } => { + // check if we need to update the known ENR + let mut to_request_enr = None; + match self.kbuckets.write().entry(&node_address.node_id.into()) { + kbucket::Entry::Present(ref mut entry, _) => { + if entry.value().seq() < enr_seq { + let enr = entry.value().clone(); + to_request_enr = Some(enr.into()); + } + } + kbucket::Entry::Pending(ref mut entry, _) => { + if entry.value().seq() < enr_seq { + let enr = entry.value().clone(); + to_request_enr = Some(enr.into()); + } + } + // don't know of the ENR, request the update + _ => {} + } + if let Some(enr) = to_request_enr { + self.request_enr(enr, None); + } + + // build the PONG response + let src = node_address.socket_addr; + let response = Response { + id, + body: ResponseBody::Pong { + enr_seq: self.local_enr.read().seq(), + ip: src.ip(), + port: src.port(), + }, + }; + debug!("Sending PONG response to {}", node_address); + let _ = self + .handler_send + .send(HandlerRequest::Response(node_address, Box::new(response))); + } + RequestBody::Talk { protocol, request } => { + let req = TalkRequest { + id, + node_address, + protocol, + body: request, + sender: Some(self.handler_send.clone()), + }; + + self.send_event(Discv5Event::TalkRequest(req)); + } + RequestBody::RegisterTopic { .. } => { + debug!("Received RegisterTopic request which is unimplemented"); + } + RequestBody::TopicQuery { .. } => { + debug!("Received TopicQuery request which is unimplemented"); + } + } + } + + /// Processes an RPC response from a peer. + fn handle_rpc_response(&mut self, node_address: NodeAddress, response: Response) { + // verify we know of the rpc_id + let id = response.id.clone(); + + if let Some(mut active_request) = self.active_requests.remove(&id) { + debug!( + "Received RPC response: {} to request: {} from: {}", + response.body, active_request.request_body, active_request.contact + ); + + // Check that the responder matches the expected request + if let Ok(request_node_address) = active_request.contact.node_address() { + if request_node_address != node_address { + warn!("Received a response from an unexpected address. Expected {}, received {}, request_id {}", request_node_address, node_address, id); + return; + } + } + + let node_id = active_request.contact.node_id(); + if !response.match_request(&active_request.request_body) { + warn!( + "Node gave an incorrect response type. Ignoring response from: {}", + active_request.contact + ); + return; + } + match response.body { + ResponseBody::Nodes { total, mut nodes } => { + // Currently a maximum of DISTANCES_TO_REQUEST_PER_PEER*BUCKET_SIZE peers can be returned. Datagrams have a max + // size of 1280 and ENR's have a max size of 300 bytes. + // + // Bucket sizes should be 16. In this case, there should be no more than 5*DISTANCES_TO_REQUEST_PER_PEER responses, to return all required peers. + if total > 5 * DISTANCES_TO_REQUEST_PER_PEER as u64 { + warn!( + "NodesResponse has a total larger than {}, nodes will be truncated", + DISTANCES_TO_REQUEST_PER_PEER * 5 + ); + } + + // These are sanitized and ordered + let distances_requested = match &active_request.request_body { + RequestBody::FindNode { distances } => distances, + _ => unreachable!(), + }; + + // This could be an ENR request from the outer service. If so respond to the + // callback and End. + if let Some(CallbackResponse::Enr(callback)) = active_request.callback.take() { + // Currently only support requesting for ENR's. Verify this is the case. + if !distances_requested.is_empty() && distances_requested[0] != 0 { + error!("Retrieved a callback request that wasn't for a peer's ENR"); + return; + } + // This must be for asking for an ENR + if nodes.len() > 1 { + warn!( + "Peer returned more than one ENR for itself. {}", + active_request.contact + ); + } + let response = nodes.pop().ok_or_else(|| { + RequestError::InvalidEnr("Peer did not return an ENR".into()) + }); + let _ = callback.send(response); + return; + } + + // Filter out any nodes that are not of the correct distance + let peer_key: kbucket::Key = node_id.into(); + + // The distances we send are sanitized an ordered. + // We never send an ENR request in combination of other requests. + if distances_requested.len() == 1 && distances_requested[0] == 0 { + // we requested an ENR update + if nodes.len() > 1 { + warn!( + "Peer returned more than one ENR for itself. Blacklisting {}", + active_request.contact + ); + } + let ban_timeout = self.config.ban_duration.map(|v| Instant::now() + v); + PERMIT_BAN_LIST.write().ban( + active_request + .contact + .node_address() + .expect("Sanitized request"), + ban_timeout, + ); + nodes.retain(|enr| peer_key.log2_distance(&enr.node_id().into()).is_none()); + } else { + let before_len = nodes.len(); + nodes.retain(|enr| { + peer_key + .log2_distance(&enr.node_id().into()) + .map(|distance| distances_requested.contains(&distance)) + .unwrap_or_else(|| false) + }); + + if nodes.len() < before_len { + // Peer sent invalid ENRs. Blacklist the Node + warn!( + "Peer sent invalid ENR. Blacklisting {}", + active_request.contact + ); + let ban_timeout = self.config.ban_duration.map(|v| Instant::now() + v); + PERMIT_BAN_LIST.write().ban( + active_request + .contact + .node_address() + .expect("Sanitized request"), + ban_timeout, + ); + } + } + + // handle the case that there is more than one response + if total > 1 { + let mut current_response = self + .active_nodes_responses + .remove(&node_id) + .unwrap_or_default(); + + debug!( + "Nodes Response: {} of {} received", + current_response.count, total + ); + // if there are more requests coming, store the nodes and wait for + // another response + // We allow for implementations to send at a minimum 3 nodes per response. + // We allow for the number of nodes to be returned as the maximum we emit. + if current_response.count < self.config.max_nodes_response / 3 + 1 + && (current_response.count as u64) < total + { + current_response.count += 1; + + current_response.received_nodes.append(&mut nodes); + self.active_nodes_responses + .insert(node_id, current_response); + self.active_requests.insert(id, active_request); + return; + } + + // have received all the Nodes responses we are willing to accept + // ignore duplicates here as they will be handled when adding + // to the DHT + current_response.received_nodes.append(&mut nodes); + nodes = current_response.received_nodes; + } + + debug!( + "Received a nodes response of len: {}, total: {}, from: {}", + nodes.len(), + total, + active_request.contact + ); + // note: If a peer sends an initial NODES response with a total > 1 then + // in a later response sends a response with a total of 1, all previous nodes + // will be ignored. + // ensure any mapping is removed in this rare case + self.active_nodes_responses.remove(&node_id); + + self.discovered(&node_id, nodes, active_request.query_id); + } + ResponseBody::Pong { enr_seq, ip, port } => { + let socket = SocketAddr::new(ip, port); + // perform ENR majority-based update if required. + // + // only attempt the majority-update if the peer supplies an ipv4 address to + // mitigate https://github.com/sigp/lighthouse/issues/2215 + // + // Only count votes that from peers we have contacted. + let key: kbucket::Key = node_id.into(); + let should_count = match self.kbuckets.write().entry(&key) { + kbucket::Entry::Present(_, status) + if status.is_connected() && !status.is_incoming() => + { + true + } + _ => false, + }; + + if should_count && socket.is_ipv4() { + let local_socket = self.local_enr.read().udp_socket(); + if let Some(ref mut ip_votes) = self.ip_votes { + ip_votes.insert(node_id, socket); + if let Some(majority_socket) = ip_votes.majority() { + if Some(majority_socket) != local_socket { + info!("Local UDP socket updated to: {}", majority_socket); + self.send_event(Discv5Event::SocketUpdated(majority_socket)); + // Update the UDP socket + if self + .local_enr + .write() + .set_udp_socket(majority_socket, &self.enr_key.read()) + .is_ok() + { + self.ping_connected_peers(); + } + } + } + } + } + + // check if we need to request a new ENR + if let Some(enr) = self.find_enr(&node_id) { + if enr.seq() < enr_seq { + // request an ENR update + debug!("Requesting an ENR update from: {}", active_request.contact); + let request_body = RequestBody::FindNode { distances: vec![0] }; + let active_request = ActiveRequest { + contact: active_request.contact, + request_body, + query_id: None, + callback: None, + }; + self.send_rpc_request(active_request); + } + self.connection_updated(node_id, ConnectionStatus::PongReceived(enr)); + } + } + ResponseBody::Talk { response } => { + // Send the response to the user + match active_request.callback { + Some(CallbackResponse::Talk(callback)) => { + let _ = callback.send(Ok(response)); + } + _ => error!("Invalid callback for response"), + } + } + ResponseBody::Ticket { .. } => { + error!("Received a TICKET response. This is unimplemented and should be unreachable."); + } + ResponseBody::RegisterConfirmation { .. } => { + error!("Received a RegisterConfirmation response. This is unimplemented and should be unreachable."); + } + } + } else { + warn!( + "Received an RPC response which doesn't match a request. Id: {}", + id + ); + } + } + + // Send RPC Requests // + + /// Sends a PING request to a node. + fn send_ping(&mut self, enr: Enr) { + let request_body = RequestBody::Ping { + enr_seq: self.local_enr.read().seq(), + }; + let active_request = ActiveRequest { + contact: enr.into(), + request_body, + query_id: None, + callback: None, + }; + self.send_rpc_request(active_request); + } + + /// Ping all peers that are connected in the routing table. + fn ping_connected_peers(&mut self) { + // maintain the ping interval + let connected_peers = { + let mut kbuckets = self.kbuckets.write(); + kbuckets + .iter() + .filter_map(|entry| { + if entry.status.is_connected() { + Some(entry.node.value.clone()) + } else { + None + } + }) + .collect::>() + }; + + for enr in connected_peers { + self.send_ping(enr.clone()); + } + } + + /// Request an external node's ENR. + fn request_enr( + &mut self, + contact: NodeContact, + callback: Option>>, + ) { + let request_body = RequestBody::FindNode { distances: vec![0] }; + let active_request = ActiveRequest { + contact, + request_body, + query_id: None, + callback: callback.map(CallbackResponse::Enr), + }; + self.send_rpc_request(active_request); + } + + /// Requests a TALK message from the peer. + fn talk_request( + &mut self, + contact: NodeContact, + protocol: Vec, + request: Vec, + callback: oneshot::Sender, RequestError>>, + ) { + let request_body = RequestBody::Talk { protocol, request }; + + let active_request = ActiveRequest { + contact, + request_body, + query_id: None, + callback: Some(CallbackResponse::Talk(callback)), + }; + self.send_rpc_request(active_request); + } + + /// Sends a NODES response, given a list of found ENR's. This function splits the nodes up + /// into multiple responses to ensure the response stays below the maximum packet size. + fn send_nodes_response( + &mut self, + node_address: NodeAddress, + rpc_id: RequestId, + mut distances: Vec, + ) { + // NOTE: At most we only allow 5 distances to be sent (see the decoder). If each of these + // buckets are full, that equates to 80 ENR's to respond with. + + let mut nodes_to_send = Vec::new(); + distances.sort_unstable(); + distances.dedup(); + + if let Some(0) = distances.first() { + // if the distance is 0 send our local ENR + nodes_to_send.push(self.local_enr.read().clone()); + debug!("Sending our ENR to node: {}", node_address); + distances.remove(0); + } + + if !distances.is_empty() { + let mut kbuckets = self.kbuckets.write(); + for node in kbuckets + .nodes_by_distances(distances.as_slice(), self.config.max_nodes_response) + .into_iter() + .filter_map(|entry| { + if entry.node.key.preimage() != &node_address.node_id { + Some(entry.node.value.clone()) + } else { + None + } + }) + { + nodes_to_send.push(node); + } + } + + // if there are no nodes, send an empty response + if nodes_to_send.is_empty() { + let response = Response { + id: rpc_id, + body: ResponseBody::Nodes { + total: 1u64, + nodes: Vec::new(), + }, + }; + trace!( + "Sending empty FINDNODES response to: {}", + node_address.node_id + ); + let _ = self + .handler_send + .send(HandlerRequest::Response(node_address, Box::new(response))); + } else { + // build the NODES response + let mut to_send_nodes: Vec> = Vec::new(); + let mut total_size = 0; + let mut rpc_index = 0; + to_send_nodes.push(Vec::new()); + for enr in nodes_to_send.into_iter() { + let entry_size = rlp::encode(&enr).len(); + // Responses assume that a session is established. Thus, on top of the encoded + // ENR's the packet should be a regular message. A regular message has an IV (16 + // bytes), and a header of 55 bytes. The find-nodes RPC requires 16 bytes for the ID and the + // `total` field. Also there is a 16 byte HMAC for encryption and an extra byte for + // RLP encoding. + // + // We could also be responding via an autheader which can take up to 282 bytes in its + // header. + // As most messages will be normal messages we will try and pack as many ENR's we + // can in and drop the response packet if a user requests an auth message of a very + // packed response. + // + // The estimated total overhead for a regular message is therefore 104 bytes. + if entry_size + total_size < MAX_PACKET_SIZE - 104 { + total_size += entry_size; + trace!( + "Adding ENR {}, size {}, total size {}", + enr, + entry_size, + total_size + ); + to_send_nodes[rpc_index].push(enr); + } else { + total_size = entry_size; + to_send_nodes.push(vec![enr]); + rpc_index += 1; + } + } + + let responses: Vec = to_send_nodes + .into_iter() + .map(|nodes| Response { + id: rpc_id.clone(), + body: ResponseBody::Nodes { + total: (rpc_index + 1) as u64, + nodes, + }, + }) + .collect(); + + for response in responses { + trace!( + "Sending FINDNODES response to: {}. Response: {} ", + node_address, + response + ); + let _ = self.handler_send.send(HandlerRequest::Response( + node_address.clone(), + Box::new(response), + )); + } + } + } + + /// Constructs and sends a request RPC to the session service given a `QueryInfo`. + fn send_rpc_query( + &mut self, + query_id: QueryId, + return_peer: NodeId, + request_body: RequestBody, + ) { + // find the ENR associated with the query + if let Some(enr) = self.find_enr(&return_peer) { + let active_request = ActiveRequest { + contact: enr.into(), + request_body, + query_id: Some(query_id), + callback: None, + }; + self.send_rpc_request(active_request); + } else { + error!("Query {} requested an unknown ENR", *query_id); + } + } + + /// Sends generic RPC requests. Each request gets added to known outputs, awaiting a response. + fn send_rpc_request(&mut self, active_request: ActiveRequest) { + // Generate a random rpc_id which is matched per node id + let id = RequestId::random(); + let request: Request = Request { + id: id.clone(), + body: active_request.request_body.clone(), + }; + let contact = active_request.contact.clone(); + self.active_requests.insert(id, active_request); + debug!("Sending RPC {} to node: {}", request, contact); + + let _ = self + .handler_send + .send(HandlerRequest::Request(contact, Box::new(request))); + } + + fn send_event(&mut self, event: Discv5Event) { + if let Some(stream) = self.event_stream.as_mut() { + if let Err(mpsc::error::TrySendError::Closed(_)) = stream.try_send(event) { + // If the stream has been dropped prevent future attempts to send events + self.event_stream = None; + } + } + } + + /// Processes discovered peers from a query. + fn discovered(&mut self, source: &NodeId, mut enrs: Vec, query_id: Option) { + let local_id = self.local_enr.read().node_id(); + enrs.retain(|enr| { + if enr.node_id() == local_id { + return false; + } + + // If any of the discovered nodes are in the routing table, and there contains an older ENR, update it. + // If there is an event stream send the Discovered event + if self.config.report_discovered_peers { + self.send_event(Discv5Event::Discovered(enr.clone())); + } + + // ignore peers that don't pass the table filter + if (self.config.table_filter)(enr) { + let key = kbucket::Key::from(enr.node_id()); + + // If the ENR exists in the routing table and the discovered ENR has a greater + // sequence number, perform some filter checks before updating the enr. + + let must_update_enr = match self.kbuckets.write().entry(&key) { + kbucket::Entry::Present(entry, _) => entry.value().seq() < enr.seq(), + kbucket::Entry::Pending(mut entry, _) => entry.value().seq() < enr.seq(), + _ => false, + }; + + if must_update_enr { + if let UpdateResult::Failed(reason) = + self.kbuckets.write().update_node(&key, enr.clone(), None) + { + self.peers_to_ping.remove(&enr.node_id()); + debug!( + "Failed to update discovered ENR. Node: {}, Reason: {:?}", + source, reason + ); + + false // Remove this peer from the discovered list + } else { + true // Keep this peer in the list + } + } else { + true // We don't need to update ENR + } + } else { + false // Didn't pass the table filter + } + }); + + // if this is part of a query, update the query + if let Some(query_id) = query_id { + if let Some(query) = self.queries.get_mut(query_id) { + let mut peer_count = 0; + for enr_ref in enrs.iter() { + if !query + .target_mut() + .untrusted_enrs + .iter() + .any(|e| e.node_id() == enr_ref.node_id()) + { + query.target_mut().untrusted_enrs.push(enr_ref.clone()); + } + peer_count += 1; + } + debug!("{} peers found for query id {:?}", peer_count, query_id); + query.on_success(source, &enrs) + } else { + debug!("Response returned for ended query {:?}", query_id) + } + } + } + + /// Update the connection status of a node in the routing table. + /// This tracks whether or not we should be pinging peers. Disconnected peers are removed from + /// the queue and newly added peers to the routing table are added to the queue. + fn connection_updated(&mut self, node_id: NodeId, new_status: ConnectionStatus) { + // Variables to that may require post-processing + let mut ping_peer = None; + let mut event_to_send = None; + + let key = kbucket::Key::from(node_id); + match new_status { + ConnectionStatus::Connected(enr, direction) => { + // attempt to update or insert the new ENR. + let status = NodeStatus { + state: ConnectionState::Connected, + direction, + }; + match self.kbuckets.write().insert_or_update(&key, enr, status) { + InsertResult::Inserted => { + // We added this peer to the table + debug!("New connected node added to routing table: {}", node_id); + self.peers_to_ping.insert(node_id); + let event = Discv5Event::NodeInserted { + node_id, + replaced: None, + }; + event_to_send = Some(event); + } + InsertResult::Pending { disconnected } => { + ping_peer = Some(disconnected); + } + InsertResult::StatusUpdated { + promoted_to_connected, + } + | InsertResult::Updated { + promoted_to_connected, + } => { + // The node was updated + if promoted_to_connected { + debug!("Node promoted to connected: {}", node_id); + self.peers_to_ping.insert(node_id); + } + } + InsertResult::ValueUpdated | InsertResult::UpdatedPending => {} + InsertResult::Failed(reason) => { + self.peers_to_ping.remove(&node_id); + trace!("Could not insert node: {}, reason: {:?}", node_id, reason); + } + } + } + ConnectionStatus::PongReceived(enr) => { + match self + .kbuckets + .write() + .update_node(&key, enr, Some(ConnectionState::Connected)) + { + UpdateResult::Failed(reason) => { + self.peers_to_ping.remove(&node_id); + debug!( + "Could not update ENR from pong. Node: {}, reason: {:?}", + node_id, reason + ); + } + update => { + debug!("Updated {:?}", update) + } // Updated ENR successfully. + } + } + ConnectionStatus::Disconnected => { + // If the node has disconnected, remove any ping timer for the node. + match self.kbuckets.write().update_node_status( + &key, + ConnectionState::Disconnected, + None, + ) { + UpdateResult::Failed(reason) => match reason { + FailureReason::KeyNonExistant => {} + others => { + warn!( + "Could not update node to disconnected. Node: {}, Reason: {:?}", + node_id, others + ); + } + }, + _ => { + debug!("Node set to disconnected: {}", node_id) + } + } + self.peers_to_ping.remove(&node_id); + } + }; + + // Post processing + + if let Some(event) = event_to_send { + self.send_event(event); + } + + if let Some(node_key) = ping_peer { + let optional_enr = { + if let kbucket::Entry::Present(entry, _status) = + self.kbuckets.write().entry(&node_key) + { + // NOTE: We don't check the status of this peer. We try and ping outdated peers. + Some(entry.value().clone()) + } else { + None + } + }; + if let Some(enr) = optional_enr { + self.send_ping(enr) + } + } + } + + /// The equivalent of libp2p `inject_connected()` for a udp session. We have no stream, but a + /// session key-pair has been negotiated. + fn inject_session_established(&mut self, enr: Enr, direction: ConnectionDirection) { + // Ignore sessions with non-contactable ENRs + if enr.udp_socket().is_none() { + return; + } + + let node_id = enr.node_id(); + debug!( + "Session established with Node: {}, direction: {}", + node_id, direction + ); + self.connection_updated(node_id, ConnectionStatus::Connected(enr, direction)); + } + + /// A session could not be established or an RPC request timed-out (after a few retries, if + /// specified). + fn rpc_failure(&mut self, id: RequestId, error: RequestError) { + trace!("RPC Error removing request. Reason: {:?}, id {}", error, id); + if let Some(active_request) = self.active_requests.remove(&id) { + // If this is initiated by the user, return an error on the callback. All callbacks + // support a request error. + match active_request.callback { + Some(CallbackResponse::Enr(callback)) => { + callback + .send(Err(error)) + .unwrap_or_else(|_| debug!("Couldn't send TALK error response to user")); + return; + } + Some(CallbackResponse::Talk(callback)) => { + // return the error + callback + .send(Err(error)) + .unwrap_or_else(|_| debug!("Couldn't send TALK error response to user")); + return; + } + None => { + // no callback to send too + } + } + + let node_id = active_request.contact.node_id(); + match active_request.request_body { + // if a failed FindNodes request, ensure we haven't partially received packets. If + // so, process the partially found nodes + RequestBody::FindNode { .. } => { + if let Some(nodes_response) = self.active_nodes_responses.remove(&node_id) { + if !nodes_response.received_nodes.is_empty() { + warn!( + "NODES Response failed, but was partially processed from: {}", + active_request.contact + ); + // if it's a query mark it as success, to process the partial + // collection of peers + self.discovered( + &node_id, + nodes_response.received_nodes, + active_request.query_id, + ); + } + } else { + // there was no partially downloaded nodes inform the query of the failure + // if it's part of a query + if let Some(query_id) = active_request.query_id { + if let Some(query) = self.queries.get_mut(query_id) { + query.on_failure(&node_id); + } + } else { + debug!( + "Failed RPC request: {}: {} ", + active_request.request_body, active_request.contact + ); + } + } + } + // for all other requests, if any are queries, mark them as failures. + _ => { + if let Some(query_id) = active_request.query_id { + if let Some(query) = self.queries.get_mut(query_id) { + debug!( + "Failed query request: {} for query: {} and {} ", + active_request.request_body, *query_id, active_request.contact + ); + query.on_failure(&node_id); + } + } else { + debug!( + "Failed RPC request: {} for node: {}, reason {:?} ", + active_request.request_body, active_request.contact, error + ); + } + } + } + + self.connection_updated(node_id, ConnectionStatus::Disconnected); + } + } + + /// A future that maintains the routing table and inserts nodes when required. This returns the + /// `Discv5Event::NodeInserted` variant if a new node has been inserted into the routing table. + async fn bucket_maintenance_poll( + kbuckets: &Arc>>, + ) -> Discv5Event { + future::poll_fn(move |_cx| { + // Drain applied pending entries from the routing table. + if let Some(entry) = kbuckets.write().take_applied_pending() { + let event = Discv5Event::NodeInserted { + node_id: entry.inserted.into_preimage(), + replaced: entry.evicted.map(|n| n.key.into_preimage()), + }; + return Poll::Ready(event); + } + Poll::Pending + }) + .await + } + + /// A future the maintains active queries. This returns completed and timed out queries, as + /// well as queries which need to be driven further with extra requests. + async fn query_event_poll(queries: &mut QueryPool) -> QueryEvent { + future::poll_fn(move |_cx| match queries.poll() { + QueryPoolState::Finished(query) => Poll::Ready(QueryEvent::Finished(Box::new(query))), + QueryPoolState::Waiting(Some((query, return_peer))) => { + let node_id = return_peer; + + let request_body = match query.target().rpc_request(return_peer) { + Ok(r) => r, + Err(e) => { + // dst node is local_key, report failure + error!("Send RPC failed: {}", e); + query.on_failure(&node_id); + return Poll::Pending; + } + }; + + Poll::Ready(QueryEvent::Waiting(query.id(), node_id, request_body)) + } + QueryPoolState::Timeout(query) => { + warn!("Query id: {:?} timed out", query.id()); + Poll::Ready(QueryEvent::TimedOut(Box::new(query))) + } + QueryPoolState::Waiting(None) | QueryPoolState::Idle => Poll::Pending, + }) + .await + } +} + +/// The result of the `query_event_poll` indicating an action is required to further progress an +/// active query. +enum QueryEvent { + /// The query is waiting for a peer to be contacted. + Waiting(QueryId, NodeId, RequestBody), + /// The query has timed out, possible returning peers. + TimedOut(Box>), + /// The query has completed successfully. + Finished(Box>), +} + +/// The types of queries that can be made. +pub enum QueryKind { + /// A FindNode query. Searches for peers that are closest to a particular target. + FindNode { target_node: NodeId }, + /// A predicate query. Searches for peers that are close to a target but filtered by a specific + /// predicate and limited by a target peer count. + Predicate { + target_node: NodeId, + target_peer_no: usize, + predicate: Box bool + Send>, + }, +} + +/// Reporting the connection status of a node. +enum ConnectionStatus { + /// A node has started a new connection with us. + Connected(Enr, ConnectionDirection), + /// We received a Pong from a new node. Do not have the connection direction. + PongReceived(Enr), + /// The node has disconnected + Disconnected, +} diff --git a/version-meld/discv5/src/service/hashset_delay.rs b/version-meld/discv5/src/service/hashset_delay.rs new file mode 100644 index 0000000..926642e --- /dev/null +++ b/version-meld/discv5/src/service/hashset_delay.rs @@ -0,0 +1,131 @@ +//! A simple hashset object coupled with a `delay_queue` which has entries that expire after a +//! fixed time. +//! +//! A `HashSetDelay` implements `Stream` which removes expired items from the map. + +/// The default delay for entries, in seconds. This is only used when `insert()` is used to add +/// entries. +const DEFAULT_DELAY: u64 = 30; + +use futures::prelude::*; +use std::{ + collections::HashMap, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; +use tokio_util::time::delay_queue::{self, DelayQueue}; + +pub struct HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// The given entries. + entries: HashMap, + /// A queue holding the timeouts of each entry. + expirations: DelayQueue, + /// The default expiration timeout of an entry. + default_entry_timeout: Duration, +} + +impl Default for HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + fn default() -> Self { + HashSetDelay::new(Duration::from_secs(DEFAULT_DELAY)) + } +} + +impl HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + /// Creates a new instance of `HashSetDelay`. + pub fn new(default_entry_timeout: Duration) -> Self { + HashSetDelay { + entries: HashMap::new(), + expirations: DelayQueue::new(), + default_entry_timeout, + } + } + + /// Insert an entry into the mapping. Entries will expire after the `default_entry_timeout`. + pub fn insert(&mut self, key: K) { + self.insert_at(key, self.default_entry_timeout); + } + + /// Inserts an entry that will expire at a given instant. + pub fn insert_at(&mut self, key: K, entry_duration: Duration) { + if self.contains_key(&key) { + // update the timeout + self.update_timeout(&key, entry_duration); + } else { + let delay_key = self.expirations.insert(key.clone(), entry_duration); + self.entries.insert(key, delay_key); + } + } + + /// Updates the timeout for a given key. Returns true if the key existed, false otherwise. + /// + /// Panics if the duration is too far in the future. + pub fn update_timeout(&mut self, key: &K, timeout: Duration) -> bool { + if let Some(delay_key) = self.entries.get_mut(key) { + self.expirations.reset(delay_key, timeout); + true + } else { + false + } + } + + /// Returns true if the key exists, false otherwise. + pub fn contains_key(&self, key: &K) -> bool { + self.entries.contains_key(key) + } + + /// Returns the length of the mapping. + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.entries.len() + } + + /// Removes a key from the map returning the value associated with the key that was in the map. + /// + /// Return None if the key was not in the map. + pub fn remove(&mut self, key: &K) -> bool { + if let Some(delay_key) = self.entries.remove(key) { + self.expirations.remove(&delay_key); + true + } else { + false + } + } + + /// Removes all entries from the map. + #[allow(dead_code)] + pub fn clear(&mut self) { + self.entries.clear(); + self.expirations.clear(); + } +} + +impl Stream for HashSetDelay +where + K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.expirations.poll_expired(cx) { + Poll::Ready(Some(Ok(key))) => match self.entries.remove(key.get_ref()) { + Some(_delay_key) => Poll::Ready(Some(Ok(key.into_inner()))), + None => Poll::Ready(Some(Err("Value no longer exists in expirations".into()))), + }, + Poll::Ready(Some(Err(e))) => { + Poll::Ready(Some(Err(format!("delay queue error: {:?}", e)))) + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/version-meld/discv5/src/service/ip_vote.rs b/version-meld/discv5/src/service/ip_vote.rs new file mode 100644 index 0000000..3d34ffd --- /dev/null +++ b/version-meld/discv5/src/service/ip_vote.rs @@ -0,0 +1,113 @@ +use enr::NodeId; +use fnv::FnvHashMap; +use std::{ + collections::HashMap, + net::SocketAddr, + time::{Duration, Instant}, +}; + +/// A collection of IP:Ports for our node reported from external peers. +pub(crate) struct IpVote { + /// The current collection of IP:Port votes. + votes: HashMap, + /// The minimum number of votes required before an IP/PORT is accepted. + minimum_threshold: usize, + /// The time votes remain valid. + vote_duration: Duration, +} + +impl IpVote { + pub fn new(minimum_threshold: usize, vote_duration: Duration) -> Self { + // do not allow minimum thresholds less than 2 + if minimum_threshold < 2 { + panic!("Setting enr_peer_update_min to a value less than 2 will cause issues with discovery with peers behind NAT"); + } + IpVote { + votes: HashMap::new(), + minimum_threshold, + vote_duration, + } + } + + pub fn insert(&mut self, key: NodeId, socket: SocketAddr) { + self.votes + .insert(key, (socket, Instant::now() + self.vote_duration)); + } + + /// Returns the majority `SocketAddr` if it exists. If there are not enough votes to meet the threshold this returns None. + pub fn majority(&mut self) -> Option { + // remove any expired votes + let instant = Instant::now(); + self.votes.retain(|_, v| v.1 > instant); + + // count votes, take majority + let mut ip_count: FnvHashMap = FnvHashMap::default(); + for (socket, _) in self.votes.values() { + *ip_count.entry(*socket).or_insert_with(|| 0) += 1; + } + + // find the maximum socket addr + ip_count + .into_iter() + .filter(|v| v.1 >= self.minimum_threshold) + .max_by_key(|v| v.1) + .map(|v| v.0) + } +} + +#[cfg(test)] +mod tests { + use super::{Duration, IpVote, NodeId, SocketAddr}; + + #[test] + fn test_three_way_vote_draw() { + let mut votes = IpVote::new(2, Duration::from_secs(10)); + + let socket_1 = SocketAddr::new("127.0.0.1".parse().unwrap(), 1); + let socket_2 = SocketAddr::new("127.0.0.1".parse().unwrap(), 2); + let socket_3 = SocketAddr::new("127.0.0.1".parse().unwrap(), 3); + + // 3 votes for each socket + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_2); + votes.insert(NodeId::random(), socket_2); + votes.insert(NodeId::random(), socket_2); + votes.insert(NodeId::random(), socket_3); + votes.insert(NodeId::random(), socket_3); + votes.insert(NodeId::random(), socket_3); + + assert_eq!(votes.majority(), Some(socket_2)); + } + + #[test] + fn test_majority_vote() { + let mut votes = IpVote::new(2, Duration::from_secs(10)); + let socket_1 = SocketAddr::new("127.0.0.1".parse().unwrap(), 1); + let socket_2 = SocketAddr::new("127.0.0.1".parse().unwrap(), 2); + let socket_3 = SocketAddr::new("127.0.0.1".parse().unwrap(), 3); + + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_2); + votes.insert(NodeId::random(), socket_3); + + assert_eq!(votes.majority(), Some(socket_1)); + } + + #[test] + fn test_below_threshold() { + let mut votes = IpVote::new(3, Duration::from_secs(10)); + let socket_1 = SocketAddr::new("127.0.0.1".parse().unwrap(), 1); + let socket_2 = SocketAddr::new("127.0.0.1".parse().unwrap(), 2); + let socket_3 = SocketAddr::new("127.0.0.1".parse().unwrap(), 3); + + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_1); + votes.insert(NodeId::random(), socket_2); + votes.insert(NodeId::random(), socket_3); + + assert_eq!(votes.majority(), None); + } +} diff --git a/version-meld/discv5/src/service/query_info.rs b/version-meld/discv5/src/service/query_info.rs new file mode 100644 index 0000000..980474e --- /dev/null +++ b/version-meld/discv5/src/service/query_info.rs @@ -0,0 +1,134 @@ +use crate::{kbucket::Key, rpc::RequestBody, Enr}; +use enr::NodeId; +use sha2::digest::generic_array::GenericArray; +use smallvec::SmallVec; +use tokio::sync::oneshot; + +/// Information about a query. +#[derive(Debug)] +pub struct QueryInfo { + /// What we are querying and why. + pub query_type: QueryType, + + /// Temporary ENRs used when trying to reach nodes. + pub untrusted_enrs: SmallVec<[Enr; 16]>, + + /// A callback channel for the service that requested the query. + pub callback: oneshot::Sender>, + + /// The number of distances we request for each peer. + pub distances_to_request: usize, +} + +/// Additional information about the query. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum QueryType { + /// The user requested a `FIND_NODE` query to be performed. It should be reported when finished. + FindNode(NodeId), +} + +impl QueryInfo { + /// Builds an RPC Request, given the QueryInfo + pub(crate) fn rpc_request(&self, peer: NodeId) -> Result { + let request = match self.query_type { + QueryType::FindNode(node_id) => { + let distances = findnode_log2distance(node_id, peer, self.distances_to_request) + .ok_or("Requested a node find itself")?; + RequestBody::FindNode { distances } + } + }; + + Ok(request) + } +} + +impl crate::query_pool::TargetKey for QueryInfo { + fn key(&self) -> Key { + match self.query_type { + QueryType::FindNode(ref node_id) => { + Key::new_raw(*node_id, *GenericArray::from_slice(&node_id.raw())) + } + } + } +} + +/// Calculates the log2 distance for a destination peer given a target and the size (number of +/// distances to request). +/// +/// As the iteration increases, FINDNODE requests adjacent distances from the exact peer distance. +/// +/// As an example, if the target has a distance of 12 from the remote peer, the sequence of distances that are sent for increasing iterations would be [12, 13, 11, 14, 10, .. ]. +fn findnode_log2distance(target: NodeId, peer: NodeId, size: usize) -> Option> { + if size > 127 { + // invoke and endless loop - coding error + panic!("Iterations cannot be greater than 127"); + } + + let dst_key: Key = peer.into(); + let distance = dst_key.log2_distance(&target.into())?; + + let mut result_list = vec![distance]; + let mut difference = 1; + while result_list.len() < size { + if distance + difference <= 256 { + result_list.push(distance + difference); + } + if result_list.len() < size { + if let Some(d) = distance.checked_sub(difference) { + result_list.push(d); + } + } + difference += 1; + } + Some(result_list[..size].to_vec()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_log2distance() { + let target = NodeId::new(&[0u8; 32]); + let mut destination = [0u8; 32]; + destination[10] = 1; // gives a log2 distance of 169 + let destination = NodeId::new(&destination); + + let expected_distances = vec![169, 170, 168, 171, 167, 172, 166, 173, 165]; + + assert_eq!( + findnode_log2distance(target, destination, expected_distances.len()).unwrap(), + expected_distances + ); + } + + #[test] + fn test_log2distance_lower() { + let target = NodeId::new(&[0u8; 32]); + let mut destination = [0u8; 32]; + destination[31] = 8; // gives a log2 distance of 5 + let destination = NodeId::new(&destination); + + let expected_distances = vec![4, 5, 3, 6, 2, 7, 1, 8, 0, 9, 10]; + + assert_eq!( + findnode_log2distance(target, destination, expected_distances.len()).unwrap(), + expected_distances + ); + } + + #[test] + fn test_log2distance_upper() { + let target = NodeId::new(&[0u8; 32]); + let mut destination = [0u8; 32]; + destination[0] = 8; // gives a log2 distance of 252 + let destination = NodeId::new(&destination); + + let expected_distances = vec![252, 253, 251, 254, 250, 255, 249, 256, 248, 247, 246]; + + assert_eq!( + findnode_log2distance(target, destination, expected_distances.len()).unwrap(), + expected_distances + ); + } +} diff --git a/version-meld/discv5/src/service/test.rs b/version-meld/discv5/src/service/test.rs new file mode 100644 index 0000000..29b21b1 --- /dev/null +++ b/version-meld/discv5/src/service/test.rs @@ -0,0 +1,173 @@ +#![cfg(test)] + +use super::*; + +use crate::{ + handler::Handler, + kbucket, + kbucket::{BucketInsertResult, KBucketsTable, NodeStatus}, + node_info::NodeContact, + query_pool::{QueryId, QueryPool}, + rpc, + rpc::RequestId, + service::{ActiveRequest, Service}, + Discv5ConfigBuilder, Enr, +}; +use enr::{CombinedKey, EnrBuilder}; +use parking_lot::RwLock; +use std::{ + collections::HashMap, + net::{IpAddr, SocketAddr}, + sync::Arc, + time::Duration, +}; +use tokio::sync::{mpsc, oneshot}; + +fn _connected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Connected, + direction: ConnectionDirection::Outgoing, + } +} + +fn disconnected_state() -> NodeStatus { + NodeStatus { + state: ConnectionState::Disconnected, + direction: ConnectionDirection::Outgoing, + } +} + +fn init() { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); +} + +async fn build_service( + local_enr: Arc>, + enr_key: Arc>, + listen_socket: SocketAddr, + filters: bool, +) -> Service { + let config = Discv5ConfigBuilder::new() + .executor(Box::new(crate::executor::TokioExecutor::default())) + .build(); + // build the session service + let (_handler_exit, handler_send, handler_recv) = Handler::spawn( + local_enr.clone(), + enr_key.clone(), + listen_socket, + config.clone(), + ) + .await + .unwrap(); + + let (table_filter, bucket_filter) = if filters { + ( + Some(Box::new(kbucket::IpTableFilter) as Box>), + Some(Box::new(kbucket::IpBucketFilter) as Box>), + ) + } else { + (None, None) + }; + + let kbuckets = Arc::new(RwLock::new(KBucketsTable::new( + local_enr.read().node_id().into(), + Duration::from_secs(60), + config.incoming_bucket_limit, + table_filter, + bucket_filter, + ))); + + // create the required channels + let (_discv5_send, discv5_recv) = mpsc::channel(30); + let (_exit_send, exit) = oneshot::channel(); + + Service { + local_enr, + enr_key, + kbuckets, + queries: QueryPool::new(config.query_timeout), + active_requests: Default::default(), + active_nodes_responses: HashMap::new(), + ip_votes: None, + handler_send, + handler_recv, + handler_exit: Some(_handler_exit), + peers_to_ping: HashSetDelay::new(config.ping_interval), + discv5_recv, + event_stream: None, + exit, + config, + } +} + +#[tokio::test] +async fn test_updating_connection_on_ping() { + init(); + let enr_key1 = CombinedKey::generate_secp256k1(); + let ip: IpAddr = "127.0.0.1".parse().unwrap(); + let enr = EnrBuilder::new("v4") + .ip(ip) + .udp(10001) + .build(&enr_key1) + .unwrap(); + let ip2: IpAddr = "127.0.0.1".parse().unwrap(); + let enr_key2 = CombinedKey::generate_secp256k1(); + let enr2 = EnrBuilder::new("v4") + .ip(ip2) + .udp(10002) + .build(&enr_key2) + .unwrap(); + + let socket_addr = enr.udp_socket().unwrap(); + + let mut service = build_service( + Arc::new(RwLock::new(enr)), + Arc::new(RwLock::new(enr_key1)), + socket_addr, + false, + ) + .await; + // Set up service with one disconnected node + let key = kbucket::Key::from(enr2.node_id()); + if let kbucket::Entry::Absent(entry) = service.kbuckets.write().entry(&key) { + match entry.insert(enr2.clone(), disconnected_state()) { + BucketInsertResult::Inserted => {} + BucketInsertResult::Full => { + panic!("Can't be full"); + } + BucketInsertResult::Pending { .. } => {} + _ => panic!("Could not be inserted"), + } + } + + // Add a fake request + let response = rpc::Response { + id: RequestId(vec![1]), + body: rpc::ResponseBody::Pong { + enr_seq: 2, + ip: ip2, + port: 10002, + }, + }; + + let node_contact = NodeContact::Enr(Box::new(enr2)); + let expected_return_addr = node_contact.node_address().unwrap(); + + service.active_requests.insert( + RequestId(vec![1]), + ActiveRequest { + contact: node_contact, + request_body: rpc::RequestBody::Ping { enr_seq: 2 }, + query_id: Some(QueryId(1)), + callback: None, + }, + ); + + // Handle the ping and expect the disconnected Node to become connected + service.handle_rpc_response(expected_return_addr, response); + let buckets = service.kbuckets.read(); + let node = buckets.iter_ref().next().unwrap(); + assert!(node.status.is_connected()) +} diff --git a/version-meld/discv5/src/socket/filter/cache.rs b/version-meld/discv5/src/socket/filter/cache.rs new file mode 100644 index 0000000..6f996f8 --- /dev/null +++ b/version-meld/discv5/src/socket/filter/cache.rs @@ -0,0 +1,113 @@ +//! This provides a cache to check rate limits as well as store data for metrics. +//! +//! The cache essentially consists of a time-ordered list of elements. The list is split into two +//! sections, within the enforced time and without. +//! +//! +//! | | Enforced Time | +//! \[x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x\] +//! +//! The enforced time represents one seconds worth of elements. The target aims to limit the +//! number of elements that can be inserted within the enforced time. The length of the list is +//! determined by the `time_window` this can be longer than one second and can be used by metrics +//! to average results over large values than one second. + +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; + +/// The time window that the size of the cache is enforced for. I.e if the size is 5 and +/// ENFORCED_SIZE_TIME is 1 second, this will allow 5 entries per second. This MUST be less than the +/// cache's `time_window`. +pub const ENFORCED_SIZE_TIME: u64 = 1; + +pub struct ReceivedPacket { + /// The source that sent us the packet. + pub content: T, + /// The time the packet was received. + pub received: Instant, +} + +pub struct ReceivedPacketCache { + /// The target number of entries per ENFORCED_SIZE_TIME before inserting new elements reports + /// failure. The maximum size of the cache is target*time_window + target: usize, + /// The cache stores `time_window` seconds worth of information to calculate a moving average. + /// This variable keeps track the number of elements in the cache within the + /// ENFORCED_SIZE_TIME. + time_window: u64, + /// This stores the current number of messages that are within the `ENFORCED_SIZE_TIME`. + within_enforced_time: usize, + /// The underlying data structure. + inner: VecDeque>, +} + +impl ReceivedPacketCache { + /// Creates a new `ReceivedPacketCache` with a specified size from which no more can enter. + pub fn new(target: usize, time_window: u64) -> Self { + Self { + target, + time_window, + within_enforced_time: 0, + inner: VecDeque::with_capacity(target * time_window as usize), + } + } + + /// Remove expired packets. We only keep, `CACHE_TIME` of data in the cache. + pub fn reset(&mut self) { + while let Some(packet) = self.inner.pop_front() { + if packet.received > Instant::now() - Duration::from_secs(self.time_window) { + // add the packet back and end + self.inner.push_front(packet); + break; + } + } + // update the within_enforced_time + let mut count = 0; + for packet in self.inner.iter().rev() { + if packet.received > Instant::now() - Duration::from_secs(ENFORCED_SIZE_TIME) { + count += 1; + } else { + break; + } + } + self.within_enforced_time = count; + } + + /// Inserts an element into the cache, removing any expired elements. + pub fn cache_insert(&mut self, content: T) -> bool { + self.reset(); + self.internal_insert(content) + } + + /// Inserts an element into the cache without removing expired elements. + fn internal_insert(&mut self, content: T) -> bool { + if self.within_enforced_time >= self.target { + // Reached the target + false + } else { + let received_packet = ReceivedPacket { + content, + received: Instant::now(), + }; + self.inner.push_back(received_packet); + self.within_enforced_time += 1; + true + } + } +} + +impl std::ops::Deref for ReceivedPacketCache { + type Target = VecDeque>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for ReceivedPacketCache { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/version-meld/discv5/src/socket/filter/config.rs b/version-meld/discv5/src/socket/filter/config.rs new file mode 100644 index 0000000..3dd755f --- /dev/null +++ b/version-meld/discv5/src/socket/filter/config.rs @@ -0,0 +1,18 @@ +use super::rate_limiter::RateLimiter; + +#[derive(Debug)] +pub struct FilterConfig { + /// Whether the packet filter is enabled or not. + pub enabled: bool, + /// Set up various rate limits for unsolicited packets. See the + /// [`crate::RateLimiterBuilder`] for + /// further details on constructing rate limits. See the [`Default`] implementation for default + /// values. + pub rate_limiter: Option, + /// The maximum number of node-ids allowed per IP address before the IP address gets banned. + /// Having this set to None, disables this feature. Default value is 10. + pub max_nodes_per_ip: Option, + /// The maximum number of nodes that can be banned by a single IP before that IP gets banned. + /// The default is 5. + pub max_bans_per_ip: Option, +} diff --git a/version-meld/discv5/src/socket/filter/mod.rs b/version-meld/discv5/src/socket/filter/mod.rs new file mode 100644 index 0000000..f755758 --- /dev/null +++ b/version-meld/discv5/src/socket/filter/mod.rs @@ -0,0 +1,223 @@ +//! A filter which decides whether to accept/reject incoming UDP packets. + +use crate::{discv5::PERMIT_BAN_LIST, metrics::METRICS, node_info::NodeAddress, packet::Packet}; +use cache::ReceivedPacketCache; +use enr::NodeId; +use lru::LruCache; +use std::{ + collections::HashSet, + net::{IpAddr, SocketAddr}, + sync::atomic::Ordering, + time::{Duration, Instant}, +}; +use tracing::{debug, warn}; + +mod cache; +mod config; +pub mod rate_limiter; +pub use config::FilterConfig; +use rate_limiter::{LimitKind, RateLimiter}; + +/// The maximum number of IPs to retain when calculating the number of nodes per IP. +const KNOWN_ADDRS_SIZE: usize = 500; +/// The number of IPs to retain at any given time that have banned nodes. +const BANNED_NODES_SIZE: usize = 50; +/// The maximum number of packets to keep record of for metrics if the rate limiter is not +/// specified. +const DEFAULT_PACKETS_PER_SECOND: usize = 20; + +/// The packet filter which decides whether we accept or reject incoming packets. +pub(crate) struct Filter { + /// Whether the filter is enabled or not. + enabled: bool, + /// An optional rate limiter for incoming packets. + rate_limiter: Option, + /// An ordered (by time) collection of recently seen packets by SocketAddr. The packet data is not + /// stored here. This stores 5 seconds of history to calculate a 5 second moving average for + /// the metrics. + raw_packets_received: ReceivedPacketCache, + /// The duration that bans by this filter last. + ban_duration: Option, + /// Keep track of node ids per socket. If someone is using too many node-ids per IP, they can + /// be banned. + known_addrs: LruCache>, + /// Keep track of Ips that have banned nodes. If a single IP has many nodes that get banned, + /// then we ban the IP address. + banned_nodes: LruCache, + /// The maximum number of node-ids allowed per IP address before the IP address gets banned. + /// Having this set to None, disables this feature. Default value is 10. + pub max_nodes_per_ip: Option, + /// The maximum number of nodes that can be banned by a single IP before that IP gets banned. + /// The default is 5. + pub max_bans_per_ip: Option, +} + +impl Filter { + pub fn new(config: FilterConfig, ban_duration: Option) -> Filter { + let expected_packets_per_second = config + .rate_limiter + .as_ref() + .map(|v| v.total_requests_per_second().round() as usize) + .unwrap_or(DEFAULT_PACKETS_PER_SECOND); + + Filter { + enabled: config.enabled, + rate_limiter: config.rate_limiter, + raw_packets_received: ReceivedPacketCache::new( + expected_packets_per_second, + METRICS.moving_window, + ), + known_addrs: LruCache::new(KNOWN_ADDRS_SIZE), + banned_nodes: LruCache::new(BANNED_NODES_SIZE), + ban_duration, + max_nodes_per_ip: config.max_nodes_per_ip, + max_bans_per_ip: config.max_bans_per_ip, + } + } + + /// The first check. This determines if a new UDP packet should be decoded or dropped. + /// Only unsolicited packets arrive here. + pub fn initial_pass(&mut self, src: &SocketAddr) -> bool { + if PERMIT_BAN_LIST.read().permit_ips.get(&src.ip()).is_some() { + return true; + } + + if PERMIT_BAN_LIST.read().ban_ips.get(&src.ip()).is_some() { + debug!("Dropped unsolicited packet from banned src: {:?}", src); + return false; + } + + // Add the un-solicited request to the cache + // If this is over the maximum requests per ENFORCED_SIZE_TIME, it will not be added, we + // leave the rate limiter to enforce the rate limits.. + let _ = self.raw_packets_received.cache_insert(*src); + + // build the metrics + METRICS + .unsolicited_requests_per_window + .store(self.raw_packets_received.len(), Ordering::Relaxed); + + // If the filter isn't enabled, pass the packet + if !self.enabled { + return true; + } + + // Check rate limits + if let Some(rate_limiter) = self.rate_limiter.as_mut() { + if rate_limiter.allows(&LimitKind::Ip(src.ip())).is_err() { + warn!("Banning IP for excessive requests: {:?}", src.ip()); + // Ban the IP address + let ban_timeout = self.ban_duration.map(|v| Instant::now() + v); + PERMIT_BAN_LIST + .write() + .ban_ips + .insert(src.ip(), ban_timeout); + return false; + } + + if rate_limiter.allows(&LimitKind::Total).is_err() { + debug!("Dropped unsolicited packet from RPC limit: {:?}", src.ip()); + return false; + } + } + true + } + + pub fn final_pass(&mut self, node_address: &NodeAddress, _packet: &Packet) -> bool { + if PERMIT_BAN_LIST + .read() + .permit_nodes + .get(&node_address.node_id) + .is_some() + { + return true; + } + + if PERMIT_BAN_LIST + .read() + .ban_nodes + .get(&node_address.node_id) + .is_some() + { + debug!( + "Dropped unsolicited packet from banned node_id: {}", + node_address + ); + return false; + } + + // If the filter isn't enabled, just pass the packet. + if !self.enabled { + return true; + } + + if let Some(rate_limiter) = self.rate_limiter.as_mut() { + if rate_limiter + .allows(&LimitKind::NodeId(node_address.node_id)) + .is_err() + { + warn!( + "Node has exceeded its request limit and is now banned {}", + node_address.node_id + ); + + // The node is being banned + let ban_timeout = self.ban_duration.map(|v| Instant::now() + v); + PERMIT_BAN_LIST + .write() + .ban_nodes + .insert(node_address.node_id, ban_timeout); + + // If we are tracking banned nodes per IP, add to the count. If the count is higher + // than our tolerance, ban the IP. + if let Some(max_bans_per_ip) = self.max_bans_per_ip { + let ip = node_address.socket_addr.ip(); + if let Some(banned_count) = self.banned_nodes.get_mut(&ip) { + *banned_count += 1; + if *banned_count >= max_bans_per_ip { + PERMIT_BAN_LIST.write().ban_ips.insert(ip, ban_timeout); + } + } else { + self.banned_nodes.put(ip, 0); + } + } + + return false; + } + } + + // Check the nodes per IP filter configuration + if let Some(max_nodes_per_ip) = self.max_nodes_per_ip { + // This option is set, store the known nodes per IP. + let ip = node_address.socket_addr.ip(); + let known_nodes = { + if let Some(known_nodes) = self.known_addrs.get_mut(&ip) { + known_nodes.insert(node_address.node_id); + known_nodes.len() + } else { + let mut ids = HashSet::new(); + ids.insert(node_address.node_id); + self.known_addrs.put(ip, ids); + 1 + } + }; + + if known_nodes >= max_nodes_per_ip { + warn!("IP has exceeded its node-id limit and is now banned {}", ip); + // The node is being banned + let ban_timeout = self.ban_duration.map(|v| Instant::now() + v); + PERMIT_BAN_LIST.write().ban_ips.insert(ip, ban_timeout); + self.known_addrs.pop(&ip); + return false; + } + } + + true + } + + pub fn prune_limiter(&mut self) { + if let Some(rate_limiter) = self.rate_limiter.as_mut() { + rate_limiter.prune(); + } + } +} diff --git a/version-meld/discv5/src/socket/filter/rate_limiter.rs b/version-meld/discv5/src/socket/filter/rate_limiter.rs new file mode 100644 index 0000000..0e8af57 --- /dev/null +++ b/version-meld/discv5/src/socket/filter/rate_limiter.rs @@ -0,0 +1,414 @@ +use enr::NodeId; +use fnv::FnvHashMap; +use std::{ + convert::TryInto, + hash::Hash, + net::IpAddr, + time::{Duration, Instant}, +}; + +/// Nanoseconds since a given time. +// Maintained as u64 to reduce footprint +// NOTE: this also implies that the rate limiter will manage checking if a batch is allowed for at +// most + u64::MAX nanosecs, ~500 years. So it is realistic to assume this is fine. +type Nanosecs = u64; + +/// User-friendly rate limiting parameters of the GCRA. +/// +/// A quota of `max_tokens` tokens every `replenish_all_every` units of time means that: +/// 1. One token is replenished every `replenish_all_every`/`max_tokens` units of time. +/// 2. Instantaneous bursts (batches) of up to `max_tokens` tokens are allowed. +/// +/// The above implies that if `max_tokens` is greater than 1, the perceived rate may be higher (but +/// bounded) than the defined rate when instantaneous bursts occur. For instance, for a rate of +/// 4T/2s a first burst of 4T is allowed with subsequent requests of 1T every 0.5s forever, +/// producing a perceived rate over the window of the first 2s of 8T. However, subsequent sliding +/// windows of 2s keep the limit. +/// +/// In this scenario using the same rate as above, the sender is always maxing out their tokens, +/// except at seconds 1.5, 3, 3.5 and 4 +/// +/// ```ignore +/// x +/// used x +/// tokens x x x +/// at a x x x x x x +/// given +--+--+--o--+--+--o--o--o--> seconds +/// time | | | | | | | | | +/// 0 1 2 3 4 +/// +/// 4 1 1 1 2 1 1 2 3 <= available tokens when the batch is received +/// ``` +/// +/// For a sender to request a batch of `n`T, they would need to wait at least +/// n*`replenish_all_every`/`max_tokens` units of time since their last request. +/// +/// To produce hard limits, set `max_tokens` to 1. +#[derive(Clone)] +pub struct Quota { + /// How often are `max_tokens` fully replenished. + replenish_all_every: Duration, + /// Token limit. This translates on how large can an instantaneous batch of + /// tokens be. + max_tokens: u64, +} + +/// Manages rate limiting of requests per peer, with differentiated rates per protocol. +#[derive(Debug, Clone)] +pub struct RateLimiter { + /// An estimate of the maximum requests per second. This is only used for estimating the size + /// of the cache for measuring metrics + total_requests_per_second: f32, + /// Creation time of the rate limiter. + init_time: Instant, + /// Total rate limit. Must be set. + total_rl: Limiter<()>, + /// Rate limit for each node + node_rl: Option>, + /// Rate limit for each ip. + ip_rl: Option>, +} + +/// Error type for non conformant requests +pub enum RateLimitedErr { + /// Required tokens for this request exceed the maximum + TooLarge, + /// Request does not fit in the quota. Gives the earliest time the request could be accepted. + TooSoon(Duration), +} + +pub enum LimitKind { + /// Request counts towards the total limit. + Total, + /// Request counts towards the NodeId limit. + NodeId(NodeId), + /// Request counts toward the ip limit. + Ip(IpAddr), +} + +/// User-friendly builder of a `RateLimiter`. The user can specify three kinds of rate limits but +/// must at least set the total quota. The three types are: +/// 1. Total Quota - Specifies the total number of inbound requests. This must be set. +/// 2. Node Quota - Specifies the number of requests per node id. +/// 3. IP Quota - Specifies the number of requests per IP. +/// +/// Quotas can be set via the X_one_every() functions to set hard limits as described above. Using +/// the `X_n_every()` functions allow for bursts. +#[derive(Default)] +pub struct RateLimiterBuilder { + /// Quota for total received RPCs. + total_quota: Option, + /// Quota for each node-id. + node_quota: Option, + /// Quota for each IP. + ip_quota: Option, +} + +#[allow(dead_code)] +impl RateLimiterBuilder { + /// Get an empty `RateLimiterBuilder`. + pub fn new() -> Self { + Default::default() + } + + /// Set the total quota. + fn total_quota(mut self, quota: Quota) -> Self { + self.total_quota = Some(quota); + self + } + + /// Set the node quota. + fn node_quota(mut self, quota: Quota) -> Self { + self.node_quota = Some(quota); + self + } + + /// Set the IP quota. + fn ip_quota(mut self, quota: Quota) -> Self { + self.ip_quota = Some(quota); + self + } + + /// Allow one token every `time_period` to be used for the total RPC limit. + /// This produces a hard limit. + pub fn total_one_every(self, time_period: Duration) -> Self { + self.total_quota(Quota { + replenish_all_every: time_period, + max_tokens: 1, + }) + } + + /// Allow one token every `time_period` to be used for the node RPC limit. + /// This produces a hard limit. + pub fn node_one_every(self, time_period: Duration) -> Self { + self.node_quota(Quota { + replenish_all_every: time_period, + max_tokens: 1, + }) + } + + /// Allow one token every `time_period` to be used for the total RPC limit per IP. + /// This produces a hard limit. + pub fn ip_one_every(self, time_period: Duration) -> Self { + self.ip_quota(Quota { + replenish_all_every: time_period, + max_tokens: 1, + }) + } + + /// Allow `n` tokens to be use used every `time_period` for the total. + pub fn total_n_every(self, n: u64, time_period: Duration) -> Self { + self.total_quota(Quota { + replenish_all_every: time_period, + max_tokens: n, + }) + } + + /// Allow `n` tokens to be use used every `time_period` for the total. + pub fn node_n_every(self, n: u64, time_period: Duration) -> Self { + self.node_quota(Quota { + replenish_all_every: time_period, + max_tokens: n, + }) + } + + /// Allow `n` tokens to be use used every `time_period` for the total. + pub fn ip_n_every(self, n: u64, time_period: Duration) -> Self { + self.ip_quota(Quota { + replenish_all_every: time_period, + max_tokens: n, + }) + } + + pub fn build(self) -> Result { + // get our quotas + let total_quota = self + .total_quota + .ok_or("Total quota not specified and must be set.")?; + + // create the rate limiters + let total_rl = Limiter::from_quota(total_quota.clone())?; + let node_rl = match self.node_quota { + Some(q) => Some(Limiter::from_quota(q)?), + None => None, + }; + let ip_rl = match self.ip_quota { + Some(q) => Some(Limiter::from_quota(q)?), + None => None, + }; + + let total_requests_per_second = if total_quota.max_tokens == 1 { + (1.0 / total_quota.replenish_all_every.as_secs_f32() + / Duration::from_secs(1).as_secs_f32()) + .round() + } else { + (2.0 * total_quota.max_tokens as f32 // multiply by 2 to account for potential bursts + / total_quota.replenish_all_every.as_secs_f32() + / Duration::from_secs(1).as_secs_f32()) + .round() + }; + + Ok(RateLimiter { + total_requests_per_second, + total_rl, + node_rl, + ip_rl, + init_time: Instant::now(), + }) + } +} + +impl RateLimiter { + /// Indicates whether the request is allowed based on the configured rate limits. + pub fn allows(&mut self, request: &LimitKind) -> Result<(), RateLimitedErr> { + let time_since_start = self.init_time.elapsed(); + let tokens = 1; // Only count each of these as one. + + // Check the limits + match request { + LimitKind::Total => self.total_rl.allows(time_since_start, &(), tokens), + LimitKind::Ip(ip_addr) => { + if let Some(limiter) = self.ip_rl.as_mut() { + limiter.allows(time_since_start, ip_addr, tokens) + } else { + Ok(()) + } + } + LimitKind::NodeId(node_id) => { + if let Some(limiter) = self.node_rl.as_mut() { + limiter.allows(time_since_start, node_id, tokens) + } else { + Ok(()) + } + } + } + } + + /// Returns the expected total requests per second. + pub fn total_requests_per_second(&self) -> f32 { + self.total_requests_per_second + } + + /// Prunes excess entries. Should be called regularly (30 seconds) to remove old entries. + pub fn prune(&mut self) { + let time_since_start = self.init_time.elapsed(); + self.total_rl.prune(time_since_start); + if let Some(v) = self.ip_rl.as_mut() { + v.prune(time_since_start) + }; + if let Some(v) = self.node_rl.as_mut() { + v.prune(time_since_start) + }; + } +} + +/// Per key rate limiter using the token bucket / leaky bucket as a meter rate limiting algorithm, +/// with the GCRA implementation. +#[derive(Debug, Clone)] +pub struct Limiter { + /// After how long is the bucket considered full via replenishing 1T every `t`. + tau: Nanosecs, + /// How often is 1T replenished. + t: Nanosecs, + /// Time when the bucket will be full for each peer. TAT (theoretical arrival time) from GCRA. + tat_per_key: FnvHashMap, +} + +impl Limiter { + pub fn from_quota(quota: Quota) -> Result { + if quota.max_tokens == 0 { + return Err("Max number of tokens should be positive"); + } + let tau = quota.replenish_all_every.as_nanos(); + if tau == 0 { + return Err("Replenish time must be positive"); + } + let t = (tau / quota.max_tokens as u128) + .try_into() + .map_err(|_| "total replenish time is too long")?; + let tau = tau + .try_into() + .map_err(|_| "total replenish time is too long")?; + Ok(Limiter { + tau, + t, + tat_per_key: FnvHashMap::default(), + }) + } + + pub fn allows( + &mut self, + time_since_start: Duration, + key: &Key, + tokens: u64, + ) -> Result<(), RateLimitedErr> { + let time_since_start = time_since_start.as_nanos() as u64; + let tau = self.tau; + let t = self.t; + // how long does it take to replenish these tokens + let additional_time = t * tokens; + if additional_time > tau { + // the time required to process this amount of tokens is longer than the time that + // makes the bucket full. So, this batch can _never_ be processed + return Err(RateLimitedErr::TooLarge); + } + // If the key is new, we consider their bucket full (which means, their request will be + // allowed) + let tat = self + .tat_per_key + .entry(key.clone()) + .or_insert(time_since_start); + // check how soon could the request be made + let earliest_time = (*tat + additional_time).saturating_sub(tau); + // earliest_time is in the future + if time_since_start < earliest_time { + Err(RateLimitedErr::TooSoon(Duration::from_nanos( + /* time they need to wait, i.e. how soon were they */ + earliest_time - time_since_start, + ))) + } else { + // calculate the new TAT + *tat = time_since_start.max(*tat) + additional_time; + Ok(()) + } + } + + /// Removes keys for which their bucket is full by `time_limit` + pub fn prune(&mut self, time_limit: Duration) { + let lim = &mut (time_limit.as_nanos() as u64); + // remove those for which tat < lim + self.tat_per_key.retain(|_k, tat| tat >= lim) + } +} + +#[cfg(test)] +mod tests { + use super::{Limiter, Quota}; + use std::time::Duration; + + #[test] + fn it_works_a() { + let mut limiter = Limiter::from_quota(Quota { + replenish_all_every: Duration::from_secs(2), + max_tokens: 4, + }) + .unwrap(); + let key = 10; + // x + // used x + // tokens x x + // x x x x + // +--+--+--+--+----> seconds + // | | | | | + // 0 1 2 + + assert!(limiter + .allows(Duration::from_secs_f32(0.0), &key, 4) + .is_ok()); + limiter.prune(Duration::from_secs_f32(0.1)); + assert!(limiter + .allows(Duration::from_secs_f32(0.1), &key, 1) + .is_err()); + assert!(limiter + .allows(Duration::from_secs_f32(0.5), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(1.0), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(1.4), &key, 1) + .is_err()); + assert!(limiter + .allows(Duration::from_secs_f32(2.0), &key, 2) + .is_ok()); + } + + #[test] + fn it_works_b() { + let mut limiter = Limiter::from_quota(Quota { + replenish_all_every: Duration::from_secs(2), + max_tokens: 4, + }) + .unwrap(); + let key = 10; + // if we limit to 4T per 2s, check that 4 requests worth 1 token can be sent before the + // first half second, when one token will be available again. Check also that before + // regaining a token, another request is rejected + + assert!(limiter + .allows(Duration::from_secs_f32(0.0), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.1), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.2), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.3), &key, 1) + .is_ok()); + assert!(limiter + .allows(Duration::from_secs_f32(0.4), &key, 1) + .is_err()); + } +} diff --git a/version-meld/discv5/src/socket/mod.rs b/version-meld/discv5/src/socket/mod.rs new file mode 100644 index 0000000..307e761 --- /dev/null +++ b/version-meld/discv5/src/socket/mod.rs @@ -0,0 +1,95 @@ +use crate::Executor; +use parking_lot::RwLock; +use recv::*; +use send::*; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; +use tokio::sync::{mpsc, oneshot}; + +mod filter; +mod recv; +mod send; + +pub use filter::{ + rate_limiter::{RateLimiter, RateLimiterBuilder}, + FilterConfig, +}; +pub use recv::InboundPacket; +pub use send::OutboundPacket; +/// Convenience objects for setting up the recv handler. +pub struct SocketConfig { + /// The executor to spawn the tasks. + pub executor: Box, + /// The listening socket. + pub socket_addr: SocketAddr, + /// Configuration details for the packet filter. + pub filter_config: FilterConfig, + /// If the filter is enabled this sets the default timeout for bans enacted by the filter. + pub ban_duration: Option, + /// The expected responses reference. + pub expected_responses: Arc>>, + /// The local node id used to decrypt messages. + pub local_node_id: enr::NodeId, +} + +/// Creates the UDP socket and handles the exit futures for the send/recv UDP handlers. +pub struct Socket { + pub send: mpsc::Sender, + pub recv: mpsc::Receiver, + sender_exit: Option>, + recv_exit: Option>, +} + +impl Socket { + /// This creates and binds a new UDP socket. + // In general this function can be expanded to handle more advanced socket creation. + pub(crate) async fn new_socket( + socket: &SocketAddr, + ) -> Result { + tokio::net::UdpSocket::bind(socket).await + } + + /// Creates a UDP socket, spawns a send/recv task and returns the channels. + /// If this struct is dropped, the send/recv tasks will shutdown. + /// This needs to be run inside of a tokio executor. + pub(crate) fn new( + socket: tokio::net::UdpSocket, + config: SocketConfig, + ) -> Result { + // Arc the udp socket for the send/recv tasks. + let recv_udp = Arc::new(socket); + let send_udp = recv_udp.clone(); + + // spawn the recv handler + let recv_config = RecvHandlerConfig { + filter_config: config.filter_config, + executor: config.executor.clone(), + recv: recv_udp, + local_node_id: config.local_node_id, + expected_responses: config.expected_responses, + ban_duration: config.ban_duration, + }; + + let (recv, recv_exit) = RecvHandler::spawn(recv_config); + // spawn the sender handler + let (send, sender_exit) = SendHandler::spawn(config.executor.clone(), send_udp); + + Ok(Socket { + send, + recv, + sender_exit: Some(sender_exit), + recv_exit: Some(recv_exit), + }) + } +} + +impl std::ops::Drop for Socket { + // close the send/recv handlers + fn drop(&mut self) { + let _ = self + .sender_exit + .take() + .expect("Exit always exists") + .send(()); + let _ = self.recv_exit.take().expect("Exit always exists").send(()); + } +} diff --git a/version-meld/discv5/src/socket/recv.rs b/version-meld/discv5/src/socket/recv.rs new file mode 100644 index 0000000..b1ba24a --- /dev/null +++ b/version-meld/discv5/src/socket/recv.rs @@ -0,0 +1,160 @@ +//! This is a standalone task that handles UDP packets as they are received. +//! +//! Every UDP packet passes a filter before being processed. + +use super::filter::{Filter, FilterConfig}; +use crate::{metrics::METRICS, node_info::NodeAddress, packet::*, Executor}; +use parking_lot::RwLock; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; +use tokio::{ + net::UdpSocket, + sync::{mpsc, oneshot}, +}; + +use tracing::{debug, trace, warn}; + +/// The object sent back by the Recv handler. +pub struct InboundPacket { + /// The originating socket addr. + pub src_address: SocketAddr, + /// The packet header. + pub header: PacketHeader, + /// The message of the packet. + pub message: Vec, + /// The authenticated data of the packet. + pub authenticated_data: Vec, +} + +/// Convenience objects for setting up the recv handler. +pub struct RecvHandlerConfig { + pub filter_config: FilterConfig, + /// If the filter is enabled this sets the default timeout for bans enacted by the filter. + pub ban_duration: Option, + pub executor: Box, + pub recv: Arc, + pub local_node_id: enr::NodeId, + pub expected_responses: Arc>>, +} + +/// The main task that handles inbound UDP packets. +pub(crate) struct RecvHandler { + /// The UDP recv socket. + recv: Arc, + /// The list of waiting responses. These are used to allow incoming packets from sources + /// that we are expected a response from bypassing the rate-limit filters. + expected_responses: Arc>>, + /// The packet filter which decides whether to accept or reject inbound packets. + filter: Filter, + /// The buffer to accept inbound datagrams. + recv_buffer: [u8; MAX_PACKET_SIZE], + /// The local node id used to decrypt headers of messages. + node_id: enr::NodeId, + /// The channel to send the packet handler. + handler: mpsc::Sender, + /// Exit channel to shutdown the recv handler. + exit: oneshot::Receiver<()>, +} + +impl RecvHandler { + /// Spawns the `RecvHandler` on a provided executor. + pub(crate) fn spawn( + config: RecvHandlerConfig, + ) -> (mpsc::Receiver, oneshot::Sender<()>) { + let (exit_sender, exit) = oneshot::channel(); + + let filter_enabled = config.filter_config.enabled; + + // create the channel to send decoded packets to the handler + let (handler, handler_recv) = mpsc::channel(30); + + let mut recv_handler = RecvHandler { + recv: config.recv, + filter: Filter::new(config.filter_config, config.ban_duration), + recv_buffer: [0; MAX_PACKET_SIZE], + node_id: config.local_node_id, + expected_responses: config.expected_responses, + handler, + exit, + }; + + // start the handler + config.executor.spawn(Box::pin(async move { + debug!("Recv handler starting"); + recv_handler.start(filter_enabled).await; + })); + (handler_recv, exit_sender) + } + + /// The main future driving the recv handler. This will shutdown when the exit future is fired. + async fn start(&mut self, filter_enabled: bool) { + // Interval to prune to rate limiter. + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30)); + + loop { + tokio::select! { + Ok((length, src)) = self.recv.recv_from(&mut self.recv_buffer) => { + METRICS.add_recv_bytes(length); + self.handle_inbound(src, length).await; + } + _ = interval.tick(), if filter_enabled => { + self.filter.prune_limiter(); + }, + _ = &mut self.exit => { + debug!("Recv handler shutdown"); + return; + } + } + } + } + + /// Handles in incoming packet. Passes through the filter, decodes and sends to the packet + /// handler. + async fn handle_inbound(&mut self, src_address: SocketAddr, length: usize) { + // Permit all expected responses + let permitted = self.expected_responses.read().get(&src_address).is_some(); + + // Perform the first run of the filter. This checks for rate limits and black listed IP + // addresses. + if !permitted && !self.filter.initial_pass(&src_address) { + trace!("Packet filtered from source: {:?}", src_address); + return; + } + // Decodes the packet + let (packet, authenticated_data) = + match Packet::decode(&self.node_id, &self.recv_buffer[..length]) { + Ok(p) => p, + Err(e) => { + debug!("Packet decoding failed: {:?}", e); // could not decode the packet, drop it + return; + } + }; + + // If this is not a challenge packet, we immediately know its src_id and so pass it + // through the second filter. + if let Some(node_id) = packet.src_id() { + // Construct the node address + let node_address = NodeAddress { + socket_addr: src_address, + node_id, + }; + + // Perform packet-level filtering + if !permitted && !self.filter.final_pass(&node_address, &packet) { + return; + } + } + + let inbound = InboundPacket { + src_address, + header: packet.header, + message: packet.message, + authenticated_data, + }; + + // send the filtered decoded packet to the handler. + self.handler + .send(inbound) + .await + .unwrap_or_else(|e| warn!("Could not send packet to handler: {}", e)); + } +} diff --git a/version-meld/discv5/src/socket/send.rs b/version-meld/discv5/src/socket/send.rs new file mode 100644 index 0000000..ce43729 --- /dev/null +++ b/version-meld/discv5/src/socket/send.rs @@ -0,0 +1,74 @@ +//! This is a standalone task that encodes and sends Discv5 UDP packets +use crate::{metrics::METRICS, node_info::NodeAddress, packet::*, Executor}; +use std::sync::Arc; +use tokio::{ + net::UdpSocket, + sync::{mpsc, oneshot}, +}; +use tracing::{debug, trace, warn}; + +pub struct OutboundPacket { + /// The destination node address + pub node_address: NodeAddress, + /// The packet to be encoded. + pub packet: Packet, +} + +/// The main task that handles outbound UDP packets. +pub(crate) struct SendHandler { + /// The UDP send socket. + send: Arc, + /// The channel to respond to send requests. + handler_recv: mpsc::Receiver, + /// Exit channel to shutdown the handler. + exit: oneshot::Receiver<()>, +} + +impl SendHandler { + /// Spawns the `SendHandler` on a provided executor. + /// This returns the sending channel to process `OutboundPacket`'s and an exit channel to + /// shutdown the handler. + pub(crate) fn spawn( + executor: Box, + send: Arc, + ) -> (mpsc::Sender, oneshot::Sender<()>) { + let (exit_send, exit) = oneshot::channel(); + let (handler_send, handler_recv) = mpsc::channel(30); + + let mut send_handler = SendHandler { + send, + handler_recv, + exit, + }; + + // start the handler + executor.spawn(Box::pin(async move { + debug!("Send handler starting"); + send_handler.start().await; + })); + (handler_send, exit_send) + } + + /// The main future driving the send handler. This will shutdown when the exit future is fired. + async fn start(&mut self) { + loop { + tokio::select! { + Some(packet) = self.handler_recv.recv() => { + let encoded_packet = packet.packet.encode(&packet.node_address.node_id); + if encoded_packet.len() > MAX_PACKET_SIZE { + warn!("Sending packet larger than max size: {} max: {}", encoded_packet.len(), MAX_PACKET_SIZE); + } + if let Err(e) = self.send.send_to(&encoded_packet, &packet.node_address.socket_addr).await { + trace!("Could not send packet. Error: {:?}", e); + } else { + METRICS.add_sent_bytes(encoded_packet.len()); + } + } + _ = &mut self.exit => { + debug!("Send handler shutdown"); + return; + } + } + } + } +} diff --git a/version-meld/enr/.cargo-ok b/version-meld/enr/.cargo-ok new file mode 100644 index 0000000..b5754e2 --- /dev/null +++ b/version-meld/enr/.cargo-ok @@ -0,0 +1 @@ +ok \ No newline at end of file diff --git a/version-meld/enr/.cargo_vcs_info.json b/version-meld/enr/.cargo_vcs_info.json new file mode 100644 index 0000000..dd50e4a --- /dev/null +++ b/version-meld/enr/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "b09b66ee006e6d16cd0371f9e5ea3a4f14bda8db" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/version-meld/enr/Cargo.toml b/version-meld/enr/Cargo.toml new file mode 100644 index 0000000..ae16b1f --- /dev/null +++ b/version-meld/enr/Cargo.toml @@ -0,0 +1,101 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "enr" +version = "0.6.2" +authors = ["Age Manning "] +exclude = [ + ".gitignore", + ".github/*", +] +description = "Rust implementation of Ethereum Node Record (ENR) EIP778" +readme = "./README.md" +keywords = [ + "ethereum", + "enr", + "record", + "EIP778", + "node", +] +categories = ["cryptography::cryptocurrencies"] +license = "MIT" +repository = "https://github.com/sigp/enr" + +[package.metadata.docs.rs] +all-features = true + +[lib] +name = "enr" +path = "src/lib.rs" + +[dependencies.base64] +version = "0.13" + +[dependencies.bs58] +version = "0.4" + +[dependencies.bytes] +version = "1" + +[dependencies.ed25519-dalek] +version = "1.0.0-pre.4" +optional = true + +[dependencies.hex] +version = "0.4.2" + +[dependencies.k256] +version = "0.11" +features = ["ecdsa"] +optional = true + +[dependencies.log] +version = "0.4.8" + +[dependencies.rand] +version = "0.8" + +[dependencies.rlp] +version = "0.5" + +[dependencies.secp256k1] +version = "0.24" +features = ["global-context"] +optional = true +default-features = false + +[dependencies.serde] +version = "1.0.110" +optional = true + +[dependencies.sha3] +version = "0.10" + +[dependencies.zeroize] +version = "1.1.0" + +[dev-dependencies.rand_07] +version = "0.7" +package = "rand" + +[dev-dependencies.secp256k1] +version = "0.24" +features = ["rand-std"] + +[features] +default = [ + "serde", + "k256", +] +ed25519 = ["ed25519-dalek"] +rust-secp256k1 = ["secp256k1"] diff --git a/version-meld/enr/Cargo.toml.orig b/version-meld/enr/Cargo.toml.orig new file mode 100644 index 0000000..3870448 --- /dev/null +++ b/version-meld/enr/Cargo.toml.orig @@ -0,0 +1,45 @@ +[package] +name = "enr" +authors = ["Age Manning "] +edition = "2018" +version = "0.6.2" +description = "Rust implementation of Ethereum Node Record (ENR) EIP778" +readme = "./README.md" +keywords = ["ethereum", "enr", "record", "EIP778", "node"] +repository = "https://github.com/sigp/enr" +categories = ["cryptography::cryptocurrencies"] +license = "MIT" +exclude = [".gitignore", ".github/*"] + +[dependencies] +base64 = "0.13" +bs58 = "0.4" +bytes = "1" +hex = "0.4.2" +log = "0.4.8" +rand = "0.8" +rlp = "0.5" +zeroize = "1.1.0" +sha3 = "0.10" +k256 = { version = "0.11", features = ["ecdsa"], optional = true } +serde = { version = "1.0.110", optional = true } +ed25519-dalek = { version = "1.0.0-pre.4", optional = true } +secp256k1 = { version = "0.24", optional = true, default-features = false, features = [ + "global-context", +] } + +[dev-dependencies] +rand_07 = { package = "rand", version = "0.7" } +secp256k1 = { features = ["rand-std"], version = "0.24" } + +[features] +default = ["serde", "k256"] +ed25519 = ["ed25519-dalek"] +rust-secp256k1 = ["secp256k1"] + +[lib] +name = "enr" +path = "src/lib.rs" + +[package.metadata.docs.rs] +all-features = true diff --git a/version-meld/enr/LICENSE b/version-meld/enr/LICENSE new file mode 100644 index 0000000..bd1c2c2 --- /dev/null +++ b/version-meld/enr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Age Manning + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/version-meld/enr/README.md b/version-meld/enr/README.md new file mode 100644 index 0000000..efc017a --- /dev/null +++ b/version-meld/enr/README.md @@ -0,0 +1,170 @@ +enr +============ + +[![Build Status]][Build Link] [![Doc Status]][Doc Link] [![Crates +Status]][Crates Link] + +[Build Status]: https://github.com/AgeManning/enr/workflows/build/badge.svg?branch=master +[Build Link]: https://github.com/AgeManning/enr/actions +[Doc Status]: https://docs.rs/enr/badge.svg +[Doc Link]: https://docs.rs/enr +[Crates Status]: https://img.shields.io/crates/v/enr.svg +[Crates Link]: https://crates.io/crates/enr + +[Documentation at docs.rs](https://docs.rs/enr) + +This crate contains an implementation of an Ethereum Node Record (ENR) as specified by +[EIP-778](https://eips.ethereum.org/EIPS/eip-778) extended to allow for the use of ed25519 keys. + +An ENR is a signed, key-value record which has an associated `NodeId` (a 32-byte identifier). +Updating/modifying an ENR requires an `EnrKey` in order to re-sign the record with the +associated key-pair. + +ENR's are identified by their sequence number. When updating an ENR, the sequence number is +increased. + +Different identity schemes can be used to define the node id and signatures. Currently only the +"v4" identity is supported and is set by default. + +## Signing Algorithms + +User's wishing to implement their own singing algorithms simply need to +implement the `EnrKey` trait and apply it to an `Enr`. + +By default, `k256::SigningKey` implement `EnrKey` and can be used to sign and +verify ENR records. This library also implements `EnrKey` for `ed25519_dalek::Keypair` via the `ed25519` +feature flag. + +Furthermore, a `CombinedKey` is provided if the `ed25519` feature flag is set, which provides an +ENR type that can support both `secp256k1` and `ed25519` signed ENR records. Examples of the +use of each of these key types is given below. + +## Features + +This crate supports a number of features. + +- `serde`: Allows for serde serialization and deserialization for ENRs. +- `ed25519`: Provides support for `ed25519_dalek` keypair types. +- `rust-secp256k1`: Uses `c-secp256k1` for secp256k1 keys. + +These can be enabled via adding the feature flag in your `Cargo.toml` + +```toml +enr = { version = "*", features = ["serde", "ed25519", "rust-secp256k1"] } +``` + +## Examples + +To build an ENR, an `EnrBuilder` is provided. + +#### Building an ENR with the default `k256` key type + +```rust +use enr::{EnrBuilder, k256}; +use std::net::Ipv4Addr; +use rand::thread_rng; + +// generate a random secp256k1 key +let mut rng = thread_rng(); +let key = k256::ecdsa::SigningKey::random(&mut rng); + +let ip = Ipv4Addr::new(192,168,0,1); +let enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + +assert_eq!(enr.ip4(), Some("192.168.0.1".parse().unwrap())); +assert_eq!(enr.id(), Some("v4".into())); +``` + +#### Building an ENR with the `CombinedKey` type (support for multiple signing algorithms). + +Note the `ed25519` feature flag must be set. This makes use of the +`EnrBuilder` struct. + +```rust +use enr::{EnrBuilder, CombinedKey}; +use std::net::Ipv4Addr; + +// create a new secp256k1 key +let key = CombinedKey::generate_secp256k1(); + +// or create a new ed25519 key +let key = CombinedKey::generate_ed25519(); + +let ip = Ipv4Addr::new(192,168,0,1); +let enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + +assert_eq!(enr.ip4(), Some("192.168.0.1".parse().unwrap())); +assert_eq!(enr.id(), Some("v4".into())); +``` + +#### Modifying an ENR + +ENR fields can be added and modified using the getters/setters on `Enr`. A custom field +can be added using `insert` and retrieved with `get`. + +```rust +use enr::{EnrBuilder, k256::ecdsa::SigningKey, Enr}; +use std::net::Ipv4Addr; +use rand::thread_rng; + +// specify the type of ENR +type DefaultEnr = Enr; + +// generate a random secp256k1 key +let mut rng = thread_rng(); +let key = SigningKey::random(&mut rng); + +let ip = Ipv4Addr::new(192,168,0,1); +let mut enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + +enr.set_tcp4(8001, &key); +// set a custom key +enr.insert("custom_key", &vec![0,0,1], &key); + +// encode to base64 +let base_64_string = enr.to_base64(); + +// decode from base64 +let decoded_enr: DefaultEnr = base_64_string.parse().unwrap(); + +assert_eq!(decoded_enr.ip4(), Some("192.168.0.1".parse().unwrap())); +assert_eq!(decoded_enr.id(), Some("v4".into())); +assert_eq!(decoded_enr.tcp4(), Some(8001)); +assert_eq!(decoded_enr.get("custom_key"), Some(vec![0,0,1].as_slice())); +``` + +#### Encoding/Decoding ENR's of various key types + +```rust +use enr::{EnrBuilder, k256::ecdsa::SigningKey, Enr, ed25519_dalek::Keypair, CombinedKey}; +use std::net::Ipv4Addr; +use rand::thread_rng; +use rand::Rng; + +// generate a random secp256k1 key +let mut rng = thread_rng(); +let key = SigningKey::random(&mut rng); +let ip = Ipv4Addr::new(192,168,0,1); +let enr_secp256k1 = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + +// encode to base64 +let base64_string_secp256k1 = enr_secp256k1.to_base64(); + +// generate a random ed25519 key +let mut rng = rand_07::thread_rng(); +let key = Keypair::generate(&mut rng); +let enr_ed25519 = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + +// encode to base64 +let base64_string_ed25519 = enr_ed25519.to_base64(); + +// decode base64 strings of varying key types +// decode the secp256k1 with default Enr +let decoded_enr_secp256k1: Enr = base64_string_secp256k1.parse().unwrap(); +// decode ed25519 ENRs +let decoded_enr_ed25519: Enr = base64_string_ed25519.parse().unwrap(); + +// use the combined key to be able to decode either +let decoded_enr: Enr = base64_string_secp256k1.parse().unwrap(); +let decoded_enr: Enr = base64_string_ed25519.parse().unwrap(); +``` diff --git a/version-meld/enr/src/builder.rs b/version-meld/enr/src/builder.rs new file mode 100644 index 0000000..aee3d61 --- /dev/null +++ b/version-meld/enr/src/builder.rs @@ -0,0 +1,187 @@ +use crate::{Enr, EnrError, EnrKey, EnrPublicKey, Key, NodeId, MAX_ENR_SIZE}; +use bytes::{Bytes, BytesMut}; +use rlp::RlpStream; +use std::{ + collections::BTreeMap, + marker::PhantomData, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, +}; + +///! The base builder for generating ENR records with arbitrary signing algorithms. +pub struct EnrBuilder { + /// The identity scheme used to build the ENR record. + id: String, + + /// The starting sequence number for the ENR record. + seq: u64, + + /// The key-value pairs for the ENR record. + /// Values are stored as RLP encoded bytes. + content: BTreeMap, + + /// Pins the generic key types. + phantom: PhantomData, +} + +impl EnrBuilder { + /// Constructs a minimal `EnrBuilder` providing only a sequence number. + /// Currently only supports the id v4 scheme and therefore disallows creation of any other + /// scheme. + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + seq: 1, + content: BTreeMap::new(), + phantom: PhantomData, + } + } + + /// Modifies the sequence number of the builder. + pub fn seq(&mut self, seq: u64) -> &mut Self { + self.seq = seq; + self + } + + /// Adds an arbitrary key-value to the `ENRBuilder`. + pub fn add_value(&mut self, key: impl AsRef<[u8]>, value: &[u8]) -> &mut Self { + self.add_value_rlp(key, rlp::encode(&value).freeze()) + } + + /// Adds an arbitrary key-value where the value is raw RLP encoded bytes. + pub fn add_value_rlp(&mut self, key: impl AsRef<[u8]>, rlp: Bytes) -> &mut Self { + self.content.insert(key.as_ref().to_vec(), rlp); + self + } + + /// Adds an `ip`/`ip6` field to the `ENRBuilder`. + pub fn ip(&mut self, ip: IpAddr) -> &mut Self { + match ip { + IpAddr::V4(addr) => self.ip4(addr), + IpAddr::V6(addr) => self.ip6(addr), + } + } + + /// Adds an `ip` field to the `ENRBuilder`. + pub fn ip4(&mut self, ip: Ipv4Addr) -> &mut Self { + self.add_value("ip", &ip.octets()); + self + } + + /// Adds an `ip6` field to the `ENRBuilder`. + pub fn ip6(&mut self, ip: Ipv6Addr) -> &mut Self { + self.add_value("ip6", &ip.octets()); + self + } + + /* + * Removed from the builder as only the v4 scheme is currently supported. + * This is set as default in the builder. + + /// Adds an `Id` field to the `ENRBuilder`. + pub fn id(&mut self, id: &str) -> &mut Self { + self.add_value("id", &id.as_bytes()); + self + } + */ + + pub fn tcp(&mut self, tcp: u16) -> &mut Self { + self.tcp4(tcp) + } + + /// Adds a `tcp` field to the `ENRBuilder`. + pub fn tcp4(&mut self, tcp: u16) -> &mut Self { + self.add_value("tcp", &tcp.to_be_bytes()); + self + } + + /// Adds a `tcp6` field to the `ENRBuilder`. + pub fn tcp6(&mut self, tcp: u16) -> &mut Self { + self.add_value("tcp6", &tcp.to_be_bytes()); + self + } + + pub fn udp(&mut self, udp: u16) -> &mut Self { + self.udp4(udp) + } + + /// Adds a `udp` field to the `ENRBuilder`. + pub fn udp4(&mut self, udp: u16) -> &mut Self { + self.add_value("udp", &udp.to_be_bytes()); + self + } + + /// Adds a `udp6` field to the `ENRBuilder`. + pub fn udp6(&mut self, udp: u16) -> &mut Self { + self.add_value("udp6", &udp.to_be_bytes()); + self + } + + /// Generates the rlp-encoded form of the ENR specified by the builder config. + fn rlp_content(&self) -> BytesMut { + let mut stream = RlpStream::new_with_buffer(BytesMut::with_capacity(MAX_ENR_SIZE)); + stream.begin_list(self.content.len() * 2 + 1); + stream.append(&self.seq); + for (k, v) in &self.content { + stream.append(k); + // The values are stored as raw RLP encoded bytes + stream.append_raw(v, 1); + } + stream.out() + } + + /// Signs record based on the identity scheme. Currently only "v4" is supported. + fn signature(&self, key: &K) -> Result, EnrError> { + match self.id.as_str() { + "v4" => key + .sign_v4(&self.rlp_content()) + .map_err(|_| EnrError::SigningError), + // unsupported identity schemes + _ => Err(EnrError::SigningError), + } + } + + /// Adds a public key to the ENR builder. + fn add_public_key(&mut self, key: &K::PublicKey) { + self.add_value(key.enr_key(), key.encode().as_ref()); + } + + /// Constructs an ENR from the `EnrBuilder`. + /// + /// # Errors + /// Fails if the identity scheme is not supported, or the record size exceeds `MAX_ENR_SIZE`. + pub fn build(&mut self, key: &K) -> Result, EnrError> { + // add the identity scheme to the content + if self.id != "v4" { + return Err(EnrError::UnsupportedIdentityScheme); + } + + // Sanitize all data, ensuring all RLP data is correctly formatted. + for (key, value) in &self.content { + if rlp::Rlp::new(value).data().is_err() { + return Err(EnrError::InvalidRlpData( + String::from_utf8_lossy(key).into(), + )); + } + } + + self.add_value_rlp("id", rlp::encode(&self.id.as_bytes()).freeze()); + + self.add_public_key(&key.public()); + let rlp_content = self.rlp_content(); + + let signature = self.signature(key)?; + + // check the size of the record + if rlp_content.len() + signature.len() + 8 > MAX_ENR_SIZE { + return Err(EnrError::ExceedsMaxSize); + } + + Ok(Enr { + seq: self.seq, + node_id: NodeId::from(key.public()), + content: self.content.clone(), + signature, + phantom: PhantomData, + }) + } +} diff --git a/version-meld/enr/src/error.rs b/version-meld/enr/src/error.rs new file mode 100644 index 0000000..55281c9 --- /dev/null +++ b/version-meld/enr/src/error.rs @@ -0,0 +1,33 @@ +//! The error type emitted for various ENR operations. + +use std::error::Error; +use std::fmt; + +#[derive(Clone, Debug)] +/// An error type for handling various ENR operations. +pub enum EnrError { + /// The ENR is too large. + ExceedsMaxSize, + /// The sequence number is too large. + SequenceNumberTooHigh, + /// There was an error with signing an ENR record. + SigningError, + /// The identity scheme is not supported. + UnsupportedIdentityScheme, + /// The entered RLP data is invalid. + InvalidRlpData(String), +} + +impl fmt::Display for EnrError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ExceedsMaxSize => write!(f, "enr exceeds max size"), + Self::SequenceNumberTooHigh => write!(f, "sequence number too large"), + Self::SigningError => write!(f, "signing error"), + Self::UnsupportedIdentityScheme => write!(f, "unsupported identity scheme"), + Self::InvalidRlpData(_rlp) => write!(f, "invalid rlp data"), + } + } +} + +impl Error for EnrError {} diff --git a/version-meld/enr/src/keys/combined.rs b/version-meld/enr/src/keys/combined.rs new file mode 100644 index 0000000..d69e654 --- /dev/null +++ b/version-meld/enr/src/keys/combined.rs @@ -0,0 +1,184 @@ +//! An implementation that combines the currently supported key types. This +//! facilitates and ENR type than can decode and read ENR's of all supported key types. +//! +//! Currently only `secp256k1` and `ed25519` key types are supported. + +use super::{ed25519_dalek as ed25519, EnrKey, EnrPublicKey, SigningError}; +use bytes::Bytes; +pub use k256; +use rand::RngCore; +use rlp::DecoderError; +use std::collections::BTreeMap; +use zeroize::Zeroize; + +use crate::Key; + +/// A standard implementation of the `EnrKey` trait used to sign and modify ENR records. The variants here represent the currently +/// supported in-built signing schemes. +pub enum CombinedKey { + /// An `secp256k1` keypair. + Secp256k1(k256::ecdsa::SigningKey), + /// An `Ed25519` keypair. + Ed25519(ed25519::Keypair), +} + +impl From for CombinedKey { + fn from(secret_key: k256::ecdsa::SigningKey) -> Self { + Self::Secp256k1(secret_key) + } +} + +impl From for CombinedKey { + fn from(keypair: ed25519_dalek::Keypair) -> Self { + Self::Ed25519(keypair) + } +} + +/// Promote an Ed25519 secret key into a keypair. +impl From for CombinedKey { + fn from(secret: ed25519::SecretKey) -> Self { + let public = ed25519::PublicKey::from(&secret); + Self::Ed25519(ed25519::Keypair { secret, public }) + } +} + +impl EnrKey for CombinedKey { + type PublicKey = CombinedPublicKey; + + /// Performs ENR-specific signing. + /// + /// Note: that this library supports a number of signing algorithms. The ENR specification + /// currently lists the `v4` identity scheme which requires the `secp256k1` signing algorithm. + /// Using `secp256k1` keys follow the `v4` identity scheme, using other types do not, although + /// they are supported. + fn sign_v4(&self, msg: &[u8]) -> Result, SigningError> { + match self { + Self::Secp256k1(ref key) => key.sign_v4(msg), + Self::Ed25519(ref key) => key.sign_v4(msg), + } + } + + /// Returns the public key associated with the private key. + fn public(&self) -> Self::PublicKey { + match self { + Self::Secp256k1(key) => CombinedPublicKey::from(key.public()), + Self::Ed25519(key) => CombinedPublicKey::from(key.public()), + } + } + + /// Decodes the raw bytes of an ENR's content into a public key if possible. + fn enr_to_public(content: &BTreeMap) -> Result { + k256::ecdsa::SigningKey::enr_to_public(content) + .map(CombinedPublicKey::Secp256k1) + .or_else(|_| ed25519::Keypair::enr_to_public(content).map(CombinedPublicKey::from)) + } +} + +impl CombinedKey { + /// Generates a new secp256k1 key. + #[must_use] + pub fn generate_secp256k1() -> Self { + let key = k256::ecdsa::SigningKey::random(rand::thread_rng()); + Self::Secp256k1(key) + } + + /// Generates a new ed25510 key. + #[must_use] + pub fn generate_ed25519() -> Self { + let mut bytes = [0_u8; 32]; + rand::thread_rng().fill_bytes(&mut bytes); + let key = + Self::from(ed25519::SecretKey::from_bytes(&bytes).expect( + "this returns `Err` only if the length is wrong; the length is correct; qed", + )); + bytes.zeroize(); + key + } + + /// Imports a secp256k1 from raw bytes in any format. + pub fn secp256k1_from_bytes(bytes: &mut [u8]) -> Result { + let key = k256::ecdsa::SigningKey::from_bytes(bytes) + .map_err(|_| DecoderError::Custom("Invalid secp256k1 secret key")) + .map(Self::from)?; + bytes.zeroize(); + Ok(key) + } + + /// Imports an ed25519 key from raw 32 bytes. + pub fn ed25519_from_bytes(bytes: &mut [u8]) -> Result { + let key = ed25519::SecretKey::from_bytes(bytes) + .map_err(|_| DecoderError::Custom("Invalid ed25519 secret key")) + .map(Self::from)?; + bytes.zeroize(); + Ok(key) + } + + /// Encodes the `CombinedKey` into compressed (where possible) bytes. + #[must_use] + pub fn encode(&self) -> Vec { + match self { + Self::Secp256k1(key) => key.to_bytes().to_vec(), + Self::Ed25519(key) => key.secret.as_bytes().to_vec(), + } + } +} + +/// A combined implementation of `EnrPublicKey` which has support for `Secp256k1` +/// and `Ed25519` for ENR signature verification. +#[derive(Clone, Debug, PartialEq)] +pub enum CombinedPublicKey { + /// An `Secp256k1` public key. + Secp256k1(k256::ecdsa::VerifyingKey), + /// An `Ed25519` public key. + Ed25519(ed25519::PublicKey), +} + +impl From for CombinedPublicKey { + fn from(public_key: k256::ecdsa::VerifyingKey) -> Self { + Self::Secp256k1(public_key) + } +} + +impl From for CombinedPublicKey { + fn from(public_key: ed25519::PublicKey) -> Self { + Self::Ed25519(public_key) + } +} + +impl EnrPublicKey for CombinedPublicKey { + type Raw = Vec; + type RawUncompressed = Vec; + + /// Verify a raw message, given a public key for the v4 identity scheme. + fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool { + match self { + Self::Secp256k1(pk) => pk.verify_v4(msg, sig), + Self::Ed25519(pk) => pk.verify_v4(msg, sig), + } + } + + /// Encodes the public key into compressed form, if possible. + fn encode(&self) -> Vec { + match self { + // serialize in compressed form: 33 bytes + Self::Secp256k1(pk) => pk.encode().to_vec(), + Self::Ed25519(pk) => pk.encode().to_vec(), + } + } + + /// Encodes the public key in uncompressed form. + fn encode_uncompressed(&self) -> Vec { + match self { + Self::Secp256k1(pk) => pk.encode_uncompressed().to_vec(), + Self::Ed25519(pk) => pk.encode_uncompressed().to_vec(), + } + } + + /// Generates the ENR public key string associated with the key type. + fn enr_key(&self) -> Key { + match self { + Self::Secp256k1(key) => key.enr_key(), + Self::Ed25519(key) => key.enr_key(), + } + } +} diff --git a/version-meld/enr/src/keys/ed25519.rs b/version-meld/enr/src/keys/ed25519.rs new file mode 100644 index 0000000..7531b71 --- /dev/null +++ b/version-meld/enr/src/keys/ed25519.rs @@ -0,0 +1,75 @@ +use super::{ + ed25519_dalek::{self as ed25519, Signer as _, Verifier as _}, + EnrKey, EnrKeyUnambiguous, EnrPublicKey, SigningError, +}; +use crate::Key; +use bytes::Bytes; +use rlp::DecoderError; +use std::{collections::BTreeMap, convert::TryFrom}; + +/// The ENR key that stores the public key in the ENR record. +pub const ENR_KEY: &str = "ed25519"; + +impl EnrKey for ed25519::Keypair { + type PublicKey = ed25519::PublicKey; + + /// Performs ENR-specific signing. + /// + /// Using `ed25519` keys do not currently follow the `v4` identity scheme, which dictates + /// `secp256k1` keys should be used. + fn sign_v4(&self, msg: &[u8]) -> Result, SigningError> { + Ok(self.sign(msg).to_bytes().to_vec()) + } + + /// Returns the public key associated with the private key. + fn public(&self) -> Self::PublicKey { + self.public + } + + /// Decodes the raw bytes of an ENR's content into a public key if possible. + fn enr_to_public(content: &BTreeMap) -> Result { + let pubkey_bytes = content + .get(ENR_KEY.as_bytes()) + .ok_or(DecoderError::Custom("Unknown signature"))?; + + // Decode the RLP + let pubkey_bytes = rlp::Rlp::new(pubkey_bytes).data()?; + + Self::decode_public(pubkey_bytes) + } +} + +impl EnrKeyUnambiguous for ed25519::Keypair { + fn decode_public(bytes: &[u8]) -> Result { + ed25519::PublicKey::from_bytes(bytes) + .map_err(|_| DecoderError::Custom("Invalid ed25519 Signature")) + } +} + +impl EnrPublicKey for ed25519::PublicKey { + type Raw = [u8; ed25519::PUBLIC_KEY_LENGTH]; + type RawUncompressed = [u8; ed25519::PUBLIC_KEY_LENGTH]; + + /// Verify a raw message, given a public key for the v4 identity scheme. + fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool { + ed25519::Signature::try_from(sig) + .and_then(|s| self.verify(msg, &s)) + .is_ok() + } + + /// Encodes the public key into compressed form, if possible. + fn encode(&self) -> Self::Raw { + self.to_bytes() + } + + /// Encodes the public key in uncompressed form. This is the same as the compressed form for + /// ed25519 keys + fn encode_uncompressed(&self) -> Self::RawUncompressed { + self.encode() + } + + /// Generates the ENR public key string associated with the ed25519 key type. + fn enr_key(&self) -> Key { + ENR_KEY.into() + } +} diff --git a/version-meld/enr/src/keys/k256_key.rs b/version-meld/enr/src/keys/k256_key.rs new file mode 100644 index 0000000..53b38da --- /dev/null +++ b/version-meld/enr/src/keys/k256_key.rs @@ -0,0 +1,106 @@ +//! An implementation for `EnrKey` for `k256::ecdsa::SigningKey` + +use super::{EnrKey, EnrKeyUnambiguous, EnrPublicKey, SigningError}; +use crate::Key; +use bytes::Bytes; +use k256::{ + ecdsa::{ + signature::{DigestVerifier, RandomizedDigestSigner, Signature as _}, + Signature, SigningKey, VerifyingKey, + }, + elliptic_curve::{ + sec1::{Coordinates, ToEncodedPoint}, + subtle::Choice, + DecompressPoint, + }, + AffinePoint, CompressedPoint, EncodedPoint, +}; +use rand::rngs::OsRng; +use rlp::DecoderError; +use sha3::{Digest, Keccak256}; +use std::{collections::BTreeMap, convert::TryFrom}; + +/// The ENR key that stores the public key in the ENR record. +pub const ENR_KEY: &str = "secp256k1"; + +impl EnrKey for SigningKey { + type PublicKey = VerifyingKey; + + fn sign_v4(&self, msg: &[u8]) -> Result, SigningError> { + // take a keccak256 hash then sign. + let digest = Keccak256::new().chain_update(msg); + let signature: Signature = self + .try_sign_digest_with_rng(&mut OsRng, digest) + .map_err(|_| SigningError::new("failed to sign"))?; + + Ok(signature.as_bytes().to_vec()) + } + + fn public(&self) -> Self::PublicKey { + self.verifying_key() + } + + fn enr_to_public(content: &BTreeMap) -> Result { + let pubkey_bytes = content + .get(ENR_KEY.as_bytes()) + .ok_or(DecoderError::Custom("Unknown signature"))?; + + // Decode the RLP + let pubkey_bytes = rlp::Rlp::new(pubkey_bytes).data()?; + + Self::decode_public(pubkey_bytes) + } +} + +impl EnrKeyUnambiguous for SigningKey { + fn decode_public(bytes: &[u8]) -> Result { + // should be encoded in compressed form, i.e 33 byte raw secp256k1 public key + VerifyingKey::from_sec1_bytes(bytes) + .map_err(|_| DecoderError::Custom("Invalid Secp256k1 Signature")) + } +} + +impl EnrPublicKey for VerifyingKey { + type Raw = CompressedPoint; + type RawUncompressed = [u8; 64]; + + fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool { + if let Ok(sig) = k256::ecdsa::Signature::try_from(sig) { + return self + .verify_digest(Keccak256::new().chain_update(msg), &sig) + .is_ok(); + } + false + } + + fn encode(&self) -> Self::Raw { + // serialize in compressed form: 33 bytes + self.to_bytes() + } + + fn encode_uncompressed(&self) -> Self::RawUncompressed { + let p = EncodedPoint::from(self); + let (x, y) = match p.coordinates() { + Coordinates::Compact { .. } | Coordinates::Identity => unreachable!(), + Coordinates::Compressed { x, y_is_odd } => ( + x, + *AffinePoint::decompress(x, Choice::from(u8::from(y_is_odd))) + .unwrap() + .to_encoded_point(false) + .y() + .unwrap(), + ), + Coordinates::Uncompressed { x, y } => (x, *y), + }; + + let mut coords = [0; 64]; + coords[..32].copy_from_slice(x); + coords[32..].copy_from_slice(&y); + + coords + } + + fn enr_key(&self) -> Key { + ENR_KEY.into() + } +} diff --git a/version-meld/enr/src/keys/mod.rs b/version-meld/enr/src/keys/mod.rs new file mode 100644 index 0000000..f54a77e --- /dev/null +++ b/version-meld/enr/src/keys/mod.rs @@ -0,0 +1,108 @@ +//! This module provides the [`EnrKey`] and [`EnrPublicKey`] traits. User's wishing to implement their +//! own signing schemes can implement these traits and apply them to a [`Enr`]. +//! +//! [`EnrKey`]: crate::EnrKey +//! [`EnrPublicKey`]: crate::EnrPublicKey +//! [`Enr`]: crate::Enr + +#[cfg(all(feature = "ed25519", feature = "k256"))] +mod combined; +#[cfg(feature = "ed25519")] +mod ed25519; +#[cfg(any(feature = "k256"))] +mod k256_key; +#[cfg(feature = "rust-secp256k1")] +mod rust_secp256k1; + +#[cfg(all(feature = "ed25519", feature = "k256"))] +pub use combined::{CombinedKey, CombinedPublicKey}; +#[cfg(feature = "ed25519")] +pub use ed25519_dalek; +#[cfg(any(feature = "k256"))] +pub use k256; +#[cfg(feature = "rust-secp256k1")] +pub use secp256k1; + +use crate::Key; +use bytes::Bytes; +use rlp::DecoderError; +use std::{ + collections::BTreeMap, + error::Error, + fmt::{self, Debug, Display}, +}; + +/// The trait required for a key to sign and modify an ENR record. +pub trait EnrKey: Send + Sync + Unpin + 'static { + type PublicKey: EnrPublicKey + Clone; + + /// Performs ENR-specific signing for the `v4` identity scheme. + fn sign_v4(&self, msg: &[u8]) -> Result, SigningError>; + + /// Returns the public key associated with current key pair. + fn public(&self) -> Self::PublicKey; + + /// Provides a method to decode a raw public key from an ENR `BTreeMap` to a useable public key. + /// + /// This method allows a key type to decode the raw bytes in an ENR to a useable + /// `EnrPublicKey`. It takes the ENR's `BTreeMap` and returns a public key. + /// + /// Note: This specifies the supported key schemes for an ENR. + fn enr_to_public(content: &BTreeMap) -> Result; +} + +/// Trait for keys that are uniquely represented +pub trait EnrKeyUnambiguous: EnrKey { + /// Decode raw bytes as corresponding public key. + fn decode_public(bytes: &[u8]) -> Result; +} + +/// The trait required for a `PublicKey` to verify an ENR record. +pub trait EnrPublicKey: Clone + Debug + Send + Sync + Unpin + 'static { + type Raw: AsRef<[u8]>; + type RawUncompressed: AsRef<[u8]>; + + /// Verify an ENR signature for the `v4` identity scheme. + fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool; + + /// Encodes the public key to bytes in compressed form, if possible. + fn encode(&self) -> Self::Raw; + + /// Encodes the public key in uncompressed form. + // For compatible keys, encode in uncompressed form. Necessary for generating the node-id + fn encode_uncompressed(&self) -> Self::RawUncompressed; + + /// Returns the ENR key identifier for the public key type. For `secp256k1` keys this + /// is `secp256k1`. + fn enr_key(&self) -> Key; +} + +/// An error during signing of a message. +#[derive(Debug)] +pub struct SigningError { + msg: String, + source: Option>, +} + +/// An error during encoding of key material. +#[allow(dead_code)] +impl SigningError { + pub(crate) fn new(msg: S) -> Self { + Self { + msg: msg.to_string(), + source: None, + } + } +} + +impl fmt::Display for SigningError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Key signing error: {}", self.msg) + } +} + +impl Error for SigningError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.source.as_ref().map(|s| &**s as &dyn Error) + } +} diff --git a/version-meld/enr/src/keys/rust_secp256k1.rs b/version-meld/enr/src/keys/rust_secp256k1.rs new file mode 100644 index 0000000..28daa1a --- /dev/null +++ b/version-meld/enr/src/keys/rust_secp256k1.rs @@ -0,0 +1,73 @@ +use super::{EnrKey, EnrKeyUnambiguous, EnrPublicKey, SigningError}; +use crate::{digest, Key}; +use bytes::Bytes; +use rlp::DecoderError; +use secp256k1::SECP256K1; +use std::collections::BTreeMap; + +/// The ENR key that stores the public key in the ENR record. +pub const ENR_KEY: &str = "secp256k1"; + +impl EnrKey for secp256k1::SecretKey { + type PublicKey = secp256k1::PublicKey; + + fn sign_v4(&self, msg: &[u8]) -> Result, SigningError> { + // take a keccak256 hash then sign. + let hash = digest(msg); + let m = secp256k1::Message::from_slice(&hash) + .map_err(|_| SigningError::new("failed to parse secp256k1 digest"))?; + // serialize to an uncompressed 64 byte vector + Ok(SECP256K1.sign_ecdsa(&m, self).serialize_compact().to_vec()) + } + + fn public(&self) -> Self::PublicKey { + Self::PublicKey::from_secret_key(SECP256K1, self) + } + + fn enr_to_public(content: &BTreeMap) -> Result { + let pubkey_bytes = content + .get(ENR_KEY.as_bytes()) + .ok_or(DecoderError::Custom("Unknown signature"))?; + // Decode the RLP + let pubkey_bytes = rlp::Rlp::new(pubkey_bytes).data()?; + + Self::decode_public(pubkey_bytes) + } +} + +impl EnrKeyUnambiguous for secp256k1::SecretKey { + fn decode_public(bytes: &[u8]) -> Result { + // should be encoded in compressed form, i.e 33 byte raw secp256k1 public key + secp256k1::PublicKey::from_slice(bytes) + .map_err(|_| DecoderError::Custom("Invalid Secp256k1 Signature")) + } +} + +impl EnrPublicKey for secp256k1::PublicKey { + type Raw = [u8; secp256k1::constants::PUBLIC_KEY_SIZE]; + type RawUncompressed = [u8; secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE - 1]; + + fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool { + let msg = digest(msg); + if let Ok(sig) = secp256k1::ecdsa::Signature::from_compact(sig) { + if let Ok(msg) = secp256k1::Message::from_slice(&msg) { + return SECP256K1.verify_ecdsa(&msg, &sig, self).is_ok(); + } + } + false + } + + fn encode(&self) -> Self::Raw { + self.serialize() + } + + fn encode_uncompressed(&self) -> Self::RawUncompressed { + let mut out = [0_u8; secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE - 1]; + out.copy_from_slice(&self.serialize_uncompressed()[1..]); + out + } + + fn enr_key(&self) -> Key { + ENR_KEY.into() + } +} diff --git a/version-meld/enr/src/lib.rs b/version-meld/enr/src/lib.rs new file mode 100644 index 0000000..7d258e4 --- /dev/null +++ b/version-meld/enr/src/lib.rs @@ -0,0 +1,1268 @@ +//! # Ethereum Node Record (ENR) +//! +//! This crate contains an implementation of an Ethereum Node Record (ENR) as specified by +//! [EIP-778](https://eips.ethereum.org/EIPS/eip-778) extended to allow for the use of ed25519 keys. +//! +//! An ENR is a signed, key-value record which has an associated [`NodeId`] (a 32-byte identifier). +//! Updating/modifying an ENR requires an [`EnrKey`] in order to re-sign the recrd with the +//! associated key-pair. +//! +//! ENR's are identified by their sequence number. When updating an ENR, the sequence number is +//! increased. +//! +//! Different identity schemes can be used to define the node id and signatures. Currently only the +//! "v4" identity is supported and is set by default. +//! +//! ## Signing Algorithms +//! +//! User's wishing to implement their own singing algorithms simply need to +//! implement the [`EnrKey`] trait and apply it to an [`Enr`]. +//! +//! By default, `k256::SigningKey` implement [`EnrKey`] and can be used to sign and +//! verify ENR records. This library also implements [`EnrKey`] for `ed25519_dalek::Keypair` via the `ed25519` +//! feature flag. +//! +//! Furthermore, a [`CombinedKey`] is provided if the `ed25519` feature flag is set, which provides an +//! ENR type that can support both `secp256k1` and `ed25519` signed ENR records. Examples of the +//! use of each of these key types is given below. +//! +//! ## Features +//! +//! This crate supports a number of features. +//! +//! - `serde`: Allows for serde serialization and deserialization for ENRs. +//! - `ed25519`: Provides support for `ed25519_dalek` keypair types. +//! - `k256`: Uses `k256` for secp256k1 keys. +//! - `rust-secp256k1`: Uses `rust-secp256k1` for secp256k1 keys. +//! +//! These can be enabled via adding the feature flag in your `Cargo.toml` +//! +//! ```toml +//! enr = { version = "*", features = ["serde", "ed25519"] } +//! ``` +//! +//! ## Examples +//! +//! To build an ENR, an [`EnrBuilder`] is provided. +//! +//! ### Building an ENR with the default `k256` `secp256k1` key type +//! +//! ```rust +//! use enr::{EnrBuilder, k256}; +//! use std::net::Ipv4Addr; +//! use rand::thread_rng; +//! +//! // generate a random secp256k1 key +//! let mut rng = thread_rng(); +//! let key = k256::ecdsa::SigningKey::random(&mut rng); +//! +//! let ip = Ipv4Addr::new(192,168,0,1); +//! let enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); +//! +//! assert_eq!(enr.ip4(), Some("192.168.0.1".parse().unwrap())); +//! assert_eq!(enr.id(), Some("v4".into())); +//! ``` +//! +//! ### Building an ENR with the `CombinedKey` type (support for multiple signing +//! algorithms). +//! +//! Note the `ed25519` feature flag must be set. This makes use of the +//! [`EnrBuilder`] struct. +//! ```rust +//! # #[cfg(feature = "ed25519")] { +//! use enr::{EnrBuilder, CombinedKey}; +//! use std::net::Ipv4Addr; +//! +//! // create a new secp256k1 key +//! let key = CombinedKey::generate_secp256k1(); +//! +//! // or create a new ed25519 key +//! let key = CombinedKey::generate_ed25519(); +//! +//! let ip = Ipv4Addr::new(192,168,0,1); +//! let enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); +//! +//! assert_eq!(enr.ip4(), Some("192.168.0.1".parse().unwrap())); +//! assert_eq!(enr.id(), Some("v4".into())); +//! # } +//! ``` +//! +//! ### Modifying an [`Enr`] +//! +//! ENR fields can be added and modified using the getters/setters on [`Enr`]. A custom field +//! can be added using [`insert`] and retrieved with [`get`]. +//! +//! ```rust +//! use enr::{EnrBuilder, k256::ecdsa::SigningKey, Enr}; +//! use std::net::Ipv4Addr; +//! use rand::thread_rng; +//! +//! // specify the type of ENR +//! type DefaultEnr = Enr; +//! +//! // generate a random secp256k1 key +//! let mut rng = thread_rng(); +//! let key = SigningKey::random(&mut rng); +//! +//! let ip = Ipv4Addr::new(192,168,0,1); +//! let mut enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); +//! +//! enr.set_tcp4(8001, &key); +//! // set a custom key +//! enr.insert("custom_key", &vec![0,0,1], &key); +//! +//! // encode to base64 +//! let base_64_string = enr.to_base64(); +//! +//! // decode from base64 +//! let decoded_enr: DefaultEnr = base_64_string.parse().unwrap(); +//! +//! assert_eq!(decoded_enr.ip4(), Some("192.168.0.1".parse().unwrap())); +//! assert_eq!(decoded_enr.id(), Some("v4".into())); +//! assert_eq!(decoded_enr.tcp4(), Some(8001)); +//! assert_eq!(decoded_enr.get("custom_key"), Some(vec![0,0,1].as_slice())); +//! ``` +//! +//! ### Encoding/Decoding ENR's of various key types +//! +//! ```rust +//! # #[cfg(feature = "ed25519")] { +//! use enr::{EnrBuilder, k256::ecdsa::SigningKey, Enr, ed25519_dalek::Keypair, CombinedKey}; +//! use std::net::Ipv4Addr; +//! use rand::thread_rng; +//! use rand::Rng; +//! +//! // generate a random secp256k1 key +//! let mut rng = thread_rng(); +//! let key = SigningKey::random(&mut rng); +//! let ip = Ipv4Addr::new(192,168,0,1); +//! let enr_secp256k1 = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); +//! +//! // encode to base64 +//! let base64_string_secp256k1 = enr_secp256k1.to_base64(); +//! +//! // generate a random ed25519 key +//! # let mut rng = rand_07::thread_rng(); +//! let key = Keypair::generate(&mut rng); +//! let enr_ed25519 = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); +//! +//! // encode to base64 +//! let base64_string_ed25519 = enr_ed25519.to_base64(); +//! +//! // decode base64 strings of varying key types +//! // decode the secp256k1 with default Enr +//! let decoded_enr_secp256k1: Enr = base64_string_secp256k1.parse().unwrap(); +//! // decode ed25519 ENRs +//! let decoded_enr_ed25519: Enr = base64_string_ed25519.parse().unwrap(); +//! +//! // use the combined key to be able to decode either +//! let decoded_enr: Enr = base64_string_secp256k1.parse().unwrap(); +//! let decoded_enr: Enr = base64_string_ed25519.parse().unwrap(); +//! # } +//! ``` +//! +//! +//! [`CombinedKey`]: enum.CombinedKey.html +//! [`EnrKey`]: trait.EnrKey.html +//! [`Enr`]: struct.EnrBase.html +//! [`EnrBuilder`]: struct.EnrBuilderBase.html +//! [`NodeId`]: struct.NodeId.html +//! [`insert`]: struct.Enr.html#method.insert +//! [`get`]: struct.Enr.html#method.get + +#![warn(clippy::all, clippy::pedantic, clippy::nursery)] +#![allow( + clippy::map_err_ignore, + clippy::missing_errors_doc, + clippy::module_name_repetitions +)] + +mod builder; +mod error; +mod keys; +mod node_id; + +use bytes::{Bytes, BytesMut}; +use log::debug; +use rlp::{DecoderError, Rlp, RlpStream}; +use std::{ + collections::BTreeMap, + net::{SocketAddrV4, SocketAddrV6}, +}; + +#[cfg(feature = "serde")] +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use sha3::{Digest, Keccak256}; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + str::FromStr, +}; + +pub use builder::EnrBuilder; +pub use error::EnrError; + +#[cfg(feature = "k256")] +pub use keys::k256; +#[cfg(feature = "rust-secp256k1")] +pub use keys::secp256k1; +#[cfg(all(feature = "ed25519", feature = "k256"))] +pub use keys::{ed25519_dalek, CombinedKey, CombinedPublicKey}; + +pub use keys::{EnrKey, EnrKeyUnambiguous, EnrPublicKey}; +pub use node_id::NodeId; +use std::marker::PhantomData; + +/// The "key" in an ENR record can be arbitrary bytes. +type Key = Vec; + +const MAX_ENR_SIZE: usize = 300; + +/// The ENR, allowing for arbitrary signing algorithms. +/// +/// This struct will always have a valid signature, known public key type, sequence number and `NodeId`. All other parameters are variable/optional. +pub struct Enr { + /// ENR sequence number. + seq: u64, + + /// The `NodeId` of the ENR record. + node_id: NodeId, + + /// Key-value contents of the ENR. A BTreeMap is used to get the keys in sorted order, which is + /// important for verifying the signature of the ENR. + /// Everything is stored as raw RLP bytes. + content: BTreeMap, + + /// The signature of the ENR record, stored as bytes. + signature: Vec, + + /// Marker to pin the generic. + phantom: PhantomData, +} + +impl Enr { + // getters // + + /// The `NodeId` for the record. + #[must_use] + pub const fn node_id(&self) -> NodeId { + self.node_id + } + + /// The current sequence number of the ENR record. + #[must_use] + pub const fn seq(&self) -> u64 { + self.seq + } + + /// Reads a custom key from the record if it exists. + pub fn get(&self, key: impl AsRef<[u8]>) -> Option<&[u8]> { + self.get_raw_rlp(key).map(|rlp_data| { + rlp::Rlp::new(rlp_data) + .data() + .expect("All data is sanitized") + }) + } + + /// Reads a custom key from the record if it exists as raw RLP bytes. + pub fn get_raw_rlp(&self, key: impl AsRef<[u8]>) -> Option<&[u8]> { + self.content.get(key.as_ref()).map(AsRef::as_ref) + } + + /// Returns an iterator over all key/value pairs in the ENR. + pub fn iter(&self) -> impl Iterator { + self.content.iter().map(|(k, v)| (k, v.as_ref())) + } + + /// Returns the IPv4 address of the ENR record if it is defined. + #[must_use] + pub fn ip4(&self) -> Option { + if let Some(ip_bytes) = self.get("ip") { + return match ip_bytes.len() { + 4 => { + let mut ip = [0_u8; 4]; + ip.copy_from_slice(ip_bytes); + Some(Ipv4Addr::from(ip)) + } + _ => None, + }; + } + None + } + + #[must_use] + pub fn ip(&self) -> Option { + self.ip4() + } + + /// Returns the IPv6 address of the ENR record if it is defined. + #[must_use] + pub fn ip6(&self) -> Option { + if let Some(ip_bytes) = self.get("ip6") { + return match ip_bytes.len() { + 16 => { + let mut ip = [0_u8; 16]; + ip.copy_from_slice(ip_bytes); + Some(Ipv6Addr::from(ip)) + } + _ => None, + }; + } + None + } + + /// The `id` of ENR record if it is defined. + #[must_use] + pub fn id(&self) -> Option { + if let Some(id_bytes) = self.get("id") { + return Some(String::from_utf8_lossy(id_bytes).to_string()); + } + None + } + + /// The TCP port of ENR record if it is defined. + #[must_use] + pub fn tcp4(&self) -> Option { + if let Some(tcp_bytes) = self.get("tcp") { + if tcp_bytes.len() <= 2 { + let mut tcp = [0_u8; 2]; + tcp[2 - tcp_bytes.len()..].copy_from_slice(tcp_bytes); + return Some(u16::from_be_bytes(tcp)); + } + } + None + } + + #[must_use] + pub fn tcp(&self) -> Option { + self.tcp4() + } + + /// The IPv6-specific TCP port of ENR record if it is defined. + #[must_use] + pub fn tcp6(&self) -> Option { + if let Some(tcp_bytes) = self.get("tcp6") { + if tcp_bytes.len() <= 2 { + let mut tcp = [0_u8; 2]; + tcp[2 - tcp_bytes.len()..].copy_from_slice(tcp_bytes); + return Some(u16::from_be_bytes(tcp)); + } + } + None + } + + /// The UDP port of ENR record if it is defined. + #[must_use] + pub fn udp4(&self) -> Option { + if let Some(udp_bytes) = self.get("udp") { + if udp_bytes.len() <= 2 { + let mut udp = [0_u8; 2]; + udp[2 - udp_bytes.len()..].copy_from_slice(udp_bytes); + return Some(u16::from_be_bytes(udp)); + } + } + None + } + + #[must_use] + pub fn udp(&self) -> Option { + self.udp4() + } + + /// The IPv6-specific UDP port of ENR record if it is defined. + #[must_use] + pub fn udp6(&self) -> Option { + if let Some(udp_bytes) = self.get("udp6") { + if udp_bytes.len() <= 2 { + let mut udp = [0_u8; 2]; + udp[2 - udp_bytes.len()..].copy_from_slice(udp_bytes); + return Some(u16::from_be_bytes(udp)); + } + } + None + } + + /// Provides a socket (based on the UDP port), if the IPv4 and UDP fields are specified. + #[must_use] + pub fn udp4_socket(&self) -> Option { + if let Some(ip) = self.ip4() { + if let Some(udp) = self.udp4() { + return Some(SocketAddrV4::new(ip, udp)); + } + } + None + } + + /// Provides a socket (based on the UDP port), if the IPv4 and UDP fields are specified. + #[must_use] + pub fn udp6_socket(&self) -> Option { + if let Some(ip6) = self.ip6() { + if let Some(udp6) = self.udp6() { + return Some(SocketAddrV6::new(ip6, udp6, 0, 0)); + } + } + None + } + + #[must_use] + pub fn udp_socket(&self) -> Option { + if let Some(v4)=self.udp4_socket(){ + return Some(SocketAddr::V4(v4)); + } + if let Some(v6)=self.udp6_socket(){ + return Some(SocketAddr::V6(v6)); + } + return None; + } + + /// Provides a socket (based on the TCP port), if the IP and TCP fields are specified. + #[must_use] + pub fn tcp4_socket(&self) -> Option { + if let Some(ip) = self.ip4() { + if let Some(tcp) = self.tcp4() { + return Some(SocketAddrV4::new(ip, tcp)); + } + } + None + } + + /// Provides a socket (based on the TCP port), if the IPv6 and TCP6 fields are specified. + #[must_use] + pub fn tcp6_socket(&self) -> Option { + if let Some(ip6) = self.ip6() { + if let Some(tcp6) = self.tcp6() { + return Some(SocketAddrV6::new(ip6, tcp6, 0, 0)); + } + } + None + } + + /// The signature of the ENR record. + #[must_use] + pub fn signature(&self) -> &[u8] { + &self.signature + } + + /// Returns the public key of the ENR record. + #[must_use] + pub fn public_key(&self) -> K::PublicKey { + K::enr_to_public(&self.content).expect("ENR's can only be created with supported keys") + } + + /// Verify the signature of the ENR record. + #[must_use] + pub fn verify(&self) -> bool { + let pubkey = self.public_key(); + match self.id() { + Some(ref id) if id == "v4" => pubkey.verify_v4(&self.rlp_content(), &self.signature), + // unsupported identity schemes + _ => false, + } + } + + /// Provides the URL-safe base64 encoded "text" version of the ENR prefixed by "enr:". + #[must_use] + pub fn to_base64(&self) -> String { + let hex = base64::encode_config(&rlp::encode(self), base64::URL_SAFE_NO_PAD); + format!("enr:{}", hex) + } + + /// Returns the current size of the ENR. + #[must_use] + pub fn size(&self) -> usize { + self.rlp_content().len() + } + + // Setters // + + /// Allows setting the sequence number to an arbitrary value. + pub fn set_seq(&mut self, seq: u64, key: &K) -> Result<(), EnrError> { + self.seq = seq; + + // sign the record + self.sign(key)?; + + // update the node id + self.node_id = NodeId::from(key.public()); + + // check the size of the record + if self.size() > MAX_ENR_SIZE { + return Err(EnrError::ExceedsMaxSize); + } + + Ok(()) + } + + /// Adds or modifies a key/value to the ENR record. A `EnrKey` is required to re-sign the record once + /// modified. + /// + /// Returns the previous value in the record if it exists. + pub fn insert( + &mut self, + key: impl AsRef<[u8]>, + value: &[u8], + enr_key: &K, + ) -> Result, EnrError> { + self.insert_raw_rlp(key, rlp::encode(&value).freeze(), enr_key) + } + + /// Adds or modifies a key/value to the ENR record. A `EnrKey` is required to re-sign the record once + /// modified. The value here is interpreted as raw RLP data. + /// + /// Returns the previous value in the record if it exists. + pub fn insert_raw_rlp( + &mut self, + key: impl AsRef<[u8]>, + value: Bytes, + enr_key: &K, + ) -> Result, EnrError> { + // currently only support "v4" identity schemes + if key.as_ref() == b"id" && &*value != b"v4" { + return Err(EnrError::UnsupportedIdentityScheme); + } + + let previous_value = self.content.insert(key.as_ref().to_vec(), value); + // add the new public key + let public_key = enr_key.public(); + let previous_key = self.content.insert( + public_key.enr_key(), + rlp::encode(&public_key.encode().as_ref()).freeze(), + ); + + // check the size of the record + if self.size() > MAX_ENR_SIZE { + // if the size of the record is too large, revert and error + // revert the public key + if let Some(key) = previous_key { + self.content.insert(public_key.enr_key(), key); + } else { + self.content.remove(&public_key.enr_key()); + } + // revert the content + if let Some(prev_value) = previous_value { + self.content.insert(key.as_ref().to_vec(), prev_value); + } else { + self.content.remove(key.as_ref()); + } + return Err(EnrError::ExceedsMaxSize); + } + // increment the sequence number + self.seq = self + .seq + .checked_add(1) + .ok_or(EnrError::SequenceNumberTooHigh)?; + + // sign the record + self.sign(enr_key)?; + + // update the node id + self.node_id = NodeId::from(enr_key.public()); + + if self.size() > MAX_ENR_SIZE { + // in case the signature size changes, inform the user the size has exceeded the maximum + return Err(EnrError::ExceedsMaxSize); + } + + Ok(previous_value) + } + + /// Sets the `ip` field of the ENR. Returns any pre-existing IP address in the record. + pub fn set_ip(&mut self, ip: IpAddr, key: &K) -> Result, EnrError> { + match ip { + IpAddr::V4(addr) => { + let prev_value = self.insert("ip", &addr.octets(), key)?; + if let Some(bytes) = prev_value { + if bytes.len() == 4 { + let mut v = [0_u8; 4]; + v.copy_from_slice(&bytes); + return Ok(Some(IpAddr::V4(Ipv4Addr::from(v)))); + } + } + } + IpAddr::V6(addr) => { + let prev_value = self.insert("ip6", &addr.octets(), key)?; + if let Some(bytes) = prev_value { + if bytes.len() == 16 { + let mut v = [0_u8; 16]; + v.copy_from_slice(&bytes); + return Ok(Some(IpAddr::V6(Ipv6Addr::from(v)))); + } + } + } + } + + Ok(None) + } + + /// Sets the `udp` field of the ENR. Returns any pre-existing UDP port in the record. + pub fn set_udp4(&mut self, udp: u16, key: &K) -> Result, EnrError> { + if let Some(udp_bytes) = self.insert("udp", &udp.to_be_bytes(), key)? { + if udp_bytes.len() <= 2 { + let mut v = [0_u8; 2]; + v[2 - udp_bytes.len()..].copy_from_slice(&udp_bytes); + return Ok(Some(u16::from_be_bytes(v))); + } + } + Ok(None) + } + + /// Sets the `udp6` field of the ENR. Returns any pre-existing UDP port in the record. + pub fn set_udp6(&mut self, udp: u16, key: &K) -> Result, EnrError> { + if let Some(udp_bytes) = self.insert("udp6", &udp.to_be_bytes(), key)? { + if udp_bytes.len() <= 2 { + let mut v = [0_u8; 2]; + v[2 - udp_bytes.len()..].copy_from_slice(&udp_bytes); + return Ok(Some(u16::from_be_bytes(v))); + } + } + Ok(None) + } + + /// Sets the `tcp` field of the ENR. Returns any pre-existing tcp port in the record. + pub fn set_tcp4(&mut self, tcp: u16, key: &K) -> Result, EnrError> { + if let Some(tcp_bytes) = self.insert("tcp", &tcp.to_be_bytes(), key)? { + if tcp_bytes.len() <= 2 { + let mut v = [0_u8; 2]; + v[2 - tcp_bytes.len()..].copy_from_slice(&tcp_bytes); + return Ok(Some(u16::from_be_bytes(v))); + } + } + Ok(None) + } + + /// Sets the `tcp6` field of the ENR. Returns any pre-existing tcp6 port in the record. + pub fn set_tcp6(&mut self, tcp: u16, key: &K) -> Result, EnrError> { + if let Some(tcp_bytes) = self.insert("tcp6", &tcp.to_be_bytes(), key)? { + if tcp_bytes.len() <= 2 { + let mut v = [0_u8; 2]; + v[2 - tcp_bytes.len()..].copy_from_slice(&tcp_bytes); + return Ok(Some(u16::from_be_bytes(v))); + } + } + Ok(None) + } + + /// Sets the IP and UDP port in a single update with a single increment in sequence number. + pub fn set_udp_socket(&mut self, socket: SocketAddr, key: &K) -> Result<(), EnrError> { + self.set_socket(socket, key, false) + } + + /// Sets the IP and TCP port in a single update with a single increment in sequence number. + pub fn set_tcp_socket(&mut self, socket: SocketAddr, key: &K) -> Result<(), EnrError> { + self.set_socket(socket, key, true) + } + + /// Helper function for `set_tcp_socket()` and `set_udp_socket`. + fn set_socket(&mut self, socket: SocketAddr, key: &K, is_tcp: bool) -> Result<(), EnrError> { + let (port_string, port_v6_string): (Key, Key) = if is_tcp { + ("tcp".into(), "tcp6".into()) + } else { + ("udp".into(), "udp6".into()) + }; + + let (prev_ip, prev_port) = match socket.ip() { + IpAddr::V4(addr) => ( + self.content.insert( + "ip".into(), + rlp::encode(&(&addr.octets() as &[u8])).freeze(), + ), + self.content.insert( + port_string.clone(), + rlp::encode(&(&socket.port().to_be_bytes() as &[u8])).freeze(), + ), + ), + IpAddr::V6(addr) => ( + self.content.insert( + "ip6".into(), + rlp::encode(&(&addr.octets() as &[u8])).freeze(), + ), + self.content.insert( + port_v6_string.clone(), + rlp::encode(&(&socket.port().to_be_bytes() as &[u8])).freeze(), + ), + ), + }; + + let public_key = key.public(); + let previous_key = self.content.insert( + public_key.enr_key(), + rlp::encode(&public_key.encode().as_ref()).freeze(), + ); + + // check the size and revert on failure + if self.size() > MAX_ENR_SIZE { + // if the size of the record is too large, revert and error + // revert the public key + if let Some(key) = previous_key { + self.content.insert(public_key.enr_key(), key); + } else { + self.content.remove(&public_key.enr_key()); + } + // revert the content + match socket.ip() { + IpAddr::V4(_) => { + if let Some(ip) = prev_ip { + self.content.insert("ip".into(), ip); + } else { + self.content.remove(b"ip".as_ref()); + } + if let Some(udp) = prev_port { + self.content.insert(port_string, udp); + } else { + self.content.remove(&port_string); + } + } + IpAddr::V6(_) => { + if let Some(ip) = prev_ip { + self.content.insert("ip6".into(), ip); + } else { + self.content.remove(b"ip6".as_ref()); + } + if let Some(udp) = prev_port { + self.content.insert(port_v6_string, udp); + } else { + self.content.remove(&port_v6_string); + } + } + } + return Err(EnrError::ExceedsMaxSize); + } + + // increment the sequence number + self.seq = self + .seq + .checked_add(1) + .ok_or(EnrError::SequenceNumberTooHigh)?; + + // sign the record + self.sign(key)?; + + // update the node id + self.node_id = NodeId::from(key.public()); + + Ok(()) + } + + /// Sets a new public key for the record. + pub fn set_public_key(&mut self, public_key: &K::PublicKey, key: &K) -> Result<(), EnrError> { + self.insert(&public_key.enr_key(), public_key.encode().as_ref(), key) + .map(|_| {}) + } + + // Private Functions // + + /// Evaluates the RLP-encoding of the content of the ENR record. + fn rlp_content(&self) -> BytesMut { + let mut stream = RlpStream::new_with_buffer(BytesMut::with_capacity(MAX_ENR_SIZE)); + stream.begin_list(self.content.len() * 2 + 1); + stream.append(&self.seq); + for (k, v) in &self.content { + // Keys are bytes + stream.append(k); + // Values are raw RLP encoded data + stream.append_raw(v, 1); + } + stream.out() + } + + /// Signs the ENR record based on the identity scheme. Currently only "v4" is supported. + fn sign(&mut self, key: &K) -> Result<(), EnrError> { + self.signature = { + match self.id() { + Some(ref id) if id == "v4" => key + .sign_v4(&self.rlp_content()) + .map_err(|_| EnrError::SigningError)?, + // other identity schemes are unsupported + _ => return Err(EnrError::SigningError), + } + }; + Ok(()) + } +} + +// traits // + +impl Clone for Enr { + fn clone(&self) -> Self { + Self { + seq: self.seq, + node_id: self.node_id, + content: self.content.clone(), + signature: self.signature.clone(), + phantom: self.phantom, + } + } +} + +impl std::cmp::Eq for Enr {} + +impl PartialEq for Enr { + fn eq(&self, other: &Self) -> bool { + self.seq == other.seq && self.node_id == other.node_id && self.signature == other.signature + } +} + +impl std::fmt::Display for Enr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "ENR: NodeId: {}, IpV4 Socket: {:?} IpV6 Socket: {:?}", + self.node_id(), + self.udp4_socket(), + self.udp6_socket() + ) + } +} + +impl std::fmt::Debug for Enr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.to_base64()) + } +} + +/// Convert a URL-SAFE base64 encoded ENR into an ENR. +impl FromStr for Enr { + type Err = String; + + fn from_str(base64_string: &str) -> Result { + if base64_string.len() < 4 { + return Err("Invalid ENR string".to_string()); + } + // support both enr prefix and not + let mut decode_string = base64_string; + if base64_string.starts_with("enr:") { + decode_string = decode_string + .get(4..) + .ok_or_else(|| "Invalid ENR string".to_string())?; + } + let bytes = base64::decode_config(decode_string, base64::URL_SAFE_NO_PAD) + .map_err(|e| format!("Invalid base64 encoding: {:?}", e))?; + rlp::decode(&bytes).map_err(|e| format!("Invalid ENR: {:?}", e)) + } +} + +#[cfg(any(feature = "serde"))] +impl Serialize for Enr { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_base64()) + } +} + +#[cfg(any(feature = "serde"))] +impl<'de, K: EnrKey> Deserialize<'de> for Enr { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: String = Deserialize::deserialize(deserializer)?; + Self::from_str(&s).map_err(D::Error::custom) + } +} + +impl rlp::Encodable for Enr { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(self.content.len() * 2 + 2); + s.append(&self.signature); + s.append(&self.seq); + // must use rlp_content to preserve ordering. + for (k, v) in &self.content { + // Keys are byte data + s.append(k); + // Values are raw RLP encoded data + s.append_raw(v, 1); + } + } +} + +impl rlp::Decodable for Enr { + fn decode(rlp: &Rlp) -> Result { + if !rlp.is_list() { + debug!("Failed to decode ENR. Not an RLP list: {}", rlp); + return Err(DecoderError::RlpExpectedToBeList); + } + + let mut rlp_iter = rlp.iter(); + + if rlp_iter.len() == 0 || rlp_iter.len() % 2 != 0 { + debug!("Failed to decode ENR. List size is not a multiple of 2."); + return Err(DecoderError::Custom("List not a multiple of two")); + } + + let signature = rlp_iter + .next() + .ok_or(DecoderError::Custom("List is empty"))? + .data()?; + let seq_bytes = rlp_iter + .next() + .ok_or(DecoderError::Custom("List has only one item"))? + .data()?; + + if seq_bytes.len() > 8 { + debug!("Failed to decode ENR. Sequence number is not a u64."); + return Err(DecoderError::Custom("Invalid Sequence number")); + } + + // build u64 from big endian vec + let mut seq: [u8; 8] = [0; 8]; + seq[8 - seq_bytes.len()..].copy_from_slice(seq_bytes); + let seq = u64::from_be_bytes(seq); + + let mut content = BTreeMap::new(); + let mut prev: Option<&[u8]> = None; + while let Some(key) = rlp_iter.next() { + let key = key.data()?; + let item = rlp_iter + .next() + .ok_or(DecoderError::Custom("List not a multiple of 2"))?; + + // Sanitize the data + let _ = item.data()?; + let value = item.as_raw(); + + if prev.is_some() && prev >= Some(key) { + return Err(DecoderError::Custom("Unsorted keys")); + } + prev = Some(key); + content.insert(key.to_vec(), Bytes::copy_from_slice(value)); + } + + // verify we know the signature type + let public_key = K::enr_to_public(&content)?; + + // calculate the node id + let node_id = NodeId::from(public_key); + + let enr = Self { + seq, + node_id, + signature: signature.into(), + content, + phantom: PhantomData, + }; + + // verify the signature before returning + // if the public key is of an unknown type, this will fail. + // An ENR record will always have a valid public-key and therefore node-id + if !enr.verify() { + return Err(DecoderError::Custom("Invalid Signature")); + } + Ok(enr) + } +} + +pub(crate) fn digest(b: &[u8]) -> [u8; 32] { + let mut output = [0_u8; 32]; + output.copy_from_slice(&Keccak256::digest(b)); + output +} + +#[cfg(test)] +#[cfg(feature = "k256")] +mod tests { + use super::*; + use std::net::Ipv4Addr; + + type DefaultEnr = Enr; + + #[cfg(feature = "k256")] + #[test] + fn test_vector_k256() { + let valid_record = hex::decode("f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f").unwrap(); + let signature = hex::decode("7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c").unwrap(); + let expected_pubkey = + hex::decode("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138") + .unwrap(); + + let enr = rlp::decode::(&valid_record).unwrap(); + + let pubkey = enr.public_key().encode(); + + assert_eq!(enr.ip4(), Some(Ipv4Addr::new(127, 0, 0, 1))); + assert_eq!(enr.id(), Some(String::from("v4"))); + assert_eq!(enr.udp4(), Some(30303)); + assert_eq!(enr.tcp4(), None); + assert_eq!(enr.signature(), &signature[..]); + assert_eq!(pubkey.to_vec(), expected_pubkey); + assert!(enr.verify()); + } + + #[cfg(feature = "k256")] + #[test] + fn test_vector_2() { + let text = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; + let signature = hex::decode("7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c").unwrap(); + let expected_pubkey = + hex::decode("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138") + .unwrap(); + let expected_node_id = + hex::decode("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7") + .unwrap(); + + let enr = text.parse::().unwrap(); + let pubkey = enr.public_key().encode(); + assert_eq!(enr.ip4(), Some(Ipv4Addr::new(127, 0, 0, 1))); + assert_eq!(enr.ip6(), None); + assert_eq!(enr.id(), Some(String::from("v4"))); + assert_eq!(enr.udp4(), Some(30303)); + assert_eq!(enr.udp6(), None); + assert_eq!(enr.tcp4(), None); + assert_eq!(enr.tcp6(), None); + assert_eq!(enr.signature(), &signature[..]); + assert_eq!(pubkey.to_vec(), expected_pubkey); + assert_eq!(enr.node_id().raw().to_vec(), expected_node_id); + + assert!(enr.verify()); + } + + #[cfg(feature = "k256")] + #[test] + fn test_vector_2_k256() { + let text = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; + let signature = hex::decode("7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c").unwrap(); + let expected_pubkey = + hex::decode("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138") + .unwrap(); + let expected_node_id = + hex::decode("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7") + .unwrap(); + + let enr = text.parse::>().unwrap(); + let pubkey = enr.public_key().encode(); + assert_eq!(enr.ip4(), Some(Ipv4Addr::new(127, 0, 0, 1))); + assert_eq!(enr.ip6(), None); + assert_eq!(enr.id(), Some(String::from("v4"))); + assert_eq!(enr.udp4(), Some(30303)); + assert_eq!(enr.udp6(), None); + assert_eq!(enr.tcp4(), None); + assert_eq!(enr.tcp6(), None); + assert_eq!(enr.signature(), &signature[..]); + assert_eq!(pubkey.to_vec(), expected_pubkey); + assert_eq!(enr.node_id().raw().to_vec(), expected_node_id); + + assert!(enr.verify()); + } + + // the values in the content are rlp lists + #[test] + fn test_rlp_list_value() { + let text = "enr:-Je4QH0uN2HkMRmscUp6yvyTOPGtOg9U6lCxBFvCGynyystnDNRJbfz5GhXXY2lcu9tsghMxRiYHoznBwG46GQ7dfm0og2V0aMfGhMvbiDiAgmlkgnY0gmlwhA6hJmuJc2VjcDI1NmsxoQJBP4kg9GNBurV3uVXgR72u1n-XIABibUZLT1WvJLKwvIN0Y3CCdyeDdWRwgncn"; + let signature = hex::decode("7d2e3761e43119ac714a7acafc9338f1ad3a0f54ea50b1045bc21b29f2cacb670cd4496dfcf91a15d763695cbbdb6c821331462607a339c1c06e3a190edd7e6d").unwrap(); + let expected_pubkey = + hex::decode("02413f8920f46341bab577b955e047bdaed67f972000626d464b4f55af24b2b0bc") + .unwrap(); + let enr = text.parse::().unwrap(); + + assert_eq!(enr.ip4(), Some(Ipv4Addr::new(14, 161, 38, 107))); + assert_eq!(enr.id(), Some(String::from("v4"))); + assert_eq!(enr.udp4(), Some(30503)); + assert_eq!(enr.tcp4(), Some(30503)); + assert_eq!(enr.seq(), 40); + assert_eq!(enr.signature(), &signature[..]); + assert_eq!(enr.public_key().encode().to_vec(), expected_pubkey); + + assert!(enr.verify()); + } + + #[cfg(feature = "k256")] + #[test] + fn test_read_enr_no_prefix() { + let text = "-Iu4QM-YJF2RRpMcZkFiWzMf2kRd1A5F1GIekPa4Sfi_v0DCLTDBfOMTMMWJhhawr1YLUPb5008CpnBKrgjY3sstjfgCgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQP8u1uyQFyJYuQUTyA1raXKhSw1HhhxNUQ2VE52LNHWMIN0Y3CCIyiDdWRwgiMo"; + text.parse::().unwrap(); + } + + #[cfg(feature = "k256")] + #[test] + fn test_read_enr_prefix() { + let text = "enr:-Iu4QM-YJF2RRpMcZkFiWzMf2kRd1A5F1GIekPa4Sfi_v0DCLTDBfOMTMMWJhhawr1YLUPb5008CpnBKrgjY3sstjfgCgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQP8u1uyQFyJYuQUTyA1raXKhSw1HhhxNUQ2VE52LNHWMIN0Y3CCIyiDdWRwgiMo"; + text.parse::().unwrap(); + } + + #[cfg(feature = "rust-secp256k1")] + #[test] + fn test_encode_decode_secp256k1() { + let mut rng = secp256k1::rand::thread_rng(); + let key = secp256k1::SecretKey::new(&mut rng); + let ip = Ipv4Addr::new(127, 0, 0, 1); + let tcp = 3000; + + let enr = { + let mut builder = EnrBuilder::new("v4"); + builder.ip4(ip); + builder.tcp4(tcp); + builder.build(&key).unwrap() + }; + + let encoded_enr = rlp::encode(&enr); + + let decoded_enr = rlp::decode::>(&encoded_enr).unwrap(); + + assert_eq!(decoded_enr.id(), Some("v4".into())); + assert_eq!(decoded_enr.ip4(), Some(ip)); + assert_eq!(decoded_enr.tcp4(), Some(tcp)); + // Must compare encoding as the public key itself can be different + assert_eq!(decoded_enr.public_key().encode(), key.public().encode()); + assert!(decoded_enr.verify()); + } + + #[cfg(feature = "k256")] + #[test] + fn test_encode_decode_k256() { + let key = k256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); + let ip = Ipv4Addr::new(127, 0, 0, 1); + let tcp = 3000; + + let enr = { + let mut builder = EnrBuilder::new("v4"); + builder.ip(ip.into()); + builder.tcp4(tcp); + builder.build(&key).unwrap() + }; + + let encoded_enr = rlp::encode(&enr); + + let decoded_enr = rlp::decode::>(&encoded_enr).unwrap(); + + assert_eq!(decoded_enr.id(), Some("v4".into())); + assert_eq!(decoded_enr.ip4(), Some(ip)); + assert_eq!(decoded_enr.tcp4(), Some(tcp)); + // Must compare encoding as the public key itself can be different + assert_eq!(decoded_enr.public_key().encode(), key.public().encode()); + decoded_enr.public_key().encode_uncompressed(); + assert!(decoded_enr.verify()); + } + + #[cfg(all(feature = "ed25519", feature = "k256"))] + #[test] + fn test_encode_decode_ed25519() { + let mut rng = rand_07::thread_rng(); + let key = ed25519_dalek::Keypair::generate(&mut rng); + let ip = Ipv4Addr::new(10, 0, 0, 1); + let tcp = 30303; + + let enr = { + let mut builder = EnrBuilder::new("v4"); + builder.ip4(ip.into()); + builder.tcp4(tcp); + builder.build(&key).unwrap() + }; + + let encoded_enr = rlp::encode(&enr); + let decoded_enr = rlp::decode::>(&encoded_enr).unwrap(); + + assert_eq!(decoded_enr.id(), Some("v4".into())); + assert_eq!(decoded_enr.ip4(), Some(ip)); + assert_eq!(decoded_enr.tcp4(), Some(tcp)); + assert_eq!(decoded_enr.public_key().encode(), key.public().encode()); + assert!(decoded_enr.verify()); + } + + #[test] + fn test_add_key() { + let mut rng = rand::thread_rng(); + let key = k256::ecdsa::SigningKey::random(&mut rng); + let ip = Ipv4Addr::new(10, 0, 0, 1); + let tcp = 30303; + + let mut enr = { + let mut builder = EnrBuilder::new("v4"); + builder.ip(ip.into()); + builder.tcp4(tcp); + builder.build(&key).unwrap() + }; + + enr.insert("random", &Vec::new(), &key).unwrap(); + assert!(enr.verify()); + } + + #[test] + fn test_set_ip() { + let mut rng = rand::thread_rng(); + let key = k256::ecdsa::SigningKey::random(&mut rng); + let tcp = 30303; + let ip = Ipv4Addr::new(10, 0, 0, 1); + + let mut enr = { + let mut builder = EnrBuilder::new("v4"); + builder.tcp4(tcp); + builder.build(&key).unwrap() + }; + + assert!(enr.set_ip(ip.into(), &key).is_ok()); + assert_eq!(enr.id(), Some("v4".into())); + assert_eq!(enr.ip4(), Some(ip)); + assert_eq!(enr.tcp4(), Some(tcp)); + assert!(enr.verify()); + + // Compare the encoding as the key itself can be differnet + assert_eq!(enr.public_key().encode(), key.public().encode(),); + } + + #[test] + fn ip_mutation_static_node_id() { + let mut rng = rand::thread_rng(); + let key = k256::ecdsa::SigningKey::random(&mut rng); + let tcp = 30303; + let udp = 30304; + let ip = Ipv4Addr::new(10, 0, 0, 1); + + let mut enr = { + let mut builder = EnrBuilder::new("v4"); + builder.ip(ip.into()); + builder.tcp4(tcp); + builder.udp4(udp); + builder.build(&key).unwrap() + }; + + let node_id = enr.node_id(); + + enr.set_udp_socket("192.168.0.1:800".parse::().unwrap(), &key) + .unwrap(); + assert_eq!(node_id, enr.node_id()); + assert_eq!( + enr.udp4_socket(), + "192.168.0.1:800".parse::().unwrap().into() + ); + } + + #[cfg(all(feature = "ed25519", feature = "k256"))] + #[test] + fn combined_key_can_decode_all() { + // generate a random secp256k1 key + let key = k256::ecdsa::SigningKey::random(&mut rand::thread_rng()); + let ip = Ipv4Addr::new(192, 168, 0, 1); + let enr_secp256k1 = EnrBuilder::new("v4") + .ip(ip.into()) + .tcp4(8000) + .build(&key) + .unwrap(); + + // encode to base64 + let base64_string_secp256k1 = enr_secp256k1.to_base64(); + + // generate a random ed25519 key + let key = ed25519_dalek::Keypair::generate(&mut rand_07::thread_rng()); + let enr_ed25519 = EnrBuilder::new("v4") + .ip(ip.into()) + .tcp4(8000) + .build(&key) + .unwrap(); + + // encode to base64 + let base64_string_ed25519 = enr_ed25519.to_base64(); + + // decode base64 strings of varying key types + // decode the secp256k1 with default Enr + let _decoded_enr_secp256k1: DefaultEnr = base64_string_secp256k1.parse().unwrap(); + // decode ed25519 ENRs + let _decoded_enr_ed25519: Enr = + base64_string_ed25519.parse().unwrap(); + + // use the combined key to be able to decode either + let _decoded_enr: Enr = base64_string_secp256k1 + .parse() + .expect("Can decode both secp"); + let _decoded_enr: Enr = base64_string_ed25519.parse().unwrap(); + } +} diff --git a/version-meld/enr/src/node_id.rs b/version-meld/enr/src/node_id.rs new file mode 100644 index 0000000..2b64dc7 --- /dev/null +++ b/version-meld/enr/src/node_id.rs @@ -0,0 +1,77 @@ +//! The identifier for an ENR record. This is the keccak256 hash of the public key (for secp256k1 +//! keys this is the uncompressed encoded form of the public key). + +use crate::{digest, keys::EnrPublicKey, Enr, EnrKey}; + +type RawNodeId = [u8; 32]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +/// The `NodeId` of an ENR (a 32 byte identifier). +pub struct NodeId { + raw: RawNodeId, +} + +impl NodeId { + /// Creates a new node record from 32 bytes. + #[must_use] + pub const fn new(raw_input: &[u8; 32]) -> Self { + Self { raw: *raw_input } + } + + /// Parses a byte slice to form a node Id. This fails if the slice isn't of length 32. + pub fn parse(raw_input: &[u8]) -> Result { + if raw_input.len() > 32 { + return Err("Input too large"); + } + + let mut raw: RawNodeId = [0_u8; 32]; + raw[..std::cmp::min(32, raw_input.len())].copy_from_slice(raw_input); + + Ok(Self { raw }) + } + + /// Generates a random `NodeId`. + #[must_use] + pub fn random() -> Self { + Self { + raw: rand::random(), + } + } + + /// Returns a `RawNodeId` which is a 32 byte list. + #[must_use] + pub const fn raw(&self) -> RawNodeId { + self.raw + } +} + +impl From for NodeId { + fn from(public_key: T) -> Self { + Self::parse(&digest(public_key.encode_uncompressed().as_ref())) + .expect("always of correct length; qed") + } +} + +impl From> for NodeId { + fn from(enr: Enr) -> Self { + enr.node_id() + } +} + +impl From<&Enr> for NodeId { + fn from(enr: &Enr) -> Self { + enr.node_id() + } +} + +impl std::fmt::Display for NodeId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let hex_encode = hex::encode(self.raw); + write!( + f, + "0x{}..{}", + &hex_encode[0..4], + &hex_encode[hex_encode.len() - 4..] + ) + } +} diff --git a/version-meld/eth2_ssz/.cargo-ok b/version-meld/eth2_ssz/.cargo-ok new file mode 100644 index 0000000..b5754e2 --- /dev/null +++ b/version-meld/eth2_ssz/.cargo-ok @@ -0,0 +1 @@ +ok \ No newline at end of file diff --git a/version-meld/eth2_ssz/.cargo_vcs_info.json b/version-meld/eth2_ssz/.cargo_vcs_info.json new file mode 100644 index 0000000..6292b69 --- /dev/null +++ b/version-meld/eth2_ssz/.cargo_vcs_info.json @@ -0,0 +1,5 @@ +{ + "git": { + "sha1": "0b319d492695daf11cd8fc0712b602b63ee5ed50" + } +} diff --git a/version-meld/eth2_ssz/Cargo.lock b/version-meld/eth2_ssz/Cargo.lock new file mode 100644 index 0000000..f94addf --- /dev/null +++ b/version-meld/eth2_ssz/Cargo.lock @@ -0,0 +1,473 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arbitrary" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db55d72333851e17d572bec876e390cd3b11eb1ef53ae821dd9f3b653d2b4569" + +[[package]] +name = "arbitrary" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510c76ecefdceada737ea728f4f9a84bd2e1ef29f1ba555e560940fe279954de" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "byte-slice-cast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c751592b77c499e7bce34d99d67c2c11bdc0574e9a488ddade14150a4698" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "darling" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "eth2_ssz" +version = "0.4.0" +dependencies = [ + "eth2_ssz_derive", + "ethereum-types", + "smallvec", +] + +[[package]] +name = "eth2_ssz_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635b86d2c941bb71e7419a571e1763d65c93e51a1bafc400352e3bef6ff59fc9" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ethbloom" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "arbitrary 0.4.7", + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "impl-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" + +[[package]] +name = "parity-scale-codec" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" + +[[package]] +name = "primitive-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rlp" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "uint" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +dependencies = [ + "arbitrary 1.0.3", + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/version-meld/eth2_ssz/Cargo.toml b/version-meld/eth2_ssz/Cargo.toml new file mode 100644 index 0000000..06596b9 --- /dev/null +++ b/version-meld/eth2_ssz/Cargo.toml @@ -0,0 +1,31 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "eth2_ssz" +version = "0.4.0" +authors = ["Paul Hauner "] +description = "SimpleSerialize (SSZ) as used in Ethereum 2.0" +license = "Apache-2.0" + +[lib] +name = "ssz" +[dependencies.ethereum-types] +version = "0.14.1" + +[dependencies.smallvec] +version = "1.6.1" +[dev-dependencies.eth2_ssz_derive] +version = "0.3.0" + +[features] +arbitrary = ["ethereum-types/arbitrary"] diff --git a/version-meld/eth2_ssz/Cargo.toml.orig b/version-meld/eth2_ssz/Cargo.toml.orig new file mode 100644 index 0000000..853fd72 --- /dev/null +++ b/version-meld/eth2_ssz/Cargo.toml.orig @@ -0,0 +1,20 @@ +[package] +name = "eth2_ssz" +version = "0.4.0" +authors = ["Paul Hauner "] +edition = "2018" +description = "SimpleSerialize (SSZ) as used in Ethereum 2.0" +license = "Apache-2.0" + +[lib] +name = "ssz" + +[dev-dependencies] +eth2_ssz_derive = "0.3.0" + +[dependencies] +ethereum-types = "0.12.1" +smallvec = "1.6.1" + +[features] +arbitrary = ["ethereum-types/arbitrary"] diff --git a/version-meld/eth2_ssz/README.md b/version-meld/eth2_ssz/README.md new file mode 100644 index 0000000..04603cd --- /dev/null +++ b/version-meld/eth2_ssz/README.md @@ -0,0 +1,3 @@ +# simpleserialize (ssz) + +[](https://crates.io/crates/eth2_ssz) diff --git a/version-meld/eth2_ssz/examples/large_list.rs b/version-meld/eth2_ssz/examples/large_list.rs new file mode 100644 index 0000000..a1b10ab --- /dev/null +++ b/version-meld/eth2_ssz/examples/large_list.rs @@ -0,0 +1,15 @@ +//! Encode and decode a list many times. +//! +//! Useful for `cargo flamegraph`. + +use ssz::{Decode, Encode}; + +fn main() { + let vec: Vec = vec![4242; 8196]; + + let output: Vec> = (0..40_000) + .map(|_| Vec::from_ssz_bytes(&vec.as_ssz_bytes()).unwrap()) + .collect(); + + println!("{}", output.len()); +} diff --git a/version-meld/eth2_ssz/examples/large_list_of_structs.rs b/version-meld/eth2_ssz/examples/large_list_of_structs.rs new file mode 100644 index 0000000..2aaaf9b --- /dev/null +++ b/version-meld/eth2_ssz/examples/large_list_of_structs.rs @@ -0,0 +1,31 @@ +//! Encode and decode a list many times. +//! +//! Useful for `cargo flamegraph`. + +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; + +#[derive(Clone, Copy, Encode, Decode)] +pub struct FixedLen { + a: u64, + b: u64, + c: u64, + d: u64, +} + +fn main() { + let fixed_len = FixedLen { + a: 42, + b: 42, + c: 42, + d: 42, + }; + + let vec: Vec = vec![fixed_len; 8196]; + + let output: Vec> = (0..40_000) + .map(|_| Vec::from_ssz_bytes(&vec.as_ssz_bytes()).unwrap()) + .collect(); + + println!("{}", output.len()); +} diff --git a/version-meld/eth2_ssz/examples/struct_definition.rs b/version-meld/eth2_ssz/examples/struct_definition.rs new file mode 100644 index 0000000..123da12 --- /dev/null +++ b/version-meld/eth2_ssz/examples/struct_definition.rs @@ -0,0 +1,73 @@ +use ssz::{Decode, DecodeError, Encode, SszDecoderBuilder, SszEncoder}; + +#[derive(Debug, PartialEq)] +pub struct Foo { + a: u16, + b: Vec, + c: u16, +} + +impl Encode for Foo { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() && as Encode>::is_ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + ::ssz_fixed_len() + + ssz::BYTES_PER_LENGTH_OFFSET + + ::ssz_fixed_len() + + self.b.ssz_bytes_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + let offset = ::ssz_fixed_len() + + as Encode>::ssz_fixed_len() + + ::ssz_fixed_len(); + + let mut encoder = SszEncoder::container(buf, offset); + + encoder.append(&self.a); + encoder.append(&self.b); + encoder.append(&self.c); + + encoder.finalize(); + } +} + +impl Decode for Foo { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() && as Decode>::is_ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let mut builder = SszDecoderBuilder::new(bytes); + + builder.register_type::()?; + builder.register_type::>()?; + builder.register_type::()?; + + let mut decoder = builder.build()?; + + Ok(Self { + a: decoder.decode_next()?, + b: decoder.decode_next()?, + c: decoder.decode_next()?, + }) + } +} + +fn main() { + let my_foo = Foo { + a: 42, + b: vec![0, 1, 2, 3], + c: 11, + }; + + let bytes = vec![42, 0, 8, 0, 0, 0, 11, 0, 0, 1, 2, 3]; + + assert_eq!(my_foo.as_ssz_bytes(), bytes); + + let decoded_foo = Foo::from_ssz_bytes(&bytes).unwrap(); + + assert_eq!(my_foo, decoded_foo); +} diff --git a/version-meld/eth2_ssz/src/decode.rs b/version-meld/eth2_ssz/src/decode.rs new file mode 100644 index 0000000..1c4c04f --- /dev/null +++ b/version-meld/eth2_ssz/src/decode.rs @@ -0,0 +1,372 @@ +use super::*; +use smallvec::{smallvec, SmallVec}; +use std::cmp::Ordering; + +type SmallVec8 = SmallVec<[T; 8]>; + +pub mod impls; + +/// Returned when SSZ decoding fails. +#[derive(Debug, PartialEq, Clone)] +pub enum DecodeError { + /// The bytes supplied were too short to be decoded into the specified type. + InvalidByteLength { len: usize, expected: usize }, + /// The given bytes were too short to be read as a length prefix. + InvalidLengthPrefix { len: usize, expected: usize }, + /// A length offset pointed to a byte that was out-of-bounds (OOB). + /// + /// A bytes may be OOB for the following reasons: + /// + /// - It is `>= bytes.len()`. + /// - When decoding variable length items, the 1st offset points "backwards" into the fixed + /// length items (i.e., `length[0] < BYTES_PER_LENGTH_OFFSET`). + /// - When decoding variable-length items, the `n`'th offset was less than the `n-1`'th offset. + OutOfBoundsByte { i: usize }, + /// An offset points “backwards” into the fixed-bytes portion of the message, essentially + /// double-decoding bytes that will also be decoded as fixed-length. + /// + /// https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view#1-Offset-into-fixed-portion + OffsetIntoFixedPortion(usize), + /// The first offset does not point to the byte that follows the fixed byte portion, + /// essentially skipping a variable-length byte. + /// + /// https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view#2-Skip-first-variable-byte + OffsetSkipsVariableBytes(usize), + /// An offset points to bytes prior to the previous offset. Depending on how you look at it, + /// this either double-decodes bytes or makes the first offset a negative-length. + /// + /// https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view#3-Offsets-are-decreasing + OffsetsAreDecreasing(usize), + /// An offset references byte indices that do not exist in the source bytes. + /// + /// https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view#4-Offsets-are-out-of-bounds + OffsetOutOfBounds(usize), + /// A variable-length list does not have a fixed portion that is cleanly divisible by + /// `BYTES_PER_LENGTH_OFFSET`. + InvalidListFixedBytesLen(usize), + /// Some item has a `ssz_fixed_len` of zero. This is illegal. + ZeroLengthItem, + /// The given bytes were invalid for some application-level reason. + BytesInvalid(String), + /// The given union selector is out of bounds. + UnionSelectorInvalid(u8), +} + +/// Performs checks on the `offset` based upon the other parameters provided. +/// +/// ## Detail +/// +/// - `offset`: the offset bytes (e.g., result of `read_offset(..)`). +/// - `previous_offset`: unless this is the first offset in the SSZ object, the value of the +/// previously-read offset. Used to ensure offsets are not decreasing. +/// - `num_bytes`: the total number of bytes in the SSZ object. Used to ensure the offset is not +/// out of bounds. +/// - `num_fixed_bytes`: the number of fixed-bytes in the struct, if it is known. Used to ensure +/// that the first offset doesn't skip any variable bytes. +/// +/// ## References +/// +/// The checks here are derived from this document: +/// +/// https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view +pub fn sanitize_offset( + offset: usize, + previous_offset: Option, + num_bytes: usize, + num_fixed_bytes: Option, +) -> Result { + if num_fixed_bytes.map_or(false, |fixed_bytes| offset < fixed_bytes) { + Err(DecodeError::OffsetIntoFixedPortion(offset)) + } else if previous_offset.is_none() + && num_fixed_bytes.map_or(false, |fixed_bytes| offset != fixed_bytes) + { + Err(DecodeError::OffsetSkipsVariableBytes(offset)) + } else if offset > num_bytes { + Err(DecodeError::OffsetOutOfBounds(offset)) + } else if previous_offset.map_or(false, |prev| prev > offset) { + Err(DecodeError::OffsetsAreDecreasing(offset)) + } else { + Ok(offset) + } +} + +/// Provides SSZ decoding (de-serialization) via the `from_ssz_bytes(&bytes)` method. +/// +/// See `examples/` for manual implementations or the crate root for implementations using +/// `#[derive(Decode)]`. +pub trait Decode: Sized { + /// Returns `true` if this object has a fixed-length. + /// + /// I.e., there are no variable length items in this object or any of it's contained objects. + fn is_ssz_fixed_len() -> bool; + + /// The number of bytes this object occupies in the fixed-length portion of the SSZ bytes. + /// + /// By default, this is set to `BYTES_PER_LENGTH_OFFSET` which is suitable for variable length + /// objects, but not fixed-length objects. Fixed-length objects _must_ return a value which + /// represents their length. + fn ssz_fixed_len() -> usize { + BYTES_PER_LENGTH_OFFSET + } + + /// Attempts to decode `Self` from `bytes`, returning a `DecodeError` on failure. + /// + /// The supplied bytes must be the exact length required to decode `Self`, excess bytes will + /// result in an error. + fn from_ssz_bytes(bytes: &[u8]) -> Result; +} + +#[derive(Copy, Clone, Debug)] +pub struct Offset { + position: usize, + offset: usize, +} + +/// Builds an `SszDecoder`. +/// +/// The purpose of this struct is to split some SSZ bytes into individual slices. The builder is +/// then converted into a `SszDecoder` which decodes those values into object instances. +/// +/// See [`SszDecoder`](struct.SszDecoder.html) for usage examples. +pub struct SszDecoderBuilder<'a> { + bytes: &'a [u8], + items: SmallVec8<&'a [u8]>, + offsets: SmallVec8, + items_index: usize, +} + +impl<'a> SszDecoderBuilder<'a> { + /// Instantiate a new builder that should build a `SszDecoder` over the given `bytes` which + /// are assumed to be the SSZ encoding of some object. + pub fn new(bytes: &'a [u8]) -> Self { + Self { + bytes, + items: smallvec![], + offsets: smallvec![], + items_index: 0, + } + } + + /// Registers a variable-length object as the next item in `bytes`, without specifying the + /// actual type. + /// + /// ## Notes + /// + /// Use of this function is generally discouraged since it cannot detect if some type changes + /// from variable to fixed length. + /// + /// Use `Self::register_type` wherever possible. + pub fn register_anonymous_variable_length_item(&mut self) -> Result<(), DecodeError> { + struct Anonymous; + + impl Decode for Anonymous { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(_bytes: &[u8]) -> Result { + unreachable!("Anonymous should never be decoded") + } + } + + self.register_type::() + } + + /// Declares that some type `T` is the next item in `bytes`. + pub fn register_type(&mut self) -> Result<(), DecodeError> { + self.register_type_parameterized(T::is_ssz_fixed_len(), T::ssz_fixed_len()) + } + + /// Declares that a type with the given parameters is the next item in `bytes`. + pub fn register_type_parameterized( + &mut self, + is_ssz_fixed_len: bool, + ssz_fixed_len: usize, + ) -> Result<(), DecodeError> { + if is_ssz_fixed_len { + let start = self.items_index; + self.items_index += ssz_fixed_len; + + let slice = self.bytes.get(start..self.items_index).ok_or_else(|| { + DecodeError::InvalidByteLength { + len: self.bytes.len(), + expected: self.items_index, + } + })?; + + self.items.push(slice); + } else { + self.offsets.push(Offset { + position: self.items.len(), + offset: sanitize_offset( + read_offset(&self.bytes[self.items_index..])?, + self.offsets.last().map(|o| o.offset), + self.bytes.len(), + None, + )?, + }); + + // Push an empty slice into items; it will be replaced later. + self.items.push(&[]); + + self.items_index += BYTES_PER_LENGTH_OFFSET; + } + + Ok(()) + } + + fn finalize(&mut self) -> Result<(), DecodeError> { + if let Some(first_offset) = self.offsets.first().map(|o| o.offset) { + // Check to ensure the first offset points to the byte immediately following the + // fixed-length bytes. + match first_offset.cmp(&self.items_index) { + Ordering::Less => return Err(DecodeError::OffsetIntoFixedPortion(first_offset)), + Ordering::Greater => { + return Err(DecodeError::OffsetSkipsVariableBytes(first_offset)) + } + Ordering::Equal => (), + } + + // Iterate through each pair of offsets, grabbing the slice between each of the offsets. + for pair in self.offsets.windows(2) { + let a = pair[0]; + let b = pair[1]; + + self.items[a.position] = &self.bytes[a.offset..b.offset]; + } + + // Handle the last offset, pushing a slice from it's start through to the end of + // `self.bytes`. + if let Some(last) = self.offsets.last() { + self.items[last.position] = &self.bytes[last.offset..] + } + } else { + // If the container is fixed-length, ensure there are no excess bytes. + if self.items_index != self.bytes.len() { + return Err(DecodeError::InvalidByteLength { + len: self.bytes.len(), + expected: self.items_index, + }); + } + } + + Ok(()) + } + + /// Finalizes the builder, returning a `SszDecoder` that may be used to instantiate objects. + pub fn build(mut self) -> Result, DecodeError> { + self.finalize()?; + + Ok(SszDecoder { items: self.items }) + } +} + +/// Decodes some slices of SSZ into object instances. Should be instantiated using +/// [`SszDecoderBuilder`](struct.SszDecoderBuilder.html). +/// +/// ## Example +/// +/// ```rust +/// use ssz_derive::{Encode, Decode}; +/// use ssz::{Decode, Encode, SszDecoder, SszDecoderBuilder}; +/// +/// #[derive(PartialEq, Debug, Encode, Decode)] +/// struct Foo { +/// a: u64, +/// b: Vec, +/// } +/// +/// fn ssz_decoding_example() { +/// let foo = Foo { +/// a: 42, +/// b: vec![1, 3, 3, 7] +/// }; +/// +/// let bytes = foo.as_ssz_bytes(); +/// +/// let mut builder = SszDecoderBuilder::new(&bytes); +/// +/// builder.register_type::().unwrap(); +/// builder.register_type::>().unwrap(); +/// +/// let mut decoder = builder.build().unwrap(); +/// +/// let decoded_foo = Foo { +/// a: decoder.decode_next().unwrap(), +/// b: decoder.decode_next().unwrap(), +/// }; +/// +/// assert_eq!(foo, decoded_foo); +/// } +/// +/// ``` +pub struct SszDecoder<'a> { + items: SmallVec8<&'a [u8]>, +} + +impl<'a> SszDecoder<'a> { + /// Decodes the next item. + /// + /// # Panics + /// + /// Panics when attempting to decode more items than actually exist. + pub fn decode_next(&mut self) -> Result { + self.decode_next_with(|slice| T::from_ssz_bytes(slice)) + } + + /// Decodes the next item using the provided function. + pub fn decode_next_with(&mut self, f: F) -> Result + where + F: FnOnce(&'a [u8]) -> Result, + { + f(self.items.remove(0)) + } +} + +/// Takes `bytes`, assuming it is the encoding for a SSZ union, and returns the union-selector and +/// the body (trailing bytes). +/// +/// ## Errors +/// +/// Returns an error if: +/// +/// - `bytes` is empty. +/// - the union selector is not a valid value (i.e., larger than the maximum number of variants. +pub fn split_union_bytes(bytes: &[u8]) -> Result<(UnionSelector, &[u8]), DecodeError> { + let selector = bytes + .first() + .copied() + .ok_or(DecodeError::OutOfBoundsByte { i: 0 }) + .and_then(UnionSelector::new)?; + let body = bytes + .get(1..) + .ok_or(DecodeError::OutOfBoundsByte { i: 1 })?; + Ok((selector, body)) +} + +/// Reads a `BYTES_PER_LENGTH_OFFSET`-byte length from `bytes`, where `bytes.len() >= +/// BYTES_PER_LENGTH_OFFSET`. +pub fn read_offset(bytes: &[u8]) -> Result { + decode_offset(bytes.get(0..BYTES_PER_LENGTH_OFFSET).ok_or_else(|| { + DecodeError::InvalidLengthPrefix { + len: bytes.len(), + expected: BYTES_PER_LENGTH_OFFSET, + } + })?) +} + +/// Decode bytes as a little-endian usize, returning an `Err` if `bytes.len() != +/// BYTES_PER_LENGTH_OFFSET`. +fn decode_offset(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = BYTES_PER_LENGTH_OFFSET; + + if len != expected { + Err(DecodeError::InvalidLengthPrefix { len, expected }) + } else { + let mut array: [u8; BYTES_PER_LENGTH_OFFSET] = std::default::Default::default(); + array.clone_from_slice(bytes); + + Ok(u32::from_le_bytes(array) as usize) + } +} diff --git a/version-meld/eth2_ssz/src/decode/impls.rs b/version-meld/eth2_ssz/src/decode/impls.rs new file mode 100644 index 0000000..29b2aec --- /dev/null +++ b/version-meld/eth2_ssz/src/decode/impls.rs @@ -0,0 +1,697 @@ +use super::*; +use core::num::NonZeroUsize; +use ethereum_types::{H256, U128, U256}; +use smallvec::SmallVec; +use std::sync::Arc; + +macro_rules! impl_decodable_for_uint { + ($type: ident, $bit_size: expr) => { + impl Decode for $type { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + $bit_size / 8 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + let mut array: [u8; $bit_size / 8] = std::default::Default::default(); + array.clone_from_slice(bytes); + + Ok(Self::from_le_bytes(array)) + } + } + } + }; +} + +impl_decodable_for_uint!(u8, 8); +impl_decodable_for_uint!(u16, 16); +impl_decodable_for_uint!(u32, 32); +impl_decodable_for_uint!(u64, 64); + +#[cfg(target_pointer_width = "32")] +impl_decodable_for_uint!(usize, 32); + +#[cfg(target_pointer_width = "64")] +impl_decodable_for_uint!(usize, 64); + +macro_rules! impl_decode_for_tuples { + ($( + $Tuple:ident { + $(($idx:tt) -> $T:ident)+ + } + )+) => { + $( + impl<$($T: Decode),+> Decode for ($($T,)+) { + fn is_ssz_fixed_len() -> bool { + $( + <$T as Decode>::is_ssz_fixed_len() && + )* + true + } + + fn ssz_fixed_len() -> usize { + if ::is_ssz_fixed_len() { + $( + <$T as Decode>::ssz_fixed_len() + + )* + 0 + } else { + BYTES_PER_LENGTH_OFFSET + } + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let mut builder = SszDecoderBuilder::new(bytes); + + $( + builder.register_type::<$T>()?; + )* + + let mut decoder = builder.build()?; + + Ok(($( + decoder.decode_next::<$T>()?, + )* + )) + } + } + )+ + } +} + +impl_decode_for_tuples! { + Tuple2 { + (0) -> A + (1) -> B + } + Tuple3 { + (0) -> A + (1) -> B + (2) -> C + } + Tuple4 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + } + Tuple5 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + } + Tuple6 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + } + Tuple7 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + } + Tuple8 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + } + Tuple9 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + } + Tuple10 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + } + Tuple11 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + } + Tuple12 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + (11) -> L + } +} + +impl Decode for bool { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 1 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + match bytes[0] { + 0b0000_0000 => Ok(false), + 0b0000_0001 => Ok(true), + _ => Err(DecodeError::BytesInvalid(format!( + "Out-of-range for boolean: {}", + bytes[0] + ))), + } + } + } +} + +impl Decode for NonZeroUsize { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + ::ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let x = usize::from_ssz_bytes(bytes)?; + + if x == 0 { + Err(DecodeError::BytesInvalid( + "NonZeroUsize cannot be zero.".to_string(), + )) + } else { + // `unwrap` is safe here as `NonZeroUsize::new()` succeeds if `x > 0` and this path + // never executes when `x == 0`. + Ok(NonZeroUsize::new(x).unwrap()) + } + } +} + +impl Decode for Arc { + fn is_ssz_fixed_len() -> bool { + T::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + T::ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + T::from_ssz_bytes(bytes).map(Arc::new) + } +} + +impl Decode for H256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + Ok(H256::from_slice(bytes)) + } + } +} + +impl Decode for U256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + Ok(U256::from_little_endian(bytes)) + } + } +} + +impl Decode for U128 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 16 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + Ok(U128::from_little_endian(bytes)) + } + } +} + +macro_rules! impl_decodable_for_u8_array { + ($len: expr) => { + impl Decode for [u8; $len] { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + $len + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + let mut array: [u8; $len] = [0; $len]; + array.copy_from_slice(bytes); + + Ok(array) + } + } + } + }; +} + +impl_decodable_for_u8_array!(4); +impl_decodable_for_u8_array!(32); + +macro_rules! impl_for_vec { + ($type: ty, $max_len: expr) => { + impl Decode for $type { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.is_empty() { + Ok(vec![].into()) + } else if T::is_ssz_fixed_len() { + bytes + .chunks(T::ssz_fixed_len()) + .map(|chunk| T::from_ssz_bytes(chunk)) + .collect() + } else { + decode_list_of_variable_length_items(bytes, $max_len).map(|vec| vec.into()) + } + } + } + }; +} + +impl_for_vec!(Vec, None); +impl_for_vec!(SmallVec<[T; 1]>, Some(1)); +impl_for_vec!(SmallVec<[T; 2]>, Some(2)); +impl_for_vec!(SmallVec<[T; 3]>, Some(3)); +impl_for_vec!(SmallVec<[T; 4]>, Some(4)); +impl_for_vec!(SmallVec<[T; 5]>, Some(5)); +impl_for_vec!(SmallVec<[T; 6]>, Some(6)); +impl_for_vec!(SmallVec<[T; 7]>, Some(7)); +impl_for_vec!(SmallVec<[T; 8]>, Some(8)); + +/// Decodes `bytes` as if it were a list of variable-length items. +/// +/// The `ssz::SszDecoder` can also perform this functionality, however it it significantly faster +/// as it is optimized to read same-typed items whilst `ssz::SszDecoder` supports reading items of +/// differing types. +pub fn decode_list_of_variable_length_items( + bytes: &[u8], + max_len: Option, +) -> Result, DecodeError> { + if bytes.is_empty() { + return Ok(vec![]); + } + + let first_offset = read_offset(bytes)?; + sanitize_offset(first_offset, None, bytes.len(), Some(first_offset))?; + + if first_offset % BYTES_PER_LENGTH_OFFSET != 0 || first_offset < BYTES_PER_LENGTH_OFFSET { + return Err(DecodeError::InvalidListFixedBytesLen(first_offset)); + } + + let num_items = first_offset / BYTES_PER_LENGTH_OFFSET; + + if max_len.map_or(false, |max| num_items > max) { + return Err(DecodeError::BytesInvalid(format!( + "Variable length list of {} items exceeds maximum of {:?}", + num_items, max_len + ))); + } + + // Only initialize the vec with a capacity if a maximum length is provided. + // + // We assume that if a max length is provided then the application is able to handle an + // allocation of this size. + let mut values = if max_len.is_some() { + Vec::with_capacity(num_items) + } else { + vec![] + }; + + let mut offset = first_offset; + for i in 1..=num_items { + let slice_option = if i == num_items { + bytes.get(offset..) + } else { + let start = offset; + + let next_offset = read_offset(&bytes[(i * BYTES_PER_LENGTH_OFFSET)..])?; + offset = sanitize_offset(next_offset, Some(offset), bytes.len(), Some(first_offset))?; + + bytes.get(start..offset) + }; + + let slice = slice_option.ok_or(DecodeError::OutOfBoundsByte { i: offset })?; + + values.push(T::from_ssz_bytes(slice)?); + } + + Ok(values) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Note: decoding of valid bytes is generally tested "indirectly" in the `/tests` dir, by + // encoding then decoding the element. + + #[test] + fn invalid_u8_array_4() { + assert_eq!( + <[u8; 4]>::from_ssz_bytes(&[0; 3]), + Err(DecodeError::InvalidByteLength { + len: 3, + expected: 4 + }) + ); + + assert_eq!( + <[u8; 4]>::from_ssz_bytes(&[0; 5]), + Err(DecodeError::InvalidByteLength { + len: 5, + expected: 4 + }) + ); + } + + #[test] + fn invalid_bool() { + assert_eq!( + bool::from_ssz_bytes(&[0; 2]), + Err(DecodeError::InvalidByteLength { + len: 2, + expected: 1 + }) + ); + + assert_eq!( + bool::from_ssz_bytes(&[]), + Err(DecodeError::InvalidByteLength { + len: 0, + expected: 1 + }) + ); + + if let Err(DecodeError::BytesInvalid(_)) = bool::from_ssz_bytes(&[2]) { + // Success. + } else { + panic!("Did not return error on invalid bool val") + } + } + + #[test] + fn invalid_h256() { + assert_eq!( + H256::from_ssz_bytes(&[0; 33]), + Err(DecodeError::InvalidByteLength { + len: 33, + expected: 32 + }) + ); + + assert_eq!( + H256::from_ssz_bytes(&[0; 31]), + Err(DecodeError::InvalidByteLength { + len: 31, + expected: 32 + }) + ); + } + + #[test] + fn empty_list() { + let vec: Vec> = vec![]; + let bytes = vec.as_ssz_bytes(); + assert!(bytes.is_empty()); + assert_eq!(Vec::from_ssz_bytes(&bytes), Ok(vec),); + } + + #[test] + fn first_length_points_backwards() { + assert_eq!( + >>::from_ssz_bytes(&[0, 0, 0, 0]), + Err(DecodeError::InvalidListFixedBytesLen(0)) + ); + + assert_eq!( + >>::from_ssz_bytes(&[1, 0, 0, 0]), + Err(DecodeError::InvalidListFixedBytesLen(1)) + ); + + assert_eq!( + >>::from_ssz_bytes(&[2, 0, 0, 0]), + Err(DecodeError::InvalidListFixedBytesLen(2)) + ); + + assert_eq!( + >>::from_ssz_bytes(&[3, 0, 0, 0]), + Err(DecodeError::InvalidListFixedBytesLen(3)) + ); + } + + #[test] + fn lengths_are_decreasing() { + assert_eq!( + >>::from_ssz_bytes(&[12, 0, 0, 0, 14, 0, 0, 0, 12, 0, 0, 0, 1, 0, 1, 0]), + Err(DecodeError::OffsetsAreDecreasing(12)) + ); + } + + #[test] + fn awkward_fixed_length_portion() { + assert_eq!( + >>::from_ssz_bytes(&[10, 0, 0, 0, 10, 0, 0, 0, 0, 0]), + Err(DecodeError::InvalidListFixedBytesLen(10)) + ); + } + + #[test] + fn length_out_of_bounds() { + assert_eq!( + >>::from_ssz_bytes(&[5, 0, 0, 0]), + Err(DecodeError::OffsetOutOfBounds(5)) + ); + assert_eq!( + >>::from_ssz_bytes(&[8, 0, 0, 0, 9, 0, 0, 0]), + Err(DecodeError::OffsetOutOfBounds(9)) + ); + assert_eq!( + >>::from_ssz_bytes(&[8, 0, 0, 0, 16, 0, 0, 0]), + Err(DecodeError::OffsetOutOfBounds(16)) + ); + } + + #[test] + fn vec_of_vec_of_u16() { + assert_eq!( + >>::from_ssz_bytes(&[4, 0, 0, 0]), + Ok(vec![vec![]]) + ); + + assert_eq!( + >::from_ssz_bytes(&[0, 0, 1, 0, 2, 0, 3, 0]), + Ok(vec![0, 1, 2, 3]) + ); + assert_eq!(::from_ssz_bytes(&[16, 0]), Ok(16)); + assert_eq!(::from_ssz_bytes(&[0, 1]), Ok(256)); + assert_eq!(::from_ssz_bytes(&[255, 255]), Ok(65535)); + + assert_eq!( + ::from_ssz_bytes(&[255]), + Err(DecodeError::InvalidByteLength { + len: 1, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[]), + Err(DecodeError::InvalidByteLength { + len: 0, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[0, 1, 2]), + Err(DecodeError::InvalidByteLength { + len: 3, + expected: 2 + }) + ); + } + + #[test] + fn vec_of_u16() { + assert_eq!(>::from_ssz_bytes(&[0, 0, 0, 0]), Ok(vec![0, 0])); + assert_eq!( + >::from_ssz_bytes(&[0, 0, 1, 0, 2, 0, 3, 0]), + Ok(vec![0, 1, 2, 3]) + ); + assert_eq!(::from_ssz_bytes(&[16, 0]), Ok(16)); + assert_eq!(::from_ssz_bytes(&[0, 1]), Ok(256)); + assert_eq!(::from_ssz_bytes(&[255, 255]), Ok(65535)); + + assert_eq!( + ::from_ssz_bytes(&[255]), + Err(DecodeError::InvalidByteLength { + len: 1, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[]), + Err(DecodeError::InvalidByteLength { + len: 0, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[0, 1, 2]), + Err(DecodeError::InvalidByteLength { + len: 3, + expected: 2 + }) + ); + } + + #[test] + fn u16() { + assert_eq!(::from_ssz_bytes(&[0, 0]), Ok(0)); + assert_eq!(::from_ssz_bytes(&[16, 0]), Ok(16)); + assert_eq!(::from_ssz_bytes(&[0, 1]), Ok(256)); + assert_eq!(::from_ssz_bytes(&[255, 255]), Ok(65535)); + + assert_eq!( + ::from_ssz_bytes(&[255]), + Err(DecodeError::InvalidByteLength { + len: 1, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[]), + Err(DecodeError::InvalidByteLength { + len: 0, + expected: 2 + }) + ); + + assert_eq!( + ::from_ssz_bytes(&[0, 1, 2]), + Err(DecodeError::InvalidByteLength { + len: 3, + expected: 2 + }) + ); + } + + #[test] + fn tuple() { + assert_eq!(<(u16, u16)>::from_ssz_bytes(&[0, 0, 0, 0]), Ok((0, 0))); + assert_eq!(<(u16, u16)>::from_ssz_bytes(&[16, 0, 17, 0]), Ok((16, 17))); + assert_eq!(<(u16, u16)>::from_ssz_bytes(&[0, 1, 2, 0]), Ok((256, 2))); + assert_eq!( + <(u16, u16)>::from_ssz_bytes(&[255, 255, 0, 0]), + Ok((65535, 0)) + ); + } +} diff --git a/version-meld/eth2_ssz/src/encode.rs b/version-meld/eth2_ssz/src/encode.rs new file mode 100644 index 0000000..cecd615 --- /dev/null +++ b/version-meld/eth2_ssz/src/encode.rs @@ -0,0 +1,196 @@ +use super::*; + +mod impls; + +/// Provides SSZ encoding (serialization) via the `as_ssz_bytes(&self)` method. +/// +/// See `examples/` for manual implementations or the crate root for implementations using +/// `#[derive(Encode)]`. +pub trait Encode { + /// Returns `true` if this object has a fixed-length. + /// + /// I.e., there are no variable length items in this object or any of it's contained objects. + fn is_ssz_fixed_len() -> bool; + + /// Append the encoding `self` to `buf`. + /// + /// Note, variable length objects need only to append their "variable length" portion, they do + /// not need to provide their offset. + fn ssz_append(&self, buf: &mut Vec); + + /// The number of bytes this object occupies in the fixed-length portion of the SSZ bytes. + /// + /// By default, this is set to `BYTES_PER_LENGTH_OFFSET` which is suitable for variable length + /// objects, but not fixed-length objects. Fixed-length objects _must_ return a value which + /// represents their length. + fn ssz_fixed_len() -> usize { + BYTES_PER_LENGTH_OFFSET + } + + /// Returns the size (in bytes) when `self` is serialized. + /// + /// Returns the same value as `self.as_ssz_bytes().len()` but this method is significantly more + /// efficient. + fn ssz_bytes_len(&self) -> usize; + + /// Returns the full-form encoding of this object. + /// + /// The default implementation of this method should suffice for most cases. + fn as_ssz_bytes(&self) -> Vec { + let mut buf = vec![]; + + self.ssz_append(&mut buf); + + buf + } +} + +/// Allow for encoding an ordered series of distinct or indistinct objects as SSZ bytes. +/// +/// **You must call `finalize(..)` after the final `append(..)` call** to ensure the bytes are +/// written to `buf`. +/// +/// ## Example +/// +/// Use `SszEncoder` to produce identical output to `foo.as_ssz_bytes()`: +/// +/// ```rust +/// use ssz_derive::{Encode, Decode}; +/// use ssz::{Decode, Encode, SszEncoder}; +/// +/// #[derive(PartialEq, Debug, Encode, Decode)] +/// struct Foo { +/// a: u64, +/// b: Vec, +/// } +/// +/// fn ssz_encode_example() { +/// let foo = Foo { +/// a: 42, +/// b: vec![1, 3, 3, 7] +/// }; +/// +/// let mut buf: Vec = vec![]; +/// let offset = ::ssz_fixed_len() + as Encode>::ssz_fixed_len(); +/// +/// let mut encoder = SszEncoder::container(&mut buf, offset); +/// +/// encoder.append(&foo.a); +/// encoder.append(&foo.b); +/// +/// encoder.finalize(); +/// +/// assert_eq!(foo.as_ssz_bytes(), buf); +/// } +/// +/// ``` +pub struct SszEncoder<'a> { + offset: usize, + buf: &'a mut Vec, + variable_bytes: Vec, +} + +impl<'a> SszEncoder<'a> { + /// Instantiate a new encoder for encoding a SSZ container. + pub fn container(buf: &'a mut Vec, num_fixed_bytes: usize) -> Self { + buf.reserve(num_fixed_bytes); + + Self { + offset: num_fixed_bytes, + buf, + variable_bytes: vec![], + } + } + + /// Append some `item` to the SSZ bytes. + pub fn append(&mut self, item: &T) { + self.append_parameterized(T::is_ssz_fixed_len(), |buf| item.ssz_append(buf)) + } + + /// Uses `ssz_append` to append the encoding of some item to the SSZ bytes. + pub fn append_parameterized(&mut self, is_ssz_fixed_len: bool, ssz_append: F) + where + F: Fn(&mut Vec), + { + if is_ssz_fixed_len { + ssz_append(&mut self.buf); + } else { + self.buf + .extend_from_slice(&encode_length(self.offset + self.variable_bytes.len())); + + ssz_append(&mut self.variable_bytes); + } + } + + /// Write the variable bytes to `self.bytes`. + /// + /// This method must be called after the final `append(..)` call when serializing + /// variable-length items. + pub fn finalize(&mut self) -> &mut Vec { + self.buf.append(&mut self.variable_bytes); + + &mut self.buf + } +} + +/// Encode `len` as a little-endian byte array of `BYTES_PER_LENGTH_OFFSET` length. +/// +/// If `len` is larger than `2 ^ BYTES_PER_LENGTH_OFFSET`, a `debug_assert` is raised. +pub fn encode_length(len: usize) -> [u8; BYTES_PER_LENGTH_OFFSET] { + // Note: it is possible for `len` to be larger than what can be encoded in + // `BYTES_PER_LENGTH_OFFSET` bytes, triggering this debug assertion. + // + // These are the alternatives to using a `debug_assert` here: + // + // 1. Use `assert`. + // 2. Push an error to the caller (e.g., `Option` or `Result`). + // 3. Ignore it completely. + // + // I have avoided (1) because it's basically a choice between "produce invalid SSZ" or "kill + // the entire program". I figure it may be possible for an attacker to trigger this assert and + // take the program down -- I think producing invalid SSZ is a better option than this. + // + // I have avoided (2) because this error will need to be propagated upstream, making encoding a + // function which may fail. I don't think this is ergonomic and the upsides don't outweigh the + // downsides. + // + // I figure a `debug_assertion` is better than (3) as it will give us a change to detect the + // error during testing. + // + // If you have a different opinion, feel free to start an issue and tag @paulhauner. + debug_assert!(len <= MAX_LENGTH_VALUE); + + let mut bytes = [0; BYTES_PER_LENGTH_OFFSET]; + bytes.copy_from_slice(&len.to_le_bytes()[0..BYTES_PER_LENGTH_OFFSET]); + bytes +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encode_length() { + assert_eq!(encode_length(0), [0; 4]); + + assert_eq!(encode_length(1), [1, 0, 0, 0]); + + assert_eq!( + encode_length(MAX_LENGTH_VALUE), + [255; BYTES_PER_LENGTH_OFFSET] + ); + } + + #[test] + #[should_panic] + #[cfg(debug_assertions)] + fn test_encode_length_above_max_debug_panics() { + encode_length(MAX_LENGTH_VALUE + 1); + } + + #[test] + #[cfg(not(debug_assertions))] + fn test_encode_length_above_max_not_debug_does_not_panic() { + assert_eq!(&encode_length(MAX_LENGTH_VALUE + 1)[..], &[0; 4]); + } +} diff --git a/version-meld/eth2_ssz/src/encode/impls.rs b/version-meld/eth2_ssz/src/encode/impls.rs new file mode 100644 index 0000000..00d3e0a --- /dev/null +++ b/version-meld/eth2_ssz/src/encode/impls.rs @@ -0,0 +1,504 @@ +use super::*; +use core::num::NonZeroUsize; +use ethereum_types::{H256, U128, U256}; +use smallvec::SmallVec; +use std::sync::Arc; + +macro_rules! impl_encodable_for_uint { + ($type: ident, $bit_size: expr) => { + impl Encode for $type { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + $bit_size / 8 + } + + fn ssz_bytes_len(&self) -> usize { + $bit_size / 8 + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.to_le_bytes()); + } + } + }; +} + +impl_encodable_for_uint!(u8, 8); +impl_encodable_for_uint!(u16, 16); +impl_encodable_for_uint!(u32, 32); +impl_encodable_for_uint!(u64, 64); + +#[cfg(target_pointer_width = "32")] +impl_encodable_for_uint!(usize, 32); + +#[cfg(target_pointer_width = "64")] +impl_encodable_for_uint!(usize, 64); + +// Based on the `tuple_impls` macro from the standard library. +macro_rules! impl_encode_for_tuples { + ($( + $Tuple:ident { + $(($idx:tt) -> $T:ident)+ + } + )+) => { + $( + impl<$($T: Encode),+> Encode for ($($T,)+) { + fn is_ssz_fixed_len() -> bool { + $( + <$T as Encode>::is_ssz_fixed_len() && + )* + true + } + + fn ssz_fixed_len() -> usize { + if ::is_ssz_fixed_len() { + $( + <$T as Encode>::ssz_fixed_len() + + )* + 0 + } else { + BYTES_PER_LENGTH_OFFSET + } + } + + fn ssz_bytes_len(&self) -> usize { + if ::is_ssz_fixed_len() { + ::ssz_fixed_len() + } else { + let mut len = 0; + $( + len += if <$T as Encode>::is_ssz_fixed_len() { + <$T as Encode>::ssz_fixed_len() + } else { + BYTES_PER_LENGTH_OFFSET + + self.$idx.ssz_bytes_len() + }; + )* + len + } + } + + fn ssz_append(&self, buf: &mut Vec) { + let offset = $( + <$T as Encode>::ssz_fixed_len() + + )* + 0; + + let mut encoder = SszEncoder::container(buf, offset); + + $( + encoder.append(&self.$idx); + )* + + encoder.finalize(); + } + } + )+ + } +} + +impl_encode_for_tuples! { + Tuple2 { + (0) -> A + (1) -> B + } + Tuple3 { + (0) -> A + (1) -> B + (2) -> C + } + Tuple4 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + } + Tuple5 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + } + Tuple6 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + } + Tuple7 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + } + Tuple8 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + } + Tuple9 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + } + Tuple10 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + } + Tuple11 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + } + Tuple12 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + (11) -> L + } +} + +impl Encode for Arc { + fn is_ssz_fixed_len() -> bool { + T::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + T::ssz_fixed_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.as_ref().ssz_append(buf) + } + + fn ssz_bytes_len(&self) -> usize { + self.as_ref().ssz_bytes_len() + } +} + +macro_rules! impl_for_vec { + ($type: ty) => { + impl Encode for $type { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_bytes_len(&self) -> usize { + if ::is_ssz_fixed_len() { + ::ssz_fixed_len() * self.len() + } else { + let mut len = self.iter().map(|item| item.ssz_bytes_len()).sum(); + len += BYTES_PER_LENGTH_OFFSET * self.len(); + len + } + } + + fn ssz_append(&self, buf: &mut Vec) { + if T::is_ssz_fixed_len() { + buf.reserve(T::ssz_fixed_len() * self.len()); + + for item in self { + item.ssz_append(buf); + } + } else { + let mut encoder = + SszEncoder::container(buf, self.len() * BYTES_PER_LENGTH_OFFSET); + + for item in self { + encoder.append(item); + } + + encoder.finalize(); + } + } + } + }; +} + +impl_for_vec!(Vec); +impl_for_vec!(SmallVec<[T; 1]>); +impl_for_vec!(SmallVec<[T; 2]>); +impl_for_vec!(SmallVec<[T; 3]>); +impl_for_vec!(SmallVec<[T; 4]>); +impl_for_vec!(SmallVec<[T; 5]>); +impl_for_vec!(SmallVec<[T; 6]>); +impl_for_vec!(SmallVec<[T; 7]>); +impl_for_vec!(SmallVec<[T; 8]>); + +impl Encode for bool { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 1 + } + + fn ssz_bytes_len(&self) -> usize { + 1 + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&(*self as u8).to_le_bytes()); + } +} + +impl Encode for NonZeroUsize { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + ::ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + std::mem::size_of::() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.get().ssz_append(buf) + } +} + +impl Encode for H256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn ssz_bytes_len(&self) -> usize { + 32 + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(self.as_bytes()); + } +} + +impl Encode for U256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn ssz_bytes_len(&self) -> usize { + 32 + } + + fn ssz_append(&self, buf: &mut Vec) { + let n = ::ssz_fixed_len(); + let s = buf.len(); + + buf.resize(s + n, 0); + self.to_little_endian(&mut buf[s..]); + } +} + +impl Encode for U128 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 16 + } + + fn ssz_bytes_len(&self) -> usize { + 16 + } + + fn ssz_append(&self, buf: &mut Vec) { + let n = ::ssz_fixed_len(); + let s = buf.len(); + + buf.resize(s + n, 0); + self.to_little_endian(&mut buf[s..]); + } +} + +macro_rules! impl_encodable_for_u8_array { + ($len: expr) => { + impl Encode for [u8; $len] { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + $len + } + + fn ssz_bytes_len(&self) -> usize { + $len + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self[..]); + } + } + }; +} + +impl_encodable_for_u8_array!(4); +impl_encodable_for_u8_array!(32); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vec_of_u8() { + let vec: Vec = vec![]; + assert_eq!(vec.as_ssz_bytes(), vec![]); + + let vec: Vec = vec![1]; + assert_eq!(vec.as_ssz_bytes(), vec![1]); + + let vec: Vec = vec![0, 1, 2, 3]; + assert_eq!(vec.as_ssz_bytes(), vec![0, 1, 2, 3]); + } + + #[test] + fn vec_of_vec_of_u8() { + let vec: Vec> = vec![]; + assert_eq!(vec.as_ssz_bytes(), vec![]); + + let vec: Vec> = vec![vec![]]; + assert_eq!(vec.as_ssz_bytes(), vec![4, 0, 0, 0]); + + let vec: Vec> = vec![vec![], vec![]]; + assert_eq!(vec.as_ssz_bytes(), vec![8, 0, 0, 0, 8, 0, 0, 0]); + + let vec: Vec> = vec![vec![0, 1, 2], vec![11, 22, 33]]; + assert_eq!( + vec.as_ssz_bytes(), + vec![8, 0, 0, 0, 11, 0, 0, 0, 0, 1, 2, 11, 22, 33] + ); + } + + #[test] + fn ssz_encode_u8() { + assert_eq!(0_u8.as_ssz_bytes(), vec![0]); + assert_eq!(1_u8.as_ssz_bytes(), vec![1]); + assert_eq!(100_u8.as_ssz_bytes(), vec![100]); + assert_eq!(255_u8.as_ssz_bytes(), vec![255]); + } + + #[test] + fn ssz_encode_u16() { + assert_eq!(1_u16.as_ssz_bytes(), vec![1, 0]); + assert_eq!(100_u16.as_ssz_bytes(), vec![100, 0]); + assert_eq!((1_u16 << 8).as_ssz_bytes(), vec![0, 1]); + assert_eq!(65535_u16.as_ssz_bytes(), vec![255, 255]); + } + + #[test] + fn ssz_encode_u32() { + assert_eq!(1_u32.as_ssz_bytes(), vec![1, 0, 0, 0]); + assert_eq!(100_u32.as_ssz_bytes(), vec![100, 0, 0, 0]); + assert_eq!((1_u32 << 16).as_ssz_bytes(), vec![0, 0, 1, 0]); + assert_eq!((1_u32 << 24).as_ssz_bytes(), vec![0, 0, 0, 1]); + assert_eq!((!0_u32).as_ssz_bytes(), vec![255, 255, 255, 255]); + } + + #[test] + fn ssz_encode_u64() { + assert_eq!(1_u64.as_ssz_bytes(), vec![1, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!( + (!0_u64).as_ssz_bytes(), + vec![255, 255, 255, 255, 255, 255, 255, 255] + ); + } + + #[test] + fn ssz_encode_usize() { + assert_eq!(1_usize.as_ssz_bytes(), vec![1, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!( + (!0_usize).as_ssz_bytes(), + vec![255, 255, 255, 255, 255, 255, 255, 255] + ); + } + + #[test] + fn ssz_encode_bool() { + assert_eq!(true.as_ssz_bytes(), vec![1]); + assert_eq!(false.as_ssz_bytes(), vec![0]); + } + + #[test] + fn ssz_encode_h256() { + assert_eq!(H256::from(&[0; 32]).as_ssz_bytes(), vec![0; 32]); + assert_eq!(H256::from(&[1; 32]).as_ssz_bytes(), vec![1; 32]); + + let bytes = vec![ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]; + + assert_eq!(H256::from_slice(&bytes).as_ssz_bytes(), bytes); + } + + #[test] + fn ssz_encode_u8_array_4() { + assert_eq!([0, 0, 0, 0].as_ssz_bytes(), vec![0; 4]); + assert_eq!([1, 0, 0, 0].as_ssz_bytes(), vec![1, 0, 0, 0]); + assert_eq!([1, 2, 3, 4].as_ssz_bytes(), vec![1, 2, 3, 4]); + } + + #[test] + fn tuple() { + assert_eq!((10u8, 11u8).as_ssz_bytes(), vec![10, 11]); + assert_eq!((10u32, 11u8).as_ssz_bytes(), vec![10, 0, 0, 0, 11]); + assert_eq!((10u8, 11u8, 12u8).as_ssz_bytes(), vec![10, 11, 12]); + } +} diff --git a/version-meld/eth2_ssz/src/legacy.rs b/version-meld/eth2_ssz/src/legacy.rs new file mode 100644 index 0000000..4953db0 --- /dev/null +++ b/version-meld/eth2_ssz/src/legacy.rs @@ -0,0 +1,265 @@ +//! Provides a "legacy" version of SSZ encoding for `Option where T: Encode + Decode`. +//! +//! The SSZ specification changed in 2021 to use a 1-byte union selector, instead of a 4-byte one +//! which was used in the Lighthouse database. +//! +//! Users can use the `four_byte_option_impl` macro to define a module that can be used with the +//! `#[ssz(with = "module")]`. +//! +//! ## Example +//! +//! ```rust +//! use ssz_derive::{Encode, Decode}; +//! use ssz::four_byte_option_impl; +//! +//! four_byte_option_impl!(impl_for_u64, u64); +//! +//! #[derive(Encode, Decode)] +//! struct Foo { +//! #[ssz(with = "impl_for_u64")] +//! a: Option, +//! } +//! ``` + +use crate::*; + +#[macro_export] +macro_rules! four_byte_option_impl { + ($mod_name: ident, $type: ty) => { + #[allow(dead_code)] + mod $mod_name { + use super::*; + + pub mod encode { + use super::*; + #[allow(unused_imports)] + use ssz::*; + + pub fn is_ssz_fixed_len() -> bool { + false + } + + pub fn ssz_fixed_len() -> usize { + BYTES_PER_LENGTH_OFFSET + } + + pub fn ssz_bytes_len(opt: &Option<$type>) -> usize { + if let Some(some) = opt { + let len = if <$type as Encode>::is_ssz_fixed_len() { + <$type as Encode>::ssz_fixed_len() + } else { + <$type as Encode>::ssz_bytes_len(some) + }; + len + BYTES_PER_LENGTH_OFFSET + } else { + BYTES_PER_LENGTH_OFFSET + } + } + + pub fn ssz_append(opt: &Option<$type>, buf: &mut Vec) { + match opt { + None => buf.extend_from_slice(&legacy::encode_four_byte_union_selector(0)), + Some(t) => { + buf.extend_from_slice(&legacy::encode_four_byte_union_selector(1)); + t.ssz_append(buf); + } + } + } + + pub fn as_ssz_bytes(opt: &Option<$type>) -> Vec { + let mut buf = vec![]; + + ssz_append(opt, &mut buf); + + buf + } + } + + pub mod decode { + use super::*; + #[allow(unused_imports)] + use ssz::*; + + pub fn is_ssz_fixed_len() -> bool { + false + } + + pub fn ssz_fixed_len() -> usize { + BYTES_PER_LENGTH_OFFSET + } + + pub fn from_ssz_bytes(bytes: &[u8]) -> Result, DecodeError> { + if bytes.len() < BYTES_PER_LENGTH_OFFSET { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: BYTES_PER_LENGTH_OFFSET, + }); + } + + let (index_bytes, value_bytes) = bytes.split_at(BYTES_PER_LENGTH_OFFSET); + + let index = legacy::read_four_byte_union_selector(index_bytes)?; + if index == 0 { + Ok(None) + } else if index == 1 { + Ok(Some(<$type as ssz::Decode>::from_ssz_bytes(value_bytes)?)) + } else { + Err(DecodeError::BytesInvalid(format!( + "{} is not a valid union index for Option", + index + ))) + } + } + } + } + }; +} + +pub fn encode_four_byte_union_selector(selector: usize) -> [u8; BYTES_PER_LENGTH_OFFSET] { + encode_length(selector) +} + +pub fn read_four_byte_union_selector(bytes: &[u8]) -> Result { + read_offset(bytes) +} + +#[cfg(test)] +mod test { + use super::*; + use crate as ssz; + use ssz_derive::{Decode, Encode}; + + type VecU16 = Vec; + + four_byte_option_impl!(impl_u16, u16); + four_byte_option_impl!(impl_vec_u16, VecU16); + + #[test] + fn ssz_encode_option_u16() { + let item = Some(65535_u16); + let bytes = vec![1, 0, 0, 0, 255, 255]; + assert_eq!(impl_u16::encode::as_ssz_bytes(&item), bytes); + assert_eq!(impl_u16::decode::from_ssz_bytes(&bytes).unwrap(), item); + + let item = None; + let bytes = vec![0, 0, 0, 0]; + assert_eq!(impl_u16::encode::as_ssz_bytes(&item), bytes); + assert_eq!(impl_u16::decode::from_ssz_bytes(&bytes).unwrap(), None); + } + + #[test] + fn ssz_encode_option_vec_u16() { + let item = Some(vec![0_u16, 1]); + let bytes = vec![1, 0, 0, 0, 0, 0, 1, 0]; + assert_eq!(impl_vec_u16::encode::as_ssz_bytes(&item), bytes); + assert_eq!(impl_vec_u16::decode::from_ssz_bytes(&bytes).unwrap(), item); + + let item = None; + let bytes = vec![0, 0, 0, 0]; + assert_eq!(impl_vec_u16::encode::as_ssz_bytes(&item), bytes); + assert_eq!(impl_vec_u16::decode::from_ssz_bytes(&bytes).unwrap(), item); + } + + fn round_trip(items: Vec) { + for item in items { + let encoded = &item.as_ssz_bytes(); + assert_eq!(item.ssz_bytes_len(), encoded.len()); + assert_eq!(T::from_ssz_bytes(encoded), Ok(item)); + } + } + + #[derive(Debug, PartialEq, Encode, Decode)] + struct TwoVariableLenOptions { + a: u16, + #[ssz(with = "impl_u16")] + b: Option, + #[ssz(with = "impl_vec_u16")] + c: Option>, + #[ssz(with = "impl_vec_u16")] + d: Option>, + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn two_variable_len_options_encoding() { + let s = TwoVariableLenOptions { + a: 42, + b: None, + c: Some(vec![0]), + d: None, + }; + + let bytes = vec![ + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + // | option | offset | offset | option = vec![ + TwoVariableLenOptions { + a: 42, + b: Some(12), + c: Some(vec![0]), + d: Some(vec![1]), + }, + TwoVariableLenOptions { + a: 42, + b: Some(12), + c: Some(vec![0]), + d: None, + }, + TwoVariableLenOptions { + a: 42, + b: None, + c: Some(vec![0]), + d: None, + }, + TwoVariableLenOptions { + a: 42, + b: None, + c: None, + d: None, + }, + ]; + + round_trip(vec); + } + + #[test] + fn tuple_u8_u16() { + let vec: Vec<(u8, u16)> = vec![ + (0, 0), + (0, 1), + (1, 0), + (u8::max_value(), u16::max_value()), + (0, u16::max_value()), + (u8::max_value(), 0), + (42, 12301), + ]; + + round_trip(vec); + } + + #[test] + fn tuple_vec_vec() { + let vec: Vec<(u64, Vec, Vec>)> = vec![ + (0, vec![], vec![vec![]]), + (99, vec![101], vec![vec![], vec![]]), + ( + 42, + vec![12, 13, 14], + vec![vec![99, 98, 97, 96], vec![42, 44, 46, 48, 50]], + ), + ]; + + round_trip(vec); + } +} diff --git a/version-meld/eth2_ssz/src/lib.rs b/version-meld/eth2_ssz/src/lib.rs new file mode 100644 index 0000000..df00c51 --- /dev/null +++ b/version-meld/eth2_ssz/src/lib.rs @@ -0,0 +1,71 @@ +//! Provides encoding (serialization) and decoding (deserialization) in the SimpleSerialize (SSZ) +//! format designed for use in Ethereum 2.0. +//! +//! Adheres to the Ethereum 2.0 [SSZ +//! specification](https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/ssz/simple-serialize.md) +//! at v0.12.1. +//! +//! ## Example +//! +//! ```rust +//! use ssz_derive::{Encode, Decode}; +//! use ssz::{Decode, Encode}; +//! +//! #[derive(PartialEq, Debug, Encode, Decode)] +//! struct Foo { +//! a: u64, +//! b: Vec, +//! } +//! +//! fn ssz_encode_decode_example() { +//! let foo = Foo { +//! a: 42, +//! b: vec![1, 3, 3, 7] +//! }; +//! +//! let ssz_bytes: Vec = foo.as_ssz_bytes(); +//! +//! let decoded_foo = Foo::from_ssz_bytes(&ssz_bytes).unwrap(); +//! +//! assert_eq!(foo, decoded_foo); +//! } +//! +//! ``` +//! +//! See `examples/` for manual implementations of the `Encode` and `Decode` traits. + +mod decode; +mod encode; +pub mod legacy; +mod union_selector; + +pub use decode::{ + impls::decode_list_of_variable_length_items, read_offset, split_union_bytes, Decode, + DecodeError, SszDecoder, SszDecoderBuilder, +}; +pub use encode::{encode_length, Encode, SszEncoder}; +pub use union_selector::UnionSelector; + +/// The number of bytes used to represent an offset. +pub const BYTES_PER_LENGTH_OFFSET: usize = 4; +/// The maximum value that can be represented using `BYTES_PER_LENGTH_OFFSET`. +#[cfg(target_pointer_width = "32")] +pub const MAX_LENGTH_VALUE: usize = (std::u32::MAX >> (8 * (4 - BYTES_PER_LENGTH_OFFSET))) as usize; +#[cfg(target_pointer_width = "64")] +pub const MAX_LENGTH_VALUE: usize = (std::u64::MAX >> (8 * (8 - BYTES_PER_LENGTH_OFFSET))) as usize; + +/// The number of bytes used to indicate the variant of a union. +pub const BYTES_PER_UNION_SELECTOR: usize = 1; +/// The highest possible union selector value (higher values are reserved for backwards compatible +/// extensions). +pub const MAX_UNION_SELECTOR: u8 = 127; + +/// Convenience function to SSZ encode an object supporting ssz::Encode. +/// +/// Equivalent to `val.as_ssz_bytes()`. +pub fn ssz_encode(val: &T) -> Vec +where + T: Encode, +{ + val.as_ssz_bytes() +} diff --git a/version-meld/eth2_ssz/src/union_selector.rs b/version-meld/eth2_ssz/src/union_selector.rs new file mode 100644 index 0000000..18bab09 --- /dev/null +++ b/version-meld/eth2_ssz/src/union_selector.rs @@ -0,0 +1,29 @@ +use crate::*; + +/// Provides the one-byte "selector" from the SSZ union specification: +/// +/// https://github.com/ethereum/consensus-specs/blob/v1.1.0-beta.3/ssz/simple-serialize.md#union +#[derive(Copy, Clone)] +pub struct UnionSelector(u8); + +impl From for u8 { + fn from(union_selector: UnionSelector) -> u8 { + union_selector.0 + } +} + +impl PartialEq for UnionSelector { + fn eq(&self, other: &u8) -> bool { + self.0 == *other + } +} + +impl UnionSelector { + /// Instantiate `self`, returning an error if `selector > MAX_UNION_SELECTOR`. + pub fn new(selector: u8) -> Result { + Some(selector) + .filter(|_| selector <= MAX_UNION_SELECTOR) + .map(Self) + .ok_or(DecodeError::UnionSelectorInvalid(selector)) + } +} diff --git a/version-meld/eth2_ssz/tests/tests.rs b/version-meld/eth2_ssz/tests/tests.rs new file mode 100644 index 0000000..7bd6252 --- /dev/null +++ b/version-meld/eth2_ssz/tests/tests.rs @@ -0,0 +1,466 @@ +use ethereum_types::H256; +use ssz::{Decode, DecodeError, Encode}; +use ssz_derive::{Decode, Encode}; + +mod round_trip { + use super::*; + + fn round_trip(items: Vec) { + for item in items { + let encoded = &item.as_ssz_bytes(); + assert_eq!(item.ssz_bytes_len(), encoded.len()); + assert_eq!(T::from_ssz_bytes(encoded), Ok(item)); + } + } + + #[test] + fn bool() { + let items: Vec = vec![true, false]; + + round_trip(items); + } + + #[test] + fn u8_array_4() { + let items: Vec<[u8; 4]> = vec![[0, 0, 0, 0], [1, 0, 0, 0], [1, 2, 3, 4], [1, 2, 0, 4]]; + + round_trip(items); + } + + #[test] + fn h256() { + let items: Vec = vec![H256::zero(), H256::from([1; 32]), H256::random()]; + + round_trip(items); + } + + #[test] + fn vec_of_h256() { + let items: Vec> = vec![ + vec![], + vec![H256::zero(), H256::from([1; 32]), H256::random()], + ]; + + round_trip(items); + } + + #[test] + fn vec_u16() { + let items: Vec> = vec![ + vec![], + vec![255], + vec![0, 1, 2], + vec![100; 64], + vec![255, 0, 255], + ]; + + round_trip(items); + } + + #[test] + fn vec_of_vec_u16() { + let items: Vec>> = vec![ + vec![], + vec![vec![]], + vec![vec![1, 2, 3]], + vec![vec![], vec![]], + vec![vec![], vec![1, 2, 3]], + vec![vec![1, 2, 3], vec![1, 2, 3]], + vec![vec![1, 2, 3], vec![], vec![1, 2, 3]], + vec![vec![], vec![], vec![1, 2, 3]], + vec![vec![], vec![1], vec![1, 2, 3]], + vec![vec![], vec![1], vec![1, 2, 3]], + ]; + + round_trip(items); + } + + #[derive(Debug, PartialEq, Encode, Decode)] + struct FixedLen { + a: u16, + b: u64, + c: u32, + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn fixed_len_struct_encoding() { + let items: Vec = vec![ + FixedLen { a: 0, b: 0, c: 0 }, + FixedLen { a: 1, b: 1, c: 1 }, + FixedLen { a: 1, b: 0, c: 1 }, + ]; + + let expected_encodings = vec![ + // | u16--| u64----------------------------| u32----------| + vec![00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00], + vec![01, 00, 01, 00, 00, 00, 00, 00, 00, 00, 01, 00, 00, 00], + vec![01, 00, 00, 00, 00, 00, 00, 00, 00, 00, 01, 00, 00, 00], + ]; + + for i in 0..items.len() { + assert_eq!( + items[i].as_ssz_bytes(), + expected_encodings[i], + "Failed on {}", + i + ); + } + } + + #[test] + fn fixed_len_excess_bytes() { + let fixed = FixedLen { a: 1, b: 2, c: 3 }; + + let mut bytes = fixed.as_ssz_bytes(); + bytes.append(&mut vec![0]); + + assert_eq!( + FixedLen::from_ssz_bytes(&bytes), + Err(DecodeError::InvalidByteLength { + len: 15, + expected: 14, + }) + ); + } + + #[test] + fn vec_of_fixed_len_struct() { + let items: Vec = vec![ + FixedLen { a: 0, b: 0, c: 0 }, + FixedLen { a: 1, b: 1, c: 1 }, + FixedLen { a: 1, b: 0, c: 1 }, + ]; + + round_trip(items); + } + + #[derive(Debug, PartialEq, Encode, Decode)] + struct VariableLen { + a: u16, + b: Vec, + c: u32, + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn offset_into_fixed_bytes() { + let bytes = vec![ + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // | offset | u32 | variable + 01, 00, 09, 00, 00, 00, 01, 00, 00, 00, 00, 00, 01, 00, 02, 00, + ]; + + assert_eq!( + VariableLen::from_ssz_bytes(&bytes), + Err(DecodeError::OffsetIntoFixedPortion(9)) + ); + } + + #[test] + fn variable_len_excess_bytes() { + let variable = VariableLen { + a: 1, + b: vec![2], + c: 3, + }; + + let mut bytes = variable.as_ssz_bytes(); + bytes.append(&mut vec![0]); + + // The error message triggered is not so helpful, it's caught by a side-effect. Just + // checking there is _some_ error is fine. + assert!(VariableLen::from_ssz_bytes(&bytes).is_err()); + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn first_offset_skips_byte() { + let bytes = vec![ + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // | offset | u32 | variable + 01, 00, 11, 00, 00, 00, 01, 00, 00, 00, 00, 00, 01, 00, 02, 00, + ]; + + assert_eq!( + VariableLen::from_ssz_bytes(&bytes), + Err(DecodeError::OffsetSkipsVariableBytes(11)) + ); + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn variable_len_struct_encoding() { + let items: Vec = vec![ + VariableLen { + a: 0, + b: vec![], + c: 0, + }, + VariableLen { + a: 1, + b: vec![0], + c: 1, + }, + VariableLen { + a: 1, + b: vec![0, 1, 2], + c: 1, + }, + ]; + + let expected_encodings = vec![ + // 00..................................09 + // | u16--| vec offset-----| u32------------| vec payload --------| + vec![00, 00, 10, 00, 00, 00, 00, 00, 00, 00], + vec![01, 00, 10, 00, 00, 00, 01, 00, 00, 00, 00, 00], + vec![ + 01, 00, 10, 00, 00, 00, 01, 00, 00, 00, 00, 00, 01, 00, 02, 00, + ], + ]; + + for i in 0..items.len() { + assert_eq!( + items[i].as_ssz_bytes(), + expected_encodings[i], + "Failed on {}", + i + ); + } + } + + #[test] + fn vec_of_variable_len_struct() { + let items: Vec = vec![ + VariableLen { + a: 0, + b: vec![], + c: 0, + }, + VariableLen { + a: 255, + b: vec![0, 1, 2, 3], + c: 99, + }, + VariableLen { + a: 255, + b: vec![0], + c: 99, + }, + VariableLen { + a: 50, + b: vec![0], + c: 0, + }, + ]; + + round_trip(items); + } + + #[derive(Debug, PartialEq, Encode, Decode)] + struct ThreeVariableLen { + a: u16, + b: Vec, + c: Vec, + d: Vec, + } + + #[test] + fn three_variable_len() { + let vec: Vec = vec![ThreeVariableLen { + a: 42, + b: vec![0], + c: vec![1], + d: vec![2], + }]; + + round_trip(vec); + } + + #[test] + #[allow(clippy::zero_prefixed_literal)] + fn offsets_decreasing() { + let bytes = vec![ + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // | offset | offset | offset | variable + 01, 00, 14, 00, 00, 00, 15, 00, 00, 00, 14, 00, 00, 00, 00, 00, + ]; + + assert_eq!( + ThreeVariableLen::from_ssz_bytes(&bytes), + Err(DecodeError::OffsetsAreDecreasing(14)) + ); + } + + #[test] + fn tuple_u8_u16() { + let vec: Vec<(u8, u16)> = vec![ + (0, 0), + (0, 1), + (1, 0), + (u8::max_value(), u16::max_value()), + (0, u16::max_value()), + (u8::max_value(), 0), + (42, 12301), + ]; + + round_trip(vec); + } + + #[test] + fn tuple_vec_vec() { + let vec: Vec<(u64, Vec, Vec>)> = vec![ + (0, vec![], vec![vec![]]), + (99, vec![101], vec![vec![], vec![]]), + ( + 42, + vec![12, 13, 14], + vec![vec![99, 98, 97, 96], vec![42, 44, 46, 48, 50]], + ), + ]; + + round_trip(vec); + } +} + +mod derive_macro { + use ssz::{Decode, Encode}; + use ssz_derive::{Decode, Encode}; + use std::fmt::Debug; + + fn assert_encode(item: &T, bytes: &[u8]) { + assert_eq!(item.as_ssz_bytes(), bytes); + } + + fn assert_encode_decode(item: &T, bytes: &[u8]) { + assert_encode(item, bytes); + assert_eq!(T::from_ssz_bytes(bytes).unwrap(), *item); + } + + #[derive(PartialEq, Debug, Encode, Decode)] + #[ssz(enum_behaviour = "union")] + enum TwoFixedUnion { + U8(u8), + U16(u16), + } + + #[derive(PartialEq, Debug, Encode, Decode)] + struct TwoFixedUnionStruct { + a: TwoFixedUnion, + } + + #[test] + fn two_fixed_union() { + let eight = TwoFixedUnion::U8(1); + let sixteen = TwoFixedUnion::U16(1); + + assert_encode_decode(&eight, &[0, 1]); + assert_encode_decode(&sixteen, &[1, 1, 0]); + + assert_encode_decode(&TwoFixedUnionStruct { a: eight }, &[4, 0, 0, 0, 0, 1]); + assert_encode_decode(&TwoFixedUnionStruct { a: sixteen }, &[4, 0, 0, 0, 1, 1, 0]); + } + + #[derive(PartialEq, Debug, Encode, Decode)] + struct VariableA { + a: u8, + b: Vec, + } + + #[derive(PartialEq, Debug, Encode, Decode)] + struct VariableB { + a: Vec, + b: u8, + } + + #[derive(PartialEq, Debug, Encode)] + #[ssz(enum_behaviour = "transparent")] + enum TwoVariableTrans { + A(VariableA), + B(VariableB), + } + + #[derive(PartialEq, Debug, Encode)] + struct TwoVariableTransStruct { + a: TwoVariableTrans, + } + + #[derive(PartialEq, Debug, Encode, Decode)] + #[ssz(enum_behaviour = "union")] + enum TwoVariableUnion { + A(VariableA), + B(VariableB), + } + + #[derive(PartialEq, Debug, Encode, Decode)] + struct TwoVariableUnionStruct { + a: TwoVariableUnion, + } + + #[test] + fn two_variable_trans() { + let trans_a = TwoVariableTrans::A(VariableA { + a: 1, + b: vec![2, 3], + }); + let trans_b = TwoVariableTrans::B(VariableB { + a: vec![1, 2], + b: 3, + }); + + assert_encode(&trans_a, &[1, 5, 0, 0, 0, 2, 3]); + assert_encode(&trans_b, &[5, 0, 0, 0, 3, 1, 2]); + + assert_encode( + &TwoVariableTransStruct { a: trans_a }, + &[4, 0, 0, 0, 1, 5, 0, 0, 0, 2, 3], + ); + assert_encode( + &TwoVariableTransStruct { a: trans_b }, + &[4, 0, 0, 0, 5, 0, 0, 0, 3, 1, 2], + ); + } + + #[test] + fn two_variable_union() { + let union_a = TwoVariableUnion::A(VariableA { + a: 1, + b: vec![2, 3], + }); + let union_b = TwoVariableUnion::B(VariableB { + a: vec![1, 2], + b: 3, + }); + + assert_encode_decode(&union_a, &[0, 1, 5, 0, 0, 0, 2, 3]); + assert_encode_decode(&union_b, &[1, 5, 0, 0, 0, 3, 1, 2]); + + assert_encode_decode( + &TwoVariableUnionStruct { a: union_a }, + &[4, 0, 0, 0, 0, 1, 5, 0, 0, 0, 2, 3], + ); + assert_encode_decode( + &TwoVariableUnionStruct { a: union_b }, + &[4, 0, 0, 0, 1, 5, 0, 0, 0, 3, 1, 2], + ); + } + + #[derive(PartialEq, Debug, Encode, Decode)] + #[ssz(enum_behaviour = "union")] + enum TwoVecUnion { + A(Vec), + B(Vec), + } + + #[test] + fn two_vec_union() { + assert_encode_decode(&TwoVecUnion::A(vec![]), &[0]); + assert_encode_decode(&TwoVecUnion::B(vec![]), &[1]); + + assert_encode_decode(&TwoVecUnion::A(vec![0]), &[0, 0]); + assert_encode_decode(&TwoVecUnion::B(vec![0]), &[1, 0]); + + assert_encode_decode(&TwoVecUnion::A(vec![0, 1]), &[0, 0, 1]); + assert_encode_decode(&TwoVecUnion::B(vec![0, 1]), &[1, 0, 1]); + } +} diff --git a/zerog-storage-client b/zerog-storage-client new file mode 160000 index 0000000..307f5b1 --- /dev/null +++ b/zerog-storage-client @@ -0,0 +1 @@ +Subproject commit 307f5b17d178df643dd981d63495bb2c92455f5f diff --git a/zerog-storage-contracts b/zerog-storage-contracts new file mode 160000 index 0000000..171cebf --- /dev/null +++ b/zerog-storage-contracts @@ -0,0 +1 @@ +Subproject commit 171cebffffdd82bf464dea85539458bcafbd73ef