diff --git a/Cargo.lock b/Cargo.lock index 8d309b31e..f77fc518f 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfdb4953a096c551ce9ace855a604d702e6e62d77fac690575ae347571717f5" +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bincode" version = "1.3.3" @@ -308,10 +319,22 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" dependencies = [ - "funty", - "radium", + "funty 1.1.0", + "radium 0.6.2", "tap", - "wyz", + "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]] @@ -420,6 +443,12 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" +[[package]] +name = "bytemuck" +version = "1.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" + [[package]] name = "byteorder" version = "1.5.0" @@ -505,6 +534,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + [[package]] name = "chrono" version = "0.4.34" @@ -563,6 +598,12 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.6" @@ -723,9 +764,9 @@ dependencies = [ [[package]] name = "curl" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8e5123ab8c31200ce725939049ecd4a090b242608f24048131dedf9dd195aed" +checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" dependencies = [ "curl-sys", "libc", @@ -953,7 +994,7 @@ version = "16.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c98847055d934070b90e806e12d3936b787d0a115068981c1d8dfd5dfef5a5" dependencies = [ - "ethereum-types", + "ethereum-types 0.12.1", "hex", "serde", "serde_json", @@ -975,17 +1016,40 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "ethbloom" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11da94e443c60508eb62cf256243a64da87304c2802ac2528847f79d750007ef" +dependencies = [ + "crunchy", + "fixed-hash", + "tiny-keccak", +] + [[package]] name = "ethereum-types" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" dependencies = [ - "ethbloom", + "ethbloom 0.11.1", "fixed-hash", "impl-rlp", "impl-serde", - "primitive-types", + "primitive-types 0.10.1", + "uint", +] + +[[package]] +name = "ethereum-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2827b94c556145446fcce834ca86b7abf0c39a805883fe20e72c5bfdb5a0dc6" +dependencies = [ + "ethbloom 0.12.1", + "fixed-hash", + "primitive-types 0.11.1", "uint", ] @@ -1130,6 +1194,12 @@ 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.1.31" @@ -1591,13 +1661,36 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-iter", + "num-rational 0.3.2", + "num-traits", +] + [[package]] name = "impl-codec" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" dependencies = [ - "parity-scale-codec", + "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.9", ] [[package]] @@ -1722,6 +1815,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -2328,7 +2430,7 @@ dependencies = [ "num-complex", "num-integer", "num-iter", - "num-rational", + "num-rational 0.4.1", "num-traits", ] @@ -2388,6 +2490,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -2561,10 +2674,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" dependencies = [ "arrayvec", - "bitvec", + "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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881331e34fa842a2fb61cc2db9643a8fedc615e47cfcc52597d1af0db9a7e8fe" +dependencies = [ + "arrayvec", + "bitvec 1.0.1", "byte-slice-cast", "impl-trait-for-tuples", - "parity-scale-codec-derive", + "parity-scale-codec-derive 3.6.9", "serde", ] @@ -2580,6 +2707,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "parity-scale-codec-derive" +version = "3.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" +dependencies = [ + "proc-macro-crate 2.0.2", + "proc-macro2 1.0.78", + "quote 1.0.35", + "syn 1.0.109", +] + [[package]] name = "parity-ws" version = "0.10.1" @@ -2870,12 +3009,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" dependencies = [ "fixed-hash", - "impl-codec", + "impl-codec 0.5.1", "impl-rlp", "impl-serde", "uint", ] +[[package]] +name = "primitive-types" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28720988bff275df1f51b171e1b2a18c30d194c4d2b61defdacecd625a5d94a" +dependencies = [ + "fixed-hash", + "impl-codec 0.6.0", + "uint", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -2892,7 +3042,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", ] [[package]] @@ -2989,6 +3149,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "qrcode" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", + "image", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -3028,6 +3198,12 @@ 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.6.5" @@ -3487,9 +3663,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "secp256k1" -version = "0.22.2" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295642060261c80709ac034f52fca8e5a9fa2c7d341ded5cdb164b7c33768b2a" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ "secp256k1-sys", "serde", @@ -3497,9 +3673,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.5.2" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" dependencies = [ "cc", ] @@ -3873,6 +4049,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.78", + "quote 1.0.35", + "rustversion", + "syn 2.0.48", +] + [[package]] name = "subtle" version = "1.0.0" @@ -4359,9 +4554,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" @@ -4374,6 +4569,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -4749,7 +4955,7 @@ dependencies = [ "bytes 1.5.0", "derive_more", "ethabi", - "ethereum-types", + "ethereum-types 0.12.1", "futures 0.3.30", "futures-timer", "headers", @@ -4767,6 +4973,16 @@ dependencies = [ "url", ] +[[package]] +name = "web3-unit-converter" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe2891c5241406679f80048578f2ab1a72a79cd86bb1c8835ac2c6c9931e9ad" +dependencies = [ + "bigdecimal", + "ethereum-types 0.13.1", +] + [[package]] name = "webbrowser" version = "0.8.12" @@ -5054,9 +5270,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.39" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] @@ -5082,7 +5298,7 @@ dependencies = [ [[package]] name = "witnet" -version = "1.7.1" +version = "2.0.0" dependencies = [ "ansi_term", "bytecount", @@ -5091,11 +5307,12 @@ dependencies = [ "failure", "futures 0.3.30", "hex", - "itertools", + "itertools 0.8.2", "lazy_static", "log 0.4.20", "num-format", "prettytable-rs", + "qrcode", "sentry", "serde", "serde_json", @@ -5129,7 +5346,7 @@ dependencies = [ [[package]] name = "witnet-centralized-ethereum-bridge" -version = "1.7.1" +version = "2.0.0" dependencies = [ "actix", "async-jsonrpc-client", @@ -5145,6 +5362,7 @@ dependencies = [ "tokio 1.36.0", "toml", "web3", + "web3-unit-converter", "witnet_config", "witnet_data_structures", "witnet_net", @@ -5193,7 +5411,7 @@ dependencies = [ [[package]] name = "witnet_data_structures" -version = "1.7.1" +version = "2.0.0" dependencies = [ "bech32", "bencher", @@ -5202,12 +5420,12 @@ dependencies = [ "byteorder", "cbor-codec", "chrono", - "ethereum-types", + "ethereum-types 0.12.1", "exonum-build", "failure", "futures 0.3.30", "hex", - "itertools", + "itertools 0.8.2", "lazy_static", "log 0.4.20", "num-traits", @@ -5221,6 +5439,8 @@ dependencies = [ "serde", "serde_cbor", "serde_json", + "strum", + "strum_macros", "vrf", "witnet_crypto", "witnet_protected", @@ -5258,7 +5478,7 @@ dependencies = [ [[package]] name = "witnet_node" -version = "1.7.1" +version = "2.0.0" dependencies = [ "actix", "ansi_term", @@ -5273,7 +5493,7 @@ dependencies = [ "futures-util", "glob", "hex", - "itertools", + "itertools 0.8.2", "jsonrpc-core 18.0.0", "jsonrpc-pubsub 18.0.0", "log 0.4.20", @@ -5365,7 +5585,7 @@ dependencies = [ [[package]] name = "witnet_toolkit" -version = "1.7.1" +version = "2.0.0" dependencies = [ "failure", "hex", @@ -5396,8 +5616,9 @@ dependencies = [ "bencher", "failure", "hex", - "itertools", + "itertools 0.11.0", "log 0.4.20", + "num-traits", "url", "witnet_config", "witnet_crypto", @@ -5408,7 +5629,7 @@ dependencies = [ [[package]] name = "witnet_wallet" -version = "1.7.1" +version = "2.0.0" dependencies = [ "actix", "async-jsonrpc-client", @@ -5420,7 +5641,7 @@ dependencies = [ "futures 0.3.30", "futures-util", "hex", - "itertools", + "itertools 0.8.2", "jsonrpc-core 15.1.0", "jsonrpc-pubsub 15.1.0", "log 0.4.20", @@ -5475,6 +5696,15 @@ 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 = "zstd-sys" version = "2.0.9+zstd.1.5.5" diff --git a/Cargo.toml b/Cargo.toml index 216d3f6ae..8d25a95bc 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "witnet" -version = "1.7.1" +version = "2.0.0" authors = ["Witnet Foundation "] publish = false repository = "witnet/witnet-rust" @@ -35,6 +35,7 @@ lazy_static = "1.4.0" log = "0.4.8" num-format = "0.4.0" prettytable-rs = { version = "0.10.0", default-features = false } +qrcode = "0.12" sentry = { version = "0.29.3", features = ["log"], optional = true } serde_json = "1.0.47" structopt = "0.3.9" diff --git a/bridges/centralized-ethereum/.env.example b/bridges/centralized-ethereum/.env.example index 9e58d54e4..3392d2cd6 100644 --- a/bridges/centralized-ethereum/.env.example +++ b/bridges/centralized-ethereum/.env.example @@ -1,22 +1,27 @@ # Sample env file -# Note that the bridge does not read .env files, you need to import these variables into the environment using some external tool -# adjustable params -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_ACCOUNT: "0x8c49CAfC4542D9EA9107D4E48412ACEd2A68aA77" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_GAS_LIMITS: "report_result = 3_000_000" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_NUM_CONFIRMATIONS: 8 -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_JSONRPC_ADDR: "10.5.1.4:21338" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WRB_CONTRACT_ADDR: "0x58D8ECe142c60f5707594a7C1D90e46eAE5AF431" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_DR_FEE_NANOWITS: 10000 -# not-to-be-changed params -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_CLIENT_URL: "http://gateway:8535" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_MAX_DR_VALUE_NANOWITS: 100000000000 # 1 WIT -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_MAX_RESULT_SIZE: 100 # 100 bytes -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_REQUEST_EXAMPLE_CONTRACT_ADDR: "0xEaA9e7Ea612b169f5b41cfF86dA6322f57264a19" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_STORAGE: "db_path = \"data/polygon_goerli\"" +# Note that the bridge does not read .env files, you need to import these variables +# into the environment using some external tool + +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_FROM: "0x8c49CAfC4542D9EA9107D4E48412ACEd2A68aA77" +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_FROM_BALANCE_THRESHOLD: 100000000000000000 # 0.1 ETH +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_GAS_LIMITS: "report_result = 3_000_000" # (optional) +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_JSONRPC_URL: "http://gateway:8535" +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_MAX_BATCH_SIZE: 64 +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_NANOWIT_WEI_PRICE: 10000 # Price of nanoWIT in WEI (optional) +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_NEW_DRS_POLLING_RATE_MS: 30000 # 30 secs, not less than average EVM's block period +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_TXS_CONFIRMATIONS: 2 +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_TXS_TIMEOUT_MS: 900000 # 15 min +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_WITNET_ORACLE: "0x58D8ECe142c60f5707594a7C1D90e46eAE5AF431" + +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_MIN_COLLATERAL_NANOWITS: 20000000000 # 20 WIT +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_MAX_FEE_NANOWITS: 100000 # 0.1 milliWIT +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_MAX_RESULT_SIZE: 64 # 64 bytes +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_MAX_VALUE_NANOWITS: 100000000000 # 1 WIT +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_TXS_POLLING_RATE_MS: 45000 # 45 secs +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_TXS_TIMEOUT_MS: 600000 # 10 min + +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_JSONRPC_SOCKET: "10.5.1.4:21338" WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_TESTNET: "false" -# timing params -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_DR_TX_UNRESOLVED_TIMEOUT_MS: 600000 # 10 min -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_CONFIRMATION_TIMEOUT_MS: 900000 # 15 min -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_NEW_DR_POLLING_RATE_MS: 45000 # 45 secs -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WIT_TALLY_POLLING_RATE_MS: 45000 # 45 secs -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WIT_DR_SENDER_POLLING_RATE_MS: 45000 # 45 secs + +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_STORAGE: "db_path = \"data/storage\"" +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_STORAGE_SKIP_FIRST: 0 diff --git a/bridges/centralized-ethereum/Cargo.toml b/bridges/centralized-ethereum/Cargo.toml index f8851c8e3..f42e852dc 100644 --- a/bridges/centralized-ethereum/Cargo.toml +++ b/bridges/centralized-ethereum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "witnet-centralized-ethereum-bridge" -version = "1.7.1" +version = "2.0.0" authors = ["Witnet Foundation "] edition = "2018" @@ -25,3 +25,4 @@ witnet_net = { path = "../../net" } witnet_node = { path = "../../node" } witnet_util = { path = "../../util" } witnet_validations = { path = "../../validations" } +web3-unit-converter = "0.1.1" diff --git a/bridges/centralized-ethereum/README.md b/bridges/centralized-ethereum/README.md index c2a88f7fa..ef03eb43d 100644 --- a/bridges/centralized-ethereum/README.md +++ b/bridges/centralized-ethereum/README.md @@ -103,11 +103,11 @@ We need to modify the configuration file that the bridge will use to establish c There are some key fields that need to be edited here to make the bridge connect properly to your Ethereum/Witnet nodes. These are: -- *witnet_jsonrpc_addr*: make sure this address is identical to the jsonRPC address of your Witnet node. -- *eth_client_url*: make sure this field points to address where your ethereum client is running. -- *wrb_contract_addr*: this field should contain the address of the WitnetRequestsBoard contract you wish your node to connect to. +- *witnet_jsonrpc_socket*: make sure this address is identical to the jsonRPC address of your Witnet node. +- *eth_jsonrpc_url*: make sure this field points to address where your ethereum client is running. +- *eth_witnet_oracle*: this field should contain the address of the WitnetRequestsBoard contract you wish your node to connect to. - *block_relay_contract_add*: this field should contain the address of the BlockRelay contract you wish your node to connect to. -- *eth_account*: this is the account you are using in your ethereum client. +- *eth_from*: this is the account you are using in your ethereum client. - **NOTE**: In the case of using ganache-cli, this needs to be the first account, since currently the only account that can relay Witnet blocks to Ethereum is the one that deployed the BlockRelay contract, which by default is the first account. diff --git a/bridges/centralized-ethereum/src/actors/dr_database.rs b/bridges/centralized-ethereum/src/actors/dr_database.rs index 7d0f258c4..880e962d4 100644 --- a/bridges/centralized-ethereum/src/actors/dr_database.rs +++ b/bridges/centralized-ethereum/src/actors/dr_database.rs @@ -53,17 +53,20 @@ pub struct DrInfoBridge { } /// Data request state -#[derive(Clone, Default, Serialize, Deserialize)] +#[derive(Clone, Copy, Default, Serialize, Deserialize)] pub enum DrState { - /// New: the data request has just been posted to the smart contract. + /// New: a new query was detected on the Witnet Oracle contract, + /// but has not yet been attended. #[default] New, - /// Pending: the data request has been created and broadcast to witnet, but it has not been - /// included in a witnet block yet. + /// Pending: a data request transaction was broadcasted to the Witnet blockchain, + /// but has not yet been resolved. Pending, - /// Finished: data request has been resolved in witnet and the result is in the smart - /// contract. + /// Finished: the data request result was reported back to the Witnet Oracle contract. Finished, + /// Dismissed: the data request result cannot be reported back to the Witnet Oracle contract, + /// or was already reported by another bridge instance. + Dismissed, } impl fmt::Display for DrState { @@ -72,24 +75,24 @@ impl fmt::Display for DrState { DrState::New => "New", DrState::Pending => "Pending", DrState::Finished => "Finished", + DrState::Dismissed => "Dismissed", }; f.write_str(s) } } -/// Data request states in Witnet Request Board contract +/// Possible query states in the Witnet Oracle contract #[derive(Serialize, Deserialize, Clone)] pub enum WitnetQueryStatus { - /// Unknown: the data request does not exist. + /// Unknown: the query does not exist, or got eventually deleted. Unknown, - /// Posted: the data request has just been posted to the smart contract. + /// Posted: the query exists, but has not yet been reported. Posted, - /// Reported: the data request has been resolved in witnet and the result is in the smart - /// contract. + /// Reported: some query result got stored into the WitnetOracle, although not yet finalized. Reported, - /// Deleted: the data request has been resolved in witnet but the result was deleted. - Deleted, + /// Finalized: the query was reported, and considered to be final. + Finalized, } impl WitnetQueryStatus { @@ -98,7 +101,7 @@ impl WitnetQueryStatus { match i { 1 => WitnetQueryStatus::Posted, 2 => WitnetQueryStatus::Reported, - 3 => WitnetQueryStatus::Deleted, + 3 => WitnetQueryStatus::Finalized, _ => WitnetQueryStatus::Unknown, } } @@ -119,7 +122,7 @@ impl Actor for DrDatabase { |dr_database_from_storage, act, _| match dr_database_from_storage { Ok(dr_database_from_storage) => { if let Some(mut dr_database_from_storage) = dr_database_from_storage { - log::info!("Load database from storage"); + log::info!("Database loaded from storage"); act.dr = std::mem::take(&mut dr_database_from_storage.dr); act.max_dr_id = dr_database_from_storage.max_dr_id; } else { @@ -164,22 +167,31 @@ impl Message for GetLastDrId { type Result = Result; } -/// Set data request id as "finished" -pub struct SetFinished { - /// Data Request Id +/// Set state of given data request id +pub struct SetDrState { + /// Data Request id pub dr_id: DrId, + /// Data Request new state + pub dr_state: DrState, } -impl Message for SetFinished { +impl Message for SetDrState { type Result = Result<(), ()>; } +/// Count number of data requests in given state +pub struct CountDrsPerState; + +impl Message for CountDrsPerState { + type Result = Result<(u64, u64, u64, u64), ()>; +} + impl Handler for DrDatabase { type Result = (); fn handle(&mut self, msg: SetDrInfoBridge, ctx: &mut Self::Context) -> Self::Result { let SetDrInfoBridge(dr_id, dr_info) = msg; - let dr_state = dr_info.dr_state.clone(); + let dr_state = dr_info.dr_state; self.dr.insert(dr_id, dr_info); self.max_dr_id = cmp::max(self.max_dr_id, dr_id); @@ -239,32 +251,24 @@ impl Handler for DrDatabase { } } -impl Handler for DrDatabase { +impl Handler for DrDatabase { type Result = Result<(), ()>; - fn handle(&mut self, msg: SetFinished, ctx: &mut Self::Context) -> Self::Result { - let SetFinished { dr_id } = msg; + fn handle(&mut self, msg: SetDrState, ctx: &mut Self::Context) -> Self::Result { + let SetDrState { dr_id, dr_state } = msg; match self.dr.entry(dr_id) { Entry::Occupied(entry) => { entry.into_mut().dr_state = DrState::Finished; - log::debug!( - "Data request #{} updated to state {}", - dr_id, - DrState::Finished - ); + log::debug!("Data request #{} updated to state {}", dr_id, dr_state,); } Entry::Vacant(entry) => { entry.insert(DrInfoBridge { dr_bytes: vec![], - dr_state: DrState::Finished, + dr_state, dr_tx_hash: None, dr_tx_creation_timestamp: None, }); - log::debug!( - "Data request #{} inserted with state {}", - dr_id, - DrState::Finished - ); + log::debug!("Data request #{} inserted with state {}", dr_id, dr_state,); } } @@ -277,6 +281,28 @@ impl Handler for DrDatabase { } } +impl Handler for DrDatabase { + type Result = Result<(u64, u64, u64, u64), ()>; + + fn handle(&mut self, _msg: CountDrsPerState, _ctx: &mut Self::Context) -> Self::Result { + let mut drs_new = u64::default(); + let mut drs_pending = u64::default(); + let mut drs_finished = u64::default(); + let mut drs_dismissed = u64::default(); + + self.dr.iter().for_each(|(_dr_id, dr_info)| { + match dr_info.dr_state { + DrState::New => drs_new += 1, + DrState::Pending => drs_pending += 1, + DrState::Finished => drs_finished += 1, + DrState::Dismissed => drs_dismissed += 1, + }; + }); + + Ok((drs_new, drs_pending, drs_finished, drs_dismissed)) + } +} + /// Required trait for being able to retrieve DrDatabase address from system registry impl actix::Supervised for DrDatabase {} diff --git a/bridges/centralized-ethereum/src/actors/dr_reporter.rs b/bridges/centralized-ethereum/src/actors/dr_reporter.rs index d0e5fc322..1919bdc72 100644 --- a/bridges/centralized-ethereum/src/actors/dr_reporter.rs +++ b/bridges/centralized-ethereum/src/actors/dr_reporter.rs @@ -1,43 +1,47 @@ use crate::{ - actors::dr_database::{DrDatabase, DrId, SetFinished, WitnetQueryStatus}, + actors::dr_database::{DrDatabase, DrId, DrState, SetDrState}, config::Config, handle_receipt, }; use actix::prelude::*; use std::{collections::HashSet, sync::Arc, time::Duration}; use web3::{ - contract::{self, Contract}, + contract::{self, tokens::Tokenize, Contract}, ethabi::{ethereum_types::H256, Token}, transports::Http, types::{H160, U256}, Web3, }; +use web3_unit_converter::Unit; use witnet_data_structures::{chain::Hash, radon_error::RadonErrors}; use witnet_node::utils::stop_system_if_panicking; /// DrReporter actor sends the the Witnet Request tally results to Ethereum #[derive(Default)] pub struct DrReporter { - /// WRB contract - pub wrb_contract: Option>>, /// Web3 pub web3: Option>, - /// eth_account - pub eth_account: H160, + /// WRB contract + pub wrb_contract: Option>>, + /// EVM account used to report data request results + pub eth_from: H160, + /// EVM account minimum balance under which alerts will be logged + pub eth_from_balance_threshold: u64, + /// Flag indicating whether low funds alert was already logged + pub eth_from_balance_alert: bool, /// report_result_limit - pub report_result_limit: Option, + pub eth_max_gas: Option, + /// Price of $nanoWit in Wei, used to improve estimation of report profits + pub eth_nanowit_wei_price: Option, + /// Max time to wait for an ethereum transaction to be confirmed before returning an error + pub eth_txs_timeout_ms: u64, + /// Number of block confirmations needed to assume finality when sending transactions to ethereum + pub eth_txs_confirmations: usize, /// maximum result size (in bytes) - pub max_result_size: usize, + pub witnet_dr_max_result_size: usize, /// Pending reportResult transactions. The actor should not attempt to report these requests /// until the timeout has elapsed - pub pending_report_result: HashSet, - /// Max time to wait for an ethereum transaction to be confirmed before returning an error - pub eth_confirmation_timeout_ms: u64, - /// Number of block confirmations needed to assume finality when sending transactions to ethereum - pub num_confirmations: usize, - /// Max ratio between the gas price recommended by the provider and the gas price of the requests in the WRB - /// That is, the bridge will refrain from paying more than these times the gas price originally set forth by the requesters. - pub report_result_max_network_gas_price_ratio: f64, + pub pending_dr_reports: HashSet, } impl Drop for DrReporter { @@ -68,20 +72,21 @@ impl DrReporter { /// Initialize `DrReporter` taking the configuration from a `Config` structure pub fn from_config( config: &Config, - wrb_contract: Arc>, web3: Web3, + wrb_contract: Arc>, ) -> Self { Self { - wrb_contract: Some(wrb_contract), web3: Some(web3), - eth_account: config.eth_account, - report_result_limit: config.gas_limits.report_result, - max_result_size: config.max_result_size, - pending_report_result: Default::default(), - eth_confirmation_timeout_ms: config.eth_confirmation_timeout_ms, - num_confirmations: config.num_confirmations, - report_result_max_network_gas_price_ratio: config - .report_result_max_network_gas_price_ratio, + wrb_contract: Some(wrb_contract), + eth_from: config.eth_from, + eth_from_balance_threshold: config.eth_from_balance_threshold, + eth_from_balance_alert: false, + eth_max_gas: config.eth_gas_limits.report_result, + eth_nanowit_wei_price: config.eth_nanowit_wei_price, + eth_txs_timeout_ms: config.eth_txs_timeout_ms, + eth_txs_confirmations: config.eth_txs_confirmations, + witnet_dr_max_result_size: config.witnet_dr_max_result_size, + pending_dr_reports: Default::default(), } } } @@ -94,13 +99,15 @@ pub struct DrReporterMsg { /// Report the result of this data request id to ethereum pub struct Report { - /// Data request id in ethereum + /// Data Request's unique query id as known by the WitnetOracle contract pub dr_id: DrId, - /// Timestamp of the solving commit txs in Witnet. If zero is provided, EVM-timestamp will be used instead - pub timestamp: u64, - /// Hash of the data request in witnet + /// Timestamp at which the reported result was actually generated + pub dr_timestamp: u64, + /// Hash of the Data Request Transaction in the Witnet blockchain pub dr_tx_hash: Hash, - /// Data request result from witnet, in bytes + /// Hash of the Data Request Tally Transaction in the Witnet blockchain + pub dr_tally_tx_hash: Hash, + /// CBOR-encoded result to Data Request, as resolved by the Witnet blockchain pub result: Vec, } @@ -114,10 +121,10 @@ impl Handler for DrReporter { fn handle(&mut self, mut msg: DrReporterMsg, ctx: &mut Self::Context) -> Self::Result { // Remove all reports that have already been reported, but whose reporting transaction is still pending msg.reports.retain(|report| { - if self.pending_report_result.contains(&report.dr_id) { + if self.pending_dr_reports.contains(&report.dr_id) { // Timeout is not over yet, no action is needed log::debug!( - "Request [{}] is already being resolved, ignoring DrReporterMsg", + "[{}] => ignored as it's currently being reported", report.dr_id ); @@ -133,15 +140,18 @@ impl Handler for DrReporter { } let dr_ids: Vec<_> = msg.reports.iter().map(|report| report.dr_id).collect(); - let dr_ids2 = dr_ids.clone(); + let incoming_dr_ids = dr_ids.clone(); let wrb_contract = self.wrb_contract.clone().unwrap(); - let eth_account = self.eth_account; - let report_result_limit = self.report_result_limit; - let num_confirmations = self.num_confirmations; - let eth_confirmation_timeout = Duration::from_millis(self.eth_confirmation_timeout_ms); + let eth_from = self.eth_from; + let eth_from_balance_threshold = self.eth_from_balance_threshold; + let mut eth_from_balance_alert = self.eth_from_balance_alert; + let eth_max_gas = self.eth_max_gas; + let eth_txs_confirmations = self.eth_txs_confirmations; + let eth_tx_timeout = Duration::from_millis(self.eth_txs_timeout_ms); + let eth_nanowit_wei_price = U256::from(self.eth_nanowit_wei_price.unwrap_or_default()); for report in &mut msg.reports { - if report.result.len() > self.max_result_size { + if report.result.len() > self.witnet_dr_max_result_size { let radon_error = RadonErrors::BridgeOversizedResult as u8; report.result = vec![0xD8, 0x27, 0x81, 0x18, radon_error] } @@ -149,170 +159,140 @@ impl Handler for DrReporter { // New request or timeout elapsed, save dr_id for report in &msg.reports { - self.pending_report_result.insert(report.dr_id); + self.pending_dr_reports.insert(report.dr_id); } let eth = self.web3.as_ref().unwrap().eth(); - let report_result_max_network_gas_price_ratio = - self.report_result_max_network_gas_price_ratio; - let fut = async move { - // Check if the request has already been resolved by some old pending transaction - // that got confirmed after the eth_confirmation_timeout has elapsed - let mut reports = vec![]; - for report in msg.reports.drain(..) { - if let Some(set_finished_msg) = - read_resolved_request_from_contract(report.dr_id, &wrb_contract).await - { - // The request is already resolved, mark as finished in the database - let dr_database_addr = DrDatabase::from_registry(); - dr_database_addr.send(set_finished_msg).await.ok(); - } else { - // Not resolved yet, insert back into the list - reports.push(report); + // Trace low funds alerts if required. + let eth_from_balance = match eth.balance(eth_from, None).await { + Ok(x) => { + if x < U256::from(eth_from_balance_threshold) { + eth_from_balance_alert = true; + log::warn!( + "EVM address {} running low of funds: {} ETH", + eth_from, + Unit::Wei(&x.to_string()).to_eth_str().unwrap_or_default() + ); + } else if eth_from_balance_alert { + log::info!("EVM address {} recovered funds.", eth_from); + eth_from_balance_alert = false; + } + + x } - } - msg.reports = reports; + Err(e) => { + log::error!("Error geting balance from address {}: {:?}", eth_from, e); + + return eth_from_balance_alert; + } + }; if msg.reports.is_empty() { // Nothing to report - return; + return eth_from_balance_alert; } - // TODO: max_gas_price is the same for all batches, it could be calculated per-batch - // We don't want to proceed with reporting if there's no way to fetch the report gas - // price from the WRB. - let mut report_gas_price = match get_max_gas_price(&msg, &wrb_contract).await { - Some(x) => x, - None => { - log::error!("Error reading report gas price"); - - return; - } - }; // We don't want to proceed with reporting if there's no way to fetch the gas price // from the provider or gateway. - let network_gas_price = match eth.gas_price().await { + let eth_gas_price = match eth.gas_price().await { Ok(x) => x, Err(e) => { log::error!("Error estimating network gas price: {}", e); - return; + return eth_from_balance_alert; } }; - let max_report_gas_price = u256_saturating_mul_f64( - report_gas_price, - report_result_max_network_gas_price_ratio, - ); - if report_gas_price <= max_report_gas_price { - // If not higher than the allowed ratio, set gas price - if network_gas_price > report_gas_price { - log::debug!("Network gas price is higher than requests' gas price. Setting report gas price to {}", network_gas_price); - } - report_gas_price = network_gas_price; - } else { - // Higher network gas price: show warning but try anyway, the reportResult transaction may fail - let ratio = u256_div_as_f64(network_gas_price, report_gas_price); - log::warn!("Network gas price is {}x higher than request's gas price. Capping report gas price to {}", ratio, max_report_gas_price); - report_gas_price = max_report_gas_price; - } - let batch_results: Vec<_> = msg + let batched_report: Vec<_> = msg .reports .iter() .map(|report| { - let dr_hash = H256::from_slice(report.dr_tx_hash.as_ref()); - + let dr_hash = H256::from_slice(report.dr_tally_tx_hash.as_ref()); // the trait `web3::contract::tokens::Tokenize` is not implemented for // `(std::vec::Vec<(web3::types::U256, web3::types::U256, web3::types::H256, std::vec::Vec)>, bool) // Need to manually convert to tuple Token::Tuple(vec![ Token::Uint(report.dr_id), - Token::Uint(report.timestamp.into()), + Token::Uint(report.dr_timestamp.into()), Token::FixedBytes(dr_hash.to_fixed_bytes().to_vec()), Token::Bytes(report.result.clone()), ]) }) .collect(); - let verbose = true; - let batches = split_by_gas_limit( - batch_results, + + let batched_reports = split_by_gas_limit( + batched_report, &wrb_contract, - eth_account, - report_result_limit, - verbose, - report_gas_price, + eth_from, + eth_gas_price, + eth_nanowit_wei_price, + eth_max_gas, ) .await; - log::debug!( - "Requests [{:?}] will be reported in {} transactions", + log::info!( + "{:?} will be reported in {} transactions", dr_ids, - batches.len() + batched_reports.len(), ); - for (batch_results, estimated_gas_limit) in batches { - if batch_results.len() > 1 { - log::debug!("Executing reportResultBatch {:?}", batch_results); - } else { - log::debug!("Executing reportResult {:?}", batch_results); - } - let params_str; - let only_1_batch = batch_results.len() == 1; - let receipt = if only_1_batch { - let (dr_id, ts, dr_tx_hash, report_result) = - unwrap_batch(batch_results[0].clone()); - params_str = format!( - "reportResult{:?}", - (&dr_id, &ts, &dr_tx_hash, &report_result) - ); - - let receipt_fut = wrb_contract.call_with_confirmations( - "reportResult", - (dr_id, ts, dr_tx_hash, report_result), - eth_account, - contract::Options::with(|opt| { - opt.gas = Some(estimated_gas_limit); - opt.gas_price = Some(report_gas_price); - }), - num_confirmations, - ); - tokio::time::timeout(eth_confirmation_timeout, receipt_fut).await - } else { - params_str = format!("reportResultBatch{:?}", (&batch_results, verbose)); - - let receipt_fut = wrb_contract.call_with_confirmations( - "reportResultBatch", - (batch_results, verbose), - eth_account, - contract::Options::with(|opt| { - opt.gas = Some(estimated_gas_limit); - opt.gas_price = Some(report_gas_price); - }), - num_confirmations, - ); - tokio::time::timeout(eth_confirmation_timeout, receipt_fut).await - }; + for (batched_report, eth_gas_limit) in batched_reports { + // log::debug!("Executing reportResultBatch {:?}", batched_report); + + let receipt_fut = wrb_contract.call_with_confirmations( + "reportResultBatch", + batched_report.clone(), + eth_from, + contract::Options::with(|opt| { + opt.gas = Some(eth_gas_limit); + opt.gas_price = Some(eth_gas_price); + }), + eth_txs_confirmations, + ); + let receipt = tokio::time::timeout(eth_tx_timeout, receipt_fut).await; match receipt { Ok(Ok(receipt)) => { - log::debug!("Request [{:?}], reportResult: {:?}", dr_ids, receipt); + log::debug!("{:?} <> {:?}", dr_ids, receipt); match handle_receipt(&receipt).await { Ok(()) => { - log::debug!("{}: success", params_str); - // Set successful reports as Finished in the database using - // SetFinished message + let mut dismissed_dr_reports: HashSet = Default::default(); for log in receipt.logs { - if let Some(finished_dr_id) = - parse_posted_result_event(wrb_contract.abi(), log) + if let Some((dismissed_dr_id, reason)) = + parse_batch_report_error_log(wrb_contract.abi(), log) { - // We assume that the PostedResult event implies that the - // data request state in the contract is "Reported" or - // "Deleted" - let dr_database_addr = DrDatabase::from_registry(); + if dismissed_dr_reports.insert(dismissed_dr_id) { + log::warn!( + "[{}] >< dismissed due to \"{}\" ...", + dismissed_dr_id, + reason + ); + } + } + } + let dr_database_addr = DrDatabase::from_registry(); + for report in &msg.reports { + if dismissed_dr_reports.contains(&report.dr_id) { + // Dismiss data requests that could not (or need not) get reported + dr_database_addr + .send(SetDrState { + dr_id: report.dr_id, + dr_state: DrState::Dismissed, + }) + .await + .ok(); + } else { + // Finalize data requests that got successfully reported + log::info!( + "[{}] <= dr_tally_tx = {}", + report.dr_id, + report.dr_tally_tx_hash + ); dr_database_addr - .send(SetFinished { - dr_id: finished_dr_id, + .send(SetDrState { + dr_id: report.dr_id, + dr_state: DrState::Finished, }) .await .ok(); @@ -320,264 +300,320 @@ impl Handler for DrReporter { } } Err(()) => { - log::error!("{}: transaction reverted (?)", params_str); + log::error!( + "reportResultBatch(..) tx reverted: {}", + receipt.transaction_hash + ); } } } Ok(Err(e)) => { // Error in call_with_confirmations - log::error!("{}: {:?}", params_str, e); + log::error!( + "{}: {:?}", + format!("Cannot call reportResultBatch{:?}", &batched_report), + e + ); } - Err(_e) => { + Err(elapsed) => { // Timeout is over - log::warn!("{}: timeout is over", params_str); + log::warn!( + "Timeout ({} secs) when calling reportResultBatch{:?}", + elapsed, + &batched_report + ); } } } - }; - ctx.spawn(fut.into_actor(self).map(move |(), act, _ctx| { - // Reset timeouts - for dr_id in dr_ids2 { - act.pending_report_result.remove(&dr_id); + if let Ok(x) = eth.balance(eth_from, None).await { + match x.cmp(ð_from_balance) { + std::cmp::Ordering::Less => { + log::warn!( + "EVM address {} loss = -{} ETH", + eth_from, + Unit::Wei(&(eth_from_balance - x).to_string()) + .to_eth_str() + .unwrap_or_default() + ); + } + std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => { + log::debug!( + "EVM address {} revenue = +{} ETH", + eth_from, + Unit::Wei(&(x - eth_from_balance).to_string()) + .to_eth_str() + .unwrap_or_default() + ); + eth_from_balance_alert = false; + } + } } - })); + + eth_from_balance_alert + }; + + ctx.spawn(fut.into_actor(self).map( + move |eth_from_balance_alert, act, _ctx: &mut Context| { + // Reset timeouts + for dr_id in incoming_dr_ids { + act.pending_dr_reports.remove(&dr_id); + } + act.eth_from_balance_alert = eth_from_balance_alert + }, + )); } } -/// Check if the request is already resolved in the WRB contract -async fn read_resolved_request_from_contract( - dr_id: U256, - wrb_contract: &Contract, -) -> Option { - let query_status: Result = wrb_contract - .query( - "getQueryStatus", - (dr_id,), - None, - contract::Options::default(), - None, - ) - .await; - - match query_status { - Ok(status) => match WitnetQueryStatus::from_code(status) { - WitnetQueryStatus::Unknown => log::debug!("[{}] does not exist, skipping", dr_id), - WitnetQueryStatus::Posted => { - log::debug!("[{}] has not got a result yet, skipping", dr_id) - } - WitnetQueryStatus::Reported => { - log::debug!("[{}] already reported", dr_id); - return Some(SetFinished { dr_id }); - } - WitnetQueryStatus::Deleted => { - log::debug!("[{}] already reported and deleted", dr_id); - return Some(SetFinished { dr_id }); - } - }, - Err(err) => { +/// Get the queryId of a PostedResult event, or return None if this is a different kind of event +fn parse_batch_report_error_log( + wrb_contract_abi: &web3::ethabi::Contract, + log: web3::types::Log, +) -> Option<(DrId, String)> { + let batch_report_error = wrb_contract_abi.events_by_name("BatchReportError").unwrap(); + // There should be exactly one PostedResult event declartion within the ABI + assert_eq!(batch_report_error.len(), 1); + let batch_report_error = &batch_report_error[0]; + // Parse log, ignoring it if the topic does not match "BatchReportError" + let batch_report_error_log = batch_report_error + .parse_log(web3::ethabi::RawLog { + topics: log.topics, + data: log.data.0, + }) + .ok()?; + let batch_report_error_log_params = batch_report_error_log.params; + let query_id = &batch_report_error_log_params[0]; + assert_eq!(query_id.name, "queryId"); + let reason = &batch_report_error_log_params[1]; + assert_eq!(reason.name, "reason"); + match (&query_id.value, &reason.value) { + (Token::Uint(query_id), Token::String(reason)) => Some((*query_id, reason.to_string())), + _ => { log::error!( - "Fail to read getQueryStatus from contract: {:?}", - err.to_string(), + "Invalid BatchReportError params: {:?}", + batch_report_error_log_params ); + None } } - - None } -async fn get_max_gas_price(msg: &DrReporterMsg, wrb_contract: &Contract) -> Option { - // The gas price of the report transaction should equal the maximum gas price paid - // by any of the requests being solved here - let mut max_gas_price: Option = None; - for report in &msg.reports { - // Read gas price - let dr_gas_price: Result = wrb_contract - .query( - "readRequestGasPrice", - report.dr_id, - None, - contract::Options::default(), - None, - ) - .await; - match dr_gas_price { - Ok(dr_gas_price) => { - max_gas_price = match max_gas_price { - None => Some(dr_gas_price), - Some(prev) => Some(std::cmp::max(prev, dr_gas_price)), - } - } - Err(e) => { - log::error!("[{}] ReadGasPrice {:?}", report.dr_id, e); - continue; - } - } - } - - max_gas_price -} - -/// Split batch_param (argument of reportResultBatch) into multiple smaller batch_param in order to -/// fit into the gas limit. +/// Split a batched report (argument of reportResultBatch) into multiple smaller +/// batched reports in order to fit into some gas limit. /// -/// Returns a list of `(batch_param, estimated_gas)` that should be used to create -/// "reportResultBatch" transactions. +/// Returns a list of `(batched_report, estimated_gas)` that should be used to +/// create multiple "reportResultBatch" transactions. async fn split_by_gas_limit( - batch_param: Vec, + batched_report: Vec, wrb_contract: &Contract, - eth_account: H160, - report_result_limit: Option, - verbose: bool, - max_gas_price: U256, + eth_from: H160, + eth_gas_price: U256, + eth_nanowit_wei_price: U256, + eth_max_gas: Option, ) -> Vec<(Vec, U256)> { let mut v = vec![]; - let mut stack = vec![batch_param]; + let mut stack = vec![batched_report]; + + while let Some(batch_params) = stack.pop() { + let eth_report_result_batch_params = batch_params.clone(); + + // -------------------------------------------------------------------------- + // First: try to estimate gas required for reporting this batch of tuples ... - while let Some(batch_param) = stack.pop() { - let params = (batch_param.clone(), verbose); let estimated_gas = wrb_contract .estimate_gas( "reportResultBatch", - params, - eth_account, + eth_report_result_batch_params.clone(), + eth_from, contract::Options::with(|opt| { - opt.gas = report_result_limit.map(Into::into); - opt.gas_price = Some(max_gas_price); + opt.gas = eth_max_gas.map(Into::into); + opt.gas_price = Some(eth_gas_price); }), ) .await; + + if let Err(e) = estimated_gas { + if batch_params.len() <= 1 { + // Skip this single-query batch if still not possible to estimate gas + log::error!("Cannot estimate gas limit: {:?}", e); + log::warn!("Skipping report batch: {:?}", batch_params); + } else { + // Split batch in half if gas estimation is not possible + let (batch_tuples_1, batch_tuples_2) = + batch_params.split_at(batch_params.len() / 2); + stack.push(batch_tuples_1.to_vec()); + stack.push(batch_tuples_2.to_vec()); + } + + continue; + } + + let estimated_gas = estimated_gas.unwrap(); log::debug!( - "reportResultBatch {} estimated gas: {:?}", - batch_param.len(), + "reportResultBatch (x{} drs) estimated gas: {:?}", + batch_params.len(), estimated_gas ); - match estimated_gas { - Ok(estimated_gas) => { - v.push((batch_param, estimated_gas)); + // ------------------------------------------------ + // Second: try to estimate actual profit, if any... + + let query_ids: Vec = batch_params + .iter() + .map(|report_params| { + if let Token::Tuple(report_params) = report_params { + assert_eq!(report_params.len(), 4); + + report_params[0].clone() + } else { + panic!("Cannot extract query id from batch tuple"); + } + }) + .collect(); + + // the size of the report result tx data may affect the actual profit + // on some layer-2 EVM chains: + let eth_report_result_batch_msg_data = wrb_contract + .abi() + .function("reportResultBatch") + .and_then(|f| f.encode_input(ð_report_result_batch_params.into_tokens())); + + let params = ( + Token::Array(query_ids), + Token::Bytes(eth_report_result_batch_msg_data.unwrap_or_default()), + Token::Uint(eth_gas_price), + Token::Uint(eth_nanowit_wei_price), + ); + + let estimated_profit: Result<(U256, U256), web3::contract::Error> = wrb_contract + .query( + "estimateReportEarnings", + params, + eth_from, + contract::Options::with(|opt| { + opt.gas = eth_max_gas.map(Into::into); + opt.gas_price = Some(eth_gas_price); + }), + None, + ) + .await; + + match estimated_profit { + Ok((revenues, expenses)) => { + log::debug!( + "reportResultBatch (x{} drs) estimated profit: {} - {} ETH", + batch_params.len(), + Unit::Wei(&revenues.to_string()) + .to_eth_str() + .unwrap_or_default(), + Unit::Wei(&expenses.to_string()) + .to_eth_str() + .unwrap_or_default(), + ); + v.push((batch_params, estimated_gas)); + continue; } + // Ok(_) => { + // if batch_params.len() <= 1 { + // log::warn!("Skipping unprofitable report: {:?}", batch_params); + // } + // } Err(e) => { - if batch_param.len() <= 1 { - log::error!("reportResultBatch estimate gas: {:?}", e); - log::warn!("skipped dr: {:?}", batch_param); - } else { - // Split batch_param in half - let (batch_param1, batch_param2) = batch_param.split_at(batch_param.len() / 2); - stack.push(batch_param1.to_vec()); - stack.push(batch_param2.to_vec()); + if batch_params.len() <= 1 { + log::error!("Cannot estimate report profit: {:?}", e); } } + }; + + if batch_params.len() > 1 { + // Split batch in half if no profit, or no profit estimation was possible + let (sub_batch_1, sub_batch_2) = batch_params.split_at(batch_params.len() / 2); + stack.push(sub_batch_1.to_vec()); + stack.push(sub_batch_2.to_vec()); } } v } -fn unwrap_batch(t: Token) -> (Token, Token, Token, Token) { - if let Token::Tuple(token_vec) = t { - assert_eq!(token_vec.len(), 4); - ( - token_vec[0].clone(), - token_vec[1].clone(), - token_vec[2].clone(), - token_vec[3].clone(), - ) - } else { - panic!("Token:Tuple not found in unwrap_batch function"); - } -} - -/// Get the queryId of a PostedResult event, or return None if this is a different kind of event -fn parse_posted_result_event( - wrb_contract_abi: &web3::ethabi::Contract, - log: web3::types::Log, -) -> Option { - let posted_result_event = wrb_contract_abi.events_by_name("PostedResult").unwrap(); - // There should be exactly one PostedResult event - assert_eq!(posted_result_event.len(), 1); - let posted_result_event = &posted_result_event[0]; - // Parse log, ignoring it if the topic does not match "PostedResult" - let posted_result_log = posted_result_event - .parse_log(web3::ethabi::RawLog { - topics: log.topics, - data: log.data.0, - }) - .ok()?; - let posted_result_log_params = posted_result_log.params; - let query_id = &posted_result_log_params[0]; - assert_eq!(query_id.name, "queryId"); +#[cfg(test)] +mod tests { + use super::*; + use crate::hack_fix_functions_with_multiple_definitions; + use web3::contract::tokens::Tokenize; - match &query_id.value { - Token::Uint(value) => Some(*value), - x => panic!("Invalid queryId type: {:?} (expected Uint)", x), + /// Returns `a / b`, as f64 + fn u256_div_as_f64(a: U256, b: U256) -> f64 { + u256_to_f64(a) / u256_to_f64(b) } -} - -/// Returns `a / b`, as f64 -fn u256_div_as_f64(a: U256, b: U256) -> f64 { - u256_to_f64(a) / u256_to_f64(b) -} - -/// Converts `U256` into `f64` in a lossy way -fn u256_to_f64(a: U256) -> f64 { - a.to_string().parse().unwrap() -} -/// Returns `a * b` as U256, saturating on overflow -fn u256_saturating_mul_f64(a: U256, b: f64) -> U256 { - assert!( - b >= 0.0, - "u256_mul_f64 only supports positive floating point values, got {}", - b - ); - - // Prevent doing any further calculations if we're multiplying zero by something else. - if a == U256::zero() || b == 0.0 { - return U256::zero(); + /// Converts `U256` into `f64` in a lossy way + fn u256_to_f64(a: U256) -> f64 { + a.to_string().parse().unwrap() } - // Binary search a value x such that x / a == b - let mut lo = U256::from(0); - let mut hi = U256::MAX; - // mid = (lo + hi) / 2, but avoid overflows - let mut mid = lo / 2 + hi / 2; - - loop { - let ratio = u256_div_as_f64(mid, a); + /// Returns `a * b` as U256, saturating on overflow + fn u256_saturating_mul_f64(a: U256, b: f64) -> U256 { + assert!( + b >= 0.0, + "u256_mul_f64 only supports positive floating point values, got {}", + b + ); - if ratio == b { - break mid; - } - if ratio > b { - hi = mid; - } - if ratio < b { - lo = mid; + // Prevent doing any further calculations if we're multiplying zero by something else. + if a == U256::zero() || b == 0.0 { + return U256::zero(); } - let new_mid = lo / 2 + hi / 2; - if new_mid == mid { + // Binary search a value x such that x / a == b + let mut lo = U256::from(0); + let mut hi = U256::MAX; + // mid = (lo + hi) / 2, but avoid overflows + let mut mid = lo / 2 + hi / 2; + + loop { + let ratio = u256_div_as_f64(mid, a); + + if ratio == b { + break mid; + } if ratio > b { - break lo; + hi = mid; } if ratio < b { - break hi; + lo = mid; + } + + let new_mid = lo / 2 + hi / 2; + if new_mid == mid { + if ratio > b { + break lo; + } + if ratio < b { + break hi; + } } + mid = new_mid; } - mid = new_mid; } -} -#[cfg(test)] -mod tests { - use super::*; - use crate::hack_fix_functions_with_multiple_definitions; - use web3::contract::tokens::Tokenize; + fn unwrap_batch(t: Token) -> (Token, Token, Token, Token) { + if let Token::Tuple(token_vec) = t { + assert_eq!(token_vec.len(), 4); + ( + token_vec[0].clone(), + token_vec[1].clone(), + token_vec[2].clone(), + token_vec[3].clone(), + ) + } else { + panic!("Token:Tuple not found in unwrap_batch function"); + } + } #[test] fn report_result_type_check() { - let wrb_contract_abi_json: &[u8] = include_bytes!("../../wrb_abi.json"); + let wrb_contract_abi_json: &[u8] = include_bytes!("../../../wrb_abi.json"); let mut wrb_contract_abi = web3::ethabi::Contract::load(wrb_contract_abi_json) .map_err(|e| format!("Unable to load WRB contract from ABI: {:?}", e)) .unwrap(); @@ -586,11 +622,15 @@ mod tests { let msg = DrReporterMsg { reports: vec![Report { dr_id: DrId::from(4358u32), - timestamp: 0, + dr_timestamp: 0, dr_tx_hash: Hash::SHA256([ 106, 107, 78, 5, 218, 5, 159, 172, 215, 12, 141, 98, 19, 163, 167, 65, 62, 79, 3, 170, 169, 162, 186, 24, 59, 135, 45, 146, 133, 85, 250, 155, ]), + dr_tally_tx_hash: Hash::SHA256([ + 106, 107, 78, 5, 218, 5, 159, 172, 215, 12, 141, 98, 19, 163, 167, 65, 62, 79, + 3, 170, 169, 162, 186, 24, 59, 135, 45, 146, 133, 85, 250, 155, + ]), result: vec![26, 160, 41, 182, 230], }], }; @@ -606,7 +646,7 @@ mod tests { // Need to manually call `.into_tokens()`: Token::Tuple(vec![ Token::Uint(report.dr_id), - Token::Uint(report.timestamp.into()), + Token::Uint(report.dr_timestamp.into()), Token::FixedBytes(dr_hash.to_fixed_bytes().to_vec()), Token::Bytes(report.result.clone()), ]) @@ -629,7 +669,7 @@ mod tests { #[test] fn parse_logs_report_result_batch() { - let wrb_contract_abi_json: &[u8] = include_bytes!("../../wrb_abi.json"); + let wrb_contract_abi_json: &[u8] = include_bytes!("../../../wrb_abi.json"); let mut wrb_contract_abi = web3::ethabi::Contract::load(wrb_contract_abi_json) .map_err(|e| format!("Unable to load WRB contract from ABI: {:?}", e)) .unwrap(); @@ -660,8 +700,11 @@ mod tests { }; assert_eq!( - parse_posted_result_event(&wrb_contract_abi, log_posted_result), - Some(U256::from(63605)), + parse_batch_report_error_log(&wrb_contract_abi, log_posted_result), + Some(( + U256::from(63605), + String::from("WitnetOracle: query not in Posted status"), + )) ); } diff --git a/bridges/centralized-ethereum/src/actors/dr_sender.rs b/bridges/centralized-ethereum/src/actors/dr_sender.rs index e0e390dd2..0f80f64f2 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender.rs @@ -11,6 +11,7 @@ use std::{fmt, time::Duration}; use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; use witnet_data_structures::{ chain::{tapi::current_active_wips, DataRequestOutput, Hashable}, + data_request::calculate_reward_collateral_ratio, error::TransactionError, proto::ProtobufConvert, radon_error::RadonErrors, @@ -28,9 +29,11 @@ mod tests; #[derive(Default)] pub struct DrSender { witnet_client: Option>, - wit_dr_sender_polling_rate_ms: u64, - max_dr_value_nanowits: u64, - dr_fee_nanowits: u64, + witnet_dr_min_collateral_nanowits: u64, + witnet_dr_max_value_nanowits: u64, + witnet_dr_max_fee_nanowits: u64, + witnet_node_pkh: Option, + polling_rate_ms: u64, } impl Drop for DrSender { @@ -49,10 +52,7 @@ impl Actor for DrSender { fn started(&mut self, ctx: &mut Self::Context) { log::debug!("DrSender actor has been started!"); - self.check_new_drs( - ctx, - Duration::from_millis(self.wit_dr_sender_polling_rate_ms), - ); + self.check_new_drs(ctx, Duration::from_millis(self.polling_rate_ms)); } } @@ -66,42 +66,78 @@ impl DrSender { /// Initialize the `DrSender` taking the configuration from a `Config` structure /// and a Json-RPC client connected to a Witnet node pub fn from_config(config: &Config, node_client: Addr) -> Self { - let max_dr_value_nanowits = config.max_dr_value_nanowits; - let wit_dr_sender_polling_rate_ms = config.wit_dr_sender_polling_rate_ms; - let dr_fee_nanowits = config.dr_fee_nanowits; - Self { + polling_rate_ms: config.eth_new_drs_polling_rate_ms / 2 + 1000, witnet_client: Some(node_client), - wit_dr_sender_polling_rate_ms, - max_dr_value_nanowits, - dr_fee_nanowits, + witnet_dr_min_collateral_nanowits: config.witnet_dr_min_collateral_nanowits, + witnet_dr_max_value_nanowits: config.witnet_dr_max_value_nanowits, + witnet_dr_max_fee_nanowits: config.witnet_dr_max_fee_nanowits, + witnet_node_pkh: None, } } fn check_new_drs(&self, ctx: &mut Context, period: Duration) { let witnet_client = self.witnet_client.clone().unwrap(); - let max_dr_value_nanowits = self.max_dr_value_nanowits; - let dr_fee_nanowits = self.dr_fee_nanowits; + let witnet_dr_min_collateral_nanowits = self.witnet_dr_min_collateral_nanowits; + let witnet_dr_max_value_nanowits = self.witnet_dr_max_value_nanowits; + let witnet_dr_max_fee_nanowits = self.witnet_dr_max_fee_nanowits; + let mut witnet_node_pkh = self.witnet_node_pkh.clone(); let fut = async move { let dr_database_addr = DrDatabase::from_registry(); let dr_reporter_addr = DrReporter::from_registry(); + if witnet_node_pkh.is_none() { + // get witnet node's pkh if not yet known + let req = jsonrpc::Request::method("getPkh").timeout(Duration::from_millis(5000)); + let res = witnet_client.send(req).await; + witnet_node_pkh = match res { + Ok(Ok(res)) => match serde_json::from_value::(res) { + Ok(pkh) => { + log::info!("Pkh is {}", pkh); + + Some(pkh) + } + Err(_) => None, + }, + Ok(Err(_)) => { + log::warn!("Cannot deserialize witnet node's pkh, will retry later"); + + None + } + Err(_) => { + log::warn!("Cannot get witnet node's pkh, will retry later"); + + None + } + }; + } else { + // TODO: alert if number of big enough utxos is less number of drs to broadcast + } + + // process latest drs added or set as New in the database let new_drs = dr_database_addr.send(GetAllNewDrs).await.unwrap().unwrap(); let mut dr_reporter_msgs = vec![]; for (dr_id, dr_bytes) in new_drs { - match deserialize_and_validate_dr_bytes(&dr_bytes, max_dr_value_nanowits) { + match deserialize_and_validate_dr_bytes( + &dr_bytes, + witnet_dr_min_collateral_nanowits, + witnet_dr_max_value_nanowits, + ) { Ok(dr_output) => { let req = jsonrpc::Request::method("sendRequest") .timeout(Duration::from_millis(5_000)) - .params(json!({"dro": dr_output, "fee": dr_fee_nanowits})) - .expect("params failed serialization"); + .params(json!({ + "dro": dr_output, + "fee": std::cmp::min(dr_output.witness_reward, witnet_dr_max_fee_nanowits) + })) + .expect("DataRequestOutput params failed serialization"); let res = witnet_client.send(req).await; let res = match res { Ok(res) => res, Err(_) => { - log::error!("Failed to connect to witnet client, will retry later"); + log::error!("Failed to connect to witnet node, will retry later"); break; } }; @@ -110,6 +146,7 @@ impl DrSender { Ok(dr_tx) => { match serde_json::from_value::(dr_tx) { Ok(dr_tx) => { + log::info!("[{}] => dr_tx = {}", dr_id, dr_tx.hash()); // Save dr_tx_hash in database and set state to Pending dr_database_addr .send(SetDrInfoBridge( @@ -126,18 +163,14 @@ impl DrSender { } Err(e) => { // Unexpected error deserializing hash - panic!("[{}] error deserializing dr_tx: {}", dr_id, e); + panic!("[{}] >< cannot deserialize dr_tx: {}", dr_id, e); } } } Err(e) => { // Error sending transaction: node not synced, not enough balance, etc. // Do nothing, will retry later. - log::error!( - "[{}] error creating data request transaction: {}", - dr_id, - e - ); + log::error!("[{}] >< cannot broadcast dr_tx: {}", dr_id, e); continue; } } @@ -145,20 +178,20 @@ impl DrSender { Err(err) => { // Error deserializing or validating data request: mark data request as // error and report error as result to ethereum. - log::error!("[{}] error: {}", dr_id, err); + log::error!("[{}] >< unacceptable data request: {}", dr_id, err); let result = err.encode_cbor(); - // In this case there is no data request transaction, so the dr_tx_hash - // field can be set to anything. - // Except all zeros, because that hash is invalid. - let dr_tx_hash = - "0000000000000000000000000000000000000000000000000000000000000001" + // In this case there is no data request transaction, so + // we set both the dr_tx_hash and dr_tally_tx_hash to zero values. + let zero_hash = + "0000000000000000000000000000000000000000000000000000000000000000" .parse() .unwrap(); dr_reporter_msgs.push(Report { dr_id, - timestamp: 0, - dr_tx_hash, + dr_timestamp: u64::from_ne_bytes(get_timestamp().to_ne_bytes()), + dr_tx_hash: zero_hash, + dr_tally_tx_hash: zero_hash, result, }); } @@ -171,12 +204,15 @@ impl DrSender { }) .await .unwrap(); + + witnet_node_pkh }; - ctx.spawn(fut.into_actor(self).then(move |(), _act, ctx| { + ctx.spawn(fut.into_actor(self).then(move |node_pkh, _act, ctx| { // Wait until the function finished to schedule next call. // This avoids tasks running in parallel. ctx.run_later(period, move |act, ctx| { + act.witnet_node_pkh = node_pkh; // Reschedule check_new_drs act.check_new_drs(ctx, period); }); @@ -190,23 +226,23 @@ impl DrSender { /// an error #[derive(Debug)] enum DrSenderError { - /// The data request bytes are not a valid DataRequestOutput + /// Cannot deserialize data request bytecode as read from the WitnetOracle contract Deserialization { msg: String }, - /// The DataRequestOutput is invalid (wrong number of witnesses, wrong min_consensus_percentage) + /// Invalid data request SLA parameters Validation { msg: String }, - /// The RADRequest is invalid (malformed radon script) + /// Malformed Radon script RadonValidation { msg: String }, - /// The specified collateral amount is invalid + /// Invalid collateral amount InvalidCollateral { msg: String }, /// E.g. the WIP-0022 reward to collateral ratio is not satisfied InvalidReward { msg: String }, - /// Overflow when calculating the data request value + /// Invalid data request total value InvalidValue { msg: String }, /// The cost of the data request is greater than the maximum allowed by the configuration of /// this bridge node ValueGreaterThanAllowed { dr_value_nanowits: u64, - max_dr_value_nanowits: u64, + dr_max_value_nanowits: u64, }, } @@ -233,12 +269,12 @@ impl fmt::Display for DrSenderError { } DrSenderError::ValueGreaterThanAllowed { dr_value_nanowits, - max_dr_value_nanowits, + dr_max_value_nanowits, } => { write!( f, "data request value ({}) higher than maximum allowed ({})", - dr_value_nanowits, max_dr_value_nanowits + dr_value_nanowits, dr_max_value_nanowits ) } } @@ -269,20 +305,17 @@ impl DrSenderError { fn deserialize_and_validate_dr_bytes( dr_bytes: &[u8], - max_dr_value_nanowits: u64, + dr_min_collateral_nanowits: u64, + dr_max_value_nanowits: u64, ) -> Result { match DataRequestOutput::from_pb_bytes(dr_bytes) { Err(e) => Err(DrSenderError::Deserialization { msg: e.to_string() }), Ok(dr_output) => { - // TODO: read from consensus constants - let collateral_minimum = 1_000_000_000; - // TODO: read from consensus_constants - let required_reward_collateral_ratio = - PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; + let mut dr_output = dr_output; validate_data_request_output( &dr_output, - collateral_minimum, - required_reward_collateral_ratio, + dr_min_collateral_nanowits, // dro_hash may be altered if dr_output.collateral goes below this value + PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, ¤t_active_wips(), ) .map_err(|e| match e { @@ -293,12 +326,41 @@ fn deserialize_and_validate_dr_bytes( })?; // Collateral value validation - // If collateral is equal to 0 means that is equal to collateral_minimum value - if (dr_output.collateral != 0) && (dr_output.collateral < collateral_minimum) { + if dr_output.collateral < dr_min_collateral_nanowits { + // modify data request's collateral if below some minimum, + // while maintaining same reward collateral ratio in such case: + let reward_collateral_ratio = calculate_reward_collateral_ratio( + dr_output.collateral, + dr_min_collateral_nanowits, + dr_output.witness_reward, + ); + let dro_hash = dr_output.hash(); + let dro_prev_collateral = dr_output.collateral; + let dro_prev_witness_reward = dr_output.witness_reward; + dr_output.collateral = dr_min_collateral_nanowits; + dr_output.witness_reward = calculate_reward_collateral_ratio( + dr_min_collateral_nanowits, + dr_min_collateral_nanowits, + reward_collateral_ratio, + ); + log::warn!( + "DRO [{}]: witnessing collateral ({}) increased to minimum ({})", + dro_hash, + dro_prev_collateral, + dr_min_collateral_nanowits + ); + log::warn!( + "DRO [{}]: witnessing reward ({}) proportionally increased ({})", + dro_hash, + dro_prev_witness_reward, + dr_output.witness_reward + ) + } + if (dr_output.collateral != 0) && (dr_output.collateral < dr_min_collateral_nanowits) { return Err(DrSenderError::InvalidCollateral { msg: format!( "Collateral ({}) must be greater than the minimum ({})", - dr_output.collateral, collateral_minimum + dr_output.collateral, dr_min_collateral_nanowits ), }); } @@ -311,10 +373,10 @@ fn deserialize_and_validate_dr_bytes( let dr_value_nanowits = dr_output .checked_total_value() .map_err(|e| DrSenderError::InvalidValue { msg: e.to_string() })?; - if dr_value_nanowits > max_dr_value_nanowits { + if dr_value_nanowits > dr_max_value_nanowits { return Err(DrSenderError::ValueGreaterThanAllowed { dr_value_nanowits, - max_dr_value_nanowits, + dr_max_value_nanowits, }); } diff --git a/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs b/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs index b492ea2b0..58ae02e43 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs @@ -113,7 +113,8 @@ fn deserialize_and_validate_dr_bytes_wip_0022() { }; let dro_bytes = dro.to_pb_bytes().unwrap(); - let max_dr_value_nanowits = 100_000_000_000; - let err = deserialize_and_validate_dr_bytes(&dro_bytes, max_dr_value_nanowits).unwrap_err(); + let witnet_dr_max_value_nanowits = 100_000_000_000; + let err = + deserialize_and_validate_dr_bytes(&dro_bytes, witnet_dr_max_value_nanowits).unwrap_err(); assert_eq!(err.encode_cbor(), vec![216, 39, 129, 24, 224]); } diff --git a/bridges/centralized-ethereum/src/actors/eth_poller.rs b/bridges/centralized-ethereum/src/actors/eth_poller.rs index 7619e1d54..8849f4232 100644 --- a/bridges/centralized-ethereum/src/actors/eth_poller.rs +++ b/bridges/centralized-ethereum/src/actors/eth_poller.rs @@ -1,6 +1,7 @@ use crate::{ actors::dr_database::{ - DrDatabase, DrInfoBridge, GetLastDrId, SetDrInfoBridge, WitnetQueryStatus, + DrDatabase, DrInfoBridge, DrState, GetLastDrId, SetDrInfoBridge, SetDrState, + WitnetQueryStatus, }, config::Config, }; @@ -8,9 +9,10 @@ use actix::prelude::*; use std::{convert::TryFrom, sync::Arc, time::Duration}; use web3::{ contract::{self, Contract}, - ethabi::Bytes, + ethabi::{Bytes, Token}, transports::Http, types::U256, + Web3, }; use witnet_node::utils::stop_system_if_panicking; @@ -18,12 +20,16 @@ use witnet_node::utils::stop_system_if_panicking; /// in the DrDatabase #[derive(Default)] pub struct EthPoller { + /// Web3 object + pub web3: Option>, /// WRB contract pub wrb_contract: Option>>, /// Period to check for new requests in the WRB - pub eth_new_dr_polling_rate_ms: u64, + pub polling_rate_ms: u64, /// Skip first requests up to index n when updating database pub skip_first: u64, + /// Max number of queries to be batched together + pub max_batch_size: u16, } impl Drop for EthPoller { @@ -42,10 +48,7 @@ impl Actor for EthPoller { fn started(&mut self, ctx: &mut Self::Context) { log::debug!("EthPoller actor has been started!"); - self.check_new_requests_from_ethereum( - ctx, - Duration::from_millis(self.eth_new_dr_polling_rate_ms), - ); + self.check_new_requests_from_ethereum(ctx, Duration::from_millis(self.polling_rate_ms)); } } @@ -57,22 +60,32 @@ impl SystemService for EthPoller {} impl EthPoller { /// Initialize `PeersManager` taking the configuration from a `Config` structure - pub fn from_config(config: &Config, wrb_contract: Arc>) -> Self { + pub fn from_config( + config: &Config, + web3: Web3, + wrb_contract: Arc>, + ) -> Self { Self { + web3: Some(web3), wrb_contract: Some(wrb_contract), - eth_new_dr_polling_rate_ms: config.eth_new_dr_polling_rate_ms, - skip_first: config.skip_first.unwrap_or(0), + polling_rate_ms: config.eth_new_drs_polling_rate_ms, + skip_first: config.storage_skip_first.unwrap_or(0), + max_batch_size: config.eth_max_batch_size, } } fn check_new_requests_from_ethereum(&self, ctx: &mut Context, period: Duration) { - log::debug!("Checking new DRs from Ethereum contract..."); - let wrb_contract = self.wrb_contract.clone().unwrap(); let skip_first = U256::from(self.skip_first); + let max_batch_size = self.max_batch_size; + + log::debug!("Polling WitnetOracle at {:?}", wrb_contract.address()); + // Check requests let fut = async move { - let total_requests_count: Result = wrb_contract + let dr_database_addr = DrDatabase::from_registry(); + + let next_dr_id: Result = wrb_contract .query( "getNextQueryId", (), @@ -90,73 +103,100 @@ impl EthPoller { err }); - let dr_database_addr = DrDatabase::from_registry(); - let db_request_count = dr_database_addr.send(GetLastDrId).await; + let last_dr_id = dr_database_addr.send(GetLastDrId).await; - if let (Ok(total_requests_count), Ok(Ok(mut db_request_count))) = - (total_requests_count, db_request_count) - { - if db_request_count < skip_first { + if let (Ok(next_dr_id), Ok(Ok(mut last_dr_id))) = (next_dr_id, last_dr_id) { + if last_dr_id < skip_first { log::debug!( - "Skipping first {} requests per skip_first config param", + "Skipping first {} queries as per SKIP_FIRST config param", skip_first ); - db_request_count = skip_first; + last_dr_id = skip_first; } - if db_request_count < total_requests_count { - let init_index = usize::try_from(db_request_count + 1).unwrap(); - let last_index = usize::try_from(total_requests_count).unwrap(); - - for i in init_index..last_index { - log::debug!("[{}] checking dr in wrb", i); - - let query_status: Result = wrb_contract - .query( - "getQueryStatus", - (U256::from(i),), - None, - contract::Options::default(), - None, - ) - .await; - - match query_status { - Ok(status) => match WitnetQueryStatus::from_code(status) { + while last_dr_id < next_dr_id { + let init_index = usize::try_from(last_dr_id + 1).unwrap(); + let last_index = match next_dr_id.cmp(&(last_dr_id + max_batch_size)) { + std::cmp::Ordering::Greater => { + usize::try_from(last_dr_id + max_batch_size).unwrap() + } + _ => usize::try_from(next_dr_id).unwrap(), + }; + let ids = init_index..last_index; + let ids: Vec = ids.map(|id| Token::Uint(id.into())).collect(); + + last_dr_id += U256::from(max_batch_size); + + let queries_status: Result, web3::contract::Error> = wrb_contract + .query( + "getQueryStatusBatch", + ids.clone(), + None, + contract::Options::default(), + None, + ) + .await; + + if let Ok(queries_status) = queries_status { + let mut posted_ids: Vec = vec![]; + for (pos, status) in queries_status.iter().enumerate() { + let query_id = ids[pos].to_owned().into_uint().unwrap(); + let status: u8 = status.to_string().parse().unwrap(); + match WitnetQueryStatus::from_code(status) { WitnetQueryStatus::Unknown => { - log::debug!("[{}] has not exist, skipping", i) + log::warn!("Skipped unavailable query [{}]", query_id); + dr_database_addr.do_send(SetDrState { + dr_id: query_id, + dr_state: DrState::Dismissed, + }); } WitnetQueryStatus::Posted => { - log::info!("[{}] new dr in wrb", i); - if let Ok(set_dr_info_bridge) = - process_posted_request(i.into(), &wrb_contract).await - { - dr_database_addr.do_send(set_dr_info_bridge); - } else { - break; - } + log::info!("Detected new query [{}]", query_id); + posted_ids.push(Token::Uint(query_id)); } - WitnetQueryStatus::Reported => { - log::debug!("[{}] already reported", i); - if let Ok(set_dr_info_bridge) = - process_posted_request(i.into(), &wrb_contract).await - { - dr_database_addr.do_send(set_dr_info_bridge); - } else { - break; - } + WitnetQueryStatus::Reported | WitnetQueryStatus::Finalized => { + log::info!("Skipped already solved query [{}]", query_id); + dr_database_addr.do_send(SetDrState { + dr_id: query_id, + dr_state: DrState::Finished, + }); } - WitnetQueryStatus::Deleted => { - log::debug!("[{}] has been deleted, skipping", i) + } + } + if !posted_ids.is_empty() { + let dr_bytecodes: Result, web3::contract::Error> = + wrb_contract + .query( + "extractWitnetDataRequests", + posted_ids.clone(), + None, + contract::Options::default(), + None, + ) + .await; + if let Ok(dr_bytecodes) = dr_bytecodes { + for (pos, dr_id) in posted_ids.iter().enumerate() { + dr_database_addr.do_send(SetDrInfoBridge( + dr_id.to_owned().into_uint().unwrap(), + DrInfoBridge { + dr_bytes: dr_bytecodes[pos].to_owned(), + ..Default::default() + }, + )); } - }, - Err(err) => { + } else { log::error!( - "Fail to read getQueryStatus from contract: {:?}", - err.to_string(), + "Fail to extract Witnet request bytecodes from queries {:?}", + posted_ids ); - break; } } + } else { + log::error!( + "Fail to get status of queries #{} to #{}: {}", + init_index, + next_dr_id, + queries_status.unwrap_err().to_string() + ); } } } @@ -174,48 +214,3 @@ impl EthPoller { })); } } - -/// Auxiliary function that process the information of a new posted request -async fn process_posted_request( - query_id: U256, - wrb_contract: &Contract, -) -> Result { - let dr_bytes: Result = wrb_contract - .query( - "readRequestBytecode", - (query_id,), - None, - contract::Options::default(), - None, - ) - .await; - - // Re-route some errors as success (explanation below) - match dr_bytes { - Ok(dr_bytes) => Ok(dr_bytes), - Err(err) => { - log::error!("Fail to read dr bytes from contract: {}", err.to_string()); - - // In some versions of the bridge contracts (those based on - // `WitnetRequestBoardTrustableBase`), we may get a revert when trying to fetch the dr - // bytes for a deleted query. - // If that's the case, we can return a success here, with empty bytes, so that the - // request can locally marked as complete, and we can move on. - if err.to_string().contains("WitnetRequestBoardTrustableBase") { - log::error!("Wait! This is an instance of `WitnetRequestBoardTrustableBase`. Let's assume we got a revert because the dr bytes were deleted, and simply move on."); - - Ok(Default::default()) - // Otherwise, handle the error normally - } else { - Err(err) - } - } - // Wrap the dr bytes in a `SetDrInfoBridge` structure - }.map(|dr_bytes| SetDrInfoBridge( - query_id, - DrInfoBridge { - dr_bytes, - ..Default::default() - }, - )) -} diff --git a/bridges/centralized-ethereum/src/actors/mod.rs b/bridges/centralized-ethereum/src/actors/mod.rs index 94a2b84d2..0f80bbc0c 100644 --- a/bridges/centralized-ethereum/src/actors/mod.rs +++ b/bridges/centralized-ethereum/src/actors/mod.rs @@ -12,3 +12,6 @@ pub mod eth_poller; /// wit_poller actor module pub mod wit_poller; + +/// watch_dog actor module +pub mod watch_dog; diff --git a/bridges/centralized-ethereum/src/actors/watch_dog.rs b/bridges/centralized-ethereum/src/actors/watch_dog.rs new file mode 100644 index 000000000..eb500c6dd --- /dev/null +++ b/bridges/centralized-ethereum/src/actors/watch_dog.rs @@ -0,0 +1,391 @@ +use crate::{ + actors::dr_database::{CountDrsPerState, DrDatabase}, + config::Config, +}; +use actix::prelude::*; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; +use web3::{ + contract::Contract, + transports::Http, + types::H160, +}; +use witnet_net::client::tcp::{jsonrpc, JsonRpcClient}; +use witnet_node::utils::stop_system_if_panicking; + +/// EthPoller actor reads periodically new requests from the WRB Contract and includes them +/// in the DrDatabase +#[derive(Default)] +pub struct WatchDog { + /// JSON WIT/RPC client connection to Wit/node + pub wit_client: Option>, + /// JSON WIT/RPC socket address + pub wit_jsonrpc_socket: String, + /// Bridge UTXO min value threshold + pub wit_utxo_min_value_threshold: u64, + /// Web3 object + pub eth_jsonrpc_url: String, + /// Web3 signer address + pub eth_account: H160, + /// WitOracle bridge contract + pub eth_contract: Option>>, + /// Polling period for global status + pub polling_rate_ms: u64, + /// Instant at which the actor is created + pub start_ts: Option, + /// Eth balance upon first metric report: + pub start_eth_balance: Option, + /// Wit balance upon last refund + pub start_wit_balance: Option, +} + +impl Drop for WatchDog { + fn drop(&mut self) { + log::trace!("Dropping WatchDog"); + stop_system_if_panicking("WatchDog"); + } +} + +/// Make actor from EthPoller +impl Actor for WatchDog { + /// Every actor has to provide execution Context in which it can run. + type Context = Context; + + /// Method to be executed when the actor is started + fn started(&mut self, ctx: &mut Self::Context) { + log::debug!("WatchDog actor has been started!"); + + self.watch_global_status(None, None, ctx, Duration::from_millis(self.polling_rate_ms)); + } +} + +#[derive(Debug, PartialEq)] +enum WatchDogStatus { + EvmDisconnect, + EvmErrors, + EvmSyncing, + WitAlmostSynced, + WitErrors, + WitDisconnect, + WitSyncing, + WitWaitingConsensus, + UpAndRunning +} + +impl WatchDogStatus { + fn to_string(&self) -> String { + match self { + WatchDogStatus::EvmDisconnect => "evm-disconnect".to_string(), + WatchDogStatus::EvmErrors => format!("evm-errors"), + WatchDogStatus::EvmSyncing => "evm-syncing".to_string(), + WatchDogStatus::WitAlmostSynced => "wit-almost-synced".to_string(), + WatchDogStatus::WitDisconnect => "wit-disconnect".to_string(), + WatchDogStatus::WitErrors => format!("wit-errors"), + WatchDogStatus::WitSyncing => "wit-syncing".to_string(), + WatchDogStatus::WitWaitingConsensus => "wit-waiting-consensus".to_string(), + WatchDogStatus::UpAndRunning => "up-and-running".to_string(), + } + } +} + +/// Required trait for being able to retrieve WatchDog address from system registry +impl actix::Supervised for WatchDog {} +impl SystemService for WatchDog {} + +impl WatchDog { + /// Initialize from config + pub fn from_config(config: &Config, eth_contract: Arc>) -> Self { + Self { + wit_client: JsonRpcClient::start(config.witnet_jsonrpc_socket.to_string().as_str()).ok(), + wit_jsonrpc_socket: config.witnet_jsonrpc_socket.to_string(), + wit_utxo_min_value_threshold: config.witnet_utxo_min_value_threshold, + eth_account: config.eth_from, + eth_contract: Some(eth_contract), + eth_jsonrpc_url: config.eth_jsonrpc_url.clone(), + polling_rate_ms: config.watch_dog_polling_rate_ms, + start_ts: Some(Instant::now()), + start_eth_balance: None, + start_wit_balance: None, + } + } + + fn watch_global_status( + &mut self, + eth_balance: Option, + wit_balance: Option, + ctx: &mut Context, + period: Duration, + ) { + if self.start_eth_balance.is_none() && eth_balance.is_some() { + self.start_eth_balance = eth_balance; + } + if let Some(wit_balance) = wit_balance { + if wit_balance > self.start_wit_balance.unwrap_or_default() { + self.start_wit_balance = Some(wit_balance); + log::warn!("Wit account refunded to {} $WIT", wit_balance); + } + } + let start_eth_balance = self.start_eth_balance; + let start_wit_balance = self.start_wit_balance; + let wit_client = self.wit_client.clone(); + let wit_jsonrpc_socket = self.wit_jsonrpc_socket.clone(); + let wit_utxo_min_value_threshold = self.wit_utxo_min_value_threshold; + let eth_jsonrpc_url = self.eth_jsonrpc_url.clone(); + let eth_account = self.eth_account; + let eth_contract_address = self.eth_contract.clone().unwrap().address(); + let running_secs = self.start_ts.unwrap().elapsed().as_secs(); + + let fut = async move { + let mut status = WatchDogStatus::UpAndRunning; + + let dr_database = DrDatabase::from_registry(); + let (_, drs_pending, drs_finished, _) = + dr_database.send(CountDrsPerState).await.unwrap().unwrap(); + + let mut metrics: String = "{".to_string(); + metrics.push_str(&format!("\"drsFinished\": {drs_finished}, ")); + metrics.push_str(&format!("\"drsPending\": {drs_pending}, ")); + metrics.push_str(&format!("\"evmAccount\": \"{eth_account}\", ")); + + if let Some(wit_client) = wit_client { + if let Err(err) = check_wit_connection_status(&wit_client).await { + status = err; + } + + let (wit_account, wit_balance, wit_utxos_above_threshold) = match fetch_wit_info( + &wit_client, + wit_utxo_min_value_threshold + ).await { + Ok((wit_account, wit_balance, wit_utxos_above_threshold)) => { + (wit_account, wit_balance, wit_utxos_above_threshold) + } + Err(err) => { + if status == WatchDogStatus::UpAndRunning { + status = err; + } + (None, None, None) + } + }; + + if wit_account.is_some() { + metrics.push_str(&format!("\"witAccount\": {:?}, ", wit_account.unwrap())); + } + if wit_balance.is_some() { + let wit_balance = wit_balance.unwrap(); + metrics.push_str(&format!("\"witBalance\": {:.5}, ", wit_balance)); + if let Some(start_wit_balance) = start_wit_balance { + let wit_hourly_expenditure = + ((start_wit_balance - wit_balance) / running_secs as f64) * 3600_f64; + metrics.push_str(&format!( + "\"witHourlyExpenditure\": {:.1}, ", + wit_hourly_expenditure + )); + } + } + metrics.push_str(&format!("\"witNodeSocket\": \"{wit_jsonrpc_socket}\", ")); + if wit_utxos_above_threshold.is_some() { + metrics.push_str(&format!( + "\"witUtxosAboveThreshold\": {}, ", + wit_utxos_above_threshold.unwrap() + )); + } + } + + let eth_balance = match check_eth_account_balance(ð_jsonrpc_url, eth_account).await { + Ok(eth_balance) => eth_balance, + Err(err) => { + if status == WatchDogStatus::UpAndRunning { + status = err; + } + None + } + }; + + if eth_balance.is_some() { + let eth_balance = eth_balance.unwrap(); + metrics.push_str(&format!("\"evmBalance\": {:.5}, ", eth_balance)); + metrics.push_str(&format!("\"evmContract\": \"{eth_contract_address}\", ")); + if let Some(start_eth_balance) = start_eth_balance { + let eth_hourly_earnings = + ((eth_balance - start_eth_balance) / running_secs as f64) * 3600_f64; + metrics.push_str(&format!( + "\"evmHourlyEarnings\": {:.5}, ", + eth_hourly_earnings + )); + } + } + + metrics.push_str(&format!("\"runningSecs\": {running_secs}, ")); + metrics.push_str(&format!("\"status\": \"{}\"", status.to_string())); + metrics.push_str("}}"); + log::info!("{metrics}"); + + (eth_balance, wit_balance) + }; + + ctx.spawn( + fut.into_actor(self) + .then(move |(eth_balance, wit_balance), _act, ctx| { + // Schedule next iteration only when finished, + // as to avoid multiple tasks running in parallel + ctx.run_later(period, move |act, ctx| { + act.watch_global_status(eth_balance, wit_balance, ctx, period); + }); + actix::fut::ready(()) + }), + ); + } +} + +async fn check_eth_account_balance( + eth_jsonrpc_url: &str, + eth_account: H160, +) -> Result, WatchDogStatus> { + let web3_http = web3::transports::Http::new(eth_jsonrpc_url) + .map_err(|_e| WatchDogStatus::EvmDisconnect) + .unwrap(); + + let web3 = web3::Web3::new(web3_http); + match web3.eth().syncing().await { + Ok(syncing) => match syncing { + web3::types::SyncState::NotSyncing => { + match web3.eth().balance(eth_account, None).await { + Ok(eth_balance) => { + let eth_balance: f64 = eth_balance.to_string().parse().unwrap_or_default(); + Ok(Some(eth_balance / 1000000000000000000.0)) + } + _ => Ok(None), + } + } + web3::types::SyncState::Syncing(_) => Err(WatchDogStatus::EvmSyncing), + }, + Err(e) => { + log::debug!("check_eth_account_balance => {}", e); + + Err(WatchDogStatus::EvmErrors) + } + } +} + +async fn check_wit_connection_status(wit_client: &Addr) -> Result<(), WatchDogStatus> { + let req = jsonrpc::Request::method("syncStatus").timeout(Duration::from_secs(5)); + let res = wit_client.send(req).await; + match res { + Ok(Ok(result)) => { + if let Some(node_state) = result["node_state"].as_str() { + match node_state { + "Synced" => Ok(()), + "AlmostSynced" => Err(WatchDogStatus::WitAlmostSynced), + "WaitingConsensus" => Err(WatchDogStatus::WitWaitingConsensus), + _ => Err(WatchDogStatus::WitSyncing) + } + } else { + log::debug!("check_wit_connection_status => unknown node_state"); + Err(WatchDogStatus::WitErrors) + } + } + Ok(Err(err)) => { + log::debug!("check_wit_connection_status => {}", err); + Err(WatchDogStatus::WitDisconnect) + } + Err(err) => { + log::debug!("check_wit_connection_status => {}", err); + Err(WatchDogStatus::WitDisconnect) + } + } +} + +async fn fetch_wit_info ( + wit_client: &Addr, + wit_utxos_min_threshold: u64, +) -> Result<(Option, Option, Option), WatchDogStatus> { + let req = jsonrpc::Request::method("getPkh").timeout(Duration::from_secs(5)); + let res = wit_client.send(req).await; + let wit_account = match res { + Ok(Ok(res)) => match serde_json::from_value::(res) { + Ok(pkh) => Some(pkh), + Err(_) => None, + }, + Ok(Err(_)) => None, + Err(err) => { + log::debug!("fetch_wit_info => {}", err); + return Err(WatchDogStatus::WitErrors); + } + }; + + let wit_account_balance = match wit_account.clone() { + Some(wit_account) => { + let req = jsonrpc::Request::method("getBalance") + .timeout(Duration::from_secs(5)) + .params(wit_account) + .expect("getBalance wrong params"); + let res = wit_client.send(req).await; + let res = match res { + Ok(res) => res, + Err(err) => { + log::debug!("fetch_wit_info => {}", err); + return Err(WatchDogStatus::WitErrors); + } + }; + match res { + Ok(value) => match value.get("total") { + Some(value) => match value.as_f64() { + Some(value) => Some(value / 1000000000.0), + None => None, + }, + None => None, + }, + Err(err) => { + log::debug!("fetch_wit_info => {}", err); + return Err(WatchDogStatus::WitErrors); + } + } + } + None => None, + }; + + let wit_utxos_above_threshold = match wit_account.clone() { + Some(wit_account) => { + let req = jsonrpc::Request::method("getUtxoInfo") + .timeout(Duration::from_secs(5)) + .params(wit_account) + .expect("getUtxoInfo wrong params"); + let res = wit_client.send(req).await; + let res = match res { + Ok(res) => res, + Err(err) => { + log::debug!("fetch_wit_info => {}", err); + return Err(WatchDogStatus::WitErrors); + } + }; + match res { + Ok(utxo_info) => { + if let Some(utxos) = utxo_info["utxos"].as_array() { + let mut counter: u64 = u64::default(); + for utxo in utxos { + if let Some(value) = utxo["value"].as_u64() { + if value >= wit_utxos_min_threshold { + counter += 1; + } + } + } + + Some(counter) + } else { + None + } + } + Err(err) => { + log::debug!("fetch_wit_info => {}", err); + return Err(WatchDogStatus::WitErrors); + } + } + } + None => None, + }; + + + Ok((wit_account, wit_account_balance, wit_utxos_above_threshold)) +} diff --git a/bridges/centralized-ethereum/src/actors/wit_poller.rs b/bridges/centralized-ethereum/src/actors/wit_poller.rs index e9851dee3..b3525495f 100644 --- a/bridges/centralized-ethereum/src/actors/wit_poller.rs +++ b/bridges/centralized-ethereum/src/actors/wit_poller.rs @@ -9,7 +9,7 @@ use actix::prelude::*; use serde_json::json; use std::{convert::TryFrom, time::Duration}; use witnet_data_structures::chain::{ - Block, ConsensusConstants, DataRequestInfo, Epoch, EpochConstants, Hash, + Block, ConsensusConstants, DataRequestInfo, Epoch, EpochConstants, Hash, Hashable, }; use witnet_net::client::tcp::{jsonrpc, JsonRpcClient}; use witnet_node::utils::stop_system_if_panicking; @@ -20,8 +20,8 @@ use witnet_util::timestamp::get_timestamp; #[derive(Default)] pub struct WitPoller { witnet_client: Option>, - wit_tally_polling_rate_ms: u64, - dr_tx_unresolved_timeout_ms: Option, + witnet_dr_txs_polling_rate_ms: u64, + witnet_dr_txs_timeout_ms: u64, } impl Drop for WitPoller { @@ -40,7 +40,10 @@ impl Actor for WitPoller { fn started(&mut self, ctx: &mut Self::Context) { log::debug!("WitPoller actor has been started!"); - self.check_tally_pending_drs(ctx, Duration::from_millis(self.wit_tally_polling_rate_ms)) + self.check_tally_pending_drs( + ctx, + Duration::from_millis(self.witnet_dr_txs_polling_rate_ms), + ) } } @@ -54,19 +57,16 @@ impl WitPoller { /// Initialize the `WitPoller` taking the configuration from a `Config` structure /// and a Json-RPC client connected to a Witnet node pub fn from_config(config: &Config, node_client: Addr) -> Self { - let wit_tally_polling_rate_ms = config.wit_tally_polling_rate_ms; - let dr_tx_unresolved_timeout_ms = config.dr_tx_unresolved_timeout_ms; - Self { witnet_client: Some(node_client), - wit_tally_polling_rate_ms, - dr_tx_unresolved_timeout_ms, + witnet_dr_txs_polling_rate_ms: config.witnet_dr_txs_polling_rate_ms, + witnet_dr_txs_timeout_ms: config.witnet_dr_txs_timeout_ms, } } fn check_tally_pending_drs(&self, ctx: &mut Context, period: Duration) { let witnet_client = self.witnet_client.clone().unwrap(); - let dr_tx_unresolved_timeout_ms = self.dr_tx_unresolved_timeout_ms; + let timeout_secs = i64::try_from(self.witnet_dr_txs_timeout_ms / 1000).unwrap(); let fut = async move { let dr_database_addr = DrDatabase::from_registry(); @@ -95,82 +95,74 @@ impl WitPoller { } }; - let report = match report { - Ok(report) => report, - - Err(e) => { - log::debug!( - "[{}] dataRequestReport call error: {}", - dr_id, - e.to_string() - ); + if let Ok(report) = report { + match serde_json::from_value::>(report) { + Ok(Some(DataRequestInfo { + tally: Some(tally), + block_hash_dr_tx: Some(dr_block_hash), + current_commit_round: dr_commits_round, + .. + })) => { + log::info!("[{}] <= dr_tx = {}", dr_id, dr_tx_hash); - if let Some(dr_timeout_ms) = dr_tx_unresolved_timeout_ms { - // In case of error, if the data request has been unresolved for more than - // X milliseconds, retry by setting it to "New" - if (current_timestamp - dr_tx_creation_timestamp) - > i64::try_from(dr_timeout_ms / 1000).unwrap() + let result = tally.tally.clone(); + // Get timestamp of the epoch at which all data request commit txs + // were incuded in the Witnet blockchain: + let dr_timestamp = match get_dr_timestamp( + witnet_client.clone(), + dr_block_hash, + dr_commits_round, + ) + .await { - log::debug!("[{}] has been unresolved after more than {} ms, setting to New", dr_id, dr_timeout_ms); - dr_database_addr - .send(SetDrInfoBridge( - dr_id, - DrInfoBridge { - dr_bytes, - dr_state: DrState::New, - dr_tx_hash: None, - dr_tx_creation_timestamp: None, - }, - )) - .await - .unwrap(); - } - } - continue; - } - }; - - match serde_json::from_value::>(report) { - Ok(Some(DataRequestInfo { - tally: Some(tally), - block_hash_dr_tx: Some(dr_block_hash), - .. - })) => { - log::info!( - "[{}] Found possible tally to be reported for dr_tx_hash {}", - dr_id, - dr_tx_hash - ); - - let result = tally.tally; - // Get timestamp of first block with commits. The timestamp of the data - // point is the timestamp of that block minus 45 seconds, because the commit - // transactions are created one epoch earlier. - // TODO: first block with commits is hard to obtain, we are simply using the - // block that included the data request. - let timestamp = - match get_block_timestamp(witnet_client.clone(), dr_block_hash).await { Ok(timestamp) => timestamp, Err(()) => continue, }; - dr_reporter_msgs.push(Report { + dr_reporter_msgs.push(Report { + dr_id, + dr_timestamp, + dr_tx_hash, + dr_tally_tx_hash: tally.hash(), + result, + }); + } + Ok(..) => { + // the data request is being resolved, just not yet + } + Err(e) => { + log::error!( + "[{}] => cannot deserialize dr_tx = {}: {:?}", + dr_id, + dr_tx_hash, + e + ); + } + }; + } else { + log::debug!("[{}] <> dr_tx = {}", dr_id, dr_tx_hash); + } + + let elapsed_secs = current_timestamp - dr_tx_creation_timestamp; + if elapsed_secs >= timeout_secs { + log::warn!( + "[{}] => will retry new dr_tx after {} secs", + dr_id, + elapsed_secs + ); + DrDatabase::from_registry() + .send(SetDrInfoBridge( dr_id, - timestamp, - dr_tx_hash, - result, - }); - } - Ok(..) => { - // No problem, this means the data request has not been resolved yet - log::debug!("[{}] Data request not resolved yet", dr_id); - continue; - } - Err(e) => { - log::error!("[{}] dataRequestReport deserialize error: {:?}", dr_id, e); - continue; - } - }; + DrInfoBridge { + dr_bytes, + dr_state: DrState::New, + dr_tx_hash: None, + dr_tx_creation_timestamp: None, + }, + )) + .await + .unwrap(); + } } dr_reporter_addr @@ -195,12 +187,13 @@ impl WitPoller { } /// Return the timestamp of this block hash -async fn get_block_timestamp( +async fn get_dr_timestamp( witnet_client: Addr, - block_hash: Hash, + drt_block_hash: Hash, + dr_commits_round: u16, ) -> Result { let method = String::from("getBlock"); - let params = json!([block_hash]); + let params = json!([drt_block_hash]); let req = jsonrpc::Request::method(method) .timeout(Duration::from_millis(5_000)) .params(params) @@ -216,7 +209,7 @@ async fn get_block_timestamp( let block = match report { Ok(value) => serde_json::from_value::(value).expect("failed to deserialize block"), Err(e) => { - log::error!("error in getBlock call ({}): {:?}", block_hash, e); + log::error!("error in getBlock call ({}): {:?}", drt_block_hash, e); return Err(()); } }; @@ -232,9 +225,10 @@ async fn get_block_timestamp( checkpoint_zero_timestamp: consensus_constants.checkpoint_zero_timestamp, checkpoints_period: consensus_constants.checkpoints_period, }; - // TODO: try to guess commit block by adding +1 to block_epoch - // When we actually use the hash of the commit block, this +1 must be removed - let timestamp = convert_block_epoch_to_timestamp(epoch_constants, block_epoch + 1); + let timestamp = convert_block_epoch_to_timestamp( + epoch_constants, + block_epoch + u32::from(dr_commits_round + 1), + ); Ok(timestamp) } diff --git a/bridges/centralized-ethereum/src/config.rs b/bridges/centralized-ethereum/src/config.rs index 8a4affce5..bc895a1e8 100644 --- a/bridges/centralized-ethereum/src/config.rs +++ b/bridges/centralized-ethereum/src/config.rs @@ -13,57 +13,72 @@ use witnet_data_structures::chain::Environment; #[derive(Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { - /// Address of the witnet node JSON-RPC server - pub witnet_jsonrpc_addr: SocketAddr, + /// Ethereum account used to report data request results + pub eth_from: H160, + /// Ethereum account balance under which alerts will be logged + pub eth_from_balance_threshold: u64, + /// Gas limits for some methods. If missing, let the client estimate + #[serde(deserialize_with = "nested_toml_if_using_envy")] + pub eth_gas_limits: Gas, /// Url of the ethereum client - pub eth_client_url: String, - /// Address of the WitnetRequestsBoard deployed contract - pub wrb_contract_addr: H160, - /// Address of a Request example deployed contract - pub request_example_contract_addr: H160, - /// Ethereum account used to create the transactions - pub eth_account: H160, - /// Period to check for new requests in the WRB - pub eth_new_dr_polling_rate_ms: u64, - /// Period to check for completed requests in Witnet - pub wit_tally_polling_rate_ms: u64, - /// Period to post new requests to Witnet - pub wit_dr_sender_polling_rate_ms: u64, - /// If the data request has been sent to witnet but it is not included in a block, retry after this many milliseconds - pub dr_tx_unresolved_timeout_ms: Option, - /// Max value that will be accepted by the bridge node in a data request - pub max_dr_value_nanowits: u64, + pub eth_jsonrpc_url: String, + /// Max number of queries to be batched together + #[serde(default = "default_max_batch_size")] + pub eth_max_batch_size: u16, + /// Price of $nanoWit in Wei, used to improve estimation of report profits + pub eth_nanowit_wei_price: Option, + /// Polling period for checking new queries in the WitnetOracle contract + pub eth_new_drs_polling_rate_ms: u64, + /// Number of block confirmations needed to assume finality when sending transactions to ethereum + #[serde(default = "one")] + pub eth_txs_confirmations: usize, + /// Max time to wait for an ethereum transaction to be confirmed before returning an error + pub eth_txs_timeout_ms: u64, + /// Address of the WitnetRequestsBoard contract + pub eth_witnet_oracle: H160, + + /// Let the dog out? + pub watch_dog_enabled: bool, + /// Watch dog polling rate + #[serde(default = "default_watch_dog_polling_rate_ms")] + pub watch_dog_polling_rate_ms: u64, + + /// Minimum collateral required on data requests read from the WitnetOracle contract + pub witnet_dr_min_collateral_nanowits: u64, + /// Maximium data request transaction fee assumed by the bridge + pub witnet_dr_max_fee_nanowits: u64, + /// Maximum data request result size (in bytes) will accept to report + pub witnet_dr_max_result_size: usize, + /// Maximum data request value that the bridge will accept to relay + pub witnet_dr_max_value_nanowits: u64, + /// Polling period for checking resolution of data requests in the Witnet blockchain + pub witnet_dr_txs_polling_rate_ms: u64, + /// Max time to wait for data request resolutions, in milliseconds + pub witnet_dr_txs_timeout_ms: u64, + /// Address of the witnet node JSON-RPC server + pub witnet_jsonrpc_socket: SocketAddr, /// Running in the witnet testnet? pub witnet_testnet: bool, - /// Gas limits for some methods. If missing, let the client estimate - #[serde(deserialize_with = "nested_toml_if_using_envy")] - pub gas_limits: Gas, + /// Bridge UTXO min value threshold + pub witnet_utxo_min_value_threshold: u64, + /// Storage #[serde(deserialize_with = "nested_toml_if_using_envy")] pub storage: Storage, /// Skip first requests up to index n when updating database - pub skip_first: Option, - /// Maximum data request result size (in bytes) - pub max_result_size: usize, - /// Max time to wait for an ethereum transaction to be confirmed before returning an error - pub eth_confirmation_timeout_ms: u64, - /// Number of block confirmations needed to assume finality when sending transactions to ethereum - #[serde(default = "one")] - pub num_confirmations: usize, - /// Miner fee for the witnet data request transactions, in nanowits - pub dr_fee_nanowits: u64, - /// Max ratio between the gas price recommended by the provider and the gas price of the requests in the WRB - /// That is, the bridge will refrain from paying more than these times the gas price originally set forth by the requesters. - #[serde(default = "one_f64")] - pub report_result_max_network_gas_price_ratio: f64, + pub storage_skip_first: Option, } fn one() -> usize { 1 } -fn one_f64() -> f64 { - 1.0 +fn default_max_batch_size() -> u16 { + 256 +} + +fn default_watch_dog_polling_rate_ms() -> u64 { + 900_000 } /// Gas limits for some methods. If missing, let the client estimate @@ -120,7 +135,7 @@ pub fn from_env() -> Result { thread_local! { /// Thread-local flag to indicate the `nested_toml_if_using_envy` function that we are indeed /// using envy. - static USING_ENVY: Cell = Cell::new(false); + static USING_ENVY: Cell = const { Cell::new(false) }; } /// If using the `envy` crate to deserialize this value, try to deserialize it as a TOML string. @@ -167,7 +182,7 @@ mod tests { struct SmallConfig { /// Gas limits for some methods. If missing, let the client estimate #[serde(deserialize_with = "nested_toml_if_using_envy")] - pub gas_limits: Gas, + pub eth_gas_limits: Gas, /// Storage #[serde(deserialize_with = "nested_toml_if_using_envy")] pub storage: Storage, @@ -186,7 +201,7 @@ mod tests { ]; let expected = SmallConfig { - gas_limits: Gas { + eth_gas_limits: Gas { post_data_request: Some(10_000), report_result: Some(20_000), }, diff --git a/bridges/centralized-ethereum/src/lib.rs b/bridges/centralized-ethereum/src/lib.rs index 5ae56d675..90fcb5992 100644 --- a/bridges/centralized-ethereum/src/lib.rs +++ b/bridges/centralized-ethereum/src/lib.rs @@ -24,15 +24,15 @@ pub mod config; /// Creates a Witnet Request Board contract from Config information pub fn create_wrb_contract( - eth_client_url: &str, - wrb_contract_addr: H160, + eth_jsonrpc_url: &str, + eth_witnet_oracle: H160, ) -> (Web3, Contract) { - let web3_http = web3::transports::Http::new(eth_client_url) + let web3_http = web3::transports::Http::new(eth_jsonrpc_url) .map_err(|e| format!("Failed to connect to Ethereum client.\nError: {:?}", e)) .unwrap(); let web3 = web3::Web3::new(web3_http); // Why read files at runtime when you can read files at compile time - let wrb_contract_abi_json: &[u8] = include_bytes!("../wrb_abi.json"); + let wrb_contract_abi_json: &[u8] = include_bytes!("../../wrb_abi.json"); let mut wrb_contract_abi = web3::ethabi::Contract::load(wrb_contract_abi_json) .map_err(|e| format!("Unable to load WRB contract from ABI: {:?}", e)) .unwrap(); @@ -41,7 +41,7 @@ pub fn create_wrb_contract( // https://github.com/witnet/witnet-rust/issues/2046 hack_fix_functions_with_multiple_definitions(&mut wrb_contract_abi); - let wrb_contract = Contract::new(web3.eth(), wrb_contract_addr, wrb_contract_abi); + let wrb_contract = Contract::new(web3.eth(), eth_witnet_oracle, wrb_contract_abi); (web3, wrb_contract) } @@ -108,8 +108,8 @@ pub async fn check_witnet_node_running(witnet_addr: &str) -> Result<(), String> } /// Check if the ethereum node is running -pub async fn check_ethereum_node_running(eth_client_url: &str) -> Result<(), String> { - let web3_http = web3::transports::Http::new(eth_client_url) +pub async fn check_ethereum_node_running(eth_jsonrpc_url: &str) -> Result<(), String> { + let web3_http = web3::transports::Http::new(eth_jsonrpc_url) .map_err(|e| format!("Failed to connect to Ethereum client.\nError: {:?}", e)) .unwrap(); let web3 = web3::Web3::new(web3_http); @@ -118,7 +118,7 @@ pub async fn check_ethereum_node_running(eth_client_url: &str) -> Result<(), Str let res = web3.eth().syncing().await; match res { Ok(syncing) => { - log::debug!("Ethereum node is running at {}", eth_client_url); + log::debug!("Ethereum node is running at {}", eth_jsonrpc_url); match syncing { web3::types::SyncState::NotSyncing => {} web3::types::SyncState::Syncing(sync_info) => { @@ -135,7 +135,7 @@ pub async fn check_ethereum_node_running(eth_client_url: &str) -> Result<(), Str { // Ignore this error because it can be caused by a non-standard ethereum provider // https://github.com/witnet/witnet-rust/issues/2141 - log::debug!("Ethereum node is running at {}", eth_client_url); + log::debug!("Ethereum node is running at {}", eth_jsonrpc_url); log::warn!("Ethereum provider returned `true` on eth_syncing method"); Ok(()) @@ -179,7 +179,7 @@ mod tests { fn test_hack_fix_functions_with_multiple_definitions() { // The hack_fix_functions_with_multiple_definitions function already does some checks // internally, so here we call it to ensure the ABI is correct. - let wrb_contract_abi_json: &[u8] = include_bytes!("../wrb_abi.json"); + let wrb_contract_abi_json: &[u8] = include_bytes!("../../wrb_abi.json"); let mut wrb_contract_abi = web3::ethabi::Contract::load(wrb_contract_abi_json) .map_err(|e| format!("Unable to load WRB contract from ABI: {:?}", e)) .unwrap(); diff --git a/bridges/centralized-ethereum/src/main.rs b/bridges/centralized-ethereum/src/main.rs index a44b1c70c..c8e6138d4 100644 --- a/bridges/centralized-ethereum/src/main.rs +++ b/bridges/centralized-ethereum/src/main.rs @@ -4,11 +4,10 @@ use actix::{Actor, System, SystemRegistry}; use std::{path::PathBuf, process::exit, sync::Arc}; use structopt::StructOpt; -use web3::{contract, types::U256}; use witnet_centralized_ethereum_bridge::{ actors::{ dr_database::DrDatabase, dr_reporter::DrReporter, dr_sender::DrSender, - eth_poller::EthPoller, wit_poller::WitPoller, + eth_poller::EthPoller, watch_dog::WatchDog, wit_poller::WitPoller, }, check_ethereum_node_running, check_witnet_node_running, config, create_wrb_contract, }; @@ -22,9 +21,6 @@ struct App { /// Path of the config file #[structopt(short = "c", long)] config: Option, - /// Post data request and exit - #[structopt(long = "post-dr")] - post_dr: bool, /// Read config from environment #[structopt(long = "env", conflicts_with = "config")] env: bool, @@ -44,30 +40,6 @@ fn init_logger() { .init(); } -async fn post_example_dr(config: Arc) { - log::info!("Posting an example of Data Request"); - let (_web3, wrb_contract) = - create_wrb_contract(&config.eth_client_url, config.wrb_contract_addr); - - log::info!("calling postDataRequest"); - - let res = wrb_contract - .call_with_confirmations( - "postDataRequest", - (config.request_example_contract_addr,), - config.eth_account, - contract::Options::with(|opt| { - opt.value = Some(U256::from_dec_str("2500000000000000").unwrap()); - // The cost of posting a data request is mainly the storage, so - // big data requests may need bigger amounts of gas - opt.gas = config.gas_limits.post_data_request.map(Into::into); - }), - 1, - ) - .await; - log::info!("The receipt is {:?}", res); -} - fn main() { init_logger(); @@ -101,60 +73,62 @@ fn run(callback: fn()) -> Result<(), String> { // Init system let system = System::new(); - let condition = app.post_dr; // Init actors system.block_on(async { // Call cb function (register interrupt handlers) callback(); - if condition { - post_example_dr(config).await; - log::info!("post post_example DR"); - } else { - let witnet_client_url = config.witnet_jsonrpc_addr.to_string(); - - // Check if Ethereum and Witnet nodes are running before starting actors - check_ethereum_node_running(&config.eth_client_url) - .await - .expect("ethereum node not running"); - check_witnet_node_running(&witnet_client_url) - .await - .expect("witnet node not running"); - - // Web3 contract using HTTP transport with an Ethereum client - let (web3, wrb_contract) = - create_wrb_contract(&config.eth_client_url, config.wrb_contract_addr); - let wrb_contract = Arc::new(wrb_contract); - - // Start DrDatabase actor - let dr_database_addr = DrDatabase::default().start(); - SystemRegistry::set(dr_database_addr); - - // Start Json-RPC actor connected to Witnet node - let node_client = JsonRpcClient::start(&witnet_client_url) - .expect("Json-RPC Client actor failed to started"); - - // Start WitPoller actor - let wit_poller_addr = WitPoller::from_config(&config, node_client.clone()).start(); - SystemRegistry::set(wit_poller_addr); - - // Start DrSender actor - let dr_sender_addr = DrSender::from_config(&config, node_client).start(); - SystemRegistry::set(dr_sender_addr); - - // Start EthPoller actor - let eth_poller_addr = EthPoller::from_config(&config, wrb_contract.clone()).start(); - SystemRegistry::set(eth_poller_addr); - - // Start DrReporter actor - let dr_reporter_addr = DrReporter::from_config(&config, wrb_contract, web3).start(); - SystemRegistry::set(dr_reporter_addr); - - // Initialize Storage Manager - let mut node_config = NodeConfig::default(); - node_config.storage.db_path = config.storage.db_path.clone(); - storage_mngr::start_from_config(node_config); + // Check if Ethereum and Witnet nodes are running before starting actors + check_ethereum_node_running(&config.eth_jsonrpc_url) + .await + .expect("ethereum node not running"); + + check_witnet_node_running(&config.witnet_jsonrpc_socket.to_string()) + .await + .expect("witnet node not running"); + + // Start DrDatabase actor + let dr_database_addr = DrDatabase::default().start(); + SystemRegistry::set(dr_database_addr); + + // Web3 contract using HTTP transport with an Ethereum client + let (web3, wrb_contract) = + create_wrb_contract(&config.eth_jsonrpc_url, config.eth_witnet_oracle); + + let wrb_contract = Arc::new(wrb_contract); + + // Start EthPoller actor + let eth_poller_addr = + EthPoller::from_config(&config, web3.clone(), wrb_contract.clone()).start(); + SystemRegistry::set(eth_poller_addr); + + // Start DrReporter actor + let dr_reporter_addr = + DrReporter::from_config(&config, web3.clone(), wrb_contract.clone()).start(); + SystemRegistry::set(dr_reporter_addr); + + // Start Json-RPC actor connected to Witnet node + let node_client = JsonRpcClient::start(&config.witnet_jsonrpc_socket.to_string()) + .expect("JSON WIT/RPC node client failed to start"); + + // Start WitPoller actor + let wit_poller_addr = WitPoller::from_config(&config, node_client.clone()).start(); + SystemRegistry::set(wit_poller_addr); + + // Start DrSender actor + let dr_sender_addr = DrSender::from_config(&config, node_client.clone()).start(); + SystemRegistry::set(dr_sender_addr); + + // Initialize Storage Manager + let mut node_config = NodeConfig::default(); + node_config.storage.db_path = config.storage.db_path.clone(); + storage_mngr::start_from_config(node_config); + + // Start WatchDog actor + if config.watch_dog_enabled { + let watch_dog_addr = WatchDog::from_config(&config, wrb_contract.clone()).start(); + SystemRegistry::set(watch_dog_addr); } }); diff --git a/bridges/centralized-ethereum/wrb_abi.json b/bridges/centralized-ethereum/wrb_abi.json deleted file mode 100644 index 02e766e97..000000000 --- a/bridges/centralized-ethereum/wrb_abi.json +++ /dev/null @@ -1,2057 +0,0 @@ -[ - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "queryId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "string", - "name": "reason", - "type": "string" - } - ], - "name": "BatchReportError", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "queryId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "from", - "type": "address" - } - ], - "name": "DeletedQuery", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "queryId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "from", - "type": "address" - } - ], - "name": "PostedRequest", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "queryId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "from", - "type": "address" - } - ], - "name": "PostedResult", - "type": "event" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asBool", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asBytes", - "outputs": [ - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asBytes32", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asErrorCode", - "outputs": [ - { - "internalType": "enum Witnet.ErrorCodes", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asErrorMessage", - "outputs": [ - { - "internalType": "enum Witnet.ErrorCodes", - "name": "", - "type": "uint8" - }, - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asFixed16", - "outputs": [ - { - "internalType": "int32", - "name": "", - "type": "int32" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asFixed16Array", - "outputs": [ - { - "internalType": "int32[]", - "name": "", - "type": "int32[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asInt128", - "outputs": [ - { - "internalType": "int128", - "name": "", - "type": "int128" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asInt128Array", - "outputs": [ - { - "internalType": "int128[]", - "name": "", - "type": "int128[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asRawError", - "outputs": [ - { - "internalType": "uint64[]", - "name": "", - "type": "uint64[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asString", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asStringArray", - "outputs": [ - { - "internalType": "string[]", - "name": "", - "type": "string[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asUint64", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asUint64Array", - "outputs": [ - { - "internalType": "uint64[]", - "name": "", - "type": "uint64[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "deleteQuery", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "reporter", - "type": "address" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "cborBytes", - "type": "bytes" - } - ], - "internalType": "struct Witnet.Response", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_gasPrice", - "type": "uint256" - } - ], - "name": "estimateReward", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getNextQueryId", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "getQueryData", - "outputs": [ - { - "components": [ - { - "components": [ - { - "internalType": "contract IWitnetRequest", - "name": "addr", - "type": "address" - }, - { - "internalType": "address", - "name": "requester", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "hash", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "gasprice", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "reward", - "type": "uint256" - } - ], - "internalType": "struct Witnet.Request", - "name": "request", - "type": "tuple" - }, - { - "components": [ - { - "internalType": "address", - "name": "reporter", - "type": "address" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "cborBytes", - "type": "bytes" - } - ], - "internalType": "struct Witnet.Response", - "name": "response", - "type": "tuple" - }, - { - "internalType": "address", - "name": "from", - "type": "address" - } - ], - "internalType": "struct Witnet.Query", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "getQueryStatus", - "outputs": [ - { - "internalType": "enum Witnet.QueryStatus", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "isError", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "isOk", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IWitnetRequest", - "name": "_addr", - "type": "address" - } - ], - "name": "postRequest", - "outputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readRequest", - "outputs": [ - { - "components": [ - { - "internalType": "contract IWitnetRequest", - "name": "addr", - "type": "address" - }, - { - "internalType": "address", - "name": "requester", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "hash", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "gasprice", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "reward", - "type": "uint256" - } - ], - "internalType": "struct Witnet.Request", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readRequestBytecode", - "outputs": [ - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readRequestGasPrice", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readRequestReward", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readResponse", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "reporter", - "type": "address" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "cborBytes", - "type": "bytes" - } - ], - "internalType": "struct Witnet.Response", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readResponseDrTxHash", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readResponseReporter", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readResponseResult", - "outputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readResponseTimestamp", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "_drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "_result", - "type": "bytes" - } - ], - "name": "reportResult", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "_drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "_result", - "type": "bytes" - } - ], - "name": "reportResult", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "uint256", - "name": "queryId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "cborBytes", - "type": "bytes" - } - ], - "internalType": "struct IWitnetRequestBoardReporter.BatchResult[]", - "name": "_batchResults", - "type": "tuple[]" - }, - { - "internalType": "bool", - "name": "_verbose", - "type": "bool" - } - ], - "name": "reportResultBatch", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "_cborBytes", - "type": "bytes" - } - ], - "name": "resultFromCborBytes", - "outputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "_cborValue", - "type": "tuple" - } - ], - "name": "resultFromCborValue", - "outputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "upgradeReward", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "stateMutability": "payable", - "type": "receive" - } -] diff --git a/bridges/wrb_abi.json b/bridges/wrb_abi.json new file mode 100644 index 000000000..6301e2155 --- /dev/null +++ b/bridges/wrb_abi.json @@ -0,0 +1,312 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "queryId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "reason", + "type": "string" + } + ], + "name": "BatchReportError", + "type": "event" + }, + { + "inputs": [], + "name": "channel", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "class", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "witnetQueryIds", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "reportTxMsgData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "reportTxGasPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nanoWitPrice", + "type": "uint256" + } + ], + "name": "estimateReportEarnings", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "queryIds", + "type": "uint256[]" + } + ], + "name": "extractWitnetDataRequests", + "outputs": [ + { + "internalType": "bytes[]", + "name": "drBytecodes", + "type": "bytes[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "contract WitnetRequestFactory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNextQueryId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "queryIds", + "type": "uint256[]" + } + ], + "name": "getQueryStatusBatch", + "outputs": [ + { + "internalType": "enum WitnetV2.QueryStatus[]", + "name": "", + "type": "uint8[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "registry", + "outputs": [ + { + "internalType": "contract WitnetRequestBytecodes", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "witnetQueryId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "witnetQueryResultTallyHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "witnetQueryResultCborBytes", + "type": "bytes" + } + ], + "name": "reportResult", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "witnetQueryId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "witnetQueryResultTimestamp", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "witnetQueryResultTallyHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "witnetQueryResultCborBytes", + "type": "bytes" + } + ], + "name": "reportResult", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "queryId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "queryResultTimestamp", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "queryResultTallyHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "queryResultCborBytes", + "type": "bytes" + } + ], + "internalType": "struct IWitnetOracleReporter.BatchResult[]", + "name": "_batchResults", + "type": "tuple[]" + } + ], + "name": "reportResultBatch", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "specs", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "queryId", + "type": "uint256" + } + ], + "name": "upgradeQueryEvmReward", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isReporter", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/config/src/config.rs b/config/src/config.rs index 72e68f2d6..d18e5e8cf 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -38,16 +38,20 @@ //! // Default config for mainnet //! // Config::from_partial(&PartialConfig::default_mainnet()); //! ``` -use std::convert::TryFrom; use std::{ - collections::HashSet, fmt, marker::PhantomData, net::SocketAddr, path::PathBuf, time::Duration, + array::IntoIter, collections::HashSet, convert::TryFrom, fmt, marker::PhantomData, + net::SocketAddr, path::PathBuf, time::Duration, }; -use partial_struct::PartialStruct; use serde::{de, Deserialize, Deserializer, Serialize}; + +use partial_struct::PartialStruct; use witnet_crypto::hash::HashFunction; -use witnet_data_structures::chain::{ConsensusConstants, Environment, PartialConsensusConstants}; -use witnet_data_structures::witnessing::WitnessingConfig; +use witnet_data_structures::{ + chain::{ConsensusConstants, Environment, Epoch, PartialConsensusConstants}, + proto::versioning::ProtocolVersion, + witnessing::WitnessingConfig, +}; use witnet_protected::ProtectedString; use crate::{ @@ -125,6 +129,11 @@ pub struct Config { #[partial_struct(ty = "PartialWitnessing")] #[partial_struct(serde(default))] pub witnessing: Witnessing, + + /// Configuration related with protocol versions + #[partial_struct(ty = "Protocol")] + #[partial_struct(serde(default))] + pub protocol: Protocol, } /// Log-specific configuration. @@ -420,6 +429,25 @@ pub struct Tapi { pub oppose_wip0027: bool, } +/// Configuration related to protocol versions. +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct Protocol { + pub v1_7: Option, + pub v1_8: Option, + pub v2_0: Option, +} + +impl Protocol { + pub fn iter(&self) -> IntoIter<(ProtocolVersion, Option), 3> { + [ + (ProtocolVersion::V1_7, self.v1_7), + (ProtocolVersion::V1_8, self.v1_8), + (ProtocolVersion::V2_0, self.v2_0), + ] + .into_iter() + } +} + fn to_partial_consensus_constants(c: &ConsensusConstants) -> PartialConsensusConstants { PartialConsensusConstants { checkpoint_zero_timestamp: Some(c.checkpoint_zero_timestamp), @@ -450,6 +478,13 @@ fn to_partial_consensus_constants(c: &ConsensusConstants) -> PartialConsensusCon } } +pub trait Partializable { + type Partial; + + fn from_partial(config: &Self::Partial, defaults: &dyn Defaults) -> Self; + fn to_partial(&self) -> Self::Partial; +} + impl Config { pub fn from_partial(config: &PartialConfig) -> Self { let defaults: &dyn Defaults = match config.environment { @@ -478,6 +513,7 @@ impl Config { mempool: Mempool::from_partial(&config.mempool, defaults), tapi: config.tapi.clone(), witnessing: Witnessing::from_partial(&config.witnessing, defaults), + protocol: Protocol::from_partial(&config.protocol, defaults), } } @@ -496,6 +532,7 @@ impl Config { mempool: self.mempool.to_partial(), tapi: self.tapi.clone(), witnessing: self.witnessing.to_partial(), + protocol: self.protocol.to_partial(), } } } @@ -1171,6 +1208,30 @@ impl Witnessing { } } +impl Partializable for Protocol { + type Partial = Self; + + fn from_partial(config: &Self::Partial, defaults: &dyn Defaults) -> Self { + let defaults = defaults.protocol_versions(); + + Protocol { + v1_7: config + .v1_7 + .or(defaults.get(&ProtocolVersion::V1_7).copied()), + v1_8: config + .v1_8 + .or(defaults.get(&ProtocolVersion::V1_8).copied()), + v2_0: config + .v2_0 + .or(defaults.get(&ProtocolVersion::V2_0).copied()), + } + } + + fn to_partial(&self) -> Self::Partial { + self.clone() + } +} + // Serialization helpers fn as_log_filter_string( diff --git a/config/src/defaults.rs b/config/src/defaults.rs index f90390484..7d0576316 100644 --- a/config/src/defaults.rs +++ b/config/src/defaults.rs @@ -2,13 +2,18 @@ //! //! This module contains per-environment default values for the Witnet //! protocol params. -use std::collections::HashSet; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::path::PathBuf; -use std::time::Duration; +use std::{ + collections::{HashMap, HashSet}, + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::PathBuf, + time::Duration, +}; use witnet_crypto::hash::HashFunction; -use witnet_data_structures::chain::Hash; +use witnet_data_structures::{ + chain::{Epoch, Hash}, + proto::versioning::ProtocolVersion, +}; use witnet_protected::ProtectedString; // When changing the defaults, remember to update the documentation! @@ -475,6 +480,10 @@ pub trait Defaults { fn mempool_max_reinserted_transactions(&self) -> u32 { 100 } + + fn protocol_versions(&self) -> HashMap { + [(ProtocolVersion::V1_7, 0)].into_iter().collect() + } } /// Allow setting a reward to collateral percentage for a data request to be included in a block @@ -486,6 +495,13 @@ pub const PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO: u64 = 125; // TODO: modify the value directly in ConsensusConstants pub const PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE: u32 = 13440; +/// Maximum weight units that a block can devote to `StakeTransaction`s. +pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000; + +/// Minimum amount of nanoWits that a `StakeTransaction` can add, and minimum amount that can be +/// left in stake by an `UnstakeTransaction`. +pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS: u64 = 10_000_000_000_000; + /// Struct that will implement all the development defaults pub struct Development; diff --git a/config/src/loaders/toml.rs b/config/src/loaders/toml.rs index e44206152..1b6b23e24 100644 --- a/config/src/loaders/toml.rs +++ b/config/src/loaders/toml.rs @@ -48,7 +48,7 @@ pub fn from_str(contents: &str) -> Result { } #[cfg(test)] -thread_local!(static FILE_CONTENTS: Cell<&'static str> = Cell::new("")); +thread_local!(static FILE_CONTENTS: Cell<&'static str> = const { Cell::new("") }); #[cfg(test)] fn read_file_contents(_filename: &Path, contents: &mut String) -> io::Result { diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index a1831753a..4255422f7 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -22,7 +22,7 @@ hmac = "0.7.1" memzero = "0.1.0" rand = "0.7.3" ring = "0.16.11" -secp256k1 = { version = "0.22.2", features = ["global-context"] } +secp256k1 = { version = "0.28.1", features = ["global-context", "recovery"] } serde = { version = "1.0.104", optional = true } sha2 = "0.8.1" tiny-bip39 = "0.7.0" diff --git a/crypto/src/key.rs b/crypto/src/key.rs index 9d99dc546..5e1def9e4 100644 --- a/crypto/src/key.rs +++ b/crypto/src/key.rs @@ -149,6 +149,9 @@ pub enum KeyDerivationError { /// Invalid seed length #[fail(display = "The length of the seed is invalid, must be between 128/512 bits")] InvalidSeedLength, + /// A secret key is greater than the curve order + #[fail(display = "The secret key is greater than the curve order")] + SecretLargerThanCurveOrder, /// Secp256k1 internal error #[fail(display = "Error in secp256k1 crate")] Secp256k1Error(secp256k1::Error), @@ -300,8 +303,11 @@ impl ExtendedSK { let (chain_code, mut secret_key) = get_chain_code_and_secret(&index_bytes, hmac512)?; - secret_key - .add_assign(&self.secret_key[..]) + let scalar = secp256k1::Scalar::from_be_bytes(self.secret_key.secret_bytes()) + .map_err(|_| KeyDerivationError::SecretLargerThanCurveOrder)?; + + secret_key = secret_key + .add_tweak(&scalar) .map_err(KeyDerivationError::Secp256k1Error)?; Ok(ExtendedSK { diff --git a/crypto/src/signature.rs b/crypto/src/signature.rs index 5da876b73..c980c47af 100644 --- a/crypto/src/signature.rs +++ b/crypto/src/signature.rs @@ -12,14 +12,14 @@ pub type PublicKey = secp256k1::PublicKey; /// secure hash function, otherwise this function is not secure. /// - Returns an Error if data is not a 32-byte array pub fn sign(secret_key: SecretKey, data: &[u8]) -> Result { - let msg = Message::from_slice(data)?; + let msg = Message::from_digest_slice(data)?; Ok(secret_key.sign_ecdsa(msg)) } /// Verify signature with a provided public key. /// - Returns an Error if data is not a 32-byte array pub fn verify(public_key: &PublicKey, data: &[u8], sig: &Signature) -> Result<(), Error> { - let msg = Message::from_slice(data)?; + let msg = Message::from_digest_slice(data)?; sig.verify(&msg, public_key) } diff --git a/data_structures/Cargo.toml b/data_structures/Cargo.toml index 264649bcd..d12761817 100644 --- a/data_structures/Cargo.toml +++ b/data_structures/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Witnet Foundation "] description = "data structures component" edition = "2021" name = "witnet_data_structures" -version = "1.7.1" +version = "2.0.0" workspace = ".." [features] @@ -33,6 +33,8 @@ rand = "0.8.5" serde = { version = "1.0.104", features = ["derive"] } serde_cbor = "0.11.1" serde_json = "1.0.48" +strum = "0.25.0" +strum_macros = "0.25.3" vrf = "0.2.3" witnet_crypto = { path = "../crypto" } @@ -51,3 +53,7 @@ rand_distr = "0.4.3" [[bench]] name = "sort_active_identities" harness = false + +[[bench]] +name = "staking" +harness = false diff --git a/data_structures/benches/staking.rs b/data_structures/benches/staking.rs new file mode 100644 index 000000000..d2d96edc3 --- /dev/null +++ b/data_structures/benches/staking.rs @@ -0,0 +1,85 @@ +#[macro_use] +extern crate bencher; +use bencher::Bencher; +use rand::Rng; +use witnet_data_structures::staking::prelude::*; + +fn populate(b: &mut Bencher) { + let mut stakes = Stakes::::default(); + let mut i = 1; + + b.iter(|| { + let address = format!("{i}"); + let coins = i; + let epoch = i; + stakes.add_stake(address.as_str(), coins, epoch).unwrap(); + + i += 1; + }); +} + +fn rank(b: &mut Bencher) { + let mut stakes = Stakes::::default(); + let mut i = 1; + + let stakers = 100_000; + let rf = 10; + + let mut rng = rand::thread_rng(); + + loop { + let coins = i; + let epoch = i; + let address = format!("{}", rng.gen::()); + + stakes.add_stake(address.as_str(), coins, epoch).unwrap(); + + i += 1; + + if i == stakers { + break; + } + } + + b.iter(|| { + let rank = stakes.rank(Capability::Mining, i); + let mut top = rank.take(usize::try_from(stakers / rf).unwrap()); + let _first = top.next(); + let _last = top.last(); + + i += 1; + }) +} + +fn query_power(b: &mut Bencher) { + let mut stakes = Stakes::::default(); + let mut i = 1; + + let stakers = 100_000; + + loop { + let coins = i; + let epoch = i; + let address = format!("{i}"); + + stakes.add_stake(address.as_str(), coins, epoch).unwrap(); + + i += 1; + + if i == stakers { + break; + } + } + + i = 1; + + b.iter(|| { + let address = format!("{i}"); + let _power = stakes.query_power(address.as_str(), Capability::Mining, i); + + i += 1; + }) +} + +benchmark_main!(benches); +benchmark_group!(benches, populate, rank, query_power); diff --git a/data_structures/build.rs b/data_structures/build.rs index 2a85b75c3..576450cb4 100644 --- a/data_structures/build.rs +++ b/data_structures/build.rs @@ -14,6 +14,8 @@ fn create_path_to_protobuf_schema_env() { } fn main() { + println!("cargo:rerun-if-changed=../schemas/witnet/witnet.proto"); + create_path_to_protobuf_schema_env(); exonum_build::protobuf_generate( diff --git a/data_structures/examples/transactions_pool_overhead.rs b/data_structures/examples/transactions_pool_overhead.rs index 5f8293444..478d114d2 100644 --- a/data_structures/examples/transactions_pool_overhead.rs +++ b/data_structures/examples/transactions_pool_overhead.rs @@ -100,7 +100,7 @@ fn random_transaction() -> (Transaction, u64) { } else { let dr_output = random_dr_output(); Transaction::DataRequest(DRTransaction { - body: DRTransactionBody::new(inputs, outputs, dr_output), + body: DRTransactionBody::new(inputs, dr_output, outputs), signatures: vec![signature; num_inputs], }) }; diff --git a/data_structures/src/capabilities.rs b/data_structures/src/capabilities.rs new file mode 100644 index 000000000..c3bee6efb --- /dev/null +++ b/data_structures/src/capabilities.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; + +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +pub enum Capability { + /// The base block mining and superblock voting capability + Mining = 0, + /// The universal HTTP GET / HTTP POST / WIP-0019 RNG capability + Witnessing = 1, +} + +#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct CapabilityMap +where + T: Default, +{ + pub mining: T, + pub witnessing: T, +} + +impl CapabilityMap +where + T: Copy + Default, +{ + #[inline] + pub fn get(&self, capability: Capability) -> T { + match capability { + Capability::Mining => self.mining, + Capability::Witnessing => self.witnessing, + } + } + + #[inline] + pub fn update(&mut self, capability: Capability, value: T) { + match capability { + Capability::Mining => self.mining = value, + Capability::Witnessing => self.witnessing = value, + } + } + + #[inline] + pub fn update_all(&mut self, value: T) { + self.mining = value; + self.witnessing = value; + } +} diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 8f905f885..303dd595c 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1,9 +1,3 @@ -/// Keeps track of priority being used by transactions included in recent blocks, and provides -/// methods for estimating sensible priority values for future transactions. -pub mod priority; -/// Contains all TAPI related structures and business logic -pub mod tapi; - use std::{ cell::{Cell, RefCell}, cmp::Ordering, @@ -17,18 +11,21 @@ use std::{ use bech32::{FromBase32, ToBase32}; use bls_signatures_rs::{bn256, bn256::Bn256, MultiSignature}; +use ethereum_types::U256; use failure::Fail; use futures::future::BoxFuture; use ordered_float::OrderedFloat; -use partial_struct::PartialStruct; use serde::{Deserialize, Serialize}; + +use partial_struct::PartialStruct; use witnet_crypto::{ hash::{calculate_sha256, Sha256}, key::ExtendedSK, merkle::merkle_tree_root as crypto_merkle_tree_root, secp256k1::{ - ecdsa::Signature as Secp256k1_Signature, PublicKey as Secp256k1_PublicKey, - SecretKey as Secp256k1_SecretKey, + self, + ecdsa::{RecoverableSignature, RecoveryId, Signature as Secp256k1_Signature}, + Message, PublicKey as Secp256k1_PublicKey, SecretKey as Secp256k1_SecretKey, }, }; use witnet_protected::Protected; @@ -42,19 +39,31 @@ use crate::{ TransactionError, }, get_environment, - proto::{schema::witnet, ProtobufConvert}, + proto::{ + versioning::{ProtocolVersion, Versioned}, + ProtobufConvert, + }, + staking::prelude::*, superblock::SuperBlockState, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, Memoized, MintTransaction, - RevealTransaction, TallyTransaction, Transaction, TxInclusionProof, VTTransaction, + RevealTransaction, StakeTransaction, TallyTransaction, Transaction, TxInclusionProof, + UnstakeTransaction, VTTransaction, }, transaction::{ MemoHash, MemoizedHashable, BETA, COMMIT_WEIGHT, OUTPUT_SIZE, REVEAL_WEIGHT, TALLY_WEIGHT, }, utxo_pool::{OldUnspentOutputsPool, OwnUnspentOutputsPool, UnspentOutputsPool}, vrf::{BlockEligibilityClaim, DataRequestEligibilityClaim}, + wit::Wit, }; +/// Keeps track of priority being used by transactions included in recent blocks, and provides +/// methods for estimating sensible priority values for future transactions. +pub mod priority; +/// Contains all TAPI related structures and business logic +pub mod tapi; + /// Define how the different structures should be hashed. pub trait Hashable { /// Calculate the hash of `self` @@ -156,7 +165,7 @@ impl Environment { PartialStruct, Debug, Clone, PartialEq, Serialize, Deserialize, ProtobufConvert, Default, )] #[partial_struct(derive(Deserialize, Serialize, Default, Debug, Clone, PartialEq))] -#[protobuf_convert(pb = "witnet::ConsensusConstants")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::ConsensusConstants")] pub struct ConsensusConstants { /// Timestamp at checkpoint 0 (the start of epoch 0) pub checkpoint_zero_timestamp: i64, @@ -359,7 +368,7 @@ impl GenesisBlockInfo { #[derive( Copy, Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize, ProtobufConvert, )] -#[protobuf_convert(pb = "witnet::CheckpointBeacon")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::CheckpointBeacon")] #[serde(rename_all = "camelCase")] pub struct CheckpointBeacon { /// The serial number for an epoch @@ -372,7 +381,7 @@ pub struct CheckpointBeacon { #[derive( Copy, Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize, ProtobufConvert, )] -#[protobuf_convert(pb = "witnet::CheckpointVRF")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::CheckpointVRF")] #[serde(rename_all = "camelCase")] pub struct CheckpointVRF { /// The serial number for an epoch @@ -386,7 +395,7 @@ pub type Epoch = u32; /// Block data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Default, Hash)] -#[protobuf_convert(pb = "witnet::Block")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Block")] pub struct Block { /// The header of the block pub block_header: BlockHeader, @@ -402,7 +411,7 @@ pub struct Block { /// Block transactions #[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] -#[protobuf_convert(pb = "witnet::Block_BlockTransactions")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Block_BlockTransactions")] pub struct BlockTransactions { /// Mint transaction, pub mint: MintTransaction, @@ -416,6 +425,10 @@ pub struct BlockTransactions { pub reveal_txns: Vec, /// A list of signed tally transactions pub tally_txns: Vec, + /// A list of signed stake transactions + pub stake_txns: Vec, + /// A list of signed unstake transactions + pub unstake_txns: Vec, } impl Block { @@ -444,6 +457,8 @@ impl Block { commit_txns: vec![], reveal_txns: vec![], tally_txns: vec![], + stake_txns: vec![], + unstake_txns: vec![], }; /// Function to calculate a merkle tree from a transaction vector @@ -468,6 +483,8 @@ impl Block { commit_hash_merkle_root: merkle_tree_root(&txns.commit_txns), reveal_hash_merkle_root: merkle_tree_root(&txns.reveal_txns), tally_hash_merkle_root: merkle_tree_root(&txns.tally_txns), + stake_hash_merkle_root: merkle_tree_root(&txns.stake_txns), + unstake_hash_merkle_root: merkle_tree_root(&txns.unstake_txns), }; Block::new( @@ -502,8 +519,28 @@ impl Block { vt_weight } + pub fn st_weight(&self) -> u32 { + let mut st_weight = 0; + for st_txn in self.txns.stake_txns.iter() { + st_weight += st_txn.weight(); + } + st_weight + } + + pub fn ut_weight(&self) -> u32 { + let mut ut_weight = 0; + for ut_txn in self.txns.unstake_txns.iter() { + ut_weight += ut_txn.weight(); + } + ut_weight + } + pub fn weight(&self) -> u32 { - self.dr_weight() + self.vt_weight() + self.dr_weight() + self.vt_weight() + self.st_weight() + self.ut_weight() + } + + pub fn is_genesis(&self, genesis: &Hash) -> bool { + self.hash().eq(genesis) } } @@ -517,6 +554,8 @@ impl BlockTransactions { + self.commit_txns.len() + self.reveal_txns.len() + self.tally_txns.len() + + self.stake_txns.len() + + self.unstake_txns.len() } /// Returns true if this block contains no transactions @@ -528,6 +567,8 @@ impl BlockTransactions { && self.commit_txns.is_empty() && self.reveal_txns.is_empty() && self.tally_txns.is_empty() + && self.stake_txns.is_empty() + && self.unstake_txns.is_empty() } /// Get a transaction given the `TransactionPointer` @@ -559,6 +600,16 @@ impl BlockTransactions { .get(i as usize) .cloned() .map(Transaction::Tally), + TransactionPointer::Stake(i) => self + .stake_txns + .get(i as usize) + .cloned() + .map(Transaction::Stake), + TransactionPointer::Unstake(i) => self + .unstake_txns + .get(i as usize) + .cloned() + .map(Transaction::Unstake), } } @@ -601,6 +652,16 @@ impl BlockTransactions { TransactionPointer::Tally(u32::try_from(i).unwrap()); items_to_add.push((tx.hash(), pointer_to_block.clone())); } + for (i, tx) in self.stake_txns.iter().enumerate() { + pointer_to_block.transaction_index = + TransactionPointer::Stake(u32::try_from(i).unwrap()); + items_to_add.push((tx.hash(), pointer_to_block.clone())); + } + for (i, tx) in self.unstake_txns.iter().enumerate() { + pointer_to_block.transaction_index = + TransactionPointer::Unstake(u32::try_from(i).unwrap()); + items_to_add.push((tx.hash(), pointer_to_block.clone())); + } items_to_add } @@ -614,7 +675,9 @@ impl Hashable for BlockHeader { impl MemoizedHashable for Block { fn hashable_bytes(&self) -> Vec { - self.block_header.to_pb_bytes().unwrap() + self.block_header + .to_versioned_pb_bytes(ProtocolVersion::guess()) + .unwrap() } fn memoized_hash(&self) -> &MemoHash { @@ -652,7 +715,7 @@ impl Hashable for PublicKey { /// Block header structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Default, Hash)] -#[protobuf_convert(pb = "witnet::Block_BlockHeader")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Block_BlockHeader")] pub struct BlockHeader { /// 32 bits for binary signaling new witnet protocol improvements. /// See [WIP-0014](https://github.com/witnet/WIPs/blob/master/wip-0014.md) for more info. @@ -668,7 +731,7 @@ pub struct BlockHeader { } /// Block merkle tree roots #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Default, Hash)] -#[protobuf_convert(pb = "witnet::Block_BlockHeader_BlockMerkleRoots")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Block_BlockHeader_BlockMerkleRoots")] pub struct BlockMerkleRoots { /// A 256-bit hash based on the mint transaction committed to this block pub mint_hash: Hash, @@ -682,6 +745,10 @@ pub struct BlockMerkleRoots { pub reveal_hash_merkle_root: Hash, /// A 256-bit hash based on all of the tally transactions committed to this block pub tally_hash_merkle_root: Hash, + /// A 256-bit hash based on all of the stake transactions committed to this block + pub stake_hash_merkle_root: Hash, + /// A 256-bit hash based on all of the unstake transactions committed to this block + pub unstake_hash_merkle_root: Hash, } /// Function to calculate a merkle tree from a transaction vector @@ -710,6 +777,8 @@ impl BlockMerkleRoots { commit_hash_merkle_root: merkle_tree_root(&txns.commit_txns), reveal_hash_merkle_root: merkle_tree_root(&txns.reveal_txns), tally_hash_merkle_root: merkle_tree_root(&txns.tally_txns), + stake_hash_merkle_root: merkle_tree_root(&txns.stake_txns), + unstake_hash_merkle_root: merkle_tree_root(&txns.unstake_txns), } } } @@ -720,7 +789,7 @@ impl BlockMerkleRoots { /// This is needed to ensure that the security and trustlessness properties of Witnet will /// be relayed to bridges with other block chains. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, ProtobufConvert, Serialize)] -#[protobuf_convert(pb = "witnet::SuperBlock")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::SuperBlock")] pub struct SuperBlock { /// Number of signing committee members, pub signing_committee_length: u32, @@ -853,7 +922,7 @@ impl SuperBlock { /// Superblock votes as sent through the network #[derive(Debug, Eq, PartialEq, Clone, Hash, ProtobufConvert, Serialize, Deserialize)] -#[protobuf_convert(pb = "witnet::SuperBlockVote")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::SuperBlockVote")] pub struct SuperBlockVote { /// BN256 signature of `superblock_index` and `superblock_hash` pub bn256_signature: Bn256Signature, @@ -911,7 +980,7 @@ impl SuperBlockVote { /// Digital signatures structure (based on supported cryptosystems) #[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Signature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Signature")] pub enum Signature { /// ECDSA over secp256k1 Secp256k1(Secp256k1Signature), @@ -943,11 +1012,30 @@ impl Signature { } } } + + pub fn verify( + &self, + msg: &Message, + public_key: &Secp256k1_PublicKey, + ) -> Result<(), failure::Error> { + match self { + Secp256k1(x) => { + let signature = Secp256k1_Signature::from_der(x.der.as_slice()) + .map_err(|_| Secp256k1ConversionError::FailSignatureConversion)?; + + signature + .verify(msg, public_key) + .map_err(|inner| Secp256k1ConversionError::Secp256k1 { inner })?; + + Ok(()) + } + } + } } /// ECDSA (over secp256k1) signature #[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Secp256k1Signature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Secp256k1Signature")] pub struct Secp256k1Signature { /// The signature serialized in DER pub der: Vec, @@ -1039,7 +1127,7 @@ impl From for ExtendedSK { /// Hash #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Hash")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Hash")] pub enum Hash { /// SHA-256 Hash SHA256(SHA256), @@ -1047,7 +1135,10 @@ pub enum Hash { impl Default for Hash { fn default() -> Hash { - Hash::SHA256([0; 32]) + Hash::SHA256([ + 227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, + 65, 228, 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85, + ]) } } @@ -1118,8 +1209,6 @@ impl Hash { /// /// If n is 0 because of a division by zero. pub fn div_mod(&self, n: u64) -> (Hash, u64) { - use ethereum_types::U256; - let hash_u256 = U256::from_big_endian(self.as_ref()); let n_u256 = U256::from(n); let (d, m) = hash_u256.div_mod(n_u256); @@ -1129,6 +1218,21 @@ impl Hash { (d_hash, m_u64) } + + /// Obtains the bytes that represent the hash digest. + /// + /// This allows for compatibility with functions that take hashes and other data as raw bytes without a newtype or + /// any other kind of wrapper. + pub fn data(&self) -> [u8; 32] { + match self { + Hash::SHA256(bytes) => *bytes, + } + } + + /// Creates an instance of Hash where all bytes are set to their max value. + pub fn max() -> Self { + Self::SHA256([u8::MAX; 32]) + } } /// Error when parsing hash from string @@ -1169,11 +1273,20 @@ pub type SHA256 = [u8; 32]; /// /// It is the first 20 bytes of the SHA256 hash of the PublicKey. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, ProtobufConvert, Ord, PartialOrd)] -#[protobuf_convert(pb = "witnet::PublicKeyHash")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::PublicKeyHash")] pub struct PublicKeyHash { pub(crate) hash: [u8; 20], } +impl PublicKeyHash { + pub fn as_secp256k1_msg(&self) -> [u8; secp256k1::constants::MESSAGE_SIZE] { + let mut msg = [0u8; secp256k1::constants::MESSAGE_SIZE]; + msg[0..20].clone_from_slice(self.as_ref()); + + msg + } +} + impl AsRef<[u8]> for PublicKeyHash { fn as_ref(&self) -> &[u8] { self.hash.as_ref() @@ -1307,7 +1420,7 @@ impl PublicKeyHash { #[derive( Debug, Default, Eq, PartialEq, Copy, Clone, Serialize, Deserialize, ProtobufConvert, Hash, )] -#[protobuf_convert(pb = "witnet::Input")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Input")] pub struct Input { output_pointer: OutputPointer, } @@ -1329,7 +1442,7 @@ impl Input { /// Value transfer output transaction data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] -#[protobuf_convert(pb = "witnet::ValueTransferOutput")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::ValueTransferOutput")] pub struct ValueTransferOutput { /// Address that will receive the value pub pkh: PublicKeyHash, @@ -1341,9 +1454,21 @@ pub struct ValueTransferOutput { pub time_lock: u64, } +impl ValueTransferOutput { + #[inline] + pub fn value(&self) -> u64 { + self.value + } + + #[inline] + pub fn weight(&self) -> u32 { + OUTPUT_SIZE + } +} + /// Data request output transaction data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] -#[protobuf_convert(pb = "witnet::DataRequestOutput")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::DataRequestOutput")] pub struct DataRequestOutput { /// Data request structure pub data_request: RADRequest, @@ -1407,6 +1532,45 @@ impl DataRequestOutput { } } +#[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] +#[protobuf_convert(pb = "crate::proto::schema::witnet::StakeOutput")] +pub struct StakeOutput { + pub authorization: KeyedSignature, + pub key: StakeKey, + pub value: u64, +} + +impl StakeOutput { + #[inline] + pub fn weight(&self) -> u32 { + crate::transaction::STAKE_OUTPUT_WEIGHT + } +} + +pub enum Output { + DataRequest(DataRequestOutput), + Stake(StakeOutput), + ValueTransfer(ValueTransferOutput), +} + +impl Output { + pub fn value(&self) -> Result { + match self { + Output::DataRequest(output) => output.checked_total_value(), + Output::Stake(output) => Ok(output.value), + Output::ValueTransfer(output) => Ok(output.value), + } + } + + pub fn weight(&self) -> u32 { + match self { + Output::DataRequest(output) => output.weight(), + Output::Stake(output) => output.weight(), + Output::ValueTransfer(output) => output.weight(), + } + } +} + /// Information about the total supply #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SupplyInfo { @@ -1436,7 +1600,7 @@ pub struct SupplyInfo { /// Keyed signature data structure #[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::KeyedSignature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::KeyedSignature")] pub struct KeyedSignature { /// Signature pub signature: Signature, @@ -1444,6 +1608,78 @@ pub struct KeyedSignature { pub public_key: PublicKey, } +impl KeyedSignature { + pub fn from_recoverable_hex( + string: &str, + msg: &[u8], + ) -> Result { + let bytes = hex::decode(string).map_err(|e| Secp256k1ConversionError::HexDecode { + hex: String::from(string), + inner: e, + })?; + + Self::from_recoverable_slice(&bytes, msg) + } + pub fn from_recoverable( + recoverable: &RecoverableSignature, + message: &[u8], + ) -> Result { + let msg = secp256k1::Message::from_digest_slice(message) + .map_err(|e| Secp256k1ConversionError::Secp256k1 { inner: e })?; + let signature = recoverable.to_standard(); + let public_key = recoverable + .recover(&msg) + .map_err(|e| Secp256k1ConversionError::Secp256k1 { inner: e })?; + + Ok(KeyedSignature { + signature: signature.into(), + public_key: public_key.into(), + }) + } + + // Recovers a keyed signature from its serialized form and a known message. + pub fn from_recoverable_slice( + compact: &[u8], + message: &[u8], + ) -> Result { + let recid = RecoveryId::from_i32(compact[0] as i32) + .map_err(|e| Secp256k1ConversionError::Secp256k1 { inner: e })?; + let recoverable = RecoverableSignature::from_compact(&compact[1..], recid) + .map_err(|e| Secp256k1ConversionError::Secp256k1 { inner: e })?; + + Self::from_recoverable(&recoverable, message) + } + + /// Serializes a `KeyedSignature` into a compact encoding form that contains the public key recovery ID as a prefix. + pub fn to_recoverable_bytes( + self, + message: &[u8], + ) -> Result<[u8; 65], Secp256k1ConversionError> { + let mut recoverable_bytes = [0; 65]; + let bytes = self + .signature + .to_bytes() + .map_err(|e| Secp256k1ConversionError::Other { + inner: e.to_string(), + })?; + recoverable_bytes[1..].clone_from_slice(&bytes); + + // Silly algorithm that tries recovery with different recovery IDs in an attempt to guess which one is correct, + // provided that our `KeyedSignature` misses that information in comparison with `RecoverableSignature` + for i in 0..4 { + recoverable_bytes[0] = i; + + let recovered = KeyedSignature::from_recoverable_slice(&recoverable_bytes, message)?; + + if recovered.public_key == self.public_key { + break; + } + } + + Ok(recoverable_bytes) + } +} + /// Public Key data structure #[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize)] pub struct PublicKey { @@ -1491,6 +1727,14 @@ impl PublicKey { } } +impl std::str::FromStr for PublicKey { + type Err = Secp256k1ConversionError; + + fn from_str(s: &str) -> Result { + Self::try_from_slice(s.as_bytes()) + } +} + /// Secret Key data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] pub struct SecretKey { @@ -1511,7 +1755,7 @@ pub struct ExtendedSecretKey { /// BN256 public key #[derive(Debug, Eq, PartialEq, Hash, Default, Clone, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Bn256PublicKey")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Bn256PublicKey")] pub struct Bn256PublicKey { /// Compressed form of a BN256 public key pub public_key: Vec, @@ -1530,7 +1774,7 @@ pub struct Bn256SecretKey { /// BN256 signature #[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Bn256Signature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Bn256Signature")] pub struct Bn256Signature { /// Signature pub signature: Vec, @@ -1538,7 +1782,7 @@ pub struct Bn256Signature { /// BN256 signature and public key #[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Bn256KeyedSignature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Bn256KeyedSignature")] pub struct Bn256KeyedSignature { /// Signature pub signature: Bn256Signature, @@ -1645,17 +1889,26 @@ pub enum RADType { /// HTTP POST request #[serde(rename = "HTTP-POST")] HttpPost, + /// HTTP HEAD request + #[serde(rename = "HTTP-HEAD")] + HttpHead, } impl RADType { pub fn is_http(&self) -> bool { - matches!(self, RADType::HttpGet | RADType::HttpPost) + matches!( + self, + RADType::HttpGet | RADType::HttpPost | RADType::HttpHead + ) } } /// RAD request data structure #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, ProtobufConvert, Hash)] -#[protobuf_convert(pb = "witnet::DataRequestOutput_RADRequest", crate = "crate")] +#[protobuf_convert( + pb = "crate::proto::schema::witnet::DataRequestOutput_RADRequest", + crate = "crate" +)] pub struct RADRequest { /// Commitments for this request will not be accepted in any block proposed for an epoch /// whose opening timestamp predates the specified time lock. This effectively prevents @@ -1689,7 +1942,7 @@ impl RADRequest { /// Retrieve script and source #[derive(Debug, Eq, PartialEq, Clone, ProtobufConvert, Hash, Default)] #[protobuf_convert( - pb = "witnet::DataRequestOutput_RADRequest_RADRetrieve", + pb = "crate::proto::schema::witnet::DataRequestOutput_RADRequest_RADRetrieve", crate = "crate" )] pub struct RADRetrieve { @@ -1701,7 +1954,7 @@ pub struct RADRetrieve { pub script: Vec, /// Body of a HTTP-POST request pub body: Vec, - /// Extra headers of a HTTP-GET or HTTP-POST request + /// Extra headers of a HTTP-GET, HTTP-POST or HTTP-HEAD request pub headers: Vec<(String, String)>, } @@ -1810,6 +2063,9 @@ impl RADRetrieve { &[Field::Body, Field::Headers], ) } + RADType::HttpHead => { + check(&[Field::Kind, Field::Url, Field::Script], &[Field::Headers]) + } } } @@ -1866,7 +2122,10 @@ impl RADRetrieve { /// Filter stage #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] -#[protobuf_convert(pb = "witnet::DataRequestOutput_RADRequest_RADFilter", crate = "crate")] +#[protobuf_convert( + pb = "crate::proto::schema::witnet::DataRequestOutput_RADRequest_RADFilter", + crate = "crate" +)] pub struct RADFilter { /// `RadonFilters` code pub op: u32, @@ -1887,7 +2146,7 @@ impl RADFilter { /// Aggregate stage #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] #[protobuf_convert( - pb = "witnet::DataRequestOutput_RADRequest_RADAggregate", + pb = "crate::proto::schema::witnet::DataRequestOutput_RADRequest_RADAggregate", crate = "crate" )] pub struct RADAggregate { @@ -1913,7 +2172,10 @@ impl RADAggregate { /// Tally stage #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] -#[protobuf_convert(pb = "witnet::DataRequestOutput_RADRequest_RADTally", crate = "crate")] +#[protobuf_convert( + pb = "crate::proto::schema::witnet::DataRequestOutput_RADRequest_RADTally", + crate = "crate" +)] pub struct RADTally { /// List of filters to be applied in sequence pub filters: Vec, @@ -1947,6 +2209,8 @@ impl From for RADTally { type PrioritizedHash = (OrderedFloat, Hash); type PrioritizedVTTransaction = (OrderedFloat, VTTransaction); type PrioritizedDRTransaction = (OrderedFloat, DRTransaction); +type PrioritizedStakeTransaction = (OrderedFloat, StakeTransaction); +type PrioritizedUnstakeTransaction = (OrderedFloat, UnstakeTransaction); #[derive(Debug, Clone, Default)] struct UnconfirmedTransactions { @@ -2003,6 +2267,10 @@ pub struct TransactionsPool { total_vt_weight: u64, // Total size of all data request transactions inside the pool in weight units total_dr_weight: u64, + // Total size of all stake transactions inside the pool in weight units + total_st_weight: u64, + // Total size of all unstake transactions inside the pool in weight units + total_ut_weight: u64, // TransactionsPool size limit in weight units weight_limit: u64, // Ratio of value transfer transaction to data request transaction that should be in the @@ -2023,6 +2291,19 @@ pub struct TransactionsPool { required_reward_collateral_ratio: u64, // Map for unconfirmed transactions unconfirmed_transactions: UnconfirmedTransactions, + // TODO: refactor to use Rc> or + // Arc> to prevent the current indirect lookup (having to + // first query the index for the hash, and then using the hash to find the actual data) + st_transactions: HashMap, + sorted_st_index: BTreeSet, + ut_transactions: HashMap, + sorted_ut_index: BTreeSet, + // Minimum fee required to include a Stake Transaction into a block. We check for this fee in the + // TransactionPool so we can choose not to insert a transaction we will not mine anyway. + minimum_st_fee: u64, + // Minimum fee required to include a Unstake Transaction into a block. We check for this fee in the + // TransactionPool so we can choose not to insert a transaction we will not mine anyway. + minimum_ut_fee: u64, } impl Default for TransactionsPool { @@ -2039,17 +2320,27 @@ impl Default for TransactionsPool { output_pointer_map: Default::default(), total_vt_weight: 0, total_dr_weight: 0, + total_st_weight: 0, + total_ut_weight: 0, // Unlimited by default weight_limit: u64::MAX, // Try to keep the same amount of value transfer weight and data request weight vt_to_dr_factor: 1.0, // Default is to include all transactions into the pool and blocks minimum_vtt_fee: 0, + // Default is to include all transactions into the pool and blocks + minimum_st_fee: 0, + // Default is to include all transactions into the pool and blocks + minimum_ut_fee: 0, // Collateral minimum from consensus constants collateral_minimum: 0, // Required minimum reward to collateral percentage is defined as a consensus constant required_reward_collateral_ratio: u64::MAX, unconfirmed_transactions: Default::default(), + st_transactions: Default::default(), + sorted_st_index: Default::default(), + ut_transactions: Default::default(), + sorted_ut_index: Default::default(), } } } @@ -2082,7 +2373,7 @@ impl TransactionsPool { ) -> Vec { self.weight_limit = weight_limit; self.vt_to_dr_factor = vt_to_dr_factor; - + // TODO: take into account stake tx self.remove_transactions_for_size_limit() } @@ -2122,6 +2413,8 @@ impl TransactionsPool { && self.dr_transactions.is_empty() && self.co_transactions.is_empty() && self.re_transactions.is_empty() + && self.st_transactions.is_empty() + && self.ut_transactions.is_empty() } /// Remove all the transactions but keep the allocated memory for reuse. @@ -2138,12 +2431,20 @@ impl TransactionsPool { output_pointer_map, total_vt_weight, total_dr_weight, + total_st_weight, + total_ut_weight, weight_limit: _, vt_to_dr_factor: _, minimum_vtt_fee: _, + minimum_st_fee: _, + minimum_ut_fee: _, collateral_minimum: _, required_reward_collateral_ratio: _, unconfirmed_transactions, + st_transactions, + sorted_st_index, + ut_transactions, + sorted_ut_index, } = self; vt_transactions.clear(); @@ -2157,7 +2458,13 @@ impl TransactionsPool { output_pointer_map.clear(); *total_vt_weight = 0; *total_dr_weight = 0; + *total_st_weight = 0; + *total_ut_weight = 0; unconfirmed_transactions.clear(); + st_transactions.clear(); + sorted_st_index.clear(); + ut_transactions.clear(); + sorted_ut_index.clear(); } /// Returns the number of value transfer transactions in the pool. @@ -2202,6 +2509,48 @@ impl TransactionsPool { self.dr_transactions.len() } + /// Returns the number of stake transactions in the pool. + /// + /// # Examples: + /// + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash}; + /// # use witnet_data_structures::transaction::{Transaction, StakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// + /// let transaction = Transaction::Stake(StakeTransaction::default()); + /// + /// assert_eq!(pool.st_len(), 0); + /// + /// pool.insert(transaction, 0); + /// + /// assert_eq!(pool.st_len(), 1); + /// ``` + pub fn st_len(&self) -> usize { + self.st_transactions.len() + } + + /// Returns the number of unstake transactions in the pool. + /// + /// # Examples: + /// + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash}; + /// # use witnet_data_structures::transaction::{Transaction, StakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// + /// let transaction = Transaction::Stake(StakeTransaction::default()); + /// + /// assert_eq!(pool.st_len(), 0); + /// + /// pool.insert(transaction, 0); + /// + /// assert_eq!(pool.st_len(), 1); + /// ``` + pub fn ut_len(&self) -> usize { + self.ut_transactions.len() + } + /// Clear commit transactions in TransactionsPool pub fn clear_commits(&mut self) { self.co_transactions.clear(); @@ -2243,6 +2592,8 @@ impl TransactionsPool { // be impossible for nodes to broadcast these kinds of transactions. Transaction::Tally(_tt) => Err(TransactionError::NotValidTransaction), Transaction::Mint(_mt) => Err(TransactionError::NotValidTransaction), + Transaction::Stake(_st) => Ok(self.st_contains(&tx_hash)), + Transaction::Unstake(_ut) => Ok(self.ut_contains(&tx_hash)), } } @@ -2335,6 +2686,53 @@ impl TransactionsPool { .unwrap_or(Ok(false)) } + /// Returns `true` if the pool contains a stake transaction for the specified hash. + /// + /// The `key` may be any borrowed form of the hash, but `Hash` and + /// `Eq` on the borrowed form must match those for the key type. + /// + /// # Examples: + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash, Hashable}; + /// # use witnet_data_structures::transaction::{Transaction, StakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// + /// let transaction = Transaction::Stake(StakeTransaction::default()); + /// let hash = transaction.hash(); + /// assert!(!pool.st_contains(&hash)); + /// + /// pool.insert(transaction, 0); + /// + /// assert!(pool.t_contains(&hash)); + /// ``` + pub fn st_contains(&self, key: &Hash) -> bool { + self.st_transactions.contains_key(key) + } + + /// Returns `true` if the pool contains an unstake transaction for the + /// specified hash. + /// + /// The `key` may be any borrowed form of the hash, but `Hash` and + /// `Eq` on the borrowed form must match those for the key type. + /// + /// # Examples: + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash, Hashable}; + /// # use witnet_data_structures::transaction::{Transaction, UnstakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// + /// let transaction = Transaction::Stake(UnstakeTransaction::default()); + /// let hash = transaction.hash(); + /// assert!(!pool.ut_contains(&hash)); + /// + /// pool.insert(transaction, 0); + /// + /// assert!(pool.t_contains(&hash)); + /// ``` + pub fn ut_contains(&self, key: &Hash) -> bool { + self.ut_transactions.contains_key(key) + } + /// Remove a value transfer transaction from the pool and make sure that other transactions /// that may try to spend the same UTXOs are also removed. /// This should be used to remove transactions that got included in a consolidated block. @@ -2469,6 +2867,7 @@ impl TransactionsPool { for hash in hashes.iter() { self.vt_remove_inner(hash, false); self.dr_remove_inner(hash, false); + self.st_remove_inner(hash, false); } } } @@ -2544,6 +2943,73 @@ impl TransactionsPool { (commits_vector, total_fee, dr_pointer_vec) } + /// Remove a stake transaction from the pool and make sure that other transactions + /// that may try to spend the same UTXOs are also removed. + /// This should be used to remove transactions that got included in a consolidated block. + /// + /// Returns an `Option` with the stake transaction for the specified hash or `None` if not exist. + /// + /// The `key` may be any borrowed form of the hash, but `Hash` and + /// `Eq` on the borrowed form must match those for the key type. + /// + /// # Examples: + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash, Hashable}; + /// # use witnet_data_structures::transaction::{Transaction, StakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// let vt_transaction = StakeTransaction::default(); + /// let transaction = Transaction::Stake(st_transaction.clone()); + /// pool.insert(transaction.clone(),0); + /// + /// assert!(pool.st_contains(&transaction.hash())); + /// + /// let op_transaction_removed = pool.st_remove(&st_transaction); + /// + /// assert_eq!(Some(st_transaction), op_transaction_removed); + /// assert!(!pool.st_contains(&transaction.hash())); + /// ``` + pub fn st_remove(&mut self, tx: &StakeTransaction) -> Option { + let key = tx.hash(); + let transaction = self.st_remove_inner(&key, true); + + self.remove_inputs(&tx.body.inputs); + + transaction + } + + /// Remove a stake from the pool but do not remove other transactions that may try to spend the + /// same UTXOs. + /// This should be used to remove transactions that did not get included in a consolidated + /// block. + /// If the transaction did get included in a consolidated block, use `st_remove` instead. + fn st_remove_inner(&mut self, key: &Hash, consolidated: bool) -> Option { + self.st_transactions + .remove(key) + .map(|(weight, transaction)| { + self.sorted_st_index.remove(&(weight, *key)); + self.total_st_weight -= u64::from(transaction.weight()); + if !consolidated { + self.remove_tx_from_output_pointer_map(key, &transaction.body.inputs); + } + transaction + }) + } + + /// Remove an unstake transaction from the pool but do not remove other transactions that + /// may try to spend the same UTXOs, because this kind of transactions spend no UTXOs. + /// This should be used to remove transactions that did not get included in a consolidated + /// block. + fn ut_remove_inner(&mut self, key: &Hash) -> Option { + self.ut_transactions + .remove(key) + .map(|(weight, transaction)| { + self.sorted_ut_index.remove(&(weight, *key)); + self.total_ut_weight -= u64::from(transaction.weight()); + + transaction + }) + } + /// Returns a tuple with a vector of reveal transactions and the value /// of all the fees obtained with those reveals pub fn get_reveals(&self, dr_pool: &DataRequestPool) -> (Vec<&RevealTransaction>, u64) { @@ -2610,11 +3076,11 @@ impl TransactionsPool { /// Returns a list of all the removed transactions. fn remove_transactions_for_size_limit(&mut self) -> Vec { let mut removed_transactions = vec![]; - - while self.total_vt_weight + self.total_dr_weight > self.weight_limit { + while self.total_transactions_weight() > self.weight_limit { // Try to split the memory between value transfer and data requests using the same // ratio as the one used in blocks - // The ratio of vt to dr in blocks is currently 4:1 + // The ratio of vt to dr in blocks is currently 1:4 + // TODO: What the criteria to delete st? It should be 1:8 #[allow(clippy::cast_precision_loss)] let more_vtts_than_drs = self.total_vt_weight as f64 >= self.total_dr_weight as f64 * self.vt_to_dr_factor; @@ -2737,6 +3203,47 @@ impl TransactionsPool { .or_default() .insert(pkh, tx_hash); } + Transaction::Stake(st_tx) => { + let weight = f64::from(st_tx.weight()); + let priority = OrderedFloat(fee as f64 / weight); + + if fee < self.minimum_st_fee { + return vec![Transaction::Stake(st_tx)]; + } else { + self.total_st_weight += u64::from(st_tx.weight()); + + for input in &st_tx.body.inputs { + self.output_pointer_map + .entry(input.output_pointer) + .or_default() + .push(st_tx.hash()); + } + + self.st_transactions.insert(key, (priority, st_tx)); + self.sorted_st_index.insert((priority, key)); + } + } + Transaction::Unstake(ut_tx) => { + let weight = f64::from(ut_tx.weight()); + let priority = OrderedFloat(fee as f64 / weight); + + if fee < self.minimum_ut_fee { + return vec![Transaction::Unstake(ut_tx)]; + } else { + self.total_st_weight += u64::from(ut_tx.weight()); + + // TODO + // for input in &ut_tx.body.inputs { + // self.output_pointer_map + // .entry(input.output_pointer) + // .or_insert_with(Vec::new) + // .push(ut_tx.hash()); + // } + + self.ut_transactions.insert(key, (priority, ut_tx)); + self.sorted_ut_index.insert((priority, key)); + } + } tx => { panic!( "Transaction kind not supported by TransactionsPool: {:?}", @@ -2785,6 +3292,24 @@ impl TransactionsPool { .filter_map(move |(_, h)| self.dr_transactions.get(h).map(|(_, t)| t)) } + /// An iterator visiting all the stake transactions + /// in the pool + pub fn st_iter(&self) -> impl Iterator { + self.sorted_st_index + .iter() + .rev() + .filter_map(move |(_, h)| self.st_transactions.get(h).map(|(_, t)| t)) + } + + /// An iterator visiting all the unstake transactions + /// in the pool + pub fn ut_iter(&self) -> impl Iterator { + self.sorted_ut_index + .iter() + .rev() + .filter_map(move |(_, h)| self.ut_transactions.get(h).map(|(_, t)| t)) + } + /// Returns a reference to the value corresponding to the key. /// /// Examples: @@ -2803,6 +3328,7 @@ impl TransactionsPool { /// /// assert!(pool.vt_get(&hash).is_some()); /// ``` + // TODO: dead code pub fn vt_get(&self, key: &Hash) -> Option<&VTTransaction> { self.vt_transactions .get(key) @@ -2836,6 +3362,7 @@ impl TransactionsPool { /// pool.vt_retain(|tx| tx.body.outputs.len()>0); /// assert_eq!(pool.vt_len(), 1); /// ``` + // TODO: dead code pub fn vt_retain(&mut self, mut f: F) where F: FnMut(&VTTransaction) -> bool, @@ -2878,6 +3405,16 @@ impl TransactionsPool { .get(hash) .map(|rt| Transaction::Reveal(rt.clone())) }) + .or_else(|| { + self.st_transactions + .get(hash) + .map(|(_, st)| Transaction::Stake(st.clone())) + }) + .or_else(|| { + self.ut_transactions + .get(hash) + .map(|(_, ut)| Transaction::Unstake(ut.clone())) + }) } /// Update unconfirmed transactions @@ -2902,6 +3439,12 @@ impl TransactionsPool { Transaction::DataRequest(_) => { let _x = self.dr_remove_inner(&hash, false); } + Transaction::Stake(_) => { + let _x = self.st_remove_inner(&hash, false); + } + Transaction::Unstake(_) => { + let _x = self.ut_remove_inner(&hash); + } _ => continue, } @@ -2915,12 +3458,16 @@ impl TransactionsPool { v } + + pub fn total_transactions_weight(&self) -> u64 { + self.total_vt_weight + self.total_dr_weight + self.total_st_weight + } } /// Unspent output data structure (equivalent of Bitcoin's UTXO) /// It is used to locate the output by its transaction identifier and its position #[derive(Default, Hash, Copy, Clone, Eq, PartialEq, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::OutputPointer")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::OutputPointer")] pub struct OutputPointer { /// Transaction identifier pub transaction_id: Hash, @@ -2983,7 +3530,7 @@ impl PartialOrd for OutputPointer { /// Inventory entry data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::InventoryEntry")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::InventoryEntry")] pub enum InventoryEntry { /// Transaction Tx(Hash), @@ -3008,6 +3555,10 @@ pub enum TransactionPointer { Tally(u32), /// Mint Mint, + // Stake + Stake(u32), + // Unstake + Unstake(u32), } /// This is how transactions are stored in the database: hash of the containing block, plus index @@ -3268,6 +3819,9 @@ pub struct ChainState { pub superblock_state: SuperBlockState, /// TAPI Engine pub tapi_engine: TapiEngine, + /// Tracks stakes for every validator in the network + #[serde(default)] + pub stakes: Stakes, /// Unspent Outputs Pool #[serde(skip)] pub unspent_outputs_pool: UnspentOutputsPool, @@ -4021,7 +4575,7 @@ pub fn transaction_example() -> Transaction { let outputs = vec![value_transfer_output]; Transaction::DataRequest(DRTransaction::new( - DRTransactionBody::new(inputs, outputs, data_request_output), + DRTransactionBody::new(inputs, data_request_output, outputs), keyed_signature, )) } @@ -4052,6 +4606,7 @@ mod tests { }; use crate::{ + proto::versioning::{ProtocolVersion, VersionedHashable}, superblock::{mining_build_superblock, ARSIdentities}, transaction::{CommitTransactionBody, RevealTransactionBody, VTTransactionBody}, }; @@ -4125,7 +4680,7 @@ mod tests { .iter() .map(|input| { DRTransaction::new( - DRTransactionBody::new(vec![*input], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![*input], DataRequestOutput::default(), vec![]), vec![], ) }) @@ -4160,7 +4715,20 @@ mod tests { fn test_block_hashable_trait() { let block = block_example(); let expected = "70e15ac70bb00f49c7a593b2423f722dca187bbae53dc2f22647063b17608c01"; - assert_eq!(block.hash().to_string(), expected); + assert_eq!( + block.versioned_hash(ProtocolVersion::V1_7).to_string(), + expected + ); + let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; + assert_eq!( + block.versioned_hash(ProtocolVersion::V1_8).to_string(), + expected + ); + let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; + assert_eq!( + block.versioned_hash(ProtocolVersion::V2_0).to_string(), + expected + ); } #[test] @@ -4433,7 +5001,7 @@ mod tests { let secp = Secp256k1::new(); let secret_key = Secp256k1_SecretKey::from_slice(&[0xcd; 32]).expect("32 bytes, within curve order"); - let msg = Secp256k1_Message::from_slice(&data).unwrap(); + let msg = Secp256k1_Message::from_digest_slice(&data).unwrap(); let signature = secp.sign_ecdsa(&msg, &secret_key); let witnet_signature = Secp256k1Signature::from(signature); @@ -4454,7 +5022,7 @@ mod tests { let secp = Secp256k1::new(); let secret_key = Secp256k1_SecretKey::from_slice(&[0xcd; 32]).expect("32 bytes, within curve order"); - let msg = Secp256k1_Message::from_slice(&data).unwrap(); + let msg = Secp256k1_Message::from_digest_slice(&data).unwrap(); let signature = secp.sign_ecdsa(&msg, &secret_key); let witnet_signature = Signature::from(signature); @@ -4599,14 +5167,14 @@ mod tests { ); let dr_1 = DRTransaction::new( - DRTransactionBody::new(vec![input], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![input], DataRequestOutput::default(), vec![]), vec![], ); let dr_2 = DRTransaction::new( DRTransactionBody::new( vec![input], - vec![ValueTransferOutput::default()], DataRequestOutput::default(), + vec![ValueTransferOutput::default()], ), vec![], ); @@ -4676,14 +5244,14 @@ mod tests { ); let dr_1 = DRTransaction::new( - DRTransactionBody::new(vec![input], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![input], DataRequestOutput::default(), vec![]), vec![], ); let dr_2 = DRTransaction::new( DRTransactionBody::new( vec![input2], - vec![ValueTransferOutput::default()], DataRequestOutput::default(), + vec![ValueTransferOutput::default()], ), vec![], ); @@ -4777,11 +5345,11 @@ mod tests { assert_ne!(input0, input1); let dr_1 = DRTransaction::new( - DRTransactionBody::new(vec![input0], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![input0], DataRequestOutput::default(), vec![]), vec![], ); let dr_2 = DRTransaction::new( - DRTransactionBody::new(vec![input0, input1], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![input0, input1], DataRequestOutput::default(), vec![]), vec![], ); @@ -4843,7 +5411,7 @@ mod tests { fn transactions_pool_malleability_dr() { let input = Input::default(); let mut dr_1 = DRTransaction::new( - DRTransactionBody::new(vec![input], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![input], DataRequestOutput::default(), vec![]), vec![KeyedSignature::default()], ); // Add dummy signature, but pretend it is valid @@ -5038,12 +5606,12 @@ mod tests { Transaction::DataRequest(DRTransaction::new( DRTransactionBody::new( vec![Input::default()], + DataRequestOutput::default(), vec![ValueTransferOutput { pkh: Default::default(), value: i, time_lock: 0, }], - DataRequestOutput::default(), ), vec![], )) @@ -5281,7 +5849,7 @@ mod tests { witnesses: 1, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { @@ -5319,7 +5887,7 @@ mod tests { commit_and_reveal_fee: 501, ..Default::default() }; - let drb1 = DRTransactionBody::new(vec![], vec![], dro1); + let drb1 = DRTransactionBody::new(vec![], dro1, vec![]); let drt1 = DRTransaction::new( drb1, vec![KeyedSignature { @@ -5334,7 +5902,7 @@ mod tests { commit_and_reveal_fee: 100, ..Default::default() }; - let drb2 = DRTransactionBody::new(vec![], vec![], dro2); + let drb2 = DRTransactionBody::new(vec![], dro2, vec![]); let drt2 = DRTransaction::new( drb2, vec![KeyedSignature { @@ -5349,7 +5917,7 @@ mod tests { commit_and_reveal_fee: 500, ..Default::default() }; - let drb3 = DRTransactionBody::new(vec![], vec![], dro3); + let drb3 = DRTransactionBody::new(vec![], dro3, vec![]); let drt3 = DRTransaction::new( drb3, vec![KeyedSignature { @@ -5441,7 +6009,7 @@ mod tests { witnesses: 2, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { @@ -5481,7 +6049,7 @@ mod tests { witnesses: 1, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { @@ -5520,7 +6088,7 @@ mod tests { witnesses: 1, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { @@ -5558,7 +6126,7 @@ mod tests { witnesses: 2, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { @@ -5596,7 +6164,7 @@ mod tests { witnesses: 2, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { diff --git a/data_structures/src/chain/priority.rs b/data_structures/src/chain/priority.rs index f0c5c6303..a4984d8b7 100644 --- a/data_structures/src/chain/priority.rs +++ b/data_structures/src/chain/priority.rs @@ -852,7 +852,7 @@ mod tests { let input = priorities_factory(10usize, 0.0..=100.0, None); let engine = PriorityEngine::from_vec_with_capacity(input.clone(), 5); - assert_eq!(engine.get(0), input.get(0)); + assert_eq!(engine.get(0), input.first()); assert_eq!(engine.get(1), input.get(1)); assert_eq!(engine.get(2), input.get(2)); assert_eq!(engine.get(3), input.get(3)); diff --git a/data_structures/src/data_request.rs b/data_structures/src/data_request.rs index fc3823037..f7d375116 100644 --- a/data_structures/src/data_request.rs +++ b/data_structures/src/data_request.rs @@ -517,6 +517,7 @@ pub fn calculate_reward_collateral_ratio( witness_reward: u64, ) -> u64 { let dr_collateral = if collateral == 0 { + // if collateral is equal to 0 means that is equal to collateral_minimum value collateral_minimum } else { collateral @@ -742,7 +743,7 @@ mod tests { ..DataRequestInfo::default() }; let dr_transaction = DRTransaction::new( - DRTransactionBody::new(vec![Input::default()], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![Input::default()], DataRequestOutput::default(), vec![]), vec![KeyedSignature::default()], ); let dr_pointer = dr_transaction.hash(); @@ -777,7 +778,7 @@ mod tests { ..DataRequestOutput::default() }; let dr_transaction = DRTransaction::new( - DRTransactionBody::new(vec![Input::default()], vec![], dr_output), + DRTransactionBody::new(vec![Input::default()], dr_output, vec![]), vec![KeyedSignature::default()], ); let dr_pointer = dr_transaction.hash(); @@ -812,7 +813,7 @@ mod tests { ..DataRequestOutput::default() }; let dr_transaction = DRTransaction::new( - DRTransactionBody::new(vec![Input::default()], vec![], dr_output), + DRTransactionBody::new(vec![Input::default()], dr_output, vec![]), vec![KeyedSignature::default()], ); let dr_pointer = dr_transaction.hash(); diff --git a/data_structures/src/error.rs b/data_structures/src/error.rs index 18e807352..8b4c994d5 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -1,7 +1,9 @@ //! Error type definitions for the data structure module. use failure::Fail; +use hex::FromHexError; use std::num::ParseIntError; +use witnet_crypto::secp256k1; use crate::chain::{ DataRequestOutput, Epoch, Hash, HashParseError, OutputPointer, PublicKeyHash, RADType, @@ -288,6 +290,40 @@ pub enum TransactionError { max_weight: u32, dr_output: Box, }, + /// Stake amount below minimum + #[fail( + display = "The amount of coins in stake ({}) is less than the minimum allowed ({})", + stake, min_stake + )] + StakeBelowMinimum { min_stake: u64, stake: u64 }, + /// Unstaking more than the total staked + #[fail( + display = "Tried to unstake more coins than the current stake ({} > {})", + unstake, stake + )] + UnstakingMoreThanStaked { stake: u64, unstake: u64 }, + /// An stake output with zero value does not make sense + #[fail(display = "Transaction {} has a zero value stake output", tx_hash)] + ZeroValueStakeOutput { tx_hash: Hash }, + /// Invalid unstake signature + #[fail( + display = "Invalid unstake signature: ({}), withdrawal ({}), operator ({})", + signature, withdrawal, operator + )] + InvalidUnstakeSignature { + signature: Hash, + withdrawal: Hash, + operator: Hash, + }, + /// Invalid unstake time_lock + #[fail( + display = "The unstake timelock: ({}) is lower than the minimum unstaking delay ({})", + time_lock, unstaking_delay_seconds + )] + InvalidUnstakeTimelock { + time_lock: u64, + unstaking_delay_seconds: u32, + }, #[fail( display = "The reward-to-collateral ratio for this data request is {}, but must be equal or less than {}", reward_collateral_ratio, required_reward_collateral_ratio @@ -411,12 +447,33 @@ pub enum BlockError { weight, max_weight )] TotalDataRequestWeightLimitExceeded { weight: u32, max_weight: u32 }, + /// Stake weight limit exceeded by a block candidate + #[fail( + display = "Total weight of Stake Transactions in a block ({}) exceeds the limit ({})", + weight, max_weight + )] + TotalStakeWeightLimitExceeded { weight: u32, max_weight: u32 }, + /// Unstake weight limit exceeded + #[fail( + display = "Total weight of Unstake Transactions in a block ({}) exceeds the limit ({})", + weight, max_weight + )] + TotalUnstakeWeightLimitExceeded { weight: u32, max_weight: u32 }, + /// Repeated operator Stake + #[fail( + display = "A single operator is receiving stake more than once in a block: ({}) ", + pkh + )] + RepeatedStakeOperator { pkh: PublicKeyHash }, /// Missing expected tallies #[fail( display = "{} expected tally transactions are missing in block candidate {}", count, block_hash )] MissingExpectedTallies { count: usize, block_hash: Hash }, + /// Missing expected tallies + #[fail(display = "Validator {} is not eligible to propose a block", validator)] + ValidatorNotEligible { validator: PublicKeyHash }, } #[derive(Debug, Fail)] @@ -432,7 +489,7 @@ pub enum OutputPointerParseError { } /// The error type for operations on a [`Secp256k1Signature`](Secp256k1Signature) -#[derive(Debug, PartialEq, Eq, Fail)] +#[derive(Debug, PartialEq, Fail)] pub enum Secp256k1ConversionError { #[fail( display = "Failed to convert `witnet_data_structures::Signature` into `secp256k1::Signature`" @@ -451,6 +508,15 @@ pub enum Secp256k1ConversionError { display = "Failed to convert `witnet_data_structures::SecretKey` into `secp256k1::SecretKey`" )] FailSecretKeyConversion, + #[fail( + display = "Cannot decode a `witnet_data_structures::KeyedSignature` from the allegedly hex-encoded string '{}': {}", + hex, inner + )] + HexDecode { hex: String, inner: FromHexError }, + #[fail(display = "{}", inner)] + Secp256k1 { inner: secp256k1::Error }, + #[fail(display = "{}", inner)] + Other { inner: String }, } /// The error type for operations on a [`DataRequestPool`](DataRequestPool) diff --git a/data_structures/src/lib.rs b/data_structures/src/lib.rs index 0b83c5cc9..67365e300 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -13,10 +13,16 @@ #[macro_use] extern crate protobuf_convert; -use crate::chain::Environment; -use lazy_static::lazy_static; use std::sync::RwLock; +use lazy_static::lazy_static; + +use crate::proto::versioning::ProtocolInfo; +use crate::{ + chain::{Environment, Epoch}, + proto::versioning::ProtocolVersion, +}; + /// Module containing functions to generate Witnet's protocol messages pub mod builders; @@ -38,6 +44,9 @@ pub mod fee; /// Module containing data_request structures pub mod data_request; +/// Module containing data structures for the staking functionality +pub mod staking; + /// Module containing superblock structures pub mod superblock; @@ -69,6 +78,9 @@ mod serialization_helpers; /// Provides convenient constants, structs and methods for handling values denominated in Wit. pub mod wit; +/// Provides support for segmented protocol capabilities. +pub mod capabilities; + lazy_static! { /// Environment in which we are running: mainnet or testnet. /// This is used for Bech32 serialization. @@ -76,6 +88,9 @@ lazy_static! { // can work without having to manually set the environment. // The default environment will also be used in tests. static ref ENVIRONMENT: RwLock = RwLock::new(Environment::Mainnet); + /// Protocol version that we are running. + /// default to legacy for now — it's the v2 bootstrapping module's responsibility to upgrade it. + static ref PROTOCOL: RwLock = RwLock::new(ProtocolInfo::default()); } /// Environment in which we are running: mainnet or testnet. @@ -108,6 +123,44 @@ pub fn set_environment(environment: Environment) { } } +/// Protocol version that we are running. +pub fn get_protocol_version(epoch: Option) -> ProtocolVersion { + // This unwrap is safe as long as the lock is not poisoned. + // The lock can only become poisoned when a writer panics. + let protocol_info = PROTOCOL.read().unwrap(); + + if let Some(epoch) = epoch { + protocol_info.all_versions.version_for_epoch(epoch) + } else { + protocol_info.current_version + } +} + +/// Let the protocol versions controller know about the a protocol version, and its activation epoch. +pub fn register_protocol_version(protocol_version: ProtocolVersion, epoch: Epoch) { + log::debug!( + "Registering protocol version {protocol_version}, which enters into force at epoch {epoch}" + ); + // This unwrap is safe as long as the lock is not poisoned. + // The lock can only become poisoned when a writer panics. + let mut protocol_info = PROTOCOL.write().unwrap(); + protocol_info.register(epoch, protocol_version); +} + +/// Set the protocol version that we are running. +pub fn set_protocol_version(protocol_version: ProtocolVersion) { + // The lock can only become poisoned when a writer panics. + let mut protocol = PROTOCOL.write().unwrap(); + protocol.current_version = protocol_version; +} + +/// Refresh the protocol version, i.e. derive the current version from the current epoch, and update `current_version` +/// accordingly. +pub fn refresh_protocol_version(current_epoch: Epoch) { + let current_version = get_protocol_version(Some(current_epoch)); + set_protocol_version(current_version) +} + #[cfg(test)] mod tests { use super::*; @@ -118,4 +171,38 @@ mod tests { // addresses serialized as Bech32 will fail assert_eq!(get_environment(), Environment::Mainnet); } + + #[test] + fn protocol_versions() { + // If this default changes before the transition to V2 is complete, almost everything will + // break because data structures change schema and, serialization changes and hash + // derivation breaks too + let version = get_protocol_version(None); + assert_eq!(version, ProtocolVersion::V1_7); + + // Register the different protocol versions + register_protocol_version(ProtocolVersion::V1_7, 100); + register_protocol_version(ProtocolVersion::V1_8, 200); + register_protocol_version(ProtocolVersion::V2_0, 300); + + // The initial protocol version should be the default one + let version = get_protocol_version(Some(0)); + assert_eq!(version, ProtocolVersion::V1_7); + + // Right after the + let version = get_protocol_version(Some(100)); + assert_eq!(version, ProtocolVersion::V1_7); + let version = get_protocol_version(Some(200)); + assert_eq!(version, ProtocolVersion::V1_8); + let version = get_protocol_version(Some(300)); + assert_eq!(version, ProtocolVersion::V2_0); + + let version = get_protocol_version(None); + assert_eq!(version, ProtocolVersion::V1_7); + + set_protocol_version(ProtocolVersion::V2_0); + + let version = get_protocol_version(None); + assert_eq!(version, ProtocolVersion::V2_0); + } } diff --git a/data_structures/src/proto/mod.rs b/data_structures/src/proto/mod.rs index 4d681d8b9..2e7ba0e2c 100644 --- a/data_structures/src/proto/mod.rs +++ b/data_structures/src/proto/mod.rs @@ -8,6 +8,7 @@ use std::convert::TryFrom; use std::fmt::Debug; pub mod schema; +pub mod versioning; /// Used for establishing correspondence between rust struct /// and protobuf rust struct @@ -51,6 +52,7 @@ impl ProtobufConvert for chain::RADType { chain::RADType::HttpGet => witnet::DataRequestOutput_RADRequest_RADType::HttpGet, chain::RADType::Rng => witnet::DataRequestOutput_RADRequest_RADType::Rng, chain::RADType::HttpPost => witnet::DataRequestOutput_RADRequest_RADType::HttpPost, + chain::RADType::HttpHead => witnet::DataRequestOutput_RADRequest_RADType::HttpHead, } } @@ -60,6 +62,7 @@ impl ProtobufConvert for chain::RADType { witnet::DataRequestOutput_RADRequest_RADType::HttpGet => chain::RADType::HttpGet, witnet::DataRequestOutput_RADRequest_RADType::Rng => chain::RADType::Rng, witnet::DataRequestOutput_RADRequest_RADType::HttpPost => chain::RADType::HttpPost, + witnet::DataRequestOutput_RADRequest_RADType::HttpHead => chain::RADType::HttpHead, }) } } diff --git a/data_structures/src/proto/versioning.rs b/data_structures/src/proto/versioning.rs new file mode 100644 index 000000000..244c52037 --- /dev/null +++ b/data_structures/src/proto/versioning.rs @@ -0,0 +1,561 @@ +use failure::{Error, Fail}; +use protobuf::Message as _; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; +use strum_macros::{Display, EnumString}; + +use crate::chain::Epoch; +use crate::proto::schema::witnet::SuperBlock; +use crate::{ + chain::Hash, + get_protocol_version, + proto::{ + schema::witnet::{ + Block, Block_BlockHeader, Block_BlockHeader_BlockMerkleRoots, Block_BlockTransactions, + LegacyBlock, LegacyBlock_LegacyBlockHeader, + LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots, + LegacyBlock_LegacyBlockTransactions, LegacyMessage, LegacyMessage_LegacyCommand, + LegacyMessage_LegacyCommand_oneof_kind, Message_Command, Message_Command_oneof_kind, + }, + ProtobufConvert, + }, + transaction::MemoizedHashable, + types::Message, +}; + +#[derive(Clone, Debug, Default)] +pub struct ProtocolInfo { + pub current_version: ProtocolVersion, + pub all_versions: VersionsMap, +} + +impl ProtocolInfo { + pub fn register(&mut self, epoch: Epoch, version: ProtocolVersion) { + self.all_versions.register(epoch, version) + } +} + +#[derive(Clone, Debug, Default)] +pub struct VersionsMap { + efv: HashMap, + vfe: BTreeMap, +} + +impl VersionsMap { + pub fn register(&mut self, epoch: Epoch, version: ProtocolVersion) { + self.efv.insert(version, epoch); + self.vfe.insert(epoch, version); + } + + pub fn version_for_epoch(&self, queried_epoch: Epoch) -> ProtocolVersion { + self.vfe + .iter() + .rev() + .find(|(epoch, _)| **epoch <= queried_epoch) + .map(|(_, version)| version) + .copied() + .unwrap_or_default() + } +} + +#[derive( + Clone, Copy, Debug, Default, Deserialize, Display, EnumString, Eq, Hash, PartialEq, Serialize, +)] +pub enum ProtocolVersion { + /// The original Witnet protocol. + // TODO: update this default once 2.0 is completely active + #[default] + V1_7, + /// The transitional protocol based on 1.x but with staking enabled. + V1_8, + /// The final Witnet 2.0 protocol. + V2_0, +} + +impl ProtocolVersion { + pub fn guess() -> Self { + get_protocol_version(None) + } +} + +pub trait Versioned: ProtobufConvert { + type LegacyType: protobuf::Message; + + /// Turn a protobuf-compatible data structure into a versioned form of itself. + /// + /// For truly versionable data structures, this method should be implemented manually. For other + /// data structures, the trait's own blanket implementation should be fine. + fn to_versioned_pb( + &self, + _version: ProtocolVersion, + ) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(Box::new(self.to_pb())) + } + /// Turn a protobuf-compaitble data structures into its serialized protobuf bytes. + /// This blanket implementation should normally not be overriden. + fn to_versioned_pb_bytes(&self, version: ProtocolVersion) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(self.to_versioned_pb(version)?.write_to_bytes()?) + } + + /// Constructs an instance of this data structure based on a protobuf instance of its legacy + /// schema. + fn from_versioned_pb(legacy: Self::LegacyType) -> Result + where + Self: From, + { + Ok(Self::from(legacy)) + } + + /// Tries to deserialize a data structure from its regular protobuf schema, and if it fails, it + /// retries with its legacy schema. + fn from_versioned_pb_bytes(bytes: &[u8]) -> Result + where + ::ProtoStruct: protobuf::Message, + Self: From, + { + let mut current = Self::ProtoStruct::new(); + let direct_attempt = current + .merge_from_bytes(bytes) + .map_err(|e| Error::from_boxed_compat(Box::new(e.compat()))) + .and_then(|_| Self::from_pb(current)); + + if direct_attempt.is_ok() { + direct_attempt + } else { + let mut legacy = Self::LegacyType::new(); + legacy.merge_from_bytes(bytes)?; + + Ok(Self::from(legacy)) + } + } +} + +impl Versioned for crate::chain::BlockMerkleRoots { + type LegacyType = LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots; + + fn to_versioned_pb( + &self, + version: ProtocolVersion, + ) -> Result, Error> { + use ProtocolVersion::*; + + let mut pb = self.to_pb(); + + let versioned: Box = match version { + // Legacy merkle roots need to get rearranged + V1_7 => Box::new(Self::LegacyType::from(pb)), + // Transition merkle roots need no transformation + V1_8 => Box::new(pb), + // Final merkle roots need to drop the mint hash + V2_0 => { + pb.set_mint_hash(Default::default()); + + Box::new(pb) + } + }; + + Ok(versioned) + } +} + +impl Versioned for crate::chain::BlockHeader { + type LegacyType = LegacyBlock_LegacyBlockHeader; + + fn to_versioned_pb( + &self, + version: ProtocolVersion, + ) -> Result, Error> { + use ProtocolVersion::*; + + let pb = self.to_pb(); + + let versioned: Box = match version { + // Legacy block headers need to be rearranged + V1_7 => Box::new(Self::LegacyType::from(pb)), + // All other block headers need no transformation + V1_8 | V2_0 => Box::new(pb), + }; + + Ok(versioned) + } +} + +impl Versioned for crate::chain::SuperBlock { + type LegacyType = SuperBlock; + + fn to_versioned_pb_bytes(&self, _version: ProtocolVersion) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(self.hashable_bytes()) + } +} + +impl Versioned for crate::chain::Block { + type LegacyType = LegacyBlock; + + fn to_versioned_pb( + &self, + _version: ProtocolVersion, + ) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(Box::new(Self::LegacyType::from(self.to_pb()))) + } +} + +impl Versioned for Message { + type LegacyType = LegacyMessage; + + fn to_versioned_pb(&self, version: ProtocolVersion) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + use ProtocolVersion::*; + + let pb = self.to_pb(); + + let versioned: Box = match version { + V1_7 => Box::new(Self::LegacyType::from(pb)), + V1_8 | V2_0 => Box::new(pb), + }; + + Ok(versioned) + } +} + +pub trait AutoVersioned: ProtobufConvert {} + +impl AutoVersioned for crate::chain::BlockHeader {} +impl AutoVersioned for crate::chain::SuperBlock {} + +pub trait VersionedHashable { + fn versioned_hash(&self, version: ProtocolVersion) -> Hash; +} + +impl VersionedHashable for T +where + T: AutoVersioned + Versioned, + ::ProtoStruct: protobuf::Message, +{ + fn versioned_hash(&self, version: ProtocolVersion) -> Hash { + // This unwrap is kept in here just because we want `VersionedHashable` to have the same interface as + // `Hashable`. + witnet_crypto::hash::calculate_sha256(&self.to_versioned_pb_bytes(version).unwrap()).into() + } +} + +impl VersionedHashable for crate::chain::Block { + fn versioned_hash(&self, version: ProtocolVersion) -> Hash { + self.block_header.versioned_hash(version) + } +} + +impl From + for LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots +{ + fn from(header: Block_BlockHeader_BlockMerkleRoots) -> Self { + let mut legacy = LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots::new(); + legacy.set_mint_hash(header.get_mint_hash().clone()); + legacy.vt_hash_merkle_root = header.vt_hash_merkle_root; + legacy.dr_hash_merkle_root = header.dr_hash_merkle_root; + legacy.commit_hash_merkle_root = header.commit_hash_merkle_root; + legacy.reveal_hash_merkle_root = header.reveal_hash_merkle_root; + legacy.tally_hash_merkle_root = header.tally_hash_merkle_root; + + legacy + } +} + +impl From + for Block_BlockHeader_BlockMerkleRoots +{ + fn from( + LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots { + mint_hash, + vt_hash_merkle_root, + dr_hash_merkle_root, + commit_hash_merkle_root, + reveal_hash_merkle_root, + tally_hash_merkle_root, + .. + }: LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots, + ) -> Self { + let mut header = Block_BlockHeader_BlockMerkleRoots::new(); + header.mint_hash = mint_hash; + header.vt_hash_merkle_root = vt_hash_merkle_root; + header.dr_hash_merkle_root = dr_hash_merkle_root; + header.commit_hash_merkle_root = commit_hash_merkle_root; + header.reveal_hash_merkle_root = reveal_hash_merkle_root; + header.tally_hash_merkle_root = tally_hash_merkle_root; + header.set_stake_hash_merkle_root(Hash::default().to_pb()); + header.set_unstake_hash_merkle_root(Hash::default().to_pb()); + + header + } +} + +impl From for LegacyBlock_LegacyBlockHeader { + fn from( + Block_BlockHeader { + signals, + beacon, + merkle_roots, + proof, + bn256_public_key, + .. + }: Block_BlockHeader, + ) -> Self { + let mut legacy = LegacyBlock_LegacyBlockHeader::new(); + legacy.signals = signals; + legacy.beacon = beacon; + legacy.merkle_roots = merkle_roots.map(Into::into); + legacy.proof = proof; + legacy.bn256_public_key = bn256_public_key; + + legacy + } +} + +impl From for Block_BlockHeader { + fn from( + LegacyBlock_LegacyBlockHeader { + signals, + beacon, + merkle_roots, + proof, + bn256_public_key, + .. + }: LegacyBlock_LegacyBlockHeader, + ) -> Self { + let mut header = Block_BlockHeader::new(); + header.signals = signals; + header.beacon = beacon; + header.merkle_roots = merkle_roots.map(Into::into); + header.proof = proof; + header.bn256_public_key = bn256_public_key; + + header + } +} + +impl From for LegacyBlock_LegacyBlockTransactions { + fn from( + Block_BlockTransactions { + mint, + value_transfer_txns, + data_request_txns, + commit_txns, + reveal_txns, + tally_txns, + .. + }: Block_BlockTransactions, + ) -> Self { + let mut legacy = LegacyBlock_LegacyBlockTransactions::new(); + legacy.mint = mint; + legacy.value_transfer_txns = value_transfer_txns; + legacy.data_request_txns = data_request_txns; + legacy.commit_txns = commit_txns; + legacy.reveal_txns = reveal_txns; + legacy.tally_txns = tally_txns; + + legacy + } +} + +impl From for Block_BlockTransactions { + fn from( + LegacyBlock_LegacyBlockTransactions { + mint, + value_transfer_txns, + data_request_txns, + commit_txns, + reveal_txns, + tally_txns, + .. + }: LegacyBlock_LegacyBlockTransactions, + ) -> Self { + let mut txns = Block_BlockTransactions::new(); + txns.mint = mint; + txns.value_transfer_txns = value_transfer_txns; + txns.data_request_txns = data_request_txns; + txns.commit_txns = commit_txns; + txns.reveal_txns = reveal_txns; + txns.tally_txns = tally_txns; + txns.stake_txns = vec![].into(); + txns.unstake_txns = vec![].into(); + + txns + } +} + +impl From for LegacyBlock { + fn from( + Block { + block_header, + block_sig, + txns, + .. + }: Block, + ) -> Self { + let mut legacy = LegacyBlock::new(); + legacy.block_header = block_header.map(Into::into); + legacy.block_sig = block_sig; + legacy.txns = txns.map(Into::into); + + legacy + } +} + +impl From for Block { + fn from( + LegacyBlock { + block_header, + block_sig, + txns, + .. + }: LegacyBlock, + ) -> Self { + let mut block = Block::new(); + block.block_header = block_header.map(Into::into); + block.block_sig = block_sig; + block.txns = txns.map(Into::into); + + block + } +} + +impl From for LegacyMessage_LegacyCommand_oneof_kind { + fn from(value: Message_Command_oneof_kind) -> Self { + match value { + Message_Command_oneof_kind::Version(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Version(x) + } + Message_Command_oneof_kind::Verack(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Verack(x) + } + Message_Command_oneof_kind::GetPeers(x) => { + LegacyMessage_LegacyCommand_oneof_kind::GetPeers(x) + } + Message_Command_oneof_kind::Peers(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Peers(x) + } + Message_Command_oneof_kind::Block(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Block(x.into()) + } + Message_Command_oneof_kind::InventoryAnnouncement(x) => { + LegacyMessage_LegacyCommand_oneof_kind::InventoryAnnouncement(x) + } + Message_Command_oneof_kind::InventoryRequest(x) => { + LegacyMessage_LegacyCommand_oneof_kind::InventoryRequest(x) + } + Message_Command_oneof_kind::LastBeacon(x) => { + LegacyMessage_LegacyCommand_oneof_kind::LastBeacon(x) + } + Message_Command_oneof_kind::Transaction(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Transaction(x) + } + Message_Command_oneof_kind::SuperBlockVote(x) => { + LegacyMessage_LegacyCommand_oneof_kind::SuperBlockVote(x) + } + Message_Command_oneof_kind::SuperBlock(x) => { + LegacyMessage_LegacyCommand_oneof_kind::SuperBlock(x) + } + } + } +} + +impl From for Message_Command_oneof_kind { + fn from(legacy: LegacyMessage_LegacyCommand_oneof_kind) -> Self { + match legacy { + LegacyMessage_LegacyCommand_oneof_kind::Version(x) => { + Message_Command_oneof_kind::Version(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Verack(x) => { + Message_Command_oneof_kind::Verack(x) + } + LegacyMessage_LegacyCommand_oneof_kind::GetPeers(x) => { + Message_Command_oneof_kind::GetPeers(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Peers(x) => { + Message_Command_oneof_kind::Peers(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Block(x) => { + Message_Command_oneof_kind::Block(x.into()) + } + LegacyMessage_LegacyCommand_oneof_kind::InventoryAnnouncement(x) => { + Message_Command_oneof_kind::InventoryAnnouncement(x) + } + LegacyMessage_LegacyCommand_oneof_kind::InventoryRequest(x) => { + Message_Command_oneof_kind::InventoryRequest(x) + } + LegacyMessage_LegacyCommand_oneof_kind::LastBeacon(x) => { + Message_Command_oneof_kind::LastBeacon(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Transaction(x) => { + Message_Command_oneof_kind::Transaction(x) + } + LegacyMessage_LegacyCommand_oneof_kind::SuperBlockVote(x) => { + Message_Command_oneof_kind::SuperBlockVote(x) + } + LegacyMessage_LegacyCommand_oneof_kind::SuperBlock(x) => { + Message_Command_oneof_kind::SuperBlock(x) + } + } + } +} + +impl From for LegacyMessage_LegacyCommand { + fn from(Message_Command { kind, .. }: Message_Command) -> Self { + let mut legacy = LegacyMessage_LegacyCommand::new(); + legacy.kind = kind.map(Into::into); + + legacy + } +} + +impl From for Message_Command { + fn from(LegacyMessage_LegacyCommand { kind, .. }: LegacyMessage_LegacyCommand) -> Self { + let mut command = Message_Command::new(); + command.kind = kind.map(Into::into); + + command + } +} + +impl From for LegacyMessage { + fn from( + crate::proto::schema::witnet::Message { magic, kind, .. }: crate::proto::schema::witnet::Message, + ) -> Self { + let mut legacy = LegacyMessage::new(); + legacy.magic = magic; + legacy.kind = kind.map(Into::into); + + legacy + } +} + +impl From for crate::proto::schema::witnet::Message { + fn from(LegacyMessage { magic, kind, .. }: LegacyMessage) -> Self { + let mut message = crate::proto::schema::witnet::Message::new(); + message.magic = magic; + message.kind = kind.map(Into::into); + + message + } +} + +impl From for Message { + fn from(legacy: LegacyMessage) -> Self { + let pb = crate::proto::schema::witnet::Message::from(legacy); + + Message::from_pb(pb).unwrap() + } +} diff --git a/data_structures/src/radon_error.rs b/data_structures/src/radon_error.rs index 7d3176629..8f43df30a 100644 --- a/data_structures/src/radon_error.rs +++ b/data_structures/src/radon_error.rs @@ -67,7 +67,7 @@ pub enum RadonErrors { /// The request is rejected on the grounds that it may cause the submitter to spend or stake an /// amount of value that is unjustifiably high when compared with the reward they will be getting BridgePoorIncentives = 0xE1, - /// The request result length exceeds a bridge contract defined limit + /// The request result length exceeds the bridge limit BridgeOversizedResult = 0xE2, // This should not exist: /// Some tally error is not intercepted but should diff --git a/data_structures/src/serialization_helpers.rs b/data_structures/src/serialization_helpers.rs index 7f7d7d4e6..a8d59ee69 100644 --- a/data_structures/src/serialization_helpers.rs +++ b/data_structures/src/serialization_helpers.rs @@ -360,7 +360,7 @@ struct RADRetrieveSerializationHelperJson { /// Body of a HTTP-POST request #[serde(default, skip_serializing_if = "Vec::is_empty")] pub body: Vec, - /// Extra headers of a HTTP-GET or HTTP-POST request + /// Extra headers of a HTTP-GET, HTTP-HEAD or HTTP-POST request #[serde(default, skip_serializing_if = "Vec::is_empty")] pub headers: Vec<(String, String)>, } @@ -377,7 +377,7 @@ struct RADRetrieveSerializationHelperBincode { pub script: Vec, /// Body of a HTTP-POST request pub body: Vec, - /// Extra headers of a HTTP-GET or HTTP-POST request + /// Extra headers of a HTTP-GET, HTTP-HEAD or HTTP-POST request pub headers: Vec<(String, String)>, } diff --git a/data_structures/src/staking/constants.rs b/data_structures/src/staking/constants.rs new file mode 100644 index 000000000..d461b0560 --- /dev/null +++ b/data_structures/src/staking/constants.rs @@ -0,0 +1,2 @@ +/// A minimum stakeable amount needs to exist to prevent spamming of the tracker. +pub const MINIMUM_STAKEABLE_AMOUNT_WITS: u64 = 10_000; diff --git a/data_structures/src/staking/errors.rs b/data_structures/src/staking/errors.rs new file mode 100644 index 000000000..03ac013a8 --- /dev/null +++ b/data_structures/src/staking/errors.rs @@ -0,0 +1,105 @@ +use crate::staking::helpers::StakeKey; +use failure::Fail; +use std::{ + convert::From, + fmt::{Debug, Display}, + sync::PoisonError, +}; + +/// All errors related to the staking functionality. +#[derive(Debug, Eq, PartialEq, Fail)] +pub enum StakesError +where + Address: Debug + Display + Sync + Send + 'static, + Coins: Debug + Display + Sync + Send + 'static, + Epoch: Debug + Display + Sync + Send + 'static, +{ + /// The amount of coins being staked or the amount that remains after unstaking is below the + /// minimum stakeable amount. + #[fail( + display = "The amount of coins being staked ({}) or the amount that remains after unstaking is below the minimum stakeable amount ({})", + amount, minimum + )] + AmountIsBelowMinimum { + /// The number of coins being staked or remaining after staking. + amount: Coins, + /// The minimum stakeable amount. + minimum: Coins, + }, + /// Tried to query `Stakes` for information that belongs to the past. + #[fail( + display = "Tried to query `Stakes` for information that belongs to the past. Query Epoch: {} Latest Epoch: {}", + epoch, latest + )] + EpochInThePast { + /// The Epoch being referred. + epoch: Epoch, + /// The latest Epoch. + latest: Epoch, + }, + /// An operation thrown an Epoch value that overflows. + #[fail( + display = "An operation thrown an Epoch value that overflows. Computed Epoch: {} Maximum Epoch: {}", + computed, maximum + )] + EpochOverflow { + /// The computed Epoch value. + computed: u64, + /// The maximum Epoch. + maximum: Epoch, + }, + /// Tried to query for a stake entry that is not registered in `Stakes`. + #[fail( + display = "Tried to query for a stake entry that is not registered in Stakes {}", + key + )] + EntryNotFound { + /// A validator and withdrawer address pair. + key: StakeKey
, + }, + /// Tried to obtain a lock on a write-locked piece of data that is already locked. + #[fail( + display = "The authentication signature contained within a stake transaction is not valid for the given validator and withdrawer addresses" + )] + PoisonedLock, + /// The authentication signature contained within a stake transaction is not valid for the given validator and + /// withdrawer addresses. + #[fail( + display = "The authentication signature contained within a stake transaction is not valid for the given validator and withdrawer addresses" + )] + InvalidAuthentication, + /// Tried to query for a stake entry by validator that is not registered in `Stakes`. + #[fail( + display = "Tried to query for a stake entry by validator ({}) that is not registered in Stakes", + validator + )] + ValidatorNotFound { + /// A validator address. + validator: Address, + }, + /// Tried to query for a stake entry by withdrawer that is not registered in `Stakes`. + #[fail( + display = "Tried to query for a stake entry by withdrawer ({}) that is not registered in Stakes", + withdrawer + )] + WithdrawerNotFound { + /// A withdrawer address. + withdrawer: Address, + }, + /// Tried to query for a stake entry without providing a validator or a withdrawer address. + #[fail( + display = "Tried to query a stake entry without providing a validator or a withdrawer address" + )] + EmptyQuery, +} + +impl From> for StakesError +where + Address: Debug + Display + Sync + Send + 'static, + Coins: Debug + Display + Sync + Send + 'static, + Epoch: Debug + Display + Sync + Send + 'static, +{ + fn from(_value: PoisonError) -> Self { + StakesError::PoisonedLock + } +} diff --git a/data_structures/src/staking/helpers.rs b/data_structures/src/staking/helpers.rs new file mode 100644 index 000000000..c32ea04cf --- /dev/null +++ b/data_structures/src/staking/helpers.rs @@ -0,0 +1,182 @@ +use std::fmt::{Debug, Display, Formatter}; +use std::{rc::Rc, str::FromStr, sync::RwLock}; + +use failure::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{chain::PublicKeyHash, proto::ProtobufConvert}; + +use crate::staking::prelude::*; + +/// Just a type alias for consistency of using the same data type to represent power. +pub type Power = u64; + +/// The resulting type for all the fallible functions in this module. +pub type StakesResult = Result>; + +/// Newtype for a reference-counted and read-write-locked instance of `Stake`. +/// +/// This newtype is needed for implementing `PartialEq` manually on the locked data, which cannot be done directly +/// because those are externally owned types. +#[derive(Clone, Debug, Default)] +pub struct SyncStake +where + Address: Default, + Epoch: Default, +{ + /// The lock itself. + pub value: Rc>>, +} + +impl From> + for SyncStake +where + Address: Default, + Epoch: Default, +{ + #[inline] + fn from(value: Stake) -> Self { + SyncStake { + value: Rc::new(RwLock::new(value)), + } + } +} + +impl PartialEq for SyncStake +where + Address: Default, + Epoch: Default + PartialEq, + Coins: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + let self_stake = self.value.read().unwrap(); + let other_stake = other.value.read().unwrap(); + + self_stake.coins.eq(&other_stake.coins) && other_stake.epochs.eq(&other_stake.epochs) + } +} + +impl<'de, Address, Coins, Epoch, Power> Deserialize<'de> for SyncStake +where + Address: Default, + Epoch: Default, + Stake: Deserialize<'de>, +{ + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + >::deserialize(deserializer).map(SyncStake::from) + } +} + +impl Serialize for SyncStake +where + Address: Default, + Epoch: Default, + Stake: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.value.read().unwrap().serialize(serializer) + } +} + +/// Couples a validator address with a withdrawer address together. This is meant to be used in `Stakes` as the index +/// for the `by_key` index. +#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub struct StakeKey
{ + /// A validator address. + pub validator: Address, + /// A withdrawer address. + pub withdrawer: Address, +} + +impl ProtobufConvert for StakeKey { + type ProtoStruct = crate::proto::schema::witnet::StakeKey; + + fn to_pb(&self) -> Self::ProtoStruct { + let mut proto = Self::ProtoStruct::new(); + proto.set_validator(self.validator.to_pb()); + proto.set_withdrawer(self.withdrawer.to_pb()); + + proto + } + + fn from_pb(mut pb: Self::ProtoStruct) -> Result { + let validator = PublicKeyHash::from_pb(pb.take_validator())?; + let withdrawer = PublicKeyHash::from_pb(pb.take_withdrawer())?; + + Ok(Self { + validator, + withdrawer, + }) + } +} + +impl From<(T, T)> for StakeKey
+where + T: Into
, +{ + fn from(val: (T, T)) -> Self { + StakeKey { + validator: val.0.into(), + withdrawer: val.1.into(), + } + } +} + +impl
From<&str> for StakeKey
+where + Address: FromStr, +
::Err: std::fmt::Debug, +{ + fn from(val: &str) -> Self { + StakeKey { + validator: Address::from_str(val).unwrap(), + withdrawer: Address::from_str(val).unwrap(), + } + } +} + +impl
Display for StakeKey
+where + Address: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "validator: {} withdrawer: {}", + self.validator, self.withdrawer + ) + } +} + +/// Couples an amount of coins, a validator address and a withdrawer address together. This is meant to be used in +/// `Stakes` as the index of the `by_coins` index. +#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct CoinsAndAddresses { + /// An amount of coins. + pub coins: Coins, + /// A validator and withdrawer addresses pair. + pub addresses: StakeKey
, +} + +/// Allows telling the `census` method in `Stakes` to source addresses from its internal `by_coins` +/// following different strategies. +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +pub enum CensusStrategy { + /// Retrieve all addresses, ordered by decreasing power. + All = 0, + /// Retrieve every Nth address, ordered by decreasing power. + StepBy(usize) = 1, + /// Retrieve the most powerful N addresses, ordered by decreasing power. + Take(usize) = 2, + /// Retrieve a total of N addresses, evenly distributed from the index, ordered by decreasing + /// power. + Evenly(usize) = 3, +} diff --git a/data_structures/src/staking/mod.rs b/data_structures/src/staking/mod.rs new file mode 100644 index 000000000..d678c71e3 --- /dev/null +++ b/data_structures/src/staking/mod.rs @@ -0,0 +1,107 @@ +#![deny(missing_docs)] + +/// Auxiliary convenience types and data structures. +pub mod helpers; +/// Constants related to the staking functionality. +pub mod constants; +/// Errors related to the staking functionality. +pub mod errors; +/// The data structure and related logic for stake entries. +pub mod stake; +/// The data structure and related logic for keeping track of multiple stake entries. +pub mod stakes; + +/// Module re-exporting virtually every submodule on a single level to ease importing of everything +/// staking-related. +pub mod prelude { + pub use crate::capabilities::*; + + pub use super::helpers::*; + pub use super::constants::*; + pub use super::errors::*; + pub use super::stake::*; + pub use super::stakes::*; +} + +#[cfg(test)] +pub mod test { + use super::prelude::*; + + #[test] + fn test_e2e() { + let mut stakes = Stakes::::with_minimum(1); + + // Alpha stakes 2 @ epoch 0 + stakes.add_stake("Alpha", 2, 0).unwrap(); + + // Nobody holds any power just yet + let rank = stakes.rank(Capability::Mining, 0).collect::>(); + assert_eq!(rank, vec![("Alpha".into(), 0)]); + + // One epoch later, Alpha starts to hold power + let rank = stakes.rank(Capability::Mining, 1).collect::>(); + assert_eq!(rank, vec![("Alpha".into(), 2)]); + + // Beta stakes 5 @ epoch 10 + stakes.add_stake("Beta", 5, 10).unwrap(); + + // Alpha is still leading, but Beta has scheduled its takeover + let rank = stakes.rank(Capability::Mining, 10).collect::>(); + assert_eq!(rank, vec![("Alpha".into(), 20), ("Beta".into(), 0)]); + + // Beta eventually takes over after epoch 16 + let rank = stakes.rank(Capability::Mining, 16).collect::>(); + assert_eq!(rank, vec![("Alpha".into(), 32), ("Beta".into(), 30)]); + let rank = stakes.rank(Capability::Mining, 17).collect::>(); + assert_eq!(rank, vec![("Beta".into(), 35), ("Alpha".into(), 34)]); + + // Gamma should never take over, even in a million epochs, because it has only 1 coin + stakes.add_stake("Gamma", 1, 30).unwrap(); + let rank = stakes + .rank(Capability::Mining, 1_000_000) + .collect::>(); + assert_eq!( + rank, + vec![ + ("Beta".into(), 4_999_950), + ("Alpha".into(), 2_000_000), + ("Gamma".into(), 999_970) + ] + ); + + // But Delta is here to change it all + stakes.add_stake("Delta", 1_000, 50).unwrap(); + let rank = stakes.rank(Capability::Mining, 50).collect::>(); + assert_eq!( + rank, + vec![ + ("Beta".into(), 200), + ("Alpha".into(), 100), + ("Gamma".into(), 20), + ("Delta".into(), 0) + ] + ); + let rank = stakes.rank(Capability::Mining, 51).collect::>(); + assert_eq!( + rank, + vec![ + ("Delta".into(), 1_000), + ("Beta".into(), 205), + ("Alpha".into(), 102), + ("Gamma".into(), 21) + ] + ); + + // If Alpha removes all of its stake, it should immediately disappear + stakes.remove_stake("Alpha", 2).unwrap(); + let rank = stakes.rank(Capability::Mining, 51).collect::>(); + assert_eq!( + rank, + vec![ + ("Delta".into(), 1_000), + ("Beta".into(), 205), + ("Gamma".into(), 21), + ] + ); + } +} diff --git a/data_structures/src/staking/stake.rs b/data_structures/src/staking/stake.rs new file mode 100644 index 000000000..3f58e40be --- /dev/null +++ b/data_structures/src/staking/stake.rs @@ -0,0 +1,138 @@ +use std::{marker::PhantomData, ops::*}; + +use serde::{Deserialize, Serialize}; + +use super::prelude::*; +use std::fmt::{Debug, Display}; + +/// A data structure that keeps track of a staker's staked coins and the epochs for different +/// capabilities. +#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct Stake +where + Address: Default, + Epoch: Default, +{ + /// An amount of staked coins. + pub coins: Coins, + /// The average epoch used to derive coin age for different capabilities. + pub epochs: CapabilityMap, + // These two phantom fields are here just for the sake of specifying generics. + phantom_address: PhantomData
, + phantom_power: PhantomData, +} + +impl Stake +where + Address: Default + Debug + Display + Sync + Send, + Coins: Copy + + From + + PartialOrd + + num_traits::Zero + + Add + + Sub + + Mul + + Mul + + Debug + + Display + + Send + + Sync, + Epoch: Copy + + Default + + num_traits::Saturating + + Sub + + From + + Debug + + Display + + Sync + + Send, + Power: Add + Div, + u64: From + From, +{ + /// Increase the amount of coins staked by a certain staker. + /// + /// When adding stake: + /// - Amounts are added together. + /// - Epochs are weight-averaged, using the amounts as the weight. + /// + /// This type of averaging makes the entry equivalent to an unbounded record of all stake + /// additions and removals, without the overhead in memory and computation. + pub fn add_stake( + &mut self, + coins: Coins, + epoch: Epoch, + minimum_stakeable: Option, + ) -> StakesResult { + // Make sure that the amount to be staked is equal or greater than the minimum + let minimum = minimum_stakeable.unwrap_or(Coins::from(MINIMUM_STAKEABLE_AMOUNT_WITS)); + if coins < minimum { + Err(StakesError::AmountIsBelowMinimum { + amount: coins, + minimum, + })?; + } + + let coins_before = self.coins; + let epoch_before = self.epochs.get(Capability::Mining); + + let product_before = coins_before * epoch_before; + let product_added = coins * epoch; + + let coins_after = coins_before + coins; + let epoch_after = Epoch::from( + (u64::from(product_before + product_added) / u64::from(coins_after)) as u32, + ); + + self.coins = coins_after; + self.epochs.update_all(epoch_after); + + Ok(coins_after) + } + + /// Construct a Stake entry from a number of coins and a capability map. This is only useful for + /// tests. + #[cfg(test)] + pub fn from_parts(coins: Coins, epochs: CapabilityMap) -> Self { + Self { + coins, + epochs, + phantom_address: Default::default(), + phantom_power: Default::default(), + } + } + + /// Derives the power of an identity in the network on a certain epoch from an entry. Most + /// normally, the epoch is the current epoch. + pub fn power(&self, capability: Capability, current_epoch: Epoch) -> Power { + self.coins * (current_epoch.saturating_sub(self.epochs.get(capability))) + } + + /// Remove a certain amount of staked coins. + pub fn remove_stake( + &mut self, + coins: Coins, + minimum_stakeable: Option, + ) -> StakesResult { + let coins_after = self.coins.sub(coins); + + if coins_after > Coins::zero() { + let minimum = minimum_stakeable.unwrap_or(Coins::from(MINIMUM_STAKEABLE_AMOUNT_WITS)); + + if coins_after < minimum { + Err(StakesError::AmountIsBelowMinimum { + amount: coins_after, + minimum, + })?; + } + } + + self.coins = coins_after; + + Ok(self.coins) + } + + /// Set the epoch for a certain capability. Most normally, the epoch is the current epoch. + pub fn reset_age(&mut self, capability: Capability, current_epoch: Epoch) { + self.epochs.update(capability, current_epoch); + } +} diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs new file mode 100644 index 000000000..50d1a294d --- /dev/null +++ b/data_structures/src/staking/stakes.rs @@ -0,0 +1,763 @@ +use std::{ + collections::{btree_map::Entry, BTreeMap}, + fmt::{Debug, Display}, + ops::{Add, Div, Mul, Sub}, +}; + +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{chain::PublicKeyHash, get_environment, transaction::StakeTransaction, wit::Wit}; + +use super::prelude::*; + +/// Message for querying stakes +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum QueryStakesKey { + /// Query stakes by validator address + Validator(Address), + /// Query stakes by withdrawer address + Withdrawer(Address), + /// Query stakes by validator and withdrawer addresses + Key(StakeKey
), +} + +impl
Default for QueryStakesKey
+where + Address: Default + Ord, +{ + fn default() -> Self { + QueryStakesKey::Validator(Address::default()) + } +} + +impl TryFrom<(Option, Option)> for QueryStakesKey
+where + Address: Default + Ord, + T: Into
, +{ + type Error = String; + fn try_from(val: (Option, Option)) -> Result { + match val { + (Some(validator), Some(withdrawer)) => Ok(QueryStakesKey::Key(StakeKey { + validator: validator.into(), + withdrawer: withdrawer.into(), + })), + (Some(validator), _) => Ok(QueryStakesKey::Validator(validator.into())), + (_, Some(withdrawer)) => Ok(QueryStakesKey::Withdrawer(withdrawer.into())), + _ => Err(String::from( + "Either a validator address, a withdrawer address or both must be provided.", + )), + } + } +} + +/// The main data structure that provides the "stakes tracker" functionality. +/// +/// This structure holds indexes of stake entries. Because the entries themselves are reference +/// counted and write-locked, we can have as many indexes here as we need at a negligible cost. +#[derive(Clone, Debug, Deserialize, Default, PartialEq, Serialize)] +pub struct Stakes +where + Address: Default + Ord, + Coins: Ord, + Epoch: Default, +{ + /// A listing of all the stakers, indexed by their address. + by_key: BTreeMap, SyncStake>, + /// A listing of all the stakers, indexed by validator. + by_validator: BTreeMap>, + /// A listing of all the stakers, indexed by withdrawer. + by_withdrawer: BTreeMap>, + /// A listing of all the stakers, indexed by their coins and address. + /// + /// Because this uses a compound key to prevent duplicates, if we want to know which addresses + /// have staked a particular amount, we just need to run a range lookup on the tree. + by_coins: BTreeMap, SyncStake>, + /// The amount of coins that can be staked or can be left staked after unstaking. + /// TODO: reconsider whether this should be here, taking into account that it hinders the possibility of adjusting + /// the minimum through TAPI or whatever. Maybe what we can do is set a skip directive for the Serialize macro so + /// it never gets persisted and rather always read from constants, or hide the field and the related method + /// behind a #[test] thing. + #[serde(skip)] + minimum_stakeable: Option, +} + +impl Stakes +where + Address: Default + Send + Sync + Display, + Coins: Copy + + Default + + Ord + + From + + Into + + num_traits::Zero + + Add + + Sub + + Mul + + Mul + + Debug + + Send + + Sync + + Display, + Address: Clone + Ord + 'static + Debug, + Epoch: Copy + + Default + + num_traits::Saturating + + Sub + + From + + Debug + + Display + + Send + + Sync, + Power: Copy + Default + Ord + Add + Div, + u64: From + From, +{ + /// Register a certain amount of additional stake for a certain address and epoch. + pub fn add_stake( + &mut self, + key: ISK, + coins: Coins, + epoch: Epoch, + ) -> StakesResult, Address, Coins, Epoch> + where + ISK: Into>, + { + let key = key.into(); + + // Find or create a matching stake entry + let stake = self.by_key.entry(key.clone()).or_default(); + + // Actually increase the number of coins + stake + .value + .write()? + .add_stake(coins, epoch, self.minimum_stakeable)?; + + // Update the position of the staker in the `by_coins` index + // If this staker was not indexed by coins, this will index it now + let coins_and_addresses = CoinsAndAddresses { + coins, + addresses: key, + }; + self.by_coins.remove(&coins_and_addresses); + self.by_coins + .insert(coins_and_addresses.clone(), stake.clone()); + + let validator_key = coins_and_addresses.clone().addresses.validator; + self.by_validator.remove(&validator_key); + self.by_validator.insert(validator_key, stake.clone()); + + let withdrawer_key = coins_and_addresses.addresses.withdrawer; + self.by_withdrawer.remove(&withdrawer_key); + self.by_withdrawer.insert(withdrawer_key, stake.clone()); + + Ok(stake.value.read()?.clone()) + } + + /// Quickly count how many stake entries are recorded into this data structure. + pub fn stakes_count(&self) -> usize { + self.by_key.len() + } + + /// Obtain a list of stakers, conveniently ordered by one of several strategies. + /// + /// ## Strategies + /// + /// - `All`: retrieve all addresses, ordered by decreasing power. + /// - `StepBy`: retrieve every Nth address, ordered by decreasing power. + /// - `Take`: retrieve the most powerful N addresses, ordered by decreasing power. + /// - `Evenly`: retrieve a total of N addresses, evenly distributed from the index, ordered by + /// decreasing power. + pub fn census( + &self, + capability: Capability, + epoch: Epoch, + strategy: CensusStrategy, + ) -> Box> + '_> { + let iterator = self.rank(capability, epoch).map(|(address, _)| address); + + match strategy { + CensusStrategy::All => Box::new(iterator), + CensusStrategy::StepBy(step) => Box::new(iterator.step_by(step)), + CensusStrategy::Take(head) => Box::new(iterator.take(head)), + CensusStrategy::Evenly(count) => { + let collected = iterator.collect::>(); + let step = collected.len() / count; + + Box::new(collected.into_iter().step_by(step).take(count)) + } + } + } + + /// Tells what is the power of an identity in the network on a certain epoch. + pub fn query_power( + &self, + key: ISK, + capability: Capability, + epoch: Epoch, + ) -> StakesResult + where + ISK: Into>, + { + let key = key.into(); + + Ok(self + .by_key + .get(&key) + .ok_or(StakesError::EntryNotFound { key })? + .value + .read()? + .power(capability, epoch)) + } + + /// For a given capability, obtain the full list of stakers ordered by their power in that + /// capability. + /// TODO: we may memoize the rank by keeping the last one in a non-serializable field in `Self` that keeps a boxed + /// iterator, so that this method doesn't have to sort multiple times if we are calling the `rank` method several + /// times in the same epoch. + pub fn rank( + &self, + capability: Capability, + current_epoch: Epoch, + ) -> impl Iterator, Power)> + '_ { + self.by_coins + .iter() + .flat_map(move |(CoinsAndAddresses { addresses, .. }, stake)| { + stake + .value + .read() + .map(move |stake| (addresses.clone(), stake.power(capability, current_epoch))) + }) + .sorted_by_key(|(_, power)| *power) + .rev() + } + + /// Remove a certain amount of staked coins from a given identity at a given epoch. + pub fn remove_stake( + &mut self, + key: ISK, + coins: Coins, + ) -> StakesResult + where + ISK: Into>, + { + let key = key.into(); + + if let Entry::Occupied(mut by_address_entry) = self.by_key.entry(key.clone()) { + let (initial_coins, final_coins) = { + let mut stake = by_address_entry.get_mut().value.write()?; + + // Check the former amount of stake + let initial_coins = stake.coins; + + // Reduce the amount of stake + let final_coins = stake.remove_stake(coins, self.minimum_stakeable)?; + + (initial_coins, final_coins) + }; + + // No need to keep the entry if the stake has gone to zero + if final_coins.is_zero() { + by_address_entry.remove(); + self.by_coins.remove(&CoinsAndAddresses { + coins: initial_coins, + addresses: key, + }); + } + + Ok(final_coins) + } else { + Err(StakesError::EntryNotFound { key }) + } + } + + /// Set the epoch for a certain address and capability. Most normally, the epoch is the current + /// epoch. + pub fn reset_age( + &mut self, + key: ISK, + capability: Capability, + current_epoch: Epoch, + ) -> StakesResult<(), Address, Coins, Epoch> + where + ISK: Into>, + { + let key = key.into(); + + let mut stake = self + .by_key + .get_mut(&key) + .ok_or(StakesError::EntryNotFound { key })? + .value + .write()?; + stake.epochs.update(capability, current_epoch); + + Ok(()) + } + + /// Creates an instance of `Stakes` with a custom minimum stakeable amount. + pub fn with_minimum(minimum: Coins) -> Self { + Stakes { + minimum_stakeable: Some(minimum), + ..Default::default() + } + } + + /// Query stakes based on different keys. + pub fn query_stakes( + &mut self, + query: TIQSK, + ) -> StakesResult + where + TIQSK: TryInto>, + { + match query.try_into() { + Ok(QueryStakesKey::Key(key)) => self.query_by_key(key), + Ok(QueryStakesKey::Validator(validator)) => self.query_by_validator(validator), + Ok(QueryStakesKey::Withdrawer(withdrawer)) => self.query_by_withdrawer(withdrawer), + Err(_) => Err(StakesError::EmptyQuery), + } + } + + /// Query stakes by stake key. + #[inline(always)] + fn query_by_key(&self, key: StakeKey
) -> StakesResult { + Ok(self + .by_key + .get(&key) + .ok_or(StakesError::EntryNotFound { key })? + .value + .read()? + .coins) + } + + /// Query stakes by validator address. + #[inline(always)] + fn query_by_validator(&self, validator: Address) -> StakesResult { + Ok(self + .by_validator + .get(&validator) + .ok_or(StakesError::ValidatorNotFound { validator })? + .value + .read()? + .coins) + } + + /// Query stakes by withdrawer address. + #[inline(always)] + fn query_by_withdrawer( + &self, + withdrawer: Address, + ) -> StakesResult { + Ok(self + .by_withdrawer + .get(&withdrawer) + .ok_or(StakesError::WithdrawerNotFound { withdrawer })? + .value + .read()? + .coins) + } +} + +/// Adds stake, based on the data from a stake transaction. +/// +/// This function was made static instead of adding it to `impl Stakes` because it is not generic over `Address` and +/// `Coins`. +pub fn process_stake_transaction( + stakes: &mut Stakes, + transaction: &StakeTransaction, + epoch: Epoch, +) -> StakesResult<(), PublicKeyHash, Wit, Epoch> +where + Epoch: Copy + + Default + + Sub + + num_traits::Saturating + + From + + Debug + + Display + + Send + + Sync, + Power: Add + Copy + Default + Div + Ord + Debug, + Wit: Mul, + u64: From + From, +{ + // This line would check that the authorization message is valid for the provided validator and withdrawer + // address. But it is commented out here because stake transactions should be validated upfront (when + // considering block candidates). The line is reproduced here for later reference when implementing those + // validations. Once those are in place, we're ok to remove this comment. + //transaction.body.authorization_is_valid().map_err(|_| StakesError::InvalidAuthentication)?; + + let key = transaction.body.output.key.clone(); + let coins = Wit::from_nanowits(transaction.body.output.value); + + let environment = get_environment(); + log::debug!( + "{} added {} Wit more stake on validator {}", + key.withdrawer.bech32(environment), + coins.wits_and_nanowits().0, + key.validator.bech32(environment) + ); + + stakes.add_stake(key, coins, epoch)?; + + log::debug!("Current state of the stakes tracker: {:#?}", stakes); + + Ok(()) +} + +/// Adds stakes, based on the data from multiple stake transactions. +/// +/// This function was made static instead of adding it to `impl Stakes` because it is not generic over `Address` and +/// `Coins`. +pub fn process_stake_transactions<'a, Epoch, Power>( + stakes: &mut Stakes, + transactions: impl Iterator, + epoch: Epoch, +) -> Result<(), StakesError> +where + Epoch: Copy + + Default + + Sub + + num_traits::Saturating + + From + + Debug + + Send + + Sync + + Display, + Power: Add + Copy + Default + Div + Ord + Debug, + Wit: Mul, + u64: From + From, +{ + for transaction in transactions { + process_stake_transaction(stakes, transaction, epoch)?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_stakes_initialization() { + let stakes = Stakes::::default(); + let ranking = stakes.rank(Capability::Mining, 0).collect::>(); + assert_eq!(ranking, Vec::default()); + } + + #[test] + fn test_add_stake() { + let mut stakes = Stakes::::with_minimum(5); + let alice = "Alice"; + let bob = "Bob"; + let charlie = "Charlie"; + let david = "David"; + + let alice_charlie = (alice, charlie); + let bob_david = (bob, david); + + // Let's check default power + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 0), + Err(StakesError::EntryNotFound { + key: StakeKey { + validator: alice.into(), + withdrawer: charlie.into() + }, + }) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 1_000), + Err(StakesError::EntryNotFound { + key: StakeKey { + validator: alice.into(), + withdrawer: charlie.into() + }, + }) + ); + + // Let's make Alice stake 100 Wit at epoch 100 + assert_eq!( + stakes.add_stake(alice_charlie, 100, 100).unwrap(), + Stake::from_parts( + 100, + CapabilityMap { + mining: 100, + witnessing: 100 + } + ) + ); + + // Let's see how Alice's stake accrues power over time + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 99), + Ok(0) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 100), + Ok(0) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 101), + Ok(100) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 200), + Ok(10_000) + ); + + // Let's make Alice stake 50 Wits at epoch 150 this time + assert_eq!( + stakes.add_stake(alice_charlie, 50, 300).unwrap(), + Stake::from_parts( + 150, + CapabilityMap { + mining: 166, + witnessing: 166 + } + ) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 299), + Ok(19_950) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 300), + Ok(20_100) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 301), + Ok(20_250) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 400), + Ok(35_100) + ); + + // Now let's make Bob stake 500 Wits at epoch 1000 this time + assert_eq!( + stakes.add_stake(bob_david, 500, 1_000).unwrap(), + Stake::from_parts( + 500, + CapabilityMap { + mining: 1_000, + witnessing: 1_000 + } + ) + ); + + // Before Bob stakes, Alice has all the power + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 999), + Ok(124950) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Mining, 999), + Ok(0) + ); + + // New stakes don't change power in the same epoch + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 1_000), + Ok(125100) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Mining, 1_000), + Ok(0) + ); + + // Shortly after, Bob's stake starts to gain power + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 1_001), + Ok(125250) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Mining, 1_001), + Ok(500) + ); + + // After enough time, Bob overpowers Alice + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 2_000), + Ok(275_100) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Mining, 2_000), + Ok(500_000) + ); + } + + #[test] + fn test_coin_age_resets() { + // First, lets create a setup with a few stakers + let mut stakes = Stakes::::with_minimum(5); + let alice = "Alice"; + let bob = "Bob"; + let charlie = "Charlie"; + let david = "David"; + let erin = "Erin"; + + let alice_charlie = (alice, charlie); + let bob_david = (bob, david); + let charlie_erin = (charlie, erin); + + stakes.add_stake(alice_charlie, 10, 0).unwrap(); + stakes.add_stake(bob_david, 20, 20).unwrap(); + stakes.add_stake(charlie_erin, 30, 30).unwrap(); + + // Let's really start our test at epoch 100 + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 100), + Ok(1_000) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Mining, 100), + Ok(1_600) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Mining, 100), + Ok(2_100) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Witnessing, 100), + Ok(1_000) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Witnessing, 100), + Ok(1_600) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Witnessing, 100), + Ok(2_100) + ); + assert_eq!( + stakes.rank(Capability::Mining, 100).collect::>(), + [ + (charlie_erin.into(), 2100), + (bob_david.into(), 1600), + (alice_charlie.into(), 1000) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 100).collect::>(), + [ + (charlie_erin.into(), 2100), + (bob_david.into(), 1600), + (alice_charlie.into(), 1000) + ] + ); + + // Now let's slash Charlie's mining coin age right after + stakes + .reset_age(charlie_erin, Capability::Mining, 101) + .unwrap(); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 101), + Ok(1_010) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Mining, 101), + Ok(1_620) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Mining, 101), + Ok(0) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Witnessing, 101), + Ok(1_010) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Witnessing, 101), + Ok(1_620) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Witnessing, 101), + Ok(2_130) + ); + assert_eq!( + stakes.rank(Capability::Mining, 101).collect::>(), + [ + (bob_david.into(), 1_620), + (alice_charlie.into(), 1_010), + (charlie_erin.into(), 0) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 101).collect::>(), + [ + (charlie_erin.into(), 2_130), + (bob_david.into(), 1_620), + (alice_charlie.into(), 1_010) + ] + ); + + // Don't panic, Charlie! After enough time, you can take over again ;) + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 300), + Ok(3_000) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Mining, 300), + Ok(5_600) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Mining, 300), + Ok(5_970) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Witnessing, 300), + Ok(3_000) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Witnessing, 300), + Ok(5_600) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Witnessing, 300), + Ok(8_100) + ); + assert_eq!( + stakes.rank(Capability::Mining, 300).collect::>(), + [ + (charlie_erin.into(), 5_970), + (bob_david.into(), 5_600), + (alice_charlie.into(), 3_000) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 300).collect::>(), + [ + (charlie_erin.into(), 8_100), + (bob_david.into(), 5_600), + (alice_charlie.into(), 3_000) + ] + ); + } + + #[test] + fn test_query_stakes() { + // First, lets create a setup with a few stakers + let mut stakes = Stakes::::with_minimum(5); + let alice = "Alice"; + let bob = "Bob"; + let charlie = "Charlie"; + let david = "David"; + let erin = "Erin"; + + let alice_charlie = (alice, charlie); + let bob_david = (bob, david); + let charlie_erin = (charlie, erin); + + stakes.add_stake(alice_charlie, 10, 0).unwrap(); + stakes.add_stake(bob_david, 20, 20).unwrap(); + stakes.add_stake(charlie_erin, 30, 30).unwrap(); + + let result = stakes.query_stakes(QueryStakesKey::Key(alice_charlie.into())); + + assert_eq!(result, Ok(10)) + } +} diff --git a/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index c627f84f7..f75407151 100644 --- a/data_structures/src/superblock.rs +++ b/data_structures/src/superblock.rs @@ -1,11 +1,3 @@ -use crate::{ - chain::{ - tapi::{after_second_hard_fork, in_emergency_period}, - AltKeys, BlockHeader, Bn256PublicKey, CheckpointBeacon, Epoch, Hash, Hashable, - PublicKeyHash, SuperBlock, SuperBlockVote, - }, - get_environment, -}; use std::{ collections::{HashMap, HashSet}, convert::{TryFrom, TryInto}, @@ -18,6 +10,16 @@ use witnet_crypto::{ merkle::merkle_tree_root as crypto_merkle_tree_root, }; +use crate::{ + chain::{ + tapi::{after_second_hard_fork, in_emergency_period}, + AltKeys, BlockHeader, Bn256PublicKey, CheckpointBeacon, Epoch, Hash, Hashable, + PublicKeyHash, SuperBlock, SuperBlockVote, + }, + get_environment, + proto::versioning::{ProtocolVersion, VersionedHashable}, +}; + /// Possible result of SuperBlockState::add_vote #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum AddSuperBlockVote { @@ -268,11 +270,15 @@ impl SuperBlockState { AddSuperBlockVote::DoubleVote } else { - let is_same_hash = - sbv.superblock_hash == self.current_superblock_beacon.hash_prev_block; + let theirs = sbv.superblock_hash; + let ours = self.current_superblock_beacon.hash_prev_block; + let votes_for_our_tip = theirs == ours; + + log::debug!("Superblock vote comparison:\n(theirs): {theirs}\n(ours): {ours}"); + self.votes_mempool.insert_vote(sbv); - if is_same_hash { + if votes_for_our_tip { AddSuperBlockVote::ValidWithSameHash } else { AddSuperBlockVote::ValidButDifferentHash @@ -698,7 +704,7 @@ pub fn mining_build_superblock( ) } Some(last_block_header) => { - let last_block_hash = last_block_header.hash(); + let last_block_hash = last_block_header.versioned_hash(ProtocolVersion::guess()); let merkle_drs: Vec = block_headers .iter() .map(|b| b.merkle_roots.dr_hash_merkle_root) @@ -754,13 +760,17 @@ pub fn hash_merkle_tree_root(hashes: &[Hash]) -> Hash { #[cfg(test)] mod tests { - use super::*; + use itertools::Itertools; + + use witnet_crypto::hash::{calculate_sha256, EMPTY_SHA256}; + use crate::{ chain::{BlockMerkleRoots, Bn256SecretKey, CheckpointBeacon, PublicKey, Signature}, + proto::versioning::{ProtocolVersion, VersionedHashable}, vrf::BlockEligibilityClaim, }; - use itertools::Itertools; - use witnet_crypto::hash::{calculate_sha256, EMPTY_SHA256}; + + use super::*; #[test] fn test_superblock_creation_no_blocks() { @@ -806,6 +816,8 @@ mod tests { commit_hash_merkle_root: default_hash, reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_1, + stake_hash_merkle_root: default_hash, + unstake_hash_merkle_root: default_hash, }, proof: default_proof, bn256_public_key: None, @@ -816,7 +828,7 @@ mod tests { default_hash, dr_merkle_root_1, 0, - block.hash(), + block.versioned_hash(ProtocolVersion::V1_7), default_hash, tally_merkle_root_1, ); @@ -855,6 +867,8 @@ mod tests { commit_hash_merkle_root: default_hash, reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_1, + stake_hash_merkle_root: default_hash, + unstake_hash_merkle_root: default_hash, }, proof: default_proof.clone(), bn256_public_key: None, @@ -870,6 +884,8 @@ mod tests { commit_hash_merkle_root: default_hash, reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_2, + stake_hash_merkle_root: default_hash, + unstake_hash_merkle_root: default_hash, }, proof: default_proof, bn256_public_key: None, @@ -880,7 +896,7 @@ mod tests { default_hash, expected_superblock_dr_root, 0, - block_2.hash(), + block_2.versioned_hash(ProtocolVersion::V1_7), default_hash, expected_superblock_tally_root, ); @@ -1108,7 +1124,7 @@ mod tests { genesis_hash, &alt_keys, None, - 1 + 1, ), expected_second_superblock ); @@ -2595,16 +2611,17 @@ mod tests { #[test] fn test_two_thirds_consensus() { - assert!(!two_thirds_consensus(2, 3)); + assert!(!two_thirds_consensus(1, 3)); + assert!(two_thirds_consensus(2, 3)); assert!(two_thirds_consensus(3, 3)); - assert!(!two_thirds_consensus(2, 4)); + assert!(!two_thirds_consensus(1, 4)); assert!(two_thirds_consensus(3, 4)); - assert!(!two_thirds_consensus(21, 32)); - assert!(two_thirds_consensus(22, 32)); - assert!(!two_thirds_consensus(22, 33)); - assert!(two_thirds_consensus(23, 33)); - assert!(!two_thirds_consensus(22, 34)); - assert!(two_thirds_consensus(23, 34)); + assert!(!two_thirds_consensus(20, 32)); + assert!(two_thirds_consensus(21, 32)); + assert!(!two_thirds_consensus(21, 33)); + assert!(two_thirds_consensus(22, 33)); + assert!(!two_thirds_consensus(21, 34)); + assert!(two_thirds_consensus(22, 34)); } #[test] @@ -2686,7 +2703,7 @@ mod tests { // 4 valid votes -> SameAsLocal let mut v3 = SuperBlockVote::new_unsigned(sb2.hash(), 2); - v3.secp256k1_signature.public_key = p3; + v3.secp256k1_signature.public_key = p3.clone(); assert_eq!(sbs.add_vote(&v3, 2), AddSuperBlockVote::ValidWithSameHash); let mut v4 = SuperBlockVote::new_unsigned(sb2.hash(), 2); v4.secp256k1_signature.public_key = p4; @@ -2694,13 +2711,16 @@ mod tests { assert_eq!(sbs.has_consensus(), SuperBlockConsensus::SameAsLocal); - // 2 valid votes, 2 double votes and 1 missing vote -> NoConsensus + // 1 valid votes, 3 double votes and 1 missing vote -> NoConsensus let mut v1_b = SuperBlockVote::new_unsigned(Hash::SHA256([2; 32]), 2); v1_b.secp256k1_signature.public_key = p1; assert_eq!(sbs.add_vote(&v1_b, 2), AddSuperBlockVote::DoubleVote); let mut v2_b = SuperBlockVote::new_unsigned(Hash::SHA256([2; 32]), 2); v2_b.secp256k1_signature.public_key = p2; assert_eq!(sbs.add_vote(&v2_b, 2), AddSuperBlockVote::DoubleVote); + let mut v3_b = SuperBlockVote::new_unsigned(Hash::SHA256([2; 32]), 2); + v3_b.secp256k1_signature.public_key = p3; + assert_eq!(sbs.add_vote(&v3_b, 2), AddSuperBlockVote::DoubleVote); assert_eq!(sbs.has_consensus(), SuperBlockConsensus::NoConsensus); } diff --git a/data_structures/src/transaction.rs b/data_structures/src/transaction.rs index 4c98820e6..4c838eaa7 100644 --- a/data_structures/src/transaction.rs +++ b/data_structures/src/transaction.rs @@ -3,13 +3,18 @@ use std::sync::{Arc, RwLock}; use protobuf::Message; use serde::{Deserialize, Serialize}; -use witnet_crypto::{hash::calculate_sha256, merkle::FullMerkleTree}; + +use witnet_crypto::{ + hash::calculate_sha256, merkle::FullMerkleTree, secp256k1::Message as Secp256k1Message, + signature::PublicKey, +}; use crate::{ chain::{ Block, Bn256PublicKey, DataRequestOutput, Epoch, Hash, Hashable, Input, KeyedSignature, - PublicKeyHash, ValueTransferOutput, + PublicKeyHash, StakeOutput, ValueTransferOutput, }, + error::TransactionError, proto::{schema::witnet, ProtobufConvert}, vrf::DataRequestEligibilityClaim, }; @@ -18,6 +23,8 @@ use crate::{ // https://github.com/witnet/WIPs/blob/master/wip-0007.md pub const INPUT_SIZE: u32 = 133; pub const OUTPUT_SIZE: u32 = 36; +pub const STAKE_OUTPUT_WEIGHT: u32 = 105; +pub const UNSTAKE_TRANSACTION_WEIGHT: u32 = 153; pub const COMMIT_WEIGHT: u32 = 400; pub const REVEAL_WEIGHT: u32 = 200; pub const TALLY_WEIGHT: u32 = 100; @@ -130,6 +137,8 @@ pub enum Transaction { Reveal(RevealTransaction), Tally(TallyTransaction), Mint(MintTransaction), + Stake(StakeTransaction), + Unstake(UnstakeTransaction), } impl From for Transaction { @@ -168,6 +177,18 @@ impl From for Transaction { } } +impl From for Transaction { + fn from(transaction: StakeTransaction) -> Self { + Self::Stake(transaction) + } +} + +impl From for Transaction { + fn from(transaction: UnstakeTransaction) -> Self { + Self::Unstake(transaction) + } +} + impl AsRef for Transaction { fn as_ref(&self) -> &Self { self @@ -249,6 +270,14 @@ impl VTTransactionBody { } } + pub fn value(&self) -> u64 { + self.outputs + .iter() + .map(ValueTransferOutput::value) + .reduce(|acc, value| acc + value) + .unwrap_or_default() + } + /// Value Transfer transaction weight. It is calculated as: /// /// ```text @@ -375,8 +404,8 @@ impl DRTransactionBody { /// Creates a new data request transaction body. pub fn new( inputs: Vec, - outputs: Vec, dr_output: DataRequestOutput, + outputs: Vec, ) -> Self { DRTransactionBody { inputs, @@ -386,6 +415,18 @@ impl DRTransactionBody { } } + pub fn value(&self) -> Result { + let dr_value = self.dr_output.checked_total_value()?; + let change_value = self + .outputs + .iter() + .map(ValueTransferOutput::value) + .reduce(|acc, value| acc + value) + .unwrap_or_default(); + + Ok(dr_value + change_value) + } + /// Data Request Transaction weight. It is calculated as: /// /// ```text @@ -683,6 +724,152 @@ impl MintTransaction { } } +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] +#[protobuf_convert(pb = "witnet::StakeTransaction")] +pub struct StakeTransaction { + pub body: StakeTransactionBody, + pub signatures: Vec, +} + +impl StakeTransaction { + // Creates a new stake transaction. + pub fn new(body: StakeTransactionBody, signatures: Vec) -> Self { + StakeTransaction { body, signatures } + } + + /// Returns the weight of a stake transaction. + /// This is the weight that will be used to calculate how many transactions can fit inside one + /// block + pub fn weight(&self) -> u32 { + self.body.weight() + } +} + +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] +#[protobuf_convert(pb = "witnet::StakeTransactionBody")] +pub struct StakeTransactionBody { + pub inputs: Vec, + pub output: StakeOutput, + pub change: Option, + + #[protobuf_convert(skip)] + #[serde(skip)] + hash: MemoHash, +} + +impl StakeTransactionBody { + pub fn authorization_is_valid(&self) -> Result<(), failure::Error> { + let msg = Secp256k1Message::from_digest(self.output.key.withdrawer.as_secp256k1_msg()); + let public_key = PublicKey::from_slice(&self.output.authorization.public_key.bytes)?; + + self.output + .authorization + .signature + .verify(&msg, &public_key) + } + + /// Construct a `StakeTransactionBody` from a list of inputs and one `StakeOutput`. + pub fn new( + inputs: Vec, + output: StakeOutput, + change: Option, + ) -> Self { + StakeTransactionBody { + inputs, + output, + change, + ..Default::default() + } + } + + pub fn value(&self) -> u64 { + let stake_value = self.output.value; + let change_value = &self + .change + .as_ref() + .map(ValueTransferOutput::value) + .unwrap_or_default(); + + stake_value + change_value + } + + /// Stake transaction weight. It is calculated as: + /// + /// ```text + /// ST_weight = N*INPUT_SIZE+M*OUTPUT_SIZE+STAKE_OUTPUT + /// + /// ``` + pub fn weight(&self) -> u32 { + let inputs_len = u32::try_from(self.inputs.len()).unwrap_or(u32::MAX); + let inputs_weight = inputs_len.saturating_mul(INPUT_SIZE); + let change_weight = if self.change.is_some() { + OUTPUT_SIZE + } else { + 0 + }; + + inputs_weight + .saturating_add(change_weight) + .saturating_add(STAKE_OUTPUT_WEIGHT) + } +} + +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] +#[protobuf_convert(pb = "witnet::UnstakeTransaction")] +pub struct UnstakeTransaction { + pub body: UnstakeTransactionBody, + pub signature: KeyedSignature, +} +impl UnstakeTransaction { + // Creates a new unstake transaction. + pub fn new(body: UnstakeTransactionBody, signature: KeyedSignature) -> Self { + UnstakeTransaction { body, signature } + } + + /// Returns the weight of a unstake transaction. + /// This is the weight that will be used to calculate + /// how many transactions can fit inside one block + pub fn weight(&self) -> u32 { + self.body.weight() + } +} + +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] +#[protobuf_convert(pb = "witnet::UnstakeTransactionBody")] +pub struct UnstakeTransactionBody { + pub validator: PublicKeyHash, + pub withdrawal: ValueTransferOutput, + + #[protobuf_convert(skip)] + #[serde(skip)] + hash: MemoHash, +} + +impl UnstakeTransactionBody { + /// Creates a new stake transaction body. + pub fn new(validator: PublicKeyHash, withdrawal: ValueTransferOutput) -> Self { + UnstakeTransactionBody { + validator, + withdrawal, + ..Default::default() + } + } + + pub fn value(&self) -> u64 { + self.withdrawal.value + } + + /// Stake transaction weight. It is calculated as: + /// + /// ```text + /// ST_weight = 153 + /// + /// ``` + pub fn weight(&self) -> u32 { + UNSTAKE_TRANSACTION_WEIGHT + } +} + impl MemoizedHashable for VTTransactionBody { fn hashable_bytes(&self) -> Vec { self.to_pb_bytes().unwrap() @@ -722,6 +909,24 @@ impl MemoizedHashable for RevealTransactionBody { &self.hash } } +impl MemoizedHashable for StakeTransactionBody { + fn hashable_bytes(&self) -> Vec { + self.to_pb_bytes().unwrap() + } + + fn memoized_hash(&self) -> &MemoHash { + &self.hash + } +} +impl MemoizedHashable for UnstakeTransactionBody { + fn hashable_bytes(&self) -> Vec { + self.to_pb_bytes().unwrap() + } + + fn memoized_hash(&self) -> &MemoHash { + &self.hash + } +} impl MemoizedHashable for TallyTransaction { fn hashable_bytes(&self) -> Vec { let Hash::SHA256(data_bytes) = self.data_poi_hash(); @@ -765,6 +970,17 @@ impl Hashable for RevealTransaction { } } +impl Hashable for StakeTransaction { + fn hash(&self) -> Hash { + self.body.hash() + } +} +impl Hashable for UnstakeTransaction { + fn hash(&self) -> Hash { + self.body.hash() + } +} + impl Hashable for Transaction { fn hash(&self) -> Hash { match self { @@ -774,6 +990,8 @@ impl Hashable for Transaction { Transaction::Reveal(tx) => tx.hash(), Transaction::Tally(tx) => tx.hash(), Transaction::Mint(tx) => tx.hash(), + Transaction::Stake(tx) => tx.hash(), + Transaction::Unstake(tx) => tx.hash(), } } } @@ -973,8 +1191,8 @@ mod tests { }; let dr_body = DRTransactionBody::new( vec![Input::default()], - vec![ValueTransferOutput::default()], dro.clone(), + vec![ValueTransferOutput::default()], ); let dr_tx = DRTransaction::new(dr_body, vec![KeyedSignature::default()]); let dr_weight = INPUT_SIZE + OUTPUT_SIZE + dro.weight(); @@ -994,8 +1212,8 @@ mod tests { }; let dr_body = DRTransactionBody::new( vec![Input::default()], - vec![ValueTransferOutput::default()], dro.clone(), + vec![ValueTransferOutput::default()], ); let dr_tx = DRTransaction::new(dr_body, vec![KeyedSignature::default()]); let dr_weight = INPUT_SIZE + OUTPUT_SIZE + dro.weight(); diff --git a/data_structures/src/transaction_factory.rs b/data_structures/src/transaction_factory.rs index 31296b267..1731e1d23 100644 --- a/data_structures/src/transaction_factory.rs +++ b/data_structures/src/transaction_factory.rs @@ -6,14 +6,15 @@ use std::{ use serde::{Deserialize, Serialize}; +use crate::transaction::UnstakeTransactionBody; use crate::{ chain::{ - DataRequestOutput, Epoch, EpochConstants, Input, OutputPointer, PublicKeyHash, + DataRequestOutput, Epoch, EpochConstants, Input, OutputPointer, PublicKeyHash, StakeOutput, ValueTransferOutput, }, error::TransactionError, fee::{AbsoluteFee, Fee}, - transaction::{DRTransactionBody, VTTransactionBody, INPUT_SIZE}, + transaction::{DRTransactionBody, StakeTransactionBody, VTTransactionBody, INPUT_SIZE}, utxo_pool::{ NodeUtxos, NodeUtxosRef, OwnUnspentOutputsPool, UnspentOutputsPool, UtxoDiff, UtxoSelectionStrategy, @@ -21,7 +22,7 @@ use crate::{ wit::Wit, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct CollectedOutputs { pub pointers: Vec, pub resolved: Vec, @@ -75,6 +76,25 @@ impl NodeBalance { } } +#[derive(Clone, Debug)] +pub enum TransactionOutputs { + DataRequest((DataRequestOutput, Option)), + Stake((StakeOutput, Option)), + Unstake(ValueTransferOutput), + ValueTransfer(Vec), +} + +impl From for Vec { + fn from(value: TransactionOutputs) -> Self { + match value { + TransactionOutputs::DataRequest((_, change)) => change.into_iter().collect(), + TransactionOutputs::Stake((_, change)) => change.into_iter().collect(), + TransactionOutputs::Unstake(output) => vec![output], + TransactionOutputs::ValueTransfer(outputs) => outputs, + } + } +} + /// Abstraction that facilitates the creation of new transactions from a set of unspent outputs. /// Transaction factories are expected to operate on this trait so that their business logic /// can be applied on many heterogeneous data structures that may implement it. @@ -160,6 +180,135 @@ pub trait OutputsCollection { } } + /// Generic inputs/outputs builder: can be used to build any kind of transaction. + #[allow(clippy::too_many_arguments)] + fn generic_transaction_factory( + &mut self, + outputs: TransactionOutputs, + fee: Fee, + timestamp: u64, + block_number_limit: Option, + utxo_strategy: &UtxoSelectionStrategy, + max_weight: u32, + ) -> Result { + let output_value; + let mut current_weight; + let inputs = vec![Input::default()]; + + // For the first estimation: 1 input and 1 output more for the change address + match outputs.clone() { + TransactionOutputs::DataRequest((dr_output, change)) => { + let body = DRTransactionBody::new(inputs, dr_output, change.into_iter().collect()); + output_value = body.value()?; + current_weight = body.weight(); + } + TransactionOutputs::Stake((stake_output, change)) => { + let body = StakeTransactionBody::new(inputs, stake_output, change); + output_value = body.value(); + current_weight = body.weight(); + } + TransactionOutputs::Unstake(withdrawal) => { + let body = UnstakeTransactionBody::new(Default::default(), withdrawal); + output_value = body.value(); + current_weight = body.weight(); + } + TransactionOutputs::ValueTransfer(outputs) => { + let body = VTTransactionBody::new(inputs, outputs); + output_value = body.value(); + current_weight = body.weight(); + } + }; + + match fee { + Fee::Absolute(absolute_fee) => { + let amount = output_value + .checked_add(absolute_fee.as_nanowits()) + .ok_or(TransactionError::FeeOverflow)?; + + // Avoid collecting UTXOs for unstake transactions, which use no inputs + let inputs = if let &TransactionOutputs::Unstake(_) = &outputs { + Default::default() + } else { + self.take_enough_utxos(amount, timestamp, block_number_limit, utxo_strategy)? + }; + + Ok(TransactionInfo { + fee: absolute_fee, + inputs, + output_value, + outputs: outputs.into(), + }) + } + Fee::Relative(priority) => { + let absolute_fee = priority.into_absolute(current_weight); + if let TransactionOutputs::Unstake(withdrawal) = outputs { + return Ok(TransactionInfo { + fee: absolute_fee, + inputs: Default::default(), + output_value, + outputs: vec![withdrawal], + }); + } + + let max_iterations = 1 + ((max_weight - current_weight) / INPUT_SIZE); + for _i in 0..max_iterations { + let amount = output_value + .checked_add(absolute_fee.as_nanowits()) + .ok_or(TransactionError::FeeOverflow)?; + + let collected_outputs = self.take_enough_utxos( + amount, + timestamp, + block_number_limit, + utxo_strategy, + )?; + let inputs = collected_outputs + .pointers + .iter() + .cloned() + .map(Input::new) + .collect(); + + let new_weight = match outputs.clone() { + TransactionOutputs::DataRequest((dr_output, change)) => { + let body = DRTransactionBody::new( + inputs, + dr_output, + change.into_iter().collect(), + ); + + body.weight() + } + TransactionOutputs::Stake((stake_output, change)) => { + let body = StakeTransactionBody::new(inputs, stake_output, change); + + body.weight() + } + TransactionOutputs::ValueTransfer(outputs) => { + let body = VTTransactionBody::new(inputs, outputs); + + body.weight() + } + _ => unreachable!(), + }; + + if new_weight == current_weight { + return Ok(TransactionInfo { + fee: absolute_fee, + inputs: collected_outputs, + output_value, + outputs: outputs.into(), + }); + } else { + current_weight = new_weight; + } + } + + unreachable!("Unexpected exit in build_inputs_outputs method"); + } + } + } + /// Generic inputs/outputs builder: can be used to build /// value transfer transactions and data request transactions. #[allow(clippy::too_many_arguments)] @@ -254,7 +403,7 @@ pub fn calculate_weight( let outputs = vec![ValueTransferOutput::default(); outputs_count]; let weight = if let Some(dr_output) = dro { - let drt = DRTransactionBody::new(inputs, outputs, dr_output.clone()); + let drt = DRTransactionBody::new(inputs, dr_output.clone(), outputs); let dr_weight = drt.weight(); if dr_weight > max_weight { return Err(TransactionError::DataRequestWeightLimitExceeded { @@ -431,8 +580,8 @@ pub fn build_drt( Ok(DRTransactionBody::new( used_pointers.collect::>(), - outputs, dr_output, + outputs, )) } @@ -583,6 +732,67 @@ pub fn transaction_outputs_sum(outputs: &[ValueTransferOutput]) -> Result Result { + let mut utxos = NodeUtxos { + all_utxos, + own_utxos, + pkh: own_pkh, + }; + + let tx_info = utxos.generic_transaction_factory( + TransactionOutputs::Stake((output.clone(), None)), + fee, + timestamp, + None, + utxo_strategy, + max_weight, + )?; + + let used_pointers = tx_info.inputs.pointers.iter().cloned().map(Input::new); + + // Mark UTXOs as used so we don't double spend + // Save the timestamp after which the transaction will be considered timed out + // and the output will become available for spending it again + if !dry_run { + utxos.set_used_output_pointer(used_pointers.clone(), timestamp + tx_pending_timeout); + } + + // Only use a change output if there is value inserted by inputs that is not consumed by outputs + // or fees + let change_value = tx_info + .inputs + .total_value + .wrapping_sub(tx_info.output_value) + .wrapping_sub(tx_info.fee.as_nanowits()); + let change = if change_value > 0 { + Some(ValueTransferOutput { + pkh: own_pkh, + value: change_value, + time_lock: 0, + }) + } else { + None + }; + + let inputs = used_pointers.collect::>(); + let body = StakeTransactionBody::new(inputs, output, change); + + Ok(body) +} + #[cfg(test)] mod tests { use std::{ diff --git a/data_structures/src/types.rs b/data_structures/src/types.rs index fa6b0cd1b..10afcc31c 100644 --- a/data_structures/src/types.rs +++ b/data_structures/src/types.rs @@ -53,7 +53,7 @@ impl fmt::Display for Command { Command::Version(_) => f.write_str("VERSION"), Command::Block(block) => write!( f, - "BLOCK: #{}: {}", + "BLOCK #{}: {}", block.block_header.beacon.checkpoint, block.hash() ), @@ -64,7 +64,7 @@ impl fmt::Display for Command { highest_superblock_checkpoint: s, }) => write!( f, - "LAST_BEACON: Block: #{}: {} Superblock: #{}: {}", + "LAST_BEACON Block: #{}: {} Superblock: #{}: {}", h.checkpoint, h.hash_prev_block, s.checkpoint, s.hash_prev_block ), Command::Transaction(tx) => { @@ -75,6 +75,8 @@ impl fmt::Display for Command { Transaction::Reveal(_) => f.write_str("REVEAL_TRANSACTION")?, Transaction::Tally(_) => f.write_str("TALLY_TRANSACTION")?, Transaction::Mint(_) => f.write_str("MINT_TRANSACTION")?, + Transaction::Stake(_) => f.write_str("STAKE_TRANSACTION")?, + Transaction::Unstake(_) => f.write_str("UNSTAKE_TRANSACTION")?, } write!(f, ": {}", tx.hash()) } diff --git a/data_structures/src/vrf.rs b/data_structures/src/vrf.rs index 57ff40c32..047594534 100644 --- a/data_structures/src/vrf.rs +++ b/data_structures/src/vrf.rs @@ -122,7 +122,7 @@ impl VrfMessage { /// Block mining eligibility claim #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Default, Hash)] -#[protobuf_convert(pb = "witnet::Block_BlockEligibilityClaim")] +#[protobuf_convert(pb = "witnet::BlockEligibilityClaim")] pub struct BlockEligibilityClaim { /// A Verifiable Random Function proof of the eligibility for a given epoch and public key pub proof: VrfProof, diff --git a/data_structures/src/wit.rs b/data_structures/src/wit.rs index d9066550e..0023df3ea 100644 --- a/data_structures/src/wit.rs +++ b/data_structures/src/wit.rs @@ -1,7 +1,9 @@ -use std::fmt; +use std::{fmt, ops::*}; use serde::{Deserialize, Serialize}; +use crate::{chain::Epoch, staking::helpers::Power}; + /// 1 nanowit is the minimal unit of value /// 1 wit = 10^9 nanowits pub const NANOWITS_PER_WIT: u64 = 1_000_000_000; @@ -19,7 +21,7 @@ impl Wit { /// Create from wits #[inline] pub fn from_wits(wits: u64) -> Self { - Self(wits.checked_mul(NANOWITS_PER_WIT).expect("overflow")) + Self::from_nanowits(wits.checked_mul(NANOWITS_PER_WIT).expect("overflow")) } /// Create from nanowits @@ -59,21 +61,46 @@ impl fmt::Display for Wit { } } -impl std::ops::Add for Wit { +impl Add for Wit { type Output = Self; #[inline] fn add(self, rhs: Self) -> Self::Output { - Self(self.nanowits() + rhs.nanowits()) + Self::from_nanowits(self.nanowits() + rhs.nanowits()) } } -impl std::ops::Sub for Wit { +impl Div for Wit { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self::from_nanowits(self.nanowits() / rhs.nanowits()) + } +} + +impl Mul for Wit { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Self::from_nanowits(self.nanowits() * rhs.nanowits()) + } +} + +impl Mul for Wit { + type Output = Power; + + fn mul(self, rhs: Epoch) -> Self::Output { + Power::from(self.nanowits() * u64::from(rhs)) + } +} + +impl Sub for Wit { type Output = Self; #[inline] fn sub(self, rhs: Self) -> Self::Output { - Self(self.nanowits() - rhs.nanowits()) + Self::from_nanowits(self.nanowits() - rhs.nanowits()) } } @@ -89,6 +116,28 @@ impl num_traits::Zero for Wit { } } +impl num_traits::ops::saturating::Saturating for Wit { + fn saturating_add(self, v: Self) -> Self { + Self::from_nanowits(self.nanowits().saturating_add(v.nanowits())) + } + + fn saturating_sub(self, v: Self) -> Self { + Self::from_nanowits(self.nanowits().saturating_sub(v.nanowits())) + } +} + +impl From for Wit { + fn from(value: u64) -> Self { + Self::from_nanowits(value) + } +} + +impl From for u64 { + fn from(value: Wit) -> Self { + value.0 + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/data_structures/tests/inclusion_proofs.rs b/data_structures/tests/inclusion_proofs.rs index bf8c975b1..e3f490d8e 100644 --- a/data_structures/tests/inclusion_proofs.rs +++ b/data_structures/tests/inclusion_proofs.rs @@ -40,7 +40,7 @@ fn example_dr(id: usize) -> DRTransaction { witness_reward: id as u64, ..Default::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dr_output); + let dr_body = DRTransactionBody::new(vec![], dr_output, vec![]); DRTransaction::new(dr_body, vec![]) } diff --git a/data_structures/tests/serializers.rs b/data_structures/tests/serializers.rs index 2b4ef28c7..df5467e6d 100644 --- a/data_structures/tests/serializers.rs +++ b/data_structures/tests/serializers.rs @@ -1,9 +1,12 @@ use witnet_data_structures::{ - proto::ProtobufConvert, + proto::{ + versioning::{ProtocolVersion, Versioned}, + ProtobufConvert, + }, {chain::*, types::*}, }; -const EXAMPLE_BLOCK_VECTOR: &[u8] = &[ +const EXAMPLE_BLOCK_VECTOR_LEGACY: &[u8] = &[ 8, 1, 18, 165, 5, 42, 162, 5, 10, 172, 2, 18, 36, 18, 34, 10, 32, 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, 0, 26, 216, 1, 10, 34, 10, 32, 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, 0, @@ -32,6 +35,68 @@ const EXAMPLE_BLOCK_VECTOR: &[u8] = &[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; +const EXAMPLE_BLOCK_VECTOR_TRANSITION: &[u8] = &[ + 8, 1, 18, 237, 5, 42, 234, 5, 10, 244, 2, 18, 36, 18, 34, 10, 32, 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, 0, 26, 160, 2, 10, 34, 10, 32, + 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, 0, + 18, 34, 10, 32, 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, 0, 26, 34, 10, 32, 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, 0, 34, 34, 10, 32, 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, 0, 42, 34, 10, 32, 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, 0, 50, 34, 10, 32, 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, 0, 58, + 34, 10, 32, 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, 0, 66, 34, 10, 32, 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, 0, 34, 39, 10, 37, 18, 35, 10, 33, 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, 0, 0, 42, 0, 18, 41, 10, 2, 10, 0, + 18, 35, 10, 33, 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, 0, 0, 26, 197, 2, 10, 0, 26, 192, 2, 10, 146, 2, 10, 38, 10, 36, 10, 34, 10, 32, + 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, 0, + 18, 24, 10, 22, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 205, 1, + 10, 202, 1, 18, 97, 8, 1, 18, 93, 104, 116, 116, 112, 115, 58, 47, 47, 111, 112, 101, 110, 119, + 101, 97, 116, 104, 101, 114, 109, 97, 112, 46, 111, 114, 103, 47, 100, 97, 116, 97, 47, 50, 46, + 53, 47, 119, 101, 97, 116, 104, 101, 114, 63, 105, 100, 61, 50, 57, 53, 48, 49, 53, 57, 38, 97, + 112, 112, 105, 100, 61, 98, 54, 57, 48, 55, 100, 50, 56, 57, 101, 49, 48, 100, 55, 49, 52, 97, + 54, 101, 56, 56, 98, 51, 48, 55, 54, 49, 102, 97, 101, 50, 50, 18, 97, 8, 1, 18, 93, 104, 116, + 116, 112, 115, 58, 47, 47, 111, 112, 101, 110, 119, 101, 97, 116, 104, 101, 114, 109, 97, 112, + 46, 111, 114, 103, 47, 100, 97, 116, 97, 47, 50, 46, 53, 47, 119, 101, 97, 116, 104, 101, 114, + 63, 105, 100, 61, 50, 57, 53, 48, 49, 53, 57, 38, 97, 112, 112, 105, 100, 61, 98, 54, 57, 48, + 55, 100, 50, 56, 57, 101, 49, 48, 100, 55, 49, 52, 97, 54, 101, 56, 56, 98, 51, 48, 55, 54, 49, + 102, 97, 101, 50, 50, 26, 0, 34, 0, 18, 41, 10, 2, 10, 0, 18, 35, 10, 33, 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, 0, 0, +]; + +const EXAMPLE_BLOCK_VECTOR_FINAL: &[u8] = &[ + 8, 1, 18, 237, 5, 42, 234, 5, 10, 244, 2, 18, 36, 18, 34, 10, 32, 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, 0, 26, 160, 2, 10, 34, 10, 32, + 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, 0, + 18, 34, 10, 32, 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, 0, 26, 34, 10, 32, 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, 0, 34, 34, 10, 32, 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, 0, 42, 34, 10, 32, 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, 0, 50, 34, 10, 32, 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, 0, 58, + 34, 10, 32, 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, 0, 66, 34, 10, 32, 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, 0, 34, 39, 10, 37, 18, 35, 10, 33, 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, 0, 0, 42, 0, 18, 41, 10, 2, 10, 0, + 18, 35, 10, 33, 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, 0, 0, 26, 197, 2, 10, 0, 26, 192, 2, 10, 146, 2, 10, 38, 10, 36, 10, 34, 10, 32, + 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, 0, + 18, 24, 10, 22, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 205, 1, + 10, 202, 1, 18, 97, 8, 1, 18, 93, 104, 116, 116, 112, 115, 58, 47, 47, 111, 112, 101, 110, 119, + 101, 97, 116, 104, 101, 114, 109, 97, 112, 46, 111, 114, 103, 47, 100, 97, 116, 97, 47, 50, 46, + 53, 47, 119, 101, 97, 116, 104, 101, 114, 63, 105, 100, 61, 50, 57, 53, 48, 49, 53, 57, 38, 97, + 112, 112, 105, 100, 61, 98, 54, 57, 48, 55, 100, 50, 56, 57, 101, 49, 48, 100, 55, 49, 52, 97, + 54, 101, 56, 56, 98, 51, 48, 55, 54, 49, 102, 97, 101, 50, 50, 18, 97, 8, 1, 18, 93, 104, 116, + 116, 112, 115, 58, 47, 47, 111, 112, 101, 110, 119, 101, 97, 116, 104, 101, 114, 109, 97, 112, + 46, 111, 114, 103, 47, 100, 97, 116, 97, 47, 50, 46, 53, 47, 119, 101, 97, 116, 104, 101, 114, + 63, 105, 100, 61, 50, 57, 53, 48, 49, 53, 57, 38, 97, 112, 112, 105, 100, 61, 98, 54, 57, 48, + 55, 100, 50, 56, 57, 101, 49, 48, 100, 55, 49, 52, 97, 54, 101, 56, 56, 98, 51, 48, 55, 54, 49, + 102, 97, 101, 50, 50, 26, 0, 34, 0, 18, 41, 10, 2, 10, 0, 18, 35, 10, 33, 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, 0, 0, +]; + #[test] fn message_last_beacon_from_bytes() { let highest_superblock_checkpoint = CheckpointBeacon { @@ -352,8 +417,16 @@ fn message_block_to_bytes() { magic: 1, }; - let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR.to_vec(); - let result: Vec = msg.to_pb_bytes().unwrap(); + let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_LEGACY.to_vec(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::V1_7).unwrap(); + assert_eq!(result, expected_buf); + + let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_TRANSITION.to_vec(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::V1_8).unwrap(); + assert_eq!(result, expected_buf); + + let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_FINAL.to_vec(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::V2_0).unwrap(); assert_eq!(result, expected_buf); } @@ -365,7 +438,17 @@ fn message_block_from_bytes() { }; assert_eq!( - Message::from_pb_bytes(EXAMPLE_BLOCK_VECTOR).unwrap(), + Message::from_versioned_pb_bytes(EXAMPLE_BLOCK_VECTOR_LEGACY).unwrap(), + expected_msg + ); + + assert_eq!( + Message::from_versioned_pb_bytes(EXAMPLE_BLOCK_VECTOR_TRANSITION).unwrap(), + expected_msg + ); + + assert_eq!( + Message::from_versioned_pb_bytes(EXAMPLE_BLOCK_VECTOR_FINAL).unwrap(), expected_msg ); } diff --git a/docker/witnet-rust/Dockerfile b/docker/witnet-rust/Dockerfile index 50e274e0e..dd7566fd9 100644 --- a/docker/witnet-rust/Dockerfile +++ b/docker/witnet-rust/Dockerfile @@ -1,14 +1,14 @@ -FROM --platform=$TARGETPLATFORM ubuntu:jammy +FROM --platform=$TARGETPLATFORM ubuntu:noble # Install basic environment dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ +RUN apt update && apt install -y --no-install-recommends \ ca-certificates \ curl \ - netcat \ + netcat-traditional \ jq # Clean up apt packages so the docker image is as compact as possible -RUN apt-get clean && apt-get autoremove +RUN apt clean && apt autoremove # Set needed environment variables ENV RUST_BACKTRACE=1 diff --git a/net/src/server/ws/mod.rs b/net/src/server/ws/mod.rs index f0facf1c4..a0e0cbe00 100644 --- a/net/src/server/ws/mod.rs +++ b/net/src/server/ws/mod.rs @@ -11,7 +11,9 @@ pub use error::Error; type PubSubHandler = pubsub::PubSubHandler>; +// We need to pass the server as an argument to avoid dropping it before the start had finished /// TODO: doc +#[allow(dead_code)] pub struct Server(server::Server); impl Server { diff --git a/node/Cargo.toml b/node/Cargo.toml index a5a0064df..57fe55a7a 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "witnet_node" -version = "1.7.1" +version = "2.0.0" authors = ["Witnet Foundation "] workspace = ".." description = "node component" diff --git a/node/src/actors/chain_manager/actor.rs b/node/src/actors/chain_manager/actor.rs index 5187188d5..b37a4968d 100644 --- a/node/src/actors/chain_manager/actor.rs +++ b/node/src/actors/chain_manager/actor.rs @@ -13,10 +13,12 @@ use witnet_data_structures::{ }, data_request::DataRequestPool, get_environment, + staking::prelude::*, superblock::SuperBlockState, types::LastBeacon, utxo_pool::{OldUnspentOutputsPool, OwnUnspentOutputsPool, UtxoWriteBatch}, vrf::VrfCtx, + wit::Wit, }; use witnet_util::timestamp::pretty_print; @@ -223,9 +225,12 @@ impl ChainManager { } // Create a new ChainInfo let bootstrap_hash = consensus_constants.bootstrap_hash; - let reputation_engine = ReputationEngine::new(consensus_constants.activity_period as usize); let hash_prev_block = bootstrap_hash; + // Initialize configurable data structures + let reputation_engine = ReputationEngine::new(consensus_constants.activity_period as usize); + let stakes = Stakes::with_minimum(Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS)); + let chain_info = ChainInfo { environment, consensus_constants: consensus_constants.clone(), @@ -257,6 +262,7 @@ impl ChainManager { own_utxos: OwnUnspentOutputsPool::new(), data_request_pool: DataRequestPool::new(consensus_constants.extra_rounds), superblock_state, + stakes, ..ChainState::default() } } diff --git a/node/src/actors/chain_manager/handlers.rs b/node/src/actors/chain_manager/handlers.rs index e45a46bcb..e30cbbeaa 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -9,14 +9,18 @@ use std::{ use actix::{prelude::*, ActorFutureExt, WrapFuture}; use futures::future::Either; -use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE; +use witnet_config::defaults::{ + PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT, + PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, +}; use witnet_data_structures::{ chain::{ tapi::ActiveWips, Block, ChainState, CheckpointBeacon, DataRequestInfo, Epoch, Hash, Hashable, NodeStats, PublicKeyHash, SuperBlockVote, SupplyInfo, }, error::{ChainInfoError, TransactionError::DataRequestNotFound}, - transaction::{DRTransaction, Transaction, VTTransaction}, + staking::errors::StakesError, + transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::{self, NodeBalance}, types::LastBeacon, utxo_pool::{get_utxo_info, UtxoInfo}, @@ -29,13 +33,14 @@ use crate::{ chain_manager::{handlers::BlockBatches::*, BlockCandidate}, messages::{ AddBlocks, AddCandidates, AddCommitReveal, AddSuperBlock, AddSuperBlockVote, - AddTransaction, Broadcast, BuildDrt, BuildVtt, EpochNotification, EstimatePriority, - GetBalance, GetBalanceTarget, GetBlocksEpochRange, GetDataRequestInfo, - GetHighestCheckpointBeacon, GetMemoryTransaction, GetMempool, GetMempoolResult, - GetNodeStats, GetReputation, GetReputationResult, GetSignalingInfo, GetState, - GetSuperBlockVotes, GetSupplyInfo, GetUtxoInfo, IsConfirmedBlock, PeersBeacons, - ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, SetLastBeacon, - SetPeersLimits, SignalingInfo, SnapshotExport, SnapshotImport, TryMineBlock, + AddTransaction, Broadcast, BuildDrt, BuildStake, BuildVtt, EpochNotification, + EstimatePriority, GetBalance, GetBalanceTarget, GetBlocksEpochRange, + GetDataRequestInfo, GetHighestCheckpointBeacon, GetMemoryTransaction, GetMempool, + GetMempoolResult, GetNodeStats, GetReputation, GetReputationResult, GetSignalingInfo, + GetState, GetSuperBlockVotes, GetSupplyInfo, GetUtxoInfo, IsConfirmedBlock, + PeersBeacons, QueryStake, ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, + SetLastBeacon, SetPeersLimits, SignalingInfo, SnapshotExport, SnapshotImport, + TryMineBlock, }, sessions_manager::SessionsManager, }, @@ -967,11 +972,17 @@ impl Handler for ChainManager { }, _, )) => { - self.sync_target = Some(SyncTarget { + let target = SyncTarget { block: consensus_beacon, superblock: superblock_consensus, - }); - log::debug!("Sync target {:?}", self.sync_target); + }; + self.sync_target = Some(target); + log::info!( + "Synchronization target has been set ({}: {})", + target.block.checkpoint, + target.block.hash_prev_block + ); + log::debug!("{:#?}", target); let our_beacon = self.get_chain_beacon(); log::debug!( @@ -997,13 +1008,18 @@ impl Handler for ChainManager { { // Fork case log::warn!( - "[CONSENSUS]: We are on {:?} but the network is on {:?}", + "[CONSENSUS]: The local chain is apparently forked.\n\ + We are on {:?} but the network is on {:?}.\n\ + The node will automatically try to recover from this forked situation by restoring the chain state from the storage.", our_beacon, consensus_beacon ); self.initialize_from_storage(ctx); - log::info!("Restored chain state from storage"); + log::info!( + "The chain state has been restored from storage.\n\ + Now the node will try to resynchronize." + ); StateMachine::WaitingConsensus } else { @@ -1277,6 +1293,81 @@ impl Handler for ChainManager { } } +impl Handler for ChainManager { + type Result = ResponseActFuture::Result>; + + fn handle(&mut self, msg: BuildStake, _ctx: &mut Self::Context) -> Self::Result { + if !msg.dry_run && self.sm_state != StateMachine::Synced { + return Box::pin(actix::fut::err( + ChainManagerError::NotSynced { + current_state: self.sm_state, + } + .into(), + )); + } + let timestamp = u64::try_from(get_timestamp()).unwrap(); + match transaction_factory::build_st( + msg.stake_output, + msg.fee, + &mut self.chain_state.own_utxos, + self.own_pkh.unwrap(), + &self.chain_state.unspent_outputs_pool, + timestamp, + self.tx_pending_timeout, + &msg.utxo_strategy, + PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT, + msg.dry_run, + ) { + Err(e) => { + log::error!("Error when building stake transaction: {}", e); + Box::pin(actix::fut::err(e.into())) + } + Ok(st) => { + let fut = signature_mngr::sign_transaction(&st, st.inputs.len()) + .into_actor(self) + .then(move |s, act, _ctx| match s { + Ok(signatures) => { + let st = StakeTransaction::new(st, signatures); + + if msg.dry_run { + Either::Right(actix::fut::result(Ok(st))) + } else { + let transaction = Transaction::Stake(st.clone()); + Either::Left( + act.add_transaction( + AddTransaction { + transaction, + broadcast_flag: true, + }, + get_timestamp(), + ) + .map_ok(move |_, _, _| st), + ) + } + } + Err(e) => { + log::error!("Failed to sign stake transaction: {}", e); + Either::Right(actix::fut::result(Err(e))) + } + }); + + Box::pin(fut) + } + } + } +} + +impl Handler for ChainManager { + type Result = ::Result; + + fn handle(&mut self, msg: QueryStake, _ctx: &mut Self::Context) -> Self::Result { + // build address from public key hash + let stakes = self.chain_state.stakes.query_stakes(msg.key); + + stakes.map_err(StakesError::from).map_err(Into::into) + } +} + impl Handler for ChainManager { type Result = ResponseActFuture>; @@ -1648,7 +1739,19 @@ impl Handler for ChainManager { type Result = (); fn handle(&mut self, _msg: TryMineBlock, ctx: &mut Self::Context) -> Self::Result { - self.try_mine_block(ctx); + if let Err(e) = self.try_mine_block(ctx) { + match e { + // Lack of eligibility is logged as debug + e @ ChainManagerError::NotEligible => { + log::debug!("{}", e); + } + // Any other errors are logged as warning (considering that this is a best-effort + // method) + e => { + log::warn!("{}", e); + } + } + } } } diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 231bdfb9c..e98083cd7 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -15,7 +15,11 @@ use actix::{ }; use ansi_term::Color::{White, Yellow}; use futures::future::{try_join_all, FutureExt}; -use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE; + +use witnet_config::defaults::{ + PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT, + PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, +}; use witnet_data_structures::{ chain::{ tapi::{after_second_hard_fork, ActiveWips}, @@ -28,12 +32,15 @@ use witnet_data_structures::{ DataRequestPool, }, error::TransactionError, - get_environment, + get_environment, get_protocol_version, + proto::versioning::{ProtocolVersion, VersionedHashable}, radon_error::RadonError, radon_report::{RadonReport, ReportContext, TypeLike}, + staking::prelude::*, transaction::{ CommitTransaction, CommitTransactionBody, DRTransactionBody, MintTransaction, - RevealTransaction, RevealTransactionBody, TallyTransaction, VTTransactionBody, + RevealTransaction, RevealTransactionBody, StakeTransactionBody, TallyTransaction, + VTTransactionBody, }, transaction_factory::{build_commit_collateral, check_commit_collateral}, utxo_pool::{UnspentOutputsPool, UtxoDiff}, @@ -47,15 +54,21 @@ use witnet_rad::{ types::{serial_iter_decode, RadonTypes}, }; use witnet_util::timestamp::get_timestamp; -use witnet_validations::validations::{ - block_reward, calculate_liars_and_errors_count_from_tally, calculate_mining_probability, - calculate_randpoe_threshold, calculate_reppoe_threshold, dr_transaction_fee, merkle_tree_root, - tally_bytes_on_encode_error, update_utxo_diff, vt_transaction_fee, +use witnet_validations::{ + eligibility::{ + current::{Eligibility, Eligible}, + legacy::*, + }, + validations::{ + block_reward, calculate_liars_and_errors_count_from_tally, dr_transaction_fee, + merkle_tree_root, st_transaction_fee, tally_bytes_on_encode_error, update_utxo_diff, + vt_transaction_fee, + }, }; use crate::{ actors::{ - chain_manager::{ChainManager, StateMachine}, + chain_manager::{ChainManager, ChainManagerError, StateMachine}, messages::{AddCommitReveal, ResolveRA, RunTally}, rad_manager::RadManager, }, @@ -64,136 +77,111 @@ use crate::{ impl ChainManager { /// Try to mine a block - pub fn try_mine_block(&mut self, ctx: &mut Context) { + pub fn try_mine_block(&mut self, ctx: &mut Context) -> Result<(), ChainManagerError> { if !self.mining_enabled { - log::debug!("Mining is disabled in the configuration"); - return; + return Err(ChainManagerError::MiningIsDisabled); } // We only want to mine in Synced state if self.sm_state != StateMachine::Synced { - log::debug!( - "Not mining because node is not in Synced state (current state is {:?})", - self.sm_state - ); - return; - } - - if self.current_epoch.is_none() { - log::warn!("Cannot mine a block because current epoch is unknown"); - - return; - } - if self.own_pkh.is_none() { - log::warn!("PublicKeyHash is not set. All mined wits will be lost!"); - } - - if self.chain_state.reputation_engine.is_none() { - log::warn!("Reputation engine is not set"); - - return; - } - if self.epoch_constants.is_none() { - log::warn!("EpochConstants is not set"); - - return; - } - if self.chain_state.chain_info.is_none() { - log::warn!("ChainInfo is not set"); - - return; + return Err(ChainManagerError::NotSynced { + current_state: self.sm_state, + }); } - let epoch_constants = self.epoch_constants.unwrap(); - let rep_engine = self.chain_state.reputation_engine.as_ref().unwrap().clone(); - let total_identities = u32::try_from(rep_engine.ars().active_identities_number()).unwrap(); - let current_epoch = self.current_epoch.unwrap(); + let current_epoch = self.current_epoch.ok_or(ChainManagerError::ChainNotReady)?; + let own_pkh = self.own_pkh.ok_or(ChainManagerError::ChainNotReady)?; + let epoch_constants = self + .epoch_constants + .ok_or(ChainManagerError::ChainNotReady)?; + let chain_info = self + .chain_state + .chain_info + .clone() + .ok_or(ChainManagerError::ChainNotReady)?; - let chain_info = self.chain_state.chain_info.as_mut().unwrap(); let max_vt_weight = chain_info.consensus_constants.max_vt_weight; let max_dr_weight = chain_info.consensus_constants.max_dr_weight; + let max_st_weight = PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT; let mining_bf = chain_info.consensus_constants.mining_backup_factor; - let mining_rf = chain_info.consensus_constants.mining_replication_factor; let collateral_minimum = chain_info.consensus_constants.collateral_minimum; let minimum_difficulty = chain_info.consensus_constants.minimum_difficulty; let initial_block_reward = chain_info.consensus_constants.initial_block_reward; let halving_period = chain_info.consensus_constants.halving_period; - let epochs_with_minimum_difficulty = chain_info - .consensus_constants - .epochs_with_minimum_difficulty; let mut beacon = chain_info.highest_block_checkpoint; let mut vrf_input = chain_info.highest_vrf_output; - if beacon.checkpoint >= current_epoch { - // We got a block from the future - // Due to block consolidation from epoch N is done in epoch N+1, - // and chain beacon is the same that the last block known. - // Our chain beacon always come from the past epoch. So, a chain beacon - // with the current epoch is the same error if it is come from the future - log::error!( - "The current highest checkpoint beacon is from the future ({:?} >= {:?})", - beacon.checkpoint, - current_epoch - ); - return; + if get_protocol_version(self.current_epoch) == ProtocolVersion::V2_0 { + let key = StakeKey::from((own_pkh, own_pkh)); + let eligibility = self + .chain_state + .stakes + .mining_eligibility(key, current_epoch) + .map_err(ChainManagerError::Staking)?; + + match eligibility { + Eligible::Yes => { + log::info!("Hurray! Found eligibility for proposing a block candidate!"); + } + Eligible::No(_) => { + log::debug!("No eligibility for proposing a block candidate."); + return Ok(()); + } + } } + // The highest checkpoint beacon should contain the current epoch beacon.checkpoint = current_epoch; vrf_input.checkpoint = current_epoch; - let own_pkh = self.own_pkh.unwrap_or_default(); - let is_ars_member = rep_engine.is_ars_member(&own_pkh); - let active_wips = ActiveWips { - active_wips: self.chain_state.tapi_engine.wip_activation.clone(), - block_epoch: current_epoch, + let target_hash = if get_protocol_version(self.current_epoch) == ProtocolVersion::V2_0 { + Hash::max() + } else { + let rep_engine = self.chain_state.reputation_engine.as_ref().unwrap().clone(); + let total_identities = + u32::try_from(rep_engine.ars().active_identities_number()).unwrap(); + let epochs_with_minimum_difficulty = chain_info + .consensus_constants + .epochs_with_minimum_difficulty; + + let active_wips = ActiveWips { + active_wips: self.chain_state.tapi_engine.wip_activation.clone(), + block_epoch: current_epoch, + }; + + // invalid: vrf_hash > target_hash + let (target_hash, _probability) = calculate_randpoe_threshold( + total_identities, + mining_bf, + current_epoch, + minimum_difficulty, + epochs_with_minimum_difficulty, + &active_wips, + ); + + target_hash }; - // Create a VRF proof and if eligible build block signature_mngr::vrf_prove(VrfMessage::block_mining(vrf_input)) .map(move |res| { res.map_err(|e| log::error!("Failed to create block eligibility proof: {}", e)) .map(move |(vrf_proof, vrf_proof_hash)| { - // invalid: vrf_hash > target_hash - let (target_hash, probability) = calculate_randpoe_threshold( - total_identities, - mining_bf, - current_epoch, - minimum_difficulty, - epochs_with_minimum_difficulty, - &active_wips, - ); - let proof_invalid = vrf_proof_hash > target_hash; + // For legacy protocol versions, check if our proof meets some thresholds + + if vrf_proof_hash > target_hash { + Err(())?; + } log::info!( - "Probability to create a valid mining proof: {:.6}%", - probability * 100_f64 + "{} Discovered eligibility for mining a block for epoch #{}", + Yellow.bold().paint("[Mining]"), + Yellow.bold().paint(beacon.checkpoint.to_string()) ); - log::trace!("Target hash: {}", target_hash); - log::trace!("Our proof: {}", vrf_proof_hash); - if proof_invalid { - log::debug!("No eligibility for mining a block"); - Err(()) - } else { - log::info!( - "{} Discovered eligibility for mining a block for epoch #{}", - Yellow.bold().paint("[Mining]"), - Yellow.bold().paint(beacon.checkpoint.to_string()) - ); - let mining_prob = calculate_mining_probability( - &rep_engine, - own_pkh, - mining_rf, - mining_bf, - ); - // Discount the already reached probability - let mining_prob = mining_prob / probability * 100.0; - log::info!( - "Probability that the mined block will be selected: {:.6}%", - mining_prob - ); - Ok(vrf_proof) - } + + // TODO: figure out if mining probability estimates make any sense in the context of PoS + + Ok::<_, ()>(vrf_proof) }) }) .flatten_err() @@ -206,12 +194,8 @@ impl ChainManager { .and_then(move |(vrf_proof, tally_transactions), act, _ctx| { let eligibility_claim = BlockEligibilityClaim { proof: vrf_proof }; - // If pkh is in ARS, no need to send bn256 public key - let bn256_public_key = if is_ars_member { - None - } else { - act.bn256_public_key.clone() - }; + // TODO: assess if bn256 keys are redundant + let bn256_public_key = act.bn256_public_key.clone(); let tapi_version = act.tapi_signals_mask(current_epoch); @@ -229,6 +213,7 @@ impl ChainManager { ), max_vt_weight, max_dr_weight, + max_st_weight, beacon, eligibility_claim, &tally_transactions, @@ -246,7 +231,10 @@ impl ChainManager { ); // Sign the block hash - signature_mngr::sign(&block_header) + let protocol = get_protocol_version(Some(block_header.beacon.checkpoint)); + let block_header_data = block_header.versioned_hash(protocol).data(); + + signature_mngr::sign_data(block_header_data) .map(|res| { res.map_err(|e| log::error!("Couldn't sign beacon: {}", e)) .map(|block_sig| Block::new(block_header, block_sig, txns)) @@ -280,6 +268,8 @@ impl ChainManager { }) .map(|_res: Result<(), ()>, _act, _ctx| ()) .wait(ctx); + + Ok(()) } /// Try to mine a data_request @@ -808,11 +798,13 @@ impl ChainManager { /// Build a new Block using the supplied leadership proof and by filling transactions from the /// `transaction_pool` /// Returns an unsigned block! +/// TODO: simplify function signature, e.g. through merging multiple related fields into new data structures. #[allow(clippy::too_many_arguments)] pub fn build_block( pools_ref: (&mut TransactionsPool, &UnspentOutputsPool, &DataRequestPool), max_vt_weight: u32, max_dr_weight: u32, + max_st_weight: u32, beacon: CheckpointBeacon, proof: BlockEligibilityClaim, tally_transactions: &[TallyTransaction], @@ -836,14 +828,21 @@ pub fn build_block( let mut transaction_fees: u64 = 0; let mut vt_weight: u32 = 0; let mut dr_weight: u32 = 0; + let mut st_weight: u32 = 0; let mut value_transfer_txns = Vec::new(); let mut data_request_txns = Vec::new(); let mut tally_txns = Vec::new(); + let mut stake_txns = Vec::new(); + // TODO: handle unstake tx + let unstake_txns = Vec::new(); + // Calculate the base weight for different types of transactions, to know when to give up trying to fit more + // transactions into a block let min_vt_weight = VTTransactionBody::new(vec![Input::default()], vec![ValueTransferOutput::default()]) .weight(); - // Currently only value transfer transactions weight is taking into account + let min_st_weight = + StakeTransactionBody::new(vec![Input::default()], Default::default(), None).weight(); for vt_tx in transactions_pool.vt_iter() { let transaction_weight = vt_tx.weight(); @@ -858,7 +857,7 @@ pub fn build_block( } }; - let new_vt_weight = vt_weight.saturating_add(transaction_weight); + let new_vt_weight = st_weight.saturating_add(transaction_weight); if new_vt_weight <= max_vt_weight { update_utxo_diff( &mut utxo_diff, @@ -949,7 +948,7 @@ pub fn build_block( witnesses: 1, ..DataRequestOutput::default() }; - let min_dr_weight = DRTransactionBody::new(vec![Input::default()], vec![], dro).weight(); + let min_dr_weight = DRTransactionBody::new(vec![Input::default()], dro, vec![]).weight(); for dr_tx in transactions_pool.dr_iter() { let transaction_weight = dr_tx.weight(); let transaction_fee = match dr_transaction_fee(dr_tx, &utxo_diff, epoch, epoch_constants) { @@ -984,6 +983,51 @@ pub fn build_block( } } + let mut included_validators = HashSet::::new(); + for st_tx in transactions_pool.st_iter() { + let validator_pkh = st_tx.body.output.authorization.public_key.pkh(); + if included_validators.contains(&validator_pkh) { + log::debug!( + "Cannot include more than one stake transaction for {} in a single block", + validator_pkh + ); + continue; + } + + let transaction_weight = st_tx.weight(); + let transaction_fee = match st_transaction_fee(st_tx, &utxo_diff, epoch, epoch_constants) { + Ok(x) => x, + Err(e) => { + log::warn!( + "Error when calculating transaction fee for transaction: {}", + e + ); + continue; + } + }; + + let new_st_weight = st_weight.saturating_add(transaction_weight); + if new_st_weight <= max_st_weight { + update_utxo_diff( + &mut utxo_diff, + st_tx.body.inputs.iter(), + st_tx.body.change.iter(), + st_tx.hash(), + ); + stake_txns.push(st_tx.clone()); + transaction_fees = transaction_fees.saturating_add(transaction_fee); + st_weight = new_st_weight; + } + + // The condition to stop is if the free space in the block for VTTransactions + // is less than the minimum stake transaction weight + if st_weight > max_st_weight.saturating_sub(min_st_weight) { + break; + } + + included_validators.insert(validator_pkh); + } + // Include Mint Transaction by miner let reward = block_reward(epoch, initial_block_reward, halving_period) + transaction_fees; let mint = MintTransaction::with_external_address( @@ -1000,6 +1044,23 @@ pub fn build_block( let commit_hash_merkle_root = merkle_tree_root(&commit_txns); let reveal_hash_merkle_root = merkle_tree_root(&reveal_txns); let tally_hash_merkle_root = merkle_tree_root(&tally_txns); + + let protocol = get_protocol_version(Some(beacon.checkpoint)); + + let stake_hash_merkle_root = if protocol == ProtocolVersion::V1_7 { + log::debug!("Legacy protocol: the default stake hash merkle root will be used"); + Hash::default() + } else { + log::debug!("Pseudo-2.0 protocol: a merkle tree will be built for the stake transactions"); + merkle_tree_root(&stake_txns) + }; + + let unstake_hash_merkle_root = if protocol == ProtocolVersion::V1_7 { + Hash::default() + } else { + merkle_tree_root(&unstake_txns) + }; + let merkle_roots = BlockMerkleRoots { mint_hash: mint.hash(), vt_hash_merkle_root, @@ -1007,6 +1068,8 @@ pub fn build_block( commit_hash_merkle_root, reveal_hash_merkle_root, tally_hash_merkle_root, + stake_hash_merkle_root, + unstake_hash_merkle_root, }; let block_header = BlockHeader { @@ -1024,6 +1087,8 @@ pub fn build_block( commit_txns, reveal_txns, tally_txns, + stake_txns, + unstake_txns, }; (block_header, txns) @@ -1065,6 +1130,7 @@ mod tests { // Set `max_vt_weight` and `max_dr_weight` to zero (no transaction should be included) let max_vt_weight = 0; let max_dr_weight = 0; + let max_st_weight = 0; // Fields required to mine a block let block_beacon = CheckpointBeacon::default(); @@ -1078,6 +1144,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], @@ -1246,6 +1313,7 @@ mod tests { // Set `max_vt_weight` to fit only `transaction_1` weight let max_vt_weight = vt_tx1.weight(); let max_dr_weight = 0; + let max_st_weight = 0; // Insert transactions into `transactions_pool` let mut transaction_pool = TransactionsPool::default(); @@ -1282,6 +1350,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], @@ -1347,6 +1416,7 @@ mod tests { // Set `max_vt_weight` to fit only 1 transaction weight let max_vt_weight = vt_tx2.weight(); let max_dr_weight = 0; + let max_st_weight = 0; // Insert transactions into `transactions_pool` let mut transaction_pool = TransactionsPool::default(); @@ -1383,6 +1453,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], @@ -1446,9 +1517,9 @@ mod tests { let mut dr3 = dr1.clone(); dr3.witnesses = 3; - let dr_body_one_output1 = DRTransactionBody::new(input.clone(), vec![], dr1); - let dr_body_one_output2 = DRTransactionBody::new(input.clone(), vec![], dr2); - let dr_body_one_output3 = DRTransactionBody::new(input, vec![], dr3); + let dr_body_one_output1 = DRTransactionBody::new(input.clone(), dr1, vec![]); + let dr_body_one_output2 = DRTransactionBody::new(input.clone(), dr2, vec![]); + let dr_body_one_output3 = DRTransactionBody::new(input, dr3, vec![]); // Build sample transactions let dr_tx1 = DRTransaction::new(dr_body_one_output1, vec![]); @@ -1462,6 +1533,7 @@ mod tests { // Set `max_vt_weight` to fit only `transaction_1` weight let max_vt_weight = 0; let max_dr_weight = dr_tx1.weight(); + let max_st_weight = 0; // Insert transactions into `transactions_pool` let mut transaction_pool = TransactionsPool::default(); @@ -1498,6 +1570,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], @@ -1542,9 +1615,9 @@ mod tests { let mut dr3 = dr1.clone(); dr3.commit_and_reveal_fee = 3; - let dr_body_one_output1 = DRTransactionBody::new(input.clone(), vec![], dr1); - let dr_body_one_output2 = DRTransactionBody::new(input.clone(), vec![], dr2); - let dr_body_one_output3 = DRTransactionBody::new(input, vec![], dr3); + let dr_body_one_output1 = DRTransactionBody::new(input.clone(), dr1, vec![]); + let dr_body_one_output2 = DRTransactionBody::new(input.clone(), dr2, vec![]); + let dr_body_one_output3 = DRTransactionBody::new(input, dr3, vec![]); // Build sample transactions let dr_tx1 = DRTransaction::new(dr_body_one_output1, vec![]); @@ -1560,6 +1633,7 @@ mod tests { // Set `max_vt_weight` to fit only `transaction_1` weight let max_vt_weight = 0; let max_dr_weight = dr_tx2.weight(); + let max_st_weight = 0; // Insert transactions into `transactions_pool` let mut transaction_pool = TransactionsPool::default(); @@ -1596,6 +1670,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index bc2c48602..1d43f84d9 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -47,6 +47,7 @@ use futures::future::{try_join_all, FutureExt}; use glob::glob; use itertools::Itertools; use rand::Rng; + use witnet_config::{ config::Tapi, defaults::{ @@ -68,8 +69,10 @@ use witnet_data_structures::{ SuperBlock, SuperBlockVote, TransactionsPool, }, data_request::DataRequestPool, - get_environment, + get_environment, get_protocol_version, + proto::versioning::ProtocolVersion, radon_report::{RadonReport, ReportContext}, + staking::prelude::*, superblock::{ARSIdentities, AddSuperBlockVote, SuperBlockConsensus}, transaction::{RevealTransaction, TallyTransaction, Transaction}, types::{ @@ -78,12 +81,16 @@ use witnet_data_structures::{ }, utxo_pool::{Diff, OwnUnspentOutputsPool, UnspentOutputsPool, UtxoWriteBatch}, vrf::VrfCtx, + wit::Wit, }; use witnet_rad::types::RadonTypes; use witnet_util::timestamp::seconds_to_human_string; -use witnet_validations::validations::{ - compare_block_candidates, validate_block, validate_block_transactions, - validate_new_transaction, validate_rad_request, verify_signatures, VrfSlots, +use witnet_validations::{ + eligibility::legacy::VrfSlots, + validations::{ + compare_block_candidates, validate_block, validate_block_transactions, + validate_new_transaction, validate_rad_request, verify_signatures, + }, }; use crate::{ @@ -124,7 +131,7 @@ pub enum ChainManagerError { #[fail(display = "A block does not exist")] BlockDoesNotExist, /// Optional fields of ChainManager are not properly initialized yet - #[fail(display = "ChainManager is not ready yet")] + #[fail(display = "ChainManager is not ready yet. This may self-fix in a little while")] ChainNotReady, /// The node attempted to do an action that is only allowed while `ChainManager` /// is in `Synced` state. @@ -153,6 +160,15 @@ pub enum ChainManagerError { /// Tells what the current epoch was current_superblock_index: u32, }, + /// Tried to mine block candidates but mining is disabled through configuration. + #[fail(display = "Mining is disabled through configuration")] + MiningIsDisabled, + /// A staking-related error happened. + #[fail(display = "A staking-related error happened: {:?}", _0)] + Staking(StakesError), + /// The node is not eligible to perform a certain action. + #[fail(display = "The node is not eligible to perform this action")] + NotEligible, } /// Synchronization target determined by the beacons received from outbound peers @@ -683,6 +699,7 @@ impl ChainManager { let mut transaction_visitor = PriorityVisitor::default(); + let protocol_version = get_protocol_version(self.current_epoch); let utxo_diff = process_validations( &block, self.current_epoch.unwrap_or_default(), @@ -698,6 +715,8 @@ impl ChainManager { resynchronizing, &active_wips, Some(&mut transaction_visitor), + protocol_version, + &self.chain_state.stakes, )?; // Extract the collected priorities from the internal state of the visitor @@ -725,8 +744,9 @@ impl ChainManager { || block.block_header.beacon.checkpoint == current_epoch + 1) { log::debug!( - "Ignoring received block #{} because its beacon is too old", - block.block_header.beacon.checkpoint + "Ignoring received block candidate because its beacon shows an old epoch ({}). The current epoch is {}.", + block.block_header.beacon.checkpoint, + current_epoch, ); return; @@ -786,6 +806,8 @@ impl ChainManager { return; } }; + let protocol_version = + get_protocol_version(Some(block.block_header.beacon.checkpoint)); if let Some(best_candidate) = &self.best_candidate { let best_hash = best_candidate.block.hash(); @@ -809,6 +831,7 @@ impl ChainManager { best_candidate.vrf_proof, best_candidate_is_active, &target_vrf_slots, + protocol_version, ) != Ordering::Greater { log::debug!("Ignoring new block candidate ({}) because a better one ({}) has been already validated", hash_block, best_hash); @@ -838,6 +861,8 @@ impl ChainManager { false, &active_wips, Some(&mut transaction_visitor), + protocol_version, + &self.chain_state.stakes, ) { Ok(utxo_diff) => { let priorities = transaction_visitor.take_state(); @@ -900,10 +925,28 @@ impl ChainManager { } }; + let current_epoch = if let Some(epoch) = self.current_epoch { + epoch + } else { + // If there is no epoch set, it's because the chain is yet to be bootstrapped, or because of a data race + match self.chain_state.chain_info.as_ref() { + // If the chain is yet to be bootstrapped (the block we are processing is the genesis block), set the epoch to zero + Some(chain_info) if chain_info.consensus_constants.genesis_hash == block.hash() => { + 0 + } + // In case of data race, shortcut the function + _ => { + log::error!("Current epoch not loaded in ChainManager"); + return; + } + } + }; + match self.chain_state { ChainState { chain_info: Some(ref mut chain_info), reputation_engine: Some(ref mut reputation_engine), + ref mut stakes, .. } => { let block_hash = block.hash(); @@ -968,7 +1011,11 @@ impl ChainManager { let miner_pkh = block.block_header.proof.proof.pkh(); - // Do not update reputation when consolidating genesis block + // Reset the coin age of the miner for all staked coins + let key = StakeKey::from((miner_pkh, miner_pkh)); + let _ = stakes.reset_age(key, Capability::Mining, current_epoch); + + // Do not update reputation or stakes when consolidating genesis block if block_hash != chain_info.consensus_constants.genesis_hash { update_reputation( reputation_engine, @@ -980,6 +1027,18 @@ impl ChainManager { block_epoch, self.own_pkh.unwrap_or_default(), ); + + let stake_txns_count = block.txns.stake_txns.len(); + if stake_txns_count > 0 { + log::debug!("Processing {stake_txns_count} stake transactions"); + + let _ = process_stake_transactions( + stakes, + block.txns.stake_txns.iter(), + block_epoch, + ); + } + //process_unstake_transactions(stakes, block.txns.unstake_txns.iter(), block_epoch); } // Update bn256 public keys with block information @@ -1964,6 +2023,7 @@ impl ChainManager { active_wips: self.chain_state.tapi_engine.wip_activation.clone(), block_epoch: block.block_header.beacon.checkpoint, }; + let protocol_version = get_protocol_version(Some(block.block_header.beacon.checkpoint)); let res = validate_block( &block, current_epoch, @@ -1973,6 +2033,8 @@ impl ChainManager { self.chain_state.reputation_engine.as_ref().unwrap(), &consensus_constants, &active_wips, + protocol_version, + &self.chain_state.stakes, ); let fut = async { @@ -2771,6 +2833,8 @@ pub fn process_validations( resynchronizing: bool, active_wips: &ActiveWips, transaction_visitor: Option<&mut dyn Visitor>, + protocol_version: ProtocolVersion, + stakes: &Stakes, ) -> Result { if !resynchronizing { let mut signatures_to_verify = vec![]; @@ -2783,6 +2847,8 @@ pub fn process_validations( rep_eng, consensus_constants, active_wips, + protocol_version, + stakes, )?; verify_signatures(signatures_to_verify, vrf_ctx)?; } @@ -2935,6 +3001,10 @@ fn update_pools( transactions_pool.remove_one_reveal(&re_tx.body.dr_pointer, &re_tx.body.pkh, &re_tx.hash()); } + for st_tx in &block.txns.stake_txns { + transactions_pool.st_remove(st_tx); + } + // Update own_utxos utxo_diff.visit( own_utxos, diff --git a/node/src/actors/inventory_manager/handlers.rs b/node/src/actors/inventory_manager/handlers.rs index 7b6484104..d2a798372 100644 --- a/node/src/actors/inventory_manager/handlers.rs +++ b/node/src/actors/inventory_manager/handlers.rs @@ -447,6 +447,7 @@ mod tests { // Set `max_vt_weight` to fit only `transaction_1` weight let max_vt_weight = vt_tx1.weight(); let max_dr_weight = 0; + let max_st_weight = 0; // Insert transactions into `transactions_pool` let mut transaction_pool = TransactionsPool::default(); @@ -480,6 +481,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index a408bc48d..3eb157310 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -23,9 +23,11 @@ use serde::{Deserialize, Serialize}; use witnet_crypto::key::KeyPath; use witnet_data_structures::{ chain::{ - tapi::ActiveWips, Block, DataRequestOutput, Epoch, Hash, Hashable, PublicKeyHash, RADType, - StateMachine, SyncStatus, + tapi::ActiveWips, Block, DataRequestOutput, Epoch, Hash, Hashable, KeyedSignature, + PublicKeyHash, RADType, StakeOutput, StateMachine, SyncStatus, }, + get_environment, + staking::prelude::*, transaction::Transaction, vrf::VrfMessage, }; @@ -37,13 +39,14 @@ use crate::{ inventory_manager::{InventoryManager, InventoryManagerError}, json_rpc::Subscriptions, messages::{ - AddCandidates, AddPeers, AddTransaction, BuildDrt, BuildVtt, ClearPeers, DropAllPeers, + AddCandidates, AddPeers, AddTransaction, AuthorizeStake, BuildDrt, BuildStake, + BuildStakeParams, BuildStakeResponse, BuildVtt, ClearPeers, DropAllPeers, EstimatePriority, GetBalance, GetBalanceTarget, GetBlocksEpochRange, GetConsolidatedPeers, GetDataRequestInfo, GetEpoch, GetHighestCheckpointBeacon, GetItemBlock, GetItemSuperblock, GetItemTransaction, GetKnownPeers, GetMemoryTransaction, GetMempool, GetNodeStats, GetReputation, GetSignalingInfo, - GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, IsConfirmedBlock, Rewind, - SnapshotExport, SnapshotImport, + GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, IsConfirmedBlock, QueryStake, + QueryStakesParams, Rewind, SnapshotExport, SnapshotImport, StakeAuthorization, }, peers_manager::PeersManager, sessions_manager::SessionsManager, @@ -136,6 +139,9 @@ pub fn attach_regular_methods( Box::pin(signaling_info()) }); server.add_actix_method(system, "priority", |_params: Params| Box::pin(priority())); + server.add_actix_method(system, "queryStakes", |params: Params| { + Box::pin(query_stakes(params.parse())) + }); } /// Attach the sensitive JSON-RPC methods to a multi-transport server. @@ -266,6 +272,23 @@ pub fn attach_sensitive_methods( |params| snapshot_import(params.parse()), )) }); + server.add_actix_method(system, "stake", move |params| { + Box::pin(if_authorized( + enable_sensitive_methods, + "stake", + params, + |params| stake(params.parse()), + )) + }); + + server.add_actix_method(system, "authorizeStake", move |params: Params| { + Box::pin(if_authorized( + enable_sensitive_methods, + "authorizeStake", + params, + |params| authorize_stake(params.parse()), + )) + }); } fn extract_topic_and_params(params: Params) -> Result<(String, Value), Error> { @@ -1921,6 +1944,181 @@ pub async fn snapshot_import(params: Result) -> Jso // Write the response back (the path to the snapshot file) serde_json::to_value(response).map_err(internal_error_s) } +/// Build a stake transaction +pub async fn stake(params: Result) -> JsonRpcResult { + // Short-circuit if parameters are wrong + let params = params?; + + let withdrawer = params + .withdrawer + .clone() + .try_do_magic(|hex_str| PublicKeyHash::from_bech32(get_environment(), &hex_str)) + .map_err(internal_error)?; + log::debug!( + "[STAKE] Creating stake transaction with withdrawer address: {}", + withdrawer + ); + + // This is the actual message that gets signed as part of the authorization + let msg = withdrawer.as_secp256k1_msg(); + + let authorization = params + .authorization + .try_do_magic(|hex_str| KeyedSignature::from_recoverable_hex(&hex_str, &msg)) + .map_err(internal_error)?; + let validator = PublicKeyHash::from_public_key(&authorization.public_key); + log::debug!( + "[STAKE] A stake authorization was provided, and it was signed by validator {}", + validator + ); + + let key = StakeKey { + validator, + withdrawer, + }; + + // Construct a BuildStake message that we can relay to the ChainManager for creation of the Stake transaction + let build_stake = BuildStake { + dry_run: params.dry_run, + fee: params.fee, + utxo_strategy: params.utxo_strategy, + stake_output: StakeOutput { + authorization, + key, + value: params.value, + }, + }; + + ChainManager::from_registry() + .send(build_stake) + .map(|res| match res { + Ok(Ok(transaction)) => { + // In the event that this is a dry run, we want to inject some additional information into the + // response, so that the user can confirm the facts surrounding the stake transaction before + // submitting it + if params.dry_run { + let staker = transaction + .signatures + .iter() + .cloned() + .map(|signature| signature.public_key.pkh()) + .collect(); + + let bsr = BuildStakeResponse { + transaction, + staker, + validator, + withdrawer, + }; + + serde_json::to_value(bsr).map_err(internal_error) + } else { + serde_json::to_value(transaction).map_err(internal_error) + } + } + Ok(Err(e)) => { + let err = internal_error_s(e); + Err(err) + } + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + }) + .await +} + +/// Create a stake authorization for the given address. +/// +/// The output of this method is a required argument to call the Stake method. +/* test +{"jsonrpc": "2.0","method": "authorizeStake", "params": {"withdrawer":"wit1lkzl4a365fvrr604pwqzykxugpglkrp5ekj0k0"}, "id": "1"} +*/ +pub async fn authorize_stake(params: Result) -> JsonRpcResult { + // Short-circuit if parameters are wrong + let params = params?; + + // If a withdrawer address is not specified, default to local node address + let withdrawer = if let Some(address) = params.withdrawer { + PublicKeyHash::from_bech32(get_environment(), &address).map_err(internal_error)? + } else { + let pk = signature_mngr::public_key().await.unwrap(); + + PublicKeyHash::from_public_key(&pk) + }; + + // This is the actual message that gets signed as part of the authorization + let msg = withdrawer.as_secp256k1_msg(); + + signature_mngr::sign_data(msg) + .map(|res| { + res.map_err(internal_error).and_then(|signature| { + let authorization = StakeAuthorization { + withdrawer, + signature, + }; + + serde_json::to_value(authorization).map_err(internal_error) + }) + }) + .await +} + +/// Param for query_stakes +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum QueryStakesArgument { + /// To query by stake validator + Validator(String), + /// To query by stake withdrawer + Withdrawer(String), + /// To query by stake validator and withdrawer + Key((String, String)), +} + +/// Query the amount of nanowits staked by an address. +pub async fn query_stakes(params: Result, Error>) -> JsonRpcResult { + // Short-circuit if parameters are wrong + let params = params?; + + // If a withdrawer address is not specified, default to local node address + let key: QueryStakesParams = if let Some(address) = params { + match address { + QueryStakesArgument::Validator(validator) => QueryStakesParams::Validator( + PublicKeyHash::from_bech32(get_environment(), &validator) + .map_err(internal_error)?, + ), + QueryStakesArgument::Withdrawer(withdrawer) => QueryStakesParams::Withdrawer( + PublicKeyHash::from_bech32(get_environment(), &withdrawer) + .map_err(internal_error)?, + ), + QueryStakesArgument::Key((validator, withdrawer)) => QueryStakesParams::Key(( + PublicKeyHash::from_bech32(get_environment(), &validator) + .map_err(internal_error)?, + PublicKeyHash::from_bech32(get_environment(), &withdrawer) + .map_err(internal_error)?, + )), + } + } else { + let pk = signature_mngr::public_key().await.map_err(internal_error)?; + + QueryStakesParams::Validator(PublicKeyHash::from_public_key(&pk)) + }; + + ChainManager::from_registry() + .send(QueryStake { key }) + .map(|res| match res { + Ok(Ok(staked_amount)) => serde_json::to_value(staked_amount).map_err(internal_error), + Ok(Err(e)) => { + let err = internal_error_s(e); + Err(err) + } + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + }) + .await +} #[cfg(test)] mod mock_actix { @@ -2124,7 +2322,7 @@ mod tests { let block = block_example(); let inv_elem = InventoryItem::Block(block); let s = serde_json::to_string(&inv_elem).unwrap(); - let expected = r#"{"block":{"block_header":{"signals":0,"beacon":{"checkpoint":0,"hashPrevBlock":"0000000000000000000000000000000000000000000000000000000000000000"},"merkle_roots":{"mint_hash":"0000000000000000000000000000000000000000000000000000000000000000","vt_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","dr_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","commit_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","reveal_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","tally_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000"},"proof":{"proof":{"proof":[],"public_key":{"compressed":0,"bytes":[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,0]}}},"bn256_public_key":null},"block_sig":{"signature":{"Secp256k1":{"der":[]}},"public_key":{"compressed":0,"bytes":[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,0]}},"txns":{"mint":{"epoch":0,"outputs":[]},"value_transfer_txns":[],"data_request_txns":[{"body":{"inputs":[{"output_pointer":"0000000000000000000000000000000000000000000000000000000000000000:0"}],"outputs":[{"pkh":"wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4","value":0,"time_lock":0}],"dr_output":{"data_request":{"time_lock":0,"retrieve":[{"kind":"HTTP-GET","url":"https://openweathermap.org/data/2.5/weather?id=2950159&appid=b6907d289e10d714a6e88b30761fae22"},{"kind":"HTTP-GET","url":"https://openweathermap.org/data/2.5/weather?id=2950159&appid=b6907d289e10d714a6e88b30761fae22"}],"aggregate":{"filters":[],"reducer":0},"tally":{"filters":[],"reducer":0}},"witness_reward":0,"witnesses":0,"commit_and_reveal_fee":0,"min_consensus_percentage":0,"collateral":0}},"signatures":[{"signature":{"Secp256k1":{"der":[]}},"public_key":{"compressed":0,"bytes":[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,0]}}]}],"commit_txns":[],"reveal_txns":[],"tally_txns":[]}}}"#; + let expected = r#"{"block":{"block_header":{"signals":0,"beacon":{"checkpoint":0,"hashPrevBlock":"0000000000000000000000000000000000000000000000000000000000000000"},"merkle_roots":{"mint_hash":"0000000000000000000000000000000000000000000000000000000000000000","vt_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","dr_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","commit_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","reveal_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","tally_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","stake_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","unstake_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000"},"proof":{"proof":{"proof":[],"public_key":{"compressed":0,"bytes":[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,0]}}},"bn256_public_key":null},"block_sig":{"signature":{"Secp256k1":{"der":[]}},"public_key":{"compressed":0,"bytes":[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,0]}},"txns":{"mint":{"epoch":0,"outputs":[]},"value_transfer_txns":[],"data_request_txns":[{"body":{"inputs":[{"output_pointer":"0000000000000000000000000000000000000000000000000000000000000000:0"}],"outputs":[{"pkh":"wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4","value":0,"time_lock":0}],"dr_output":{"data_request":{"time_lock":0,"retrieve":[{"kind":"HTTP-GET","url":"https://openweathermap.org/data/2.5/weather?id=2950159&appid=b6907d289e10d714a6e88b30761fae22"},{"kind":"HTTP-GET","url":"https://openweathermap.org/data/2.5/weather?id=2950159&appid=b6907d289e10d714a6e88b30761fae22"}],"aggregate":{"filters":[],"reducer":0},"tally":{"filters":[],"reducer":0}},"witness_reward":0,"witnesses":0,"commit_and_reveal_fee":0,"min_consensus_percentage":0,"collateral":0}},"signatures":[{"signature":{"Secp256k1":{"der":[]}},"public_key":{"compressed":0,"bytes":[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,0]}}]}],"commit_txns":[],"reveal_txns":[],"tally_txns":[],"stake_txns":[],"unstake_txns":[]}}}"#; assert_eq!(s, expected, "\n{}\n", s); } @@ -2187,7 +2385,7 @@ mod tests { let inputs = vec![value_transfer_input]; Transaction::DataRequest(DRTransaction::new( - DRTransactionBody::new(inputs, vec![], data_request_output), + DRTransactionBody::new(inputs, data_request_output, vec![]), keyed_signatures, )) } @@ -2226,6 +2424,7 @@ mod tests { all_methods_vec, vec![ "addPeers", + "authorizeStake", "chainExport", "chainImport", "clearPeers", @@ -2256,6 +2455,7 @@ mod tests { "sendValue", "sign", "signalingInfo", + "stake", "syncStatus", "tryRequest", "witnet_subscribe", @@ -2274,6 +2474,7 @@ mod tests { let expected_sensitive_methods = vec![ "addPeers", + "authorizeStake", "clearPeers", "createVRF", "getPkh", @@ -2286,6 +2487,7 @@ mod tests { "sendValue", "sign", "tryRequest", + "stake", ]; for method_name in expected_sensitive_methods { diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index d11776e2b..efb18b360 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -21,18 +21,21 @@ use witnet_data_structures::{ priority::PrioritiesEstimate, tapi::{ActiveWips, BitVotesCounter}, Block, CheckpointBeacon, DataRequestInfo, DataRequestOutput, Epoch, EpochConstants, Hash, - InventoryEntry, InventoryItem, NodeStats, PointerToBlock, PublicKeyHash, - PublicKeyHashParseError, RADRequest, RADTally, Reputation, StateMachine, SuperBlock, - SuperBlockVote, SupplyInfo, ValueTransferOutput, + InventoryEntry, InventoryItem, KeyedSignature, NodeStats, PointerToBlock, PublicKeyHash, + PublicKeyHashParseError, RADRequest, RADTally, Reputation, StakeOutput, StateMachine, + SuperBlock, SuperBlockVote, SupplyInfo, ValueTransferOutput, }, fee::{deserialize_fee_backwards_compatible, Fee}, radon_report::RadonReport, + staking::{helpers::StakeKey, stakes::QueryStakesKey}, transaction::{ - CommitTransaction, DRTransaction, RevealTransaction, Transaction, VTTransaction, + CommitTransaction, DRTransaction, RevealTransaction, StakeTransaction, Transaction, + VTTransaction, }, transaction_factory::NodeBalance, types::LastBeacon, utxo_pool::{UtxoInfo, UtxoSelectionStrategy}, + wit::Wit, }; use witnet_p2p::{ error::SessionsError, @@ -220,6 +223,134 @@ impl Message for BuildVtt { type Result = Result; } +/// Builds a `StakeTransaction` from a list of `ValueTransferOutput`s +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct BuildStake { + /// One instance of `StakeOutput` + pub stake_output: StakeOutput, + /// Fee + #[serde(default)] + pub fee: Fee, + /// Strategy to sort the unspent outputs pool + #[serde(default)] + pub utxo_strategy: UtxoSelectionStrategy, + /// Construct the transaction but do not broadcast it + #[serde(default)] + pub dry_run: bool, +} + +impl Message for BuildStake { + type Result = Result; +} + +/// Builds a `StakeTransaction` from a list of `ValueTransferOutput`s +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct BuildStakeParams { + /// Authorization signature and public key + #[serde(default)] + pub authorization: MagicEither, + /// List of `ValueTransferOutput`s + #[serde(default)] + pub value: u64, + /// Withdrawer + #[serde(default)] + pub withdrawer: MagicEither, + /// Fee + #[serde(default)] + pub fee: Fee, + /// Strategy to sort the unspent outputs pool + #[serde(default)] + pub utxo_strategy: UtxoSelectionStrategy, + /// Construct the transaction but do not broadcast it + #[serde(default)] + pub dry_run: bool, +} + +/// The response to a `BuildStake` message. It gives important feedback about the addresses that will be involved in a +/// stake transactions, subject to review and confirmation from the user. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct BuildStakeResponse { + /// A stake transaction that has been created as a response to a `BuildStake` message. + pub transaction: StakeTransaction, + /// The addresses of the staker. These are the addresses used in the stake transaction inputs. + pub staker: Vec, + /// The address of the validator. This shall be the address of the node that will operate this stake on behalf of + /// the staker. + pub validator: PublicKeyHash, + /// The address of the withdrawer. This shall be the an address controlled by the staker. When unstaking, the + /// staked principal plus any yield will only be spendable by this address. + pub withdrawer: PublicKeyHash, +} + +/// Builds an `AuthorizeStake` +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct AuthorizeStake { + /// Address that can withdraw the stake + #[serde(default)] + pub withdrawer: Option, +} + +impl Message for AuthorizeStake { + type Result = Result; +} + +/// Builds an `StakeAuthorization` +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct StakeAuthorization { + /// Address that can withdraw the stake + pub withdrawer: PublicKeyHash, + /// A node's signature of a withdrawer's address + pub signature: KeyedSignature, +} + +impl Message for StakeAuthorization { + type Result = Result; +} + +/// Stake key for quering stakes +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum QueryStakesParams { + /// To search by the validator public key hash + Validator(PublicKeyHash), + /// To search by the withdrawer public key hash + Withdrawer(PublicKeyHash), + /// To search by validator and withdrawer public key hashes + Key((PublicKeyHash, PublicKeyHash)), +} + +impl Default for QueryStakesParams { + fn default() -> Self { + QueryStakesParams::Validator(PublicKeyHash::default()) + } +} + +/// Message for querying stakes +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct QueryStake { + /// stake key used to search the stake + pub key: QueryStakesParams, +} + +impl Message for QueryStake { + type Result = Result; +} + +impl
From for QueryStakesKey
+where + Address: Default + Ord + From, +{ + fn from(query: QueryStakesParams) -> Self { + match query { + QueryStakesParams::Key(key) => QueryStakesKey::Key(StakeKey { + validator: key.0.into(), + withdrawer: key.1.into(), + }), + QueryStakesParams::Validator(v) => QueryStakesKey::Validator(v.into()), + QueryStakesParams::Withdrawer(w) => QueryStakesKey::Withdrawer(w.into()), + } + } +} + /// Builds a `DataRequestTransaction` from a `DataRequestOutput` #[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct BuildDrt { @@ -1280,3 +1411,43 @@ pub struct EstimatePriority; impl Message for EstimatePriority { type Result = Result; } + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +/// A value that can either be L, R, where an R can always be obtained through the `do_magic` method. +pub enum MagicEither { + /// A first variant. + Left(L), + /// A second variant. + Right(R), +} + +impl MagicEither { + /// Obtain an R value, even if this was an instance of L. + pub fn do_magic(self, trick: F) -> R + where + F: Fn(L) -> R, + { + match self { + Self::Left(l) => trick(l), + Self::Right(r) => r, + } + } + + /// Fallible version of `do_magic`. + pub fn try_do_magic(self, trick: F) -> Result + where + F: Fn(L) -> Result, + { + match self { + Self::Left(l) => trick(l), + Self::Right(r) => Ok(r), + } + } +} + +impl Default for MagicEither { + fn default() -> Self { + MagicEither::Right(R::default()) + } +} diff --git a/node/src/actors/session/handlers.rs b/node/src/actors/session/handlers.rs index 9b127ae2f..c7aafa6e8 100644 --- a/node/src/actors/session/handlers.rs +++ b/node/src/actors/session/handlers.rs @@ -14,7 +14,7 @@ use witnet_data_structures::{ Block, CheckpointBeacon, Epoch, Hashable, InventoryEntry, InventoryItem, SuperBlock, SuperBlockVote, }, - proto::ProtobufConvert, + proto::versioning::Versioned, transaction::Transaction, types::{ Address, Command, InventoryAnnouncement, InventoryRequest, LastBeacon, @@ -22,8 +22,8 @@ use witnet_data_structures::{ }, }; use witnet_p2p::sessions::{SessionStatus, SessionType}; +use witnet_util::timestamp::get_timestamp; -use super::Session; use crate::actors::{ chain_manager::ChainManager, inventory_manager::InventoryManager, @@ -39,7 +39,7 @@ use crate::actors::{ sessions_manager::SessionsManager, }; -use witnet_util::timestamp::get_timestamp; +use super::Session; #[derive(Debug, Eq, Fail, PartialEq)] enum HandshakeError { @@ -133,7 +133,7 @@ impl StreamHandler> for Session { } let bytes = res.unwrap(); - let result = WitnetMessage::from_pb_bytes(&bytes); + let result = WitnetMessage::from_versioned_pb_bytes(&bytes); match result { Err(err) => { @@ -1105,9 +1105,10 @@ fn process_superblock_vote(_session: &mut Session, superblock_vote: SuperBlockVo #[cfg(test)] mod tests { - use super::*; use witnet_data_structures::chain::Hash; + use super::*; + #[test] fn handshake_bootstrap_before_epoch_zero() { // Check that when the last beacon has epoch 0 and the current epoch is not 0, diff --git a/node/src/storage_mngr/node_migrations.rs b/node/src/storage_mngr/node_migrations.rs index 2315a98e3..aae34f27c 100644 --- a/node/src/storage_mngr/node_migrations.rs +++ b/node/src/storage_mngr/node_migrations.rs @@ -1,9 +1,12 @@ -use super::*; use witnet_data_structures::{ - chain::{tapi::TapiEngine, ChainState}, + chain::{tapi::TapiEngine, ChainState, Epoch, PublicKeyHash}, + staking::stakes::Stakes, utxo_pool::UtxoWriteBatch, + wit::Wit, }; +use super::*; + macro_rules! as_failure { ($e:expr) => { failure::Error::from_boxed_compat(Box::new($e)) @@ -55,6 +58,22 @@ fn migrate_chain_state_v2_to_v3(chain_state_bytes: &mut [u8]) { chain_state_bytes[0..4].copy_from_slice(&db_version_bytes); } +fn migrate_chain_state_v3_to_v4(old_chain_state_bytes: &[u8]) -> Vec { + let db_version: u32 = 4; + let db_version_bytes = db_version.to_le_bytes(); + + // Extra fields in ChainState v4: + let stakes = Stakes::::default(); + let stakes_bytes = bincode::serialize(&stakes).unwrap(); + + [ + &db_version_bytes, + &old_chain_state_bytes[4..], + &stakes_bytes, + ] + .concat() +} + fn migrate_chain_state(mut bytes: Vec) -> Result { loop { match check_chain_state_version(&bytes) { @@ -73,6 +92,11 @@ fn migrate_chain_state(mut bytes: Vec) -> Result log::debug!("Successfully migrated ChainState v2 to v3"); } Ok(3) => { + // Migrate from v3 to v4 + bytes = migrate_chain_state_v3_to_v4(&bytes); + log::debug!("Successfully migrated ChainState v3 to v4"); + } + Ok(4) => { // Latest version // Skip the first 4 bytes because they are used to encode db_version return match deserialize(&bytes[4..]) { @@ -193,10 +217,12 @@ where #[cfg(test)] mod tests { - use super::*; use serde::{Deserialize, Serialize}; + use witnet_data_structures::chain::ChainInfo; + use super::*; + #[test] fn bincode_version() { #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] diff --git a/node/tests/data_request_examples.rs b/node/tests/data_request_examples.rs index 2a7eacde1..44b9b6a9e 100644 --- a/node/tests/data_request_examples.rs +++ b/node/tests/data_request_examples.rs @@ -1,8 +1,4 @@ -use std::{ - collections::HashMap, - convert::{TryFrom, TryInto}, - fs, -}; +use std::{collections::HashMap, convert::TryFrom, fs}; use serde::{Deserialize, Serialize}; @@ -84,11 +80,10 @@ fn run_dr_locally_with_data( log::info!("Aggregation result: {:?}", aggregation_result); // Assume that all the required witnesses will report the same value - let reported_values: Result, _> = - vec![aggregation_result; dr.witnesses.try_into().unwrap()] - .into_iter() - .map(RadonTypes::try_from) - .collect(); + let reported_values: Result, _> = vec![aggregation_result; dr.witnesses.into()] + .into_iter() + .map(RadonTypes::try_from) + .collect(); log::info!("Running tally with values {:?}", reported_values); let tally_result = witnet_rad::run_tally(reported_values?, &dr.data_request.tally, &all_wips_active())?; diff --git a/rad/src/lib.rs b/rad/src/lib.rs index 64c826780..3c3b836d0 100644 --- a/rad/src/lib.rs +++ b/rad/src/lib.rs @@ -25,10 +25,11 @@ use crate::{ create_radon_script_from_filters_and_reducer, execute_radon_script, unpack_radon_script, RadonScriptExecutionSettings, }, - types::{array::RadonArray, bytes::RadonBytes, string::RadonString, RadonTypes}, + types::{array::RadonArray, bytes::RadonBytes, map::RadonMap, string::RadonString, RadonTypes}, user_agents::UserAgent, }; use core::convert::From; +use std::collections::BTreeMap; use witnet_net::client::http::{WitnetHttpBody, WitnetHttpRequest}; pub mod conditions; @@ -173,6 +174,31 @@ fn string_response_with_data_report( execute_radon_script(input, &radon_script, context, settings) } +/// Handle HTTP-HEAD response with data, and return a `RadonReport`. +fn headers_response_with_data_report( + retrieve: &RADRetrieve, + response: &str, + context: &mut ReportContext, + settings: RadonScriptExecutionSettings, +) -> Result> { + let headers: BTreeMap = response + .split("\r\n") + .map(|line| { + let parts: Vec<&str> = line.split(':').map(|part| part.trim()).collect(); + // todo: check there are two parts, and two parts only + // todo: make sure that values from repeated keys get appended within a RadonArray + ( + String::from(parts[0]), + RadonTypes::from(RadonString::from(parts[1])), + ) + }) + .collect(); + let input = RadonTypes::from(RadonMap::from(headers)); + let radon_script = unpack_radon_script(&retrieve.script)?; + + execute_radon_script(input, &radon_script, context, settings) +} + /// Handle Rng response with data report fn rng_response_with_data_report( response: &str, @@ -197,6 +223,9 @@ pub fn run_retrieval_with_data_report( RADType::HttpPost => { string_response_with_data_report(retrieve, response, context, settings) } + RADType::HttpHead => { + headers_response_with_data_report(retrieve, response, context, settings) + } _ => Err(RadError::UnknownRetrieval), } } @@ -214,7 +243,7 @@ pub fn run_retrieval_with_data( .map(RadonReport::into_inner) } -/// Handle generic HTTP (GET/POST) response +/// Handle generic HTTP (GET/POST/HEAD) response async fn http_response( retrieve: &RADRetrieve, context: &mut ReportContext, @@ -259,6 +288,10 @@ async fn http_response( WitnetHttpBody::from(retrieve.body.clone()), ) } + RADType::HttpHead => ( + builder.method("HEAD").uri(&retrieve.url), + WitnetHttpBody::empty(), + ), _ => panic!( "Called http_response with invalid retrieval kind {:?}", retrieve.kind @@ -357,6 +390,7 @@ pub async fn run_retrieval_report( RADType::HttpGet => http_response(retrieve, context, settings, client).await, RADType::Rng => rng_response(context, settings).await, RADType::HttpPost => http_response(retrieve, context, settings, client).await, + RADType::HttpHead => http_response(retrieve, context, settings, client).await, _ => Err(RadError::UnknownRetrieval), } } diff --git a/rad/src/operators/array.rs b/rad/src/operators/array.rs index a4136d9f8..80fbc22ab 100644 --- a/rad/src/operators/array.rs +++ b/rad/src/operators/array.rs @@ -64,7 +64,7 @@ fn inner_get(input: &RadonArray, args: &[Value]) -> Result input .value() .get(index) - .map(Clone::clone) + .cloned() .ok_or_else(|| not_found(index)) } diff --git a/rad/src/operators/map.rs b/rad/src/operators/map.rs index c81da9eef..d02d1dc8c 100644 --- a/rad/src/operators/map.rs +++ b/rad/src/operators/map.rs @@ -22,7 +22,7 @@ fn inner_get(input: &RadonMap, args: &[Value]) -> Result { input .value() .get(&key) - .map(Clone::clone) + .cloned() .ok_or_else(|| not_found(key)) } @@ -116,7 +116,7 @@ pub mod legacy { #[cfg(test)] mod tests { - use std::{collections::BTreeMap, convert::TryFrom}; + use std::collections::BTreeMap; use crate::{ operators::{Operable, RadonOpCodes}, @@ -131,7 +131,7 @@ mod tests { fn test_map_get() { let key = "Zero"; let value = RadonTypes::Integer(RadonInteger::from(0)); - let args = vec![Value::try_from(String::from(key)).unwrap()]; + let args = vec![Value::from(String::from(key))]; let mut map = BTreeMap::new(); map.insert(key.to_string(), value.clone()); diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index 64b1b04e0..9a56022a6 100644 --- a/schemas/witnet/witnet.proto +++ b/schemas/witnet/witnet.proto @@ -2,6 +2,28 @@ syntax = "proto3"; package witnet; +message LegacyMessage { + message LegacyCommand { + oneof kind { + Version Version = 1; + Verack Verack = 2; + GetPeers GetPeers = 3; + Peers Peers = 4; + LegacyBlock Block = 5; + InventoryAnnouncement InventoryAnnouncement = 6; + InventoryRequest InventoryRequest = 7; + LastBeacon LastBeacon = 8; + Transaction Transaction = 9; + SuperBlockVote SuperBlockVote = 10; + SuperBlock SuperBlock = 11; + } + } + + // uint32 is not a fixed-size 32 bit integer: it uses variable length encoding + uint32 magic = 1; + LegacyCommand kind = 2; +} + message Message { message Command { oneof kind { @@ -47,10 +69,42 @@ message Peers { repeated Address peers = 1; } -message Block { - message BlockEligibilityClaim { - VrfProof proof = 1; +message BlockEligibilityClaim { + VrfProof proof = 1; +} + +message LegacyBlock { + message LegacyBlockHeader { + message LegacyBlockMerkleRoots { + Hash mint_hash = 1; + Hash vt_hash_merkle_root = 2; + Hash dr_hash_merkle_root = 3; + Hash commit_hash_merkle_root = 4; + Hash reveal_hash_merkle_root = 5; + Hash tally_hash_merkle_root = 6; + } + + uint32 signals = 1; + CheckpointBeacon beacon = 2; + LegacyBlockMerkleRoots merkle_roots = 3; + BlockEligibilityClaim proof = 4; + Bn256PublicKey bn256_public_key = 5; } + message LegacyBlockTransactions { + MintTransaction mint = 1; + repeated VTTransaction value_transfer_txns = 2; + repeated DRTransaction data_request_txns = 3; + repeated CommitTransaction commit_txns = 4; + repeated RevealTransaction reveal_txns = 5; + repeated TallyTransaction tally_txns = 6; + } + + LegacyBlockHeader block_header = 1; + KeyedSignature block_sig = 2; + LegacyBlockTransactions txns = 3; +} + +message Block { message BlockHeader { message BlockMerkleRoots { Hash mint_hash = 1; @@ -59,6 +113,8 @@ message Block { Hash commit_hash_merkle_root = 4; Hash reveal_hash_merkle_root = 5; Hash tally_hash_merkle_root = 6; + Hash stake_hash_merkle_root = 7; + Hash unstake_hash_merkle_root = 8; } uint32 signals = 1; CheckpointBeacon beacon = 2; @@ -73,6 +129,8 @@ message Block { repeated CommitTransaction commit_txns = 4; repeated RevealTransaction reveal_txns = 5; repeated TallyTransaction tally_txns = 6; + repeated StakeTransaction stake_txns = 7; + repeated UnstakeTransaction unstake_txns = 8; } BlockHeader block_header = 1; @@ -121,6 +179,7 @@ message DataRequestOutput { HttpGet = 1; Rng = 2; HttpPost = 3; + HttpHead = 4; } message RADFilter { uint32 op = 1; @@ -133,7 +192,7 @@ message DataRequestOutput { bytes script = 3; // Body of HTTP-POST request bytes body = 4; - // Extra headers for HTTP-GET and HTTP-POST requests + // Extra headers for HTTP-GET, HTTP-HEAD and HTTP-POST requests repeated StringPair headers = 5; } message RADAggregate { @@ -229,6 +288,39 @@ message MintTransaction { repeated ValueTransferOutput outputs = 2; } +message StakeKey { + PublicKeyHash validator = 1; + PublicKeyHash withdrawer = 2; +} + +message StakeOutput { + uint64 value = 1; + StakeKey key = 2; + KeyedSignature authorization = 3; +} + +message StakeTransactionBody { + repeated Input inputs = 1; + StakeOutput output = 2; + ValueTransferOutput change = 3; +} + +message StakeTransaction { + StakeTransactionBody body = 1 ; + repeated KeyedSignature signatures = 2; +} + +message UnstakeTransactionBody { + PublicKeyHash validator = 1; + ValueTransferOutput withdrawal = 2; + ValueTransferOutput change = 3; +} + +message UnstakeTransaction { + UnstakeTransactionBody body = 1 ; + KeyedSignature signature = 2; +} + message Transaction { oneof kind { VTTransaction ValueTransfer = 1; @@ -237,6 +329,8 @@ message Transaction { RevealTransaction Reveal = 4; TallyTransaction Tally = 5; MintTransaction Mint = 6; + StakeTransaction Stake = 7; + UnstakeTransaction Unstake = 8; } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index bf8ebd663..a48344572 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -6,6 +6,7 @@ use terminal_size as term; use env_logger::TimestampPrecision; use witnet_config as config; +use witnet_data_structures::register_protocol_version; mod node; mod wallet; @@ -56,6 +57,13 @@ pub fn exec( let _guard = init_logger(log_opts); witnet_data_structures::set_environment(config.environment); + log::debug!("{:#?}", config); + for (version, epoch) in config.protocol.iter() { + if let Some(epoch) = epoch { + register_protocol_version(version, epoch); + } + } + exec_cmd(cmd, config_path, config) } diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index c1a9ee6c9..f82db9ea3 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{BTreeSet, HashMap, HashSet}, convert::TryFrom, fmt, fs::File, @@ -14,7 +14,9 @@ use failure::{bail, Fail}; use itertools::Itertools; use num_format::{Locale, ToFormattedString}; use prettytable::{row, Table}; +use qrcode::render::unicode; use serde::{de::DeserializeOwned, Deserialize, Serialize}; + use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; use witnet_crypto::{ hash::calculate_sha256, @@ -29,8 +31,9 @@ use witnet_data_structures::{ SupplyInfo, SyncStatus, ValueTransferOutput, }, fee::Fee, + get_environment, proto::ProtobufConvert, - transaction::{DRTransaction, Transaction, VTTransaction}, + transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::NodeBalance, types::SequentialId, utxo_pool::{UtxoInfo, UtxoSelectionStrategy}, @@ -38,8 +41,13 @@ use witnet_data_structures::{ }; use witnet_node::actors::{ chain_manager::run_dr_locally, - json_rpc::api::{AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult}, - messages::{BuildDrt, BuildVtt, GetBalanceTarget, GetReputationResult, SignalingInfo}, + json_rpc::api::{ + AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult, QueryStakesArgument, + }, + messages::{ + AuthorizeStake, BuildDrt, BuildStakeParams, BuildStakeResponse, BuildVtt, GetBalanceTarget, + GetReputationResult, MagicEither, SignalingInfo, StakeAuthorization, + }, }; use witnet_rad::types::RadonTypes; use witnet_util::{files::create_private_file, timestamp::pretty_print}; @@ -855,6 +863,195 @@ pub fn send_dr( Ok(()) } +#[allow(clippy::too_many_arguments)] +pub fn send_st( + addr: SocketAddr, + value: u64, + authorization: MagicEither, + validator: MagicEither, + withdrawer: MagicEither, + fee: Option, + sorted_bigger: Option, + requires_confirmation: Option, + dry_run: bool, +) -> Result<(), failure::Error> { + let mut stream = start_client(addr)?; + let mut id = SequentialId::initialize(1u8); + + // Prepare for fee estimation if no fee value was specified + let (fee, estimate) = unwrap_fee_or_estimate_priority(fee, &mut stream, &mut id)?; + + let utxo_strategy = match sorted_bigger { + Some(true) => UtxoSelectionStrategy::BigFirst { from: None }, + Some(false) => UtxoSelectionStrategy::SmallFirst { from: None }, + None => UtxoSelectionStrategy::Random { from: None }, + }; + + let mut build_stake_params = BuildStakeParams { + authorization, + withdrawer, + value, + fee, + utxo_strategy, + dry_run, + }; + + // If no fee was specified, we first need to do a dry run for each of the priority tiers to + // find out the actual transaction weight (as different priorities will affect the number + // of inputs being used, and thus also the weight). + if let Some(PrioritiesEstimate { + vtt_stinky, + vtt_low, + vtt_medium, + vtt_high, + vtt_opulent, + .. + }) = estimate + { + let priorities = vec![ + (vtt_stinky, "Stinky"), + (vtt_low, "Low"), + (vtt_medium, "Medium"), + (vtt_high, "High"), + (vtt_opulent, "Opulent"), + ]; + let mut estimates = vec![]; + let mut fee; + + // Iterative algorithm for transaction weight discovery. It calculates the fees for this + // transaction assuming that it has the minimum weight, and then repeats the estimation + // using the actual weight of the latest created transaction, until the weight stabilizes + // or after 5 rounds. + for ( + PriorityEstimate { + priority, + time_to_block, + }, + label, + ) in priorities + { + // The minimum ST size is N*133+M*36+105` where `N` is the number of `inputs`, and `M` + // is 0 or 1 depending on whether a `change` output is used + let mut weight = 238u32; + let mut rounds = 0u8; + // Iterative algorithm for weight discovery + loop { + // Calculate fee for current priority and weight + fee = Fee::absolute_from_wit(priority.derive_fee_wit(weight)); + + // Create and dry run a Stake transaction using that fee + let dry_params = BuildStakeParams { + fee, + dry_run: true, + ..build_stake_params.clone() + }; + let (bsr, ..): (BuildStakeResponse, _) = + issue_method("stake", Some(dry_params), &mut stream, id.next())?; + let dry_weight = bsr.transaction.weight(); + + // We retry up to 5 times, or until the weight is stable + if rounds > 5 || dry_weight == weight { + break; + } + + weight = dry_weight; + rounds += 1; + } + + estimates.push((label, priority, fee, time_to_block)); + } + + // We are ready to compose the params for the actual transaction. + build_stake_params.fee = prompt_user_for_priority_selection(estimates)?; + } + + let params = BuildStakeParams { + dry_run: true, + ..build_stake_params.clone() + }; + let (dry, _): (BuildStakeResponse, _) = + issue_method("stake", Some(params), &mut stream, id.next())?; + + let validator_address = validator + .try_do_magic(|hex_str| PublicKeyHash::from_bech32(get_environment(), &hex_str))?; + if validator_address != dry.validator { + bail!( + "The specified validator ({}) does not match the validator recovered from the authorization string ({}), please double check all arguments.", + validator_address, + dry.validator.to_string(), + ); + } + + let confirmation = if requires_confirmation.unwrap_or(true) { + // Exactly what it says: shows all the facts about the staking transaction, and expects confirmation through + // user input + if prompt_user_for_stake_confirmation(&dry)? { + Some(dry) + } else { + None + } + } else { + Some(dry) + }; + + if let Some(dry) = confirmation { + // Finally ask the node to create the transaction with the chosen fee. + build_stake_params.dry_run = dry_run; + let (st, (request, response)): (StakeTransaction, _) = + issue_method("stake", Some(build_stake_params), &mut stream, id.next())?; + + println!("> {}", request); + println!("< {}", response); + + let environment = get_environment(); + let value = Wit::from_nanowits(st.body.output.value).to_string(); + let staker = dry + .staker + .iter() + .map(|pkh| pkh.bech32(environment)) + .collect::>() + .iter() + .join(","); + let validator = dry.validator.bech32(environment); + let withdrawer = dry.withdrawer.bech32(environment); + + println!("Congratulations! {} Wit have been staked by addresses {:?} onto validator {}, using {} as the withdrawal address.", value, staker, validator, withdrawer); + } else { + println!("The stake facts have not been confirmed. No stake transaction has been created."); + } + + Ok(()) +} + +pub fn authorize_st(addr: SocketAddr, withdrawer: Option) -> Result<(), failure::Error> { + let mut stream = start_client(addr)?; + let mut id = SequentialId::initialize(1u8); + + let params = AuthorizeStake { withdrawer }; + let (authorization, (_, _response)): (StakeAuthorization, _) = + issue_method("authorizeStake", Some(params), &mut stream, id.next())?; + + let message = authorization.withdrawer.as_secp256k1_msg(); + + let auth_bytes = authorization.signature.to_recoverable_bytes(&message)?; + let auth_string = hex::encode(auth_bytes); + + let auth_qr = qrcode::QrCode::new(&auth_string)?; + let auth_ascii = auth_qr + .render::() + .quiet_zone(true) + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + + println!( + "Authorization code:\n{}\nQR code for myWitWallet:\n{}", + auth_string, auth_ascii + ); + + Ok(()) +} + pub fn master_key_export( addr: SocketAddr, write_to_path: Option<&Path>, @@ -1642,6 +1839,34 @@ pub fn priority(addr: SocketAddr, json: bool) -> Result<(), failure::Error> { Ok(()) } +pub fn query_stakes( + addr: SocketAddr, + validator: Option, + withdrawer: Option, +) -> Result<(), failure::Error> { + let mut stream = start_client(addr)?; + + let params = match (validator, withdrawer) { + (Some(validator), Some(withdrawer)) => { + Some(QueryStakesArgument::Key((validator, withdrawer))) + } + (Some(validator), _) => Some(QueryStakesArgument::Validator(validator)), + (_, Some(withdrawer)) => Some(QueryStakesArgument::Withdrawer(withdrawer)), + (None, None) => None, + }; + + let response = send_request( + &mut stream, + &format!( + r#"{{"jsonrpc": "2.0","method": "queryStakes", "params": {}, "id": 1}}"#, + serde_json::to_string(¶ms).unwrap() + ), + )?; + log::info!("{}", response); + + Ok(()) +} + #[derive(Serialize, Deserialize)] struct SignatureWithData { address: String, @@ -1728,10 +1953,10 @@ struct JsonRpcError { /// Id. Can be null, a number, or a string #[derive(Debug, Deserialize)] #[serde(untagged)] -enum Id<'a> { +enum Id { Null, - Number(u64), - String(&'a str), + Number(), + String(), } /// A failed request returns an error with code and message @@ -1849,7 +2074,6 @@ where id.unwrap_or(1) ); let response = send_request(stream, &request)?; - parse_response::(&response).map(|output| (output, (request, response))) } @@ -1900,6 +2124,84 @@ fn prompt_user_for_priority_selection( Ok(fee) } +fn prompt_user_for_stake_confirmation(data: &BuildStakeResponse) -> Result { + let environment = get_environment(); + let value = Wit::from_nanowits(data.transaction.body.output.value).to_string(); + + // Time to print the data + println!("╔══════════════════════════════════════════════════════════════════════════════╗"); + println!("║ PLEASE CAREFULLY REVIEW THE DATA BELOW ║"); + println!("╟──────────────────────────────────────────────────────────────────────────────╢"); + println!("║ Failing to review this information diligently may result in stakes that ║"); + println!("║ cannot be operated or withdrawn, i.e. loss of funds. ║"); + println!("╠══════════════════════════════════════════════════════════════════════════════╣"); + println!("║ 1. STAKER ADDRESSES ║"); + println!("║ These are the addresses from which the coins to stake will be sourced. ║"); + println!("║ None of these addresses will be able to unstake or spend the staked ║"); + println!("║ coins, unless one of them is also the withdrawer address below. ║"); + println!("║ ║"); + for (i, address) in data + .staker + .iter() + .collect::>() + .into_iter() + .enumerate() + { + let address = address.bech32(environment); + println!("║ #{:0>2}: {: <69}║", i, address); + } + println!("╟──────────────────────────────────────────────────────────────────────────────╢"); + println!("║ 2. VALIDATOR ADDRESS ║"); + println!("║ This is the address of the node that will be operating the staked coins. ║"); + println!("║ The validator will not be able to unstake or spend the staked coins — ║"); + println!("║ that role is reserved for the withdrawer address below. ║"); + println!("║ ║"); + println!( + "║ Validator address: {: <55}║", + data.validator.bech32(environment) + ); + println!("╟──────────────────────────────────────────────────────────────────────────────╢"); + println!("║ 3. WITHDRAWER ADDRESS ║"); + println!("║ This is the only address that will be allowed to unstake and eventually ║"); + println!("║ spend the staked coins, and the accumulated rewards if any. ║"); + println!("║ This MUST belong to your wallet, otherwise you may be giving away or ║"); + println!("║ or burning your coins. ║"); + println!("║ ║"); + println!( + "║ Withdrawer address: {: <54}║", + data.withdrawer.bech32(environment) + ); + println!("╟──────────────────────────────────────────────────────────────────────────────╢"); + println!("║ 4. STAKE AMOUNT ║"); + println!("║ This is the number of coins that will be staked. While staked, the coins ║"); + println!("║ cannot be transferred or spent. They can only be unstaked and eventually ║"); + println!("║ spent by the withdrawer address above. ║"); + println!("║ ║"); + println!("║ Stake amount: {} {: <44}║", value, "Wit coins"); + println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + + // This is where we prompt the user for typing the desired priority tier from the options + // printed above. This is done in a loop until a valid option is selected. + let mut input = String::new(); + let stdin = io::stdin(); + let mut stdin = stdin.lock(); + loop { + print!("Please double-check the information above and confirm if it is correct (y/N): ",); + io::stdout().flush()?; + input.clear(); + stdin.read_line(&mut input)?; + let selected = input.trim().to_uppercase(); + + if ["Y", "YES"].contains(&selected.as_str()) { + return Ok(true); + } else if ["", "N", "NO"].contains(&selected.as_str()) { + break; + } + } + + Ok(false) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/cli/node/with_node.rs b/src/cli/node/with_node.rs index 41321f29c..315c80aef 100644 --- a/src/cli/node/with_node.rs +++ b/src/cli/node/with_node.rs @@ -10,7 +10,7 @@ use structopt::StructOpt; use witnet_config::config::Config; use witnet_data_structures::{chain::Epoch, fee::Fee}; use witnet_node as node; -use witnet_node::actors::messages::GetBalanceTarget; +use witnet_node::actors::messages::{GetBalanceTarget, MagicEither}; use super::json_rpc_client as rpc; @@ -269,6 +269,34 @@ pub fn exec_cmd( Command::Rewind { node, epoch } => rpc::rewind(node.unwrap_or(default_jsonrpc), epoch), Command::SignalingInfo { node } => rpc::signaling_info(node.unwrap_or(default_jsonrpc)), Command::Priority { node, json } => rpc::priority(node.unwrap_or(default_jsonrpc), json), + Command::Stake { + node, + value, + authorization, + validator, + withdrawer, + fee, + require_confirmation, + dry_run, + } => rpc::send_st( + node.unwrap_or(default_jsonrpc), + value, + MagicEither::Left(authorization), + MagicEither::Left(validator), + MagicEither::Left(withdrawer), + fee.map(Fee::absolute_from_nanowits), + None, + require_confirmation, + dry_run, + ), + Command::AuthorizeStake { node, withdrawer } => { + rpc::authorize_st(node.unwrap_or(default_jsonrpc), withdrawer) + } + Command::QueryStakes { + node, + validator, + withdrawer, + } => rpc::query_stakes(node.unwrap_or(default_jsonrpc), validator, withdrawer), } } @@ -730,6 +758,52 @@ pub enum Command { #[structopt(long = "json", help = "Show output in JSON format")] json: bool, }, + #[structopt(name = "stake", about = "Create a stake transaction")] + Stake { + /// Socket address of the Witnet node to query + #[structopt(short = "n", long = "node")] + node: Option, + /// Value + #[structopt(long = "value")] + value: u64, + /// Stake authorization code (the withdrawer address, signed by the validator node) + #[structopt(long = "authorization")] + authorization: String, + /// Validator + #[structopt(long = "validator")] + validator: String, + /// Withdrawer + #[structopt(long = "withdrawer")] + withdrawer: String, + /// Fee + #[structopt(long = "fee")] + fee: Option, + /// If unset or set to true, the command is interactive and prompts for user confirmation. + /// If set to false, skip confirmation and complete the command without user confirmation. + #[structopt(long = "require_confirmation")] + require_confirmation: Option, + /// Print the request that would be sent to the node and exit without doing anything + #[structopt(long = "dry-run")] + dry_run: bool, + }, + #[structopt(name = "authorizeStake", about = "Create an stake authorization")] + AuthorizeStake { + /// Socket address of the Witnet node to query + #[structopt(short = "n", long = "node")] + node: Option, + /// Withdrawer address + #[structopt(long = "withdrawer")] + withdrawer: Option, + }, + QueryStakes { + /// Socket address of the Witnet node to query + #[structopt(short = "n", long = "node")] + node: Option, + #[structopt(short = "v", long = "validator")] + validator: Option, + #[structopt(short = "w", long = "withdrawer")] + withdrawer: Option, + }, } #[derive(Debug, StructOpt)] diff --git a/toolkit/Cargo.toml b/toolkit/Cargo.toml index e6dce5291..7ac7b72d4 100644 --- a/toolkit/Cargo.toml +++ b/toolkit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "witnet_toolkit" -version = "1.7.1" +version = "2.0.0" authors = ["Adán SDPC "] edition = "2021" diff --git a/validations/Cargo.toml b/validations/Cargo.toml index eb00b2011..2178fdf7a 100644 --- a/validations/Cargo.toml +++ b/validations/Cargo.toml @@ -8,7 +8,7 @@ workspace = ".." [dependencies] failure = "0.1.8" -itertools = "0.8.2" +itertools = "0.11.0" log = "0.4.8" url = "2.2.2" @@ -16,6 +16,7 @@ witnet_config = { path = "../config" } witnet_crypto = { path = "../crypto" } witnet_data_structures = { path = "../data_structures" } witnet_rad = { path = "../rad" } +num-traits = "0.2.18" [dev-dependencies] approx = "0.5.0" diff --git a/validations/src/eligibility/current.rs b/validations/src/eligibility/current.rs new file mode 100644 index 000000000..b4c48f247 --- /dev/null +++ b/validations/src/eligibility/current.rs @@ -0,0 +1,314 @@ +use std::{ + fmt::{Debug, Display}, + ops::{Add, Div, Mul, Sub}, +}; +use witnet_data_structures::staking::prelude::*; + +const MINING_REPLICATION_FACTOR: usize = 4; +const WITNESSING_MAX_ROUNDS: usize = 4; + +/// Different reasons for ineligibility of a validator, stake entry or eligibility proof. +#[derive(Copy, Debug, Clone, PartialEq)] +pub enum IneligibilityReason { + /// The stake entry has no power enough to perform such action. + InsufficientPower, + /// No matching stake entry has been found. + NotStaking, +} + +/// Signals whether a validator, stake entry or eligibility proof is eligible or not, and in the negative case, it also +/// provides a reason for ineligibility. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Eligible { + /// It is eligible. + Yes, + /// It is not eligible (provides a reason). + No(IneligibilityReason), +} + +impl From for Eligible { + #[inline] + fn from(reason: IneligibilityReason) -> Self { + Eligible::No(reason) + } +} + +/// Trait providing eligibility calculation for multiple protocol capabilities. +pub trait Eligibility +where + Address: Debug + Display + Sync + Send + 'static, + Coins: Debug + Display + Sync + Send + 'static, + Epoch: Debug + Display + Sync + Send + 'static, +{ + /// Tells whether a VRF proof meets the requirements to become eligible for mining. Unless an error occurs, returns + /// an `Eligibility` structure signaling eligibility or lack thereof (in which case you also get an + /// `IneligibilityReason`. + fn mining_eligibility( + &self, + key: ISK, + epoch: Epoch, + ) -> StakesResult + where + ISK: Into>; + + /// Tells whether a VRF proof meets the requirements to become eligible for mining. Because this function returns a + /// simple `bool`, it is best-effort: both lack of eligibility and any error cases are mapped to `false`. + fn mining_eligibility_bool(&self, key: ISK, epoch: Epoch) -> bool + where + ISK: Into>, + { + matches!(self.mining_eligibility(key, epoch), Ok(Eligible::Yes)) + } + + /// Tells whether a VRF proof meets the requirements to become eligible for witnessing. Unless an error occurs, + /// returns an `Eligibility` structure signaling eligibility or lack thereof (in which case you also get an + /// `IneligibilityReason`. + fn witnessing_eligibility( + &self, + key: ISK, + epoch: Epoch, + witnesses: u8, + round: u8, + ) -> StakesResult + where + ISK: Into>; + + /// Tells whether a VRF proof meets the requirements to become eligible for witnessing. Because this function + /// returns a simple `bool`, it is best-effort: both lack of eligibility and any error cases are mapped to `false`. + fn witnessing_eligibility_bool( + &self, + key: ISK, + epoch: Epoch, + witnesses: u8, + round: u8, + ) -> bool + where + ISK: Into>, + { + matches!( + self.witnessing_eligibility(key, epoch, witnesses, round), + Ok(Eligible::Yes) + ) + } +} + +impl Eligibility + for Stakes +where + Address: Clone + Debug + Default + Display + Ord + Sync + Send + 'static, + Coins: Copy + + Debug + + Default + + Display + + Ord + + From + + Into + + num_traits::Zero + + Add + + Sub + + Mul + + Mul + + Sync + + Send + + 'static, + Epoch: Copy + + Debug + + Default + + Display + + num_traits::Saturating + + Sub + + From + + Sync + + Send + + 'static, + Power: Copy + + Default + + Ord + + Add + + Sub + + Mul + + Div + + From, + u64: From + From, +{ + fn mining_eligibility( + &self, + key: ISK, + epoch: Epoch, + ) -> StakesResult + where + ISK: Into>, + { + let power = match self.query_power(key, Capability::Mining, epoch) { + Ok(p) => p, + Err(e) => { + // Early exit if the stake key does not exist + return match e { + StakesError::EntryNotFound { .. } => Ok(IneligibilityReason::NotStaking.into()), + e => Err(e), + }; + } + }; + + let mut rank = self.rank(Capability::Mining, epoch); + + // Requirement no. 3 from the WIP: + // "the mining power of the block proposer is greater than `max_power / rf`" + // (This goes before no. 2 because it is cheaper, computation-wise, and we want validations to exit ASAP) + // TODO: verify if defaulting to 0 makes sense + let (_, max_power) = rank.next().unwrap_or_default(); + let threshold = max_power / Power::from(MINING_REPLICATION_FACTOR as u64); + if power <= threshold { + return Ok(IneligibilityReason::InsufficientPower.into()); + } + + // Requirement no. 2 from the WIP: + // "the mining power of the block proposer is in the `rf / stakers`th quantile among the mining powers of all + // the stakers" + let stakers = self.stakes_count(); + let quantile = stakers / MINING_REPLICATION_FACTOR; + // TODO: verify if defaulting to 0 makes sense + let (_, threshold) = rank.nth(quantile).unwrap_or_default(); + if power <= threshold { + return Ok(IneligibilityReason::InsufficientPower.into()); + } + + // If all the requirements are met, we can deem it as eligible + Ok(Eligible::Yes) + } + + fn witnessing_eligibility( + &self, + key: ISK, + epoch: Epoch, + witnesses: u8, + round: u8, + ) -> StakesResult + where + ISK: Into>, + { + let power = match self.query_power(key, Capability::Witnessing, epoch) { + Ok(p) => p, + Err(e) => { + // Early exit if the stake key does not exist + return match e { + StakesError::EntryNotFound { .. } => Ok(IneligibilityReason::NotStaking.into()), + e => Err(e), + }; + } + }; + + let mut rank = self.rank(Capability::Mining, epoch); + let rf = 2usize.pow(round as u32) * witnesses as usize; + + // Requirement no. 2 from the WIP: + // "the witnessing power of the block proposer is in the `rf / stakers`th quantile among the witnessing powers + // of all the stakers" + let stakers = self.stakes_count(); + let quantile = stakers / MINING_REPLICATION_FACTOR; + // TODO: verify if defaulting to 0 makes sense + let (_, threshold) = rank.nth(quantile).unwrap_or_default(); + if power <= threshold { + return Ok(IneligibilityReason::InsufficientPower.into()); + } + + // Requirement no. 3 from the WIP: + // "the big-endian value of the VRF output is less than + // `max_rounds * own_power / (max_power * (rf - max_rounds) - rf * threshold_power)`" + // TODO: verify if defaulting to 0 makes sense + let (_, max_power) = rank.next().unwrap_or_default(); + let stakers = self.stakes_count(); + let quantile = stakers / rf; + // TODO: verify if defaulting to 0 makes sense + let (_, threshold_power) = rank.nth(quantile).unwrap_or_default(); + let dividend = Power::from(WITNESSING_MAX_ROUNDS as u64) * power; + let divisor = max_power * Power::from((rf - WITNESSING_MAX_ROUNDS) as u64) + - Power::from(rf as u64) * threshold_power; + let threshold = dividend / divisor; + println!("{}", u64::from(power)); + println!("{}", u64::from(threshold)); + if power <= threshold { + return Ok(IneligibilityReason::InsufficientPower.into()); + } + + Ok(Eligible::Yes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mining_eligibility_no_stakers() { + let stakes = >::with_minimum(100u64); + let isk = ("validator", "withdrawer"); + + assert_eq!( + stakes.mining_eligibility(isk, 0), + Ok(Eligible::No(IneligibilityReason::NotStaking)) + ); + assert_eq!(stakes.mining_eligibility_bool(isk, 0), false); + + assert_eq!( + stakes.mining_eligibility(isk, 100), + Ok(Eligible::No(IneligibilityReason::NotStaking)) + ); + assert_eq!(stakes.mining_eligibility_bool(isk, 100), false); + } + + #[test] + fn test_mining_eligibility_absolute_power() { + let mut stakes = >::with_minimum(100u64); + let isk = ("validator", "withdrawer"); + + stakes.add_stake(isk, 1_000, 0).unwrap(); + + assert_eq!( + stakes.mining_eligibility(isk, 0), + Ok(Eligible::No(IneligibilityReason::InsufficientPower)) + ); + assert_eq!(stakes.mining_eligibility_bool(isk, 0), false); + + assert_eq!(stakes.mining_eligibility(isk, 100), Ok(Eligible::Yes)); + assert_eq!(stakes.mining_eligibility_bool(isk, 100), true); + } + + #[test] + fn test_witnessing_eligibility_no_stakers() { + let stakes = >::with_minimum(100u64); + let isk = ("validator", "withdrawer"); + + assert_eq!( + stakes.witnessing_eligibility(isk, 0, 10, 0), + Ok(Eligible::No(IneligibilityReason::NotStaking)) + ); + assert_eq!(stakes.witnessing_eligibility_bool(isk, 0, 10, 0), false); + + assert_eq!( + stakes.witnessing_eligibility(isk, 100, 10, 0), + Ok(Eligible::No(IneligibilityReason::NotStaking)) + ); + assert_eq!(stakes.witnessing_eligibility_bool(isk, 100, 10, 0), false); + } + + #[test] + fn test_witnessing_eligibility_absolute_power() { + let mut stakes = >::with_minimum(100u64); + let isk = ("validator", "withdrawer"); + + stakes.add_stake(isk, 1_000, 0).unwrap(); + + assert_eq!( + stakes.witnessing_eligibility(isk, 0, 10, 0), + Ok(Eligible::No(IneligibilityReason::InsufficientPower)) + ); + assert_eq!(stakes.witnessing_eligibility_bool(isk, 0, 10, 0), false); + + assert_eq!( + stakes.witnessing_eligibility(isk, 100, 10, 0), + Ok(Eligible::Yes) + ); + assert_eq!(stakes.witnessing_eligibility_bool(isk, 100, 10, 0), true); + } +} diff --git a/validations/src/eligibility/legacy.rs b/validations/src/eligibility/legacy.rs new file mode 100644 index 000000000..23a0a55ba --- /dev/null +++ b/validations/src/eligibility/legacy.rs @@ -0,0 +1,232 @@ +use witnet_data_structures::chain::{tapi::ActiveWips, Hash, PublicKeyHash, ReputationEngine}; + +/// Calculate the target hash needed to create a valid VRF proof of eligibility used for block +/// mining. +pub fn calculate_randpoe_threshold( + total_identities: u32, + replication_factor: u32, + block_epoch: u32, + minimum_difficulty: u32, + epochs_with_minimum_difficulty: u32, + active_wips: &ActiveWips, +) -> (Hash, f64) { + let max = u64::max_value(); + let minimum_difficulty = std::cmp::max(1, minimum_difficulty); + let target = if block_epoch <= epochs_with_minimum_difficulty { + max / u64::from(minimum_difficulty) + } else if active_wips.wips_0009_0011_0012() { + let difficulty = std::cmp::max(total_identities, minimum_difficulty); + (max / u64::from(difficulty)).saturating_mul(u64::from(replication_factor)) + } else { + let difficulty = std::cmp::max(1, total_identities); + (max / u64::from(difficulty)).saturating_mul(u64::from(replication_factor)) + }; + let target = u32::try_from(target >> 32).unwrap(); + + let probability = f64::from(target) / f64::from(u32::try_from(max >> 32).unwrap()); + (Hash::with_first_u32(target), probability) +} + +/// Calculate the target hash needed to create a valid VRF proof of eligibility used for data +/// request witnessing. +pub fn calculate_reppoe_threshold( + rep_eng: &ReputationEngine, + pkh: &PublicKeyHash, + num_witnesses: u16, + minimum_difficulty: u32, + active_wips: &ActiveWips, +) -> (Hash, f64) { + // Set minimum total_active_reputation to 1 to avoid division by zero + let total_active_rep = std::cmp::max(rep_eng.total_active_reputation(), 1); + // Add 1 to reputation because otherwise a node with 0 reputation would + // never be eligible for a data request + let my_eligibility = u64::from(rep_eng.get_eligibility(pkh)) + 1; + + let max = u64::max_value(); + // Compute target eligibility and hard-cap it if required + let target = if active_wips.wip0016() { + let factor = u64::from(num_witnesses); + (max / std::cmp::max(total_active_rep, u64::from(minimum_difficulty))) + .saturating_mul(my_eligibility) + .saturating_mul(factor) + } else if active_wips.third_hard_fork() { + let factor = u64::from(rep_eng.threshold_factor(num_witnesses)); + // Eligibility must never be greater than (max/minimum_difficulty) + std::cmp::min( + max / u64::from(minimum_difficulty), + (max / total_active_rep).saturating_mul(my_eligibility), + ) + .saturating_mul(factor) + } else { + let factor = u64::from(rep_eng.threshold_factor(num_witnesses)); + // Check for overflow: when the probability is more than 100%, cap it to 100% + (max / total_active_rep) + .saturating_mul(my_eligibility) + .saturating_mul(factor) + }; + let target = u32::try_from(target >> 32).unwrap(); + + let probability = f64::from(target) / f64::from(u32::try_from(max >> 32).unwrap()); + (Hash::with_first_u32(target), probability) +} + +/// Used to classify VRF hashes into slots. +/// +/// When trying to mine a block, the node considers itself eligible if the hash of the VRF is lower +/// than `calculate_randpoe_threshold(total_identities, rf, 1001,0,0)` with `rf = mining_backup_factor`. +/// +/// However, in order to consolidate a block, the nodes choose the best block that is valid under +/// `rf = mining_replication_factor`. If there is no valid block within that range, it retries with +/// increasing values of `rf`. For example, with `mining_backup_factor = 4` and +/// `mining_replication_factor = 8`, there are 5 different slots: +/// `rf = 4, rf = 5, rf = 6, rf = 7, rf = 8`. Blocks in later slots can only be better candidates +/// if the previous slots have zero valid blocks. +#[derive(Clone, Debug, Default)] +pub struct VrfSlots { + target_hashes: Vec, +} + +impl VrfSlots { + /// Create new list of slots with the given target hashes. + /// + /// `target_hashes` must be sorted + pub fn new(target_hashes: Vec) -> Self { + Self { target_hashes } + } + + /// Create new list of slots with the given parameters + pub fn from_rf( + total_identities: u32, + replication_factor: u32, + backup_factor: u32, + block_epoch: u32, + minimum_difficulty: u32, + epochs_with_minimum_difficulty: u32, + active_wips: &ActiveWips, + ) -> Self { + Self::new( + (replication_factor..=backup_factor) + .map(|rf| { + calculate_randpoe_threshold( + total_identities, + rf, + block_epoch, + minimum_difficulty, + epochs_with_minimum_difficulty, + active_wips, + ) + .0 + }) + .collect(), + ) + } + + /// Return the slot number that contains the given hash + pub fn slot(&self, hash: &Hash) -> u32 { + let num_sections = self.target_hashes.len(); + u32::try_from( + self.target_hashes + .iter() + // The section is the index of the first section hash that is less + // than or equal to the provided hash + .position(|th| hash <= th) + // If the provided hash is greater than all of the section hashes, + // return the number of sections + .unwrap_or(num_sections), + ) + .unwrap() + } + + /// Return the target hash for each slot + pub fn target_hashes(&self) -> &[Hash] { + &self.target_hashes + } +} + +#[allow(clippy::many_single_char_names)] +fn internal_calculate_mining_probability( + rf: u32, + n: f64, + k: u32, // k: iterative rf until reach bf + m: i32, // M: nodes with reputation greater than me + l: i32, // L: nodes with reputation equal than me + r: i32, // R: nodes with reputation less than me +) -> f64 { + if k == rf { + let rf = f64::from(rf); + // Prob to mine is the probability that a node with the same reputation than me mine, + // divided by all the nodes with the same reputation: + // 1/L * (1 - ((N-RF)/N)^L) + let prob_to_mine = (1.0 / f64::from(l)) * (1.0 - ((n - rf) / n).powi(l)); + // Prob that a node with more reputation than me mine is: + // ((N-RF)/N)^M + let prob_greater_neg = ((n - rf) / n).powi(m); + + prob_to_mine * prob_greater_neg + } else { + let k = f64::from(k); + // Here we take into account that rf = 1 because is only a new slot + let prob_to_mine = (1.0 / f64::from(l)) * (1.0 - ((n - 1.0) / n).powi(l)); + // The same equation than before + let prob_bigger_neg = ((n - k) / n).powi(m); + // Prob that a node with less or equal reputation than me mine with a lower slot is: + // ((N+1-RF)/N)^(L+R-1) + let prob_lower_slot_neg = ((n + 1.0 - k) / n).powi(l + r - 1); + + prob_to_mine * prob_bigger_neg * prob_lower_slot_neg + } +} + +/// Calculate the probability that the block candidate proposed by this identity will be the +/// consolidated block selected by the network. +pub fn calculate_mining_probability( + rep_engine: &ReputationEngine, + own_pkh: PublicKeyHash, + rf: u32, + bf: u32, +) -> f64 { + let n = u32::try_from(rep_engine.ars().active_identities_number()).unwrap(); + + // In case of any active node, the probability is maximum + if n == 0 { + return 1.0; + } + + // First we need to know how many nodes have more or equal reputation than us + let own_rep = rep_engine.trs().get(&own_pkh); + let is_active_node = rep_engine.ars().contains(&own_pkh); + let mut greater = 0; + let mut equal = 0; + let mut less = 0; + for &active_id in rep_engine.ars().active_identities() { + let rep = rep_engine.trs().get(&active_id); + match (rep.0 > 0, own_rep.0 > 0) { + (true, false) => greater += 1, + (false, true) => less += 1, + _ => equal += 1, + } + } + // In case of not being active, the equal value is plus 1. + if !is_active_node { + equal += 1; + } + + if rf > n && greater == 0 { + // In case of replication factor exceed the active node number and being the most reputed + // we obtain the maximum probability divided in the nodes we share the same reputation + 1.0 / f64::from(equal) + } else if rf > n && greater > 0 { + // In case of replication factor exceed the active node number and not being the most reputed + // we obtain the minimum probability + 0.0 + } else { + let mut aux = + internal_calculate_mining_probability(rf, f64::from(n), rf, greater, equal, less); + let mut k = rf + 1; + while k <= bf && k <= n { + aux += internal_calculate_mining_probability(rf, f64::from(n), k, greater, equal, less); + k += 1; + } + aux + } +} diff --git a/validations/src/eligibility/mod.rs b/validations/src/eligibility/mod.rs new file mode 100644 index 000000000..689cbbe12 --- /dev/null +++ b/validations/src/eligibility/mod.rs @@ -0,0 +1,6 @@ +/// Eligibility-related functions specific to former versions of the Witnet protocol. +pub mod legacy; + +/// Eligibility-related functions specific to the latest Proof-of-Stake version of the Witnet +/// protocol. +pub mod current; diff --git a/validations/src/lib.rs b/validations/src/lib.rs index 01422f6e7..930efff2d 100644 --- a/validations/src/lib.rs +++ b/validations/src/lib.rs @@ -7,11 +7,14 @@ #![deny(unused_mut)] #![deny(missing_docs)] -/// Module containing validations +/// Module containing general purpose validations pub mod validations; /// Module containing validations specific to witnessing pub mod witnessing; +/// Module contaning validations specific to eligibility +pub mod eligibility; + #[cfg(test)] mod tests; diff --git a/validations/src/tests/compare_block_candidates.rs b/validations/src/tests/compare_block_candidates.rs index df1707abe..fc61c36cc 100644 --- a/validations/src/tests/compare_block_candidates.rs +++ b/validations/src/tests/compare_block_candidates.rs @@ -2,7 +2,7 @@ use witnet_data_structures::chain::{tapi::current_active_wips, Hash, Reputation} use std::cmp::Ordering; -use crate::validations::*; +use crate::{eligibility::legacy::*, validations::*}; #[test] fn test_compare_candidate_same_section() { diff --git a/validations/src/tests/mod.rs b/validations/src/tests/mod.rs index b14bb19c3..5b51b0991 100644 --- a/validations/src/tests/mod.rs +++ b/validations/src/tests/mod.rs @@ -6,7 +6,10 @@ use std::{ use itertools::Itertools; -use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; +use witnet_config::defaults::{ + PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS, + PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, +}; use witnet_crypto::{ secp256k1::{PublicKey as Secp256k1_PublicKey, SecretKey as Secp256k1_SecretKey}, signature::sign, @@ -47,6 +50,8 @@ mod witnessing; static ONE_WIT: u64 = 1_000_000_000; const MAX_VT_WEIGHT: u32 = 20_000; const MAX_DR_WEIGHT: u32 = 80_000; +const MIN_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS; + const REQUIRED_REWARD_COLLATERAL_RATIO: u64 = PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; const INITIAL_BLOCK_REWARD: u64 = 250 * 1_000_000_000; @@ -433,7 +438,7 @@ fn vtt_no_inputs_zero_output() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); - // Try to create a data request with no inputs + // Try to create a value transfer with no inputs let pkh = PublicKeyHash::default(); let vto0 = ValueTransferOutput { pkh, @@ -1450,7 +1455,7 @@ fn data_request_no_inputs() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![], dr_output, vec![]); let dr_transaction = DRTransaction::new(dr_tx_body, vec![]); let x = validate_dr_transaction( &dr_transaction, @@ -1486,7 +1491,7 @@ fn data_request_no_inputs_but_one_signature() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); let x = validate_dr_transaction( @@ -1531,7 +1536,7 @@ fn data_request_one_input_but_no_signature() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let dr_transaction = DRTransaction::new(dr_tx_body, vec![]); @@ -1576,7 +1581,7 @@ fn data_request_one_input_signatures() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); test_signature_empty_wrong_bad(dr_tx_body, |dr_tx_body, drs| { let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -1622,7 +1627,7 @@ fn data_request_input_double_spend() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti; 2], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti; 2], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs; 2]); let x = validate_dr_transaction( @@ -1662,7 +1667,7 @@ fn data_request_input_not_in_utxo() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); let x = validate_dr_transaction( @@ -1707,7 +1712,7 @@ fn data_request_input_not_enough_value() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); let x = validate_dr_transaction( @@ -1776,7 +1781,7 @@ fn data_request_output_value_overflow() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti0, vti1], vec![vto0, vto1], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti0, vti1], dr_output, vec![vto0, vto1]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs; 2]); let x = validate_dr_transaction( @@ -1812,7 +1817,7 @@ fn test_drtx(dr_output: DataRequestOutput) -> Result<(), failure::Error> { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2210,7 +2215,7 @@ fn data_request_http_post_before_wip_activation() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2278,7 +2283,7 @@ fn data_request_http_get_with_headers_before_wip_activation() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2336,7 +2341,7 @@ fn data_request_parse_xml_before_wip_activation() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2390,7 +2395,7 @@ fn data_request_parse_xml_after_wip_activation() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2426,8 +2431,8 @@ fn dr_validation_weight_limit_exceeded() { let dr_body = DRTransactionBody::new( vec![Input::default()], - vec![ValueTransferOutput::default()], dro.clone(), + vec![ValueTransferOutput::default()], ); let dr_tx = DRTransaction::new(dr_body, vec![]); let dr_weight = dr_tx.weight(); @@ -2517,7 +2522,7 @@ fn data_request_miner_fee() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2568,7 +2573,7 @@ fn data_request_miner_fee_with_change() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![change_output], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![change_output]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2619,7 +2624,7 @@ fn data_request_change_to_different_pkh() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![change_output], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![change_output]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2680,7 +2685,7 @@ fn data_request_two_change_outputs() { let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); let dr_tx_body = - DRTransactionBody::new(vec![vti], vec![change_output_1, change_output_2], dr_output); + DRTransactionBody::new(vec![vti], dr_output, vec![change_output_1, change_output_2]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2733,7 +2738,7 @@ fn data_request_miner_fee_with_too_much_change() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![change_output], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![change_output]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2781,7 +2786,7 @@ fn data_request_zero_value_output() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![change_output], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![change_output]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2828,7 +2833,7 @@ fn data_request_reward_collateral_ratio_wip() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2899,7 +2904,7 @@ fn data_request_reward_collateral_ratio_limit() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2930,7 +2935,7 @@ fn data_request_reward_collateral_ratio_limit() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2967,6 +2972,7 @@ fn test_empty_commit(c_tx: &CommitTransaction) -> Result<(), failure::Error> { let block_number = 0; let minimum_reppoe_difficulty = 1; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); + let superblock_period = 1; validate_commit_transaction( c_tx, @@ -2982,6 +2988,7 @@ fn test_empty_commit(c_tx: &CommitTransaction) -> Result<(), failure::Error> { block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .map(|_| ()) } @@ -2998,6 +3005,7 @@ fn test_commit_with_dr_and_utxo_set( let collateral_minimum = 1; let collateral_age = 1; let minimum_reppoe_difficulty = 1; + let superblock_period = 1; let mut dr_pool = DataRequestPool::default(); let vrf_input = CheckpointVRF::default(); @@ -3011,7 +3019,7 @@ fn test_commit_with_dr_and_utxo_set( collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3036,6 +3044,7 @@ fn test_commit_with_dr_and_utxo_set( block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, )?; verify_signatures_test(signatures_to_verify)?; @@ -3071,7 +3080,7 @@ fn test_commit_difficult_proof() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3110,6 +3119,7 @@ fn test_commit_difficult_proof() { let active_wips = ActiveWips::default(); let mut signatures_to_verify = vec![]; + let superblock_period = 8; let x = validate_commit_transaction( &c_tx, &dr_pool, @@ -3124,6 +3134,7 @@ fn test_commit_difficult_proof() { block_number, minimum_reppoe_difficulty, &active_wips, + superblock_period, ) .and_then(|_| verify_signatures_test(signatures_to_verify)); @@ -3158,7 +3169,7 @@ fn test_commit_with_collateral( collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3186,6 +3197,8 @@ fn test_commit_with_collateral( let cs = sign_tx(PRIV_KEY_1, &cb); let c_tx = CommitTransaction::new(cb, vec![cs]); + let superblock_period = 1; + validate_commit_transaction( &c_tx, &dr_pool, @@ -3200,6 +3213,7 @@ fn test_commit_with_collateral( block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .map(|_| ()) } @@ -3321,7 +3335,7 @@ fn commitment_no_signature() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3418,7 +3432,7 @@ fn commitment_invalid_proof() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_epoch = 0; @@ -3431,6 +3445,8 @@ fn commitment_invalid_proof() { let c_tx = CommitTransaction::new(cb, vec![cs]); let mut signatures_to_verify = vec![]; + let superblock_period = 1; + let x = validate_commit_transaction( &c_tx, &dr_pool, @@ -3445,6 +3461,7 @@ fn commitment_invalid_proof() { block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .and_then(|_| verify_signatures_test(signatures_to_verify)); @@ -3485,7 +3502,7 @@ fn commitment_dr_in_reveal_stage() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3507,6 +3524,8 @@ fn commitment_dr_in_reveal_stage() { dr_pool.update_data_request_stages(); let mut signatures_to_verify = vec![]; + let superblock_period = 1; + let x = validate_commit_transaction( &c_tx, &dr_pool, @@ -3521,6 +3540,7 @@ fn commitment_dr_in_reveal_stage() { block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ); assert_eq!( x.unwrap_err().downcast::().unwrap(), @@ -3856,7 +3876,7 @@ fn commitment_collateral_zero_is_minimum() { collateral: 0, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3892,6 +3912,8 @@ fn commitment_collateral_zero_is_minimum() { let cs = sign_tx(PRIV_KEY_1, &cb); let c_tx = CommitTransaction::new(cb, vec![cs]); + let superblock_period = 1; + validate_commit_transaction( &c_tx, &dr_pool, @@ -3906,6 +3928,7 @@ fn commitment_collateral_zero_is_minimum() { block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .map(|_| ()) }; @@ -3945,7 +3968,7 @@ fn commitment_timelock() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3983,6 +4006,7 @@ fn commitment_timelock() { let active_wips = active_wips_from_mainnet(epoch); let mut signatures_to_verify = vec![]; + let superblock_period = 1; validate_commit_transaction( &c_tx, &dr_pool, @@ -3997,6 +4021,7 @@ fn commitment_timelock() { block_number, minimum_reppoe_difficulty, &active_wips, + superblock_period, ) .map(|_| ())?; @@ -4042,7 +4067,7 @@ fn dr_pool_with_dr_in_reveal_stage() -> (DataRequestPool, Hash) { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_pointer = dr_transaction.hash(); @@ -4161,7 +4186,7 @@ fn reveal_dr_in_commit_stage() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_pointer = dr_transaction.hash(); @@ -4292,7 +4317,7 @@ fn reveal_valid_commitment() { ..DataRequestOutput::default() }; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output), + body: DRTransactionBody::new(vec![], dr_output, vec![]), signatures: vec![KeyedSignature::default()], }; let dr_pointer = dr_transaction.hash(); @@ -4577,7 +4602,7 @@ fn dr_pool_with_dr_in_tally_all_errors( // Create DRTransaction let epoch = 0; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output.clone()), + body: DRTransactionBody::new(vec![], dr_output.clone(), vec![]), signatures: vec![KeyedSignature { signature: Default::default(), public_key: dr_public_key.clone(), @@ -4690,7 +4715,7 @@ fn dr_pool_with_dr_in_tally_stage_generic( // Create DRTransaction let epoch = 0; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output.clone()), + body: DRTransactionBody::new(vec![], dr_output.clone(), vec![]), signatures: vec![KeyedSignature { signature: Default::default(), public_key: dr_public_key.clone(), @@ -4907,7 +4932,7 @@ fn tally_dr_not_tally_stage() { data_request: example_data_request(), collateral: DEFAULT_COLLATERAL, }; - let dr_transaction_body = DRTransactionBody::new(vec![], vec![], dr_output.clone()); + let dr_transaction_body = DRTransactionBody::new(vec![], dr_output.clone(), vec![]); let dr_transaction_signature = sign_tx(PRIV_KEY_2, &dr_transaction_body); let dr_transaction = DRTransaction::new(dr_transaction_body, vec![dr_transaction_signature]); let dr_pointer = dr_transaction.hash(); @@ -5205,7 +5230,7 @@ fn generic_tally_test_inner( // Create DRTransaction let epoch = 0; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output), + body: DRTransactionBody::new(vec![], dr_output, vec![]), signatures: vec![KeyedSignature { signature: Default::default(), public_key: dr_public_key.clone(), @@ -8448,6 +8473,107 @@ fn tally_error_encode_reveal_wip() { x.unwrap(); } +#[test] +fn st_no_inputs() { + let utxo_set = UnspentOutputsPool::default(); + let block_number = 0; + let utxo_diff = UtxoDiff::new(&utxo_set, block_number); + + // Try to create a stake tx with no inputs + let st_output = StakeOutput { + value: MIN_STAKE_NANOWITS + 1, + ..Default::default() + }; + + let st_body = StakeTransactionBody::new(vec![], st_output, None); + let st_tx = StakeTransaction::new(st_body, vec![]); + let x = validate_stake_transaction( + &st_tx, + &utxo_diff, + Epoch::default(), + EpochConstants::default(), + &mut vec![], + ); + assert_eq!( + x.unwrap_err().downcast::().unwrap(), + TransactionError::NoInputs { + tx_hash: st_tx.hash(), + } + ); +} + +#[test] +fn st_one_input_but_no_signature() { + let mut signatures_to_verify = vec![]; + let utxo_set = UnspentOutputsPool::default(); + let block_number = 0; + let utxo_diff = UtxoDiff::new(&utxo_set, block_number); + let vti = Input::new( + "2222222222222222222222222222222222222222222222222222222222222222:0" + .parse() + .unwrap(), + ); + + // No signatures but 1 input + let stake_output = StakeOutput { + value: MIN_STAKE_NANOWITS + 1, + ..Default::default() + }; + + let stake_tx_body = StakeTransactionBody::new(vec![vti], stake_output, None); + let stake_tx = StakeTransaction::new(stake_tx_body, vec![]); + let x = validate_stake_transaction( + &stake_tx, + &utxo_diff, + Epoch::default(), + EpochConstants::default(), + &mut signatures_to_verify, + ); + assert_eq!( + x.unwrap_err().downcast::().unwrap(), + TransactionError::MismatchingSignaturesNumber { + signatures_n: 0, + inputs_n: 1, + } + ); +} + +#[test] +fn st_below_min_stake() { + let mut signatures_to_verify = vec![]; + let utxo_set = UnspentOutputsPool::default(); + let block_number = 0; + let utxo_diff = UtxoDiff::new(&utxo_set, block_number); + let vti = Input::new( + "2222222222222222222222222222222222222222222222222222222222222222:0" + .parse() + .unwrap(), + ); + + // No signatures but 1 input + let stake_output = StakeOutput { + value: 1, + ..Default::default() + }; + + let stake_tx_body = StakeTransactionBody::new(vec![vti], stake_output, None); + let stake_tx = StakeTransaction::new(stake_tx_body, vec![]); + let x = validate_stake_transaction( + &stake_tx, + &utxo_diff, + Epoch::default(), + EpochConstants::default(), + &mut signatures_to_verify, + ); + assert_eq!( + x.unwrap_err().downcast::().unwrap(), + TransactionError::StakeBelowMinimum { + min_stake: MIN_STAKE_NANOWITS, + stake: 1 + } + ); +} + static LAST_VRF_INPUT: &str = "4da71b67e7e50ae4ad06a71e505244f8b490da55fc58c50386c908f7146d2239"; #[test] @@ -9098,7 +9224,7 @@ fn block_duplicated_commits() { data_request: example_data_request(), collateral: DEFAULT_COLLATERAL, }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -9191,7 +9317,7 @@ fn block_duplicated_reveals() { data_request: example_data_request(), collateral: DEFAULT_COLLATERAL, }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -9360,7 +9486,7 @@ fn block_before_and_after_hard_fork() { data_request: example_data_request_before_wip19(), collateral: DEFAULT_COLLATERAL, }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro.clone()); + let dr_body = DRTransactionBody::new(vec![], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_epoch = 0; @@ -9375,7 +9501,7 @@ fn block_before_and_after_hard_fork() { }; let utxo_set = build_utxo_set_with_mint(vec![vto], None, vec![]); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dro); + let dr_tx_body = DRTransactionBody::new(vec![vti], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -9923,7 +10049,7 @@ fn block_add_drt() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -9959,7 +10085,7 @@ fn block_add_2_drt_same_input() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_tx1 = DRTransaction::new(dr_tx_body, vec![drs]); @@ -9979,7 +10105,7 @@ fn block_add_2_drt_same_input() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_tx2 = DRTransaction::new(dr_tx_body, vec![drs]); @@ -10020,7 +10146,7 @@ fn block_add_1_drt_and_1_vtt_same_input() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_tx = DRTransaction::new(dr_tx_body, vec![drs]); @@ -10506,7 +10632,7 @@ fn validate_commit_transactions_included_in_utxo_diff() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -10898,12 +11024,12 @@ fn validate_dr_weight_overflow() { let dr_value = dro.checked_total_value().unwrap(); let dr_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![], dro.clone()); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_tx = DRTransaction::new(dr_body, vec![drs]); assert_eq!(dr_tx.weight(), 1589); - let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], vec![], dro); + let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], dro, vec![]); let drs2 = sign_tx(PRIV_KEY_1, &dr_body2); let dr_tx2 = DRTransaction::new(dr_body2, vec![drs2]); assert_eq!(dr_tx2.weight(), 1589); @@ -10940,7 +11066,7 @@ fn validate_dr_weight_overflow_126_witnesses() { let dr_value = dro.checked_total_value().unwrap(); let dr_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![], dro.clone()); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_tx = DRTransaction::new(dr_body, vec![drs]); @@ -10979,12 +11105,12 @@ fn validate_dr_weight_valid() { let dr_value = dro.checked_total_value().unwrap(); let dr_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![], dro.clone()); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_tx = DRTransaction::new(dr_body, vec![drs]); assert_eq!(dr_tx.weight(), 1589); - let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], vec![], dro); + let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], dro, vec![]); let drs2 = sign_tx(PRIV_KEY_1, &dr_body2); let dr_tx2 = DRTransaction::new(dr_body2, vec![drs2]); assert_eq!(dr_tx2.weight(), 1589); diff --git a/validations/src/tests/randpoe.rs b/validations/src/tests/randpoe.rs index 4eb9b3c1b..f0c3fb32d 100644 --- a/validations/src/tests/randpoe.rs +++ b/validations/src/tests/randpoe.rs @@ -5,7 +5,7 @@ use witnet_data_structures::chain::{ Alpha, Hash, PublicKeyHash, Reputation, ReputationEngine, }; -use crate::validations::*; +use crate::eligibility::legacy::*; #[test] fn target_randpoe() { diff --git a/validations/src/tests/reppoe.rs b/validations/src/tests/reppoe.rs index a11c9c135..fcdac1805 100644 --- a/validations/src/tests/reppoe.rs +++ b/validations/src/tests/reppoe.rs @@ -8,7 +8,7 @@ use witnet_data_structures::{ transaction::DRTransaction, }; -use crate::validations::*; +use crate::eligibility::legacy::*; fn calculate_reppoe_threshold_v1( rep_eng: &ReputationEngine, diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 2ed59872c..5e034bade 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -6,11 +6,14 @@ use std::{ }; use itertools::Itertools; + use witnet_config::defaults::{ + PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS, PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, }; use witnet_crypto::{ + hash, hash::{calculate_sha256, Sha256}, merkle::{merkle_tree_root as crypto_merkle_tree_root, ProgressiveMerkleTree}, signature::{verify, PublicKey, Signature}, @@ -21,23 +24,26 @@ use witnet_data_structures::{ ConsensusConstants, DataRequestOutput, DataRequestStage, DataRequestState, Epoch, EpochConstants, Hash, Hashable, Input, KeyedSignature, OutputPointer, PublicKeyHash, RADRequest, RADTally, RADType, Reputation, ReputationEngine, SignaturesToVerify, - ValueTransferOutput, + StakeOutput, ValueTransferOutput, }, data_request::{ calculate_reward_collateral_ratio, calculate_tally_change, calculate_witness_reward, calculate_witness_reward_before_second_hard_fork, create_tally, DataRequestPool, }, error::{BlockError, DataRequestError, TransactionError}, + get_protocol_version, + proto::versioning::{ProtocolVersion, VersionedHashable}, radon_report::{RadonReport, ReportContext}, + staking::{prelude::StakeKey, stakes::Stakes}, transaction::{ - CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, TallyTransaction, - Transaction, VTTransaction, + CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeTransaction, + TallyTransaction, Transaction, UnstakeTransaction, VTTransaction, }, transaction_factory::{transaction_inputs_sum, transaction_outputs_sum}, types::visitor::Visitor, utxo_pool::{Diff, UnspentOutputsPool, UtxoDiff}, vrf::{BlockEligibilityClaim, DataRequestEligibilityClaim, VrfCtx}, - wit::NANOWITS_PER_WIT, + wit::{Wit, NANOWITS_PER_WIT}, }; use witnet_rad::{ conditions::{ @@ -50,6 +56,20 @@ use witnet_rad::{ types::{serial_iter_decode, RadonTypes}, }; +use crate::eligibility::{ + current::{ + Eligibility, Eligible, + IneligibilityReason::{InsufficientPower, NotStaking}, + }, + legacy::*, +}; + +// TODO: move to a configuration +const MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000; +const MIN_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS; +const MAX_UNSTAKE_BLOCK_WEIGHT: u32 = 5_000; +const UNSTAKING_DELAY_SECONDS: u32 = 1_209_600; + /// Returns the fee of a value transfer transaction. /// /// The fee is the difference between the outputs and the inputs @@ -96,6 +116,43 @@ pub fn dr_transaction_fee( } } +/// Returns the fee of a stake transaction. +/// +/// The fee is the difference between the outputs and the inputs of the transaction. +pub fn st_transaction_fee( + st_tx: &StakeTransaction, + utxo_diff: &UtxoDiff<'_>, + epoch: Epoch, + epoch_constants: EpochConstants, +) -> Result { + let in_value = transaction_inputs_sum(&st_tx.body.inputs, utxo_diff, epoch, epoch_constants)?; + let out_value = st_tx.body.output.value; + + if out_value > in_value { + Err(TransactionError::NegativeFee.into()) + } else { + Ok(in_value - out_value) + } +} + +/// Returns the fee of a unstake transaction. +/// +/// The fee is the difference between the output and the inputs +/// of the transaction. The pool parameter is used to find the +/// outputs pointed by the inputs and that contain the actual +/// their value. +pub fn ut_transaction_fee(ut_tx: &UnstakeTransaction) -> Result { + // TODO: take in_value from stakes tracker + let in_value = 0; + let out_value = ut_tx.body.value(); + + if out_value > in_value { + Err(TransactionError::NegativeFee.into()) + } else { + Ok(in_value - out_value) + } +} + /// Returns the fee of a data request transaction. /// /// The fee is the difference between the outputs (with the data request value) @@ -375,8 +432,6 @@ pub fn validate_vt_transaction<'a>( let fee = vt_transaction_fee(vt_tx, utxo_diff, epoch, epoch_constants)?; - // FIXME(#514): Implement value transfer transaction validation - Ok(( vt_tx.body.inputs.iter().collect(), vt_tx.body.outputs.iter().collect(), @@ -1129,6 +1184,133 @@ pub fn validate_tally_transaction<'a>( Ok((ta_tx.outputs.iter().collect(), tally_extra_fee)) } +/// A type alias for the very complex return type of `fn validate_stake_transaction`. +pub type ValidatedStakeTransaction<'a> = ( + Vec<&'a Input>, + &'a StakeOutput, + u64, + u32, + &'a Option, +); + +/// Function to validate a stake transaction. +pub fn validate_stake_transaction<'a>( + st_tx: &'a StakeTransaction, + utxo_diff: &UtxoDiff<'_>, + epoch: Epoch, + epoch_constants: EpochConstants, + signatures_to_verify: &mut Vec, +) -> Result, failure::Error> { + // Check that the amount of coins to stake is equal or greater than the minimum allowed + if st_tx.body.output.value < MIN_STAKE_NANOWITS { + Err(TransactionError::StakeBelowMinimum { + min_stake: MIN_STAKE_NANOWITS, + stake: st_tx.body.output.value, + })?; + } + + validate_transaction_signature( + &st_tx.signatures, + &st_tx.body.inputs, + st_tx.hash(), + utxo_diff, + signatures_to_verify, + )?; + + // A stake transaction must have at least one input + if st_tx.body.inputs.is_empty() { + Err(TransactionError::NoInputs { + tx_hash: st_tx.hash(), + })?; + } + + let fee = st_transaction_fee(st_tx, utxo_diff, epoch, epoch_constants)?; + + Ok(( + st_tx.body.inputs.iter().collect(), + &st_tx.body.output, + fee, + st_tx.weight(), + &st_tx.body.change, + )) +} + +/// Function to validate a unstake transaction +pub fn validate_unstake_transaction<'a>( + ut_tx: &'a UnstakeTransaction, + st_tx: &'a StakeTransaction, + _utxo_diff: &UtxoDiff<'_>, + _epoch: Epoch, + _epoch_constants: EpochConstants, +) -> Result<(u64, u32), failure::Error> { + // Check if is unstaking more than the total stake + // FIXME: actually query the stakes tracker for staked value + let amount_to_unstake = ut_tx.body.withdrawal.value; + if amount_to_unstake > st_tx.body.output.value { + return Err(TransactionError::UnstakingMoreThanStaked { + unstake: MIN_STAKE_NANOWITS, + stake: st_tx.body.output.value, + } + .into()); + } + + // Check that the stake is greater than the min allowed + if amount_to_unstake - st_tx.body.output.value < MIN_STAKE_NANOWITS { + return Err(TransactionError::StakeBelowMinimum { + min_stake: MIN_STAKE_NANOWITS, + stake: st_tx.body.output.value, + } + .into()); + } + + // TODO: take the operator from the StakesTracker when implemented + let operator = PublicKeyHash::default(); + // validate unstake_signature + validate_unstake_signature(ut_tx, operator)?; + + // Validate unstake timestamp + validate_unstake_timelock(ut_tx)?; + + // let fee = ut_tx.body.withdrawal.value; + let fee = ut_transaction_fee(ut_tx)?; + let weight = st_tx.weight(); + + Ok((fee, weight)) +} + +/// Validate unstake timelock +pub fn validate_unstake_timelock(ut_tx: &UnstakeTransaction) -> Result<(), failure::Error> { + // TODO: is this correct or should we use calculate it from the staking tx epoch? + if ut_tx.body.withdrawal.time_lock >= UNSTAKING_DELAY_SECONDS.into() { + return Err(TransactionError::InvalidUnstakeTimelock { + time_lock: ut_tx.body.withdrawal.time_lock, + unstaking_delay_seconds: UNSTAKING_DELAY_SECONDS, + } + .into()); + } + + Ok(()) +} + +/// Function to validate a unstake authorization +pub fn validate_unstake_signature( + ut_tx: &UnstakeTransaction, + operator: PublicKeyHash, +) -> Result<(), failure::Error> { + let ut_tx_pkh = ut_tx.signature.public_key.hash(); + // TODO: move to variables and use better names + if ut_tx_pkh != ut_tx.body.withdrawal.pkh.hash() || ut_tx_pkh != operator.hash() { + return Err(TransactionError::InvalidUnstakeSignature { + signature: ut_tx_pkh, + withdrawal: ut_tx.body.withdrawal.pkh.hash(), + operator: operator.hash(), + } + .into()); + } + + Ok(()) +} + /// Function to validate a block signature pub fn validate_block_signature( block: &Block, @@ -1149,7 +1331,9 @@ pub fn validate_block_signature( let signature = keyed_signature.signature.clone().try_into()?; let public_key = keyed_signature.public_key.clone().try_into()?; - let Hash::SHA256(message) = block.hash(); + let Hash::SHA256(message) = block.versioned_hash(get_protocol_version(Some( + block.block_header.beacon.checkpoint, + ))); add_secp_block_signature_to_verify(signatures_to_verify, &public_key, &message, &signature); @@ -1427,9 +1611,8 @@ pub fn validate_block_transactions( mut visitor: Option<&mut dyn Visitor>, ) -> Result { let epoch = block.block_header.beacon.checkpoint; - let is_genesis = block.hash() == consensus_constants.genesis_hash; + let is_genesis = block.is_genesis(&consensus_constants.genesis_hash); let mut utxo_diff = UtxoDiff::new(utxo_set, block_number); - // Init total fee let mut total_fee = 0; // When validating genesis block, keep track of total value created @@ -1699,6 +1882,110 @@ pub fn validate_block_transactions( } let dr_hash_merkle_root = dr_mt.root(); + let protocol_version = get_protocol_version(Some(epoch)); + let (st_root, ut_root) = if protocol_version != ProtocolVersion::V1_7 { + // validate stake transactions in a block + let mut st_mt = ProgressiveMerkleTree::sha256(); + let mut st_weight: u32 = 0; + + // Check if the block contains more than one stake tx from the same operator + let duplicate = block + .txns + .stake_txns + .iter() + .map(|stake_tx| &stake_tx.body.output.authorization.public_key) + .duplicates() + .next(); + + if let Some(duplicate) = duplicate { + return Err(BlockError::RepeatedStakeOperator { + pkh: duplicate.pkh(), + } + .into()); + } + + for transaction in &block.txns.stake_txns { + let (inputs, _output, fee, weight, change) = validate_stake_transaction( + transaction, + &utxo_diff, + epoch, + epoch_constants, + signatures_to_verify, + )?; + + total_fee += fee; + + // Update st weight + let acc_weight = st_weight.saturating_add(weight); + if acc_weight > MAX_STAKE_BLOCK_WEIGHT { + return Err(BlockError::TotalStakeWeightLimitExceeded { + weight: acc_weight, + max_weight: MAX_STAKE_BLOCK_WEIGHT, + } + .into()); + } + st_weight = acc_weight; + + let outputs = change.iter().collect_vec(); + update_utxo_diff(&mut utxo_diff, inputs, outputs, transaction.hash()); + + // Add new hash to merkle tree + st_mt.push(transaction.hash().into()); + + // TODO: Move validations to a visitor + // // Execute visitor + // if let Some(visitor) = &mut visitor { + // let transaction = Transaction::ValueTransfer(transaction.clone()); + // visitor.visit(&(transaction, fee, weight)); + // } + } + + let mut ut_mt = ProgressiveMerkleTree::sha256(); + let mut ut_weight: u32 = 0; + + for transaction in &block.txns.unstake_txns { + // TODO: get tx, default to compile + let st_tx = StakeTransaction::default(); + let (fee, weight) = validate_unstake_transaction( + transaction, + &st_tx, + &utxo_diff, + epoch, + epoch_constants, + )?; + + total_fee += fee; + + // Update ut weight + let acc_weight = ut_weight.saturating_add(weight); + if acc_weight > MAX_UNSTAKE_BLOCK_WEIGHT { + return Err(BlockError::TotalUnstakeWeightLimitExceeded { + weight: acc_weight, + max_weight: MAX_UNSTAKE_BLOCK_WEIGHT, + } + .into()); + } + ut_weight = acc_weight; + + // Add new hash to merkle tree + let txn_hash = transaction.hash(); + let Hash::SHA256(sha) = txn_hash; + ut_mt.push(Sha256(sha)); + + // TODO: Move validations to a visitor + // // Execute visitor + // if let Some(visitor) = &mut visitor { + // let transaction = Transaction::ValueTransfer(transaction.clone()); + // visitor.visit(&(transaction, fee, weight)); + // } + } + + (st_mt.root(), ut_mt.root()) + } else { + // Nullify stake and unstake merkle roots for the legacy protocol version + (hash::EMPTY_SHA256, hash::EMPTY_SHA256) + }; + if !is_genesis { // Validate mint validate_mint_transaction( @@ -1726,9 +2013,16 @@ pub fn validate_block_transactions( commit_hash_merkle_root: Hash::from(co_hash_merkle_root), reveal_hash_merkle_root: Hash::from(re_hash_merkle_root), tally_hash_merkle_root: Hash::from(ta_hash_merkle_root), + stake_hash_merkle_root: Hash::from(st_root), + unstake_hash_merkle_root: Hash::from(ut_root), }; if merkle_roots != block.block_header.merkle_roots { + log::debug!( + "{:?} vs {:?}", + merkle_roots, + block.block_header.merkle_roots + ); Err(BlockError::NotValidMerkleTree.into()) } else { Ok(utxo_diff.take_diff()) @@ -1746,6 +2040,8 @@ pub fn validate_block( rep_eng: &ReputationEngine, consensus_constants: &ConsensusConstants, active_wips: &ActiveWips, + protocol_version: ProtocolVersion, + stakes: &Stakes, ) -> Result<(), failure::Error> { let block_epoch = block.block_header.beacon.checkpoint; let hash_prev_block = block.block_header.beacon.hash_prev_block; @@ -1773,15 +2069,30 @@ pub fn validate_block( // with the genesis_block_hash validate_genesis_block(block, consensus_constants.genesis_hash).map_err(Into::into) } else { - let total_identities = u32::try_from(rep_eng.ars().active_identities_number())?; - let (target_hash, _) = calculate_randpoe_threshold( - total_identities, - consensus_constants.mining_backup_factor, - block_epoch, - consensus_constants.minimum_difficulty, - consensus_constants.epochs_with_minimum_difficulty, - active_wips, - ); + let target_hash = if protocol_version == ProtocolVersion::V2_0 { + let validator = block.block_sig.public_key.pkh(); + let validator_key = StakeKey::from((validator, validator)); + let eligibility = stakes.mining_eligibility(validator_key, block_epoch); + if eligibility == Ok(Eligible::No(InsufficientPower)) + || eligibility == Ok(Eligible::No(NotStaking)) + { + return Err(BlockError::ValidatorNotEligible { validator }.into()); + } + + Hash::max() + } else { + let total_identities = u32::try_from(rep_eng.ars().active_identities_number())?; + let (target_hash, _) = calculate_randpoe_threshold( + total_identities, + consensus_constants.mining_backup_factor, + block_epoch, + consensus_constants.minimum_difficulty, + consensus_constants.epochs_with_minimum_difficulty, + active_wips, + ); + + target_hash + }; add_block_vrf_signature_to_verify( signatures_to_verify, @@ -1894,238 +2205,15 @@ pub fn validate_new_transaction( Transaction::Reveal(tx) => { validate_reveal_transaction(tx, data_request_pool, signatures_to_verify) } - _ => Err(TransactionError::NotValidTransaction.into()), - } -} - -/// Calculate the target hash needed to create a valid VRF proof of eligibility used for block -/// mining. -pub fn calculate_randpoe_threshold( - total_identities: u32, - replication_factor: u32, - block_epoch: u32, - minimum_difficulty: u32, - epochs_with_minimum_difficulty: u32, - active_wips: &ActiveWips, -) -> (Hash, f64) { - let max = u64::max_value(); - let minimum_difficulty = std::cmp::max(1, minimum_difficulty); - let target = if block_epoch <= epochs_with_minimum_difficulty { - max / u64::from(minimum_difficulty) - } else if active_wips.wips_0009_0011_0012() { - let difficulty = std::cmp::max(total_identities, minimum_difficulty); - (max / u64::from(difficulty)).saturating_mul(u64::from(replication_factor)) - } else { - let difficulty = std::cmp::max(1, total_identities); - (max / u64::from(difficulty)).saturating_mul(u64::from(replication_factor)) - }; - let target = u32::try_from(target >> 32).unwrap(); - - let probability = f64::from(target) / f64::from(u32::try_from(max >> 32).unwrap()); - (Hash::with_first_u32(target), probability) -} - -/// Calculate the target hash needed to create a valid VRF proof of eligibility used for data -/// request witnessing. -pub fn calculate_reppoe_threshold( - rep_eng: &ReputationEngine, - pkh: &PublicKeyHash, - num_witnesses: u16, - minimum_difficulty: u32, - active_wips: &ActiveWips, -) -> (Hash, f64) { - // Set minimum total_active_reputation to 1 to avoid division by zero - let total_active_rep = std::cmp::max(rep_eng.total_active_reputation(), 1); - // Add 1 to reputation because otherwise a node with 0 reputation would - // never be eligible for a data request - let my_eligibility = u64::from(rep_eng.get_eligibility(pkh)) + 1; - - let max = u64::max_value(); - // Compute target eligibility and hard-cap it if required - let target = if active_wips.wip0016() { - let factor = u64::from(num_witnesses); - (max / std::cmp::max(total_active_rep, u64::from(minimum_difficulty))) - .saturating_mul(my_eligibility) - .saturating_mul(factor) - } else if active_wips.third_hard_fork() { - let factor = u64::from(rep_eng.threshold_factor(num_witnesses)); - // Eligibility must never be greater than (max/minimum_difficulty) - std::cmp::min( - max / u64::from(minimum_difficulty), - (max / total_active_rep).saturating_mul(my_eligibility), - ) - .saturating_mul(factor) - } else { - let factor = u64::from(rep_eng.threshold_factor(num_witnesses)); - // Check for overflow: when the probability is more than 100%, cap it to 100% - (max / total_active_rep) - .saturating_mul(my_eligibility) - .saturating_mul(factor) - }; - let target = u32::try_from(target >> 32).unwrap(); - - let probability = f64::from(target) / f64::from(u32::try_from(max >> 32).unwrap()); - (Hash::with_first_u32(target), probability) -} - -/// Used to classify VRF hashes into slots. -/// -/// When trying to mine a block, the node considers itself eligible if the hash of the VRF is lower -/// than `calculate_randpoe_threshold(total_identities, rf, 1001,0,0)` with `rf = mining_backup_factor`. -/// -/// However, in order to consolidate a block, the nodes choose the best block that is valid under -/// `rf = mining_replication_factor`. If there is no valid block within that range, it retries with -/// increasing values of `rf`. For example, with `mining_backup_factor = 4` and -/// `mining_replication_factor = 8`, there are 5 different slots: -/// `rf = 4, rf = 5, rf = 6, rf = 7, rf = 8`. Blocks in later slots can only be better candidates -/// if the previous slots have zero valid blocks. -#[derive(Clone, Debug, Default)] -pub struct VrfSlots { - target_hashes: Vec, -} - -impl VrfSlots { - /// Create new list of slots with the given target hashes. - /// - /// `target_hashes` must be sorted - pub fn new(target_hashes: Vec) -> Self { - Self { target_hashes } - } - - /// Create new list of slots with the given parameters - pub fn from_rf( - total_identities: u32, - replication_factor: u32, - backup_factor: u32, - block_epoch: u32, - minimum_difficulty: u32, - epochs_with_minimum_difficulty: u32, - active_wips: &ActiveWips, - ) -> Self { - Self::new( - (replication_factor..=backup_factor) - .map(|rf| { - calculate_randpoe_threshold( - total_identities, - rf, - block_epoch, - minimum_difficulty, - epochs_with_minimum_difficulty, - active_wips, - ) - .0 - }) - .collect(), - ) - } - - /// Return the slot number that contains the given hash - pub fn slot(&self, hash: &Hash) -> u32 { - let num_sections = self.target_hashes.len(); - u32::try_from( - self.target_hashes - .iter() - // The section is the index of the first section hash that is less - // than or equal to the provided hash - .position(|th| hash <= th) - // If the provided hash is greater than all of the section hashes, - // return the number of sections - .unwrap_or(num_sections), + Transaction::Stake(tx) => validate_stake_transaction( + tx, + &utxo_diff, + current_epoch, + epoch_constants, + signatures_to_verify, ) - .unwrap() - } - - /// Return the target hash for each slot - pub fn target_hashes(&self) -> &[Hash] { - &self.target_hashes - } -} - -#[allow(clippy::many_single_char_names)] -fn internal_calculate_mining_probability( - rf: u32, - n: f64, - k: u32, // k: iterative rf until reach bf - m: i32, // M: nodes with reputation greater than me - l: i32, // L: nodes with reputation equal than me - r: i32, // R: nodes with reputation less than me -) -> f64 { - if k == rf { - let rf = f64::from(rf); - // Prob to mine is the probability that a node with the same reputation than me mine, - // divided by all the nodes with the same reputation: - // 1/L * (1 - ((N-RF)/N)^L) - let prob_to_mine = (1.0 / f64::from(l)) * (1.0 - ((n - rf) / n).powi(l)); - // Prob that a node with more reputation than me mine is: - // ((N-RF)/N)^M - let prob_greater_neg = ((n - rf) / n).powi(m); - - prob_to_mine * prob_greater_neg - } else { - let k = f64::from(k); - // Here we take into account that rf = 1 because is only a new slot - let prob_to_mine = (1.0 / f64::from(l)) * (1.0 - ((n - 1.0) / n).powi(l)); - // The same equation than before - let prob_bigger_neg = ((n - k) / n).powi(m); - // Prob that a node with less or equal reputation than me mine with a lower slot is: - // ((N+1-RF)/N)^(L+R-1) - let prob_lower_slot_neg = ((n + 1.0 - k) / n).powi(l + r - 1); - - prob_to_mine * prob_bigger_neg * prob_lower_slot_neg - } -} - -/// Calculate the probability that the block candidate proposed by this identity will be the -/// consolidated block selected by the network. -pub fn calculate_mining_probability( - rep_engine: &ReputationEngine, - own_pkh: PublicKeyHash, - rf: u32, - bf: u32, -) -> f64 { - let n = u32::try_from(rep_engine.ars().active_identities_number()).unwrap(); - - // In case of any active node, the probability is maximum - if n == 0 { - return 1.0; - } - - // First we need to know how many nodes have more or equal reputation than us - let own_rep = rep_engine.trs().get(&own_pkh); - let is_active_node = rep_engine.ars().contains(&own_pkh); - let mut greater = 0; - let mut equal = 0; - let mut less = 0; - for &active_id in rep_engine.ars().active_identities() { - let rep = rep_engine.trs().get(&active_id); - match (rep.0 > 0, own_rep.0 > 0) { - (true, false) => greater += 1, - (false, true) => less += 1, - _ => equal += 1, - } - } - // In case of not being active, the equal value is plus 1. - if !is_active_node { - equal += 1; - } - - if rf > n && greater == 0 { - // In case of replication factor exceed the active node number and being the most reputed - // we obtain the maximum probability divided in the nodes we share the same reputation - 1.0 / f64::from(equal) - } else if rf > n && greater > 0 { - // In case of replication factor exceed the active node number and not being the most reputed - // we obtain the minimum probability - 0.0 - } else { - let mut aux = - internal_calculate_mining_probability(rf, f64::from(n), rf, greater, equal, less); - let mut k = rf + 1; - while k <= bf && k <= n { - aux += internal_calculate_mining_probability(rf, f64::from(n), k, greater, equal, less); - k += 1; - } - aux + .map(|(_, _, fee, _, _)| fee), + _ => Err(TransactionError::NotValidTransaction.into()), } } @@ -2166,6 +2254,8 @@ pub fn validate_merkle_tree(block: &Block) -> bool { commit_hash_merkle_root: merkle_tree_root(&block.txns.commit_txns), reveal_hash_merkle_root: merkle_tree_root(&block.txns.reveal_txns), tally_hash_merkle_root: merkle_tree_root(&block.txns.tally_txns), + stake_hash_merkle_root: merkle_tree_root(&block.txns.stake_txns), + unstake_hash_merkle_root: merkle_tree_root(&block.txns.unstake_txns), }; merkle_roots == block.block_header.merkle_roots @@ -2223,33 +2313,45 @@ pub fn compare_block_candidates( b2_vrf_hash: Hash, b2_is_active: bool, s: &VrfSlots, + version: ProtocolVersion, ) -> Ordering { - let section1 = s.slot(&b1_vrf_hash); - let section2 = s.slot(&b2_vrf_hash); - // Bigger section implies worse block candidate - section1 - .cmp(§ion2) - .reverse() - // Blocks created with nodes with reputation are better candidates than the others - .then({ - match (b1_rep.0 > 0, b2_rep.0 > 0) { - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - _ => Ordering::Equal, - } - }) - // Blocks created with active nodes are better candidates than the others - .then({ - match (b1_is_active, b2_is_active) { - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - _ => Ordering::Equal, - } - }) + let ordering = if version == ProtocolVersion::V2_0 { // Bigger vrf hash implies worse block candidate - .then(b1_vrf_hash.cmp(&b2_vrf_hash).reverse()) - // Bigger block implies worse block candidate - .then(b1_hash.cmp(&b2_hash).reverse()) + b1_vrf_hash + .cmp(&b2_vrf_hash) + .reverse() + // Bigger block implies worse block candidate + .then(b1_hash.cmp(&b2_hash).reverse()) + } else { + let section1 = s.slot(&b1_vrf_hash); + let section2 = s.slot(&b2_vrf_hash); + // Bigger section implies worse block candidate + section1 + .cmp(§ion2) + .reverse() + // Blocks created with nodes with reputation are better candidates than the others + .then({ + match (b1_rep.0 > 0, b2_rep.0 > 0) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + _ => Ordering::Equal, + } + }) + // Blocks created with active nodes are better candidates than the others + .then({ + match (b1_is_active, b2_is_active) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + _ => Ordering::Equal, + } + }) + // Bigger vrf hash implies worse block candidate + .then(b1_vrf_hash.cmp(&b2_vrf_hash).reverse()) + // Bigger block implies worse block candidate + .then(b1_hash.cmp(&b2_hash).reverse()) + }; + + ordering } /// Blocking process to verify signatures diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index bb1f7ef9b..5a7221a5f 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -2,7 +2,7 @@ authors = ["Witnet Foundation "] edition = "2021" name = "witnet_wallet" -version = "1.7.1" +version = "2.0.0" workspace = ".." [dependencies] diff --git a/wallet/src/actors/worker/methods.rs b/wallet/src/actors/worker/methods.rs index 268daf6cf..23efd591a 100644 --- a/wallet/src/actors/worker/methods.rs +++ b/wallet/src/actors/worker/methods.rs @@ -300,6 +300,7 @@ impl Worker { wallet_id: &str, password: &[u8], ) -> Result { + log::debug!("Unlocking wallet with ID {wallet_id}"); let (salt, iv) = self .wallets .wallet_salt_and_iv(wallet_id) @@ -308,7 +309,9 @@ impl Worker { | repository::Error::WalletNotFound => Error::WalletNotFound, err => Error::Repository(err), })?; + log::debug!("Found salt and IV for wallet with ID {wallet_id}. Deriving key now."); let key = crypto::key_from_password(password, &salt, self.params.db_hash_iterations); + log::debug!("Derived key for wallet with ID {wallet_id}. Generating session now."); let session_id: types::SessionId = From::from(crypto::gen_session_id( &mut self.rng, &self.params.id_hash_function, @@ -320,6 +323,7 @@ impl Worker { let wallet_db = db::EncryptedDb::new(self.db.clone(), prefix, key, iv); // Check if password-derived key is able to read the special stored value + log::debug!("Wallet {wallet_id} now has a session with ID {session_id}"); wallet_db .get(&constants::ENCRYPTION_CHECK_KEY) .map_err(|err| match err { @@ -327,13 +331,16 @@ impl Worker { err => Error::Db(err), })?; + log::debug!("Encryption key for wallet {wallet_id} seems to be valid. Decrypting wallet object now."); let wallet = Arc::new(repository::Wallet::unlock( wallet_id, session_id.clone(), wallet_db, self.params.clone(), )?); + log::debug!("Extracting public data for wallet {wallet_id}."); let data = wallet.public_data()?; + log::debug!("Wallet data: {:?}", data); Ok(types::UnlockedSessionWallet { wallet, @@ -618,7 +625,7 @@ impl Worker { .body .outputs .get(output.output_index as usize) - .map(ValueTransferOutput::clone) + .cloned() .ok_or_else(|| { Error::OutputIndexNotFound(output.output_index, format!("{:?}", txn)) }), @@ -626,21 +633,21 @@ impl Worker { .body .outputs .get(output.output_index as usize) - .map(ValueTransferOutput::clone) + .cloned() .ok_or_else(|| { Error::OutputIndexNotFound(output.output_index, format!("{:?}", txn)) }), Transaction::Tally(tally) => tally .outputs .get(output.output_index as usize) - .map(ValueTransferOutput::clone) + .cloned() .ok_or_else(|| { Error::OutputIndexNotFound(output.output_index, format!("{:?}", txn)) }), Transaction::Mint(mint) => mint .outputs .get(output.output_index as usize) - .map(ValueTransferOutput::clone) + .cloned() .ok_or_else(|| { Error::OutputIndexNotFound(output.output_index, format!("{:?}", txn)) }), @@ -648,7 +655,7 @@ impl Worker { .body .outputs .get(output.output_index as usize) - .map(ValueTransferOutput::clone) + .cloned() .ok_or_else(|| { Error::OutputIndexNotFound(output.output_index, format!("{:?}", txn)) }), diff --git a/wallet/src/db/encrypted/engine.rs b/wallet/src/db/encrypted/engine.rs index cf7fc4652..d11b42dbd 100644 --- a/wallet/src/db/encrypted/engine.rs +++ b/wallet/src/db/encrypted/engine.rs @@ -1,6 +1,7 @@ -use super::*; use crate::types; +use super::*; + #[derive(Clone)] pub struct CryptoEngine { key: types::Secret, @@ -31,4 +32,16 @@ impl CryptoEngine { Ok(value) } + + pub fn decrypt_with(&self, bytes: &[u8], with: F) -> Result + where + T: DeserializeOwned, + F: Fn(&[u8]) -> Vec, + { + let decrypted = cipher::decrypt_aes_cbc(self.key.as_ref(), bytes, &self.iv)?; + let with_bytes = with(&decrypted); + let value = bincode::deserialize(&with_bytes)?; + + Ok(value) + } } diff --git a/wallet/src/db/encrypted/mod.rs b/wallet/src/db/encrypted/mod.rs index 7309b428e..a7fb1d283 100644 --- a/wallet/src/db/encrypted/mod.rs +++ b/wallet/src/db/encrypted/mod.rs @@ -1,9 +1,13 @@ +use serde::de::DeserializeOwned; use std::sync::Arc; use witnet_crypto::cipher; use super::*; -use crate::{db::encrypted::write_batch::EncryptedWriteBatch, types}; +use crate::{ + db::{encrypted::write_batch::EncryptedWriteBatch, GetWith}, + types, +}; mod engine; mod prefix; @@ -101,3 +105,25 @@ impl Database for EncryptedDb { EncryptedWriteBatch::new(self.prefixer.clone(), self.engine.clone()) } } + +impl GetWith for EncryptedDb { + fn get_with_opt(&self, key: &Key, with: F) -> Result> + where + K: AsRef<[u8]>, + V: DeserializeOwned, + F: Fn(&[u8]) -> Vec, + { + let prefix_key = self.prefixer.prefix(key); + let enc_key = self.engine.encrypt(&prefix_key)?; + let res = self.as_ref().get(enc_key)?; + + match res { + Some(dbvec) => { + let value = self.engine.decrypt_with(&dbvec, with)?; + + Ok(Some(value)) + } + None => Ok(None), + } + } +} diff --git a/wallet/src/db/mod.rs b/wallet/src/db/mod.rs index c29a7a3f2..f5ac6370b 100644 --- a/wallet/src/db/mod.rs +++ b/wallet/src/db/mod.rs @@ -71,3 +71,23 @@ pub trait WriteBatch { V: serde::Serialize + ?Sized, Vref: Borrow; } + +pub trait GetWith { + fn get_with(&self, key: &Key, with: F) -> Result + where + K: AsRef<[u8]> + Debug, + V: serde::de::DeserializeOwned, + F: Fn(&[u8]) -> Vec, + { + let opt = self.get_with_opt(key, with)?; + + opt.ok_or_else(|| Error::DbKeyNotFound { + key: format!("{:?}", key), + }) + } + fn get_with_opt(&self, key: &Key, with: F) -> Result> + where + K: AsRef<[u8]>, + V: serde::de::DeserializeOwned, + F: Fn(&[u8]) -> Vec; +} diff --git a/wallet/src/db/tests.rs b/wallet/src/db/tests.rs index 2d4275674..98bfeb3ee 100644 --- a/wallet/src/db/tests.rs +++ b/wallet/src/db/tests.rs @@ -1,3 +1,4 @@ +use serde::de::DeserializeOwned; use std::{cell::RefCell, collections::HashMap, rc::Rc}; use super::*; @@ -137,6 +138,26 @@ impl IntoIterator for HashMapWriteBatch { } } +impl GetWith for HashMapDb { + fn get_with_opt(&self, key: &Key, with: F) -> Result> + where + K: AsRef<[u8]>, + V: DeserializeOwned, + F: Fn(&[u8]) -> Vec, + { + let k = key.as_ref().to_vec(); + let res = match RefCell::borrow(&self.rc).get(&k) { + Some(value) => { + let value = with(value); + Some(bincode::deserialize(&value)?) + } + None => None, + }; + + Ok(res) + } +} + #[test] fn test_hashmap_db() { let db = HashMapDb::default(); diff --git a/wallet/src/model.rs b/wallet/src/model.rs index ec8ac954b..35ef8bdba 100644 --- a/wallet/src/model.rs +++ b/wallet/src/model.rs @@ -187,6 +187,8 @@ pub enum TransactionData { Mint(MintData), #[serde(rename = "commit")] Commit(VtData), + // #[serde(rename = "stake")] + // Stake(StakeData), } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] diff --git a/wallet/src/repository/wallet/mod.rs b/wallet/src/repository/wallet/mod.rs index 451586ad0..5fde718d0 100644 --- a/wallet/src/repository/wallet/mod.rs +++ b/wallet/src/repository/wallet/mod.rs @@ -13,7 +13,7 @@ use state::State; use witnet_crypto::{ hash::calculate_sha256, key::{ExtendedPK, ExtendedSK, KeyPath, PK}, - signature, + secp256k1, signature, }; use witnet_data_structures::{ chain::{ @@ -37,7 +37,7 @@ use witnet_util::timestamp::get_timestamp; use crate::{ constants, crypto, - db::{Database, WriteBatch as _}, + db::{Database, GetWith, WriteBatch as _}, model, params::Params, types, @@ -189,7 +189,7 @@ pub struct Wallet { impl Wallet where - T: Database, + T: Database + GetWith, { /// Generate transient addresses for synchronization purposes /// This function only creates and inserts addresses @@ -295,6 +295,11 @@ where let id = id.to_owned(); let name = db.get_opt(&keys::wallet_name())?; let description = db.get_opt(&keys::wallet_description())?; + log::debug!( + "Unlocking wallet with name '{}' and description '{}'", + name.clone().unwrap_or_default(), + description.clone().unwrap_or_default() + ); let account = db.get_or_default(&keys::wallet_default_account())?; let available_accounts = db .get_opt(&keys::wallet_accounts())? @@ -333,6 +338,7 @@ where unconfirmed: balance_info, confirmed: balance_info, }; + log::debug!("Wallet {id} has balance: {:?}", balance); let last_sync = db .get(&keys::wallet_last_sync()) @@ -342,17 +348,34 @@ where }); let last_confirmed = last_sync; + log::debug!( + "Wallet {id} has last_sync={:?} and last_confirmed={:?}", + last_sync, + last_confirmed + ); - let external_key = db.get(&keys::account_key(account, constants::EXTERNAL_KEYCHAIN))?; + let external_key = db.get_with( + &keys::account_key(account, constants::EXTERNAL_KEYCHAIN), + backwards_compatible_keypair_decoding, + )?; let next_external_index = db.get_or_default(&keys::account_next_index( account, constants::EXTERNAL_KEYCHAIN, ))?; - let internal_key = db.get(&keys::account_key(account, constants::INTERNAL_KEYCHAIN))?; + log::debug!( + "Loaded external keys for wallet {id}. Next external index is {next_external_index}." + ); + let internal_key = db.get_with( + &keys::account_key(account, constants::INTERNAL_KEYCHAIN), + backwards_compatible_keypair_decoding, + )?; let next_internal_index = db.get_or_default(&keys::account_next_index( account, constants::INTERNAL_KEYCHAIN, ))?; + log::debug!( + "Loaded internal keys for wallet {id}. Next internal index is {next_internal_index}." + ); let keychains = [external_key, internal_key]; let epoch_constants = params.epoch_constants; let birth_date = db.get(&keys::birth_date()).unwrap_or(CheckpointBeacon { @@ -1144,7 +1167,7 @@ where .map(Input::new) .collect_vec(); - let body = DRTransactionBody::new(pointers_as_inputs.clone(), outputs, request); + let body = DRTransactionBody::new(pointers_as_inputs.clone(), request, outputs); let sign_data = body.hash(); let signatures = self.create_signatures_from_inputs(pointers_as_inputs, sign_data, &mut state); @@ -1490,6 +1513,8 @@ where Transaction::Reveal(_) => None, Transaction::Tally(_) => None, Transaction::Mint(_) => None, + Transaction::Stake(tx) => Some(&tx.body.inputs), + Transaction::Unstake(_) => None, }; let empty_hashset = HashSet::default(); @@ -2273,6 +2298,15 @@ fn vtt_to_outputs( .collect::>() } +#[inline] +fn backwards_compatible_keypair_decoding(bytes: &[u8]) -> Vec { + let skip = bytes + .len() + .saturating_sub(8 + 2 * secp256k1::constants::SECRET_KEY_SIZE); + + bytes[skip..].to_vec() +} + #[cfg(test)] impl Wallet where @@ -2359,3 +2393,16 @@ fn test_get_tx_ranges_exceed() { assert_eq!(pending_range, None); assert_eq!(db_range, Some(0..5)); } + +#[test] +fn test_backwards_compatible_keypair_decoding() { + // Test that the old keypair prefix is detected and omitted + let old = backwards_compatible_keypair_decoding(&hex::decode("20000000000000001837c1be8e2995ec11cda2b066151be2cfb48adf9e47b151d46adab3a21cdf6720000000000000007923408dadd3c7b56eed15567707ae5e5dca089de972e07f3b860450e2a3b70e").unwrap()); + let new = backwards_compatible_keypair_decoding(&hex::decode("1837c1be8e2995ec11cda2b066151be2cfb48adf9e47b151d46adab3a21cdf6720000000000000007923408dadd3c7b56eed15567707ae5e5dca089de972e07f3b860450e2a3b70e").unwrap()); + assert_eq!(old, new); + + // Test that shorter strings don't get clipped or panic + let short = hex::decode("fabadaacabadaa").unwrap(); + let compatible = backwards_compatible_keypair_decoding(&short); + assert_eq!(short, compatible); +} diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 84c9a9513..4286895ee 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -22,7 +22,8 @@ use witnet_data_structures::{ fee::Fee, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, MintTransaction, RevealTransaction, - TallyTransaction, Transaction, VTTransaction, VTTransactionBody, + StakeTransaction, TallyTransaction, Transaction, UnstakeTransaction, VTTransaction, + VTTransactionBody, }, utxo_pool::UtxoSelectionStrategy, }; @@ -121,6 +122,7 @@ pub struct Account { pub internal: ExtendedSK, } +#[derive(Debug)] pub struct WalletData { pub id: String, pub name: Option, @@ -322,6 +324,8 @@ pub enum TransactionHelper { Reveal(RevealTransaction), Tally(TallyTransaction), Mint(MintTransaction), + Stake(StakeTransaction), + Unstake(UnstakeTransaction), } impl From for TransactionHelper { @@ -337,6 +341,10 @@ impl From for TransactionHelper { Transaction::Reveal(revealtransaction) => TransactionHelper::Reveal(revealtransaction), Transaction::Tally(tallytransaction) => TransactionHelper::Tally(tallytransaction), Transaction::Mint(minttransaction) => TransactionHelper::Mint(minttransaction), + Transaction::Stake(staketransaction) => TransactionHelper::Stake(staketransaction), + Transaction::Unstake(unstaketransaction) => { + TransactionHelper::Unstake(unstaketransaction) + } } } } @@ -354,6 +362,10 @@ impl From for Transaction { TransactionHelper::Reveal(revealtransaction) => Transaction::Reveal(revealtransaction), TransactionHelper::Tally(tallytransaction) => Transaction::Tally(tallytransaction), TransactionHelper::Mint(minttransaction) => Transaction::Mint(minttransaction), + TransactionHelper::Stake(staketransaction) => Transaction::Stake(staketransaction), + TransactionHelper::Unstake(unstaketransaction) => { + Transaction::Unstake(unstaketransaction) + } } } } @@ -413,7 +425,7 @@ impl From for DRTransactionBodyHelper { impl From for DRTransactionBody { fn from(x: DRTransactionBodyHelper) -> Self { - DRTransactionBody::new(x.inputs, x.outputs, x.dr_output) + DRTransactionBody::new(x.inputs, x.dr_output, x.outputs) } } diff --git a/witnet_centralized_ethereum_bridge.toml b/witnet_centralized_ethereum_bridge.toml index f07e7b504..1f3909e31 100644 --- a/witnet_centralized_ethereum_bridge.toml +++ b/witnet_centralized_ethereum_bridge.toml @@ -1,60 +1,69 @@ -# Address of the witnet node JSON-RPC server -witnet_jsonrpc_addr = "127.0.0.1:21338" +# Ethereum account used to create the transactions +eth_from = "0x8c49CAfC4542D9EA9107D4E48412ACEd2A68aA77" + +# Ethereum account balance under which alerts will be logged +eth_from_balance_threshold = 100000000000000000 # Url of the ethereum client -eth_client_url = "http://127.0.0.1:8544" +eth_jsonrpc_url = "http://127.0.0.1:8503" + +# Max number of queries to be batched together +eth_max_batch_size = 64 + +# Price of $nanoWit in Wei, used to improve estimation of report profits +eth_nanowit_wei_price = 1 + +# Polling period for checking new queries in the WitnetOracle contract +eth_new_drs_polling_rate_ms = 45_000 + +# Number of block confirmations needed to assume finality when sending transactions to ethereum +eth_txs_confirmations = 2 + +# Max time to wait for an ethereum transaction to be confirmed before returning an error +eth_txs_timeout_ms = 900000 # Address of the WitnetRequestsBoard deployed contract -wrb_contract_addr = "0x6cE42a35C61ccfb42907EEE57eDF14Bb69C7fEF4" +eth_witnet_oracle = "0x77703aE126B971c9946d562F41Dd47071dA00777" -# Address of a Request Example deployed contract -request_example_contract_addr = "0xEaA9e7Ea612b169f5b41cfF86dA6322f57264a19" +# Let the dog out? +watch_dog_enabled = true -# Ethereum account used to create the transactions -eth_account = "0x8d86Bc475bEDCB08179c5e6a4d494EbD3b44Ea8B" +# Polling period for checking and tracing global status +watch_dog_polling_rate_ms = 5_000 -# Period to check for new requests in the WRB -eth_new_dr_polling_rate_ms = 45_000 +# Minimum collateral required on data requests read from the WitnetOracle contract +witnet_dr_min_collateral_nanowits = 20_000_000_000 -# Period to check for completed requests in Witnet -wit_tally_polling_rate_ms = 45_000 +# Maximium data request transaction fee assumed by the bridge +witnet_dr_max_fee_nanowits = 100_000 -# Period to post new requests to Witnet -wit_dr_sender_polling_rate_ms = 45_000 +# Maximum data request result size (in bytes) will accept to report +witnet_dr_max_result_size = 64 -# If the data request has been sent to witnet but it is not included in a block, retry after this many milliseconds -dr_tx_unresolved_timeout_ms = 600_000 # 10 minutes +# Maximum data request value that the bridge will accept to relay +witnet_dr_max_value_nanowits = 100_000_000_000 -# Maximum data request result size (in bytes) -# TODO: Choose a proper value -max_result_size = 100 +# Polling period for checking resolution of data requests in the Witnet blockchain +witnet_dr_txs_polling_rate_ms = 45000 -# Max time to wait for an ethereum transaction to be confirmed before returning an error -eth_confirmation_timeout_ms = 900_000 # 15 minutes +# Max time to wait for data request resolutions, in milliseconds +witnet_dr_txs_timeout_ms = 600000 -# Max value that will be accepted by the bridge node in a data request -# This is the maximum amount that the relayer is willing to lose per one data request -max_dr_value_nanowits = 100_000_000_000 +# Address of the witnet node JSON-RPC server +witnet_jsonrpc_socket = "127.0.0.1:21338" # Running in the witnet testnet? witnet_testnet = false -# Number of block confirmations needed to assume finality when sending transactions to ethereum -num_confirmations = 1 - -# Miner fee for the witnet data request transactions, in nanowits -dr_fee_nanowits = 10_000 - -# Max ratio between the gas price recommended by the provider and the gas price of the requests in the WRB -# That is, the bridge will refrain from paying more than these times the gas price originally set forth by the requesters. -report_result_max_network_gas_price_ratio = 1.0 +# Bridge UTXO min value threshold +witnet_utxo_min_value_threshold = 2_000_000_000 +[eth_gas_limits] # Gas limits for some methods. # To let the client estimate, comment out the fields -[gas_limits] -post_data_request = 10000000 -report_result = 2000000 +#report_result = 2000000 [storage] # Path of the folder where RocksDB storage files will be written to. db_path = ".witnet_bridge/storage" + diff --git a/witnet_ethereum_bridge.toml b/witnet_ethereum_bridge.toml index 84d296e59..92487143f 100644 --- a/witnet_ethereum_bridge.toml +++ b/witnet_ethereum_bridge.toml @@ -1,13 +1,13 @@ # Address of the witnet node JSON-RPC server -witnet_jsonrpc_addr = "127.0.0.1:21336" +witnet_jsonrpc_socket = "127.0.0.1:21336" # Url of the ethereum client -eth_client_url = "http://127.0.0.1:8888" +eth_jsonrpc_url = "http://127.0.0.1:8888" # Address of the WitnetRequestsBoard deployed contract -wrb_contract_addr = "0x354B08f9fD4b171e774898261908DfeA113b0e14" +eth_witnet_oracle = "0x354B08f9fD4b171e774898261908DfeA113b0e14" # Address of the BlockRelay deployed contract block_relay_contract_addr = "0xEaA9e7Ea612b169f5b41cfF86dA6322f57264a19" # Ethereum account used to create the transactions -eth_account = "0x333b18f64949C59Cd0b84b51C7580f260c563c31" +eth_from = "0x333b18f64949C59Cd0b84b51C7580f260c563c31" # Enable block relay from witnet to ethereum, relay only new blocks # (blocks that were recently consolidated) enable_block_relay_new_blocks = true @@ -38,7 +38,7 @@ claim_dr_rate_ms = 30_000 # Period to check for state updates in existing requests in the WRB eth_existing_dr_polling_rate_ms = 10_000 # Period to check for new requests in the WRB -eth_new_dr_polling_rate_ms = 1_000 +eth_new_drs_polling_rate_ms = 1_000 # Running in the witnet testnet? witnet_testnet = false @@ -47,7 +47,7 @@ read_dr_hash_interval_ms = 10_000 # Gas limits for some methods. # To let the client estimate, comment out the fields -[gas_limits] +[eth_gas_limits] claim_data_requests = 5000000 post_data_request = 10000000 post_new_block = 2000000