diff --git a/Cargo.lock b/Cargo.lock index 9575885ce..a7163aee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,9 +436,9 @@ checksum = "b170cd256a3f9fa6b9edae3e44a7dfdfc77e8124dbc3e2612d75f9c3e2396dae" [[package]] name = "bstr" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502ae1441a0a5adb8fbd38a5955a6416b9493e92b465de5e4a9bde6a539c2c48" +checksum = "2889e6d50f394968c8bf4240dc3f2a7eb4680844d27308f798229ac9d4725f41" dependencies = [ "lazy_static", "memchr", @@ -1069,7 +1069,7 @@ dependencies = [ "frame-support", "frame-system", "hex-literal", - "pallet-session", + "pallet-session 0.5.0", "pallet-timestamp", "parity-scale-codec", "rlp 0.4.4 (git+https://github.com/darwinia-network/parity-common.git)", @@ -1078,7 +1078,7 @@ dependencies = [ "sp-core", "sp-io", "sp-runtime", - "sp-staking", + "sp-staking 0.5.0", "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", ] @@ -1158,14 +1158,14 @@ dependencies = [ "frame-support", "frame-system", "pallet-authorship", - "pallet-session", + "pallet-session 0.5.0", "pallet-timestamp", "parity-scale-codec", "serde", "sp-core", "sp-io", "sp-runtime", - "sp-staking", + "sp-staking 0.5.0", "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", "substrate-test-utils", ] @@ -3745,7 +3745,7 @@ dependencies = [ "node-primitives", "pallet-authority-discovery", "pallet-authorship", - "pallet-babe", + "pallet-babe 0.5.0", "pallet-collective", "pallet-contracts", "pallet-contracts-primitives", @@ -3759,7 +3759,7 @@ dependencies = [ "pallet-offences", "pallet-randomness-collective-flip", "pallet-recovery", - "pallet-session", + "pallet-session 0.5.0", "pallet-society", "pallet-sudo", "pallet-timestamp", @@ -3779,7 +3779,7 @@ dependencies = [ "sp-offchain", "sp-runtime", "sp-session", - "sp-staking", + "sp-staking 0.5.0", "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", "sp-transaction-pool", "sp-version", @@ -3956,12 +3956,11 @@ dependencies = [ [[package]] name = "pallet-authority-discovery" -version = "2.0.0-alpha.3" -source = "git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3#013c1ee167354a08283fb69915fda56a62fee943" +version = "0.5.0" dependencies = [ "frame-support", "frame-system", - "pallet-session", + "pallet-session 0.5.0", "parity-scale-codec", "serde", "sp-application-crypto", @@ -3969,6 +3968,7 @@ dependencies = [ "sp-core", "sp-io", "sp-runtime", + "sp-staking 0.5.0", "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", ] @@ -3991,38 +3991,48 @@ dependencies = [ [[package]] name = "pallet-babe" -version = "2.0.0-alpha.3" -source = "git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3#013c1ee167354a08283fb69915fda56a62fee943" +version = "0.5.0" dependencies = [ "frame-support", "frame-system", "hex-literal", - "pallet-session", + "lazy_static", + "pallet-session 0.5.0", "pallet-timestamp", "parity-scale-codec", + "parking_lot 0.10.0", "serde", "sp-consensus-babe", + "sp-core", "sp-inherents", "sp-io", "sp-runtime", - "sp-staking", + "sp-staking 0.5.0", "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", "sp-timestamp", + "sp-version", + "substrate-test-runtime", ] [[package]] -name = "pallet-balances" +name = "pallet-babe" version = "2.0.0-alpha.3" source = "git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3#013c1ee167354a08283fb69915fda56a62fee943" dependencies = [ - "frame-benchmarking", "frame-support", "frame-system", + "hex-literal", + "pallet-session 2.0.0-alpha.3", + "pallet-timestamp", "parity-scale-codec", "serde", + "sp-consensus-babe", + "sp-inherents", "sp-io", "sp-runtime", + "sp-staking 2.0.0-alpha.3", "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", + "sp-timestamp", ] [[package]] @@ -4119,19 +4129,19 @@ dependencies = [ [[package]] name = "pallet-grandpa" -version = "2.0.0-alpha.3" -source = "git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3#013c1ee167354a08283fb69915fda56a62fee943" +version = "0.5.0" dependencies = [ "frame-support", "frame-system", "pallet-finality-tracker", - "pallet-session", + "pallet-session 0.5.0", "parity-scale-codec", "serde", "sp-core", "sp-finality-grandpa", + "sp-io", "sp-runtime", - "sp-staking", + "sp-staking 0.5.0", "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", ] @@ -4153,20 +4163,19 @@ dependencies = [ [[package]] name = "pallet-im-online" -version = "2.0.0-alpha.3" -source = "git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3#013c1ee167354a08283fb69915fda56a62fee943" +version = "0.5.0" dependencies = [ "frame-support", "frame-system", "pallet-authorship", - "pallet-session", + "pallet-session 0.5.0", "parity-scale-codec", "serde", "sp-application-crypto", "sp-core", "sp-io", "sp-runtime", - "sp-staking", + "sp-staking 0.5.0", "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", ] @@ -4202,16 +4211,16 @@ dependencies = [ [[package]] name = "pallet-offences" -version = "2.0.0-alpha.3" -source = "git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3#013c1ee167354a08283fb69915fda56a62fee943" +version = "0.5.0" dependencies = [ "frame-support", "frame-system", - "pallet-balances", "parity-scale-codec", "serde", + "sp-core", + "sp-io", "sp-runtime", - "sp-staking", + "sp-staking 0.5.0", "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", ] @@ -4243,6 +4252,26 @@ dependencies = [ "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", ] +[[package]] +name = "pallet-session" +version = "0.5.0" +dependencies = [ + "frame-support", + "frame-system", + "impl-trait-for-tuples", + "lazy_static", + "pallet-timestamp", + "parity-scale-codec", + "serde", + "sp-application-crypto", + "sp-core", + "sp-io", + "sp-runtime", + "sp-staking 0.5.0", + "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", + "sp-trie", +] + [[package]] name = "pallet-session" version = "2.0.0-alpha.3" @@ -4256,7 +4285,7 @@ dependencies = [ "serde", "sp-io", "sp-runtime", - "sp-staking", + "sp-staking 2.0.0-alpha.3", "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", "sp-trie", ] @@ -4472,9 +4501,9 @@ checksum = "aa9777aa91b8ad9dd5aaa04a9b6bcb02c7f1deb952fca5a66034d5e63afc5c6f" [[package]] name = "parity-util-mem" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1476e40bf8f5c6776e9600983435821ca86eb9819d74a6207cca69d091406a" +checksum = "9344bc978467339b9ae688f9dcf279d1aaa0ccfc88e9a780c729b765a82d57d5" dependencies = [ "cfg-if", "impl-trait-for-tuples", @@ -5156,9 +5185,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.3.4" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8" +checksum = "8900ebc1363efa7ea1c399ccc32daed870b4002651e0bed86e72d501ebbe0048" dependencies = [ "aho-corasick", "memchr", @@ -5177,9 +5206,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.16" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1132f845907680735a84409c3bebc64d1364a5683ffbce899550cd09d5eaefc1" +checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" [[package]] name = "region" @@ -5341,9 +5370,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" +checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" [[package]] name = "safe-mix" @@ -6633,6 +6662,20 @@ dependencies = [ "sp-version", ] +[[package]] +name = "sp-consensus-aura" +version = "0.8.0-alpha.3" +source = "git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3#013c1ee167354a08283fb69915fda56a62fee943" +dependencies = [ + "parity-scale-codec", + "sp-api", + "sp-application-crypto", + "sp-inherents", + "sp-runtime", + "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", + "sp-timestamp", +] + [[package]] name = "sp-consensus-babe" version = "0.8.0-alpha.3" @@ -6889,6 +6932,15 @@ dependencies = [ "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", ] +[[package]] +name = "sp-staking" +version = "0.5.0" +dependencies = [ + "parity-scale-codec", + "sp-runtime", + "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", +] + [[package]] name = "sp-staking" version = "2.0.0-alpha.3" @@ -7184,6 +7236,45 @@ dependencies = [ "tokio 0.2.13", ] +[[package]] +name = "substrate-test-runtime" +version = "2.0.0-dev" +source = "git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3#013c1ee167354a08283fb69915fda56a62fee943" +dependencies = [ + "cfg-if", + "frame-executive", + "frame-support", + "frame-system", + "frame-system-rpc-runtime-api", + "log 0.4.8", + "memory-db", + "pallet-babe 2.0.0-alpha.3", + "pallet-timestamp", + "parity-scale-codec", + "parity-util-mem", + "sc-client", + "serde", + "sp-api", + "sp-application-crypto", + "sp-block-builder", + "sp-consensus-aura", + "sp-consensus-babe", + "sp-core", + "sp-inherents", + "sp-io", + "sp-keyring", + "sp-offchain", + "sp-runtime", + "sp-runtime-interface", + "sp-session", + "sp-std 2.0.0-alpha.3 (git+https://github.com/darwinia-network/substrate.git?tag=v2.0.0-alpha.3)", + "sp-transaction-pool", + "sp-trie", + "sp-version", + "substrate-wasm-builder-runner", + "trie-db", +] + [[package]] name = "substrate-test-utils" version = "2.0.0-alpha.3" diff --git a/Cargo.toml b/Cargo.toml index 69b52ffec..17f36705d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,20 +11,27 @@ members = [ # "bin/node/testing", "bin/node/transaction-factory", "client/cli", + "frame/authority-discovery", + "frame/babe", "frame/balances/kton", "frame/balances/ring", "frame/claims", "frame/chainrelay/eth/backing", "frame/chainrelay/eth/relay", "frame/elections-phragmen", + "frame/grandpa", + "frame/im-online", + "frame/offences", + "frame/session", "frame/staking", "frame/support", "frame/treasury", "frame/vesting", "primitives/ethash", + "primitives/eth-primitives", "primitives/merkle-patricia-trie", "primitives/phragmen", - "primitives/eth-primitives", + "primitives/staking", ] [profile.release] diff --git a/bin/node/cli/Cargo.toml b/bin/node/cli/Cargo.toml index 0141d8e61..c0d87d87f 100644 --- a/bin/node/cli/Cargo.toml +++ b/bin/node/cli/Cargo.toml @@ -80,9 +80,11 @@ sp-transaction-pool = { git = "https://github.com/darwinia-network/substrate.git frame-support = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } frame-system = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-authority-discovery = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-authority-discovery = { path = "../../../frame/authority-discovery" } pallet-contracts = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-im-online = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-im-online = { default-features = false, path = "../../../frame/im-online" } pallet-indices = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-timestamp = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-transaction-payment = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } diff --git a/bin/node/cli/src/chain_spec.rs b/bin/node/cli/src/chain_spec.rs index f47c5c55d..50368ce35 100644 --- a/bin/node/cli/src/chain_spec.rs +++ b/bin/node/cli/src/chain_spec.rs @@ -293,7 +293,6 @@ pub fn darwinia_genesis( .collect(), }), pallet_staking: Some(StakingConfig { - current_era: 0, validator_count: initial_authorities.len() as u32 * 2, minimum_validator_count: initial_authorities.len() as u32, stakers: initial_authorities diff --git a/bin/node/executor/Cargo.toml b/bin/node/executor/Cargo.toml index 860756653..950c7f320 100644 --- a/bin/node/executor/Cargo.toml +++ b/bin/node/executor/Cargo.toml @@ -37,10 +37,13 @@ node-runtime = { path = "../runtime" } # #pallet-balances = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } #pallet-contracts = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -#pallet-grandpa = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -#pallet-im-online = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +#pallet-grandpa = { path = "../../../frame/grandpa" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +#pallet-im-online = { path = "../../../frame/session" } #pallet-indices = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -#pallet-session = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +#pallet-session = { path = "../../../frame/session" } #pallet-timestamp = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } #pallet-transaction-payment = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } #pallet-treasury = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 4f94dfcda..f533258f1 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -27,7 +27,8 @@ sp-keyring = { optional = true, git = "https://github.com/darwinia-network/subst sp-offchain = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } sp-runtime = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } sp-session = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -sp-staking = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +sp-staking = { default-features = false, path = "../../../primitives/staking" } sp-std = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } sp-transaction-pool = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } sp-version = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } @@ -42,24 +43,30 @@ frame-support = { default-features = false, git = "https://github.com/darwinia-n frame-system = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } frame-system-rpc-runtime-api = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-authority-discovery = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-authority-discovery = { default-features = false, path = "../../../frame/authority-discovery" } pallet-authorship = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-babe = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-babe = { default-features = false, path = "../../../frame/babe" } pallet-collective = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-contracts = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-contracts-primitives = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-contracts-rpc-runtime-api = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } #pallet-democracy = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-finality-tracker = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-grandpa = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-im-online = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-grandpa = { default-features = false, path = "../../../frame/grandpa" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-im-online = { default-features = false, path = "../../../frame/im-online" } pallet-indices = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-identity = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-membership = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-offences = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-offences = { default-features = false, path = "../../../frame/offences" } pallet-randomness-collective-flip = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-recovery = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-session = { default-features = false, features = ["historical"], git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-session = { default-features = false, features = ["historical"], path = "../../../frame/session" } pallet-society = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-sudo = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-timestamp = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 42111bd89..c4b781812 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -542,7 +542,8 @@ parameter_types! { pub const BondingDurationInEra: pallet_staking::EraIndex = 14 * 24 * (HOURS / (SESSIONS_PER_ERA * BLOCKS_PER_SESSION)); pub const BondingDurationInBlockNumber: BlockNumber = 14 * DAYS; pub const SlashDeferDuration: pallet_staking::EraIndex = 7 * 24; // 1/4 the bonding duration. - + pub const MaxNominatorRewardedPerValidator: u32 = 64; + // --- custom --- pub const Cap: Balance = CAP; pub const TotalPower: Power = TOTAL_POWER; } @@ -556,6 +557,7 @@ impl pallet_staking::Trait for Runtime { /// A super-majority of the council can cancel the slash. type SlashCancelOrigin = pallet_collective::EnsureProportionAtLeast<_3, _4, AccountId, CouncilCollective>; type SessionInterface = Self; + type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator; type RingCurrency = Ring; type RingRewardRemainder = Treasury; // send the slashed funds to the treasury. diff --git a/bin/node/testing/Cargo.toml b/bin/node/testing/Cargo.toml index 2dce3f6d9..69e7d1503 100644 --- a/bin/node/testing/Cargo.toml +++ b/bin/node/testing/Cargo.toml @@ -21,9 +21,11 @@ frame-support = { git = "https://github.com/darwinia-network/substrate.git", tag frame-system = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-contracts = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-grandpa = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-grandpa = { path = "../../../frame/grandpa" } pallet-indices = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-session = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-session = { path = "../../../frame/session" } pallet-society = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-timestamp = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-transaction-payment = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } diff --git a/bin/node/testing/src/genesis.rs b/bin/node/testing/src/genesis.rs index 4da012f03..490b4c136 100644 --- a/bin/node/testing/src/genesis.rs +++ b/bin/node/testing/src/genesis.rs @@ -63,7 +63,6 @@ pub fn config_endowed(support_changes_trie: bool, code: Option<&[u8]>, extra_end ], }), pallet_staking: Some(StakingConfig { - current_era: 0, stakers: vec![ (dave(), alice(), 111 * DOLLARS, pallet_staking::StakerStatus::Validator), (eve(), bob(), 100 * DOLLARS, pallet_staking::StakerStatus::Validator), diff --git a/frame/authority-discovery/Cargo.toml b/frame/authority-discovery/Cargo.toml new file mode 100644 index 000000000..5cf14829b --- /dev/null +++ b/frame/authority-discovery/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "pallet-authority-discovery" +version = "0.5.0" +authors = ["Darwinia Network "] +description = "FRAME pallet for authority discovery" +edition = "2018" +license = "GPL-3.0" +homepage = "https://darwinia.network/" +repository = "https://github.com/darwinia-network/darwinia/" + +[dependencies] +# crates.io +codec = { package = "parity-scale-codec", version = "1.2.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.101", optional = true } + +# github.com +frame-support = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +frame-system = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-session = { default-features = false, features = ["historical" ], path = "../session" } + +sp-application-crypto = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-authority-discovery = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-core = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-io = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-runtime = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-std = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +[dev-dependencies] +# TODO https://github.com/darwinia-network/darwinia/issues/347 +sp-staking = { default-features = false, path = "../../primitives/staking" } + +[features] +default = ["std"] +std = [ + "codec/std", + "serde", + + "frame-support/std", + "frame-system/std", + + "pallet-session/std", + + "sp-application-crypto/std", + "sp-authority-discovery/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] diff --git a/frame/authority-discovery/src/lib.rs b/frame/authority-discovery/src/lib.rs new file mode 100644 index 000000000..8ee4931e4 --- /dev/null +++ b/frame/authority-discovery/src/lib.rs @@ -0,0 +1,246 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! # Authority discovery module. +//! +//! This module is used by the `client/authority-discovery` to retrieve the +//! current set of authorities. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_std::prelude::*; +use frame_support::{decl_module, decl_storage}; +use sp_authority_discovery::AuthorityId; + +/// The module's config trait. +pub trait Trait: frame_system::Trait + pallet_session::Trait {} + +decl_storage! { + trait Store for Module as AuthorityDiscovery { + /// Keys of the current authority set. + Keys get(fn keys): Vec; + } + add_extra_genesis { + config(keys): Vec; + build(|config| Module::::initialize_keys(&config.keys)) + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + } +} + +impl Module { + /// Retrieve authority identifiers of the current authority set. + pub fn authorities() -> Vec { + Keys::get() + } + + fn initialize_keys(keys: &[AuthorityId]) { + if !keys.is_empty() { + assert!(Keys::get().is_empty(), "Keys are already initialized!"); + Keys::put(keys); + } + } +} + +impl sp_runtime::BoundToRuntimeAppPublic for Module { + type Public = AuthorityId; +} + +impl pallet_session::OneSessionHandler for Module { + type Key = AuthorityId; + + fn on_genesis_session<'a, I: 'a>(authorities: I) + where + I: Iterator, + { + let keys = authorities.map(|x| x.1).collect::>(); + Self::initialize_keys(&keys); + } + + fn on_new_session<'a, I: 'a>(changed: bool, validators: I, _queued_validators: I) + where + I: Iterator, + { + // Remember who the authorities are for the new session. + if changed { + Keys::put(validators.map(|x| x.1).collect::>()); + } + } + + fn on_disabled(_i: usize) { + // ignore + } +} + +#[cfg(test)] +mod tests { + use super::*; + use sp_authority_discovery::{AuthorityPair}; + use sp_application_crypto::Pair; + use sp_core::{crypto::key_types, H256}; + use sp_io::TestExternalities; + use sp_runtime::{ + testing::{Header, UintAuthorityId}, traits::{ConvertInto, IdentityLookup, OpaqueKeys}, + Perbill, KeyTypeId, + }; + use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; + + type AuthorityDiscovery = Module; + + #[derive(Clone, Eq, PartialEq)] + pub struct Test; + impl Trait for Test {} + + parameter_types! { + pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(33); + } + + impl pallet_session::Trait for Test { + type SessionManager = (); + type Keys = UintAuthorityId; + type ShouldEndSession = pallet_session::PeriodicSessions; + type SessionHandler = TestSessionHandler; + type Event = (); + type ValidatorId = AuthorityId; + type ValidatorIdOf = ConvertInto; + type DisabledValidatorsThreshold = DisabledValidatorsThreshold; + } + + impl pallet_session::historical::Trait for Test { + type FullIdentification = (); + type FullIdentificationOf = (); + } + + pub type BlockNumber = u64; + + parameter_types! { + pub const Period: BlockNumber = 1; + pub const Offset: BlockNumber = 0; + pub const UncleGenerations: u64 = 0; + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + } + + impl frame_system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = BlockNumber; + type Call = (); + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AuthorityId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type AvailableBlockRatio = AvailableBlockRatio; + type MaximumBlockLength = MaximumBlockLength; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + } + + impl_outer_origin! { + pub enum Origin for Test where system = frame_system {} + } + + pub struct TestSessionHandler; + impl pallet_session::SessionHandler for TestSessionHandler { + const KEY_TYPE_IDS: &'static [KeyTypeId] = &[key_types::DUMMY]; + + fn on_new_session( + _changed: bool, + _validators: &[(AuthorityId, Ks)], + _queued_validators: &[(AuthorityId, Ks)], + ) { + } + + fn on_disabled(_validator_index: usize) {} + + fn on_genesis_session(_validators: &[(AuthorityId, Ks)]) {} + } + + #[test] + fn authorities_returns_current_authority_set() { + // The whole authority discovery module ignores account ids, but we still need it for + // `pallet_session::OneSessionHandler::on_new_session`, thus its safe to use the same value everywhere. + let account_id = AuthorityPair::from_seed_slice(vec![10; 32].as_ref()).unwrap().public(); + + let first_authorities: Vec = vec![0, 1].into_iter() + .map(|i| AuthorityPair::from_seed_slice(vec![i; 32].as_ref()).unwrap().public()) + .map(AuthorityId::from) + .collect(); + + let second_authorities: Vec = vec![2, 3].into_iter() + .map(|i| AuthorityPair::from_seed_slice(vec![i; 32].as_ref()).unwrap().public()) + .map(AuthorityId::from) + .collect(); + + // Needed for `pallet_session::OneSessionHandler::on_new_session`. + let second_authorities_and_account_ids: Vec<(&AuthorityId, AuthorityId)> = second_authorities.clone() + .into_iter() + .map(|id| (&account_id, id)) + .collect(); + + // Build genesis. + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + GenesisConfig { + keys: vec![], + } + .assimilate_storage::(&mut t) + .unwrap(); + + // Create externalities. + let mut externalities = TestExternalities::new(t); + + externalities.execute_with(|| { + use pallet_session::OneSessionHandler; + + AuthorityDiscovery::on_genesis_session( + first_authorities.iter().map(|id| (id, id.clone())) + ); + assert_eq!(first_authorities, AuthorityDiscovery::authorities()); + + // When `changed` set to false, the authority set should not be updated. + AuthorityDiscovery::on_new_session( + false, + second_authorities_and_account_ids.clone().into_iter(), + vec![].into_iter(), + ); + assert_eq!(first_authorities, AuthorityDiscovery::authorities()); + + // When `changed` set to true, the authority set should be updated. + AuthorityDiscovery::on_new_session( + true, + second_authorities_and_account_ids.into_iter(), + vec![].into_iter(), + ); + assert_eq!(second_authorities, AuthorityDiscovery::authorities()); + }); + } +} diff --git a/frame/babe/Cargo.toml b/frame/babe/Cargo.toml new file mode 100644 index 000000000..33fb3373b --- /dev/null +++ b/frame/babe/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "pallet-babe" +version = "0.5.0" +authors = ["Darwinia Network "] +description = "Consensus extension module for BABE consensus. Collects on-chain randomness from VRF outputs and manages epoch transitions." +edition = "2018" +license = "GPL-3.0" +homepage = "https://darwinia.network/" +repository = "https://github.com/darwinia-network/darwinia/" + +[dependencies] +# crates.io +codec = { package = "parity-scale-codec", version = "1.2.0", default-features = false, features = ["derive"] } +hex-literal = "0.2.1" +serde = { version = "1.0.101", optional = true } + +# github.com +frame-support = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +frame-system = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-session = { default-features = false, path = "../session" } +pallet-timestamp = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +sp-consensus-babe = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-inherents = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-io ={ default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-runtime = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +sp-staking = { default-features = false, path = "../../primitives/staking" } +sp-std = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-timestamp = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +[dev-dependencies] +lazy_static = "1.4.0" +parking_lot = "0.10.0" + +sp-core = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-version = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +substrate-test-runtime = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +[features] +default = ["std"] +std = [ + "codec/std", + "serde", + + "frame-support/std", + "frame-system/std", + + "pallet-session/std", + "pallet-timestamp/std", + + "sp-consensus-babe/std", + "sp-inherents/std", + "sp-io/std", + "sp-runtime/std", + "sp-staking/std", + "sp-std/std", + "sp-timestamp/std", +] diff --git a/frame/babe/src/lib.rs b/frame/babe/src/lib.rs new file mode 100644 index 000000000..4dc9304fa --- /dev/null +++ b/frame/babe/src/lib.rs @@ -0,0 +1,556 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Consensus extension module for BABE consensus. Collects on-chain randomness +//! from VRF outputs and manages epoch transitions. + +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unused_must_use, unsafe_code, unused_variables, unused_must_use)] +#![deny(unused_imports)] +pub use pallet_timestamp; + +use sp_std::{result, prelude::*}; +use frame_support::{decl_storage, decl_module, traits::{FindAuthor, Get, Randomness as RandomnessT}}; +use sp_timestamp::OnTimestampSet; +use sp_runtime::{generic::DigestItem, ConsensusEngineId, Perbill, PerThing}; +use sp_runtime::traits::{IsMember, SaturatedConversion, Saturating, Hash}; +use sp_staking::{ + SessionIndex, + offence::{Offence, Kind}, +}; + +use codec::{Encode, Decode}; +use sp_inherents::{InherentIdentifier, InherentData, ProvideInherent, MakeFatalError}; +use sp_consensus_babe::{ + BABE_ENGINE_ID, ConsensusLog, BabeAuthorityWeight, SlotNumber, + inherents::{INHERENT_IDENTIFIER, BabeInherentData}, + digests::{NextEpochDescriptor, RawPreDigest}, +}; +pub use sp_consensus_babe::{AuthorityId, VRF_OUTPUT_LENGTH, PUBLIC_KEY_LENGTH}; + +#[cfg(all(feature = "std", test))] +mod tests; + +#[cfg(all(feature = "std", test))] +mod mock; + +pub trait Trait: pallet_timestamp::Trait { + /// The amount of time, in slots, that each epoch should last. + type EpochDuration: Get; + + /// The expected average block time at which BABE should be creating + /// blocks. Since BABE is probabilistic it is not trivial to figure out + /// what the expected average block time should be based on the slot + /// duration and the security parameter `c` (where `1 - c` represents + /// the probability of a slot being empty). + type ExpectedBlockTime: Get; + + /// BABE requires some logic to be triggered on every block to query for whether an epoch + /// has ended and to perform the transition to the next epoch. + /// + /// Typically, the `ExternalTrigger` type should be used. An internal trigger should only be used + /// when no other module is responsible for changing authority set. + type EpochChangeTrigger: EpochChangeTrigger; +} + +/// Trigger an epoch change, if any should take place. +pub trait EpochChangeTrigger { + /// Trigger an epoch change, if any should take place. This should be called + /// during every block, after initialization is done. + fn trigger(now: T::BlockNumber); +} + +/// A type signifying to BABE that an external trigger +/// for epoch changes (e.g. pallet-session) is used. +pub struct ExternalTrigger; + +impl EpochChangeTrigger for ExternalTrigger { + fn trigger(_: T::BlockNumber) { } // nothing - trigger is external. +} + +/// A type signifying to BABE that it should perform epoch changes +/// with an internal trigger, recycling the same authorities forever. +pub struct SameAuthoritiesForever; + +impl EpochChangeTrigger for SameAuthoritiesForever { + fn trigger(now: T::BlockNumber) { + if >::should_epoch_change(now) { + let authorities = >::authorities(); + let next_authorities = authorities.clone(); + + >::enact_epoch_change(authorities, next_authorities); + } + } +} + +/// The length of the BABE randomness +pub const RANDOMNESS_LENGTH: usize = 32; + +const UNDER_CONSTRUCTION_SEGMENT_LENGTH: usize = 256; + +type MaybeVrf = Option<[u8; 32 /* VRF_OUTPUT_LENGTH */]>; + +decl_storage! { + trait Store for Module as Babe { + /// Current epoch index. + pub EpochIndex get(fn epoch_index): u64; + + /// Current epoch authorities. + pub Authorities get(fn authorities): Vec<(AuthorityId, BabeAuthorityWeight)>; + + /// The slot at which the first epoch actually started. This is 0 + /// until the first block of the chain. + pub GenesisSlot get(fn genesis_slot): u64; + + /// Current slot number. + pub CurrentSlot get(fn current_slot): u64; + + /// The epoch randomness for the *current* epoch. + /// + /// # Security + /// + /// This MUST NOT be used for gambling, as it can be influenced by a + /// malicious validator in the short term. It MAY be used in many + /// cryptographic protocols, however, so long as one remembers that this + /// (like everything else on-chain) it is public. For example, it can be + /// used where a number is needed that cannot have been chosen by an + /// adversary, for purposes such as public-coin zero-knowledge proofs. + // NOTE: the following fields don't use the constants to define the + // array size because the metadata API currently doesn't resolve the + // variable to its underlying value. + pub Randomness get(fn randomness): [u8; 32 /* RANDOMNESS_LENGTH */]; + + /// Next epoch randomness. + NextRandomness: [u8; 32 /* RANDOMNESS_LENGTH */]; + + /// Randomness under construction. + /// + /// We make a tradeoff between storage accesses and list length. + /// We store the under-construction randomness in segments of up to + /// `UNDER_CONSTRUCTION_SEGMENT_LENGTH`. + /// + /// Once a segment reaches this length, we begin the next one. + /// We reset all segments and return to `0` at the beginning of every + /// epoch. + SegmentIndex build(|_| 0): u32; + UnderConstruction: map hasher(blake2_256) u32 => Vec<[u8; 32 /* VRF_OUTPUT_LENGTH */]>; + + /// Temporary value (cleared at block finalization) which is `Some` + /// if per-block initialization has already been called for current block. + Initialized get(fn initialized): Option; + } + add_extra_genesis { + config(authorities): Vec<(AuthorityId, BabeAuthorityWeight)>; + build(|config| Module::::initialize_authorities(&config.authorities)) + } +} + +decl_module! { + /// The BABE Pallet + pub struct Module for enum Call where origin: T::Origin { + /// The number of **slots** that an epoch takes. We couple sessions to + /// epochs, i.e. we start a new session once the new epoch begins. + const EpochDuration: u64 = T::EpochDuration::get(); + + /// The expected average block time at which BABE should be creating + /// blocks. Since BABE is probabilistic it is not trivial to figure out + /// what the expected average block time should be based on the slot + /// duration and the security parameter `c` (where `1 - c` represents + /// the probability of a slot being empty). + const ExpectedBlockTime: T::Moment = T::ExpectedBlockTime::get(); + + /// Initialization + fn on_initialize(now: T::BlockNumber) { + Self::do_initialize(now); + } + + /// Block finalization + fn on_finalize() { + // at the end of the block, we can safely include the new VRF output + // from this block into the under-construction randomness. If we've determined + // that this block was the first in a new epoch, the changeover logic has + // already occurred at this point, so the under-construction randomness + // will only contain outputs from the right epoch. + if let Some(Some(vrf_output)) = Initialized::take() { + Self::deposit_vrf_output(&vrf_output); + } + } + } +} + +impl RandomnessT<::Hash> for Module { + fn random(subject: &[u8]) -> T::Hash { + let mut subject = subject.to_vec(); + subject.reserve(VRF_OUTPUT_LENGTH); + subject.extend_from_slice(&Self::randomness()[..]); + + ::Hashing::hash(&subject[..]) + } +} + +/// A BABE public key +pub type BabeKey = [u8; PUBLIC_KEY_LENGTH]; + +impl FindAuthor for Module { + fn find_author<'a, I>(digests: I) -> Option where + I: 'a + IntoIterator + { + for (id, mut data) in digests.into_iter() { + if id == BABE_ENGINE_ID { + let pre_digest = RawPreDigest::decode(&mut data).ok()?; + return Some(match pre_digest { + RawPreDigest::Primary { authority_index, .. } => + authority_index, + RawPreDigest::Secondary { authority_index, .. } => + authority_index, + }); + } + } + + return None; + } +} + +impl IsMember for Module { + fn is_member(authority_id: &AuthorityId) -> bool { + >::authorities() + .iter() + .any(|id| &id.0 == authority_id) + } +} + +impl pallet_session::ShouldEndSession for Module { + fn should_end_session(now: T::BlockNumber) -> bool { + // it might be (and it is in current implementation) that session module is calling + // should_end_session() from it's own on_initialize() handler + // => because pallet_session on_initialize() is called earlier than ours, let's ensure + // that we have synced with digest before checking if session should be ended. + Self::do_initialize(now); + + Self::should_epoch_change(now) + } +} + +// TODO [slashing]: @marcio use this, remove the dead_code annotation. +/// A BABE equivocation offence report. +/// +/// When a validator released two or more blocks at the same slot. +struct BabeEquivocationOffence { + /// A babe slot number in which this incident happened. + slot: u64, + /// The session index in which the incident happened. + session_index: SessionIndex, + /// The size of the validator set at the time of the offence. + validator_set_count: u32, + /// The authority that produced the equivocation. + offender: FullIdentification, +} + +impl Offence for BabeEquivocationOffence { + const ID: Kind = *b"babe:equivocatio"; + type TimeSlot = u64; + + fn offenders(&self) -> Vec { + vec![self.offender.clone()] + } + + fn session_index(&self) -> SessionIndex { + self.session_index + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> Self::TimeSlot { + self.slot + } + + fn slash_fraction( + offenders_count: u32, + validator_set_count: u32, + ) -> Perbill { + // the formula is min((3k / n)^2, 1) + let x = Perbill::from_rational_approximation(3 * offenders_count, validator_set_count); + // _ ^ 2 + x.square() + } +} + +impl Module { + /// Determine the BABE slot duration based on the Timestamp module configuration. + pub fn slot_duration() -> T::Moment { + // we double the minimum block-period so each author can always propose within + // the majority of their slot. + ::MinimumPeriod::get().saturating_mul(2.into()) + } + + /// Determine whether an epoch change should take place at this block. + /// Assumes that initialization has already taken place. + pub fn should_epoch_change(now: T::BlockNumber) -> bool { + // The epoch has technically ended during the passage of time + // between this block and the last, but we have to "end" the epoch now, + // since there is no earlier possible block we could have done it. + // + // The exception is for block 1: the genesis has slot 0, so we treat + // epoch 0 as having started at the slot of block 1. We want to use + // the same randomness and validator set as signalled in the genesis, + // so we don't rotate the epoch. + now != sp_runtime::traits::One::one() && { + let diff = CurrentSlot::get().saturating_sub(Self::current_epoch_start()); + diff >= T::EpochDuration::get() + } + } + + /// DANGEROUS: Enact an epoch change. Should be done on every block where `should_epoch_change` has returned `true`, + /// and the caller is the only caller of this function. + /// + /// Typically, this is not handled directly by the user, but by higher-level validator-set manager logic like + /// `pallet-session`. + pub fn enact_epoch_change( + authorities: Vec<(AuthorityId, BabeAuthorityWeight)>, + next_authorities: Vec<(AuthorityId, BabeAuthorityWeight)>, + ) { + // PRECONDITION: caller has done initialization and is guaranteed + // by the session module to be called before this. + #[cfg(debug_assertions)] + { + assert!(Self::initialized().is_some()) + } + + // Update epoch index + let epoch_index = EpochIndex::get() + .checked_add(1) + .expect("epoch indices will never reach 2^64 before the death of the universe; qed"); + + EpochIndex::put(epoch_index); + Authorities::put(authorities); + + // Update epoch randomness. + let next_epoch_index = epoch_index + .checked_add(1) + .expect("epoch indices will never reach 2^64 before the death of the universe; qed"); + + // Returns randomness for the current epoch and computes the *next* + // epoch randomness. + let randomness = Self::randomness_change_epoch(next_epoch_index); + Randomness::put(randomness); + + // After we update the current epoch, we signal the *next* epoch change + // so that nodes can track changes. + let next_randomness = NextRandomness::get(); + + let next = NextEpochDescriptor { + authorities: next_authorities, + randomness: next_randomness, + }; + + Self::deposit_consensus(ConsensusLog::NextEpochData(next)) + } + + // finds the start slot of the current epoch. only guaranteed to + // give correct results after `do_initialize` of the first block + // in the chain (as its result is based off of `GenesisSlot`). + pub fn current_epoch_start() -> SlotNumber { + (EpochIndex::get() * T::EpochDuration::get()) + GenesisSlot::get() + } + + fn deposit_consensus(new: U) { + let log: DigestItem = DigestItem::Consensus(BABE_ENGINE_ID, new.encode()); + >::deposit_log(log.into()) + } + + fn deposit_vrf_output(vrf_output: &[u8; VRF_OUTPUT_LENGTH]) { + let segment_idx = ::get(); + let mut segment = ::get(&segment_idx); + if segment.len() < UNDER_CONSTRUCTION_SEGMENT_LENGTH { + // push onto current segment: not full. + segment.push(*vrf_output); + ::insert(&segment_idx, &segment); + } else { + // move onto the next segment and update the index. + let segment_idx = segment_idx + 1; + ::insert(&segment_idx, &vec![*vrf_output]); + ::put(&segment_idx); + } + } + + fn do_initialize(now: T::BlockNumber) { + // since do_initialize can be called twice (if session module is present) + // => let's ensure that we only modify the storage once per block + let initialized = Self::initialized().is_some(); + if initialized { + return; + } + + let maybe_pre_digest = >::digest() + .logs + .iter() + .filter_map(|s| s.as_pre_runtime()) + .filter_map(|(id, mut data)| if id == BABE_ENGINE_ID { + RawPreDigest::decode(&mut data).ok() + } else { + None + }) + .next(); + + let maybe_vrf = maybe_pre_digest.and_then(|digest| { + // on the first non-zero block (i.e. block #1) + // this is where the first epoch (epoch #0) actually starts. + // we need to adjust internal storage accordingly. + if GenesisSlot::get() == 0 { + GenesisSlot::put(digest.slot_number()); + debug_assert_ne!(GenesisSlot::get(), 0); + + // deposit a log because this is the first block in epoch #0 + // we use the same values as genesis because we haven't collected any + // randomness yet. + let next = NextEpochDescriptor { + authorities: Self::authorities(), + randomness: Self::randomness(), + }; + + Self::deposit_consensus(ConsensusLog::NextEpochData(next)) + } + + CurrentSlot::put(digest.slot_number()); + + if let RawPreDigest::Primary { vrf_output, .. } = digest { + // place the VRF output into the `Initialized` storage item + // and it'll be put onto the under-construction randomness + // later, once we've decided which epoch this block is in. + Some(vrf_output) + } else { + None + } + }); + + Initialized::put(maybe_vrf); + + // enact epoch change, if necessary. + T::EpochChangeTrigger::trigger::(now) + } + + /// Call this function exactly once when an epoch changes, to update the + /// randomness. Returns the new randomness. + fn randomness_change_epoch(next_epoch_index: u64) -> [u8; RANDOMNESS_LENGTH] { + let this_randomness = NextRandomness::get(); + let segment_idx: u32 = ::mutate(|s| sp_std::mem::replace(s, 0)); + + // overestimate to the segment being full. + let rho_size = segment_idx.saturating_add(1) as usize * UNDER_CONSTRUCTION_SEGMENT_LENGTH; + + let next_randomness = compute_randomness( + this_randomness, + next_epoch_index, + (0..segment_idx).flat_map(|i| ::take(&i)), + Some(rho_size), + ); + NextRandomness::put(&next_randomness); + this_randomness + } + + fn initialize_authorities(authorities: &[(AuthorityId, BabeAuthorityWeight)]) { + if !authorities.is_empty() { + assert!(Authorities::get().is_empty(), "Authorities are already initialized!"); + Authorities::put(authorities); + } + } +} + +impl OnTimestampSet for Module { + fn on_timestamp_set(_moment: T::Moment) { } +} + +impl sp_runtime::BoundToRuntimeAppPublic for Module { + type Public = AuthorityId; +} + +impl pallet_session::OneSessionHandler for Module { + type Key = AuthorityId; + + fn on_genesis_session<'a, I: 'a>(validators: I) + where I: Iterator + { + let authorities = validators.map(|(_, k)| (k, 1)).collect::>(); + Self::initialize_authorities(&authorities); + } + + fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, queued_validators: I) + where I: Iterator + { + let authorities = validators.map(|(_account, k)| { + (k, 1) + }).collect::>(); + + let next_authorities = queued_validators.map(|(_account, k)| { + (k, 1) + }).collect::>(); + + Self::enact_epoch_change(authorities, next_authorities) + } + + fn on_disabled(i: usize) { + Self::deposit_consensus(ConsensusLog::OnDisabled(i as u32)) + } +} + +// compute randomness for a new epoch. rho is the concatenation of all +// VRF outputs in the prior epoch. +// +// an optional size hint as to how many VRF outputs there were may be provided. +fn compute_randomness( + last_epoch_randomness: [u8; RANDOMNESS_LENGTH], + epoch_index: u64, + rho: impl Iterator, + rho_size_hint: Option, +) -> [u8; RANDOMNESS_LENGTH] { + let mut s = Vec::with_capacity(40 + rho_size_hint.unwrap_or(0) * VRF_OUTPUT_LENGTH); + s.extend_from_slice(&last_epoch_randomness); + s.extend_from_slice(&epoch_index.to_le_bytes()); + + for vrf_output in rho { + s.extend_from_slice(&vrf_output[..]); + } + + sp_io::hashing::blake2_256(&s) +} + +impl ProvideInherent for Module { + type Call = pallet_timestamp::Call; + type Error = MakeFatalError; + const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER; + + fn create_inherent(_: &InherentData) -> Option { + None + } + + fn check_inherent(call: &Self::Call, data: &InherentData) -> result::Result<(), Self::Error> { + let timestamp = match call { + pallet_timestamp::Call::set(ref timestamp) => timestamp.clone(), + _ => return Ok(()), + }; + + let timestamp_based_slot = (timestamp / Self::slot_duration()).saturated_into::(); + let seal_slot = data.babe_inherent_data()?; + + if timestamp_based_slot == seal_slot { + Ok(()) + } else { + Err(sp_inherents::Error::from("timestamp set in block doesn't match slot in seal").into()) + } + } +} diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs new file mode 100644 index 000000000..2ec083728 --- /dev/null +++ b/frame/babe/src/mock.rs @@ -0,0 +1,110 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Test utilities + +use super::{Trait, Module, GenesisConfig}; +use sp_runtime::{ + traits::IdentityLookup, Perbill, testing::{Header, UintAuthorityId}, impl_opaque_keys, +}; +use sp_version::RuntimeVersion; +use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; +use sp_io; +use sp_core::H256; + +impl_outer_origin!{ + pub enum Origin for Test where system = frame_system {} +} + +type DummyValidatorId = u64; + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Test; + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const MinimumPeriod: u64 = 1; + pub const EpochDuration: u64 = 3; + pub const ExpectedBlockTime: u64 = 1; + pub const Version: RuntimeVersion = substrate_test_runtime::VERSION; + pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(16); +} + +impl frame_system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = (); + type Hash = H256; + type Version = Version; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = DummyValidatorId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type AvailableBlockRatio = AvailableBlockRatio; + type MaximumBlockLength = MaximumBlockLength; + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); +} + +impl_opaque_keys! { + pub struct MockSessionKeys { + pub dummy: UintAuthorityId, + } +} + +impl pallet_session::Trait for Test { + type Event = (); + type ValidatorId = ::AccountId; + type ShouldEndSession = Babe; + type SessionHandler = (Babe,Babe,); + type SessionManager = (); + type ValidatorIdOf = (); + type Keys = MockSessionKeys; + type DisabledValidatorsThreshold = DisabledValidatorsThreshold; +} + +impl pallet_timestamp::Trait for Test { + type Moment = u64; + type OnTimestampSet = Babe; + type MinimumPeriod = MinimumPeriod; +} + +impl Trait for Test { + type EpochDuration = EpochDuration; + type ExpectedBlockTime = ExpectedBlockTime; + type EpochChangeTrigger = crate::ExternalTrigger; +} + +pub fn new_test_ext(authorities: Vec) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + GenesisConfig { + authorities: authorities.into_iter().map(|a| (UintAuthorityId(a).to_public_key(), 1)).collect(), + }.assimilate_storage::(&mut t).unwrap(); + t.into() +} + +pub type System = frame_system::Module; +pub type Babe = Module; diff --git a/frame/babe/src/tests.rs b/frame/babe/src/tests.rs new file mode 100644 index 000000000..84f8166b1 --- /dev/null +++ b/frame/babe/src/tests.rs @@ -0,0 +1,130 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Consensus extension module tests for BABE consensus. + +use super::*; +use mock::{new_test_ext, Babe, System}; +use sp_runtime::{traits::OnFinalize, testing::{Digest, DigestItem}}; +use pallet_session::ShouldEndSession; + +const EMPTY_RANDOMNESS: [u8; 32] = [ + 74, 25, 49, 128, 53, 97, 244, 49, + 222, 202, 176, 2, 231, 66, 95, 10, + 133, 49, 213, 228, 86, 161, 164, 127, + 217, 153, 138, 37, 48, 192, 248, 0, +]; + +fn make_pre_digest( + authority_index: sp_consensus_babe::AuthorityIndex, + slot_number: sp_consensus_babe::SlotNumber, + vrf_output: [u8; sp_consensus_babe::VRF_OUTPUT_LENGTH], + vrf_proof: [u8; sp_consensus_babe::VRF_PROOF_LENGTH], +) -> Digest { + let digest_data = sp_consensus_babe::digests::RawPreDigest::Primary { + authority_index, + slot_number, + vrf_output, + vrf_proof, + }; + let log = DigestItem::PreRuntime(sp_consensus_babe::BABE_ENGINE_ID, digest_data.encode()); + Digest { logs: vec![log] } +} + +#[test] +fn empty_randomness_is_correct() { + let s = compute_randomness([0; RANDOMNESS_LENGTH], 0, std::iter::empty(), None); + assert_eq!(s, EMPTY_RANDOMNESS); +} + +#[test] +fn initial_values() { + new_test_ext(vec![0, 1, 2, 3]).execute_with(|| { + assert_eq!(Babe::authorities().len(), 4) + }) +} + +#[test] +fn check_module() { + new_test_ext(vec![0, 1, 2, 3]).execute_with(|| { + assert!(!Babe::should_end_session(0), "Genesis does not change sessions"); + assert!(!Babe::should_end_session(200000), + "BABE does not include the block number in epoch calculations"); + }) +} + +#[test] +fn first_block_epoch_zero_start() { + new_test_ext(vec![0, 1, 2, 3]).execute_with(|| { + let genesis_slot = 100; + let first_vrf = [1; 32]; + let pre_digest = make_pre_digest( + 0, + genesis_slot, + first_vrf, + [0xff; 64], + ); + + assert_eq!(Babe::genesis_slot(), 0); + System::initialize( + &1, + &Default::default(), + &Default::default(), + &pre_digest, + Default::default(), + ); + + // see implementation of the function for details why: we issue an + // epoch-change digest but don't do it via the normal session mechanism. + assert!(!Babe::should_end_session(1)); + assert_eq!(Babe::genesis_slot(), genesis_slot); + assert_eq!(Babe::current_slot(), genesis_slot); + assert_eq!(Babe::epoch_index(), 0); + + Babe::on_finalize(1); + let header = System::finalize(); + + assert_eq!(SegmentIndex::get(), 0); + assert_eq!(UnderConstruction::get(0), vec![first_vrf]); + assert_eq!(Babe::randomness(), [0; 32]); + assert_eq!(NextRandomness::get(), [0; 32]); + + assert_eq!(header.digest.logs.len(), 2); + assert_eq!(pre_digest.logs.len(), 1); + assert_eq!(header.digest.logs[0], pre_digest.logs[0]); + + let authorities = Babe::authorities(); + let consensus_log = sp_consensus_babe::ConsensusLog::NextEpochData( + sp_consensus_babe::digests::NextEpochDescriptor { + authorities, + randomness: Babe::randomness(), + } + ); + let consensus_digest = DigestItem::Consensus(BABE_ENGINE_ID, consensus_log.encode()); + + // first epoch descriptor has same info as last. + assert_eq!(header.digest.logs[1], consensus_digest.clone()) + }) +} + +#[test] +fn authority_index() { + new_test_ext(vec![0, 1, 2, 3]).execute_with(|| { + assert_eq!( + Babe::find_author((&[(BABE_ENGINE_ID, &[][..])]).into_iter().cloned()), None, + "Trivially invalid authorities are ignored") + }) +} diff --git a/frame/chainrelay/eth/backing/Cargo.toml b/frame/chainrelay/eth/backing/Cargo.toml index 1523675b0..dc8a260a3 100644 --- a/frame/chainrelay/eth/backing/Cargo.toml +++ b/frame/chainrelay/eth/backing/Cargo.toml @@ -35,11 +35,13 @@ rustc-hex = "2.0" rlp = { package = "rlp", git = "https://github.com/darwinia-network/parity-common.git" } -pallet-session = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3"} +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-session = { path = "../../../session" } pallet-timestamp = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } sp-io = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -sp-staking = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +sp-staking = { path = "../../../../primitives/staking" } pallet-ring = { package = "darwinia-ring", path = "../../../../frame/balances/ring" } pallet-kton = { package = "darwinia-kton", path = "../../../../frame/balances/kton" } diff --git a/frame/chainrelay/eth/backing/src/mock.rs b/frame/chainrelay/eth/backing/src/mock.rs index e1484da9e..f1038caeb 100644 --- a/frame/chainrelay/eth/backing/src/mock.rs +++ b/frame/chainrelay/eth/backing/src/mock.rs @@ -169,6 +169,7 @@ parameter_types! { pub const BondingDurationInEra: EraIndex = 3; // assume 60 blocks per session pub const BondingDurationInBlockNumber: BlockNumber = 3 * 3 * 60; + pub const MaxNominatorRewardedPerValidator: u32 = 64; pub const Cap: Balance = CAP; pub const TotalPower: Power = TOTAL_POWER; @@ -182,6 +183,7 @@ impl pallet_staking::Trait for Test { type SlashDeferDuration = (); type SlashCancelOrigin = system::EnsureRoot; type SessionInterface = Self; + type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator; type RingCurrency = Ring; type RingRewardRemainder = (); type RingSlash = (); diff --git a/frame/claims/src/lib.rs b/frame/claims/src/lib.rs index 0cb883b90..a985ed028 100644 --- a/frame/claims/src/lib.rs +++ b/frame/claims/src/lib.rs @@ -126,13 +126,13 @@ decl_storage! { trait Store for Module as Claims { ClaimsFromDot get(claims_from_dot) - :map hasher(blake2_256) EthereumAddress => Option>; + : map hasher(blake2_256) EthereumAddress => Option>; ClaimsFromEth get(claims_from_eth) - :map hasher(blake2_256) EthereumAddress => Option>; + : map hasher(blake2_256) EthereumAddress => Option>; ClaimsFromTron get(claims_from_tron) - :map hasher(blake2_256) TronAddress => Option>; + : map hasher(blake2_256) TronAddress => Option>; Total get(total): RingBalance; } @@ -529,9 +529,9 @@ mod tests { #[test] fn basic_setup_works() { new_test_ext().execute_with(|| { - assert_eq!(Claims::total(), 600); + assert_eq!(Claims::total(), 5500); - assert_eq!(Claims::claims_from_dot(ð(&alice())), Some(100)); + assert_eq!(Claims::claims_from_dot(ð(&alice())), Some(5000)); assert_eq!(Claims::claims_from_eth(ð(&alice())), None); assert_eq!(Claims::claims_from_tron(&tron(&alice())), None); @@ -569,7 +569,7 @@ mod tests { 1, OtherSignature::Dot(sig(&alice(), &1u64.encode(), ETHEREUM_SIGNED_MESSAGE)), )); - assert_eq!(Ring::free_balance(&1), 100); + assert_eq!(Ring::free_balance(&1), 5000); assert_eq!(Claims::total(), 500); assert_eq!(Ring::free_balance(2), 0); @@ -609,14 +609,14 @@ mod tests { >::SignerHasNoClaim, ); assert_ok!(Claims::mint_claim(Origin::ROOT, OtherAddress::Dot(eth(&bob())), 200)); - assert_eq!(Claims::total(), 800); + assert_eq!(Claims::total(), 5700); assert_ok!(Claims::claim( Origin::NONE, 69, OtherSignature::Dot(sig(&bob(), &69u64.encode(), ETHEREUM_SIGNED_MESSAGE)), )); assert_eq!(Ring::free_balance(&69), 200); - assert_eq!(Claims::total(), 600); + assert_eq!(Claims::total(), 5500); }); } diff --git a/frame/elections-phragmen/src/lib.rs b/frame/elections-phragmen/src/lib.rs index d982d4e4d..85d55b726 100644 --- a/frame/elections-phragmen/src/lib.rs +++ b/frame/elections-phragmen/src/lib.rs @@ -772,7 +772,7 @@ mod tests { use substrate_test_utils::assert_eq_uvec; use crate as elections; - use darwinia_support::balance::lock::LockFor; + use darwinia_support::balance::{lock::LockFor, AccountData}; use elections::*; parameter_types! { @@ -799,7 +799,7 @@ mod tests { type AvailableBlockRatio = AvailableBlockRatio; type Version = (); type ModuleToIndex = (); - type AccountData = pallet_ring::AccountData; + type AccountData = AccountData; type OnNewAccount = (); type OnKilledAccount = (); } diff --git a/frame/grandpa/Cargo.toml b/frame/grandpa/Cargo.toml new file mode 100644 index 000000000..419552cdb --- /dev/null +++ b/frame/grandpa/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "pallet-grandpa" +version = "0.5.0" +authors = ["Darwinia Network "] +description = "FRAME pallet for GRANDPA finality gadget" +edition = "2018" +license = "GPL-3.0" +homepage = "https://darwinia.network/" +repository = "https://github.com/darwinia-network/darwinia/" + +[dependencies] +# crates.io +codec = { package = "parity-scale-codec", version = "1.2.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.101", optional = true, features = ["derive"] } + +# github.com +frame-support = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +frame-system = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +pallet-finality-tracker = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-session = { default-features = false, path = "../session" } + +sp-core = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-finality-grandpa = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-runtime = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +sp-staking = { default-features = false, path = "../../primitives/staking" } +sp-std = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +[dev-dependencies] +sp-io ={ git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +[features] +default = ["std"] +std = [ + "codec/std", + "serde", + + "frame-support/std", + "frame-system/std", + + "pallet-finality-tracker/std", + "pallet-session/std", + + "sp-core/std", + "sp-finality-grandpa/std", + "sp-runtime/std", + "sp-staking/std", + "sp-std/std", +] +migrate-authorities = [] diff --git a/frame/grandpa/src/lib.rs b/frame/grandpa/src/lib.rs new file mode 100644 index 000000000..3210627f9 --- /dev/null +++ b/frame/grandpa/src/lib.rs @@ -0,0 +1,524 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! GRANDPA Consensus module for runtime. +//! +//! This manages the GRANDPA authority set ready for the native code. +//! These authorities are only for GRANDPA finality, not for consensus overall. +//! +//! In the future, it will also handle misbehavior reports, and on-chain +//! finality notifications. +//! +//! For full integration with GRANDPA, the `GrandpaApi` should be implemented. +//! The necessary items are re-exported via the `fg_primitives` crate. + +#![cfg_attr(not(feature = "std"), no_std)] + +// re-export since this is necessary for `impl_apis` in runtime. +pub use sp_finality_grandpa as fg_primitives; + +use sp_std::prelude::*; +use codec::{self as codec, Encode, Decode}; +use frame_support::{decl_event, decl_storage, decl_module, decl_error, storage}; +use sp_runtime::{ + DispatchResult, generic::{DigestItem, OpaqueDigestItemId}, traits::Zero, Perbill, PerThing, +}; +use sp_staking::{ + SessionIndex, + offence::{Offence, Kind}, +}; +use fg_primitives::{ + GRANDPA_AUTHORITIES_KEY, GRANDPA_ENGINE_ID, ScheduledChange, ConsensusLog, SetId, RoundNumber, +}; +pub use fg_primitives::{AuthorityId, AuthorityList, AuthorityWeight, VersionedAuthorityList}; +use frame_system::{self as system, ensure_signed, DigestOf}; + +mod mock; +mod tests; + +pub trait Trait: frame_system::Trait { + /// The event type of this module. + type Event: From + Into<::Event>; +} + +/// A stored pending change, old format. +// TODO: remove shim +// https://github.com/paritytech/substrate/issues/1614 +#[derive(Encode, Decode)] +pub struct OldStoredPendingChange { + /// The block number this was scheduled at. + pub scheduled_at: N, + /// The delay in blocks until it will be applied. + pub delay: N, + /// The next authority set. + pub next_authorities: AuthorityList, +} + +/// A stored pending change. +#[derive(Encode)] +pub struct StoredPendingChange { + /// The block number this was scheduled at. + pub scheduled_at: N, + /// The delay in blocks until it will be applied. + pub delay: N, + /// The next authority set. + pub next_authorities: AuthorityList, + /// If defined it means the change was forced and the given block number + /// indicates the median last finalized block when the change was signaled. + pub forced: Option, +} + +impl Decode for StoredPendingChange { + fn decode(value: &mut I) -> core::result::Result { + let old = OldStoredPendingChange::decode(value)?; + let forced = >::decode(value).unwrap_or(None); + + Ok(StoredPendingChange { + scheduled_at: old.scheduled_at, + delay: old.delay, + next_authorities: old.next_authorities, + forced, + }) + } +} + +/// Current state of the GRANDPA authority set. State transitions must happen in +/// the same order of states defined below, e.g. `Paused` implies a prior +/// `PendingPause`. +#[derive(Decode, Encode)] +#[cfg_attr(test, derive(Debug, PartialEq))] +pub enum StoredState { + /// The current authority set is live, and GRANDPA is enabled. + Live, + /// There is a pending pause event which will be enacted at the given block + /// height. + PendingPause { + /// Block at which the intention to pause was scheduled. + scheduled_at: N, + /// Number of blocks after which the change will be enacted. + delay: N + }, + /// The current GRANDPA authority set is paused. + Paused, + /// There is a pending resume event which will be enacted at the given block + /// height. + PendingResume { + /// Block at which the intention to resume was scheduled. + scheduled_at: N, + /// Number of blocks after which the change will be enacted. + delay: N, + }, +} + +decl_event! { + pub enum Event { + /// New authority set has been applied. + NewAuthorities(AuthorityList), + /// Current authority set has been paused. + Paused, + /// Current authority set has been resumed. + Resumed, + } +} + +decl_error! { + pub enum Error for Module { + /// Attempt to signal GRANDPA pause when the authority set isn't live + /// (either paused or already pending pause). + PauseFailed, + /// Attempt to signal GRANDPA resume when the authority set isn't paused + /// (either live or already pending resume). + ResumeFailed, + /// Attempt to signal GRANDPA change with one already pending. + ChangePending, + /// Cannot signal forced change so soon after last. + TooSoon, + } +} + +decl_storage! { + trait Store for Module as GrandpaFinality { + /// DEPRECATED + /// + /// This used to store the current authority set, which has been migrated to the well-known + /// GRANDPA_AUTHORITIES_KEY unhashed key. + #[cfg(feature = "migrate-authorities")] + pub(crate) Authorities get(fn authorities): AuthorityList; + + /// State of the current authority set. + State get(fn state): StoredState = StoredState::Live; + + /// Pending change: (signaled at, scheduled change). + PendingChange: Option>; + + /// next block number where we can force a change. + NextForced get(fn next_forced): Option; + + /// `true` if we are currently stalled. + Stalled get(fn stalled): Option<(T::BlockNumber, T::BlockNumber)>; + + /// The number of changes (both in terms of keys and underlying economic responsibilities) + /// in the "set" of Grandpa validators from genesis. + CurrentSetId get(fn current_set_id) build(|_| fg_primitives::SetId::default()): SetId; + + /// A mapping from grandpa set ID to the index of the *most recent* session for which its members were responsible. + SetIdSession get(fn session_for_set): map hasher(blake2_256) SetId => Option; + } + add_extra_genesis { + config(authorities): AuthorityList; + build(|config| Module::::initialize_authorities(&config.authorities)) + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + type Error = Error; + + fn deposit_event() = default; + + /// Report some misbehavior. + fn report_misbehavior(origin, _report: Vec) { + ensure_signed(origin)?; + // FIXME: https://github.com/paritytech/substrate/issues/1112 + } + + fn on_initialize() { + #[cfg(feature = "migrate-authorities")] + Self::migrate_authorities(); + } + + fn on_finalize(block_number: T::BlockNumber) { + // check for scheduled pending authority set changes + if let Some(pending_change) = >::get() { + // emit signal if we're at the block that scheduled the change + if block_number == pending_change.scheduled_at { + if let Some(median) = pending_change.forced { + Self::deposit_log(ConsensusLog::ForcedChange( + median, + ScheduledChange { + delay: pending_change.delay, + next_authorities: pending_change.next_authorities.clone(), + } + )) + } else { + Self::deposit_log(ConsensusLog::ScheduledChange( + ScheduledChange{ + delay: pending_change.delay, + next_authorities: pending_change.next_authorities.clone(), + } + )); + } + } + + // enact the change if we've reached the enacting block + if block_number == pending_change.scheduled_at + pending_change.delay { + Self::set_grandpa_authorities(&pending_change.next_authorities); + Self::deposit_event( + Event::NewAuthorities(pending_change.next_authorities) + ); + >::kill(); + } + } + + // check for scheduled pending state changes + match >::get() { + StoredState::PendingPause { scheduled_at, delay } => { + // signal change to pause + if block_number == scheduled_at { + Self::deposit_log(ConsensusLog::Pause(delay)); + } + + // enact change to paused state + if block_number == scheduled_at + delay { + >::put(StoredState::Paused); + Self::deposit_event(Event::Paused); + } + }, + StoredState::PendingResume { scheduled_at, delay } => { + // signal change to resume + if block_number == scheduled_at { + Self::deposit_log(ConsensusLog::Resume(delay)); + } + + // enact change to live state + if block_number == scheduled_at + delay { + >::put(StoredState::Live); + Self::deposit_event(Event::Resumed); + } + }, + _ => {}, + } + } + } +} + +impl Module { + /// Get the current set of authorities, along with their respective weights. + pub fn grandpa_authorities() -> AuthorityList { + storage::unhashed::get_or_default::(GRANDPA_AUTHORITIES_KEY).into() + } + + /// Set the current set of authorities, along with their respective weights. + fn set_grandpa_authorities(authorities: &AuthorityList) { + storage::unhashed::put( + GRANDPA_AUTHORITIES_KEY, + &VersionedAuthorityList::from(authorities), + ); + } + + /// Schedule GRANDPA to pause starting in the given number of blocks. + /// Cannot be done when already paused. + pub fn schedule_pause(in_blocks: T::BlockNumber) -> DispatchResult { + if let StoredState::Live = >::get() { + let scheduled_at = >::block_number(); + >::put(StoredState::PendingPause { + delay: in_blocks, + scheduled_at, + }); + + Ok(()) + } else { + Err(Error::::PauseFailed)? + } + } + + /// Schedule a resume of GRANDPA after pausing. + pub fn schedule_resume(in_blocks: T::BlockNumber) -> DispatchResult { + if let StoredState::Paused = >::get() { + let scheduled_at = >::block_number(); + >::put(StoredState::PendingResume { + delay: in_blocks, + scheduled_at, + }); + + Ok(()) + } else { + Err(Error::::ResumeFailed)? + } + } + + /// Schedule a change in the authorities. + /// + /// The change will be applied at the end of execution of the block + /// `in_blocks` after the current block. This value may be 0, in which + /// case the change is applied at the end of the current block. + /// + /// If the `forced` parameter is defined, this indicates that the current + /// set has been synchronously determined to be offline and that after + /// `in_blocks` the given change should be applied. The given block number + /// indicates the median last finalized block number and it should be used + /// as the canon block when starting the new grandpa voter. + /// + /// No change should be signaled while any change is pending. Returns + /// an error if a change is already pending. + pub fn schedule_change( + next_authorities: AuthorityList, + in_blocks: T::BlockNumber, + forced: Option, + ) -> DispatchResult { + if !>::exists() { + let scheduled_at = >::block_number(); + + if let Some(_) = forced { + if Self::next_forced().map_or(false, |next| next > scheduled_at) { + Err(Error::::TooSoon)? + } + + // only allow the next forced change when twice the window has passed since + // this one. + >::put(scheduled_at + in_blocks * 2.into()); + } + + >::put(StoredPendingChange { + delay: in_blocks, + scheduled_at, + next_authorities, + forced, + }); + + Ok(()) + } else { + Err(Error::::ChangePending)? + } + } + + /// Deposit one of this module's logs. + fn deposit_log(log: ConsensusLog) { + let log: DigestItem = DigestItem::Consensus(GRANDPA_ENGINE_ID, log.encode()); + >::deposit_log(log.into()); + } + + fn initialize_authorities(authorities: &AuthorityList) { + if !authorities.is_empty() { + assert!( + Self::grandpa_authorities().is_empty(), + "Authorities are already initialized!" + ); + Self::set_grandpa_authorities(authorities); + } + } + + #[cfg(feature = "migrate-authorities")] + fn migrate_authorities() { + if Authorities::exists() { + Self::set_grandpa_authorities(&Authorities::take()); + } + } +} + +impl Module { + /// Attempt to extract a GRANDPA log from a generic digest. + pub fn grandpa_log(digest: &DigestOf) -> Option> { + let id = OpaqueDigestItemId::Consensus(&GRANDPA_ENGINE_ID); + digest.convert_first(|l| l.try_to::>(id)) + } + + /// Attempt to extract a pending set-change signal from a digest. + pub fn pending_change(digest: &DigestOf) + -> Option> + { + Self::grandpa_log(digest).and_then(|signal| signal.try_into_change()) + } + + /// Attempt to extract a forced set-change signal from a digest. + pub fn forced_change(digest: &DigestOf) + -> Option<(T::BlockNumber, ScheduledChange)> + { + Self::grandpa_log(digest).and_then(|signal| signal.try_into_forced_change()) + } + + /// Attempt to extract a pause signal from a digest. + pub fn pending_pause(digest: &DigestOf) + -> Option + { + Self::grandpa_log(digest).and_then(|signal| signal.try_into_pause()) + } + + /// Attempt to extract a resume signal from a digest. + pub fn pending_resume(digest: &DigestOf) + -> Option + { + Self::grandpa_log(digest).and_then(|signal| signal.try_into_resume()) + } +} + +impl sp_runtime::BoundToRuntimeAppPublic for Module { + type Public = AuthorityId; +} + +impl pallet_session::OneSessionHandler for Module + where T: pallet_session::Trait +{ + type Key = AuthorityId; + + fn on_genesis_session<'a, I: 'a>(validators: I) + where I: Iterator + { + let authorities = validators.map(|(_, k)| (k, 1)).collect::>(); + Self::initialize_authorities(&authorities); + } + + fn on_new_session<'a, I: 'a>(changed: bool, validators: I, _queued_validators: I) + where I: Iterator + { + // Always issue a change if `session` says that the validators have changed. + // Even if their session keys are the same as before, the underlying economic + // identities have changed. + let current_set_id = if changed { + let next_authorities = validators.map(|(_, k)| (k, 1)).collect::>(); + if let Some((further_wait, median)) = >::take() { + let _ = Self::schedule_change(next_authorities, further_wait, Some(median)); + } else { + let _ = Self::schedule_change(next_authorities, Zero::zero(), None); + } + CurrentSetId::mutate(|s| { *s += 1; *s }) + } else { + // nothing's changed, neither economic conditions nor session keys. update the pointer + // of the current set. + Self::current_set_id() + }; + + // if we didn't issue a change, we update the mapping to note that the current + // set corresponds to the latest equivalent session (i.e. now). + let session_index = >::current_index(); + SetIdSession::insert(current_set_id, &session_index); + } + + fn on_disabled(i: usize) { + Self::deposit_log(ConsensusLog::OnDisabled(i as u64)) + } +} + +impl pallet_finality_tracker::OnFinalizationStalled for Module { + fn on_stalled(further_wait: T::BlockNumber, median: T::BlockNumber) { + // when we record old authority sets, we can use `pallet_finality_tracker::median` + // to figure out _who_ failed. until then, we can't meaningfully guard + // against `next == last` the way that normal session changes do. + >::put((further_wait, median)); + } +} + +/// A round number and set id which point on the time of an offence. +#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Encode, Decode)] +struct GrandpaTimeSlot { + // The order of these matters for `derive(Ord)`. + set_id: SetId, + round: RoundNumber, +} + +// TODO [slashing]: Integrate this. +/// A grandpa equivocation offence report. +struct GrandpaEquivocationOffence { + /// Time slot at which this incident happened. + time_slot: GrandpaTimeSlot, + /// The session index in which the incident happened. + session_index: SessionIndex, + /// The size of the validator set at the time of the offence. + validator_set_count: u32, + /// The authority which produced this equivocation. + offender: FullIdentification, +} + +impl Offence for GrandpaEquivocationOffence { + const ID: Kind = *b"grandpa:equivoca"; + type TimeSlot = GrandpaTimeSlot; + + fn offenders(&self) -> Vec { + vec![self.offender.clone()] + } + + fn session_index(&self) -> SessionIndex { + self.session_index + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> Self::TimeSlot { + self.time_slot + } + + fn slash_fraction( + offenders_count: u32, + validator_set_count: u32, + ) -> Perbill { + // the formula is min((3k / n)^2, 1) + let x = Perbill::from_rational_approximation(3 * offenders_count, validator_set_count); + // _ ^ 2 + x.square() + } +} diff --git a/frame/grandpa/src/mock.rs b/frame/grandpa/src/mock.rs new file mode 100644 index 000000000..8b94becd5 --- /dev/null +++ b/frame/grandpa/src/mock.rs @@ -0,0 +1,99 @@ +// Copyright 2018-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Test utilities + +#![cfg(test)] + +use sp_runtime::{Perbill, DigestItem, traits::IdentityLookup, testing::{Header, UintAuthorityId}}; +use sp_io; +use frame_support::{impl_outer_origin, impl_outer_event, parameter_types, weights::Weight}; +use sp_core::H256; +use codec::{Encode, Decode}; +use crate::{AuthorityId, AuthorityList, GenesisConfig, Trait, Module, ConsensusLog}; +use sp_finality_grandpa::GRANDPA_ENGINE_ID; + +use frame_system as system; +impl_outer_origin!{ + pub enum Origin for Test where system = frame_system {} +} + +pub fn grandpa_log(log: ConsensusLog) -> DigestItem { + DigestItem::Consensus(GRANDPA_ENGINE_ID, log.encode()) +} + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug, Decode, Encode)] +pub struct Test; + +impl Trait for Test { + type Event = TestEvent; +} +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); +} +impl frame_system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = (); + type Hash = H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); +} + +mod grandpa { + pub use crate::Event; +} + +impl_outer_event!{ + pub enum TestEvent for Test { + system, + grandpa, + } +} + +pub fn to_authorities(vec: Vec<(u64, u64)>) -> AuthorityList { + vec.into_iter() + .map(|(id, weight)| (UintAuthorityId(id).to_public_key::(), weight)) + .collect() +} + +pub fn new_test_ext(authorities: Vec<(u64, u64)>) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + GenesisConfig { + authorities: to_authorities(authorities), + }.assimilate_storage::(&mut t).unwrap(); + t.into() +} + +pub type System = frame_system::Module; +pub type Grandpa = Module; diff --git a/frame/grandpa/src/tests.rs b/frame/grandpa/src/tests.rs new file mode 100644 index 000000000..ff3841b8d --- /dev/null +++ b/frame/grandpa/src/tests.rs @@ -0,0 +1,338 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Tests for the module. + +#![cfg(test)] + +use sp_runtime::{testing::{H256, Digest}, traits::{Header, OnFinalize}}; +use crate::mock::*; +use frame_system::{EventRecord, Phase}; +use codec::{Decode, Encode}; +use fg_primitives::ScheduledChange; +use super::*; + +fn initialize_block(number: u64, parent_hash: H256) { + System::initialize( + &number, + &parent_hash, + &Default::default(), + &Default::default(), + Default::default(), + ); +} + +#[test] +fn authorities_change_logged() { + new_test_ext(vec![(1, 1), (2, 1), (3, 1)]).execute_with(|| { + initialize_block(1, Default::default()); + Grandpa::schedule_change(to_authorities(vec![(4, 1), (5, 1), (6, 1)]), 0, None).unwrap(); + + System::note_finished_extrinsics(); + Grandpa::on_finalize(1); + + let header = System::finalize(); + assert_eq!(header.digest, Digest { + logs: vec![ + grandpa_log(ConsensusLog::ScheduledChange( + ScheduledChange { delay: 0, next_authorities: to_authorities(vec![(4, 1), (5, 1), (6, 1)]) } + )), + ], + }); + + assert_eq!(System::events(), vec![ + EventRecord { + phase: Phase::Finalization, + event: Event::NewAuthorities(to_authorities(vec![(4, 1), (5, 1), (6, 1)])).into(), + topics: vec![], + }, + ]); + }); +} + +#[test] +fn authorities_change_logged_after_delay() { + new_test_ext(vec![(1, 1), (2, 1), (3, 1)]).execute_with(|| { + initialize_block(1, Default::default()); + Grandpa::schedule_change(to_authorities(vec![(4, 1), (5, 1), (6, 1)]), 1, None).unwrap(); + Grandpa::on_finalize(1); + let header = System::finalize(); + assert_eq!(header.digest, Digest { + logs: vec![ + grandpa_log(ConsensusLog::ScheduledChange( + ScheduledChange { delay: 1, next_authorities: to_authorities(vec![(4, 1), (5, 1), (6, 1)]) } + )), + ], + }); + + // no change at this height. + assert_eq!(System::events(), vec![]); + + initialize_block(2, header.hash()); + System::note_finished_extrinsics(); + Grandpa::on_finalize(2); + + let _header = System::finalize(); + assert_eq!(System::events(), vec![ + EventRecord { + phase: Phase::Finalization, + event: Event::NewAuthorities(to_authorities(vec![(4, 1), (5, 1), (6, 1)])).into(), + topics: vec![], + }, + ]); + }); +} + +#[test] +fn cannot_schedule_change_when_one_pending() { + new_test_ext(vec![(1, 1), (2, 1), (3, 1)]).execute_with(|| { + initialize_block(1, Default::default()); + Grandpa::schedule_change(to_authorities(vec![(4, 1), (5, 1), (6, 1)]), 1, None).unwrap(); + assert!(>::exists()); + assert!(Grandpa::schedule_change(to_authorities(vec![(5, 1)]), 1, None).is_err()); + + Grandpa::on_finalize(1); + let header = System::finalize(); + + initialize_block(2, header.hash()); + assert!(>::exists()); + assert!(Grandpa::schedule_change(to_authorities(vec![(5, 1)]), 1, None).is_err()); + + Grandpa::on_finalize(2); + let header = System::finalize(); + + initialize_block(3, header.hash()); + assert!(!>::exists()); + assert!(Grandpa::schedule_change(to_authorities(vec![(5, 1)]), 1, None).is_ok()); + + Grandpa::on_finalize(3); + let _header = System::finalize(); + }); +} + +#[test] +fn new_decodes_from_old() { + let old = OldStoredPendingChange { + scheduled_at: 5u32, + delay: 100u32, + next_authorities: to_authorities(vec![(1, 5), (2, 10), (3, 2)]), + }; + + let encoded = old.encode(); + let new = StoredPendingChange::::decode(&mut &encoded[..]).unwrap(); + assert!(new.forced.is_none()); + assert_eq!(new.scheduled_at, old.scheduled_at); + assert_eq!(new.delay, old.delay); + assert_eq!(new.next_authorities, old.next_authorities); +} + +#[test] +fn dispatch_forced_change() { + new_test_ext(vec![(1, 1), (2, 1), (3, 1)]).execute_with(|| { + initialize_block(1, Default::default()); + Grandpa::schedule_change( + to_authorities(vec![(4, 1), (5, 1), (6, 1)]), + 5, + Some(0), + ).unwrap(); + + assert!(>::exists()); + assert!(Grandpa::schedule_change(to_authorities(vec![(5, 1)]), 1, Some(0)).is_err()); + + Grandpa::on_finalize(1); + let mut header = System::finalize(); + + for i in 2..7 { + initialize_block(i, header.hash()); + assert!(>::get().unwrap().forced.is_some()); + assert_eq!(Grandpa::next_forced(), Some(11)); + assert!(Grandpa::schedule_change(to_authorities(vec![(5, 1)]), 1, None).is_err()); + assert!(Grandpa::schedule_change(to_authorities(vec![(5, 1)]), 1, Some(0)).is_err()); + + Grandpa::on_finalize(i); + header = System::finalize(); + } + + // change has been applied at the end of block 6. + // add a normal change. + { + initialize_block(7, header.hash()); + assert!(!>::exists()); + assert_eq!(Grandpa::grandpa_authorities(), to_authorities(vec![(4, 1), (5, 1), (6, 1)])); + assert!(Grandpa::schedule_change(to_authorities(vec![(5, 1)]), 1, None).is_ok()); + Grandpa::on_finalize(7); + header = System::finalize(); + } + + // run the normal change. + { + initialize_block(8, header.hash()); + assert!(>::exists()); + assert_eq!(Grandpa::grandpa_authorities(), to_authorities(vec![(4, 1), (5, 1), (6, 1)])); + assert!(Grandpa::schedule_change(to_authorities(vec![(5, 1)]), 1, None).is_err()); + Grandpa::on_finalize(8); + header = System::finalize(); + } + + // normal change applied. but we can't apply a new forced change for some + // time. + for i in 9..11 { + initialize_block(i, header.hash()); + assert!(!>::exists()); + assert_eq!(Grandpa::grandpa_authorities(), to_authorities(vec![(5, 1)])); + assert_eq!(Grandpa::next_forced(), Some(11)); + assert!(Grandpa::schedule_change(to_authorities(vec![(5, 1), (6, 1)]), 5, Some(0)).is_err()); + Grandpa::on_finalize(i); + header = System::finalize(); + } + + { + initialize_block(11, header.hash()); + assert!(!>::exists()); + assert!(Grandpa::schedule_change(to_authorities(vec![(5, 1), (6, 1), (7, 1)]), 5, Some(0)).is_ok()); + assert_eq!(Grandpa::next_forced(), Some(21)); + Grandpa::on_finalize(11); + header = System::finalize(); + } + let _ = header; + }); +} + +#[test] +fn schedule_pause_only_when_live() { + new_test_ext(vec![(1, 1), (2, 1), (3, 1)]).execute_with(|| { + // we schedule a pause at block 1 with delay of 1 + initialize_block(1, Default::default()); + Grandpa::schedule_pause(1).unwrap(); + + // we've switched to the pending pause state + assert_eq!( + Grandpa::state(), + StoredState::PendingPause { + scheduled_at: 1u64, + delay: 1, + }, + ); + + Grandpa::on_finalize(1); + let _ = System::finalize(); + + initialize_block(2, Default::default()); + + // signaling a pause now should fail + assert!(Grandpa::schedule_pause(1).is_err()); + + Grandpa::on_finalize(2); + let _ = System::finalize(); + + // after finalizing block 2 the set should have switched to paused state + assert_eq!( + Grandpa::state(), + StoredState::Paused, + ); + }); +} + +#[test] +fn schedule_resume_only_when_paused() { + new_test_ext(vec![(1, 1), (2, 1), (3, 1)]).execute_with(|| { + initialize_block(1, Default::default()); + + // the set is currently live, resuming it is an error + assert!(Grandpa::schedule_resume(1).is_err()); + + assert_eq!( + Grandpa::state(), + StoredState::Live, + ); + + // we schedule a pause to be applied instantly + Grandpa::schedule_pause(0).unwrap(); + Grandpa::on_finalize(1); + let _ = System::finalize(); + + assert_eq!( + Grandpa::state(), + StoredState::Paused, + ); + + // we schedule the set to go back live in 2 blocks + initialize_block(2, Default::default()); + Grandpa::schedule_resume(2).unwrap(); + Grandpa::on_finalize(2); + let _ = System::finalize(); + + initialize_block(3, Default::default()); + Grandpa::on_finalize(3); + let _ = System::finalize(); + + initialize_block(4, Default::default()); + Grandpa::on_finalize(4); + let _ = System::finalize(); + + // it should be live at block 4 + assert_eq!( + Grandpa::state(), + StoredState::Live, + ); + }); +} + +#[test] +fn time_slot_have_sane_ord() { + // Ensure that `Ord` implementation is sane. + const FIXTURE: &[GrandpaTimeSlot] = &[ + GrandpaTimeSlot { + set_id: 0, + round: 0, + }, + GrandpaTimeSlot { + set_id: 0, + round: 1, + }, + GrandpaTimeSlot { + set_id: 1, + round: 0, + }, + GrandpaTimeSlot { + set_id: 1, + round: 1, + }, + GrandpaTimeSlot { + set_id: 1, + round: 2, + } + ]; + assert!(FIXTURE.windows(2).all(|f| f[0] < f[1])); +} + +#[test] +#[cfg(feature = "migrate-authorities")] +fn authorities_migration() { + use sp_runtime::traits::OnInitialize; + + with_externalities(&mut new_test_ext(vec![]), || { + let authorities = to_authorities(vec![(1, 1), (2, 1), (3, 1)]); + + Authorities::put(authorities.clone()); + assert!(Grandpa::grandpa_authorities().is_empty()); + + Grandpa::on_initialize(1); + + assert!(!Authorities::exists()); + assert_eq!(Grandpa::grandpa_authorities(), authorities); + }); +} diff --git a/frame/im-online/Cargo.toml b/frame/im-online/Cargo.toml new file mode 100644 index 000000000..f49666656 --- /dev/null +++ b/frame/im-online/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "pallet-im-online" +version = "0.5.0" +authors = ["Darwinia Network "] +description = "FRAME's I'm online pallet" +edition = "2018" +license = "GPL-3.0" +homepage = "https://darwinia.network/" +repository = "https://github.com/darwinia-network/darwinia/" + +[dependencies] +# crates.io +codec = { package = "parity-scale-codec", version = "1.2.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.101", optional = true } + +# github.com +frame-support = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +frame-system = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +pallet-authorship = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-session = { default-features = false, path = "../session" } + +sp-application-crypto = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-core = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-io = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-runtime = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +sp-staking = { default-features = false, path = "../../primitives/staking" } +sp-std = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +[features] +default = ["std", "pallet-session/historical"] +std = [ + "codec/std", + "serde", + + "frame-support/std", + "frame-system/std", + + "pallet-authorship/std", + "pallet-session/std", + + "sp-application-crypto/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-staking/std", + "sp-std/std", +] diff --git a/frame/im-online/src/lib.rs b/frame/im-online/src/lib.rs new file mode 100644 index 000000000..9aa8d2d67 --- /dev/null +++ b/frame/im-online/src/lib.rs @@ -0,0 +1,713 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! # I'm online Module +//! +//! If the local node is a validator (i.e. contains an authority key), this module +//! gossips a heartbeat transaction with each new session. The heartbeat functions +//! as a simple mechanism to signal that the node is online in the current era. +//! +//! Received heartbeats are tracked for one era and reset with each new era. The +//! module exposes two public functions to query if a heartbeat has been received +//! in the current era or session. +//! +//! The heartbeat is a signed transaction, which was signed using the session key +//! and includes the recent best block number of the local validators chain as well +//! as the [NetworkState](../../client/offchain/struct.NetworkState.html). +//! It is submitted as an Unsigned Transaction via off-chain workers. +//! +//! - [`im_online::Trait`](./trait.Trait.html) +//! - [`Call`](./enum.Call.html) +//! - [`Module`](./struct.Module.html) +//! +//! ## Interface +//! +//! ### Public Functions +//! +//! - `is_online` - True if the validator sent a heartbeat in the current session. +//! +//! ## Usage +//! +//! ``` +//! use frame_support::{decl_module, dispatch}; +//! use frame_system::{self as system, ensure_signed}; +//! use pallet_im_online::{self as im_online}; +//! +//! pub trait Trait: im_online::Trait {} +//! +//! decl_module! { +//! pub struct Module for enum Call where origin: T::Origin { +//! pub fn is_online(origin, authority_index: u32) -> dispatch::DispatchResult { +//! let _sender = ensure_signed(origin)?; +//! let _is_online = >::is_online(authority_index); +//! Ok(()) +//! } +//! } +//! } +//! # fn main() { } +//! ``` +//! +//! ## Dependencies +//! +//! This module depends on the [Session module](../pallet_session/index.html). + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +mod mock; +mod tests; + +use sp_application_crypto::RuntimeAppPublic; +use codec::{Encode, Decode}; +use sp_core::offchain::OpaqueNetworkState; +use sp_std::prelude::*; +use sp_std::convert::TryInto; +use pallet_session::historical::IdentificationTuple; +use sp_runtime::{ + offchain::storage::StorageValueRef, + RuntimeDebug, + traits::{Convert, Member, Saturating, AtLeast32Bit}, Perbill, PerThing, + transaction_validity::{ + TransactionValidity, ValidTransaction, InvalidTransaction, + TransactionPriority, + }, +}; +use sp_staking::{ + SessionIndex, + offence::{ReportOffence, Offence, Kind}, +}; +use frame_support::{ + decl_module, decl_event, decl_storage, Parameter, debug, decl_error, + traits::Get, +}; +use frame_system::{self as system, ensure_none}; +use frame_system::offchain::SubmitUnsignedTransaction; + +pub mod sr25519 { + mod app_sr25519 { + use sp_application_crypto::{app_crypto, key_types::IM_ONLINE, sr25519}; + app_crypto!(sr25519, IM_ONLINE); + } + + sp_application_crypto::with_pair! { + /// An i'm online keypair using sr25519 as its crypto. + pub type AuthorityPair = app_sr25519::Pair; + } + + /// An i'm online signature using sr25519 as its crypto. + pub type AuthoritySignature = app_sr25519::Signature; + + /// An i'm online identifier using sr25519 as its crypto. + pub type AuthorityId = app_sr25519::Public; +} + +pub mod ed25519 { + mod app_ed25519 { + use sp_application_crypto::{app_crypto, key_types::IM_ONLINE, ed25519}; + app_crypto!(ed25519, IM_ONLINE); + } + + sp_application_crypto::with_pair! { + /// An i'm online keypair using ed25519 as its crypto. + pub type AuthorityPair = app_ed25519::Pair; + } + + /// An i'm online signature using ed25519 as its crypto. + pub type AuthoritySignature = app_ed25519::Signature; + + /// An i'm online identifier using ed25519 as its crypto. + pub type AuthorityId = app_ed25519::Public; +} + +const DB_PREFIX: &[u8] = b"parity/im-online-heartbeat/"; +/// How many blocks do we wait for heartbeat transaction to be included +/// before sending another one. +const INCLUDE_THRESHOLD: u32 = 3; + +/// Status of the offchain worker code. +/// +/// This stores the block number at which heartbeat was requested and when the worker +/// has actually managed to produce it. +/// Note we store such status for every `authority_index` separately. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)] +struct HeartbeatStatus { + /// An index of the session that we are supposed to send heartbeat for. + pub session_index: SessionIndex, + /// A block number at which the heartbeat for that session has been actually sent. + /// + /// It may be 0 in case the sending failed. In such case we should just retry + /// as soon as possible (i.e. in a worker running for the next block). + pub sent_at: BlockNumber, +} + +impl HeartbeatStatus { + /// Returns true if heartbeat has been recently sent. + /// + /// Parameters: + /// `session_index` - index of current session. + /// `now` - block at which the offchain worker is running. + /// + /// This function will return `true` iff: + /// 1. the session index is the same (we don't care if it went up or down) + /// 2. the heartbeat has been sent recently (within the threshold) + /// + /// The reasoning for 1. is that it's better to send an extra heartbeat than + /// to stall or not send one in case of a bug. + fn is_recent(&self, session_index: SessionIndex, now: BlockNumber) -> bool { + self.session_index == session_index && self.sent_at + INCLUDE_THRESHOLD.into() > now + } +} + +/// Error which may occur while executing the off-chain code. +#[cfg_attr(test, derive(PartialEq))] +enum OffchainErr { + TooEarly(BlockNumber), + WaitingForInclusion(BlockNumber), + AlreadyOnline(u32), + FailedSigning, + FailedToAcquireLock, + NetworkState, + SubmitTransaction, +} + +impl sp_std::fmt::Debug for OffchainErr { + fn fmt(&self, fmt: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + match *self { + OffchainErr::TooEarly(ref block) => + write!(fmt, "Too early to send heartbeat, next expected at {:?}", block), + OffchainErr::WaitingForInclusion(ref block) => + write!(fmt, "Heartbeat already sent at {:?}. Waiting for inclusion.", block), + OffchainErr::AlreadyOnline(auth_idx) => + write!(fmt, "Authority {} is already online", auth_idx), + OffchainErr::FailedSigning => write!(fmt, "Failed to sign heartbeat"), + OffchainErr::FailedToAcquireLock => write!(fmt, "Failed to acquire lock"), + OffchainErr::NetworkState => write!(fmt, "Failed to fetch network state"), + OffchainErr::SubmitTransaction => write!(fmt, "Failed to submit transaction"), + } + } +} + +pub type AuthIndex = u32; + +/// Heartbeat which is sent/received. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)] +pub struct Heartbeat + where BlockNumber: PartialEq + Eq + Decode + Encode, +{ + /// Block number at the time heartbeat is created.. + pub block_number: BlockNumber, + /// A state of local network (peer id and external addresses) + pub network_state: OpaqueNetworkState, + /// Index of the current session. + pub session_index: SessionIndex, + /// An index of the authority on the list of validators. + pub authority_index: AuthIndex, +} + +pub trait Trait: frame_system::Trait + pallet_session::historical::Trait { + /// The identifier type for an authority. + type AuthorityId: Member + Parameter + RuntimeAppPublic + Default + Ord; + + /// The overarching event type. + type Event: From> + Into<::Event>; + + /// A dispatchable call type. + type Call: From>; + + /// A transaction submitter. + type SubmitTransaction: SubmitUnsignedTransaction::Call>; + + /// An expected duration of the session. + /// + /// This parameter is used to determine the longevity of `heartbeat` transaction + /// and a rough time when we should start considering sending heartbeats, + /// since the workers avoids sending them at the very beginning of the session, assuming + /// there is a chance the authority will produce a block and they won't be necessary. + type SessionDuration: Get; + + /// A type that gives us the ability to submit unresponsiveness offence reports. + type ReportUnresponsiveness: + ReportOffence< + Self::AccountId, + IdentificationTuple, + UnresponsivenessOffence>, + >; +} + +decl_event!( + pub enum Event where + ::AuthorityId, + IdentificationTuple = IdentificationTuple, + { + /// A new heartbeat was received from `AuthorityId` + HeartbeatReceived(AuthorityId), + /// At the end of the session, no offence was committed. + AllGood, + /// At the end of the session, at least once validator was found to be offline. + SomeOffline(Vec), + } +); + +decl_storage! { + trait Store for Module as ImOnline { + /// The block number after which it's ok to send heartbeats in current session. + /// + /// At the beginning of each session we set this to a value that should + /// fall roughly in the middle of the session duration. + /// The idea is to first wait for the validators to produce a block + /// in the current session, so that the heartbeat later on will not be necessary. + HeartbeatAfter get(fn heartbeat_after): T::BlockNumber; + + /// The current set of keys that may issue a heartbeat. + Keys get(fn keys): Vec; + + /// For each session index, we keep a mapping of `AuthIndex` + /// to `offchain::OpaqueNetworkState`. + ReceivedHeartbeats get(fn received_heartbeats): + double_map hasher(blake2_256) SessionIndex, hasher(blake2_256) AuthIndex + => Option>; + + /// For each session index, we keep a mapping of `T::ValidatorId` to the + /// number of blocks authored by the given authority. + AuthoredBlocks get(fn authored_blocks): + double_map hasher(blake2_256) SessionIndex, hasher(blake2_256) T::ValidatorId => u32; + } + add_extra_genesis { + config(keys): Vec; + build(|config| Module::::initialize_keys(&config.keys)) + } +} + +decl_error! { + /// Error for the im-online module. + pub enum Error for Module { + /// Non existent public key. + InvalidKey, + /// Duplicated heartbeat. + DuplicatedHeartbeat, + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + type Error = Error; + + fn deposit_event() = default; + + fn heartbeat( + origin, + heartbeat: Heartbeat, + // since signature verification is done in `validate_unsigned` + // we can skip doing it here again. + _signature: ::Signature + ) { + ensure_none(origin)?; + + let current_session = >::current_index(); + let exists = ::contains_key( + ¤t_session, + &heartbeat.authority_index + ); + let keys = Keys::::get(); + let public = keys.get(heartbeat.authority_index as usize); + if let (false, Some(public)) = (exists, public) { + Self::deposit_event(Event::::HeartbeatReceived(public.clone())); + + let network_state = heartbeat.network_state.encode(); + ::insert( + ¤t_session, + &heartbeat.authority_index, + &network_state + ); + } else if exists { + Err(Error::::DuplicatedHeartbeat)? + } else { + Err(Error::::InvalidKey)? + } + } + + // Runs after every block. + fn offchain_worker(now: T::BlockNumber) { + // Only send messages if we are a potential validator. + if sp_io::offchain::is_validator() { + for res in Self::send_heartbeats(now).into_iter().flatten() { + if let Err(e) = res { + debug::debug!( + target: "imonline", + "Skipping heartbeat at {:?}: {:?}", + now, + e, + ) + } + } + } else { + debug::trace!( + target: "imonline", + "Skipping heartbeat at {:?}. Not a validator.", + now, + ) + } + } + } +} + +type OffchainResult = Result::BlockNumber>>; + +/// Keep track of number of authored blocks per authority, uncles are counted as +/// well since they're a valid proof of being online. +impl pallet_authorship::EventHandler for Module { + fn note_author(author: T::ValidatorId) { + Self::note_authorship(author); + } + + fn note_uncle(author: T::ValidatorId, _age: T::BlockNumber) { + Self::note_authorship(author); + } +} + +impl Module { + /// Returns `true` if a heartbeat has been received for the authority at + /// `authority_index` in the authorities series or if the authority has + /// authored at least one block, during the current session. Otherwise + /// `false`. + pub fn is_online(authority_index: AuthIndex) -> bool { + let current_validators = >::validators(); + + if authority_index >= current_validators.len() as u32 { + return false; + } + + let authority = ¤t_validators[authority_index as usize]; + + Self::is_online_aux(authority_index, authority) + } + + fn is_online_aux(authority_index: AuthIndex, authority: &T::ValidatorId) -> bool { + let current_session = >::current_index(); + + ::contains_key(¤t_session, &authority_index) || + >::get( + ¤t_session, + authority, + ) != 0 + } + + /// Returns `true` if a heartbeat has been received for the authority at `authority_index` in + /// the authorities series, during the current session. Otherwise `false`. + pub fn received_heartbeat_in_current_session(authority_index: AuthIndex) -> bool { + let current_session = >::current_index(); + ::contains_key(¤t_session, &authority_index) + } + + /// Note that the given authority has authored a block in the current session. + fn note_authorship(author: T::ValidatorId) { + let current_session = >::current_index(); + + >::mutate( + ¤t_session, + author, + |authored| *authored += 1, + ); + } + + pub(crate) fn send_heartbeats(block_number: T::BlockNumber) + -> OffchainResult>> + { + let heartbeat_after = >::get(); + if block_number < heartbeat_after { + return Err(OffchainErr::TooEarly(heartbeat_after)) + } + + let session_index = >::current_index(); + Ok(Self::local_authority_keys() + .map(move |(authority_index, key)| + Self::send_single_heartbeat(authority_index, key, session_index, block_number) + )) + } + + + fn send_single_heartbeat( + authority_index: u32, + key: T::AuthorityId, + session_index: SessionIndex, + block_number: T::BlockNumber + ) -> OffchainResult { + // A helper function to prepare heartbeat call. + let prepare_heartbeat = || -> OffchainResult> { + let network_state = sp_io::offchain::network_state() + .map_err(|_| OffchainErr::NetworkState)?; + let heartbeat_data = Heartbeat { + block_number, + network_state, + session_index, + authority_index, + }; + let signature = key.sign(&heartbeat_data.encode()).ok_or(OffchainErr::FailedSigning)?; + Ok(Call::heartbeat(heartbeat_data, signature)) + }; + + if Self::is_online(authority_index) { + return Err(OffchainErr::AlreadyOnline(authority_index)); + } + + // acquire lock for that authority at current heartbeat to make sure we don't + // send concurrent heartbeats. + Self::with_heartbeat_lock( + authority_index, + session_index, + block_number, + || { + let call = prepare_heartbeat()?; + debug::info!( + target: "imonline", + "[index: {:?}] Reporting im-online at block: {:?} (session: {:?}): {:?}", + authority_index, + block_number, + session_index, + call, + ); + + T::SubmitTransaction::submit_unsigned(call) + .map_err(|_| OffchainErr::SubmitTransaction)?; + + Ok(()) + }, + ) + } + + fn local_authority_keys() -> impl Iterator { + // we run only when a local authority key is configured + let authorities = Keys::::get(); + let mut local_keys = T::AuthorityId::all(); + local_keys.sort(); + + authorities.into_iter() + .enumerate() + .filter_map(move |(index, authority)| { + local_keys.binary_search(&authority) + .ok() + .map(|location| (index as u32, local_keys[location].clone())) + }) + } + + fn with_heartbeat_lock( + authority_index: u32, + session_index: SessionIndex, + now: T::BlockNumber, + f: impl FnOnce() -> OffchainResult, + ) -> OffchainResult { + let key = { + let mut key = DB_PREFIX.to_vec(); + key.extend(authority_index.encode()); + key + }; + let storage = StorageValueRef::persistent(&key); + let res = storage.mutate(|status: Option>>| { + // Check if there is already a lock for that particular block. + // This means that the heartbeat has already been sent, and we are just waiting + // for it to be included. However if it doesn't get included for INCLUDE_THRESHOLD + // we will re-send it. + match status { + // we are still waiting for inclusion. + Some(Some(status)) if status.is_recent(session_index, now) => { + Err(OffchainErr::WaitingForInclusion(status.sent_at)) + }, + // attempt to set new status + _ => Ok(HeartbeatStatus { + session_index, + sent_at: now, + }), + } + })?; + + let mut new_status = res.map_err(|_| OffchainErr::FailedToAcquireLock)?; + + // we got the lock, let's try to send the heartbeat. + let res = f(); + + // clear the lock in case we have failed to send transaction. + if res.is_err() { + new_status.sent_at = 0.into(); + storage.set(&new_status); + } + + res + } + + fn initialize_keys(keys: &[T::AuthorityId]) { + if !keys.is_empty() { + assert!(Keys::::get().is_empty(), "Keys are already initialized!"); + Keys::::put(keys); + } + } +} + +impl sp_runtime::BoundToRuntimeAppPublic for Module { + type Public = T::AuthorityId; +} + +impl pallet_session::OneSessionHandler for Module { + type Key = T::AuthorityId; + + fn on_genesis_session<'a, I: 'a>(validators: I) + where I: Iterator + { + let keys = validators.map(|x| x.1).collect::>(); + Self::initialize_keys(&keys); + } + + fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, _queued_validators: I) + where I: Iterator + { + // Tell the offchain worker to start making the next session's heartbeats. + // Since we consider producing blocks as being online, + // the heartbeat is deferred a bit to prevent spamming. + let block_number = >::block_number(); + let half_session = T::SessionDuration::get() / 2.into(); + >::put(block_number + half_session); + + // Remember who the authorities are for the new session. + Keys::::put(validators.map(|x| x.1).collect::>()); + } + + fn on_before_session_ending() { + let session_index = >::current_index(); + let keys = Keys::::get(); + let current_validators = >::validators(); + + let offenders = current_validators.into_iter().enumerate() + .filter(|(index, id)| + !Self::is_online_aux(*index as u32, id) + ).filter_map(|(_, id)| + T::FullIdentificationOf::convert(id.clone()).map(|full_id| (id, full_id)) + ).collect::>>(); + + // Remove all received heartbeats and number of authored blocks from the + // current session, they have already been processed and won't be needed + // anymore. + ::remove_prefix(&>::current_index()); + >::remove_prefix(&>::current_index()); + + if offenders.is_empty() { + Self::deposit_event(RawEvent::AllGood); + } else { + Self::deposit_event(RawEvent::SomeOffline(offenders.clone())); + + let validator_set_count = keys.len() as u32; + let offence = UnresponsivenessOffence { session_index, validator_set_count, offenders }; + if let Err(e) = T::ReportUnresponsiveness::report_offence(vec![], offence) { + sp_runtime::print(e); + } + } + } + + fn on_disabled(_i: usize) { + // ignore + } +} + +impl frame_support::unsigned::ValidateUnsigned for Module { + type Call = Call; + + fn validate_unsigned(call: &Self::Call) -> TransactionValidity { + if let Call::heartbeat(heartbeat, signature) = call { + if >::is_online(heartbeat.authority_index) { + // we already received a heartbeat for this authority + return InvalidTransaction::Stale.into(); + } + + // check if session index from heartbeat is recent + let current_session = >::current_index(); + if heartbeat.session_index != current_session { + return InvalidTransaction::Stale.into(); + } + + // verify that the incoming (unverified) pubkey is actually an authority id + let keys = Keys::::get(); + let authority_id = match keys.get(heartbeat.authority_index as usize) { + Some(id) => id, + None => return InvalidTransaction::BadProof.into(), + }; + + // check signature (this is expensive so we do it last). + let signature_valid = heartbeat.using_encoded(|encoded_heartbeat| { + authority_id.verify(&encoded_heartbeat, &signature) + }); + + if !signature_valid { + return InvalidTransaction::BadProof.into(); + } + + Ok(ValidTransaction { + priority: TransactionPriority::max_value(), + requires: vec![], + provides: vec![(current_session, authority_id).encode()], + longevity: TryInto::::try_into(T::SessionDuration::get() / 2.into()).unwrap_or(64_u64), + propagate: true, + }) + } else { + InvalidTransaction::Call.into() + } + } +} + +/// An offence that is filed if a validator didn't send a heartbeat message. +#[derive(RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] +pub struct UnresponsivenessOffence { + /// The current session index in which we report the unresponsive validators. + /// + /// It acts as a time measure for unresponsiveness reports and effectively will always point + /// at the end of the session. + session_index: SessionIndex, + /// The size of the validator set in current session/era. + validator_set_count: u32, + /// Authorities that were unresponsive during the current era. + offenders: Vec, +} + +impl Offence for UnresponsivenessOffence { + const ID: Kind = *b"im-online:offlin"; + type TimeSlot = SessionIndex; + + fn offenders(&self) -> Vec { + self.offenders.clone() + } + + fn session_index(&self) -> SessionIndex { + self.session_index + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> Self::TimeSlot { + self.session_index + } + + fn slash_fraction(offenders: u32, validator_set_count: u32) -> Perbill { + // the formula is min((3 * (k - (n / 10 + 1))) / n, 1) * 0.07 + // basically, 10% can be offline with no slash, but after that, it linearly climbs up to 7% + // when 13/30 are offline (around 5% when 1/3 are offline). + if let Some(threshold) = offenders.checked_sub(validator_set_count / 10 + 1) { + let x = Perbill::from_rational_approximation(3 * threshold, validator_set_count); + x.saturating_mul(Perbill::from_percent(7)) + } else { + Perbill::default() + } + } +} diff --git a/frame/im-online/src/mock.rs b/frame/im-online/src/mock.rs new file mode 100644 index 000000000..78b6409d5 --- /dev/null +++ b/frame/im-online/src/mock.rs @@ -0,0 +1,181 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Test utilities + +#![cfg(test)] + +use std::cell::RefCell; + +use crate::{Module, Trait}; +use sp_runtime::Perbill; +use sp_staking::{SessionIndex, offence::{ReportOffence, OffenceError}}; +use sp_runtime::testing::{Header, UintAuthorityId, TestXt}; +use sp_runtime::traits::{IdentityLookup, BlakeTwo256, ConvertInto}; +use sp_core::H256; +use frame_support::{impl_outer_origin, impl_outer_dispatch, parameter_types, weights::Weight}; + +use frame_system as system; +impl_outer_origin!{ + pub enum Origin for Runtime {} +} + +impl_outer_dispatch! { + pub enum Call for Runtime where origin: Origin { + imonline::ImOnline, + } +} + +thread_local! { + pub static VALIDATORS: RefCell>> = RefCell::new(Some(vec![1, 2, 3])); +} + +pub struct TestSessionManager; +impl pallet_session::SessionManager for TestSessionManager { + fn new_session(_new_index: SessionIndex) -> Option> { + VALIDATORS.with(|l| l.borrow_mut().take()) + } + fn end_session(_: SessionIndex) {} + fn start_session(_: SessionIndex) {} +} + +impl pallet_session::historical::SessionManager for TestSessionManager { + fn new_session(_new_index: SessionIndex) -> Option> { + VALIDATORS.with(|l| l + .borrow_mut() + .take() + .map(|validators| { + validators.iter().map(|v| (*v, *v)).collect() + }) + ) + } + fn end_session(_: SessionIndex) {} + fn start_session(_: SessionIndex) {} +} + +/// An extrinsic type used for tests. +pub type Extrinsic = TestXt; +type SubmitTransaction = frame_system::offchain::TransactionSubmitter<(), Call, Extrinsic>; +type IdentificationTuple = (u64, u64); +type Offence = crate::UnresponsivenessOffence; + +thread_local! { + pub static OFFENCES: RefCell, Offence)>> = RefCell::new(vec![]); +} + +/// A mock offence report handler. +pub struct OffenceHandler; +impl ReportOffence for OffenceHandler { + fn report_offence(reporters: Vec, offence: Offence) -> Result<(), OffenceError> { + OFFENCES.with(|l| l.borrow_mut().push((reporters, offence))); + Ok(()) + } +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + t.into() +} + + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Runtime; + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); +} + +impl frame_system::Trait for Runtime { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = Call; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); +} + +parameter_types! { + pub const Period: u64 = 1; + pub const Offset: u64 = 0; +} + +parameter_types! { + pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(33); +} + +impl pallet_session::Trait for Runtime { + type ShouldEndSession = pallet_session::PeriodicSessions; + type SessionManager = pallet_session::historical::NoteHistoricalRoot; + type SessionHandler = (ImOnline, ); + type ValidatorId = u64; + type ValidatorIdOf = ConvertInto; + type Keys = UintAuthorityId; + type Event = (); + type DisabledValidatorsThreshold = DisabledValidatorsThreshold; +} + +impl pallet_session::historical::Trait for Runtime { + type FullIdentification = u64; + type FullIdentificationOf = ConvertInto; +} + +parameter_types! { + pub const UncleGenerations: u32 = 5; +} + +impl pallet_authorship::Trait for Runtime { + type FindAuthor = (); + type UncleGenerations = UncleGenerations; + type FilterUncle = (); + type EventHandler = ImOnline; +} + +impl Trait for Runtime { + type AuthorityId = UintAuthorityId; + type Event = (); + type Call = Call; + type SubmitTransaction = SubmitTransaction; + type ReportUnresponsiveness = OffenceHandler; + type SessionDuration = Period; +} + +/// Im Online module. +pub type ImOnline = Module; +pub type System = frame_system::Module; +pub type Session = pallet_session::Module; + +pub fn advance_session() { + let now = System::block_number(); + System::set_block_number(now + 1); + Session::rotate_session(); + assert_eq!(Session::current_index(), (now / Period::get()) as u32); +} diff --git a/frame/im-online/src/tests.rs b/frame/im-online/src/tests.rs new file mode 100644 index 000000000..b43adca0f --- /dev/null +++ b/frame/im-online/src/tests.rs @@ -0,0 +1,344 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Tests for the im-online module. + +#![cfg(test)] + +use super::*; +use crate::mock::*; +use sp_core::offchain::{ + OpaquePeerId, + OffchainExt, + TransactionPoolExt, + testing::{TestOffchainExt, TestTransactionPoolExt}, +}; +use frame_support::{dispatch, assert_noop}; +use sp_runtime::testing::UintAuthorityId; + +#[test] +fn test_unresponsiveness_slash_fraction() { + // A single case of unresponsiveness is not slashed. + assert_eq!( + UnresponsivenessOffence::<()>::slash_fraction(1, 50), + Perbill::zero(), + ); + + assert_eq!( + UnresponsivenessOffence::<()>::slash_fraction(5, 50), + Perbill::zero(), // 0% + ); + + assert_eq!( + UnresponsivenessOffence::<()>::slash_fraction(7, 50), + Perbill::from_parts(4200000), // 0.42% + ); + + // One third offline should be punished around 5%. + assert_eq!( + UnresponsivenessOffence::<()>::slash_fraction(17, 50), + Perbill::from_parts(46200000), // 4.62% + ); +} + +#[test] +fn should_report_offline_validators() { + new_test_ext().execute_with(|| { + // given + let block = 1; + System::set_block_number(block); + // buffer new validators + Session::rotate_session(); + // enact the change and buffer another one + let validators = vec![1, 2, 3, 4, 5, 6]; + VALIDATORS.with(|l| *l.borrow_mut() = Some(validators.clone())); + Session::rotate_session(); + + // when + // we end current session and start the next one + Session::rotate_session(); + + // then + let offences = OFFENCES.with(|l| l.replace(vec![])); + assert_eq!(offences, vec![ + (vec![], UnresponsivenessOffence { + session_index: 2, + validator_set_count: 3, + offenders: vec![ + (1, 1), + (2, 2), + (3, 3), + ], + }) + ]); + + // should not report when heartbeat is sent + for (idx, v) in validators.into_iter().take(4).enumerate() { + let _ = heartbeat(block, 3, idx as u32, v.into()).unwrap(); + } + Session::rotate_session(); + + // then + let offences = OFFENCES.with(|l| l.replace(vec![])); + assert_eq!(offences, vec![ + (vec![], UnresponsivenessOffence { + session_index: 3, + validator_set_count: 6, + offenders: vec![ + (5, 5), + (6, 6), + ], + }) + ]); + }); +} + +fn heartbeat( + block_number: u64, + session_index: u32, + authority_index: u32, + id: UintAuthorityId, +) -> dispatch::DispatchResult { + use frame_support::unsigned::ValidateUnsigned; + + let heartbeat = Heartbeat { + block_number, + network_state: OpaqueNetworkState { + peer_id: OpaquePeerId(vec![1]), + external_addresses: vec![], + }, + session_index, + authority_index, + }; + let signature = id.sign(&heartbeat.encode()).unwrap(); + + ImOnline::pre_dispatch(&crate::Call::heartbeat(heartbeat.clone(), signature.clone())) + .map_err(|e| <&'static str>::from(e))?; + ImOnline::heartbeat( + Origin::system(frame_system::RawOrigin::None), + heartbeat, + signature + ) +} + +#[test] +fn should_mark_online_validator_when_heartbeat_is_received() { + new_test_ext().execute_with(|| { + advance_session(); + // given + VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6])); + assert_eq!(Session::validators(), Vec::::new()); + // enact the change and buffer another one + advance_session(); + + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![1, 2, 3]); + + assert!(!ImOnline::is_online(0)); + assert!(!ImOnline::is_online(1)); + assert!(!ImOnline::is_online(2)); + + // when + let _ = heartbeat(1, 2, 0, 1.into()).unwrap(); + + // then + assert!(ImOnline::is_online(0)); + assert!(!ImOnline::is_online(1)); + assert!(!ImOnline::is_online(2)); + + // and when + let _ = heartbeat(1, 2, 2, 3.into()).unwrap(); + + // then + assert!(ImOnline::is_online(0)); + assert!(!ImOnline::is_online(1)); + assert!(ImOnline::is_online(2)); + }); +} + +#[test] +fn late_heartbeat_should_fail() { + new_test_ext().execute_with(|| { + advance_session(); + // given + VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 4, 4, 5, 6])); + assert_eq!(Session::validators(), Vec::::new()); + // enact the change and buffer another one + advance_session(); + + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![1, 2, 3]); + + // when + assert_noop!(heartbeat(1, 3, 0, 1.into()), "Transaction is outdated"); + assert_noop!(heartbeat(1, 1, 0, 1.into()), "Transaction is outdated"); + }); +} + +#[test] +fn should_generate_heartbeats() { + use sp_runtime::traits::OffchainWorker; + + let mut ext = new_test_ext(); + let (offchain, _state) = TestOffchainExt::new(); + let (pool, state) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + + ext.execute_with(|| { + // given + let block = 1; + System::set_block_number(block); + UintAuthorityId::set_all_keys(vec![0, 1, 2]); + // buffer new validators + Session::rotate_session(); + // enact the change and buffer another one + VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6])); + Session::rotate_session(); + + // when + ImOnline::offchain_worker(block); + + // then + let transaction = state.write().transactions.pop().unwrap(); + // All validators have `0` as their session key, so we generate 2 transactions. + assert_eq!(state.read().transactions.len(), 2); + + // check stuff about the transaction. + let ex: Extrinsic = Decode::decode(&mut &*transaction).unwrap(); + let heartbeat = match ex.call { + crate::mock::Call::ImOnline(crate::Call::heartbeat(h, _)) => h, + e => panic!("Unexpected call: {:?}", e), + }; + + assert_eq!(heartbeat, Heartbeat { + block_number: block, + network_state: sp_io::offchain::network_state().unwrap(), + session_index: 2, + authority_index: 2, + }); + }); +} + +#[test] +fn should_cleanup_received_heartbeats_on_session_end() { + new_test_ext().execute_with(|| { + advance_session(); + + VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3])); + assert_eq!(Session::validators(), Vec::::new()); + + // enact the change and buffer another one + advance_session(); + + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![1, 2, 3]); + + // send an heartbeat from authority id 0 at session 2 + let _ = heartbeat(1, 2, 0, 1.into()).unwrap(); + + // the heartbeat is stored + assert!(!ImOnline::received_heartbeats(&2, &0).is_none()); + + advance_session(); + + // after the session has ended we have already processed the heartbeat + // message, so any messages received on the previous session should have + // been pruned. + assert!(ImOnline::received_heartbeats(&2, &0).is_none()); + }); +} + +#[test] +fn should_mark_online_validator_when_block_is_authored() { + use pallet_authorship::EventHandler; + + new_test_ext().execute_with(|| { + advance_session(); + // given + VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6])); + assert_eq!(Session::validators(), Vec::::new()); + // enact the change and buffer another one + advance_session(); + + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![1, 2, 3]); + + for i in 0..3 { + assert!(!ImOnline::is_online(i)); + } + + // when + ImOnline::note_author(1); + ImOnline::note_uncle(2, 0); + + // then + assert!(ImOnline::is_online(0)); + assert!(ImOnline::is_online(1)); + assert!(!ImOnline::is_online(2)); + }); +} + +#[test] +fn should_not_send_a_report_if_already_online() { + use pallet_authorship::EventHandler; + + let mut ext = new_test_ext(); + let (offchain, _state) = TestOffchainExt::new(); + let (pool, pool_state) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + + ext.execute_with(|| { + advance_session(); + // given + VALIDATORS.with(|l| *l.borrow_mut() = Some(vec![1, 2, 3, 4, 5, 6])); + assert_eq!(Session::validators(), Vec::::new()); + // enact the change and buffer another one + advance_session(); + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![1, 2, 3]); + ImOnline::note_author(2); + ImOnline::note_uncle(3, 0); + + // when + UintAuthorityId::set_all_keys(vec![0]); // all authorities use pallet_session key 0 + // we expect error, since the authority is already online. + let mut res = ImOnline::send_heartbeats(4).unwrap(); + assert_eq!(res.next().unwrap().unwrap(), ()); + assert_eq!(res.next().unwrap().unwrap_err(), OffchainErr::AlreadyOnline(1)); + assert_eq!(res.next().unwrap().unwrap_err(), OffchainErr::AlreadyOnline(2)); + assert_eq!(res.next(), None); + + // then + let transaction = pool_state.write().transactions.pop().unwrap(); + // All validators have `0` as their session key, but we should only produce 1 heartbeat. + assert_eq!(pool_state.read().transactions.len(), 0); + // check stuff about the transaction. + let ex: Extrinsic = Decode::decode(&mut &*transaction).unwrap(); + let heartbeat = match ex.call { + crate::mock::Call::ImOnline(crate::Call::heartbeat(h, _)) => h, + e => panic!("Unexpected call: {:?}", e), + }; + + assert_eq!(heartbeat, Heartbeat { + block_number: 4, + network_state: sp_io::offchain::network_state().unwrap(), + session_index: 2, + authority_index: 0, + }); + }); +} diff --git a/frame/offences/Cargo.toml b/frame/offences/Cargo.toml new file mode 100644 index 000000000..137207693 --- /dev/null +++ b/frame/offences/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "pallet-offences" +version = "0.5.0" +authors = ["Darwinia Network "] +description = "FRAME offences pallet" +edition = "2018" +license = "GPL-3.0" +homepage = "https://darwinia.network/" +repository = "https://github.com/darwinia-network/darwinia/" + +[dependencies] +# crates.io +codec = { package = "parity-scale-codec", version = "1.2.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.101", optional = true } + +# github.com +frame-support = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +frame-system = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +sp-std = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-runtime = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +sp-staking = { default-features = false, path = "../../primitives/staking" } + +[dev-dependencies] +sp-io = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-core = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +[features] +default = ["std"] +std = [ + "codec/std", + "serde", + + "frame-support/std", + "frame-system/std", + + "sp-runtime/std", + "sp-staking/std", + "sp-std/std", +] diff --git a/frame/offences/src/lib.rs b/frame/offences/src/lib.rs new file mode 100644 index 000000000..27983cbb5 --- /dev/null +++ b/frame/offences/src/lib.rs @@ -0,0 +1,250 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! # Offences Module +//! +//! Tracks reported offences + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +mod mock; +mod tests; + +use sp_std::vec::Vec; +use frame_support::{ + decl_module, decl_event, decl_storage, Parameter, +}; +use sp_runtime::traits::Hash; +use sp_staking::{ + offence::{Offence, ReportOffence, Kind, OnOffenceHandler, OffenceDetails, OffenceError}, +}; +use codec::{Encode, Decode}; +use frame_system as system; + +/// A binary blob which represents a SCALE codec-encoded `O::TimeSlot`. +type OpaqueTimeSlot = Vec; + +/// A type alias for a report identifier. +type ReportIdOf = ::Hash; + +/// Offences trait +pub trait Trait: frame_system::Trait { + /// The overarching event type. + type Event: From + Into<::Event>; + /// Full identification of the validator. + type IdentificationTuple: Parameter + Ord; + /// A handler called for every offence report. + type OnOffenceHandler: OnOffenceHandler; +} + +decl_storage! { + trait Store for Module as Offences { + /// The primary structure that holds all offence records keyed by report identifiers. + Reports get(fn reports): map hasher(blake2_256) ReportIdOf => Option>; + + /// A vector of reports of the same kind that happened at the same time slot. + ConcurrentReportsIndex: + double_map hasher(blake2_256) Kind, hasher(blake2_256) OpaqueTimeSlot + => Vec>; + + /// Enumerates all reports of a kind along with the time they happened. + /// + /// All reports are sorted by the time of offence. + /// + /// Note that the actual type of this mapping is `Vec`, this is because values of + /// different types are not supported at the moment so we are doing the manual serialization. + ReportsByKindIndex: map hasher(blake2_256) Kind => Vec; // (O::TimeSlot, ReportIdOf) + } +} + +decl_event!( + pub enum Event { + /// There is an offence reported of the given `kind` happened at the `session_index` and + /// (kind-specific) time slot. This event is not deposited for duplicate slashes. + Offence(Kind, OpaqueTimeSlot), + } +); + +decl_module! { + /// Offences module, currently just responsible for taking offence reports. + pub struct Module for enum Call where origin: T::Origin { + fn deposit_event() = default; + } +} +impl> + ReportOffence for Module +where + T::IdentificationTuple: Clone, +{ + fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError> { + let offenders = offence.offenders(); + let time_slot = offence.time_slot(); + let validator_set_count = offence.validator_set_count(); + + // Go through all offenders in the offence report and find all offenders that was spotted + // in unique reports. + let TriageOutcome { concurrent_offenders } = match Self::triage_offence_report::( + reporters, + &time_slot, + offenders, + ) { + Some(triage) => triage, + // The report contained only duplicates, so there is no need to slash again. + None => return Err(OffenceError::DuplicateReport), + }; + + // Deposit the event. + Self::deposit_event(Event::Offence(O::ID, time_slot.encode())); + + let offenders_count = concurrent_offenders.len() as u32; + + // The amount new offenders are slashed + let new_fraction = O::slash_fraction(offenders_count, validator_set_count); + + let slash_perbill: Vec<_> = (0..concurrent_offenders.len()) + .map(|_| new_fraction.clone()).collect(); + + T::OnOffenceHandler::on_offence( + &concurrent_offenders, + &slash_perbill, + offence.session_index(), + ); + + Ok(()) + } +} + +impl Module { + /// Compute the ID for the given report properties. + /// + /// The report id depends on the offence kind, time slot and the id of offender. + fn report_id>( + time_slot: &O::TimeSlot, + offender: &T::IdentificationTuple, + ) -> ReportIdOf { + (O::ID, time_slot.encode(), offender).using_encoded(T::Hashing::hash) + } + + /// Triages the offence report and returns the set of offenders that was involved in unique + /// reports along with the list of the concurrent offences. + fn triage_offence_report>( + reporters: Vec, + time_slot: &O::TimeSlot, + offenders: Vec, + ) -> Option> { + let mut storage = ReportIndexStorage::::load(time_slot); + + let mut any_new = false; + for offender in offenders { + let report_id = Self::report_id::(time_slot, &offender); + + if !>::contains_key(&report_id) { + any_new = true; + >::insert( + &report_id, + OffenceDetails { + offender, + reporters: reporters.clone(), + }, + ); + + storage.insert(time_slot, report_id); + } + } + + if any_new { + // Load report details for the all reports happened at the same time. + let concurrent_offenders = storage.concurrent_reports + .iter() + .filter_map(|report_id| >::get(report_id)) + .collect::>(); + + storage.save(); + + Some(TriageOutcome { + concurrent_offenders, + }) + } else { + None + } + } +} + +struct TriageOutcome { + /// Other reports for the same report kinds. + concurrent_offenders: Vec>, +} + +/// An auxiliary struct for working with storage of indexes localized for a specific offence +/// kind (specified by the `O` type parameter). +/// +/// This struct is responsible for aggregating storage writes and the underlying storage should not +/// accessed directly meanwhile. +#[must_use = "The changes are not saved without called `save`"] +struct ReportIndexStorage> { + opaque_time_slot: OpaqueTimeSlot, + concurrent_reports: Vec>, + same_kind_reports: Vec<(O::TimeSlot, ReportIdOf)>, +} + +impl> ReportIndexStorage { + /// Preload indexes from the storage for the specific `time_slot` and the kind of the offence. + fn load(time_slot: &O::TimeSlot) -> Self { + let opaque_time_slot = time_slot.encode(); + + let same_kind_reports = ::get(&O::ID); + let same_kind_reports = + Vec::<(O::TimeSlot, ReportIdOf)>::decode(&mut &same_kind_reports[..]) + .unwrap_or_default(); + + let concurrent_reports = >::get(&O::ID, &opaque_time_slot); + + Self { + opaque_time_slot, + concurrent_reports, + same_kind_reports, + } + } + + /// Insert a new report to the index. + fn insert(&mut self, time_slot: &O::TimeSlot, report_id: ReportIdOf) { + // Insert the report id into the list while maintaining the ordering by the time + // slot. + let pos = match self + .same_kind_reports + .binary_search_by_key(&time_slot, |&(ref when, _)| when) + { + Ok(pos) => pos, + Err(pos) => pos, + }; + self.same_kind_reports + .insert(pos, (time_slot.clone(), report_id)); + + // Update the list of concurrent reports. + self.concurrent_reports.push(report_id); + } + + /// Dump the indexes to the storage. + fn save(self) { + ::insert(&O::ID, self.same_kind_reports.encode()); + >::insert( + &O::ID, + &self.opaque_time_slot, + &self.concurrent_reports, + ); + } +} diff --git a/frame/offences/src/mock.rs b/frame/offences/src/mock.rs new file mode 100644 index 000000000..a003ad691 --- /dev/null +++ b/frame/offences/src/mock.rs @@ -0,0 +1,169 @@ +// Copyright 2018-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Test utilities + +#![cfg(test)] + +use std::cell::RefCell; +use crate::{Module, Trait}; +use codec::Encode; +use sp_runtime::Perbill; +use sp_staking::{ + SessionIndex, + offence::{self, Kind, OffenceDetails}, +}; +use sp_runtime::testing::Header; +use sp_runtime::traits::{IdentityLookup, BlakeTwo256}; +use sp_core::H256; +use frame_support::{ + impl_outer_origin, impl_outer_event, parameter_types, StorageMap, StorageDoubleMap, + weights::Weight, +}; +use frame_system as system; + +impl_outer_origin!{ + pub enum Origin for Runtime {} +} + +pub struct OnOffenceHandler; + +thread_local! { + pub static ON_OFFENCE_PERBILL: RefCell> = RefCell::new(Default::default()); +} + +impl offence::OnOffenceHandler for OnOffenceHandler { + fn on_offence( + _offenders: &[OffenceDetails], + slash_fraction: &[Perbill], + _offence_session: SessionIndex, + ) { + ON_OFFENCE_PERBILL.with(|f| { + *f.borrow_mut() = slash_fraction.to_vec(); + }); + } +} + +pub fn with_on_offence_fractions) -> R>(f: F) -> R { + ON_OFFENCE_PERBILL.with(|fractions| { + f(&mut *fractions.borrow_mut()) + }) +} + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Runtime; +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); +} +impl frame_system::Trait for Runtime { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); +} + +impl Trait for Runtime { + type Event = TestEvent; + type IdentificationTuple = u64; + type OnOffenceHandler = OnOffenceHandler; +} + +mod offences { + pub use crate::Event; +} + +impl_outer_event! { + pub enum TestEvent for Runtime { + system, + offences, + } +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + t.into() +} + +/// Offences module. +pub type Offences = Module; +pub type System = frame_system::Module; + +pub const KIND: [u8; 16] = *b"test_report_1234"; + +/// Returns all offence details for the specific `kind` happened at the specific time slot. +pub fn offence_reports(kind: Kind, time_slot: u128) -> Vec> { + >::get(&kind, &time_slot.encode()) + .into_iter() + .map(|report_id| { + >::get(&report_id) + .expect("dangling report id is found in ConcurrentReportsIndex") + }) + .collect() +} + +#[derive(Clone)] +pub struct Offence { + pub validator_set_count: u32, + pub offenders: Vec, + pub time_slot: u128, +} + +impl offence::Offence for Offence { + const ID: offence::Kind = KIND; + type TimeSlot = u128; + + fn offenders(&self) -> Vec { + self.offenders.clone() + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> u128 { + self.time_slot + } + + fn session_index(&self) -> SessionIndex { + 1 + } + + fn slash_fraction( + offenders_count: u32, + validator_set_count: u32, + ) -> Perbill { + Perbill::from_percent(5 + offenders_count * 100 / validator_set_count) + } +} diff --git a/frame/offences/src/tests.rs b/frame/offences/src/tests.rs new file mode 100644 index 000000000..0ed98427c --- /dev/null +++ b/frame/offences/src/tests.rs @@ -0,0 +1,214 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Tests for the offences module. + +#![cfg(test)] + +use super::*; +use crate::mock::{ + Offences, System, Offence, TestEvent, KIND, new_test_ext, with_on_offence_fractions, + offence_reports, +}; +use sp_runtime::Perbill; +use frame_system::{EventRecord, Phase}; + +#[test] +fn should_report_an_authority_and_trigger_on_offence() { + new_test_ext().execute_with(|| { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let offence = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + + // when + Offences::report_offence(vec![], offence).unwrap(); + + // then + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + }); + }); +} + +#[test] +fn should_not_report_the_same_authority_twice_in_the_same_slot() { + new_test_ext().execute_with(|| { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let offence = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + Offences::report_offence(vec![], offence.clone()).unwrap(); + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + f.clear(); + }); + + // when + // report for the second time + assert_eq!(Offences::report_offence(vec![], offence), Err(OffenceError::DuplicateReport)); + + // then + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![]); + }); + }); +} + + +#[test] +fn should_report_in_different_time_slot() { + new_test_ext().execute_with(|| { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let mut offence = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + Offences::report_offence(vec![], offence.clone()).unwrap(); + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + f.clear(); + }); + + // when + // report for the second time + offence.time_slot += 1; + Offences::report_offence(vec![], offence).unwrap(); + + // then + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + }); + }); +} + +#[test] +fn should_deposit_event() { + new_test_ext().execute_with(|| { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let offence = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + + // when + Offences::report_offence(vec![], offence).unwrap(); + + // then + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::ApplyExtrinsic(0), + event: TestEvent::offences(crate::Event::Offence(KIND, time_slot.encode())), + topics: vec![], + }] + ); + }); +} + +#[test] +fn doesnt_deposit_event_for_dups() { + new_test_ext().execute_with(|| { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let offence = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + Offences::report_offence(vec![], offence.clone()).unwrap(); + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + f.clear(); + }); + + // when + // report for the second time + assert_eq!(Offences::report_offence(vec![], offence), Err(OffenceError::DuplicateReport)); + + // then + // there is only one event. + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::ApplyExtrinsic(0), + event: TestEvent::offences(crate::Event::Offence(KIND, time_slot.encode())), + topics: vec![], + }] + ); + }); +} + +#[test] +fn should_properly_count_offences() { + // We report two different authorities for the same issue. Ultimately, the 1st authority + // should have `count` equal 2 and the count of the 2nd one should be equal to 1. + new_test_ext().execute_with(|| { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let offence1 = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + let offence2 = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![4], + }; + Offences::report_offence(vec![], offence1).unwrap(); + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + f.clear(); + }); + + // when + // report for the second time + Offences::report_offence(vec![], offence2).unwrap(); + + // then + // the 1st authority should have count 2 and the 2nd one should be reported only once. + assert_eq!( + offence_reports(KIND, time_slot), + vec![ + OffenceDetails { offender: 5, reporters: vec![] }, + OffenceDetails { offender: 4, reporters: vec![] }, + ] + ); + }); +} diff --git a/frame/session/Cargo.toml b/frame/session/Cargo.toml new file mode 100644 index 000000000..aafd25d3f --- /dev/null +++ b/frame/session/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "pallet-session" +version = "0.5.0" +authors = ["Darwinia Network "] +description = "FRAME sessions pallet" +edition = "2018" +license = "GPL-3.0" +homepage = "https://darwinia.network/" +repository = "https://github.com/darwinia-network/darwinia/" + +[dependencies] +# crates.io +codec = { package = "parity-scale-codec", version = "1.2.0", default-features = false, features = ["derive"] } +impl-trait-for-tuples = "0.1.3" +serde = { version = "1.0.101", optional = true } + +# github.com +frame-support = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +frame-system = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +sp-io ={ default-features = false , git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-runtime = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +sp-staking = { default-features = false, path = "../../primitives/staking" } +sp-std = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-trie = { optional = true, default-features = false , git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +pallet-timestamp = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +[dev-dependencies] +lazy_static = "1.4.0" + +sp-application-crypto = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-core = { git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +[features] +default = ["std", "historical"] +historical = ["sp-trie"] +std = [ + "codec/std", + "serde", + + "frame-support/std", + "frame-system/std", + + "sp-io/std", + "sp-runtime/std", + "sp-staking/std", + "sp-std/std", + "sp-trie/std", + + "pallet-timestamp/std", +] diff --git a/frame/session/src/historical.rs b/frame/session/src/historical.rs new file mode 100644 index 000000000..6c305a1a1 --- /dev/null +++ b/frame/session/src/historical.rs @@ -0,0 +1,423 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! An opt-in utility for tracking historical sessions in FRAME-session. +//! +//! This is generally useful when implementing blockchains that require accountable +//! safety where validators from some amount f prior sessions must remain slashable. +//! +//! Rather than store the full session data for any given session, we instead commit +//! to the roots of merkle tries containing the session data. +//! +//! These roots and proofs of inclusion can be generated at any time during the current session. +//! Afterwards, the proofs can be fed to a consensus module when reporting misbehavior. + +use sp_std::prelude::*; +use codec::{Encode, Decode}; +use sp_runtime::KeyTypeId; +use sp_runtime::traits::{Convert, OpaqueKeys, Hash as HashT}; +use frame_support::{decl_module, decl_storage}; +use frame_support::{Parameter, print}; +use sp_trie::{MemoryDB, Trie, TrieMut, Recorder, EMPTY_PREFIX}; +use sp_trie::trie_types::{TrieDBMut, TrieDB}; +use super::{SessionIndex, Module as SessionModule}; + +type ValidatorCount = u32; + +/// Trait necessary for the historical module. +pub trait Trait: super::Trait { + /// Full identification of the validator. + type FullIdentification: Parameter; + + /// A conversion from validator ID to full identification. + /// + /// This should contain any references to economic actors associated with the + /// validator, since they may be outdated by the time this is queried from a + /// historical trie. + /// + /// It must return the identification for the current session index. + type FullIdentificationOf: Convert>; +} + +decl_storage! { + trait Store for Module as Session { + /// Mapping from historical session indices to session-data root hash and validator count. + HistoricalSessions get(fn historical_root): + map hasher(blake2_256) SessionIndex => Option<(T::Hash, ValidatorCount)>; + /// The range of historical sessions we store. [first, last) + StoredRange: Option<(SessionIndex, SessionIndex)>; + /// Deprecated. + CachedObsolete: + map hasher(blake2_256) SessionIndex + => Option>; + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + fn on_initialize(_n: T::BlockNumber) { + CachedObsolete::::remove_all(); + } + } +} + +impl Module { + /// Prune historical stored session roots up to (but not including) + /// `up_to`. + pub fn prune_up_to(up_to: SessionIndex) { + ::StoredRange::mutate(|range| { + let (start, end) = match *range { + Some(range) => range, + None => return, // nothing to prune. + }; + + let up_to = sp_std::cmp::min(up_to, end); + + if up_to < start { + return // out of bounds. harmless. + } + + (start..up_to).for_each(::HistoricalSessions::remove); + + let new_start = up_to; + *range = if new_start == end { + None // nothing is stored. + } else { + Some((new_start, end)) + } + }) + } +} + +/// Specialization of the crate-level `SessionManager` which returns the set of full identification +/// when creating a new session. +pub trait SessionManager: crate::SessionManager { + /// If there was a validator set change, its returns the set of new validators along with their + /// full identifications. + fn new_session(new_index: SessionIndex) -> Option>; + fn start_session(start_index: SessionIndex); + fn end_session(end_index: SessionIndex); +} + +/// An `SessionManager` implementation that wraps an inner `I` and also +/// sets the historical trie root of the ending session. +pub struct NoteHistoricalRoot(sp_std::marker::PhantomData<(T, I)>); + +impl crate::SessionManager for NoteHistoricalRoot + where I: SessionManager +{ + fn new_session(new_index: SessionIndex) -> Option> { + StoredRange::mutate(|range| { + range.get_or_insert_with(|| (new_index, new_index)).1 = new_index + 1; + }); + + let new_validators_and_id = >::new_session(new_index); + let new_validators = new_validators_and_id.as_ref().map(|new_validators| { + new_validators.iter().map(|(v, _id)| v.clone()).collect() + }); + + if let Some(new_validators) = new_validators_and_id { + let count = new_validators.len() as u32; + match ProvingTrie::::generate_for(new_validators) { + Ok(trie) => >::insert(new_index, &(trie.root, count)), + Err(reason) => { + print("Failed to generate historical ancestry-inclusion proof."); + print(reason); + } + }; + } else { + let previous_index = new_index.saturating_sub(1); + if let Some(previous_session) = >::get(previous_index) { + >::insert(new_index, previous_session); + } + } + + new_validators + } + fn start_session(start_index: SessionIndex) { + >::start_session(start_index) + } + fn end_session(end_index: SessionIndex) { + >::end_session(end_index) + } +} + +type HasherOf = <::Hashing as HashT>::Hasher; + +/// A tuple of the validator's ID and their full identification. +pub type IdentificationTuple = (::ValidatorId, ::FullIdentification); + +/// a trie instance for checking and generating proofs. +pub struct ProvingTrie { + db: MemoryDB>, + root: T::Hash, +} + +impl ProvingTrie { + fn generate_for(validators: I) -> Result + where I: IntoIterator + { + let mut db = MemoryDB::default(); + let mut root = Default::default(); + + { + let mut trie = TrieDBMut::new(&mut db, &mut root); + for (i, (validator, full_id)) in validators.into_iter().enumerate() { + let i = i as u32; + let keys = match >::load_keys(&validator) { + None => continue, + Some(k) => k, + }; + + let full_id = (validator, full_id); + + // map each key to the owner index. + for key_id in T::Keys::key_ids() { + let key = keys.get_raw(*key_id); + let res = (key_id, key).using_encoded(|k| + i.using_encoded(|v| trie.insert(k, v)) + ); + + let _ = res.map_err(|_| "failed to insert into trie")?; + } + + // map each owner index to the full identification. + let _ = i.using_encoded(|k| full_id.using_encoded(|v| trie.insert(k, v))) + .map_err(|_| "failed to insert into trie")?; + } + } + + Ok(ProvingTrie { + db, + root, + }) + } + + fn from_nodes(root: T::Hash, nodes: &[Vec]) -> Self { + use sp_trie::HashDBT; + + let mut memory_db = MemoryDB::default(); + for node in nodes { + HashDBT::insert(&mut memory_db, EMPTY_PREFIX, &node[..]); + } + + ProvingTrie { + db: memory_db, + root, + } + } + + /// Prove the full verification data for a given key and key ID. + pub fn prove(&self, key_id: KeyTypeId, key_data: &[u8]) -> Option>> { + let trie = TrieDB::new(&self.db, &self.root).ok()?; + let mut recorder = Recorder::new(); + let val_idx = (key_id, key_data).using_encoded(|s| { + trie.get_with(s, &mut recorder) + .ok()? + .and_then(|raw| u32::decode(&mut &*raw).ok()) + })?; + + val_idx.using_encoded(|s| { + trie.get_with(s, &mut recorder) + .ok()? + .and_then(|raw| >::decode(&mut &*raw).ok()) + })?; + + Some(recorder.drain().into_iter().map(|r| r.data).collect()) + } + + /// Access the underlying trie root. + pub fn root(&self) -> &T::Hash { + &self.root + } + + // Check a proof contained within the current memory-db. Returns `None` if the + // nodes within the current `MemoryDB` are insufficient to query the item. + fn query(&self, key_id: KeyTypeId, key_data: &[u8]) -> Option> { + let trie = TrieDB::new(&self.db, &self.root).ok()?; + let val_idx = (key_id, key_data).using_encoded(|s| trie.get(s)) + .ok()? + .and_then(|raw| u32::decode(&mut &*raw).ok())?; + + val_idx.using_encoded(|s| trie.get(s)) + .ok()? + .and_then(|raw| >::decode(&mut &*raw).ok()) + } + +} + +/// Proof of ownership of a specific key. +#[derive(Encode, Decode, Clone)] +pub struct Proof { + session: SessionIndex, + trie_nodes: Vec>, +} + +impl> frame_support::traits::KeyOwnerProofSystem<(KeyTypeId, D)> + for Module +{ + type Proof = Proof; + type IdentificationTuple = IdentificationTuple; + + fn prove(key: (KeyTypeId, D)) -> Option { + let session = >::current_index(); + let validators = >::validators().into_iter() + .filter_map(|validator| { + T::FullIdentificationOf::convert(validator.clone()) + .map(|full_id| (validator, full_id)) + }); + let trie = ProvingTrie::::generate_for(validators).ok()?; + + let (id, data) = key; + + trie.prove(id, data.as_ref()).map(|trie_nodes| Proof { + session, + trie_nodes, + }) + } + + fn check_proof(key: (KeyTypeId, D), proof: Proof) -> Option> { + let (id, data) = key; + + if proof.session == >::current_index() { + >::key_owner(id, data.as_ref()).and_then(|owner| + T::FullIdentificationOf::convert(owner.clone()).map(move |id| (owner, id)) + ) + } else { + let (root, _) = >::get(&proof.session)?; + let trie = ProvingTrie::::from_nodes(root, &proof.trie_nodes); + + trie.query(id, data.as_ref()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use sp_core::crypto::key_types::DUMMY; + use sp_runtime::{traits::OnInitialize, testing::UintAuthorityId}; + use crate::mock::{ + NEXT_VALIDATORS, force_new_session, + set_next_validators, Test, System, Session, + }; + use frame_support::traits::KeyOwnerProofSystem; + + type Historical = Module; + + fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + crate::GenesisConfig:: { + keys: NEXT_VALIDATORS.with(|l| + l.borrow().iter().cloned().map(|i| (i, i, UintAuthorityId(i).into())).collect() + ), + }.assimilate_storage(&mut t).unwrap(); + sp_io::TestExternalities::new(t) + } + + #[test] + fn generated_proof_is_good() { + new_test_ext().execute_with(|| { + set_next_validators(vec![1, 2]); + force_new_session(); + + System::set_block_number(1); + Session::on_initialize(1); + + let encoded_key_1 = UintAuthorityId(1).encode(); + let proof = Historical::prove((DUMMY, &encoded_key_1[..])).unwrap(); + + // proof-checking in the same session is OK. + assert!(Historical::check_proof((DUMMY, &encoded_key_1[..]), proof.clone()).is_some()); + + set_next_validators(vec![1, 2, 4]); + force_new_session(); + + System::set_block_number(2); + Session::on_initialize(2); + + assert!(Historical::historical_root(proof.session).is_some()); + assert!(Session::current_index() > proof.session); + + // proof-checking in the next session is also OK. + assert!(Historical::check_proof((DUMMY, &encoded_key_1[..]), proof.clone()).is_some()); + + set_next_validators(vec![1, 2, 5]); + + force_new_session(); + System::set_block_number(3); + Session::on_initialize(3); + }); + } + + #[test] + fn prune_up_to_works() { + new_test_ext().execute_with(|| { + for i in 1..99u64 { + set_next_validators(vec![i]); + force_new_session(); + + System::set_block_number(i); + Session::on_initialize(i); + + } + + assert_eq!(StoredRange::get(), Some((0, 100))); + + for i in 0..100 { + assert!(Historical::historical_root(i).is_some()) + } + + Historical::prune_up_to(10); + assert_eq!(StoredRange::get(), Some((10, 100))); + + Historical::prune_up_to(9); + assert_eq!(StoredRange::get(), Some((10, 100))); + + for i in 10..100 { + assert!(Historical::historical_root(i).is_some()) + } + + Historical::prune_up_to(99); + assert_eq!(StoredRange::get(), Some((99, 100))); + + Historical::prune_up_to(100); + assert_eq!(StoredRange::get(), None); + + for i in 99..199u64 { + set_next_validators(vec![i]); + force_new_session(); + + System::set_block_number(i); + Session::on_initialize(i); + + } + + assert_eq!(StoredRange::get(), Some((100, 200))); + + for i in 100..200 { + assert!(Historical::historical_root(i).is_some()) + } + + Historical::prune_up_to(9999); + assert_eq!(StoredRange::get(), None); + + for i in 100..200 { + assert!(Historical::historical_root(i).is_none()) + } + }); + } +} diff --git a/frame/session/src/lib.rs b/frame/session/src/lib.rs new file mode 100644 index 000000000..1097cfd6b --- /dev/null +++ b/frame/session/src/lib.rs @@ -0,0 +1,1050 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! # Session Module +//! +//! The Session module allows validators to manage their session keys, provides a function for changing +//! the session length, and handles session rotation. +//! +//! - [`session::Trait`](./trait.Trait.html) +//! - [`Call`](./enum.Call.html) +//! - [`Module`](./struct.Module.html) +//! +//! ## Overview +//! +//! ### Terminology +//! +//! +//! - **Session:** A session is a period of time that has a constant set of validators. Validators can only join +//! or exit the validator set at a session change. It is measured in block numbers. The block where a session is +//! ended is determined by the `ShouldEndSession` trait. When the session is ending, a new validator set +//! can be chosen by `OnSessionEnding` implementations. +//! - **Session key:** A session key is actually several keys kept together that provide the various signing +//! functions required by network authorities/validators in pursuit of their duties. +//! - **Validator ID:** Every account has an associated validator ID. For some simple staking systems, this +//! may just be the same as the account ID. For staking systems using a stash/controller model, +//! the validator ID would be the stash account ID of the controller. +//! - **Session key configuration process:** Session keys are set using `set_keys` for use not in +//! the next session, but the session after next. They are stored in `NextKeys`, a mapping between +//! the caller's `ValidatorId` and the session keys provided. `set_keys` allows users to set their +//! session key prior to being selected as validator. +//! It is a public call since it uses `ensure_signed`, which checks that the origin is a signed account. +//! As such, the account ID of the origin stored in `NextKeys` may not necessarily be associated with +//! a block author or a validator. The session keys of accounts are removed once their account balance is zero. +//! - **Session length:** This pallet does not assume anything about the length of each session. +//! Rather, it relies on an implementation of `ShouldEndSession` to dictate a new session's start. +//! This pallet provides the `PeriodicSessions` struct for simple periodic sessions. +//! - **Session rotation configuration:** Configure as either a 'normal' (rewardable session where rewards are +//! applied) or 'exceptional' (slashable) session rotation. +//! - **Session rotation process:** At the beginning of each block, the `on_initialize` function +//! queries the provided implementation of `ShouldEndSession`. If the session is to end the newly +//! activated validator IDs and session keys are taken from storage and passed to the +//! `SessionHandler`. The validator set supplied by `SessionManager::new_session` and the corresponding session +//! keys, which may have been registered via `set_keys` during the previous session, are written +//! to storage where they will wait one session before being passed to the `SessionHandler` +//! themselves. +//! +//! ### Goals +//! +//! The Session pallet is designed to make the following possible: +//! +//! - Set session keys of the validator set for upcoming sessions. +//! - Control the length of sessions. +//! - Configure and switch between either normal or exceptional session rotations. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! - `set_keys` - Set a validator's session keys for upcoming sessions. +//! +//! ### Public Functions +//! +//! - `rotate_session` - Change to the next session. Register the new authority set. Queue changes +//! for next session rotation. +//! - `disable_index` - Disable a validator by index. +//! - `disable` - Disable a validator by Validator ID +//! +//! ## Usage +//! +//! ### Example from the FRAME +//! +//! The [Staking pallet](../pallet_staking/index.html) uses the Session pallet to get the validator set. +//! +//! ``` +//! use pallet_session as session; +//! +//! fn validators() -> Vec<::ValidatorId> { +//! >::validators() +//! } +//! # fn main(){} +//! ``` +//! +//! ## Related Modules +//! +//! - [Staking](../pallet_staking/index.html) + +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_std::{prelude::*, marker::PhantomData, ops::{Sub, Rem}}; +use codec::Decode; +use sp_runtime::{KeyTypeId, Perbill, RuntimeAppPublic, BoundToRuntimeAppPublic}; +use frame_support::weights::SimpleDispatchInfo; +use sp_runtime::traits::{Convert, Zero, Member, OpaqueKeys}; +use sp_staking::SessionIndex; +use frame_support::{ensure, decl_module, decl_event, decl_storage, decl_error, ConsensusEngineId}; +use frame_support::{traits::{Get, FindAuthor, ValidatorRegistration}, Parameter}; +use frame_support::dispatch::{self, DispatchResult, DispatchError}; +use frame_system::{self as system, ensure_signed}; + +#[cfg(test)] +mod mock; + +#[cfg(feature = "historical")] +pub mod historical; + +/// Decides whether the session should be ended. +pub trait ShouldEndSession { + /// Return `true` if the session should be ended. + fn should_end_session(now: BlockNumber) -> bool; +} + +/// Ends the session after a fixed period of blocks. +/// +/// The first session will have length of `Offset`, and +/// the following sessions will have length of `Period`. +/// This may prove nonsensical if `Offset` >= `Period`. +pub struct PeriodicSessions< + Period, + Offset, +>(PhantomData<(Period, Offset)>); + +impl< + BlockNumber: Rem + Sub + Zero + PartialOrd, + Period: Get, + Offset: Get, +> ShouldEndSession for PeriodicSessions { + fn should_end_session(now: BlockNumber) -> bool { + let offset = Offset::get(); + now >= offset && ((now - offset) % Period::get()).is_zero() + } +} + +/// A trait for managing creation of new validator set. +pub trait SessionManager { + /// Plan a new session, and optionally provide the new validator set. + /// + /// Even if the validator-set is the same as before, if any underlying economic + /// conditions have changed (i.e. stake-weights), the new validator set must be returned. + /// This is necessary for consensus engines making use of the session module to + /// issue a validator-set change so misbehavior can be provably associated with the new + /// economic conditions as opposed to the old. + /// The returned validator set, if any, will not be applied until `new_index`. + /// `new_index` is strictly greater than from previous call. + /// + /// The first session start at index 0. + fn new_session(new_index: SessionIndex) -> Option>; + /// End the session. + /// + /// Because the session pallet can queue validator set the ending session can be lower than the + /// last new session index. + fn end_session(end_index: SessionIndex); + /// Start the session. + /// + /// The session start to be used for validation + fn start_session(start_index: SessionIndex); +} + +impl SessionManager for () { + fn new_session(_: SessionIndex) -> Option> { None } + fn start_session(_: SessionIndex) {} + fn end_session(_: SessionIndex) {} +} + +/// Handler for session life cycle events. +pub trait SessionHandler { + /// All the key type ids this session handler can process. + /// + /// The order must be the same as it expects them in + /// [`on_new_session`](Self::on_new_session) and [`on_genesis_session`](Self::on_genesis_session). + const KEY_TYPE_IDS: &'static [KeyTypeId]; + + /// The given validator set will be used for the genesis session. + /// It is guaranteed that the given validator set will also be used + /// for the second session, therefore the first call to `on_new_session` + /// should provide the same validator set. + fn on_genesis_session(validators: &[(ValidatorId, Ks)]); + + /// Session set has changed; act appropriately. Note that this can be called + /// before initialization of your module. + /// + /// `changed` is true whenever any of the session keys or underlying economic + /// identities or weightings behind those keys has changed. + fn on_new_session( + changed: bool, + validators: &[(ValidatorId, Ks)], + queued_validators: &[(ValidatorId, Ks)], + ); + + /// A notification for end of the session. + /// + /// Note it is triggered before any `SessionManager::end_session` handlers, + /// so we can still affect the validator set. + fn on_before_session_ending() {} + + /// A validator got disabled. Act accordingly until a new session begins. + fn on_disabled(validator_index: usize); +} + +/// A session handler for specific key type. +pub trait OneSessionHandler: BoundToRuntimeAppPublic { + /// The key type expected. + type Key: Decode + Default + RuntimeAppPublic; + + fn on_genesis_session<'a, I: 'a>(validators: I) + where I: Iterator, ValidatorId: 'a; + + /// Session set has changed; act appropriately. Note that this can be called + /// before initialization of your module. + /// + /// `changed` is true when at least one of the session keys + /// or the underlying economic identities/distribution behind one the + /// session keys has changed, false otherwise. + /// + /// The `validators` are the validators of the incoming session, and `queued_validators` + /// will follow. + fn on_new_session<'a, I: 'a>( + changed: bool, + validators: I, + queued_validators: I, + ) where I: Iterator, ValidatorId: 'a; + + + /// A notification for end of the session. + /// + /// Note it is triggered before any `SessionManager::end_session` handlers, + /// so we can still affect the validator set. + fn on_before_session_ending() {} + + /// A validator got disabled. Act accordingly until a new session begins. + fn on_disabled(_validator_index: usize); +} + +#[impl_trait_for_tuples::impl_for_tuples(1, 30)] +#[tuple_types_no_default_trait_bound] +impl SessionHandler for Tuple { + for_tuples!( where #( Tuple: OneSessionHandler )* ); + + for_tuples!( + const KEY_TYPE_IDS: &'static [KeyTypeId] = &[ #( ::ID ),* ]; + ); + + fn on_genesis_session(validators: &[(AId, Ks)]) { + for_tuples!( + #( + let our_keys: Box> = Box::new(validators.iter() + .map(|k| (&k.0, k.1.get::(::ID) + .unwrap_or_default()))); + + Tuple::on_genesis_session(our_keys); + )* + ) + } + + fn on_new_session( + changed: bool, + validators: &[(AId, Ks)], + queued_validators: &[(AId, Ks)], + ) { + for_tuples!( + #( + let our_keys: Box> = Box::new(validators.iter() + .map(|k| (&k.0, k.1.get::(::ID) + .unwrap_or_default()))); + let queued_keys: Box> = Box::new(queued_validators.iter() + .map(|k| (&k.0, k.1.get::(::ID) + .unwrap_or_default()))); + Tuple::on_new_session(changed, our_keys, queued_keys); + )* + ) + } + + fn on_before_session_ending() { + for_tuples!( #( Tuple::on_before_session_ending(); )* ) + } + + fn on_disabled(i: usize) { + for_tuples!( #( Tuple::on_disabled(i); )* ) + } +} + +/// `SessionHandler` for tests that use `UintAuthorityId` as `Keys`. +pub struct TestSessionHandler; +impl SessionHandler for TestSessionHandler { + const KEY_TYPE_IDS: &'static [KeyTypeId] = &[sp_runtime::key_types::DUMMY]; + + fn on_genesis_session(_: &[(AId, Ks)]) {} + + fn on_new_session(_: bool, _: &[(AId, Ks)], _: &[(AId, Ks)]) {} + + fn on_before_session_ending() {} + + fn on_disabled(_: usize) {} +} + +impl ValidatorRegistration for Module { + fn is_registered(id: &T::ValidatorId) -> bool { + Self::load_keys(id).is_some() + } +} + +pub trait Trait: frame_system::Trait { + /// The overarching event type. + type Event: From + Into<::Event>; + + /// A stable ID for a validator. + type ValidatorId: Member + Parameter; + + /// A conversion from account ID to validator ID. + type ValidatorIdOf: Convert>; + + /// Indicator for when to end the session. + type ShouldEndSession: ShouldEndSession; + + /// Handler for managing new session. + type SessionManager: SessionManager; + + /// Handler when a session has changed. + type SessionHandler: SessionHandler; + + /// The keys. + type Keys: OpaqueKeys + Member + Parameter + Default; + + /// The fraction of validators set that is safe to be disabled. + /// + /// After the threshold is reached `disabled` method starts to return true, + /// which in combination with `pallet_staking` forces a new era. + type DisabledValidatorsThreshold: Get; +} + +const DEDUP_KEY_PREFIX: &[u8] = b":session:keys"; + +decl_storage! { + trait Store for Module as Session { + /// The current set of validators. + Validators get(fn validators): Vec; + + /// Current index of the session. + CurrentIndex get(fn current_index): SessionIndex; + + /// True if the underlying economic identities or weighting behind the validators + /// has changed in the queued validator set. + QueuedChanged: bool; + + /// The queued keys for the next session. When the next session begins, these keys + /// will be used to determine the validator's session keys. + QueuedKeys get(fn queued_keys): Vec<(T::ValidatorId, T::Keys)>; + + /// Indices of disabled validators. + /// + /// The set is cleared when `on_session_ending` returns a new set of identities. + DisabledValidators get(fn disabled_validators): Vec; + + /// The next session keys for a validator. + /// + /// The first key is always `DEDUP_KEY_PREFIX` to have all the data in the same branch of + /// the trie. Having all data in the same branch should prevent slowing down other queries. + // TODO: Migrate to a normal map now https://github.com/paritytech/substrate/issues/4917 + NextKeys: double_map hasher(twox_64_concat) Vec, hasher(blake2_256) T::ValidatorId + => Option; + + /// The owner of a key. The second key is the `KeyTypeId` + the encoded key. + /// + /// The first key is always `DEDUP_KEY_PREFIX` to have all the data in the same branch of + /// the trie. Having all data in the same branch should prevent slowing down other queries. + // TODO: Migrate to a normal map now https://github.com/paritytech/substrate/issues/4917 + KeyOwner: double_map hasher(twox_64_concat) Vec, hasher(blake2_256) (KeyTypeId, Vec) + => Option; + } + add_extra_genesis { + config(keys): Vec<(T::AccountId, T::ValidatorId, T::Keys)>; + build(|config: &GenesisConfig| { + if T::SessionHandler::KEY_TYPE_IDS.len() != T::Keys::key_ids().len() { + panic!("Number of keys in session handler and session keys does not match"); + } + + T::SessionHandler::KEY_TYPE_IDS.iter().zip(T::Keys::key_ids()).enumerate() + .for_each(|(i, (sk, kk))| { + if sk != kk { + panic!( + "Session handler and session key expect different key type at index: {}", + i, + ); + } + }); + + for (account, val, keys) in config.keys.iter().cloned() { + >::inner_set_keys(&val, keys) + .expect("genesis config must not contain duplicates; qed"); + system::Module::::inc_ref(&account); + } + + let initial_validators_0 = T::SessionManager::new_session(0) + .unwrap_or_else(|| { + frame_support::print("No initial validator provided by `SessionManager`, use \ + session config keys to generate initial validator set."); + config.keys.iter().map(|x| x.1.clone()).collect() + }); + assert!(!initial_validators_0.is_empty(), "Empty validator set for session 0 in genesis block!"); + + let initial_validators_1 = T::SessionManager::new_session(1) + .unwrap_or_else(|| initial_validators_0.clone()); + assert!(!initial_validators_1.is_empty(), "Empty validator set for session 1 in genesis block!"); + + let queued_keys: Vec<_> = initial_validators_1 + .iter() + .cloned() + .map(|v| ( + v.clone(), + >::load_keys(&v).unwrap_or_default(), + )) + .collect(); + + // Tell everyone about the genesis session keys + T::SessionHandler::on_genesis_session::(&queued_keys); + + >::put(initial_validators_0); + >::put(queued_keys); + + T::SessionManager::start_session(0); + }); + } +} + +decl_event!( + pub enum Event { + /// New session has happened. Note that the argument is the session index, not the block + /// number as the type might suggest. + NewSession(SessionIndex), + } +); + +decl_error! { + /// Error for the session module. + pub enum Error for Module { + /// Invalid ownership proof. + InvalidProof, + /// No associated validator ID for account. + NoAssociatedValidatorId, + /// Registered duplicate key. + DuplicatedKey, + /// No keys are associated with this account. + NoKeys, + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + /// Used as first key for `NextKeys` and `KeyOwner` to put all the data into the same branch + /// of the trie. + const DEDUP_KEY_PREFIX: &[u8] = DEDUP_KEY_PREFIX; + + type Error = Error; + + fn deposit_event() = default; + + /// Sets the session key(s) of the function caller to `keys`. + /// Allows an account to set its session key prior to becoming a validator. + /// This doesn't take effect until the next session. + /// + /// The dispatch origin of this function must be signed. + /// + /// # + /// - O(log n) in number of accounts. + /// - One extra DB entry. + /// - Increases system account refs by one on success iff there were previously no keys set. + /// In this case, purge_keys will need to be called before the account can be removed. + /// # + #[weight = SimpleDispatchInfo::FixedNormal(150_000)] + fn set_keys(origin, keys: T::Keys, proof: Vec) -> dispatch::DispatchResult { + let who = ensure_signed(origin)?; + + ensure!(keys.ownership_proof_is_valid(&proof), Error::::InvalidProof); + + Self::do_set_keys(&who, keys)?; + + Ok(()) + } + + /// Removes any session key(s) of the function caller. + /// This doesn't take effect until the next session. + /// + /// The dispatch origin of this function must be signed. + /// + /// # + /// - O(N) in number of key types. + /// - Removes N + 1 DB entries. + /// - Reduces system account refs by one on success. + /// # + #[weight = SimpleDispatchInfo::FixedNormal(150_000)] + fn purge_keys(origin) { + let who = ensure_signed(origin)?; + Self::do_purge_keys(&who)?; + } + + /// Called when a block is initialized. Will rotate session if it is the last + /// block of the current session. + fn on_initialize(n: T::BlockNumber) { + if T::ShouldEndSession::should_end_session(n) { + Self::rotate_session(); + } + } + } +} + +impl Module { + /// Move on to next session. Register new validator set and session keys. Changes + /// to the validator set have a session of delay to take effect. This allows for + /// equivocation punishment after a fork. + pub fn rotate_session() { + let session_index = CurrentIndex::get(); + + let changed = QueuedChanged::get(); + + // Inform the session handlers that a session is going to end. + T::SessionHandler::on_before_session_ending(); + + T::SessionManager::end_session(session_index); + + // Get queued session keys and validators. + let session_keys = >::get(); + let validators = session_keys.iter() + .map(|(validator, _)| validator.clone()) + .collect::>(); + >::put(&validators); + + if changed { + // reset disabled validators + DisabledValidators::take(); + } + + // Increment session index. + let session_index = session_index + 1; + CurrentIndex::put(session_index); + + T::SessionManager::start_session(session_index); + + // Get next validator set. + let maybe_next_validators = T::SessionManager::new_session(session_index + 1); + let (next_validators, next_identities_changed) + = if let Some(validators) = maybe_next_validators + { + // NOTE: as per the documentation on `OnSessionEnding`, we consider + // the validator set as having changed even if the validators are the + // same as before, as underlying economic conditions may have changed. + (validators, true) + } else { + (>::get(), false) + }; + + // Queue next session keys. + let (queued_amalgamated, next_changed) = { + // until we are certain there has been a change, iterate the prior + // validators along with the current and check for changes + let mut changed = next_identities_changed; + + let mut now_session_keys = session_keys.iter(); + let mut check_next_changed = |keys: &T::Keys| { + if changed { return } + // since a new validator set always leads to `changed` starting + // as true, we can ensure that `now_session_keys` and `next_validators` + // have the same length. this function is called once per iteration. + if let Some(&(_, ref old_keys)) = now_session_keys.next() { + if old_keys != keys { + changed = true; + return + } + } + }; + let queued_amalgamated = next_validators.into_iter() + .map(|a| { + let k = Self::load_keys(&a).unwrap_or_default(); + check_next_changed(&k); + (a, k) + }) + .collect::>(); + + (queued_amalgamated, changed) + }; + + >::put(queued_amalgamated.clone()); + QueuedChanged::put(next_changed); + + // Record that this happened. + Self::deposit_event(Event::NewSession(session_index)); + + // Tell everyone about the new session keys. + T::SessionHandler::on_new_session::( + changed, + &session_keys, + &queued_amalgamated, + ); + } + + /// Disable the validator of index `i`. + /// + /// Returns `true` if this causes a `DisabledValidatorsThreshold` of validators + /// to be already disabled. + pub fn disable_index(i: usize) -> bool { + let (fire_event, threshold_reached) = DisabledValidators::mutate(|disabled| { + let i = i as u32; + if let Err(index) = disabled.binary_search(&i) { + let count = >::decode_len().unwrap_or(0) as u32; + let threshold = T::DisabledValidatorsThreshold::get() * count; + disabled.insert(index, i); + (true, disabled.len() as u32 > threshold) + } else { + (false, false) + } + }); + + if fire_event { + T::SessionHandler::on_disabled(i); + } + + threshold_reached + } + + /// Disable the validator identified by `c`. (If using with the staking module, + /// this would be their *stash* account.) + /// + /// Returns `Ok(true)` if more than `DisabledValidatorsThreshold` validators in current + /// session is already disabled. + /// If used with the staking module it allows to force a new era in such case. + pub fn disable(c: &T::ValidatorId) -> sp_std::result::Result { + Self::validators().iter().position(|i| i == c).map(Self::disable_index).ok_or(()) + } + + /// Perform the set_key operation, checking for duplicates. Does not set `Changed`. + /// + /// This ensures that the reference counter in system is incremented appropriately and as such + /// must accept an account ID, rather than a validator ID. + fn do_set_keys(account: &T::AccountId, keys: T::Keys) -> dispatch::DispatchResult { + let who = T::ValidatorIdOf::convert(account.clone()) + .ok_or(Error::::NoAssociatedValidatorId)?; + + let old_keys = Self::inner_set_keys(&who, keys)?; + if old_keys.is_none() { + system::Module::::inc_ref(&account); + } + + Ok(()) + } + + /// Perform the set_key operation, checking for duplicates. Does not set `Changed`. + /// + /// The old keys for this validator are returned, or `None` if there were none. + /// + /// This does not ensure that the reference counter in system is incremented appropriately, it + /// must be done by the caller or the keys will be leaked in storage. + fn inner_set_keys(who: &T::ValidatorId, keys: T::Keys) -> Result, DispatchError> { + let old_keys = Self::load_keys(who); + + for id in T::Keys::key_ids() { + let key = keys.get_raw(*id); + + // ensure keys are without duplication. + ensure!( + Self::key_owner(*id, key).map_or(true, |owner| &owner == who), + Error::::DuplicatedKey, + ); + + if let Some(old) = old_keys.as_ref().map(|k| k.get_raw(*id)) { + if key == old { + continue; + } + + Self::clear_key_owner(*id, old); + } + + Self::put_key_owner(*id, key, who); + } + + Self::put_keys(who, &keys); + Ok(old_keys) + } + + fn do_purge_keys(account: &T::AccountId) -> DispatchResult { + let who = T::ValidatorIdOf::convert(account.clone()) + .ok_or(Error::::NoAssociatedValidatorId)?; + + let old_keys = Self::take_keys(&who).ok_or(Error::::NoKeys)?; + for id in T::Keys::key_ids() { + let key_data = old_keys.get_raw(*id); + Self::clear_key_owner(*id, key_data); + } + system::Module::::dec_ref(&account); + + Ok(()) + } + + fn load_keys(v: &T::ValidatorId) -> Option { + >::get(DEDUP_KEY_PREFIX, v) + } + + fn take_keys(v: &T::ValidatorId) -> Option { + >::take(DEDUP_KEY_PREFIX, v) + } + + fn put_keys(v: &T::ValidatorId, keys: &T::Keys) { + >::insert(DEDUP_KEY_PREFIX, v, keys); + } + + fn key_owner(id: KeyTypeId, key_data: &[u8]) -> Option { + >::get(DEDUP_KEY_PREFIX, (id, key_data)) + } + + fn put_key_owner(id: KeyTypeId, key_data: &[u8], v: &T::ValidatorId) { + >::insert(DEDUP_KEY_PREFIX, (id, key_data), v) + } + + fn clear_key_owner(id: KeyTypeId, key_data: &[u8]) { + >::remove(DEDUP_KEY_PREFIX, (id, key_data)); + } +} + +/// Wraps the author-scraping logic for consensus engines that can recover +/// the canonical index of an author. This then transforms it into the +/// registering account-ID of that session key index. +pub struct FindAccountFromAuthorIndex(sp_std::marker::PhantomData<(T, Inner)>); + +impl> FindAuthor + for FindAccountFromAuthorIndex +{ + fn find_author<'a, I>(digests: I) -> Option + where I: 'a + IntoIterator + { + let i = Inner::find_author(digests)?; + + let validators = >::validators(); + validators.get(i as usize).map(|k| k.clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::assert_ok; + use sp_core::crypto::key_types::DUMMY; + use sp_runtime::{traits::OnInitialize, testing::UintAuthorityId}; + use mock::{ + NEXT_VALIDATORS, SESSION_CHANGED, TEST_SESSION_CHANGED, authorities, force_new_session, + set_next_validators, set_session_length, session_changed, Test, Origin, System, Session, + reset_before_session_end_called, before_session_end_called, + }; + + fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + GenesisConfig:: { + keys: NEXT_VALIDATORS.with(|l| + l.borrow().iter().cloned().map(|i| (i, i, UintAuthorityId(i).into())).collect() + ), + }.assimilate_storage(&mut t).unwrap(); + sp_io::TestExternalities::new(t) + } + + fn initialize_block(block: u64) { + SESSION_CHANGED.with(|l| *l.borrow_mut() = false); + System::set_block_number(block); + Session::on_initialize(block); + } + + #[test] + fn simple_setup_should_work() { + new_test_ext().execute_with(|| { + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(3)]); + assert_eq!(Session::validators(), vec![1, 2, 3]); + }); + } + + #[test] + fn put_get_keys() { + new_test_ext().execute_with(|| { + Session::put_keys(&10, &UintAuthorityId(10).into()); + assert_eq!(Session::load_keys(&10), Some(UintAuthorityId(10).into())); + }) + } + + #[test] + fn keys_cleared_on_kill() { + let mut ext = new_test_ext(); + ext.execute_with(|| { + assert_eq!(Session::validators(), vec![1, 2, 3]); + assert_eq!(Session::load_keys(&1), Some(UintAuthorityId(1).into())); + + let id = DUMMY; + assert_eq!(Session::key_owner(id, UintAuthorityId(1).get_raw(id)), Some(1)); + + assert!(!System::allow_death(&1)); + assert_ok!(Session::purge_keys(Origin::signed(1))); + assert!(System::allow_death(&1)); + + assert_eq!(Session::load_keys(&1), None); + assert_eq!(Session::key_owner(id, UintAuthorityId(1).get_raw(id)), None); + }) + } + + #[test] + fn authorities_should_track_validators() { + reset_before_session_end_called(); + + new_test_ext().execute_with(|| { + set_next_validators(vec![1, 2]); + force_new_session(); + initialize_block(1); + assert_eq!(Session::queued_keys(), vec![ + (1, UintAuthorityId(1).into()), + (2, UintAuthorityId(2).into()), + ]); + assert_eq!(Session::validators(), vec![1, 2, 3]); + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(3)]); + assert!(before_session_end_called()); + reset_before_session_end_called(); + + force_new_session(); + initialize_block(2); + assert_eq!(Session::queued_keys(), vec![ + (1, UintAuthorityId(1).into()), + (2, UintAuthorityId(2).into()), + ]); + assert_eq!(Session::validators(), vec![1, 2]); + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(2)]); + assert!(before_session_end_called()); + reset_before_session_end_called(); + + set_next_validators(vec![1, 2, 4]); + assert_ok!(Session::set_keys(Origin::signed(4), UintAuthorityId(4).into(), vec![])); + force_new_session(); + initialize_block(3); + assert_eq!(Session::queued_keys(), vec![ + (1, UintAuthorityId(1).into()), + (2, UintAuthorityId(2).into()), + (4, UintAuthorityId(4).into()), + ]); + assert_eq!(Session::validators(), vec![1, 2]); + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(2)]); + assert!(before_session_end_called()); + + force_new_session(); + initialize_block(4); + assert_eq!(Session::queued_keys(), vec![ + (1, UintAuthorityId(1).into()), + (2, UintAuthorityId(2).into()), + (4, UintAuthorityId(4).into()), + ]); + assert_eq!(Session::validators(), vec![1, 2, 4]); + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(4)]); + }); + } + + #[test] + fn should_work_with_early_exit() { + new_test_ext().execute_with(|| { + set_session_length(10); + + initialize_block(1); + assert_eq!(Session::current_index(), 0); + + initialize_block(2); + assert_eq!(Session::current_index(), 0); + + force_new_session(); + initialize_block(3); + assert_eq!(Session::current_index(), 1); + + initialize_block(9); + assert_eq!(Session::current_index(), 1); + + initialize_block(10); + assert_eq!(Session::current_index(), 2); + }); + } + + #[test] + fn session_change_should_work() { + new_test_ext().execute_with(|| { + // Block 1: No change + initialize_block(1); + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(3)]); + + // Block 2: Session rollover, but no change. + initialize_block(2); + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(3)]); + + // Block 3: Set new key for validator 2; no visible change. + initialize_block(3); + assert_ok!(Session::set_keys(Origin::signed(2), UintAuthorityId(5).into(), vec![])); + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(3)]); + + // Block 4: Session rollover; no visible change. + initialize_block(4); + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(3)]); + + // Block 5: No change. + initialize_block(5); + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(3)]); + + // Block 6: Session rollover; authority 2 changes. + initialize_block(6); + assert_eq!(authorities(), vec![UintAuthorityId(1), UintAuthorityId(5), UintAuthorityId(3)]); + }); + } + + #[test] + fn duplicates_are_not_allowed() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + Session::on_initialize(1); + assert!(Session::set_keys(Origin::signed(4), UintAuthorityId(1).into(), vec![]).is_err()); + assert!(Session::set_keys(Origin::signed(1), UintAuthorityId(10).into(), vec![]).is_ok()); + + // is fine now that 1 has migrated off. + assert!(Session::set_keys(Origin::signed(4), UintAuthorityId(1).into(), vec![]).is_ok()); + }); + } + + #[test] + fn session_changed_flag_works() { + reset_before_session_end_called(); + + new_test_ext().execute_with(|| { + TEST_SESSION_CHANGED.with(|l| *l.borrow_mut() = true); + + force_new_session(); + initialize_block(1); + assert!(!session_changed()); + assert!(before_session_end_called()); + reset_before_session_end_called(); + + force_new_session(); + initialize_block(2); + assert!(!session_changed()); + assert!(before_session_end_called()); + reset_before_session_end_called(); + + Session::disable_index(0); + force_new_session(); + initialize_block(3); + assert!(!session_changed()); + assert!(before_session_end_called()); + reset_before_session_end_called(); + + force_new_session(); + initialize_block(4); + assert!(session_changed()); + assert!(before_session_end_called()); + reset_before_session_end_called(); + + force_new_session(); + initialize_block(5); + assert!(!session_changed()); + assert!(before_session_end_called()); + reset_before_session_end_called(); + + assert_ok!(Session::set_keys(Origin::signed(2), UintAuthorityId(5).into(), vec![])); + force_new_session(); + initialize_block(6); + assert!(!session_changed()); + assert!(before_session_end_called()); + reset_before_session_end_called(); + + // changing the keys of a validator leads to change. + assert_ok!(Session::set_keys(Origin::signed(69), UintAuthorityId(69).into(), vec![])); + force_new_session(); + initialize_block(7); + assert!(session_changed()); + assert!(before_session_end_called()); + reset_before_session_end_called(); + + // while changing the keys of a non-validator does not. + force_new_session(); + initialize_block(7); + assert!(!session_changed()); + assert!(before_session_end_called()); + reset_before_session_end_called(); + }); + } + + #[test] + fn periodic_session_works() { + struct Period; + struct Offset; + + impl Get for Period { + fn get() -> u64 { 10 } + } + + impl Get for Offset { + fn get() -> u64 { 3 } + } + + + type P = PeriodicSessions; + + for i in 0..3 { + assert!(!P::should_end_session(i)); + } + + assert!(P::should_end_session(3)); + + for i in (1..10).map(|i| 3 + i) { + assert!(!P::should_end_session(i)); + } + + assert!(P::should_end_session(13)); + } + + #[test] + fn session_keys_generate_output_works_as_set_keys_input() { + new_test_ext().execute_with(|| { + let new_keys = mock::MockSessionKeys::generate(None); + assert_ok!( + Session::set_keys( + Origin::signed(2), + ::Keys::decode(&mut &new_keys[..]).expect("Decode keys"), + vec![], + ) + ); + }); + } + + #[test] + fn return_true_if_more_than_third_is_disabled() { + new_test_ext().execute_with(|| { + set_next_validators(vec![1, 2, 3, 4, 5, 6, 7]); + force_new_session(); + initialize_block(1); + // apply the new validator set + force_new_session(); + initialize_block(2); + + assert_eq!(Session::disable_index(0), false); + assert_eq!(Session::disable_index(1), false); + assert_eq!(Session::disable_index(2), true); + assert_eq!(Session::disable_index(3), true); + }); + } +} diff --git a/frame/session/src/mock.rs b/frame/session/src/mock.rs new file mode 100644 index 000000000..9d64285b9 --- /dev/null +++ b/frame/session/src/mock.rs @@ -0,0 +1,217 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Mock helpers for Session. + +use super::*; +use std::cell::RefCell; +use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; +use sp_core::{crypto::key_types::DUMMY, H256}; +use sp_runtime::{ + Perbill, impl_opaque_keys, + traits::{BlakeTwo256, IdentityLookup, ConvertInto}, + testing::{Header, UintAuthorityId}, +}; +use sp_staking::SessionIndex; + +impl_opaque_keys! { + pub struct MockSessionKeys { + pub dummy: UintAuthorityId, + } +} + +impl From for MockSessionKeys { + fn from(dummy: UintAuthorityId) -> Self { + Self { dummy } + } +} + +impl_outer_origin! { + pub enum Origin for Test where system = frame_system {} +} + +thread_local! { + pub static VALIDATORS: RefCell> = RefCell::new(vec![1, 2, 3]); + pub static NEXT_VALIDATORS: RefCell> = RefCell::new(vec![1, 2, 3]); + pub static AUTHORITIES: RefCell> = + RefCell::new(vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(3)]); + pub static FORCE_SESSION_END: RefCell = RefCell::new(false); + pub static SESSION_LENGTH: RefCell = RefCell::new(2); + pub static SESSION_CHANGED: RefCell = RefCell::new(false); + pub static TEST_SESSION_CHANGED: RefCell = RefCell::new(false); + pub static DISABLED: RefCell = RefCell::new(false); + // Stores if `on_before_session_end` was called + pub static BEFORE_SESSION_END_CALLED: RefCell = RefCell::new(false); +} + +pub struct TestShouldEndSession; +impl ShouldEndSession for TestShouldEndSession { + fn should_end_session(now: u64) -> bool { + let l = SESSION_LENGTH.with(|l| *l.borrow()); + now % l == 0 || FORCE_SESSION_END.with(|l| { let r = *l.borrow(); *l.borrow_mut() = false; r }) + } +} + +pub struct TestSessionHandler; +impl SessionHandler for TestSessionHandler { + const KEY_TYPE_IDS: &'static [sp_runtime::KeyTypeId] = &[UintAuthorityId::ID]; + fn on_genesis_session(_validators: &[(u64, T)]) {} + fn on_new_session( + changed: bool, + validators: &[(u64, T)], + _queued_validators: &[(u64, T)], + ) { + SESSION_CHANGED.with(|l| *l.borrow_mut() = changed); + AUTHORITIES.with(|l| + *l.borrow_mut() = validators.iter() + .map(|(_, id)| id.get::(DUMMY).unwrap_or_default()) + .collect() + ); + } + fn on_disabled(_validator_index: usize) { + DISABLED.with(|l| *l.borrow_mut() = true) + } + fn on_before_session_ending() { + BEFORE_SESSION_END_CALLED.with(|b| *b.borrow_mut() = true); + } +} + +pub struct TestSessionManager; +impl SessionManager for TestSessionManager { + fn end_session(_: SessionIndex) {} + fn start_session(_: SessionIndex) {} + fn new_session(_: SessionIndex) -> Option> { + if !TEST_SESSION_CHANGED.with(|l| *l.borrow()) { + VALIDATORS.with(|v| { + let mut v = v.borrow_mut(); + *v = NEXT_VALIDATORS.with(|l| l.borrow().clone()); + Some(v.clone()) + }) + } else if DISABLED.with(|l| std::mem::replace(&mut *l.borrow_mut(), false)) { + // If there was a disabled validator, underlying conditions have changed + // so we return `Some`. + Some(VALIDATORS.with(|v| v.borrow().clone())) + } else { + None + } + } +} + +#[cfg(feature = "historical")] +impl crate::historical::SessionManager for TestSessionManager { + fn end_session(_: SessionIndex) {} + fn start_session(_: SessionIndex) {} + fn new_session(new_index: SessionIndex) + -> Option> + { + >::new_session(new_index) + .map(|vals| vals.into_iter().map(|val| (val, val)).collect()) + } +} + +pub fn authorities() -> Vec { + AUTHORITIES.with(|l| l.borrow().to_vec()) +} + +pub fn force_new_session() { + FORCE_SESSION_END.with(|l| *l.borrow_mut() = true ) +} + +pub fn set_session_length(x: u64) { + SESSION_LENGTH.with(|l| *l.borrow_mut() = x ) +} + +pub fn session_changed() -> bool { + SESSION_CHANGED.with(|l| *l.borrow()) +} + +pub fn set_next_validators(next: Vec) { + NEXT_VALIDATORS.with(|v| *v.borrow_mut() = next); +} + +pub fn before_session_end_called() -> bool { + BEFORE_SESSION_END_CALLED.with(|b| *b.borrow()) +} + +pub fn reset_before_session_end_called() { + BEFORE_SESSION_END_CALLED.with(|b| *b.borrow_mut() = false); +} + +#[derive(Clone, Eq, PartialEq)] +pub struct Test; + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const MinimumPeriod: u64 = 5; + pub const AvailableBlockRatio: Perbill = Perbill::one(); +} + +impl frame_system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type AvailableBlockRatio = AvailableBlockRatio; + type MaximumBlockLength = MaximumBlockLength; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); +} + +impl pallet_timestamp::Trait for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; +} + +parameter_types! { + pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(33); +} + +impl Trait for Test { + type ShouldEndSession = TestShouldEndSession; + #[cfg(feature = "historical")] + type SessionManager = crate::historical::NoteHistoricalRoot; + #[cfg(not(feature = "historical"))] + type SessionManager = TestSessionManager; + type SessionHandler = TestSessionHandler; + type ValidatorId = u64; + type ValidatorIdOf = ConvertInto; + type Keys = MockSessionKeys; + type Event = (); + type DisabledValidatorsThreshold = DisabledValidatorsThreshold; +} + +#[cfg(feature = "historical")] +impl crate::historical::Trait for Test { + type FullIdentification = u64; + type FullIdentificationOf = sp_runtime::traits::ConvertInto; +} + +pub type System = frame_system::Module; +pub type Session = Module; diff --git a/frame/staking/Cargo.toml b/frame/staking/Cargo.toml index d54477472..ee099ad30 100644 --- a/frame/staking/Cargo.toml +++ b/frame/staking/Cargo.toml @@ -18,12 +18,14 @@ frame-support = { default-features = false, git = "https://github.com/darwinia-n frame-system = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } pallet-authorship = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -pallet-session = { default-features = false, features = ["historical"], git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +pallet-session = { default-features = false, features = ["historical"], path = "../session" } sp-core = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } sp-io ={ default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } sp-runtime = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } -sp-staking = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +# TODO https://github.com/darwinia-network/darwinia/issues/347 +sp-staking = { default-features = false, path = "../../primitives/staking" } sp-std = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } # darwinia diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index 954fbc6bf..8126384fc 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -88,6 +88,11 @@ //! The **reward and slashing** procedure is the core of the Staking module, attempting to _embrace //! valid behavior_ while _punishing any misbehavior or lack of availability_. //! +//! Reward must be claimed by stakers for each era before it gets too old by $HISTORY_DEPTH using +//! `payout_nominator` and `payout_validator` calls. +//! Only the [`T::MaxNominatorRewardedPerValidator`] biggest stakers can claim their reward. This +//! limit the i/o cost to compute nominators payout. +//! //! Slashing can occur at any point in time, once misbehavior is reported. Once slashing is //! determined, a value is deducted from the balance of the validator and all the nominators who //! voted for this validator (values are deducted from the _stash_ account of the slashed entity). @@ -106,6 +111,11 @@ //! //! An account can step back via the [`chill`](enum.Call.html#variant.chill) call. //! +//! ### Session managing +//! +//! The module implement the trait `SessionManager`. Which is the only API to query new validator +//! set and allowing these validator set to be rewarded once their era is ended. +//! //! ## Interface //! //! ### Dispatchable Functions @@ -143,14 +153,6 @@ //! //! ## Implementation Details //! -//! ### Slot Stake -//! -//! The term [`SlotStake`](./struct.Module.html#method.slot_stake) will be used throughout this -//! section. It refers to a value calculated at the end of each era, containing the _minimum value -//! at stake among all validators._ Note that a validator's value at stake might be a combination -//! of the validator's own stake and the votes it received. See [`Exposure`](./struct.Exposure.html) -//! for more details. -//! //! ### Reward Calculation //! //! Validators and nominators are rewarded at the end of each era. The total reward of an era is @@ -220,6 +222,7 @@ //! ## GenesisConfig //! //! The Staking module depends on the [`GenesisConfig`](./struct.GenesisConfig.html). +//! The `GenesisConfig` is optional and allow to set some initial stakers. //! //! ## Related Modules //! @@ -231,25 +234,24 @@ #![feature(drain_filter)] #![recursion_limit = "128"] -#[cfg(test)] -mod darwinia_tests; #[cfg(test)] mod mock; -#[cfg(test)] -mod substrate_tests; + +// #[cfg(test)] +// mod darwinia_tests; +// #[cfg(test)] +// mod substrate_tests; mod inflation; mod slashing; mod types { - use sp_std::prelude::*; - use crate::*; /// Counter for the number of eras that have passed. pub type EraIndex = u32; /// Counter for the number of "reward" points earned by a given validator. - pub type Points = u32; + pub type RewardPoint = u32; /// Balance of an account. pub type Balance = u128; @@ -272,8 +274,6 @@ mod types { pub type MomentT = as Time>::Moment; - pub type Rewards = (RingBalance, Vec, RingBalance>>); - /// A timestamp: milliseconds since the unix epoch. /// `u64` is enough to represent a duration of half a billion years, when the /// time scale is milliseconds. @@ -295,18 +295,16 @@ use frame_support::{ }; use frame_system::{self as system, ensure_root, ensure_signed}; use sp_runtime::{ - traits::{ - AtLeast32Bit, CheckedSub, Convert, EnsureOrigin, One, SaturatedConversion, Saturating, StaticLookup, Zero, - }, + traits::{AtLeast32Bit, CheckedSub, Convert, EnsureOrigin, SaturatedConversion, Saturating, StaticLookup, Zero}, DispatchResult, PerThing, Perbill, Perquintill, RuntimeDebug, }; #[cfg(feature = "std")] use sp_runtime::{Deserialize, Serialize}; use sp_staking::{ - offence::{Offence, OffenceDetails, OnOffenceHandler, ReportOffence}, + offence::{Offence, OffenceDetails, OffenceError, OnOffenceHandler, ReportOffence}, SessionIndex, }; -use sp_std::{borrow::ToOwned, convert::TryInto, marker::PhantomData, prelude::*}; +use sp_std::{collections::btree_map::BTreeMap, convert::TryInto, marker::PhantomData, prelude::*}; use darwinia_phragmen::{PhragmenStakedAssignment, Power, Votes}; use darwinia_support::{ @@ -322,28 +320,7 @@ const MAX_NOMINATIONS: usize = 16; const MAX_UNLOCKING_CHUNKS: usize = 32; const STAKING_ID: LockIdentifier = *b"staking "; -/// Reward points of an era. Used to split era total payout between validators. -#[derive(Encode, Decode, Default)] -pub struct EraPoints { - /// Total number of points. Equals the sum of reward points for each validator. - total: Points, - /// The reward points earned by a given validator. The index of this vec corresponds to the - /// index into the current validator set. - individual: Vec, -} - -impl EraPoints { - /// Add the reward to the validator at the given index. Index must be valid - /// (i.e. `index < current_elected.len()`). - fn add_points_to_index(&mut self, index: u32, points: u32) { - if let Some(new_total) = self.total.checked_add(points) { - self.total = new_total; - self.individual - .resize((index as usize + 1).max(self.individual.len()), 0); - self.individual[index as usize] += points; // Addition is less than total - } - } -} +// --- enum --- /// Indicates the initial status of the staker. #[derive(RuntimeDebug)] @@ -374,20 +351,23 @@ impl Default for RewardDestination { } } -/// Preference of what happens regarding validation. -#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] -pub struct ValidatorPrefs { - /// Reward that validator takes up-front; only the rest is split between themselves and - /// nominators. - #[codec(compact)] - pub commission: Perbill, +/// Mode of era-forcing. +#[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum Forcing { + /// Not forcing anything - just let whatever happen. + NotForcing, + /// Force a new era, then reset to `NotForcing` as soon as it is done. + ForceNew, + /// Avoid a new era indefinitely. + ForceNone, + /// Force a new era at the end of all sessions indefinitely. + ForceAlways, } -impl Default for ValidatorPrefs { +impl Default for Forcing { fn default() -> Self { - ValidatorPrefs { - commission: Default::default(), - } + Forcing::NotForcing } } @@ -413,15 +393,46 @@ where } } -/// The *Ring* under deposit. +// --- struct --- + +/// Information regarding the active era (era in used in session). +#[derive(Debug, Encode, Decode)] +pub struct ActiveEraInfo { + /// Index of era. + index: EraIndex, + /// Moment of start + /// + /// Start can be none if start hasn't been set for the era yet, + /// Start is set on the first on_finalize of the era to guarantee usage of `Time`. + start: Option, +} + +/// Reward points of an era. Used to split era total payout between validators. +/// +/// This points will be used to reward validators and their respective nominators. +#[derive(PartialEq, Encode, Decode, Default, Debug)] +pub struct EraRewardPoints { + /// Total number of points. Equals the sum of reward points for each validator. + total: RewardPoint, + /// The reward points earned by a given validator. + individual: BTreeMap, +} + +/// Preference of what happens regarding validation. #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] -pub struct TimeDepositItem { - #[codec(compact)] - pub value: RingBalance, - #[codec(compact)] - pub start_time: Moment, +pub struct ValidatorPrefs { + /// Reward that validator takes up-front; only the rest is split between themselves and + /// nominators. #[codec(compact)] - pub expire_time: Moment, + pub commission: Perbill, +} + +impl Default for ValidatorPrefs { + fn default() -> Self { + ValidatorPrefs { + commission: Default::default(), + } + } } /// The ledger of a (bonded) stash. @@ -455,6 +466,9 @@ where pub ring_staking_lock: StakingLock, // TODO doc pub kton_staking_lock: StakingLock, + + /// The latest and highest era which the staker has claimed reward for. + pub last_reward: Option, } impl @@ -588,34 +602,30 @@ where } } +/// The *Ring* under deposit. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] +pub struct TimeDepositItem { + #[codec(compact)] + pub value: RingBalance, + #[codec(compact)] + pub start_time: Moment, + #[codec(compact)] + pub expire_time: Moment, +} + /// A record of the nominations made by a specific account. #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] pub struct Nominations { /// The targets of nomination. pub targets: Vec, /// The era the nominations were submitted. + /// + /// Except for initial nominations which are considered submitted at era 0. pub submitted_in: EraIndex, /// Whether the nominations have been suppressed. pub suppressed: bool, } -/// The amount of exposure (to slashing) than an individual nominator has. -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug)] -pub struct IndividualExposure -where - RingBalance: HasCompact, - KtonBalance: HasCompact, -{ - /// The stash account of the nominator in question. - who: AccountId, - /// Amount of funds exposed. - #[codec(compact)] - ring_balance: RingBalance, - #[codec(compact)] - kton_balance: KtonBalance, - power: Power, -} - /// A snapshot of the stake backing a single validator in the system. #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, Default, RuntimeDebug)] pub struct Exposure @@ -635,6 +645,23 @@ where pub others: Vec>, } +/// The amount of exposure (to slashing) than an individual nominator has. +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug)] +pub struct IndividualExposure +where + RingBalance: HasCompact, + KtonBalance: HasCompact, +{ + /// The stash account of the nominator in question. + who: AccountId, + /// Amount of funds exposed. + #[codec(compact)] + ring_balance: RingBalance, + #[codec(compact)] + kton_balance: KtonBalance, + power: Power, +} + /// A pending slash record. The value of the slash has been computed but not applied yet, /// rather deferred for several eras. #[derive(Encode, Decode, Default, RuntimeDebug)] @@ -651,24 +678,7 @@ pub struct UnappliedSlash { payout: slashing::RK, } -// FIXME: RingBalance: HasCompact -// TODO: doc -#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] -pub struct ValidatorReward { - who: AccountId, - #[codec(compact)] - amount: RingBalance, - nominators_reward: Vec>, -} - -// FIXME: RingBalance: HasCompact -// TODO: doc -#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] -pub struct NominatorReward { - who: AccountId, - #[codec(compact)] - amount: RingBalance, -} +// --- trait --- /// Means for interacting with a specialized version of the `session` trait. /// @@ -712,6 +722,9 @@ where pub trait Trait: frame_system::Trait { /// Time used for computing era duration. + /// + /// It is guaranteed to start being called from the first `on_finalize`. Thus value at genesis + /// is not used. type Time: Time; /// The overarching event type. @@ -736,6 +749,12 @@ pub trait Trait: frame_system::Trait { /// Interface for interacting with a session module. type SessionInterface: self::SessionInterface; + /// The maximum number of nominator rewarded for each validator. + /// + /// For each validator only the `$MaxNominatorRewardedPerValidator` biggest stakers can claim + /// their reward. This used to limit the i/o cost for the nominator payout. + type MaxNominatorRewardedPerValidator: Get; + /// The *RING* currency. type RingCurrency: LockableCurrency; /// Tokens have been minted and are unused for validator-reward. @@ -759,33 +778,24 @@ pub trait Trait: frame_system::Trait { type TotalPower: Get; } -/// Mode of era-forcing. -#[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum Forcing { - /// Not forcing anything - just let whatever happen. - NotForcing, - /// Force a new era, then reset to `NotForcing` as soon as it is done. - ForceNew, - /// Avoid a new era indefinitely. - ForceNone, - /// Force a new era at the end of all sessions indefinitely. - ForceAlways, -} - -impl Default for Forcing { - fn default() -> Self { - Forcing::NotForcing - } -} - decl_storage! { trait Store for Module as Staking { + /// Number of era to keep in history. + /// + /// Information is kept for eras in `[current_era - history_depth; current_era] + /// + /// Must be more than the number of era delayed by session otherwise. + /// i.e. active era must always be in history. + /// i.e. `active_era > current_era - history_depth` must be guaranteed. + HistoryDepth get(fn history_depth) config(): u32 = 336; + /// The ideal number of staking participants. pub ValidatorCount get(fn validator_count) config(): u32; /// Minimum number of staking participants before emergency conditions are imposed. - pub MinimumValidatorCount get(fn minimum_validator_count) config(): u32 = DEFAULT_MINIMUM_VALIDATOR_COUNT; + pub MinimumValidatorCount + get(fn minimum_validator_count) config() + : u32 = DEFAULT_MINIMUM_VALIDATOR_COUNT; /// Any validators that may never be slashed or forcibly kicked. It's a Vec since they're /// easy to initialize and the performance hit is minimal (we expect no more than four @@ -802,46 +812,87 @@ decl_storage! { pub Payee get(fn payee): map hasher(blake2_256) T::AccountId => RewardDestination; /// The map from (wannabe) validator stash key to the preferences of that validator. - pub Validators get(fn validators): linked_map hasher(blake2_256) T::AccountId => ValidatorPrefs; + pub Validators + get(fn validators) + : linked_map hasher(blake2_256) T::AccountId => ValidatorPrefs; /// The map from nominator stash key to the set of stash keys of all validators to nominate. - /// - /// NOTE: is private so that we can ensure upgraded before all typical accesses. - /// Direct storage APIs can still bypass this protection. - Nominators get(fn nominators): linked_map hasher(blake2_256) T::AccountId => Option>; - - /// Nominators for a particular account that is in action right now. You can't iterate - /// through validators here, but you can find them in the Session module. - /// - /// This is keyed by the stash account. - pub Stakers get(fn stakers): map hasher(blake2_256) T::AccountId => Exposure, KtonBalance>; - - /// The currently elected validator set keyed by stash account ID. - pub CurrentElected get(fn current_elected): Vec; + pub Nominators + get(fn nominators) + : linked_map hasher(blake2_256) T::AccountId => Option>; /// The current era index. - pub CurrentEra get(fn current_era) config(): EraIndex; - - /// The start of the current era. - pub CurrentEraStart get(fn current_era_start): MomentT; + /// + /// This is the latest planned era, depending on how session module queues the validator + /// set, it might be active or not. + pub CurrentEra get(fn current_era): Option; - /// The session index at which the current era started. - pub CurrentEraStartSessionIndex get(fn current_era_start_session_index): SessionIndex; + /// The active era information, it holds index and start. + /// + /// The active era is the era currently rewarded. + /// Validator set of this era must be equal to `SessionInterface::validators`. + pub ActiveEra get(fn active_era): Option>>; - /// Rewards for the current era. Using indices of current elected set. - CurrentEraPointsEarned get(fn current_era_reward): EraPoints; + /// The session index at which the era start for the last `HISTORY_DEPTH` eras + pub ErasStartSessionIndex + get(fn eras_start_session_index) + : map hasher(blake2_256) EraIndex => Option; - /// The amount of balance actively at stake for each validator slot, currently. + /// Exposure of validator at era. /// - /// This is used to derive rewards and punishments. - pub SlotStake get(fn slot_stake) build(|config: &GenesisConfig| { - config - .stakers - .iter() - .map(|&(_, _, r, _)| >::currency_to_power::<_>(r, >::ring_pool())) - .min() - .unwrap_or_default() - }): Power; + /// This is keyed first by the era index to allow bulk deletion and then the stash account. + /// + /// Is it removed after `HISTORY_DEPTH` eras. + /// If stakers hasn't been set or has been removed then empty exposure is returned. + pub ErasStakers + get(fn eras_stakers) + : double_map hasher(twox_64_concat) EraIndex, hasher(twox_64_concat) T::AccountId + => Exposure, KtonBalance>; + + /// Clipped Exposure of validator at era. + /// + /// This is similar to [`ErasStakers`] but number of nominators exposed is reduce to the + /// `T::MaxNominatorRewardedPerValidator` biggest stakers. + /// This is used to limit the i/o cost for the nominator payout. + /// + /// This is keyed fist by the era index to allow bulk deletion and then the stash account. + /// + /// Is it removed after `HISTORY_DEPTH` eras. + /// If stakers hasn't been set or has been removed then empty exposure is returned. + pub ErasStakersClipped + get(fn eras_stakers_clipped) + : double_map hasher(twox_64_concat) EraIndex, hasher(twox_64_concat) T::AccountId + => Exposure, KtonBalance>; + + /// Similarly to `ErasStakers` this holds the preferences of validators. + /// + /// This is keyed fist by the era index to allow bulk deletion and then the stash account. + /// + /// Is it removed after `HISTORY_DEPTH` eras. + // If prefs hasn't been set or has been removed then 0 commission is returned. + pub ErasValidatorPrefs + get(fn eras_validator_prefs) + : double_map hasher(twox_64_concat) EraIndex, hasher(twox_64_concat) T::AccountId + => ValidatorPrefs; + + /// The total validator era payout for the last `HISTORY_DEPTH` eras. + /// + /// Eras that haven't finished yet or has been removed doesn't have reward. + pub ErasValidatorReward + get(fn eras_validator_reward) + : map hasher(blake2_256) EraIndex => Option>; + + /// Rewards for the last `HISTORY_DEPTH` eras. + /// If reward hasn't been set or has been removed then 0 reward is returned. + pub ErasRewardPoints + get(fn eras_reward_points) + : map hasher(blake2_256) EraIndex => EraRewardPoints; + + /// The total amount staked for the last `HISTORY_DEPTH` eras. + /// If total hasn't been set or has been removed then 0 stake is returned. + pub ErasTotalStake + get(fn eras_total_stake) + : map hasher(blake2_256) EraIndex => Power; /// True if the next session change will be a new era regardless of index. pub ForceEra get(fn force_era) config(): Forcing; @@ -856,24 +907,35 @@ decl_storage! { pub CanceledSlashPayout get(fn canceled_payout) config(): Power; /// All unapplied slashes that are queued for later. - pub UnappliedSlashes: map hasher(blake2_256) EraIndex => Vec, KtonBalance>>; + pub UnappliedSlashes + : map hasher(blake2_256) EraIndex + => Vec, KtonBalance>>; /// A mapping from still-bonded eras to the first session index of that era. + /// + /// Must contains information for eras for the range: + /// `[active_era - bounding_duration; active_era]` BondedEras: Vec<(EraIndex, SessionIndex)>; /// All slashing events on validators, mapped by era to the highest slash proportion /// and slash value of the era. - ValidatorSlashInEra: double_map hasher(blake2_256) EraIndex, hasher(twox_128) T::AccountId => Option<(Perbill, slashing::RKT)>; + ValidatorSlashInEra + : double_map hasher(blake2_256) EraIndex, hasher(twox_128) T::AccountId + => Option<(Perbill, slashing::RKT)>; /// All slashing events on nominators, mapped by era to the highest slash value of the era. - NominatorSlashInEra: double_map hasher(blake2_256) EraIndex, hasher(twox_128) T::AccountId => Option>; + NominatorSlashInEra + : double_map hasher(blake2_256) EraIndex, hasher(twox_128) T::AccountId + => Option>; /// Slashing spans for stash accounts. SlashingSpans: map hasher(blake2_256) T::AccountId => Option; /// Records information about the maximum slash of a stash within a slashing span, /// as well as how much reward has been paid out. - SpanSlash: map hasher(blake2_256) (T::AccountId, slashing::SpanIndex) => slashing::SpanRecord, KtonBalance>; + SpanSlash + : map hasher(blake2_256) (T::AccountId, slashing::SpanIndex) + => slashing::SpanRecord, KtonBalance>; /// The earliest era for which we have a pending, unapplied slash. EarliestUnappliedSlash: Option; @@ -957,9 +1019,8 @@ decl_event!( /// `amount` om `KtonBalance`, `now` in `BlockNumber` UnbondKton(KtonBalance, BlockNumber), - /// All validators have been rewarded by the first balance; the second is the remainder - /// from the maximum amount of reward; the third is validator and nominators' reward. - Reward(RingBalance, RingBalance, Vec>), + /// The staker has been rewarded by this amount. AccountId is controller account. + Reward(AccountId, RingBalance), /// One validator (and its nominators) has been slashed by the given amount. Slash(AccountId, RingBalance, KtonBalance), @@ -992,6 +1053,10 @@ decl_error! { NoMoreChunks, /// Attempting to target a stash that still has funds. FundedTarget, + /// Invalid era to reward. + InvalidEraToReward, + /// Invalid number of nominations. + InvalidNumberOfNominations, } } @@ -1020,9 +1085,11 @@ decl_module! { } fn on_finalize() { - // Set the start of the first era. - if !>::exists() { - >::put(T::Time::now()); + if let Some(mut active_era) = Self::active_era() { + if active_era.start.is_none() { + active_era.start = Some(T::Time::now()); + >::put(active_era); + } } } @@ -1057,6 +1124,7 @@ decl_module! { let ledger = StakingLedger { stash: stash.clone(), + last_reward: Self::current_era(), ..Default::default() }; let promise_month = promise_month.min(36); @@ -1390,25 +1458,6 @@ decl_module! { >::insert(&controller, ledger); } - // /// Rebond a portion of the stash scheduled to be unlocked. - // /// - // /// # - // /// - Time complexity: O(1). Bounded by `MAX_UNLOCKING_CHUNKS`. - // /// - Storage changes: Can't increase storage, only decrease it. - // /// # - // #[weight = SimpleDispatchInfo::FixedNormal(500_000)] - // fn rebond(origin, #[compact] value: BalanceOf) { - // let controller = ensure_signed(origin)?; - // let ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; - // ensure!( - // ledger.unlocking.len() > 0, - // Error::::NoUnlockChunk, - // ); - // - // let ledger = ledger.rebond(value); - // Self::update_ledger(&controller, &ledger); - // } - /// Declare the desire to validate for the origin controller. /// /// Effects will be felt at the beginning of the next era. @@ -1459,7 +1508,8 @@ decl_module! { .collect::, _>>()?; let nominations = Nominations { targets, - submitted_in: Self::current_era(), + // initial nominations are considered submitted at era 0. See `Nominations` doc + submitted_in: Self::current_era().unwrap_or(0), suppressed: false, }; @@ -1630,6 +1680,74 @@ decl_module! { ::UnappliedSlashes::insert(&era, &unapplied); } + /// Make one nominator's payout for one era. + /// + /// - `who` is the controller account of the nominator to pay out. + /// - `era` may not be lower than one following the most recently paid era. If it is higher, + /// then it indicates an instruction to skip the payout of all previous eras. + /// - `validators` is the list of all validators that `who` had exposure to during `era`. + /// If it is incomplete, then less than the full reward will be paid out. + /// It must not exceed `MAX_NOMINATIONS`. + /// + /// WARNING: once an era is payed for a validator such validator can't claim the payout of + /// previous era. + /// + /// WARNING: Incorrect arguments here can result in loss of payout. Be very careful. + /// + /// # + /// - Number of storage read of `O(validators)`; `validators` is the argument of the call, + /// and is bounded by `MAX_NOMINATIONS`. + /// - Each storage read is `O(N)` size and decode complexity; `N` is the maximum + /// nominations that can be given to a single validator. + /// - Computation complexity: `O(MAX_NOMINATIONS * logN)`; `MAX_NOMINATIONS` is the + /// maximum number of validators that may be nominated by a single nominator, it is + /// bounded only economically (all nominators are required to place a minimum stake). + /// # + #[weight = SimpleDispatchInfo::FixedNormal(500_000)] + fn payout_nominator(origin, era: EraIndex, validators: Vec<(T::AccountId, u32)>) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_payout_nominator(who, era, validators) + } + + /// Make one validator's payout for one era. + /// + /// - `who` is the controller account of the validator to pay out. + /// - `era` may not be lower than one following the most recently paid era. If it is higher, + /// then it indicates an instruction to skip the payout of all previous eras. + /// + /// WARNING: once an era is payed for a validator such validator can't claim the payout of + /// previous era. + /// + /// WARNING: Incorrect arguments here can result in loss of payout. Be very careful. + /// + /// # + /// - Time complexity: O(1). + /// - Contains a limited number of reads and writes. + /// # + #[weight = SimpleDispatchInfo::FixedNormal(500_000)] + fn payout_validator(origin, era: EraIndex) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_payout_validator(who, era) + } + + /// Set history_depth value. + /// + /// Origin must be root. + #[weight = SimpleDispatchInfo::FixedOperational(500_000)] + fn set_history_depth(origin, #[compact] new_history_depth: EraIndex) { + ensure_root(origin)?; + if let Some(current_era) = Self::current_era() { + HistoryDepth::mutate(|history_depth| { + let last_kept = current_era.checked_sub(*history_depth).unwrap_or(0); + let new_last_kept = current_era.checked_sub(new_history_depth).unwrap_or(0); + for era_index in last_kept..new_last_kept { + Self::clear_era_information(era_index); + } + *history_depth = new_history_depth + }) + } + } + /// Remove all data structure concerning a staker/stash once its balance is zero. /// This is essentially equivalent to `withdraw_unbonded` except it can be called by anyone /// and the target `stash` must have no funds left. @@ -1639,8 +1757,8 @@ decl_module! { /// - `stash`: The stash account to reap. Its balance must be zero. fn reap_stash(_origin, stash: T::AccountId) { Self::ensure_storage_upgraded(); - ensure!(T::RingCurrency::total_balance(&stash).is_zero(), Error::::FundedTarget); - ensure!(T::KtonCurrency::total_balance(&stash).is_zero(), Error::::FundedTarget); + ensure!(T::RingCurrency::total_balance(&stash).is_zero(), >::FundedTarget); + ensure!(T::KtonCurrency::total_balance(&stash).is_zero(), >::FundedTarget); Self::kill_stash(&stash)?; T::RingCurrency::remove_lock(STAKING_ID, &stash); @@ -1768,6 +1886,106 @@ impl Module { // MUTABLES (DANGEROUS) + fn do_payout_nominator(who: T::AccountId, era: EraIndex, validators: Vec<(T::AccountId, u32)>) -> DispatchResult { + // validators len must not exceed `MAX_NOMINATIONS` to avoid querying more validator + // exposure than necessary. + ensure!( + validators.len() <= MAX_NOMINATIONS, + >::InvalidNumberOfNominations + ); + + // Note: if era has no reward to be claimed, era may be future. better not to update + // `nominator_ledger.last_reward` in this case. + let era_payout = >::get(&era).ok_or_else(|| >::InvalidEraToReward)?; + + let mut nominator_ledger = >::get(&who).ok_or_else(|| >::NotController)?; + + if nominator_ledger + .last_reward + .map(|last_reward| last_reward >= era) + .unwrap_or(false) + { + return Err(>::InvalidEraToReward.into()); + } + + nominator_ledger.last_reward = Some(era); + >::insert(&who, &nominator_ledger); + + let mut reward = Perbill::zero(); + let era_reward_points = >::get(&era); + + for (validator, nominator_index) in validators.into_iter() { + let commission = Self::eras_validator_prefs(&era, &validator).commission; + let validator_exposure = >::get(&era, &validator); + + if let Some(nominator_exposure) = validator_exposure.others.get(nominator_index as usize) { + if nominator_exposure.who != nominator_ledger.stash { + continue; + } + + let nominator_exposure_part = + Perbill::from_rational_approximation(nominator_exposure.power, validator_exposure.total_power); + let validator_point = era_reward_points + .individual + .get(&validator) + .map(|points| *points) + .unwrap_or_else(|| Zero::zero()); + let validator_point_part = + Perbill::from_rational_approximation(validator_point, era_reward_points.total); + reward = reward.saturating_add( + validator_point_part + .saturating_mul(Perbill::one().saturating_sub(commission)) + .saturating_mul(nominator_exposure_part), + ); + } + } + + if let Some(imbalance) = Self::make_payout(&nominator_ledger.stash, reward * era_payout) { + Self::deposit_event(RawEvent::Reward(who, imbalance.peek())); + } + + Ok(()) + } + + fn do_payout_validator(who: T::AccountId, era: EraIndex) -> DispatchResult { + // Note: if era has no reward to be claimed, era may be future. better not to update + // `ledger.last_reward` in this case. + let era_payout = >::get(&era).ok_or_else(|| >::InvalidEraToReward)?; + + let mut ledger = >::get(&who).ok_or_else(|| >::NotController)?; + if ledger + .last_reward + .map(|last_reward| last_reward >= era) + .unwrap_or(false) + { + return Err(>::InvalidEraToReward.into()); + } + + ledger.last_reward = Some(era); + >::insert(&who, &ledger); + + let era_reward_points = >::get(&era); + let commission = Self::eras_validator_prefs(&era, &ledger.stash).commission; + let exposure = >::get(&era, &ledger.stash); + + let exposure_part = Perbill::from_rational_approximation(exposure.own_power, exposure.total_power); + let validator_point = era_reward_points + .individual + .get(&ledger.stash) + .map(|points| *points) + .unwrap_or_else(|| Zero::zero()); + let validator_point_part = Perbill::from_rational_approximation(validator_point, era_reward_points.total); + let reward = validator_point_part.saturating_mul( + commission.saturating_add(Perbill::one().saturating_sub(commission).saturating_mul(exposure_part)), + ); + + if let Some(imbalance) = Self::make_payout(&ledger.stash, reward * era_payout) { + Self::deposit_event(RawEvent::Reward(who, imbalance.peek())); + } + + Ok(()) + } + /// Update the ledger for a controller. This will also update the stash lock. The lock will /// will lock the entire funds except paying for further transactions. fn update_ledger(controller: &T::AccountId, ledger: &mut StakingLedgerT, staking_balance: StakingBalanceT) { @@ -1821,9 +2039,6 @@ impl Module { } /// Ensures storage is upgraded to most recent necessary state. - /// - /// Right now it's a no-op as all networks that are supported by Substrate Frame Core are - /// running with the latest staking storage scheme. fn ensure_storage_upgraded() {} /// Actually make a payment to a staker. This uses the currency's reward function @@ -1847,126 +2062,82 @@ impl Module { } } - /// Reward a given validator by a specific amount. Add the reward to the validator's, and its - /// nominators' balance, pro-rata based on their exposure, after having removed the validator's - /// pre-payout cut. - fn reward_validator(stash: &T::AccountId, reward: RingBalance) -> (RingPositiveImbalance, Rewards) { - let off_the_table = Self::validators(stash).commission * reward; - let reward = reward.saturating_sub(off_the_table); - let mut imbalance = >::zero(); - let mut nominators_reward = vec![]; - let validator_cut = if reward.is_zero() { - Zero::zero() - } else { - let exposure = Self::stakers(stash); - let total = exposure.total_power.max(One::one()); - - for i in &exposure.others { - let per_u64 = Perbill::from_rational_approximation(i.power, total); - let nominator_reward = per_u64 * reward; - - imbalance.maybe_subsume(Self::make_payout(&i.who, nominator_reward)); - nominators_reward.push(NominatorReward { - who: i.who.to_owned(), - amount: nominator_reward, - }); - } + /// Plan a new session potentially trigger a new era. + fn new_session(session_index: SessionIndex) -> Option> { + if let Some(current_era) = Self::current_era() { + // Initial era has been set. - let per_u64 = Perbill::from_rational_approximation(exposure.own_power, total); - per_u64 * reward - }; - let validator_reward = validator_cut + off_the_table; + let current_era_start_session_index = Self::eras_start_session_index(current_era).unwrap_or_else(|| { + frame_support::print("Error: start_session_index must be set for current_era"); + 0 + }); - imbalance.maybe_subsume(Self::make_payout(stash, validator_reward)); + let era_length = session_index.checked_sub(current_era_start_session_index).unwrap_or(0); // Must never happen. - (imbalance, (validator_reward, nominators_reward)) + match ForceEra::get() { + Forcing::ForceNew => ForceEra::kill(), + Forcing::ForceAlways => (), + Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (), + _ => return None, + } + Self::new_era(session_index) + } else { + // Set initial era + Self::new_era(session_index) + } } - /// Session has just ended. Provide the validator set for the next session if it's an era-end. - fn new_session(session_index: SessionIndex) -> Option> { - let era_length = session_index - .checked_sub(Self::current_era_start_session_index()) - .unwrap_or(0); - match ForceEra::get() { - Forcing::ForceNew => ForceEra::kill(), - Forcing::ForceAlways => (), - Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (), - _ => return None, + /// Start a session potentially starting an era. + fn start_session(start_session: SessionIndex) { + let next_active_era = Self::active_era().map(|e| e.index + 1).unwrap_or(0); + if let Some(next_active_era_start_session_index) = Self::eras_start_session_index(next_active_era) { + if next_active_era_start_session_index == start_session { + Self::start_era(start_session); + } else if next_active_era_start_session_index < start_session { + // This arm should never happen, but better handle it than to stall the + // staking pallet. + frame_support::print("Warning: A session appears to have been skipped."); + Self::start_era(start_session); + } } - - Self::new_era(session_index) } - /// Initialize the first session (and consequently the first era) - fn initial_session() -> Option> { - // note: `CurrentEraStart` is set in `on_finalize` of the first block because now is not - // available yet. - CurrentEraStartSessionIndex::put(0); - BondedEras::mutate(|bonded| bonded.push((0, 0))); - Self::select_validators().1 - } + /// End a session potentially ending an era. + fn end_session(session_index: SessionIndex) { + if let Some(active_era) = Self::active_era() { + let next_active_era_start_session_index = Self::eras_start_session_index(active_era.index + 1) + .unwrap_or_else(|| { + frame_support::print("Error: start_session_index must be set for active_era + 1"); + 0 + }); - /// The era has changed - enact new staking set. - /// - /// NOTE: This always happens immediately before a session change to ensure that new validators - /// get a chance to set their session keys. - fn new_era(start_session_index: SessionIndex) -> Option> { - // Payout - let points = CurrentEraPointsEarned::take(); - let now = T::Time::now(); - let previous_era_start = >::mutate(|v| sp_std::mem::replace(v, now)); - let era_duration = now - previous_era_start; - if !era_duration.is_zero() { - let validators = Self::current_elected(); - let (total_payout, max_payout) = inflation::compute_total_payout::( - era_duration, - now - Self::genesis_time(), - T::Cap::get().saturating_sub(T::RingCurrency::total_issuance()), - PayoutFraction::get(), - ); - let mut total_imbalance = >::zero(); - let mut validators_reward = vec![]; - - for (v, p) in validators.iter().zip(points.individual.into_iter()) { - if p != 0 { - let reward = Perbill::from_rational_approximation(p, points.total) * total_payout; - let (imbalance, (validator_reward, nominators_reward)) = Self::reward_validator(v, reward); - - total_imbalance.subsume(imbalance); - validators_reward.push(ValidatorReward { - who: v.to_owned(), - amount: validator_reward, - nominators_reward, - }); - } + if next_active_era_start_session_index == session_index + 1 { + Self::end_era(active_era, session_index); } - - // assert!(total_imbalance.peek() == total_payout) - let total_payout = total_imbalance.peek(); - let rest = max_payout.saturating_sub(total_payout); - - Self::deposit_event(RawEvent::Reward(total_payout, rest, validators_reward)); - - T::RingReward::on_unbalanced(total_imbalance); - T::RingRewardRemainder::on_unbalanced(T::RingCurrency::issue(rest)); } + } - // Increment current era. - let current_era = CurrentEra::mutate(|s| { - *s += 1; - *s + /// * Increment `active_era.index`, + /// * reset `active_era.start`, + /// * update `BondedEras` and apply slashes. + fn start_era(start_session: SessionIndex) { + let active_era = >::mutate(|active_era| { + let new_index = active_era.as_ref().map(|info| info.index + 1).unwrap_or(0); + *active_era = Some(ActiveEraInfo { + index: new_index, + // Set new active era start in next `on_finalize`. To guarantee usage of `Time` + start: None, + }); + new_index }); - CurrentEraStartSessionIndex::mutate(|v| { - *v = start_session_index; - }); - let bonding_duration_in_era = T::BondingDurationInEra::get(); + let bonding_duration = T::BondingDurationInEra::get(); BondedEras::mutate(|bonded| { - bonded.push((current_era, start_session_index)); + bonded.push((active_era, start_session)); - if current_era > bonding_duration_in_era { - let first_kept = current_era - bonding_duration_in_era; + if active_era > bonding_duration { + let first_kept = active_era - bonding_duration; // prune out everything that's from before the first-kept index. let n_to_prune = bonded.iter().take_while(|&&(era_idx, _)| era_idx < first_kept).count(); @@ -1982,19 +2153,65 @@ impl Module { } }); - // Reassign all Stakers. - let (_slot_stake, maybe_new_validators) = Self::select_validators(); - Self::apply_unapplied_slashes(current_era); + Self::apply_unapplied_slashes(active_era); + } + + /// Compute payout for era. + fn end_era(active_era: ActiveEraInfo>, _session_index: SessionIndex) { + // Note: active_era_start can be None if end era is called during genesis config. + if let Some(active_era_start) = active_era.start { + let now = T::Time::now(); + + let era_duration = now - active_era_start; + let (total_payout, _max_payout) = inflation::compute_total_payout::( + era_duration, + now - Self::genesis_time(), + T::Cap::get().saturating_sub(T::RingCurrency::total_issuance()), + PayoutFraction::get(), + ); + + // Set ending era reward. + >::insert(&active_era.index, total_payout); + } + } + + /// Plan a new era. Return the potential new staking set. + fn new_era(start_session_index: SessionIndex) -> Option> { + // Increment or set current era. + let current_era = CurrentEra::mutate(|s| { + *s = Some(s.map(|s| s + 1).unwrap_or(0)); + s.unwrap() + }); + ErasStartSessionIndex::insert(¤t_era, &start_session_index); + + // Clean old era information. + if let Some(old_era) = current_era.checked_sub(Self::history_depth() + 1) { + Self::clear_era_information(old_era); + } + + // Set staking information for new era. + let maybe_new_validators = Self::select_validators(current_era); maybe_new_validators } + /// Clear all era information for given era. + fn clear_era_information(era_index: EraIndex) { + >::remove_prefix(era_index); + >::remove_prefix(era_index); + >::remove_prefix(era_index); + >::remove(era_index); + >::remove(era_index); + ErasTotalStake::remove(era_index); + ErasStartSessionIndex::remove(era_index); + } + /// Apply previously-unapplied slashes on the beginning of a new era, after a delay. - fn apply_unapplied_slashes(current_era: EraIndex) { + fn apply_unapplied_slashes(active_era: EraIndex) { let slash_defer_duration = T::SlashDeferDuration::get(); ::EarliestUnappliedSlash::mutate(|earliest| { if let Some(ref mut earliest) = earliest { - let keep_from = current_era.saturating_sub(slash_defer_duration); + let keep_from = active_era.saturating_sub(slash_defer_duration); for era in (*earliest)..keep_from { let era_slashes = ::UnappliedSlashes::take(&era); for slash in era_slashes { @@ -2007,21 +2224,25 @@ impl Module { }) } - /// Select a new validator set from the assembled stakers and their role preferences. + /// Select a new validator set from the assembled stakers and their role preferences, and store + /// staking information for the new current era. /// - /// Returns the new `SlotStake` value and a set of newly selected _stash_ IDs. + /// Fill the storages `ErasStakers`, `ErasStakersClipped`, `ErasValidatorPrefs` and + /// `ErasTotalStake` for current era. + /// + /// Returns a set of newly selected _stash_ IDs. /// /// Assumes storage is coherent with the declaration. - fn select_validators() -> (Power, Option>) { + fn select_validators(current_era: EraIndex) -> Option> { let mut all_nominators: Vec<(T::AccountId, Vec)> = vec![]; - let all_validator_candidates_iter = >::enumerate(); - let all_validators = all_validator_candidates_iter - .map(|(who, _pref)| { - let self_vote = (who.clone(), vec![who.clone()]); - all_nominators.push(self_vote); - who - }) - .collect::>(); + let mut all_validators_and_prefs = BTreeMap::new(); + let mut all_validators = Vec::new(); + for (validator, preference) in >::enumerate() { + let self_vote = (validator.clone(), vec![validator.clone()]); + all_nominators.push(self_vote); + all_validators_and_prefs.insert(validator.clone(), preference); + all_validators.push(validator); + } let nominator_votes = >::enumerate().map(|(nominator, nominations)| { let Nominations { submitted_in, @@ -2051,8 +2272,8 @@ impl Module { if let Some(phragmen_result) = maybe_phragmen_result { let elected_stashes = phragmen_result .winners - .iter() - .map(|(s, _)| s.clone()) + .into_iter() + .map(|(s, _)| s) .collect::>(); let assignments = phragmen_result.assignments; @@ -2065,13 +2286,8 @@ impl Module { Self::stake_of, ); - // Clear Stakers. - for v in Self::current_elected().iter() { - >::remove(v); - } - - // Populate Stakers and figure out the minimum stake behind a slot. - let mut slot_stake = T::TotalPower::get(); + // Populate stakers information and figure out the total staked. + let mut total_staked = 0; for (c, s) in supports.into_iter() { // build `struct exposure` from `support` let mut own_ring_balance: RingBalance = Zero::zero(); @@ -2102,6 +2318,9 @@ impl Module { total_power += power; }, ); + + total_staked += total_power; + let exposure = Exposure { own_ring_balance, own_kton_balance, @@ -2110,24 +2329,33 @@ impl Module { others, }; - if exposure.total_power < slot_stake { - slot_stake = exposure.total_power; + >::insert(¤t_era, &c, &exposure); + + let mut exposure_clipped = exposure; + let clipped_max_len = T::MaxNominatorRewardedPerValidator::get() as usize; + if exposure_clipped.others.len() > clipped_max_len { + exposure_clipped + .others + .sort_unstable_by(|a, b| a.power.cmp(&b.power).reverse()); + exposure_clipped.others.truncate(clipped_max_len); } - >::insert(&c, exposure.clone()); + >::insert(¤t_era, &c, exposure_clipped); } - // Update slot stake. - SlotStake::put(&slot_stake); - - // Set the new validator set in sessions. - >::put(&elected_stashes); + // Insert current era staking informations + ErasTotalStake::insert(¤t_era, total_staked); + let default_pref = ValidatorPrefs::default(); + for stash in &elected_stashes { + let pref = all_validators_and_prefs.get(stash).unwrap_or(&default_pref); // Must never happen, but better to be safe. + >::insert(¤t_era, stash, pref); + } // In order to keep the property required by `n_session_ending` // that we must return the new validator set even if it's the same as the old, // as long as any underlying economic conditions have changed, we don't attempt // to do any optimization where we compare against the prior set. - (slot_stake, Some(elected_stashes)) + Some(elected_stashes) } else { // There were not enough candidates for even our minimal level of functionality. // This is bad. @@ -2135,7 +2363,7 @@ impl Module { // and let the chain keep producing blocks until we can decide on a sufficiently // substantial set. // TODO: #2494 - (Self::slot_stake(), None) + None } } @@ -2147,7 +2375,7 @@ impl Module { /// - after a `withdraw_unbond()` call that frees all of a stash's bonded balance. /// - through `reap_stash()` if the balance has fallen to zero (through slashing). fn kill_stash(stash: &T::AccountId) -> DispatchResult { - let controller = Bonded::::take(stash).ok_or(Error::::NotStash)?; + let controller = >::take(stash).ok_or(>::NotStash)?; >::remove(&controller); >::remove(stash); @@ -2156,7 +2384,7 @@ impl Module { slashing::clear_stash_metadata::(stash); - system::Module::::dec_ref(stash); + >::dec_ref(stash); Ok(()) } @@ -2174,30 +2402,14 @@ impl Module { /// COMPLEXITY: Complexity is `number_of_validator_to_reward x current_elected_len`. /// If you need to reward lots of validator consider using `reward_by_indices`. pub fn reward_by_ids(validators_points: impl IntoIterator) { - CurrentEraPointsEarned::mutate(|rewards| { - let current_elected = Self::current_elected(); - for (validator, points) in validators_points.into_iter() { - if let Some(index) = current_elected.iter().position(|elected| *elected == validator) { - rewards.add_points_to_index(index as u32, points); + if let Some(active_era) = Self::active_era() { + >::mutate(active_era.index, |era_rewards| { + for (validator, points) in validators_points.into_iter() { + *era_rewards.individual.entry(validator).or_default() += points; + era_rewards.total += points; } - } - }); - } - - /// Add reward points to validators using their validator index. - /// - /// For each element in the iterator the given number of points in u32 is added to the - /// validator, thus duplicates are handled. - pub fn reward_by_indices(validators_points: impl IntoIterator) { - let current_elected_len = Self::current_elected().len() as u32; - - CurrentEraPointsEarned::mutate(|rewards| { - for (validator_index, points) in validators_points.into_iter() { - if validator_index < current_elected_len { - rewards.add_points_to_index(validator_index, points); - } - } - }); + }); + } } /// Ensures that at the end of the current session there will be a new era. @@ -2212,14 +2424,18 @@ impl Module { impl pallet_session::SessionManager for Module { fn new_session(new_index: SessionIndex) -> Option> { Self::ensure_storage_upgraded(); - if new_index == 0 { - return Self::initial_session(); - } - Self::new_session(new_index - 1) + Self::new_session(new_index) + } + fn end_session(end_index: SessionIndex) { + Self::end_session(end_index) + } + fn start_session(start_index: SessionIndex) { + Self::start_session(start_index) } - fn end_session(_end_index: SessionIndex) {} } +/// This implementation has the same constrains as the implementation of +/// `pallet_session::SessionManager`. impl pallet_session::historical::SessionManager, KtonBalance>> for Module @@ -2228,15 +2444,22 @@ impl new_index: SessionIndex, ) -> Option, KtonBalance>)>> { >::new_session(new_index).map(|validators| { + let current_era = Self::current_era() + // Must be some as a new era has been created. + .unwrap_or(0); + validators .into_iter() .map(|v| { - let exposure = >::get(&v); + let exposure = Self::eras_stakers(current_era, &v); (v, exposure) }) .collect() }) } + fn start_session(start_index: SessionIndex) { + >::start_session(start_index) + } fn end_session(end_index: SessionIndex) { >::end_session(end_index) } @@ -2246,12 +2469,15 @@ impl /// * 20 points to the block producer for producing a (non-uncle) block in the relay chain, /// * 2 points to the block producer for each reference to a previously unreferenced uncle, and /// * 1 point to the producer of each referenced uncle block. -impl pallet_authorship::EventHandler for Module { +impl pallet_authorship::EventHandler for Module +where + T: Trait + pallet_authorship::Trait + pallet_session::Trait, +{ fn note_author(author: T::AccountId) { Self::reward_by_ids(vec![(author, 20)]); } fn note_uncle(author: T::AccountId, _age: T::BlockNumber) { - Self::reward_by_ids(vec![(>::author(), 2), (author, 1)]) + Self::reward_by_ids(vec![(>::author(), 2), (author, 1)]); } } @@ -2265,13 +2491,20 @@ impl Convert> for StashOf { } } -/// A typed conversion from stash account ID to the current exposure of nominators +/// A typed conversion from stash account ID to the active exposure of nominators /// on that account. +/// +/// Active exposure is the exposure of the validator set currently validating, i.e. in +/// `active_era`. It can differ from the latest planned exposure in `current_era`. pub struct ExposureOf(PhantomData); impl Convert, KtonBalance>>> for ExposureOf { fn convert(validator: T::AccountId) -> Option, KtonBalance>> { - Some(>::stakers(&validator)) + if let Some(active_era) = >::active_era() { + Some(>::eras_stakers(active_era.index, &validator)) + } else { + None + } } } @@ -2295,12 +2528,25 @@ where Self::ensure_storage_upgraded(); let reward_proportion = SlashRewardFraction::get(); - let era_now = Self::current_era(); - let window_start = era_now.saturating_sub(T::BondingDurationInEra::get()); - let current_era_start_session = CurrentEraStartSessionIndex::get(); - // fast path for current-era report - most likely. - let slash_era = if slash_session >= current_era_start_session { - era_now + + let active_era = { + let active_era = Self::active_era(); + if active_era.is_none() { + return; + } + active_era.unwrap().index + }; + let active_era_start_session_index = Self::eras_start_session_index(active_era).unwrap_or_else(|| { + frame_support::print("Error: start_session_index must be set for current_era"); + 0 + }); + + let window_start = active_era.saturating_sub(T::BondingDurationInEra::get()); + + // fast path for active-era report - most likely. + // `slash_session` cannot be in a future active era. It must be in `active_era` or before. + let slash_era = if slash_session >= active_era_start_session_index { + active_era } else { // reverse because it's more likely to find reports from recent eras. match BondedEras::get() @@ -2316,7 +2562,7 @@ where ::EarliestUnappliedSlash::mutate(|earliest| { if earliest.is_none() { - *earliest = Some(era_now) + *earliest = Some(active_era) } }); @@ -2337,7 +2583,7 @@ where exposure, slash_era, window_start, - now: era_now, + now: active_era, reward_proportion, }); @@ -2348,7 +2594,7 @@ where slashing::apply_slash::(unapplied); } else { // defer to end of some `slash_defer_duration` from now. - ::UnappliedSlashes::mutate(era_now, move |for_later| for_later.push(unapplied)); + ::UnappliedSlashes::mutate(active_era, move |for_later| for_later.push(unapplied)); } } } @@ -2366,7 +2612,7 @@ where R: ReportOffence, O: Offence, { - fn report_offence(reporters: Vec, offence: O) { + fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError> { >::ensure_storage_upgraded(); // disallow any slashing from before the current bonding period. @@ -2380,7 +2626,8 @@ where { R::report_offence(reporters, offence) } else { - >::deposit_event(RawEvent::OldSlashingReportDiscarded(offence_session)) + >::deposit_event(RawEvent::OldSlashingReportDiscarded(offence_session)); + Ok(()) } } } diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs index b9cf12378..f1cd085cf 100644 --- a/frame/staking/src/mock.rs +++ b/frame/staking/src/mock.rs @@ -1,6 +1,9 @@ //! Test utilities -use std::{cell::RefCell, collections::HashSet}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, +}; use frame_support::{ assert_ok, impl_outer_origin, parameter_types, @@ -11,7 +14,7 @@ use frame_support::{ use sp_core::{crypto::key_types, H256}; use sp_runtime::{ testing::{Header, UintAuthorityId}, - traits::{IdentityLookup, OnInitialize, OpaqueKeys, SaturatedConversion}, + traits::{IdentityLookup, OnFinalize, OnInitialize, OpaqueKeys, SaturatedConversion}, {KeyTypeId, Perbill}, }; use sp_staking::{ @@ -201,6 +204,7 @@ parameter_types! { pub const BondingDurationInEra: EraIndex = 3; // assume 60 blocks per session pub const BondingDurationInBlockNumber: BlockNumber = 3 * 3 * 60; + pub const MaxNominatorRewardedPerValidator: u32 = 64; pub const Cap: Balance = CAP; pub const TotalPower: Power = TOTAL_POWER; @@ -214,6 +218,7 @@ impl Trait for Test { type SlashDeferDuration = SlashDeferDuration; type SlashCancelOrigin = system::EnsureRoot; type SessionInterface = Self; + type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator; type RingCurrency = Ring; type RingRewardRemainder = (); type RingSlash = (); @@ -356,7 +361,6 @@ impl ExtBuilder { }; let nominated = if self.nominate { vec![11, 21] } else { vec![] }; let _ = GenesisConfig:: { - current_era: 0, stakers: vec![ // (stash, controller, staked_amount, status) (11, 10, balance_factor * 1000, StakerStatus::::Validator), @@ -396,46 +400,36 @@ impl ExtBuilder { } } -pub fn check_exposure_all() { - Staking::current_elected() - .into_iter() - .for_each(|acc| check_exposure(acc)); +pub fn check_exposure_all(era: EraIndex) { + >::iter_prefix(era).for_each(check_exposure) } -pub fn check_nominator_all() { - >::enumerate().for_each(|(acc, _)| check_nominator_exposure(acc)); +pub fn check_nominator_all(era: EraIndex) { + >::enumerate().for_each(|(acc, _)| check_nominator_exposure(era, acc)); } /// Check for each selected validator: expo.total = Sum(expo.other) + expo.own -pub fn check_exposure(stash: AccountId) { - assert_is_stash(stash); - let expo = Staking::stakers(&stash); +pub fn check_exposure(expo: Exposure) { assert_eq!( expo.total_power, expo.own_power + expo.others.iter().map(|e| e.power).sum::(), - "wrong total exposure for {:?}: {:?}", - stash, + "wrong total exposure {:?}", expo, ); } -pub fn assert_ledger_consistent(stash: u64) { - assert_is_stash(stash); - let ledger = Staking::ledger(stash - 1).unwrap(); - - assert_eq!(ledger.active_ring, ledger.ring_staking_lock.staking_amount); - assert_eq!(ledger.active_kton, ledger.kton_staking_lock.staking_amount); -} - /// Check that for each nominator: slashable_balance > sum(used_balance) /// Note: we might not consume all of a nominator's balance, but we MUST NOT over spend it. -pub fn check_nominator_exposure(stash: AccountId) { +pub fn check_nominator_exposure(era: EraIndex, stash: AccountId) { assert_is_stash(stash); let mut sum = 0; - Staking::current_elected() - .iter() - .map(|v| Staking::stakers(v)) - .for_each(|e| e.others.iter().filter(|i| i.who == stash).for_each(|i| sum += i.power)); + >::iter_prefix(era).for_each(|exposure| { + exposure + .others + .iter() + .filter(|i| i.who == stash) + .for_each(|i| sum += i.power) + }); let nominator_power = Staking::power_of(&stash); // a nominator cannot over-spend. assert!( @@ -451,6 +445,14 @@ pub fn assert_is_stash(acc: AccountId) { assert!(Staking::bonded(&acc).is_some(), "Not a stash."); } +pub fn assert_ledger_consistent(stash: AccountId) { + assert_is_stash(stash); + let ledger = Staking::ledger(stash - 1).unwrap(); + + assert_eq!(ledger.active_ring, ledger.ring_staking_lock.staking_amount); + assert_eq!(ledger.active_kton, ledger.kton_staking_lock.staking_amount); +} + pub fn bond(acc: AccountId, val: StakingBalanceT) { // a = controller // a + 1 = stash @@ -488,9 +490,8 @@ pub fn advance_session() { } pub fn start_session(session_index: SessionIndex) { - // Compensate for session delay - let session_index = session_index + 1; for i in Session::current_index()..session_index { + Staking::on_finalize(System::block_number()); System::set_block_number((i + 1).into()); Timestamp::set_timestamp(System::block_number() * 1000); Session::on_initialize(System::block_number()); @@ -501,7 +502,7 @@ pub fn start_session(session_index: SessionIndex) { pub fn start_era(era_index: EraIndex) { start_session((era_index * 3).into()); - assert_eq!(Staking::current_era(), era_index); + assert_eq!(Staking::active_era().unwrap().index, era_index); } pub fn current_total_payout_for_duration(duration: u64) -> Balance { @@ -515,7 +516,9 @@ pub fn current_total_payout_for_duration(duration: u64) -> Balance { } pub fn reward_all_elected() { - let rewards = Staking::current_elected().iter().map(|v| (*v, 1)).collect::>(); + let rewards = ::SessionInterface::validators() + .into_iter() + .map(|v| (v, 1)); Staking::reward_by_ids(rewards) } @@ -542,8 +545,12 @@ pub fn on_offence_in_era( } } - if Staking::current_era() == era { - Staking::on_offence(offenders, slash_fraction, Staking::current_era_start_session_index()); + if Staking::active_era().unwrap().index == era { + Staking::on_offence( + offenders, + slash_fraction, + Staking::eras_start_session_index(era).unwrap(), + ); } else { panic!("cannot slash in era {}", era); } @@ -553,6 +560,41 @@ pub fn on_offence_now( offenders: &[OffenceDetails>], slash_fraction: &[Perbill], ) { - let now = Staking::current_era(); + let now = Staking::active_era().unwrap().index; on_offence_in_era(offenders, slash_fraction, now) } + +/// Make all validator and nominator request their payment +pub fn make_all_reward_payment(era: EraIndex) { + let validators_with_reward = >::get(era) + .individual + .keys() + .cloned() + .collect::>(); + + // reward nominators + let mut nominator_controllers = HashMap::new(); + for validator in Staking::eras_reward_points(era).individual.keys() { + let validator_exposure = Staking::eras_stakers_clipped(era, validator); + for (nom_index, nom) in validator_exposure.others.iter().enumerate() { + if let Some(nom_ctrl) = Staking::bonded(nom.who) { + nominator_controllers + .entry(nom_ctrl) + .or_insert(vec![]) + .push((validator.clone(), nom_index as u32)); + } + } + } + for (nominator_controller, validators_with_nom_index) in nominator_controllers { + assert_ok!(Staking::payout_nominator( + Origin::signed(nominator_controller), + era, + validators_with_nom_index, + )); + } + + // reward validators + for validator_controller in validators_with_reward.iter().filter_map(Staking::bonded) { + assert_ok!(Staking::payout_validator(Origin::signed(validator_controller), era)); + } +} diff --git a/frame/vesting/src/lib.rs b/frame/vesting/src/lib.rs index f0221c097..88eb19170 100644 --- a/frame/vesting/src/lib.rs +++ b/frame/vesting/src/lib.rs @@ -273,19 +273,20 @@ where #[cfg(test)] mod tests { - use super::*; + use std::cell::RefCell; use frame_support::{assert_noop, assert_ok, impl_outer_origin, parameter_types, traits::Get, weights::Weight}; use sp_core::H256; - use std::cell::RefCell; // The testing primitives are very useful for avoiding having to work with signatures // or public keys. `u64` is used as the `AccountId` and no `Signature`s are required. use sp_runtime::{ testing::Header, - traits::{BlakeTwo256, Identity, IdentityLookup, OnInitialize}, + traits::{BlakeTwo256, Identity, IdentityLookup}, Perbill, }; - use sp_storage::Storage; + + use crate::*; + use darwinia_support::balance::AccountData; impl_outer_origin! { pub enum Origin for Test where system = frame_system {} @@ -304,10 +305,10 @@ mod tests { } impl frame_system::Trait for Test { type Origin = Origin; + type Call = (); type Index = u64; type BlockNumber = u64; type Hash = H256; - type Call = (); type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup; @@ -319,7 +320,7 @@ mod tests { type AvailableBlockRatio = AvailableBlockRatio; type Version = (); type ModuleToIndex = (); - type AccountData = pallet_balances::balance::AccountData; + type AccountData = AccountData; type OnNewAccount = (); type OnKilledAccount = (); } @@ -329,6 +330,7 @@ mod tests { type Event = (); type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; + type TryDropKton = (); } impl Trait for Test { type Event = (); @@ -389,54 +391,6 @@ mod tests { } } - #[test] - fn vesting_info_via_migration_should_work() { - let mut s = Storage::default(); - use hex_literal::hex; - // A dump of data from the previous version for which we know account 6 vests 30 of its 60 - // over 5 blocks from block 3. - let data = vec![ - (hex!["26aa394eea5630e07c48ae0c9558cef702a5c1b19ab7a04f536c519aca4983ac"].to_vec(), hex!["0100000000000000"].to_vec()), - (hex!["26aa394eea5630e07c48ae0c9558cef70a98fdbe9ce6c55837576c60c7af3850"].to_vec(), hex!["02000000"].to_vec()), - (hex!["26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7"].to_vec(), hex!["08000000000000000000000000"].to_vec()), - (hex!["26aa394eea5630e07c48ae0c9558cef78a42f33323cb5ced3b44dd825fda9fcc"].to_vec(), hex!["4545454545454545454545454545454545454545454545454545454545454545"].to_vec()), - (hex!["26aa394eea5630e07c48ae0c9558cef7a44704b568d21667356a5a050c11874681e47a19e6b29b0a65b9591762ce5143ed30d0261e5d24a3201752506b20f15c"].to_vec(), hex!["4545454545454545454545454545454545454545454545454545454545454545"].to_vec()), - (hex!["3a636f6465"].to_vec(), hex![""].to_vec()), - (hex!["3a65787472696e7369635f696e646578"].to_vec(), hex!["00000000"].to_vec()), - (hex!["3a686561707061676573"].to_vec(), hex!["0800000000000000"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f218f26c73add634897550b4003b26bc61dbd7d0b561a41d23c2a469ad42fbd70d5438bae826f6fd607413190c37c363b"].to_vec(), hex!["046d697363202020200300000000000000ffffffffffffffff04"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f218f26c73add634897550b4003b26bc66cddb367afbd583bb48f9bbd7d5ba3b1d0738b4881b1cddd38169526d8158137"].to_vec(), hex!["0474786665657320200300000000000000ffffffffffffffff01"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f218f26c73add634897550b4003b26bc6e88b43fded6323ef02ffeffbd8c40846ee09bf316271bd22369659c959dd733a"].to_vec(), hex!["08616c6c20202020200300000000000000ffffffffffffffff1f64656d6f63726163ffffffffffffffff030000000000000002"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f3c22813def93ef32c365b55cb92f10f91dbd7d0b561a41d23c2a469ad42fbd70d5438bae826f6fd607413190c37c363b"].to_vec(), hex!["0500000000000000"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f57c875e4cff74148e4628f264b974c80"].to_vec(), hex!["d200000000000000"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f5f27b51b5ec208ee9cb25b55d8728243b8788bb218b185b63e3e92653953f29b6b143fb8cf5159fc908632e6fe490501"].to_vec(), hex!["1e0000000000000006000000000000000200000000000000"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b41dbd7d0b561a41d23c2a469ad42fbd70d5438bae826f6fd607413190c37c363b"].to_vec(), hex!["0500000000000000"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b46cddb367afbd583bb48f9bbd7d5ba3b1d0738b4881b1cddd38169526d8158137"].to_vec(), hex!["1e00000000000000"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b4b8788bb218b185b63e3e92653953f29b6b143fb8cf5159fc908632e6fe490501"].to_vec(), hex!["3c00000000000000"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b4e88b43fded6323ef02ffeffbd8c40846ee09bf316271bd22369659c959dd733a"].to_vec(), hex!["1400000000000000"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b4e96760d274653a39b429a87ebaae9d3aa4fdf58b9096cf0bebc7c4e5a4c2ed8d"].to_vec(), hex!["2800000000000000"].to_vec()), - (hex!["c2261276cc9d1f8598ea4b6a74b15c2f6482b9ade7bc6657aaca787ba1add3b4effb728943197fd12e694cbf3f3ede28fbf7498b0370c6dfa0013874b417c178"].to_vec(), hex!["3200000000000000"].to_vec()), - (hex!["f2794c22e353e9a839f12faab03a911b7f17cdfbfa73331856cca0acddd7842e"].to_vec(), hex!["00000000"].to_vec()), - (hex!["f2794c22e353e9a839f12faab03a911bbdcb0c5143a8617ed38ae3810dd45bc6"].to_vec(), hex!["00000000"].to_vec()), - (hex!["f2794c22e353e9a839f12faab03a911be2f6cb0456905c189bcb0458f9440f13"].to_vec(), hex!["00000000"].to_vec()), - ]; - s.top = data.into_iter().collect(); - sp_io::TestExternalities::new(s).execute_with(|| { - Balances::on_initialize(1); - assert_eq!(Balances::free_balance(6), 60); - assert_eq!(Balances::usable_balance(&6), 30); - System::set_block_number(2); - assert_ok!(Vesting::vest(Origin::signed(6))); - assert_eq!(Balances::usable_balance(&6), 30); - System::set_block_number(3); - assert_ok!(Vesting::vest(Origin::signed(6))); - assert_eq!(Balances::usable_balance(&6), 36); - System::set_block_number(4); - assert_ok!(Vesting::vest(Origin::signed(6))); - assert_eq!(Balances::usable_balance(&6), 42); - }); - } - #[test] fn check_vesting_status() { ExtBuilder::default().existential_deposit(256).build().execute_with(|| { diff --git a/primitives/staking/Cargo.toml b/primitives/staking/Cargo.toml new file mode 100644 index 000000000..d607cf5d5 --- /dev/null +++ b/primitives/staking/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "sp-staking" +version = "0.5.0" +authors = ["Darwinia Network "] +description = "A crate which contains primitives that are useful for implementation that uses staking approaches in general. Definitions related to sessions, slashing, etc go here." +edition = "2018" +license = "GPL-3.0" +homepage = "https://darwinia.network/" +repository = "https://github.com/darwinia-network/darwinia/" + +[dependencies] +# crates.io +codec = { package = "parity-scale-codec", version = "1.2.0", default-features = false, features = ["derive"] } + +# github.com +sp-runtime = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } +sp-std = { default-features = false, git = "https://github.com/darwinia-network/substrate.git", tag = "v2.0.0-alpha.3" } + +[features] +default = ["std"] +std = [ + "codec/std", + + "sp-runtime/std", + "sp-std/std", +] diff --git a/primitives/staking/src/lib.rs b/primitives/staking/src/lib.rs new file mode 100644 index 000000000..3f6c1873f --- /dev/null +++ b/primitives/staking/src/lib.rs @@ -0,0 +1,23 @@ + +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +#![cfg_attr(not(feature = "std"), no_std)] + +//! A crate which contains primitives that are useful for implementation that uses staking +//! approaches in general. Definitions related to sessions, slashing, etc go here. + +pub mod offence; + +/// Simple index type with which we can count sessions. +pub type SessionIndex = u32; diff --git a/primitives/staking/src/offence.rs b/primitives/staking/src/offence.rs new file mode 100644 index 000000000..06e73f018 --- /dev/null +++ b/primitives/staking/src/offence.rs @@ -0,0 +1,168 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Common traits and types that are useful for describing offences for usage in environments +//! that use staking. + +use sp_std::vec::Vec; + +use codec::{Encode, Decode}; +use sp_runtime::Perbill; + +use crate::SessionIndex; + +/// The kind of an offence, is a byte string representing some kind identifier +/// e.g. `b"im-online:offlin"`, `b"babe:equivocatio"` +// TODO [slashing]: Is there something better we can have here that is more natural but still +// flexible? as you see in examples, they get cut off with long names. +pub type Kind = [u8; 16]; + +/// Number of times the offence of this authority was already reported in the past. +/// +/// Note that we don't buffer offence reporting, so every time we see a new offence +/// of the same kind, we will report past authorities again. +/// This counter keeps track of how many times the authority was already reported in the past, +/// so that we can slash it accordingly. +pub type OffenceCount = u32; + +/// A trait implemented by an offence report. +/// +/// This trait assumes that the offence is legitimate and was validated already. +/// +/// Examples of offences include: a BABE equivocation or a GRANDPA unjustified vote. +pub trait Offence { + /// Identifier which is unique for this kind of an offence. + const ID: Kind; + + /// A type that represents a point in time on an abstract timescale. + /// + /// See `Offence::time_slot` for details. The only requirement is that such timescale could be + /// represented by a single `u128` value. + type TimeSlot: Clone + codec::Codec + Ord; + + /// The list of all offenders involved in this incident. + /// + /// The list has no duplicates, so it is rather a set. + fn offenders(&self) -> Vec; + + /// The session index that is used for querying the validator set for the `slash_fraction` + /// function. + /// + /// This is used for filtering historical sessions. + fn session_index(&self) -> SessionIndex; + + /// Return a validator set count at the time when the offence took place. + fn validator_set_count(&self) -> u32; + + /// A point in time when this offence happened. + /// + /// This is used for looking up offences that happened at the "same time". + /// + /// The timescale is abstract and doesn't have to be the same across different implementations + /// of this trait. The value doesn't represent absolute timescale though since it is interpreted + /// along with the `session_index`. Two offences are considered to happen at the same time iff + /// both `session_index` and `time_slot` are equal. + /// + /// As an example, for GRANDPA timescale could be a round number and for BABE it could be a slot + /// number. Note that for GRANDPA the round number is reset each epoch. + fn time_slot(&self) -> Self::TimeSlot; + + /// A slash fraction of the total exposure that should be slashed for this + /// particular offence kind for the given parameters that happened at a singular `TimeSlot`. + /// + /// `offenders_count` - the count of unique offending authorities. It is >0. + /// `validator_set_count` - the cardinality of the validator set at the time of offence. + fn slash_fraction( + offenders_count: u32, + validator_set_count: u32, + ) -> Perbill; +} + +/// Errors that may happen on offence reports. +#[derive(PartialEq, sp_runtime::RuntimeDebug)] +pub enum OffenceError { + /// The report has already been sumbmitted. + DuplicateReport, + + /// Other error has happened. + Other(u8), +} + +impl sp_runtime::traits::Printable for OffenceError { + fn print(&self) { + "OffenceError".print(); + match self { + Self::DuplicateReport => "DuplicateReport".print(), + Self::Other(e) => { + "Other".print(); + e.print(); + } + } + } +} + +/// A trait for decoupling offence reporters from the actual handling of offence reports. +pub trait ReportOffence> { + /// Report an `offence` and reward given `reporters`. + fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError>; +} + +impl> ReportOffence for () { + fn report_offence(_reporters: Vec, _offence: O) -> Result<(), OffenceError> { Ok(()) } +} + +/// A trait to take action on an offence. +/// +/// Used to decouple the module that handles offences and +/// the one that should punish for those offences. +pub trait OnOffenceHandler { + /// A handler for an offence of a particular kind. + /// + /// Note that this contains a list of all previous offenders + /// as well. The implementer should cater for a case, where + /// the same authorities were reported for the same offence + /// in the past (see `OffenceCount`). + /// + /// The vector of `slash_fraction` contains `Perbill`s + /// the authorities should be slashed and is computed + /// according to the `OffenceCount` already. This is of the same length as `offenders.` + /// Zero is a valid value for a fraction. + /// + /// The `session` parameter is the session index of the offence. + fn on_offence( + offenders: &[OffenceDetails], + slash_fraction: &[Perbill], + session: SessionIndex, + ); +} + +impl OnOffenceHandler for () { + fn on_offence( + _offenders: &[OffenceDetails], + _slash_fraction: &[Perbill], + _session: SessionIndex, + ) {} +} + +/// A details about an offending authority for a particular kind of offence. +#[derive(Clone, PartialEq, Eq, Encode, Decode, sp_runtime::RuntimeDebug)] +pub struct OffenceDetails { + /// The offending authority id + pub offender: Offender, + /// A list of reporters of offences of this authority ID. Possibly empty where there are no + /// particular reporters. + pub reporters: Vec, +}