diff --git a/.github/workflows/daily-deny.yml b/.github/workflows/daily-deny.yml index 547ea318..383b7d6b 100644 --- a/.github/workflows/daily-deny.yml +++ b/.github/workflows/daily-deny.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 - name: Install cargo deny - run: cargo +1.91.1 install cargo-deny --version =0.18.8 + run: cargo +1.91.1 install cargo-deny --version =0.18.9 - name: Run cargo deny run: cargo deny -L error --all-features check --hide-inclusion-graph diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6a4fdc33..62ac7cda 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 - name: Install cargo deny - run: cargo +1.91.1 install cargo-deny --version =0.18.8 + run: cargo +1.91.1 install cargo-deny --version =0.18.9 - name: Run cargo deny run: cargo deny -L error --all-features check --hide-inclusion-graph diff --git a/Cargo.lock b/Cargo.lock index 3aa2b283..1a811a63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,9 +92,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-chains" -version = "0.2.21" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ebac8ff9c2f07667e1803dc777304337e160ce5153335beb45e8ec0751808" +checksum = "35d744058a9daa51a8cf22a3009607498fcf82d3cf4c5444dd8056cdf651f471" dependencies = [ "alloy-primitives", "num_enum", @@ -4321,9 +4321,9 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3104e13b51e4711ff5738caa1fb54467c8604c2e94d607e27745bcf709068774" +checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" dependencies = [ "bs58", "ed25519-dalek", @@ -8441,7 +8441,9 @@ dependencies = [ "serai-core-pallet", "serai-dex-pallet", "serai-genesis-liquidity-pallet", + "serai-signals-pallet", "serai-validator-sets-pallet", + "sp-application-crypto", "sp-core", ] diff --git a/substrate/in-instructions/Cargo.toml b/substrate/in-instructions/Cargo.toml index a9cb2ca2..2b9deb0e 100644 --- a/substrate/in-instructions/Cargo.toml +++ b/substrate/in-instructions/Cargo.toml @@ -23,6 +23,7 @@ workspace = true scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } +sp-application-crypto = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } @@ -32,6 +33,7 @@ serai-abi = { path = "../abi", default-features = false, features = ["substrate" serai-core-pallet = { path = "../core", default-features = false } serai-coins-pallet = { path = "../coins", default-features = false } serai-validator-sets-pallet = { path = "../validator-sets", default-features = false } +serai-signals-pallet = { path = "../signals", default-features = false } serai-dex-pallet = { path = "../dex", default-features = false } serai-genesis-liquidity-pallet = { path = "../genesis-liquidity", default-features = false } @@ -40,6 +42,7 @@ std = [ "scale/std", "sp-core/std", + "sp-application-crypto/std", "frame-system/std", "frame-support/std", @@ -49,6 +52,7 @@ std = [ "serai-core-pallet/std", "serai-coins-pallet/std", "serai-validator-sets-pallet/std", + "serai-signals-pallet/std", "serai-dex-pallet/std", "serai-genesis-liquidity-pallet/std", ] @@ -61,6 +65,7 @@ try-runtime = [ "serai-core-pallet/try-runtime", "serai-coins-pallet/try-runtime", "serai-validator-sets-pallet/try-runtime", + "serai-signals-pallet/try-runtime", "serai-dex-pallet/try-runtime", "serai-genesis-liquidity-pallet/try-runtime", ] @@ -72,6 +77,7 @@ runtime-benchmarks = [ "serai-core-pallet/runtime-benchmarks", "serai-coins-pallet/runtime-benchmarks", "serai-validator-sets-pallet/runtime-benchmarks", + "serai-signals-pallet/runtime-benchmarks", "serai-dex-pallet/runtime-benchmarks", "serai-genesis-liquidity-pallet/runtime-benchmarks", ] diff --git a/substrate/in-instructions/src/lib.rs b/substrate/in-instructions/src/lib.rs index 739ecb3a..d070e2fd 100644 --- a/substrate/in-instructions/src/lib.rs +++ b/substrate/in-instructions/src/lib.rs @@ -7,6 +7,8 @@ extern crate alloc; #[expect(clippy::cast_possible_truncation)] #[frame_support::pallet] mod pallet { + use sp_application_crypto::RuntimePublic; + use frame_system::pallet_prelude::*; use frame_support::pallet_prelude::*; @@ -26,6 +28,7 @@ mod pallet { + serai_core_pallet::Config + serai_coins_pallet::Config + serai_validator_sets_pallet::Config + + serai_signals_pallet::Config + serai_coins_pallet::Config + serai_dex_pallet::Config + serai_genesis_liquidity_pallet::Config @@ -36,6 +39,18 @@ mod pallet { #[pallet::error] pub enum Error {} + /// The block the last batch was published during. + /// + /// This is used to restrict publication of batches to once-per-block, limiting the impact of a + /// compromised publisher from bloating the Serai blockchain with spam. + #[pallet::storage] + type BlockOfLastBatch = + StorageMap<_, Identity, ExternalNetworkId, BlockNumberFor, OptionQuery>; + + /// The ID of the last batch which was published. + #[pallet::storage] + type LastBatch = StorageMap<_, Identity, ExternalNetworkId, u32, OptionQuery>; + /// The Pallet struct. #[pallet::pallet] pub struct Pallet(_); @@ -55,6 +70,113 @@ mod pallet { todo!("TODO") } } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { + let batch = match call { + Call::execute_batch { batch } => batch, + Call::__Ignore(_, _) => Err(InvalidTransaction::Call)?, + }; + + let network = batch.batch.network(); + + // Verify the network isn't halted + if serai_signals_pallet::Pallet::::halted(network) { + Err(InvalidTransaction::Custom(1))?; + } + + // Verify the `Batch`'s signature + let mut signed_by_latest_decided_session = false; + { + let message = batch.batch.publish_batch_message(); + let signed_by_session = |session| { + let Some(key) = + serai_validator_sets_pallet::Pallet::::oraclization_key(ExternalValidatorSet { + network, + session, + }) + else { + return false; + }; + key.verify(&message, &batch.signature.into()) + }; + let Some(current_session) = + serai_validator_sets_pallet::Pallet::::current_session(NetworkId::from(network)) + else { + Err(InvalidTransaction::BadProof)? + }; + if !signed_by_session(current_session) { + let latest_decided_session = + serai_validator_sets_pallet::Pallet::::latest_decided_session(NetworkId::from( + network, + )) + .expect("current session yet never one decided?"); + if !signed_by_session(latest_decided_session) { + Err(InvalidTransaction::BadProof)?; + } + signed_by_latest_decided_session = true; + } + } + + // Verify this is the first `Batch` for this block + let current_block_number = frame_system::Pallet::::block_number(); + if BlockOfLastBatch::::get(network) == Some(current_block_number) { + // This transaction is valid in the future, the next block, but not now + Err(InvalidTransaction::Future)?; + } + + // Verify this `Batch` descends immediately from the prior `Batch` + let required_last_batch = batch.batch.id().checked_sub(1); + { + let last_batch = LastBatch::::get(network); + if last_batch < required_last_batch { + Err(InvalidTransaction::Future)?; + } + if last_batch > required_last_batch { + Err(InvalidTransaction::Stale)?; + } + } + + /* + Set the metadata fields as necessary for further batches to be verified. While this is + mutating the state within the verification function, it's necessary for the verification of + the following transactions. + + Additionally, we know these state changes occur as tests verify we can publish `Batch`es + and have the `LastBatch` field be incremented. + */ + BlockOfLastBatch::::set(network, Some(current_block_number)); + LastBatch::::set(network, Some(batch.batch.id())); + if signed_by_latest_decided_session { + // Because the latest decided session, which is not the current session, has taken over + // for publishing `Batch`es, it has agreed to become the current session + serai_validator_sets_pallet::Pallet::::accept_handover(network); + } + + let mut builder = ValidTransaction::with_tag_prefix("InInstructions"); + if let Some(required_last_batch) = required_last_batch { + // TODO: Should this replace the DB mutations present within this verification function? + builder = builder.and_requires((network, required_last_batch)); + } + builder + .and_provides((network, batch.batch.id())) + // Set a 10 block longevity, though this should be included in the next block + .longevity(10) + .propagate(true) + .build() + } + + /// Explicitly provide a `pre_dispatch` which calls `validate_unsigned`. + /// + /// This is reasonably assumed, and the current provided implementation, but not guaranteed by + /// the documentation. + fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { + Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()) + } + } } pub use pallet::*; diff --git a/substrate/signals/src/lib.rs b/substrate/signals/src/lib.rs index 97425d03..0fb9eb35 100644 --- a/substrate/signals/src/lib.rs +++ b/substrate/signals/src/lib.rs @@ -234,6 +234,11 @@ pub mod pallet { Ok(()) } + + /// Check if an external network was halted. + pub fn halted(network: ExternalNetworkId) -> bool { + Halted::::contains_key(network) + } } /// An error from the `signals` pallet. diff --git a/substrate/validator-sets/src/lib.rs b/substrate/validator-sets/src/lib.rs index 559d6bbe..df62d494 100644 --- a/substrate/validator-sets/src/lib.rs +++ b/substrate/validator-sets/src/lib.rs @@ -345,6 +345,14 @@ mod pallet { Ok(()) } + /// Have the latest decided session become the current session. + /// + /// This is restricted to `ExternalNetworkId` as this process happens internally for + /// `NetworkId::Serai`. + pub fn accept_handover(network: ExternalNetworkId) { + Abstractions::::accept_handover(network.into()); + } + /* TODO pub fn distribute_block_rewards( network: NetworkId,