From f85e422dee914745a6748c0c975d4093c4e8a050 Mon Sep 17 00:00:00 2001 From: unarbos Date: Sun, 12 Jul 2026 14:21:05 -0600 Subject: [PATCH 01/68] docs: fix "fill or kill type or order" typo in allow_partial doc comments Co-authored-by: Cursor --- pallets/subtensor/src/macros/dispatches.rs | 6 +++--- pallets/subtensor/src/staking/add_stake.rs | 2 +- pallets/subtensor/src/staking/remove_stake.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index b831ba085c..3907c29e01 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1390,7 +1390,7 @@ mod dispatches { /// * `limit_price`: The limit price expressed in units of RAO per one Alpha. /// /// * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes - /// fill or kill type or order. + /// fill or kill type of order. /// /// # Events /// * `StakeAdded`: On the successfully adding stake to a global account. @@ -1443,7 +1443,7 @@ mod dispatches { /// * `limit_price`: The limit price expressed in units of RAO per one Alpha. /// /// * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes - /// fill or kill type or order. + /// fill or kill type of order. /// /// # Events /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. @@ -1484,7 +1484,7 @@ mod dispatches { /// * `destination_netuid`: The network/subnet ID to which stake is added. /// * `alpha_amount`: The amount of stake to swap. /// * `limit_price`: The limit price expressed in units of RAO per one Alpha. - /// * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes fill or kill type or order. + /// * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes fill or kill type of order. /// /// # Errors /// * `BadOrigin`: The transaction is not signed. diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index 936d162f73..1d500d6a1b 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -78,7 +78,7 @@ impl Pallet { /// * `limit_price`: The limit price expressed in units of RAO per one Alpha. /// /// * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes - /// fill or kill type or order. + /// fill or kill type of order. /// /// # Events /// * `StakeAdded`: On the successfully adding stake to a global account. diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 239f124943..0b36dbf224 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -283,7 +283,7 @@ impl Pallet { /// * `limit_price`: The limit price expressed in units of RAO per one Alpha. /// /// * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes - /// fill or kill type or order. + /// fill or kill type of order. /// /// # Events /// * `StakeRemoved`: On the successfully removing stake from the hotkey account. From a2402dba6bac565c1f81e231cc27caf2edde8cc6 Mon Sep 17 00:00:00 2001 From: Loom Agent Date: Sun, 12 Jul 2026 21:53:48 +0000 Subject: [PATCH 02/68] feat(node): add optional jemalloc global allocator for archive-node memory (#2724) Archive nodes serving sustained RPC load show unbounded anonymous-memory growth (~3-4 GB/h) that fits the glibc-malloc arena-fragmentation profile across the node's hundreds of threads; the release binary, unlike polkadot, is not linked against jemalloc. Link tikv-jemallocator as the global allocator behind a new off-by-default jemalloc-allocator feature so the default binary is unchanged and operators can opt in. This provides the mechanism requested in #2724; effectiveness under load still needs operator soak testing. --- node/Cargo.toml | 10 ++++++++++ node/src/main.rs | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/node/Cargo.toml b/node/Cargo.toml index 333fc4a093..5d2e161f20 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -132,6 +132,13 @@ pallet-subtensor-swap-rpc = { workspace = true, features = ["std"] } pallet-subtensor-swap-runtime-api = { workspace = true, features = ["std"] } subtensor-macros.workspace = true +# Optional global allocator (off by default). Archive nodes serving sustained RPC load show +# unbounded anonymous-memory growth (~3-4 GB/h) that fits the glibc-malloc arena-fragmentation +# profile across the node's hundreds of threads; linking jemalloc as the global allocator (as the +# polkadot binary does) is the proposed mitigation (issue #2724). Gated behind a feature so the +# default release binary is unchanged until an operator opts in. +tikv-jemallocator = { version = "0.5", optional = true, default-features = false } + [build-dependencies] substrate-build-script-utils.workspace = true @@ -145,6 +152,9 @@ rocksdb = [ "sc-cli/rocksdb", ] default = ["rocksdb", "sql", "txpool"] +# Use tikv-jemallocator as the global allocator in the release binary (off by default). +# See the `tikv-jemallocator` dependency note and issue #2724. +jemalloc-allocator = ["dep:tikv-jemallocator"] fast-runtime = [ "node-subtensor-runtime/fast-runtime", "subtensor-runtime-common/fast-runtime", diff --git a/node/src/main.rs b/node/src/main.rs index a6aa15038f..a9145acd27 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -1,6 +1,14 @@ //! Substrate Node Subtensor CLI library. #![warn(missing_docs)] +// Use jemalloc as the global allocator when the `jemalloc-allocator` feature is enabled (off by +// default). Archive nodes serving sustained RPC load exhibit unbounded anonymous-memory growth that +// matches the glibc-malloc arena-fragmentation profile across the node's hundreds of threads; +// jemalloc (the allocator the polkadot binary ships with) avoids it. See issue #2724. +#[cfg(all(unix, feature = "jemalloc-allocator"))] +#[global_allocator] +static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; + #[cfg(feature = "runtime-benchmarks")] mod benchmarking; mod chain_spec; From 5ba46f9bbd0ab49cc211bf032c8a6493a223dcc1 Mon Sep 17 00:00:00 2001 From: Loom Agent Date: Sun, 12 Jul 2026 22:40:24 +0000 Subject: [PATCH 03/68] test(node): gate jemalloc-allocator feature with a build + activation test The off-by-default `jemalloc-allocator` feature is never compiled by the default build, so a `tikv-jemallocator` bump, a `#[global_allocator]` typo, or a `cfg` mistake would break the opt-in allocator path with nothing failing. Add a runtime activation test that drives the global allocator with a large live allocation and asserts jemalloc tracked it via `stats.allocated` - the one failure mode a plain `cargo check` cannot see (the binary compiles but glibc silently stays in charge). `tikv-jemalloc-ctl` (the canonical companion crate, same 0.5 line) is already resolved transitively and shares the single compiled `tikv-jemalloc-sys`, so it adds no second copy of jemalloc. Gate the feature explicitly in check-rust.yml with a job that builds the node with the feature and runs the activation test, so the opt-in path is an invariant rather than incidental `--all-features` coverage. --- .github/workflows/check-rust.yml | 33 +++++++++++++++++ node/Cargo.toml | 8 ++++- node/src/main.rs | 62 ++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-rust.yml b/.github/workflows/check-rust.yml index 4fb0863d70..d6c4f37658 100644 --- a/.github/workflows/check-rust.yml +++ b/.github/workflows/check-rust.yml @@ -173,6 +173,39 @@ jobs: - name: cargo test --doc --workspace --all-features run: cargo test --doc --workspace --all-features + # The `jemalloc-allocator` feature is off by default, so the default build + # never compiles it. Without an explicit gate it rots silently: a + # `tikv-jemallocator` bump, a `#[global_allocator]` typo, or a `cfg` mistake + # would break the opt-in allocator path with nothing failing. This job + # compiles+links the node with the feature (build gate) and runs the + # activation test proving jemalloc is the *live* global allocator, not merely + # linked - the failure mode a plain `cargo check` cannot see. + jemalloc-feature: + name: jemalloc-allocator feature gate + needs: [trusted-pr, changes] + if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' + runs-on: [self-hosted, fireactions-turbo-8] + environment: + name: ${{ github.event_name == 'workflow_dispatch' && 'sccache-reader' || 'sccache-writer' }} + deployment: false + env: + SKIP_WASM_BUILD: 1 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/rust-setup + with: + cache-key: jemalloc-feature + sccache-credential-mode: auto + sccache-writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} + sccache-writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} + - name: Build gate (node compiles + links the feature) + run: cargo check -p node-subtensor --features jemalloc-allocator --all-targets + # No test-name filter: a filtered `cargo test` exits 0 if the test is ever + # renamed (0 run), so the gate would pass silently. Running the node's + # full test target with the feature guarantees the activation test runs. + - name: Activation test (jemalloc is the live global allocator) + run: cargo test -p node-subtensor --features jemalloc-allocator + # The browser seam: bittensor-core without its `host` feature must keep # compiling for wasm32 (the future wasm-bindgen/TS binding target). This # check is what keeps "browser-compatible" an invariant instead of an diff --git a/node/Cargo.toml b/node/Cargo.toml index 5d2e161f20..da20b81641 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -138,6 +138,12 @@ subtensor-macros.workspace = true # polkadot binary does) is the proposed mitigation (issue #2724). Gated behind a feature so the # default release binary is unchanged until an operator opts in. tikv-jemallocator = { version = "0.5", optional = true, default-features = false } +# Companion to `tikv-jemallocator`, used by the `jemalloc-allocator` feature's +# activation test to read allocator stats over `mallctl`. It already resolves +# transitively and shares the single compiled `tikv-jemalloc-sys` with +# `tikv-jemallocator` (see Cargo.lock), so enabling it adds no second copy of +# jemalloc - only this small ctl FFI shim. +tikv-jemalloc-ctl = { version = "0.5", optional = true } [build-dependencies] substrate-build-script-utils.workspace = true @@ -154,7 +160,7 @@ rocksdb = [ default = ["rocksdb", "sql", "txpool"] # Use tikv-jemallocator as the global allocator in the release binary (off by default). # See the `tikv-jemallocator` dependency note and issue #2724. -jemalloc-allocator = ["dep:tikv-jemallocator"] +jemalloc-allocator = ["dep:tikv-jemallocator", "dep:tikv-jemalloc-ctl"] fast-runtime = [ "node-subtensor-runtime/fast-runtime", "subtensor-runtime-common/fast-runtime", diff --git a/node/src/main.rs b/node/src/main.rs index a9145acd27..179843061d 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -26,3 +26,65 @@ mod service; fn main() -> sc_cli::Result<()> { command::run() } + +// Regression guard for the optional `jemalloc-allocator` feature. +// +// The feature is off by default, so the default build never compiles it, and a +// `tikv-jemallocator` bump, a `#[global_allocator]` typo, or a `cfg` mistake can +// leave the binary *compiling* while jemalloc is no longer the active global +// allocator (glibc silently stays in charge). A compile check cannot catch that: +// `stats.allocated` only counts bytes routed through jemalloc, so this test +// drives the global allocator with a large live allocation and asserts jemalloc +// tracked it. If the `#[global_allocator]`/`cfg` wiring is broken the Vec goes +// to glibc and the delta is just a few bytes of `mallctl` noise - well below the +// threshold - and the test fails. +#[cfg(all(test, unix, feature = "jemalloc-allocator"))] +mod jemalloc_allocator_feature { + use tikv_jemalloc_ctl::{epoch, stats}; + + // A live allocation routed through the *global* allocator. Large enough to + // dwarf any bookkeeping the `mallctl` calls do themselves. + const PAYLOAD: usize = 16 * 1024 * 1024; + + // The workspace denies `expect_used`/`unwrap_used`, and `tikv_jemalloc_ctl`'s + // Error is a transparent wrapper that does not implement std::error::Error, + // so the Result cannot be propagated with `?` either. Unwrap the two stat + // helpers by hand instead. + fn advance_epoch() { + if let Err(error) = epoch::advance() { + panic!("advance jemalloc epoch: {error:?}"); + } + } + + fn allocated_bytes() -> usize { + match stats::allocated::read() { + Ok(bytes) => bytes, + Err(error) => panic!("read stats.allocated: {error:?}"), + } + } + + #[test] + fn jemalloc_is_the_active_global_allocator() { + // jemalloc caches stats; advance the epoch to refresh them before each read. + advance_epoch(); + let before = allocated_bytes(); + + // Held across the second read so jemalloc cannot reclaim it first. + let hold = vec![0u8; PAYLOAD]; + + advance_epoch(); + let after = allocated_bytes(); + + assert!( + after.saturating_sub(before) > PAYLOAD / 2, + "jemalloc did not track a {}-byte global-allocator allocation \ + (before={}, after={}): it is linked but not the active global \ + allocator, so the jemalloc-allocator feature wiring is broken", + PAYLOAD, + before, + after, + ); + + drop(hold); + } +} From 8de570c151bf382f924f3103d093368479067cda Mon Sep 17 00:00:00 2001 From: Loom Agent Date: Sun, 12 Jul 2026 23:14:30 +0000 Subject: [PATCH 04/68] test(node): isolate jemalloc activation test with thread-local stats The activation test read jemalloc's process-global `stats::allocated` before and after a 16 MiB allocation. `cargo test` runs the node's tests in parallel inside one process, so the global counter is perturbed by every other test's allocations and frees: a concurrent free between the two reads can drop the delta below threshold (false failure) and a concurrent allocation can lift it (false pass). Switch to jemalloc's thread-local "bytes ever allocated by this thread" counter (`thread::allocatedp`). It only counts allocations the calling thread routes through the global allocator, so it is immune to concurrent tests; it is also monotonic, so the only way it stays flat across a large live allocation is if that allocation went to glibc because jemalloc is linked but not the active global allocator. Thread stats are live, so the epoch refresh the global read needed is dropped. --- node/src/main.rs | 58 ++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/node/src/main.rs b/node/src/main.rs index 179843061d..cd23935b24 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -33,50 +33,56 @@ fn main() -> sc_cli::Result<()> { // `tikv-jemallocator` bump, a `#[global_allocator]` typo, or a `cfg` mistake can // leave the binary *compiling* while jemalloc is no longer the active global // allocator (glibc silently stays in charge). A compile check cannot catch that: -// `stats.allocated` only counts bytes routed through jemalloc, so this test -// drives the global allocator with a large live allocation and asserts jemalloc -// tracked it. If the `#[global_allocator]`/`cfg` wiring is broken the Vec goes -// to glibc and the delta is just a few bytes of `mallctl` noise - well below the -// threshold - and the test fails. +// only bytes routed *through* jemalloc are observable, so this test drives the +// global allocator with a large live allocation and asserts jemalloc tracked it. +// If the `#[global_allocator]`/`cfg` wiring is broken the Vec goes to glibc and +// the delta stays at zero - well below the threshold - and the test fails. +// +// The delta is read from jemalloc's *thread-local* "bytes ever allocated by this +// thread" counter, not the process-global `stats.allocated`. `cargo test` runs the +// node's tests in parallel inside one process, and the global stat is perturbed by +// every other test's allocations and frees, so a before/after delta around it is +// racy: a concurrent free can drop it below threshold (false failure) and a +// concurrent allocation can lift it (false pass). The thread-local counter only +// ever counts bytes the *calling* thread routes through the global allocator, so +// it is immune to concurrent tests; it is also monotonic (a free cannot decrease +// it), so the only way it stays flat across a large live allocation is if that +// allocation went to glibc because jemalloc is linked but not active. #[cfg(all(test, unix, feature = "jemalloc-allocator"))] mod jemalloc_allocator_feature { - use tikv_jemalloc_ctl::{epoch, stats}; + use tikv_jemalloc_ctl::thread; // A live allocation routed through the *global* allocator. Large enough to - // dwarf any bookkeeping the `mallctl` calls do themselves. + // dwarf any bookkeeping the stat reads do themselves. const PAYLOAD: usize = 16 * 1024 * 1024; // The workspace denies `expect_used`/`unwrap_used`, and `tikv_jemalloc_ctl`'s // Error is a transparent wrapper that does not implement std::error::Error, - // so the Result cannot be propagated with `?` either. Unwrap the two stat - // helpers by hand instead. - fn advance_epoch() { - if let Err(error) = epoch::advance() { - panic!("advance jemalloc epoch: {error:?}"); - } - } - - fn allocated_bytes() -> usize { - match stats::allocated::read() { - Ok(bytes) => bytes, - Err(error) => panic!("read stats.allocated: {error:?}"), + // so the Result cannot be propagated with `?` either. Resolve the stat handle + // by hand instead. + fn allocated_pointer() -> thread::ThreadLocal { + match thread::allocatedp::read() { + Ok(pointer) => pointer, + Err(error) => panic!("read thread.allocatedp: {error:?}"), } } #[test] fn jemalloc_is_the_active_global_allocator() { - // jemalloc caches stats; advance the epoch to refresh them before each read. - advance_epoch(); - let before = allocated_bytes(); + // `read()` returns a thread-local pointer to the cumulative byte count + // for *this* thread; `get()` dereferences it (safe: the raw-pointer read + // is encapsulated in `tikv_jemalloc_ctl`). Thread stats are live, so - + // unlike `stats::allocated` - no epoch refresh is needed before a read. + let pointer = allocated_pointer(); + let before = pointer.get(); - // Held across the second read so jemalloc cannot reclaim it first. + // Held across the second read so the allocator cannot reclaim it first. let hold = vec![0u8; PAYLOAD]; - advance_epoch(); - let after = allocated_bytes(); + let after = pointer.get(); assert!( - after.saturating_sub(before) > PAYLOAD / 2, + after.saturating_sub(before) >= PAYLOAD as u64, "jemalloc did not track a {}-byte global-allocator allocation \ (before={}, after={}): it is linked but not the active global \ allocator, so the jemalloc-allocator feature wiring is broken", From f765f7d5acbd898c78f93fbbbb18b737932e93f9 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 08:05:28 -0600 Subject: [PATCH 05/68] fix: regen calls.py for doc typo fix, allow deprecated aliases in migration test The doc comment typo fix propagates into runtime metadata, so the generated Python calls.py must be regenerated to keep the metadata drift gate green. Also silence clippy::deprecated on the LastTxBlockDelegateTake migration test, which intentionally exercises the deprecated storage aliases. Co-authored-by: Cursor --- pallets/subtensor/src/tests/migration.rs | 1 + sdk/python/bittensor/_generated/calls.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 8e81447fbc..85eb1290c8 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -1253,6 +1253,7 @@ fn test_per_u16_encodes_identically_to_u16() { assert_eq!(PerU16::zero().encode(), 0u16.encode()); } +#[allow(deprecated)] #[test] fn test_migrate_last_tx_block_delegate_take() { new_test_ext(1).execute_with(|| { diff --git a/sdk/python/bittensor/_generated/calls.py b/sdk/python/bittensor/_generated/calls.py index 8d1ab2e64f..31084b8998 100644 --- a/sdk/python/bittensor/_generated/calls.py +++ b/sdk/python/bittensor/_generated/calls.py @@ -212,7 +212,7 @@ def add_stake_burn(hotkey: 'AccountId32', netuid: 'NetUid', amount: 'TaoBalance' @staticmethod def add_stake_limit(hotkey: 'AccountId32', netuid: 'NetUid', amount_staked: 'TaoBalance', limit_price: 'TaoBalance', allow_partial: 'bool') -> Call: - "Adds stake to a hotkey on a subnet with a price limit. This extrinsic allows to specify the limit price for alpha token at which or better (lower) the staking should execute. In case if slippage occurs and the price shall move beyond the limit price, the staking order may execute only partially or not execute at all. # Arguments * `origin`: The signature of the caller's coldkey. * `hotkey`: The associated hotkey account. * `netuid`: Subnetwork UID. * `amount_staked`: The amount of stake to be added to the hotkey staking account. * `limit_price`: The limit price expressed in units of RAO per one Alpha. * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes fill or kill type or order. # Events * `StakeAdded`: On the successfully adding stake to a global account. # Errors * `NotEnoughBalanceToStake`: Not enough balance on the coldkey to add onto the global account. * `NonAssociatedColdKey`: The calling coldkey is not associated with this hotkey. * `BalanceWithdrawalError`: Errors stemming from transaction pallet." + "Adds stake to a hotkey on a subnet with a price limit. This extrinsic allows to specify the limit price for alpha token at which or better (lower) the staking should execute. In case if slippage occurs and the price shall move beyond the limit price, the staking order may execute only partially or not execute at all. # Arguments * `origin`: The signature of the caller's coldkey. * `hotkey`: The associated hotkey account. * `netuid`: Subnetwork UID. * `amount_staked`: The amount of stake to be added to the hotkey staking account. * `limit_price`: The limit price expressed in units of RAO per one Alpha. * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes fill or kill type of order. # Events * `StakeAdded`: On the successfully adding stake to a global account. # Errors * `NotEnoughBalanceToStake`: Not enough balance on the coldkey to add onto the global account. * `NonAssociatedColdKey`: The calling coldkey is not associated with this hotkey. * `BalanceWithdrawalError`: Errors stemming from transaction pallet." return Call('SubtensorModule', 'add_stake_limit', {'hotkey': hotkey, 'netuid': netuid, 'amount_staked': amount_staked, 'limit_price': limit_price, 'allow_partial': allow_partial}) @staticmethod @@ -372,7 +372,7 @@ def remove_stake_full_limit(hotkey: 'AccountId32', netuid: 'NetUid', limit_price @staticmethod def remove_stake_limit(hotkey: 'AccountId32', netuid: 'NetUid', amount_unstaked: 'AlphaBalance', limit_price: 'TaoBalance', allow_partial: 'bool') -> Call: - "Removes stake from a hotkey on a subnet with a price limit. This extrinsic allows to specify the limit price for alpha token at which or better (higher) the staking should execute. In case if slippage occurs and the price shall move beyond the limit price, the staking order may execute only partially or not execute at all. # Arguments * `origin`: The signature of the caller's coldkey. * `hotkey`: The associated hotkey account. * `netuid`: Subnetwork UID. * `amount_unstaked`: The amount of stake to be added to the hotkey staking account. * `limit_price`: The limit price expressed in units of RAO per one Alpha. * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes fill or kill type or order. # Events * `StakeRemoved`: On the successfully removing stake from the hotkey account. # Errors * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdwraw this amount." + "Removes stake from a hotkey on a subnet with a price limit. This extrinsic allows to specify the limit price for alpha token at which or better (higher) the staking should execute. In case if slippage occurs and the price shall move beyond the limit price, the staking order may execute only partially or not execute at all. # Arguments * `origin`: The signature of the caller's coldkey. * `hotkey`: The associated hotkey account. * `netuid`: Subnetwork UID. * `amount_unstaked`: The amount of stake to be added to the hotkey staking account. * `limit_price`: The limit price expressed in units of RAO per one Alpha. * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes fill or kill type of order. # Events * `StakeRemoved`: On the successfully removing stake from the hotkey account. # Errors * `NotRegistered`: Thrown if the account we are attempting to unstake from is non existent. * `NonAssociatedColdKey`: Thrown if the coldkey does not own the hotkey we are unstaking from. * `NotEnoughStakeToWithdraw`: Thrown if there is not enough stake on the hotkey to withdwraw this amount." return Call('SubtensorModule', 'remove_stake_limit', {'hotkey': hotkey, 'netuid': netuid, 'amount_unstaked': amount_unstaked, 'limit_price': limit_price, 'allow_partial': allow_partial}) @staticmethod @@ -552,7 +552,7 @@ def swap_stake(hotkey: 'AccountId32', origin_netuid: 'NetUid', destination_netui @staticmethod def swap_stake_limit(hotkey: 'AccountId32', origin_netuid: 'NetUid', destination_netuid: 'NetUid', alpha_amount: 'AlphaBalance', limit_price: 'TaoBalance', allow_partial: 'bool') -> Call: - 'Swaps a specified amount of stake from one subnet to another, while keeping the same coldkey and hotkey. # Arguments * `origin`: The origin of the transaction, which must be signed by the coldkey that owns the `hotkey`. * `hotkey`: The hotkey whose stake is being swapped. * `origin_netuid`: The network/subnet ID from which stake is removed. * `destination_netuid`: The network/subnet ID to which stake is added. * `alpha_amount`: The amount of stake to swap. * `limit_price`: The limit price expressed in units of RAO per one Alpha. * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes fill or kill type or order. # Errors * `BadOrigin`: The transaction is not signed. * `SameNetuid`: `origin_netuid` and `destination_netuid` are the same. * `SubnetNotExists`: Either `origin_netuid` or `destination_netuid` does not exist. * `SubtokenDisabled`: The subtoken is disabled on the origin or destination subnet. * `HotKeyAccountNotExists`: The `hotkey` account does not exist. * `NotEnoughStakeToWithdraw`: The `(coldkey, hotkey, origin_netuid)` position has less stake than `alpha_amount`. * `InsufficientLiquidity`: The swap simulation on the origin subnet fails. * `AmountTooLow`: The TAO-equivalent of the swap is below the minimum stake requirement. * `SlippageTooHigh`: `allow_partial` is false and the amount would cross the limit price. * `StakeUnavailable`: The remaining stake would not cover the locked amount on the origin subnet. # Events May emit a `StakeSwapped` event on success.' + 'Swaps a specified amount of stake from one subnet to another, while keeping the same coldkey and hotkey. # Arguments * `origin`: The origin of the transaction, which must be signed by the coldkey that owns the `hotkey`. * `hotkey`: The hotkey whose stake is being swapped. * `origin_netuid`: The network/subnet ID from which stake is removed. * `destination_netuid`: The network/subnet ID to which stake is added. * `alpha_amount`: The amount of stake to swap. * `limit_price`: The limit price expressed in units of RAO per one Alpha. * `allow_partial`: Allows partial execution of the amount. If set to false, this becomes fill or kill type of order. # Errors * `BadOrigin`: The transaction is not signed. * `SameNetuid`: `origin_netuid` and `destination_netuid` are the same. * `SubnetNotExists`: Either `origin_netuid` or `destination_netuid` does not exist. * `SubtokenDisabled`: The subtoken is disabled on the origin or destination subnet. * `HotKeyAccountNotExists`: The `hotkey` account does not exist. * `NotEnoughStakeToWithdraw`: The `(coldkey, hotkey, origin_netuid)` position has less stake than `alpha_amount`. * `InsufficientLiquidity`: The swap simulation on the origin subnet fails. * `AmountTooLow`: The TAO-equivalent of the swap is below the minimum stake requirement. * `SlippageTooHigh`: `allow_partial` is false and the amount would cross the limit price. * `StakeUnavailable`: The remaining stake would not cover the locked amount on the origin subnet. # Events May emit a `StakeSwapped` event on success.' return Call('SubtensorModule', 'swap_stake_limit', {'hotkey': hotkey, 'origin_netuid': origin_netuid, 'destination_netuid': destination_netuid, 'alpha_amount': alpha_amount, 'limit_price': limit_price, 'allow_partial': allow_partial}) @staticmethod From f82018afcd40f3c018a5fa8a2601784bd9027879 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 08:19:20 -0600 Subject: [PATCH 06/68] ci: retry docker pull in rust-e2e jobs to ride out transient GHCR denials With ~120 parallel e2e jobs pulling the same localnet image, GHCR occasionally returns a spurious permission_denied. Retry with backoff instead of failing the shard on the first attempt. Co-authored-by: Cursor --- .github/workflows/check-bittensor-e2e-tests.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-bittensor-e2e-tests.yml b/.github/workflows/check-bittensor-e2e-tests.yml index f720171683..c3c475f4af 100644 --- a/.github/workflows/check-bittensor-e2e-tests.yml +++ b/.github/workflows/check-bittensor-e2e-tests.yml @@ -347,7 +347,15 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Pull Docker Image - run: docker pull "$LOCALNET_IMAGE_CI" + # GHCR intermittently returns permission_denied/timeouts under the + # ~120-job pull fan-out, so retry with backoff instead of failing the shard. + run: | + for i in 1 2 3; do + docker pull "$LOCALNET_IMAGE_CI" && exit 0 + echo "docker pull failed (attempt $i), retrying in $((i * 15))s..." + sleep $((i * 15)) + done + docker pull "$LOCALNET_IMAGE_CI" - name: Retag Docker Image run: docker tag "$LOCALNET_IMAGE_CI" "$LOCALNET_IMAGE" From 70564c2bdd8ef93536ac899ea1e233dc9e1e1d1a Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 08:20:13 -0600 Subject: [PATCH 07/68] ci: idempotent PyPI publish re-runs; fix stale branch-strategy review docs - uv publish --check-url: the rc publish uploaded the SDK wheel and then failed on bittensor-core (missing trusted publisher); a plain re-run would 400 on the duplicate wheel. --check-url skips files already on the index so re-runs after partial uploads just resume. - copilot-instructions.md still described the pre-release-train branch strategy (PRs target devnet); the auditor cited it to block a routine PR against main. Align it with common.md: PRs target main, the network branches are ruleset-locked CI mirrors. Co-authored-by: Cursor --- .github/copilot-instructions.md | 6 +++--- .github/workflows/release-train.yml | 6 +++++- .github/workflows/watch-mainnet-release.yml | 6 +++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0383c6b2e1..f5f4126cf6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,9 +3,9 @@ You are reviewing code for a Substrate-based blockchain with a $4B market cap. Lives and livelihoods depend on the security and correctness of this code. Be thorough, precise, and uncompromising on safety. ## Branch Strategy -* Unless this is a hotfix or deployment PR (`devnet` => `testnet`, or `testnet` => `mainnet`), all PRs must target `devnet` -* Flag PRs targeting `mainnet` directly unless they are hotfixes -* `testnet` and `mainnet` branches must only receive promotion merges from the branch directly upstream of them +* All PRs target `main`. Deployment is automated, not PR-driven: merges to `main` ride the release train, which promotes devnet → testnet → mainnet via on-chain `setCode` (see `docs/internals/release-process.mdx`) +* `devnet`, `testnet`, and `mainnet` are CI-managed mirror branches recording what each network currently runs; they are ruleset-locked and only the release train updates them +* Flag any PR that targets `devnet`, `testnet`, or `mainnet` — those branches never receive merges ## CRITICAL: Runtime Safety (Chain-Bricking Prevention) The runtime CANNOT panic under any circumstances. A single panic can brick the entire chain. diff --git a/.github/workflows/release-train.yml b/.github/workflows/release-train.yml index 7869eb3bf5..05871a8d67 100644 --- a/.github/workflows/release-train.yml +++ b/.github/workflows/release-train.yml @@ -360,10 +360,14 @@ jobs: - name: Build SDK wheel and sdist working-directory: sdk/python run: uv build --out-dir ../../dist + # --check-url makes a re-run after a partial upload idempotent: files + # already on the index are skipped instead of failing with a 400 + # duplicate (e.g. the SDK wheel landed but bittensor-core didn't). - name: Publish to PyPI run: | ls -la dist - uv publish --trusted-publishing always dist/* + uv publish --trusted-publishing always \ + --check-url https://pypi.org/simple/ dist/* # ------------------------------------------------------------------ # Stage 3: mainnet — the human gate. diff --git a/.github/workflows/watch-mainnet-release.yml b/.github/workflows/watch-mainnet-release.yml index 28d2cce2e8..5e9fe9f96e 100644 --- a/.github/workflows/watch-mainnet-release.yml +++ b/.github/workflows/watch-mainnet-release.yml @@ -221,10 +221,14 @@ jobs: working-directory: sdk/python run: uv build --out-dir ../../dist + # --check-url makes a re-run after a partial upload idempotent: files + # already on the index are skipped instead of failing with a 400 + # duplicate (e.g. the SDK wheel landed but bittensor-core didn't). - name: Publish to PyPI run: | ls -la dist - uv publish --trusted-publishing always dist/* + uv publish --trusted-publishing always \ + --check-url https://pypi.org/simple/ dist/* publish-crates: name: Publish Rust crates to crates.io From 098dd84ffda39cf99355a42a0f5387f4e22fbc96 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Sun, 12 Jul 2026 22:32:17 +0200 Subject: [PATCH 08/68] ci: clean up snapshot cache controls --- .../workflows/refresh-mainnet-snapshot.yml | 4 +- .github/workflows/runtime-checks.yml | 43 ++++++------------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/.github/workflows/refresh-mainnet-snapshot.yml b/.github/workflows/refresh-mainnet-snapshot.yml index 0e48e72a7a..79ee374a3e 100644 --- a/.github/workflows/refresh-mainnet-snapshot.yml +++ b/.github/workflows/refresh-mainnet-snapshot.yml @@ -32,7 +32,9 @@ permissions: contents: read concurrency: - group: refresh-mainnet-snapshot + # A test-branch dispatch must not cancel the production refresh on main (or + # vice versa). Schedule and dispatch on the same ref still supersede safely. + group: refresh-mainnet-snapshot-${{ github.ref }} cancel-in-progress: true env: diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 33038f1d35..335f48ae9e 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -16,18 +16,10 @@ on: description: "Bypass cached try-runtime snapshots and scrape all networks live" type: boolean default: false - snapshot_source_branch: - description: "Trusted snapshot artifact branch (manual measurement only)" - type: string - default: main try_runtime_only: description: "Run only the try-runtime build and cached network checks" type: boolean default: false - skip_devnet: - description: "Skip devnet when measuring a partially completed producer run" - type: boolean - default: false concurrency: group: runtime-checks-${{ github.ref }} @@ -79,7 +71,7 @@ jobs: files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename') # SDK-only changes are covered by sdk-checks and the Rust SDK e2e # workflow; they should not force clone-upgrade or SDK drift. - runtime_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor|clones)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^website/apps/bittensor-website/scripts/|^\.github/(workflows/(runtime-checks|refresh-mainnet-snapshot)\.yml|actions/(rust-setup|sccache-setup)/|scripts/(sccache-configure|snapshot-artifact|test-snapshot-artifact)\.sh$)' + runtime_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor|clones)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^website/apps/bittensor-website/scripts/|^\.github/(workflows/(runtime-checks|refresh-mainnet-snapshot)\.yml|actions/(rust-setup|sccache-setup)/|scripts/(sccache-configure|snapshot-artifact|test-snapshot-artifact)\.sh)$' docs_pattern='^website/|^sdk/python/|^\.github/workflows/runtime-checks\.yml$' python_sdk_pattern='^sdk/(python|bittensor-core|bittensor-core-py|bittensor-core-wasm)/|^Cargo.lock$|^\.github/workflows/runtime-checks\.yml$' sdk_drift_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$' @@ -225,33 +217,24 @@ jobs: strategy: fail-fast: false matrix: - selected: [devnet, testnet, mainnet] - exclude: - - selected: ${{ github.event_name == 'workflow_dispatch' && inputs.skip_devnet && 'devnet' || '__none__' }} - include: + network: # NOTE: the hostnames are misleading — archive.dev.opentensor.ai # serves the TESTNET archive on :8443 and the MAINNET archive on # :443 (verified by genesis hash; try-runtime wants archive nodes, # which is why we don't use the entrypoint hostnames). The # "Verify endpoint" step below enforces each URI's identity. - - selected: devnet - network: - name: devnet - uri: wss://dev.chain.opentensor.ai:443 - genesis: "0x077899043eb684c5277b6814a39161f4ce072b45e782e12c81a521c63fb4f3e5" - - selected: testnet - network: - name: testnet - uri: wss://archive.dev.opentensor.ai:8443 - genesis: "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105" - - selected: mainnet - network: - name: mainnet - uri: wss://archive.dev.opentensor.ai:443 - genesis: "0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03" + - name: devnet + uri: wss://dev.chain.opentensor.ai:443 + genesis: "0x077899043eb684c5277b6814a39161f4ce072b45e782e12c81a521c63fb4f3e5" + - name: testnet + uri: wss://archive.dev.opentensor.ai:8443 + genesis: "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105" # Keep endpoint-heavy checks near the archive RPCs. Measurements # from BHS showed materially higher per-request latency, which # compounds during try-runtime's state traversal. + - name: mainnet + uri: wss://archive.dev.opentensor.ai:443 + genesis: "0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03" steps: - uses: actions/checkout@v4 @@ -309,7 +292,7 @@ jobs: set -euo pipefail .github/scripts/snapshot-artifact.sh select \ "try-runtime-snap-v${TRY_RUNTIME_VERSION}-${{ matrix.network.name }}" \ - "${{ github.event_name == 'workflow_dispatch' && inputs.snapshot_source_branch || github.event.repository.default_branch }}" \ + "${{ github.event.repository.default_branch }}" \ "${{ github.repository_id }}" \ 72 "$GITHUB_OUTPUT" required @@ -528,7 +511,7 @@ jobs: set -euo pipefail .github/scripts/snapshot-artifact.sh select \ mainnet-snapshot \ - "${{ github.event_name == 'workflow_dispatch' && inputs.snapshot_source_branch || github.event.repository.default_branch }}" \ + "${{ github.event.repository.default_branch }}" \ "${{ github.repository_id }}" \ 168 "$GITHUB_OUTPUT" optional From b2311f23b5cea8209565aa36e1afa2a33317dfd4 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Sun, 12 Jul 2026 22:49:09 +0200 Subject: [PATCH 09/68] ci: move scheduled smoke checks to hosted runners --- .github/workflows/sccache-warm.yml | 5 ++--- .github/workflows/scheduled-smoke-tests.yml | 9 ++++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sccache-warm.yml b/.github/workflows/sccache-warm.yml index a30f0cc3e7..ccce428d32 100644 --- a/.github/workflows/sccache-warm.yml +++ b/.github/workflows/sccache-warm.yml @@ -2,7 +2,7 @@ name: Warm R2 sccache # Normal same-repository PR and main CI writes through while it compiles. This # workflow is maintenance only: refresh deployed-state branches when they move, -# repair expired/missing main entries at Amsterdam noon, or recover manually. +# repair expired/missing main entries at 10:00 UTC, or recover manually. on: push: @@ -10,8 +10,7 @@ on: - devnet - testnet schedule: - - cron: "0 12 * * *" - timezone: "Europe/Amsterdam" + - cron: "0 10 * * *" workflow_dispatch: inputs: source_ref: diff --git a/.github/workflows/scheduled-smoke-tests.yml b/.github/workflows/scheduled-smoke-tests.yml index 7f436bb8d2..a5dbe18333 100644 --- a/.github/workflows/scheduled-smoke-tests.yml +++ b/.github/workflows/scheduled-smoke-tests.yml @@ -1,12 +1,12 @@ name: Scheduled Smoke Tests -# Read-only smoke suites against the live networks, every 6 hours. The same +# Read-only smoke suites against the live networks, daily at 10:00 UTC. The same # suites gate promotion in release-train.yml; this catches drift between # trains. on: schedule: - - cron: "0 */6 * * *" + - cron: "0 10 * * *" workflow_dispatch: permissions: @@ -15,7 +15,10 @@ permissions: jobs: smoke: name: smoke-e2e-${{ matrix.test }} - runs-on: [self-hosted, fireactions-tryruntime] + # Periodic monitoring must not compete with state-heavy PR checks for the + # small try-runtime runner pool. This public repository can use standard + # GitHub-hosted Linux runners for these read-only public-RPC suites. + runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false From b2ebcf1303e490c6fbc6144d73d67cdea31884e5 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Sun, 12 Jul 2026 23:09:10 +0200 Subject: [PATCH 10/68] ci: increase snapshot RPC concurrency --- .github/workflows/refresh-mainnet-snapshot.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/refresh-mainnet-snapshot.yml b/.github/workflows/refresh-mainnet-snapshot.yml index 79ee374a3e..822d765b71 100644 --- a/.github/workflows/refresh-mainnet-snapshot.yml +++ b/.github/workflows/refresh-mainnet-snapshot.yml @@ -199,9 +199,13 @@ jobs: snapshot="${{ matrix.network.name }}.snap" manifest="${{ matrix.network.name }}.manifest.json" # The positional path must precede --uri because --uri is variadic. + # remote-externalities creates four value-fetch workers per URI. The + # archive endpoint is dedicated to this workload, so open four + # independent clients to it (16 value-fetch workers in total). + rpc_uri="${{ matrix.network.uri }}" "$TRY_RUNTIME_BIN" create-snapshot \ "$snapshot" \ - --uri "${{ matrix.network.uri }}" \ + --uri "$rpc_uri" "$rpc_uri" "$rpc_uri" "$rpc_uri" \ --at "${{ steps.source.outputs.block-hash }}" snapshot_size=$(stat -c '%s' "$snapshot") From a51cec7ec901550b4ed7fb9ed9f7be3cea297819 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Sun, 12 Jul 2026 23:57:40 +0200 Subject: [PATCH 11/68] ci: speed up cached snapshot downloads --- .github/workflows/runtime-checks.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 335f48ae9e..6c7f49a32c 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -302,13 +302,14 @@ jobs: - name: Download selected try-runtime state snapshot if: steps.state-mode.outputs.fresh-state != 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - .github/scripts/snapshot-artifact.sh download \ - "${{ steps.snapshot.outputs.artifact-id }}" \ - "${{ steps.snapshot.outputs.digest }}" \ - snap + uses: actions/download-artifact@v4 + with: + artifact-ids: ${{ steps.snapshot.outputs.artifact-id }} + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ steps.snapshot.outputs.run-id }} + path: snap + merge-multiple: true - name: Validate try-runtime state snapshot if: steps.state-mode.outputs.fresh-state != 'true' From 033e48ba471fd836a609b2d01c7b20534aaf26fb Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 00:05:54 +0200 Subject: [PATCH 12/68] revert: retain faster snapshot download path --- .github/workflows/runtime-checks.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 6c7f49a32c..335f48ae9e 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -302,14 +302,13 @@ jobs: - name: Download selected try-runtime state snapshot if: steps.state-mode.outputs.fresh-state != 'true' - uses: actions/download-artifact@v4 - with: - artifact-ids: ${{ steps.snapshot.outputs.artifact-id }} - github-token: ${{ github.token }} - repository: ${{ github.repository }} - run-id: ${{ steps.snapshot.outputs.run-id }} - path: snap - merge-multiple: true + env: + GH_TOKEN: ${{ github.token }} + run: | + .github/scripts/snapshot-artifact.sh download \ + "${{ steps.snapshot.outputs.artifact-id }}" \ + "${{ steps.snapshot.outputs.digest }}" \ + snap - name: Validate try-runtime state snapshot if: steps.state-mode.outputs.fresh-state != 'true' From 62868fee5ae288b1a3a58ef51d60f2c2738f7a2e Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 00:16:02 +0200 Subject: [PATCH 13/68] ci: benchmark parallel snapshot downloads --- .../scripts/benchmark-artifact-download.sh | 133 ++++++++++++++++++ .../workflows/artifact-download-benchmark.yml | 116 +++++++++++++++ 2 files changed, 249 insertions(+) create mode 100755 .github/scripts/benchmark-artifact-download.sh create mode 100644 .github/workflows/artifact-download-benchmark.yml diff --git a/.github/scripts/benchmark-artifact-download.sh b/.github/scripts/benchmark-artifact-download.sh new file mode 100755 index 0000000000..df1eb6d548 --- /dev/null +++ b/.github/scripts/benchmark-artifact-download.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 8 ]]; then + echo "usage: $0 ARTIFACT_ID DIGEST SIZE_BYTES CONCURRENCY DESTINATION NETWORK GENESIS CLI_VERSION" >&2 + exit 2 +fi + +artifact_id="$1" +expected_digest="$2" +size_bytes="$3" +concurrency="$4" +destination="$5" +network="$6" +genesis="$7" +cli_version="$8" + +[[ "$artifact_id" =~ ^[0-9]+$ ]] || { echo "invalid artifact id" >&2; exit 2; } +[[ "$expected_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "invalid artifact digest" >&2; exit 2; } +[[ "$size_bytes" =~ ^[1-9][0-9]*$ ]] || { echo "invalid artifact size" >&2; exit 2; } +[[ "$concurrency" =~ ^[1-9][0-9]*$ ]] || { echo "invalid concurrency" >&2; exit 2; } +: "${GH_TOKEN:?GH_TOKEN must be set}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" + +archive=$(mktemp) +parts_dir=$(mktemp -d) +trap 'rm -f "$archive"; rm -rf "$parts_dir"' EXIT + +api_url="https://api.github.com/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" + +get_download_url() { + local headers status url + headers=$(mktemp) + status=$(curl --disable --silent --show-error \ + --dump-header "$headers" \ + --output /dev/null \ + --max-redirs 0 \ + --header 'Accept: application/vnd.github+json' \ + --header "Authorization: Bearer $GH_TOKEN" \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + --write-out '%{http_code}' \ + "$api_url") + if [[ "$status" != 302 ]]; then + echo "artifact redirect returned HTTP $status" >&2 + rm -f "$headers" + return 1 + fi + url=$(awk 'BEGIN { IGNORECASE=1 } /^location:/ { sub(/^[^:]+:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit }' "$headers") + rm -f "$headers" + [[ "$url" == https://* ]] || { echo "artifact redirect did not contain an HTTPS URL" >&2; return 1; } + printf '%s' "$url" +} + +download_range() { + local start="$1" end="$2" part="$3" expected_size attempt url actual_size + expected_size=$((end - start + 1)) + for attempt in 1 2 3; do + url=$(get_download_url) || continue + if curl --disable --fail --silent --show-error \ + --range "$start-$end" \ + --output "$part" \ + "$url"; then + actual_size=$(stat -c '%s' "$part") + if [[ "$actual_size" == "$expected_size" ]]; then + return 0 + fi + echo "range $start-$end size mismatch: expected $expected_size, got $actual_size" >&2 + fi + rm -f "$part" + sleep "$attempt" + done + return 1 +} + +chunk_size=$(((size_bytes + concurrency - 1) / concurrency)) +overall_started=$(date +%s) +download_started=$(date +%s) +pids=() +parts=() +for ((worker = 0; worker < concurrency; worker++)); do + start=$((worker * chunk_size)) + ((start < size_bytes)) || break + end=$((start + chunk_size - 1)) + ((end < size_bytes)) || end=$((size_bytes - 1)) + printf -v part '%s/part-%03d' "$parts_dir" "$worker" + parts+=("$part") + download_range "$start" "$end" "$part" & + pids+=("$!") +done + +failed=0 +for pid in "${pids[@]}"; do + wait "$pid" || failed=1 +done +((failed == 0)) || { echo "one or more range downloads failed" >&2; exit 1; } +download_seconds=$(($(date +%s) - download_started)) + +assembly_started=$(date +%s) +for index in "${!parts[@]}"; do + command cat "${parts[$index]}" >> "$archive" +done +assembly_seconds=$(($(date +%s) - assembly_started)) + +actual_size=$(stat -c '%s' "$archive") +[[ "$actual_size" == "$size_bytes" ]] || { echo "archive size mismatch" >&2; exit 1; } + +verify_started=$(date +%s) +actual_digest="sha256:$(sha256sum "$archive" | awk '{print $1}')" +[[ "$actual_digest" == "$expected_digest" ]] || { + echo "artifact digest mismatch: expected $expected_digest, got $actual_digest" >&2 + exit 1 +} +verify_seconds=$(($(date +%s) - verify_started)) + +extract_started=$(date +%s) +mkdir -p "$destination" +unzip -q "$archive" -d "$destination" +extract_seconds=$(($(date +%s) - extract_started)) + +validation_started=$(date +%s) +.github/scripts/snapshot-artifact.sh validate \ + "$destination/$network.manifest.json" \ + "$destination/$network.snap" \ + "$network" "$genesis" "$cli_version" +validation_seconds=$(($(date +%s) - validation_started)) +total_seconds=$(($(date +%s) - overall_started)) +mbps=$(awk -v bytes="$size_bytes" -v seconds="$download_seconds" 'BEGIN { if (seconds == 0) seconds = 1; printf "%.1f", bytes * 8 / seconds / 1000000 }') +trial="${BENCHMARK_TRIAL:-unknown}" + +echo "artifact=$artifact_id trial=$trial concurrency=$concurrency download=${download_seconds}s assembly=${assembly_seconds}s archive_verify=${verify_seconds}s extract=${extract_seconds}s snapshot_validate=${validation_seconds}s total=${total_seconds}s throughput=${mbps}Mbps" +if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + echo "| $trial | $concurrency | $size_bytes | ${download_seconds}s | ${assembly_seconds}s | ${verify_seconds}s | ${extract_seconds}s | ${validation_seconds}s | ${total_seconds}s | ${mbps} Mbps |" >> "$GITHUB_STEP_SUMMARY" +fi diff --git a/.github/workflows/artifact-download-benchmark.yml b/.github/workflows/artifact-download-benchmark.yml new file mode 100644 index 0000000000..a1c474bc94 --- /dev/null +++ b/.github/workflows/artifact-download-benchmark.yml @@ -0,0 +1,116 @@ +name: Artifact Download Benchmark + +on: + push: + branches: + - codex/try-runtime-snapshot-cache-test + workflow_dispatch: + inputs: + network: + description: "Snapshot size to benchmark" + type: choice + default: testnet + options: + - testnet + - devnet + - mainnet + +permissions: + actions: read + contents: read + +concurrency: + group: artifact-download-benchmark-${{ github.ref }} + cancel-in-progress: true + +jobs: + resolve: + name: Resolve immutable artifact + runs-on: ubuntu-latest + outputs: + artifact-id: ${{ steps.snapshot.outputs.artifact-id }} + digest: ${{ steps.snapshot.outputs.digest }} + size-bytes: ${{ steps.snapshot.outputs.size-bytes }} + network: ${{ steps.identity.outputs.network }} + genesis: ${{ steps.identity.outputs.genesis }} + steps: + - uses: actions/checkout@v4 + - name: Resolve network identity + id: identity + env: + NETWORK: ${{ inputs.network || 'testnet' }} + run: | + case "$NETWORK" in + mainnet) genesis=0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03 ;; + testnet) genesis=0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105 ;; + devnet) genesis=0x077899043eb684c5277b6814a39161f4ce072b45e782e12c81a521c63fb4f3e5 ;; + *) echo "invalid network: $NETWORK" >&2; exit 2 ;; + esac + echo "network=$NETWORK" >> "$GITHUB_OUTPUT" + echo "genesis=$genesis" >> "$GITHUB_OUTPUT" + - name: Select trusted snapshot + id: snapshot + env: + GH_TOKEN: ${{ github.token }} + run: | + .github/scripts/snapshot-artifact.sh select \ + "try-runtime-snap-v0.10.1-${{ inputs.network || 'testnet' }}" \ + "${{ github.event.repository.default_branch }}" \ + "${{ github.repository_id }}" \ + 72 "$GITHUB_OUTPUT" required + + hosted: + name: hosted sequential range sweep + needs: resolve + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Benchmark download, verification, and extraction + env: + GH_TOKEN: ${{ github.token }} + run: | + echo '| Trial | Ranges | Bytes | Download | Assembly | Archive SHA | Extract | Snapshot SHA | Total | Throughput |' >> "$GITHUB_STEP_SUMMARY" + echo '|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|' >> "$GITHUB_STEP_SUMMARY" + trial=0 + for concurrency in 1 4 8 16 16 8 4 1; do + trial=$((trial + 1)) + destination="$RUNNER_TEMP/snapshot-$trial-$concurrency" + BENCHMARK_TRIAL="$trial" .github/scripts/benchmark-artifact-download.sh \ + "${{ needs.resolve.outputs.artifact-id }}" \ + "${{ needs.resolve.outputs.digest }}" \ + "${{ needs.resolve.outputs.size-bytes }}" \ + "$concurrency" "$destination" \ + "${{ needs.resolve.outputs.network }}" \ + "${{ needs.resolve.outputs.genesis }}" \ + "0.10.1" + rm -rf "$destination" + done + + self-hosted: + name: self-hosted sequential range sweep + needs: resolve + runs-on: [self-hosted, fireactions-tryruntime] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Benchmark download, verification, and extraction + env: + GH_TOKEN: ${{ github.token }} + run: | + echo '| Trial | Ranges | Bytes | Download | Assembly | Archive SHA | Extract | Snapshot SHA | Total | Throughput |' >> "$GITHUB_STEP_SUMMARY" + echo '|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|' >> "$GITHUB_STEP_SUMMARY" + trial=0 + for concurrency in 1 4 8 16 16 8 4 1; do + trial=$((trial + 1)) + destination="$RUNNER_TEMP/snapshot-$trial-$concurrency" + BENCHMARK_TRIAL="$trial" .github/scripts/benchmark-artifact-download.sh \ + "${{ needs.resolve.outputs.artifact-id }}" \ + "${{ needs.resolve.outputs.digest }}" \ + "${{ needs.resolve.outputs.size-bytes }}" \ + "$concurrency" "$destination" \ + "${{ needs.resolve.outputs.network }}" \ + "${{ needs.resolve.outputs.genesis }}" \ + "0.10.1" + rm -rf "$destination" + done From 6af747f073ed0a2bd17d46ab078aa7ce879ca5e5 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 00:20:35 +0200 Subject: [PATCH 14/68] ci: benchmark mainnet restore across runner pools --- .../workflows/artifact-download-benchmark.yml | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/artifact-download-benchmark.yml b/.github/workflows/artifact-download-benchmark.yml index a1c474bc94..5181efb768 100644 --- a/.github/workflows/artifact-download-benchmark.yml +++ b/.github/workflows/artifact-download-benchmark.yml @@ -38,7 +38,7 @@ jobs: - name: Resolve network identity id: identity env: - NETWORK: ${{ inputs.network || 'testnet' }} + NETWORK: ${{ inputs.network || 'mainnet' }} run: | case "$NETWORK" in mainnet) genesis=0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03 ;; @@ -54,7 +54,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | .github/scripts/snapshot-artifact.sh select \ - "try-runtime-snap-v0.10.1-${{ inputs.network || 'testnet' }}" \ + "try-runtime-snap-v0.10.1-${{ inputs.network || 'mainnet' }}" \ "${{ github.event.repository.default_branch }}" \ "${{ github.repository_id }}" \ 72 "$GITHUB_OUTPUT" required @@ -73,7 +73,7 @@ jobs: echo '| Trial | Ranges | Bytes | Download | Assembly | Archive SHA | Extract | Snapshot SHA | Total | Throughput |' >> "$GITHUB_STEP_SUMMARY" echo '|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|' >> "$GITHUB_STEP_SUMMARY" trial=0 - for concurrency in 1 4 8 16 16 8 4 1; do + for concurrency in 8 16 16 8; do trial=$((trial + 1)) destination="$RUNNER_TEMP/snapshot-$trial-$concurrency" BENCHMARK_TRIAL="$trial" .github/scripts/benchmark-artifact-download.sh \ @@ -101,7 +101,35 @@ jobs: echo '| Trial | Ranges | Bytes | Download | Assembly | Archive SHA | Extract | Snapshot SHA | Total | Throughput |' >> "$GITHUB_STEP_SUMMARY" echo '|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|' >> "$GITHUB_STEP_SUMMARY" trial=0 - for concurrency in 1 4 8 16 16 8 4 1; do + for concurrency in 8 16 16 8; do + trial=$((trial + 1)) + destination="$RUNNER_TEMP/snapshot-$trial-$concurrency" + BENCHMARK_TRIAL="$trial" .github/scripts/benchmark-artifact-download.sh \ + "${{ needs.resolve.outputs.artifact-id }}" \ + "${{ needs.resolve.outputs.digest }}" \ + "${{ needs.resolve.outputs.size-bytes }}" \ + "$concurrency" "$destination" \ + "${{ needs.resolve.outputs.network }}" \ + "${{ needs.resolve.outputs.genesis }}" \ + "0.10.1" + rm -rf "$destination" + done + + turbo-bhs: + name: BHS turbo sequential range sweep + needs: resolve + runs-on: [self-hosted, fireactions-turbo-8] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Benchmark download, verification, and extraction + env: + GH_TOKEN: ${{ github.token }} + run: | + echo '| Trial | Ranges | Bytes | Download | Assembly | Archive SHA | Extract | Snapshot SHA | Total | Throughput |' >> "$GITHUB_STEP_SUMMARY" + echo '|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|' >> "$GITHUB_STEP_SUMMARY" + trial=0 + for concurrency in 8 16 16 8; do trial=$((trial + 1)) destination="$RUNNER_TEMP/snapshot-$trial-$concurrency" BENCHMARK_TRIAL="$trial" .github/scripts/benchmark-artifact-download.sh \ From 435766fa5a346d4429db5ee10d6e975760db75a8 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 00:39:45 +0200 Subject: [PATCH 15/68] ci: parallelize cached snapshot downloads --- .../scripts/benchmark-artifact-download.sh | 133 ---------------- .github/scripts/snapshot-artifact.sh | 109 ++++++++++++- .github/scripts/test-snapshot-artifact.sh | 74 +++++++++ .../workflows/artifact-download-benchmark.yml | 144 ------------------ .github/workflows/runtime-checks.yml | 12 +- 5 files changed, 189 insertions(+), 283 deletions(-) delete mode 100755 .github/scripts/benchmark-artifact-download.sh delete mode 100644 .github/workflows/artifact-download-benchmark.yml diff --git a/.github/scripts/benchmark-artifact-download.sh b/.github/scripts/benchmark-artifact-download.sh deleted file mode 100755 index df1eb6d548..0000000000 --- a/.github/scripts/benchmark-artifact-download.sh +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -ne 8 ]]; then - echo "usage: $0 ARTIFACT_ID DIGEST SIZE_BYTES CONCURRENCY DESTINATION NETWORK GENESIS CLI_VERSION" >&2 - exit 2 -fi - -artifact_id="$1" -expected_digest="$2" -size_bytes="$3" -concurrency="$4" -destination="$5" -network="$6" -genesis="$7" -cli_version="$8" - -[[ "$artifact_id" =~ ^[0-9]+$ ]] || { echo "invalid artifact id" >&2; exit 2; } -[[ "$expected_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "invalid artifact digest" >&2; exit 2; } -[[ "$size_bytes" =~ ^[1-9][0-9]*$ ]] || { echo "invalid artifact size" >&2; exit 2; } -[[ "$concurrency" =~ ^[1-9][0-9]*$ ]] || { echo "invalid concurrency" >&2; exit 2; } -: "${GH_TOKEN:?GH_TOKEN must be set}" -: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" - -archive=$(mktemp) -parts_dir=$(mktemp -d) -trap 'rm -f "$archive"; rm -rf "$parts_dir"' EXIT - -api_url="https://api.github.com/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" - -get_download_url() { - local headers status url - headers=$(mktemp) - status=$(curl --disable --silent --show-error \ - --dump-header "$headers" \ - --output /dev/null \ - --max-redirs 0 \ - --header 'Accept: application/vnd.github+json' \ - --header "Authorization: Bearer $GH_TOKEN" \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - --write-out '%{http_code}' \ - "$api_url") - if [[ "$status" != 302 ]]; then - echo "artifact redirect returned HTTP $status" >&2 - rm -f "$headers" - return 1 - fi - url=$(awk 'BEGIN { IGNORECASE=1 } /^location:/ { sub(/^[^:]+:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit }' "$headers") - rm -f "$headers" - [[ "$url" == https://* ]] || { echo "artifact redirect did not contain an HTTPS URL" >&2; return 1; } - printf '%s' "$url" -} - -download_range() { - local start="$1" end="$2" part="$3" expected_size attempt url actual_size - expected_size=$((end - start + 1)) - for attempt in 1 2 3; do - url=$(get_download_url) || continue - if curl --disable --fail --silent --show-error \ - --range "$start-$end" \ - --output "$part" \ - "$url"; then - actual_size=$(stat -c '%s' "$part") - if [[ "$actual_size" == "$expected_size" ]]; then - return 0 - fi - echo "range $start-$end size mismatch: expected $expected_size, got $actual_size" >&2 - fi - rm -f "$part" - sleep "$attempt" - done - return 1 -} - -chunk_size=$(((size_bytes + concurrency - 1) / concurrency)) -overall_started=$(date +%s) -download_started=$(date +%s) -pids=() -parts=() -for ((worker = 0; worker < concurrency; worker++)); do - start=$((worker * chunk_size)) - ((start < size_bytes)) || break - end=$((start + chunk_size - 1)) - ((end < size_bytes)) || end=$((size_bytes - 1)) - printf -v part '%s/part-%03d' "$parts_dir" "$worker" - parts+=("$part") - download_range "$start" "$end" "$part" & - pids+=("$!") -done - -failed=0 -for pid in "${pids[@]}"; do - wait "$pid" || failed=1 -done -((failed == 0)) || { echo "one or more range downloads failed" >&2; exit 1; } -download_seconds=$(($(date +%s) - download_started)) - -assembly_started=$(date +%s) -for index in "${!parts[@]}"; do - command cat "${parts[$index]}" >> "$archive" -done -assembly_seconds=$(($(date +%s) - assembly_started)) - -actual_size=$(stat -c '%s' "$archive") -[[ "$actual_size" == "$size_bytes" ]] || { echo "archive size mismatch" >&2; exit 1; } - -verify_started=$(date +%s) -actual_digest="sha256:$(sha256sum "$archive" | awk '{print $1}')" -[[ "$actual_digest" == "$expected_digest" ]] || { - echo "artifact digest mismatch: expected $expected_digest, got $actual_digest" >&2 - exit 1 -} -verify_seconds=$(($(date +%s) - verify_started)) - -extract_started=$(date +%s) -mkdir -p "$destination" -unzip -q "$archive" -d "$destination" -extract_seconds=$(($(date +%s) - extract_started)) - -validation_started=$(date +%s) -.github/scripts/snapshot-artifact.sh validate \ - "$destination/$network.manifest.json" \ - "$destination/$network.snap" \ - "$network" "$genesis" "$cli_version" -validation_seconds=$(($(date +%s) - validation_started)) -total_seconds=$(($(date +%s) - overall_started)) -mbps=$(awk -v bytes="$size_bytes" -v seconds="$download_seconds" 'BEGIN { if (seconds == 0) seconds = 1; printf "%.1f", bytes * 8 / seconds / 1000000 }') -trial="${BENCHMARK_TRIAL:-unknown}" - -echo "artifact=$artifact_id trial=$trial concurrency=$concurrency download=${download_seconds}s assembly=${assembly_seconds}s archive_verify=${verify_seconds}s extract=${extract_seconds}s snapshot_validate=${validation_seconds}s total=${total_seconds}s throughput=${mbps}Mbps" -if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then - echo "| $trial | $concurrency | $size_bytes | ${download_seconds}s | ${assembly_seconds}s | ${verify_seconds}s | ${extract_seconds}s | ${validation_seconds}s | ${total_seconds}s | ${mbps} Mbps |" >> "$GITHUB_STEP_SUMMARY" -fi diff --git a/.github/scripts/snapshot-artifact.sh b/.github/scripts/snapshot-artifact.sh index 0edaa9e0b2..d66bea644d 100755 --- a/.github/scripts/snapshot-artifact.sh +++ b/.github/scripts/snapshot-artifact.sh @@ -140,20 +140,118 @@ download_artifact() { local artifact_id="$1" local digest="$2" local destination="$3" - local archive actual_digest + local archive actual_digest size_bytes concurrency parts_dir api_url download_url retry_delay + local chunk_size download_started download_seconds assembly_started assembly_seconds [[ "$artifact_id" =~ ^[0-9]+$ ]] || { echo "invalid artifact id: $artifact_id" >&2; exit 2; } [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "invalid artifact digest: $digest" >&2; exit 2; } archive=$(mktemp) - trap 'rm -f "$archive"' RETURN + parts_dir=$(mktemp -d) + trap 'rm -f "$archive"; rm -rf "$parts_dir"' EXIT if [[ -n "${ARTIFACT_ZIP_FILE:-}" ]]; then cp "$ARTIFACT_ZIP_FILE" "$archive" else : "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" - gh api \ + : "${GH_TOKEN:?GH_TOKEN must be set}" + concurrency="${ARTIFACT_DOWNLOAD_CONCURRENCY:-1}" + [[ "$concurrency" =~ ^[1-9][0-9]*$ ]] && ((concurrency <= 64)) || { + echo "invalid ARTIFACT_DOWNLOAD_CONCURRENCY: $concurrency" >&2 + exit 2 + } + retry_delay="${ARTIFACT_RETRY_DELAY_SECONDS:-1}" + [[ "$retry_delay" =~ ^[0-9]+$ ]] && ((retry_delay <= 60)) || { + echo "invalid ARTIFACT_RETRY_DELAY_SECONDS: $retry_delay" >&2 + exit 2 + } + size_bytes=$(gh api \ -H 'Accept: application/vnd.github+json' \ - "repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" > "$archive" + "repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id" \ + --jq '.size_in_bytes') + [[ "$size_bytes" =~ ^[1-9][0-9]*$ ]] || { echo "invalid artifact size: $size_bytes" >&2; exit 1; } + api_url="https://api.github.com/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" + + get_download_url() { + local headers status url + headers=$(mktemp) + status=$(curl --disable --silent --show-error \ + --connect-timeout 15 \ + --max-time 30 \ + --dump-header "$headers" \ + --output /dev/null \ + --max-redirs 0 \ + --header 'Accept: application/vnd.github+json' \ + --header "Authorization: Bearer $GH_TOKEN" \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + --write-out '%{http_code}' \ + "$api_url") + if [[ "$status" != 302 ]]; then + echo "artifact redirect returned HTTP $status" >&2 + rm -f "$headers" + return 1 + fi + url=$(awk 'tolower($0) ~ /^location:/ { sub(/^[^:]+:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit }' "$headers") + rm -f "$headers" + [[ "$url" == https://* ]] || { + echo "artifact redirect did not contain an HTTPS URL" >&2 + return 1 + } + printf '%s' "$url" + } + + download_range() { + local start="$1" end="$2" part="$3" expected_size attempt url actual_size + expected_size=$((end - start + 1)) + url="$download_url" + for attempt in 1 2 3; do + if ((attempt > 1)); then + url=$(get_download_url) || continue + fi + if curl --disable --fail --silent --show-error \ + --connect-timeout 30 \ + --max-time 900 \ + --range "$start-$end" \ + --output "$part" \ + "$url"; then + actual_size=$(file_size "$part") + if [[ "$actual_size" == "$expected_size" ]]; then + return 0 + fi + echo "range $start-$end size mismatch: expected $expected_size, got $actual_size" >&2 + fi + rm -f "$part" + sleep "$((attempt * retry_delay))" + done + return 1 + } + + download_url=$(get_download_url) + chunk_size=$(((size_bytes + concurrency - 1) / concurrency)) + download_started=$(date +%s) + local pids=() parts=() failed=0 worker start end part pid index + for ((worker = 0; worker < concurrency; worker++)); do + start=$((worker * chunk_size)) + ((start < size_bytes)) || break + end=$((start + chunk_size - 1)) + ((end < size_bytes)) || end=$((size_bytes - 1)) + printf -v part '%s/part-%03d' "$parts_dir" "$worker" + parts+=("$part") + download_range "$start" "$end" "$part" & + pids+=("$!") + done + for pid in "${pids[@]}"; do + wait "$pid" || failed=1 + done + ((failed == 0)) || { echo "one or more artifact ranges failed" >&2; exit 1; } + download_seconds=$(($(date +%s) - download_started)) + + assembly_started=$(date +%s) + for index in "${!parts[@]}"; do + command cat "${parts[$index]}" >> "$archive" + done + assembly_seconds=$(($(date +%s) - assembly_started)) + [[ "$(file_size "$archive")" == "$size_bytes" ]] || { echo "artifact size mismatch" >&2; exit 1; } + echo "Downloaded artifact $artifact_id in ${download_seconds}s with $concurrency ranges; assembled in ${assembly_seconds}s." fi actual_digest="sha256:$(sha256sum "$archive" | awk '{print $1}')" @@ -164,7 +262,8 @@ download_artifact() { mkdir -p "$destination" unzip -q "$archive" -d "$destination" rm -f "$archive" - trap - RETURN + rm -rf "$parts_dir" + trap - EXIT echo "Downloaded and verified artifact $artifact_id." } diff --git a/.github/scripts/test-snapshot-artifact.sh b/.github/scripts/test-snapshot-artifact.sh index 203270731b..0f08296844 100755 --- a/.github/scripts/test-snapshot-artifact.sh +++ b/.github/scripts/test-snapshot-artifact.sh @@ -97,6 +97,80 @@ if ARTIFACT_ZIP_FILE="$tmp/artifact.zip" \ exit 1 fi +# Exercise the real ranged-download branch with deterministic fake GitHub and +# byte-range clients. One worker fails once, forcing a fresh signed URL; exact +# part sizing, ordered assembly, archive digest, and extraction must still pass. +fake_bin="$tmp/fake-bin" +mkdir -p "$fake_bin" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + 'wc -c < "$MOCK_ARTIFACT_ZIP" | tr -d " "' \ + > "$fake_bin/gh" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + 'headers= output= range=' \ + 'while (($#)); do' \ + ' case "$1" in' \ + ' --dump-header) headers="$2"; shift 2 ;;' \ + ' --output) output="$2"; shift 2 ;;' \ + ' --range) range="$2"; shift 2 ;;' \ + ' --connect-timeout|--max-time|--header|--write-out) shift 2 ;;' \ + ' *) shift ;;' \ + ' esac' \ + 'done' \ + 'if [[ -n "$headers" ]]; then' \ + ' printf "HTTP/1.1 302 Found\r\nLocation: https://artifact.invalid/download\r\n\r\n" > "$headers"' \ + ' printf "x\n" >> "$MOCK_REDIRECT_LOG"' \ + ' printf 302' \ + ' exit 0' \ + 'fi' \ + 'if [[ -n "${MOCK_FAIL_ONCE_DIR:-}" ]] && mkdir "$MOCK_FAIL_ONCE_DIR" 2>/dev/null; then' \ + ' exit 22' \ + 'fi' \ + 'start=${range%-*}; end=${range#*-}; count=$((end - start + 1))' \ + 'if [[ "${MOCK_IGNORE_RANGE:-false}" == true ]]; then' \ + ' cp "$MOCK_ARTIFACT_ZIP" "$output"' \ + 'else' \ + ' dd if="$MOCK_ARTIFACT_ZIP" of="$output" bs=1 skip="$start" count="$count" status=none' \ + 'fi' \ + > "$fake_bin/curl" +chmod +x "$fake_bin/gh" "$fake_bin/curl" + +mock_digest="sha256:$(sha256sum "$tmp/artifact.zip" | awk '{print $1}')" +redirect_log="$tmp/redirect.log" +PATH="$fake_bin:$PATH" \ + GH_TOKEN=test-token \ + GITHUB_REPOSITORY=RaoFoundation/subtensor \ + MOCK_ARTIFACT_ZIP="$tmp/artifact.zip" \ + MOCK_REDIRECT_LOG="$redirect_log" \ + MOCK_FAIL_ONCE_DIR="$tmp/fail-once" \ + ARTIFACT_DOWNLOAD_CONCURRENCY=4 \ + ARTIFACT_RETRY_DELAY_SECONDS=0 \ + "$helper" download 12 "$mock_digest" "$tmp/ranged-download" >/dev/null +grep -qx 'artifact payload' "$tmp/ranged-download/payload.txt" +[[ "$(wc -l < "$redirect_log" | tr -d ' ')" == 2 ]] + +# A server that ignores Range must fail closed after bounded retries and clean +# every temporary part/archive even though the helper exits from the function. +range_tmp="$tmp/range-tmp" +mkdir -p "$range_tmp" +if PATH="$fake_bin:$PATH" \ + TMPDIR="$range_tmp" \ + GH_TOKEN=test-token \ + GITHUB_REPOSITORY=RaoFoundation/subtensor \ + MOCK_ARTIFACT_ZIP="$tmp/artifact.zip" \ + MOCK_REDIRECT_LOG="$tmp/ignored-range-redirect.log" \ + MOCK_IGNORE_RANGE=true \ + ARTIFACT_DOWNLOAD_CONCURRENCY=4 \ + ARTIFACT_RETRY_DELAY_SECONDS=0 \ + "$helper" download 12 "$mock_digest" "$tmp/ignored-range" >/dev/null 2>&1; then + echo "expected ignored range responses to fail" >&2 + exit 1 +fi +[[ -z "$(find "$range_tmp" -mindepth 1 -print -quit)" ]] + # Exactly 72 hours is accepted, one second older fails, and optional lookup # reports a miss. An artifact older than 36 hours remains usable with a warning. at_boundary=$(artifact 19 try-runtime-snap-v0.10.1-mainnet 2026-07-09T14:00:00Z main "$repo_id" false 199) diff --git a/.github/workflows/artifact-download-benchmark.yml b/.github/workflows/artifact-download-benchmark.yml deleted file mode 100644 index 5181efb768..0000000000 --- a/.github/workflows/artifact-download-benchmark.yml +++ /dev/null @@ -1,144 +0,0 @@ -name: Artifact Download Benchmark - -on: - push: - branches: - - codex/try-runtime-snapshot-cache-test - workflow_dispatch: - inputs: - network: - description: "Snapshot size to benchmark" - type: choice - default: testnet - options: - - testnet - - devnet - - mainnet - -permissions: - actions: read - contents: read - -concurrency: - group: artifact-download-benchmark-${{ github.ref }} - cancel-in-progress: true - -jobs: - resolve: - name: Resolve immutable artifact - runs-on: ubuntu-latest - outputs: - artifact-id: ${{ steps.snapshot.outputs.artifact-id }} - digest: ${{ steps.snapshot.outputs.digest }} - size-bytes: ${{ steps.snapshot.outputs.size-bytes }} - network: ${{ steps.identity.outputs.network }} - genesis: ${{ steps.identity.outputs.genesis }} - steps: - - uses: actions/checkout@v4 - - name: Resolve network identity - id: identity - env: - NETWORK: ${{ inputs.network || 'mainnet' }} - run: | - case "$NETWORK" in - mainnet) genesis=0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03 ;; - testnet) genesis=0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105 ;; - devnet) genesis=0x077899043eb684c5277b6814a39161f4ce072b45e782e12c81a521c63fb4f3e5 ;; - *) echo "invalid network: $NETWORK" >&2; exit 2 ;; - esac - echo "network=$NETWORK" >> "$GITHUB_OUTPUT" - echo "genesis=$genesis" >> "$GITHUB_OUTPUT" - - name: Select trusted snapshot - id: snapshot - env: - GH_TOKEN: ${{ github.token }} - run: | - .github/scripts/snapshot-artifact.sh select \ - "try-runtime-snap-v0.10.1-${{ inputs.network || 'mainnet' }}" \ - "${{ github.event.repository.default_branch }}" \ - "${{ github.repository_id }}" \ - 72 "$GITHUB_OUTPUT" required - - hosted: - name: hosted sequential range sweep - needs: resolve - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - name: Benchmark download, verification, and extraction - env: - GH_TOKEN: ${{ github.token }} - run: | - echo '| Trial | Ranges | Bytes | Download | Assembly | Archive SHA | Extract | Snapshot SHA | Total | Throughput |' >> "$GITHUB_STEP_SUMMARY" - echo '|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|' >> "$GITHUB_STEP_SUMMARY" - trial=0 - for concurrency in 8 16 16 8; do - trial=$((trial + 1)) - destination="$RUNNER_TEMP/snapshot-$trial-$concurrency" - BENCHMARK_TRIAL="$trial" .github/scripts/benchmark-artifact-download.sh \ - "${{ needs.resolve.outputs.artifact-id }}" \ - "${{ needs.resolve.outputs.digest }}" \ - "${{ needs.resolve.outputs.size-bytes }}" \ - "$concurrency" "$destination" \ - "${{ needs.resolve.outputs.network }}" \ - "${{ needs.resolve.outputs.genesis }}" \ - "0.10.1" - rm -rf "$destination" - done - - self-hosted: - name: self-hosted sequential range sweep - needs: resolve - runs-on: [self-hosted, fireactions-tryruntime] - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - name: Benchmark download, verification, and extraction - env: - GH_TOKEN: ${{ github.token }} - run: | - echo '| Trial | Ranges | Bytes | Download | Assembly | Archive SHA | Extract | Snapshot SHA | Total | Throughput |' >> "$GITHUB_STEP_SUMMARY" - echo '|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|' >> "$GITHUB_STEP_SUMMARY" - trial=0 - for concurrency in 8 16 16 8; do - trial=$((trial + 1)) - destination="$RUNNER_TEMP/snapshot-$trial-$concurrency" - BENCHMARK_TRIAL="$trial" .github/scripts/benchmark-artifact-download.sh \ - "${{ needs.resolve.outputs.artifact-id }}" \ - "${{ needs.resolve.outputs.digest }}" \ - "${{ needs.resolve.outputs.size-bytes }}" \ - "$concurrency" "$destination" \ - "${{ needs.resolve.outputs.network }}" \ - "${{ needs.resolve.outputs.genesis }}" \ - "0.10.1" - rm -rf "$destination" - done - - turbo-bhs: - name: BHS turbo sequential range sweep - needs: resolve - runs-on: [self-hosted, fireactions-turbo-8] - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - name: Benchmark download, verification, and extraction - env: - GH_TOKEN: ${{ github.token }} - run: | - echo '| Trial | Ranges | Bytes | Download | Assembly | Archive SHA | Extract | Snapshot SHA | Total | Throughput |' >> "$GITHUB_STEP_SUMMARY" - echo '|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|' >> "$GITHUB_STEP_SUMMARY" - trial=0 - for concurrency in 8 16 16 8; do - trial=$((trial + 1)) - destination="$RUNNER_TEMP/snapshot-$trial-$concurrency" - BENCHMARK_TRIAL="$trial" .github/scripts/benchmark-artifact-download.sh \ - "${{ needs.resolve.outputs.artifact-id }}" \ - "${{ needs.resolve.outputs.digest }}" \ - "${{ needs.resolve.outputs.size-bytes }}" \ - "$concurrency" "$destination" \ - "${{ needs.resolve.outputs.network }}" \ - "${{ needs.resolve.outputs.genesis }}" \ - "0.10.1" - rm -rf "$destination" - done diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 335f48ae9e..1223a1f131 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -20,6 +20,14 @@ on: description: "Run only the try-runtime build and cached network checks" type: boolean default: false + benchmark_runner_pool: + description: "Temporary cached try-runtime runner comparison" + type: choice + default: current + options: + - current + - bhs + - hosted concurrency: group: runtime-checks-${{ github.ref }} @@ -209,7 +217,8 @@ jobs: try-runtime: name: try-runtime ${{ matrix.network.name }} needs: build-try-runtime - runs-on: [self-hosted, fireactions-tryruntime] + runs-on: ${{ fromJSON(inputs.benchmark_runner_pool == 'bhs' && '["self-hosted","fireactions-turbo-8"]' || inputs.benchmark_runner_pool == 'hosted' && '"ubuntu-latest"' || '["self-hosted","fireactions-tryruntime"]') }} + timeout-minutes: 90 permissions: contents: read # Cross-run artifact download (the nightly try-runtime state snapshot). @@ -304,6 +313,7 @@ jobs: if: steps.state-mode.outputs.fresh-state != 'true' env: GH_TOKEN: ${{ github.token }} + ARTIFACT_DOWNLOAD_CONCURRENCY: 16 run: | .github/scripts/snapshot-artifact.sh download \ "${{ steps.snapshot.outputs.artifact-id }}" \ From f47e986896be896eb156b5d8a638b261c91079fb Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 00:58:33 +0200 Subject: [PATCH 16/68] ci: benchmark try-runtime on turbo16 --- .github/workflows/runtime-checks.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 1223a1f131..2a89d1e30e 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -27,6 +27,7 @@ on: options: - current - bhs + - turbo16 - hosted concurrency: @@ -217,7 +218,7 @@ jobs: try-runtime: name: try-runtime ${{ matrix.network.name }} needs: build-try-runtime - runs-on: ${{ fromJSON(inputs.benchmark_runner_pool == 'bhs' && '["self-hosted","fireactions-turbo-8"]' || inputs.benchmark_runner_pool == 'hosted' && '"ubuntu-latest"' || '["self-hosted","fireactions-tryruntime"]') }} + runs-on: ${{ fromJSON(inputs.benchmark_runner_pool == 'bhs' && '["self-hosted","fireactions-turbo-8"]' || inputs.benchmark_runner_pool == 'turbo16' && '["self-hosted","fireactions-turbo-16"]' || inputs.benchmark_runner_pool == 'hosted' && '"ubuntu-latest"' || '["self-hosted","fireactions-tryruntime"]') }} timeout-minutes: 90 permissions: contents: read From df09700efa4a6fd6ef787b964f4a9246d13af067 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 01:06:54 +0200 Subject: [PATCH 17/68] ci: run try-runtime on turbo16 --- .github/workflows/runtime-checks.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 2a89d1e30e..4079a902a5 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -20,16 +20,6 @@ on: description: "Run only the try-runtime build and cached network checks" type: boolean default: false - benchmark_runner_pool: - description: "Temporary cached try-runtime runner comparison" - type: choice - default: current - options: - - current - - bhs - - turbo16 - - hosted - concurrency: group: runtime-checks-${{ github.ref }} cancel-in-progress: true @@ -218,7 +208,7 @@ jobs: try-runtime: name: try-runtime ${{ matrix.network.name }} needs: build-try-runtime - runs-on: ${{ fromJSON(inputs.benchmark_runner_pool == 'bhs' && '["self-hosted","fireactions-turbo-8"]' || inputs.benchmark_runner_pool == 'turbo16' && '["self-hosted","fireactions-turbo-16"]' || inputs.benchmark_runner_pool == 'hosted' && '"ubuntu-latest"' || '["self-hosted","fireactions-tryruntime"]') }} + runs-on: [self-hosted, fireactions-turbo-16] timeout-minutes: 90 permissions: contents: read From f9ffd0026f9c9093aca1ef963cecba346d0ceb63 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 01:16:03 +0200 Subject: [PATCH 18/68] ci: run try-runtime on GitHub-hosted runners --- .github/workflows/runtime-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 4079a902a5..ffa4abfbb6 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -208,7 +208,7 @@ jobs: try-runtime: name: try-runtime ${{ matrix.network.name }} needs: build-try-runtime - runs-on: [self-hosted, fireactions-turbo-16] + runs-on: ubuntu-latest timeout-minutes: 90 permissions: contents: read From c0bf45dd8ed800032c38af691b1a2a725f29c366 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 01:48:14 +0200 Subject: [PATCH 19/68] ci: harden cached snapshot workflows --- .github/scripts/classify-runtime-changes.sh | 39 ++++ .github/scripts/snapshot-artifact.sh | 206 +++++------------- .github/scripts/test-runtime-change-filter.sh | 31 +++ .github/scripts/test-snapshot-artifact.sh | 157 +++++-------- .../workflows/refresh-mainnet-snapshot.yml | 10 +- .github/workflows/runtime-checks.yml | 67 +++--- .github/workflows/sccache-warm.yml | 4 +- .github/workflows/scheduled-smoke-tests.yml | 4 +- 8 files changed, 213 insertions(+), 305 deletions(-) create mode 100755 .github/scripts/classify-runtime-changes.sh create mode 100755 .github/scripts/test-runtime-change-filter.sh diff --git a/.github/scripts/classify-runtime-changes.sh b/.github/scripts/classify-runtime-changes.sh new file mode 100755 index 0000000000..75827c634c --- /dev/null +++ b/.github/scripts/classify-runtime-changes.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: classify-runtime-changes.sh OUTPUT_FILE" >&2 + exit 2 +fi + +output_file="$1" + +# SDK-only changes are covered by sdk-checks and the Rust SDK e2e workflow; +# they should not force clone-upgrade or SDK drift. +runtime_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor|clones)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^website/apps/bittensor-website/scripts/|^\.github/(workflows/(runtime-checks|refresh-mainnet-snapshot)\.yml|actions/(rust-setup|sccache-setup)/.*|scripts/(classify-runtime-changes|test-runtime-change-filter|sccache-configure|snapshot-artifact|test-snapshot-artifact)\.sh)$' +docs_pattern='^website/|^sdk/python/|^\.github/workflows/runtime-checks\.yml$' +python_sdk_pattern='^sdk/(python|bittensor-core|bittensor-core-py|bittensor-core-wasm)/|^Cargo\.lock$|^\.github/workflows/runtime-checks\.yml$' +sdk_drift_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$' +snapshot_pattern='^\.github/(workflows/(runtime-checks|refresh-mainnet-snapshot)\.yml|scripts/(classify-runtime-changes|test-runtime-change-filter|snapshot-artifact|test-snapshot-artifact)\.sh)$' + +files=$(< /dev/stdin) +runtime=false +docs=false +python_sdk=false +sdk_drift=false +snapshot_ci=false +grep -qE "$runtime_pattern" <<< "$files" && runtime=true +grep -qE "$docs_pattern" <<< "$files" && docs=true +grep -qE "$python_sdk_pattern" <<< "$files" && python_sdk=true +grep -qE "$sdk_drift_pattern" <<< "$files" && sdk_drift=true +grep -qE "$snapshot_pattern" <<< "$files" && snapshot_ci=true + +echo "runtime=$runtime, docs=$docs, python_sdk=$python_sdk, sdk_drift=$sdk_drift, snapshot_ci=$snapshot_ci" +{ + echo "runtime=$runtime" + echo "docs=$docs" + echo "python_sdk=$python_sdk" + echo "sdk_drift=$sdk_drift" + echo "snapshot_ci=$snapshot_ci" +} >> "$output_file" diff --git a/.github/scripts/snapshot-artifact.sh b/.github/scripts/snapshot-artifact.sh index d66bea644d..d2c977ecf6 100755 --- a/.github/scripts/snapshot-artifact.sh +++ b/.github/scripts/snapshot-artifact.sh @@ -6,13 +6,11 @@ usage() { cat >&2 <<'EOF' Usage: snapshot-artifact.sh mode EVENT_NAME LABELS_JSON MANUAL_FRESH OUTPUT_FILE - snapshot-artifact.sh select ARTIFACT_NAME BRANCH REPOSITORY_ID MAX_AGE_HOURS OUTPUT_FILE [required|optional] - snapshot-artifact.sh download ARTIFACT_ID DIGEST DESTINATION - snapshot-artifact.sh validate MANIFEST_FILE SNAPSHOT_FILE NETWORK GENESIS_HASH CLI_VERSION + snapshot-artifact.sh select ARTIFACT_NAME BRANCH REPOSITORY_ID WORKFLOW_PATH MAX_AGE_HOURS OUTPUT_FILE [required|optional] + snapshot-artifact.sh validate MANIFEST_FILE SNAPSHOT_FILE NETWORK GENESIS_HASH CLI_VERSION PRODUCER_SHA -For tests, set ARTIFACTS_JSON_FILE instead of calling the GitHub API, -ARTIFACT_ZIP_FILE instead of downloading an artifact, and NOW_EPOCH to -override the current time. +For tests, set ARTIFACTS_JSON_FILE and WORKFLOW_RUNS_JSON_FILE instead of +calling the GitHub API and NOW_EPOCH to override the current time. EOF exit 2 } @@ -62,16 +60,21 @@ resolve_mode() { } select_artifact() { - [[ $# -eq 6 ]] || usage + [[ $# -eq 7 ]] || usage local artifact_name="$1" local branch="$2" local repository_id="$3" - local max_age_hours="$4" - local output_file="$5" - local requirement="$6" - local payload now candidate created_epoch age_seconds age_hours + local workflow_path="$4" + local max_age_hours="$5" + local output_file="$6" + local requirement="$7" + local payload workflow_runs workflow_file now candidate created_epoch age_seconds age_hours [[ "$repository_id" =~ ^[0-9]+$ ]] || { echo "invalid repository id: $repository_id" >&2; exit 2; } + [[ "$workflow_path" =~ ^\.github/workflows/[A-Za-z0-9._-]+\.ya?ml$ ]] || { + echo "invalid workflow path: $workflow_path" >&2 + exit 2 + } [[ "$max_age_hours" =~ ^[0-9]+$ ]] || { echo "invalid maximum age: $max_age_hours" >&2; exit 2; } [[ "$requirement" == required || "$requirement" == optional ]] || usage @@ -82,30 +85,58 @@ select_artifact() { payload=$(gh api \ "repos/$GITHUB_REPOSITORY/actions/artifacts?name=$artifact_name&per_page=100") fi + if [[ -n "${WORKFLOW_RUNS_JSON_FILE:-}" ]]; then + workflow_runs=$(<"$WORKFLOW_RUNS_JSON_FILE") + else + workflow_file="${workflow_path##*/}" + workflow_runs=$(gh api --method GET \ + "repos/$GITHUB_REPOSITORY/actions/workflows/$workflow_file/runs" \ + -f branch="$branch" -F per_page=100 -F exclude_pull_requests=true \ + --jq '{workflow_runs: [.workflow_runs[] | { + id, path, head_branch, head_sha, + repository: {id: .repository.id}, + head_repository: {id: .head_repository.id} + }]}') + fi now="${NOW_EPOCH:-$(date -u +%s)}" [[ "$now" =~ ^[0-9]+$ ]] || { echo "invalid NOW_EPOCH: $now" >&2; exit 2; } - candidate=$(jq -cer \ + candidate=$(jq -ncer \ --arg name "$artifact_name" \ --arg branch "$branch" \ + --arg workflow_path "$workflow_path" \ --argjson repository_id "$repository_id" \ --argjson now "$now" \ - --argjson max_age_seconds "$((max_age_hours * 3600))" ' + --argjson max_age_seconds "$((max_age_hours * 3600))" \ + --argjson artifacts "$payload" \ + --argjson workflow_runs "$workflow_runs" ' + ($workflow_runs.workflow_runs + | map(select( + .path == $workflow_path and + .head_branch == $branch and + .repository.id == $repository_id and + .head_repository.id == $repository_id and + (.head_sha | test("^[0-9a-f]{40}$")) + )) + | map({key: (.id | tostring), value: .head_sha}) + | from_entries) as $trusted_runs + | [ - .artifacts[] + $artifacts.artifacts[] | select(.name == $name) | select(.expired == false) | select(.workflow_run.head_branch == $branch) | select(.workflow_run.repository_id == $repository_id) | select(.workflow_run.head_repository_id == $repository_id) + | select($trusted_runs[(.workflow_run.id | tostring)] == .workflow_run.head_sha) | .created_epoch = (.created_at | fromdateiso8601) | select(.created_epoch <= $now) | select(($now - .created_epoch) <= $max_age_seconds) ] | sort_by(.created_epoch) | last // empty - ' <<<"$payload" 2>/dev/null || true) + ' 2>/dev/null || true) if [[ -z "$candidate" ]]; then set_output "$output_file" found false @@ -124,156 +155,23 @@ select_artifact() { set_output "$output_file" found true set_output "$output_file" artifact-id "$(jq -er '.id' <<<"$candidate")" set_output "$output_file" run-id "$(jq -er '.workflow_run.id' <<<"$candidate")" + set_output "$output_file" producer-sha "$(jq -er '.workflow_run.head_sha' <<<"$candidate")" set_output "$output_file" created-at "$(jq -er '.created_at' <<<"$candidate")" set_output "$output_file" age-hours "$age_hours" - set_output "$output_file" size-bytes "$(jq -er '.size_in_bytes' <<<"$candidate")" - set_output "$output_file" digest "$(jq -er '.digest // ""' <<<"$candidate")" - if ((age_seconds > 36 * 3600)); then echo "::warning::$artifact_name is ${age_hours}h old; refresh is expected daily and this artifact becomes unusable after ${max_age_hours}h." fi echo "Selected $artifact_name artifact $(jq -er '.id' <<<"$candidate") from run $(jq -er '.workflow_run.id' <<<"$candidate") (${age_hours}h old)." } -download_artifact() { - [[ $# -eq 3 ]] || usage - local artifact_id="$1" - local digest="$2" - local destination="$3" - local archive actual_digest size_bytes concurrency parts_dir api_url download_url retry_delay - local chunk_size download_started download_seconds assembly_started assembly_seconds - - [[ "$artifact_id" =~ ^[0-9]+$ ]] || { echo "invalid artifact id: $artifact_id" >&2; exit 2; } - [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "invalid artifact digest: $digest" >&2; exit 2; } - - archive=$(mktemp) - parts_dir=$(mktemp -d) - trap 'rm -f "$archive"; rm -rf "$parts_dir"' EXIT - if [[ -n "${ARTIFACT_ZIP_FILE:-}" ]]; then - cp "$ARTIFACT_ZIP_FILE" "$archive" - else - : "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" - : "${GH_TOKEN:?GH_TOKEN must be set}" - concurrency="${ARTIFACT_DOWNLOAD_CONCURRENCY:-1}" - [[ "$concurrency" =~ ^[1-9][0-9]*$ ]] && ((concurrency <= 64)) || { - echo "invalid ARTIFACT_DOWNLOAD_CONCURRENCY: $concurrency" >&2 - exit 2 - } - retry_delay="${ARTIFACT_RETRY_DELAY_SECONDS:-1}" - [[ "$retry_delay" =~ ^[0-9]+$ ]] && ((retry_delay <= 60)) || { - echo "invalid ARTIFACT_RETRY_DELAY_SECONDS: $retry_delay" >&2 - exit 2 - } - size_bytes=$(gh api \ - -H 'Accept: application/vnd.github+json' \ - "repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id" \ - --jq '.size_in_bytes') - [[ "$size_bytes" =~ ^[1-9][0-9]*$ ]] || { echo "invalid artifact size: $size_bytes" >&2; exit 1; } - api_url="https://api.github.com/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" - - get_download_url() { - local headers status url - headers=$(mktemp) - status=$(curl --disable --silent --show-error \ - --connect-timeout 15 \ - --max-time 30 \ - --dump-header "$headers" \ - --output /dev/null \ - --max-redirs 0 \ - --header 'Accept: application/vnd.github+json' \ - --header "Authorization: Bearer $GH_TOKEN" \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - --write-out '%{http_code}' \ - "$api_url") - if [[ "$status" != 302 ]]; then - echo "artifact redirect returned HTTP $status" >&2 - rm -f "$headers" - return 1 - fi - url=$(awk 'tolower($0) ~ /^location:/ { sub(/^[^:]+:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit }' "$headers") - rm -f "$headers" - [[ "$url" == https://* ]] || { - echo "artifact redirect did not contain an HTTPS URL" >&2 - return 1 - } - printf '%s' "$url" - } - - download_range() { - local start="$1" end="$2" part="$3" expected_size attempt url actual_size - expected_size=$((end - start + 1)) - url="$download_url" - for attempt in 1 2 3; do - if ((attempt > 1)); then - url=$(get_download_url) || continue - fi - if curl --disable --fail --silent --show-error \ - --connect-timeout 30 \ - --max-time 900 \ - --range "$start-$end" \ - --output "$part" \ - "$url"; then - actual_size=$(file_size "$part") - if [[ "$actual_size" == "$expected_size" ]]; then - return 0 - fi - echo "range $start-$end size mismatch: expected $expected_size, got $actual_size" >&2 - fi - rm -f "$part" - sleep "$((attempt * retry_delay))" - done - return 1 - } - - download_url=$(get_download_url) - chunk_size=$(((size_bytes + concurrency - 1) / concurrency)) - download_started=$(date +%s) - local pids=() parts=() failed=0 worker start end part pid index - for ((worker = 0; worker < concurrency; worker++)); do - start=$((worker * chunk_size)) - ((start < size_bytes)) || break - end=$((start + chunk_size - 1)) - ((end < size_bytes)) || end=$((size_bytes - 1)) - printf -v part '%s/part-%03d' "$parts_dir" "$worker" - parts+=("$part") - download_range "$start" "$end" "$part" & - pids+=("$!") - done - for pid in "${pids[@]}"; do - wait "$pid" || failed=1 - done - ((failed == 0)) || { echo "one or more artifact ranges failed" >&2; exit 1; } - download_seconds=$(($(date +%s) - download_started)) - - assembly_started=$(date +%s) - for index in "${!parts[@]}"; do - command cat "${parts[$index]}" >> "$archive" - done - assembly_seconds=$(($(date +%s) - assembly_started)) - [[ "$(file_size "$archive")" == "$size_bytes" ]] || { echo "artifact size mismatch" >&2; exit 1; } - echo "Downloaded artifact $artifact_id in ${download_seconds}s with $concurrency ranges; assembled in ${assembly_seconds}s." - fi - - actual_digest="sha256:$(sha256sum "$archive" | awk '{print $1}')" - [[ "$actual_digest" == "$digest" ]] || { - echo "artifact archive checksum mismatch: expected $digest, got $actual_digest" >&2 - exit 1 - } - mkdir -p "$destination" - unzip -q "$archive" -d "$destination" - rm -f "$archive" - rm -rf "$parts_dir" - trap - EXIT - echo "Downloaded and verified artifact $artifact_id." -} - validate_manifest() { - [[ $# -eq 5 ]] || usage + [[ $# -eq 6 ]] || usage local manifest_file="$1" local snapshot_file="$2" local network="$3" local genesis_hash="$4" local cli_version="$5" + local producer_sha="$6" local expected_file expected_size expected_sha actual_size actual_sha [[ -f "$manifest_file" ]] || { echo "missing snapshot manifest: $manifest_file" >&2; exit 1; } @@ -282,7 +180,8 @@ validate_manifest() { jq -e \ --arg network "$network" \ --arg genesis "$genesis_hash" \ - --arg cli "$cli_version" ' + --arg cli "$cli_version" \ + --arg producer_sha "$producer_sha" ' .schema_version == 1 and .kind == "try-runtime-state" and .network == $network and @@ -293,7 +192,7 @@ validate_manifest() { (.source_spec_name | type == "string" and length > 0) and (.source_spec_version | type == "number") and (.created_at | fromdateiso8601 | type == "number") and - (.producer_sha | test("^[0-9a-f]{40}$")) and + .producer_sha == $producer_sha and (.snapshot_file | type == "string" and length > 0) and (.snapshot_size_bytes | type == "number") and (.snapshot_sha256 | test("^[0-9a-f]{64}$")) @@ -330,7 +229,6 @@ shift || true case "$command" in mode) resolve_mode "$@" ;; select) select_artifact "$@" ;; - download) download_artifact "$@" ;; validate) validate_manifest "$@" ;; *) usage ;; esac diff --git a/.github/scripts/test-runtime-change-filter.sh b/.github/scripts/test-runtime-change-filter.sh new file mode 100755 index 0000000000..733f20e367 --- /dev/null +++ b/.github/scripts/test-runtime-change-filter.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +classifier="$script_dir/classify-runtime-changes.sh" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +assert_classification() { + local path="$1" expected="$2" output="$tmp/output" + : > "$output" + printf '%s\n' "$path" | "$classifier" "$output" >/dev/null + diff -u <(printf '%s\n' "$expected") "$output" +} + +all_false=$'runtime=false\ndocs=false\npython_sdk=false\nsdk_drift=false\nsnapshot_ci=false' +runtime_only=$'runtime=true\ndocs=false\npython_sdk=false\nsdk_drift=false\nsnapshot_ci=false' +runtime_and_snapshot_only=$'runtime=true\ndocs=false\npython_sdk=false\nsdk_drift=false\nsnapshot_ci=true' +runtime_and_snapshot=$'runtime=true\ndocs=true\npython_sdk=true\nsdk_drift=false\nsnapshot_ci=true' +docs_and_python=$'runtime=false\ndocs=true\npython_sdk=true\nsdk_drift=false\nsnapshot_ci=false' + +assert_classification README.md "$all_false" +assert_classification .github/actions/rust-setup/action.yml "$runtime_only" +assert_classification .github/actions/sccache-setup/action.yml "$runtime_only" +assert_classification .github/scripts/classify-runtime-changes.sh "$runtime_and_snapshot_only" +assert_classification .github/scripts/test-runtime-change-filter.sh "$runtime_and_snapshot_only" +assert_classification .github/workflows/runtime-checks.yml "$runtime_and_snapshot" +assert_classification $'README.md\nsdk/python/example.py' "$docs_and_python" + +echo "runtime change filter tests passed" diff --git a/.github/scripts/test-snapshot-artifact.sh b/.github/scripts/test-snapshot-artifact.sh index 0f08296844..a76d59f5d8 100755 --- a/.github/scripts/test-snapshot-artifact.sh +++ b/.github/scripts/test-snapshot-artifact.sh @@ -13,14 +13,34 @@ now=$(jq -nr '"2026-07-12T14:00:00Z" | fromdateiso8601') repo_id=608683796 write_artifacts() { - jq -n --argjson artifacts "$1" '{artifacts: $artifacts}' > "$tmp/artifacts.json" + local artifacts="$1" + jq -n --argjson artifacts "$artifacts" '{artifacts: $artifacts}' > "$tmp/artifacts.json" + jq -n --argjson artifacts "$artifacts" ' + { + workflow_runs: [ + $artifacts[] + | { + id: .workflow_run.id, + path: ".github/workflows/refresh-mainnet-snapshot.yml", + head_branch: .workflow_run.head_branch, + head_sha: .workflow_run.head_sha, + repository: {id: .workflow_run.repository_id}, + head_repository: {id: .workflow_run.head_repository_id}, + conclusion: (.producer_conclusion // "success") + } + ] | unique_by(.id) + } + ' > "$tmp/workflow-runs.json" } artifact() { local id="$1" name="$2" created="$3" branch="$4" repository_id="$5" expired="$6" run_id="$7" + local head_sha + printf -v head_sha '%040x' "$run_id" jq -nc \ --argjson id "$id" --arg name "$name" --arg created "$created" --arg branch "$branch" \ - --argjson repository_id "$repository_id" --argjson expired "$expired" --argjson run_id "$run_id" ' + --argjson repository_id "$repository_id" --argjson expired "$expired" --argjson run_id "$run_id" \ + --arg head_sha "$head_sha" ' { id: $id, name: $name, @@ -32,7 +52,8 @@ artifact() { id: $run_id, repository_id: $repository_id, head_repository_id: $repository_id, - head_branch: $branch + head_branch: $branch, + head_sha: $head_sha } } ' @@ -43,8 +64,11 @@ select_fixture() { local output="$tmp/output" local status=0 : > "$output" - ARTIFACTS_JSON_FILE="$tmp/artifacts.json" NOW_EPOCH="$now" \ - "$helper" select try-runtime-snap-v0.10.1-mainnet main "$repo_id" 72 "$output" "$requirement" || status=$? + ARTIFACTS_JSON_FILE="$tmp/artifacts.json" \ + WORKFLOW_RUNS_JSON_FILE="$tmp/workflow-runs.json" \ + NOW_EPOCH="$now" \ + "$helper" select try-runtime-snap-v0.10.1-mainnet main "$repo_id" \ + .github/workflows/refresh-mainnet-snapshot.yml 72 "$output" "$requirement" || status=$? cat "$output" return "$status" } @@ -76,100 +100,19 @@ valid_new=$(artifact 12 try-runtime-snap-v0.10.1-mainnet 2026-07-12T02:00:00Z ma wrong_branch=$(artifact 13 try-runtime-snap-v0.10.1-mainnet 2026-07-12T13:00:00Z feature "$repo_id" false 103) wrong_repo=$(artifact 14 try-runtime-snap-v0.10.1-mainnet 2026-07-12T13:30:00Z main 999 false 104) expired=$(artifact 15 try-runtime-snap-v0.10.1-mainnet 2026-07-12T13:45:00Z main "$repo_id" true 105) -write_artifacts "$(jq -nc --argjson a "$valid_old" --argjson b "$valid_new" --argjson c "$wrong_branch" --argjson d "$wrong_repo" --argjson e "$expired" '[ $a, $b, $c, $d, $e ]')" +wrong_workflow=$(artifact 16 try-runtime-snap-v0.10.1-mainnet 2026-07-12T13:50:00Z main "$repo_id" false 106) +wrong_sha=$(artifact 17 try-runtime-snap-v0.10.1-mainnet 2026-07-12T13:55:00Z main "$repo_id" false 107) +write_artifacts "$(jq -nc --argjson a "$valid_old" --argjson b "$valid_new" --argjson c "$wrong_branch" --argjson d "$wrong_repo" --argjson e "$expired" --argjson f "$wrong_workflow" --argjson g "$wrong_sha" '[ $a, $b, $c, $d, $e, $f, $g ]')" +jq '(.workflow_runs[] | select(.id == 106) | .path) = ".github/workflows/untrusted-producer.yml"' \ + "$tmp/workflow-runs.json" > "$tmp/workflow-runs-updated.json" +mv "$tmp/workflow-runs-updated.json" "$tmp/workflow-runs.json" +jq '(.workflow_runs[] | select(.id == 107) | .head_sha) = "ffffffffffffffffffffffffffffffffffffffff"' \ + "$tmp/workflow-runs.json" > "$tmp/workflow-runs-updated.json" +mv "$tmp/workflow-runs-updated.json" "$tmp/workflow-runs.json" selected=$(select_fixture) grep -qx 'artifact-id=12' <<<"$selected" grep -qx 'run-id=102' <<<"$selected" - -# The selected immutable artifact ID is downloaded as its archive, verified -# against the API digest, and only then extracted. -mkdir -p "$tmp/archive-input" -printf 'artifact payload' > "$tmp/archive-input/payload.txt" -(cd "$tmp/archive-input" && zip -q "$tmp/artifact.zip" payload.txt) -archive_digest="sha256:$(sha256sum "$tmp/artifact.zip" | awk '{print $1}')" -ARTIFACT_ZIP_FILE="$tmp/artifact.zip" \ - "$helper" download 12 "$archive_digest" "$tmp/downloaded" >/dev/null -grep -qx 'artifact payload' "$tmp/downloaded/payload.txt" -if ARTIFACT_ZIP_FILE="$tmp/artifact.zip" \ - "$helper" download 12 sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \ - "$tmp/bad-download" >/dev/null 2>&1; then - echo "expected artifact archive checksum mismatch to fail" >&2 - exit 1 -fi - -# Exercise the real ranged-download branch with deterministic fake GitHub and -# byte-range clients. One worker fails once, forcing a fresh signed URL; exact -# part sizing, ordered assembly, archive digest, and extraction must still pass. -fake_bin="$tmp/fake-bin" -mkdir -p "$fake_bin" -printf '%s\n' \ - '#!/usr/bin/env bash' \ - 'set -euo pipefail' \ - 'wc -c < "$MOCK_ARTIFACT_ZIP" | tr -d " "' \ - > "$fake_bin/gh" -printf '%s\n' \ - '#!/usr/bin/env bash' \ - 'set -euo pipefail' \ - 'headers= output= range=' \ - 'while (($#)); do' \ - ' case "$1" in' \ - ' --dump-header) headers="$2"; shift 2 ;;' \ - ' --output) output="$2"; shift 2 ;;' \ - ' --range) range="$2"; shift 2 ;;' \ - ' --connect-timeout|--max-time|--header|--write-out) shift 2 ;;' \ - ' *) shift ;;' \ - ' esac' \ - 'done' \ - 'if [[ -n "$headers" ]]; then' \ - ' printf "HTTP/1.1 302 Found\r\nLocation: https://artifact.invalid/download\r\n\r\n" > "$headers"' \ - ' printf "x\n" >> "$MOCK_REDIRECT_LOG"' \ - ' printf 302' \ - ' exit 0' \ - 'fi' \ - 'if [[ -n "${MOCK_FAIL_ONCE_DIR:-}" ]] && mkdir "$MOCK_FAIL_ONCE_DIR" 2>/dev/null; then' \ - ' exit 22' \ - 'fi' \ - 'start=${range%-*}; end=${range#*-}; count=$((end - start + 1))' \ - 'if [[ "${MOCK_IGNORE_RANGE:-false}" == true ]]; then' \ - ' cp "$MOCK_ARTIFACT_ZIP" "$output"' \ - 'else' \ - ' dd if="$MOCK_ARTIFACT_ZIP" of="$output" bs=1 skip="$start" count="$count" status=none' \ - 'fi' \ - > "$fake_bin/curl" -chmod +x "$fake_bin/gh" "$fake_bin/curl" - -mock_digest="sha256:$(sha256sum "$tmp/artifact.zip" | awk '{print $1}')" -redirect_log="$tmp/redirect.log" -PATH="$fake_bin:$PATH" \ - GH_TOKEN=test-token \ - GITHUB_REPOSITORY=RaoFoundation/subtensor \ - MOCK_ARTIFACT_ZIP="$tmp/artifact.zip" \ - MOCK_REDIRECT_LOG="$redirect_log" \ - MOCK_FAIL_ONCE_DIR="$tmp/fail-once" \ - ARTIFACT_DOWNLOAD_CONCURRENCY=4 \ - ARTIFACT_RETRY_DELAY_SECONDS=0 \ - "$helper" download 12 "$mock_digest" "$tmp/ranged-download" >/dev/null -grep -qx 'artifact payload' "$tmp/ranged-download/payload.txt" -[[ "$(wc -l < "$redirect_log" | tr -d ' ')" == 2 ]] - -# A server that ignores Range must fail closed after bounded retries and clean -# every temporary part/archive even though the helper exits from the function. -range_tmp="$tmp/range-tmp" -mkdir -p "$range_tmp" -if PATH="$fake_bin:$PATH" \ - TMPDIR="$range_tmp" \ - GH_TOKEN=test-token \ - GITHUB_REPOSITORY=RaoFoundation/subtensor \ - MOCK_ARTIFACT_ZIP="$tmp/artifact.zip" \ - MOCK_REDIRECT_LOG="$tmp/ignored-range-redirect.log" \ - MOCK_IGNORE_RANGE=true \ - ARTIFACT_DOWNLOAD_CONCURRENCY=4 \ - ARTIFACT_RETRY_DELAY_SECONDS=0 \ - "$helper" download 12 "$mock_digest" "$tmp/ignored-range" >/dev/null 2>&1; then - echo "expected ignored range responses to fail" >&2 - exit 1 -fi -[[ -z "$(find "$range_tmp" -mindepth 1 -print -quit)" ]] +grep -qx 'producer-sha=0000000000000000000000000000000000000066' <<<"$selected" # Exactly 72 hours is accepted, one second older fails, and optional lookup # reports a miss. An artifact older than 36 hours remains usable with a warning. @@ -182,8 +125,10 @@ warning_age=$(artifact 18 try-runtime-snap-v0.10.1-mainnet 2026-07-11T01:59:59Z write_artifacts "[$warning_age]" warning_output="$tmp/warning-output" : > "$warning_output" -warning_log=$(ARTIFACTS_JSON_FILE="$tmp/artifacts.json" NOW_EPOCH="$now" \ - "$helper" select try-runtime-snap-v0.10.1-mainnet main "$repo_id" 72 "$warning_output" required) +warning_log=$(ARTIFACTS_JSON_FILE="$tmp/artifacts.json" \ + WORKFLOW_RUNS_JSON_FILE="$tmp/workflow-runs.json" NOW_EPOCH="$now" \ + "$helper" select try-runtime-snap-v0.10.1-mainnet main "$repo_id" \ + .github/workflows/refresh-mainnet-snapshot.yml 72 "$warning_output" required) grep -q '^::warning::' <<<"$warning_log" too_old=$(artifact 20 try-runtime-snap-v0.10.1-mainnet 2026-07-09T13:59:59Z main "$repo_id" false 200) @@ -220,17 +165,27 @@ jq -n \ ' > "$tmp/mainnet.manifest.json" "$helper" validate "$tmp/mainnet.manifest.json" "$tmp/mainnet.snap" mainnet \ - 0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03 0.10.1 >/dev/null + 0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03 \ + 0.10.1 647ca2b0493ed5c74399b73f2595643ba785c1b8 >/dev/null + +if "$helper" validate "$tmp/mainnet.manifest.json" "$tmp/mainnet.snap" mainnet \ + 0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03 \ + 0.10.1 ffffffffffffffffffffffffffffffffffffffff >/dev/null 2>&1; then + echo "expected producer SHA mismatch to fail" >&2 + exit 1 +fi if "$helper" validate "$tmp/mainnet.manifest.json" "$tmp/mainnet.snap" testnet \ - 0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03 0.10.1 >/dev/null 2>&1; then + 0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03 \ + 0.10.1 647ca2b0493ed5c74399b73f2595643ba785c1b8 >/dev/null 2>&1; then echo "expected network mismatch to fail" >&2 exit 1 fi printf 'corrupt' >> "$tmp/mainnet.snap" if "$helper" validate "$tmp/mainnet.manifest.json" "$tmp/mainnet.snap" mainnet \ - 0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03 0.10.1 >/dev/null 2>&1; then + 0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03 \ + 0.10.1 647ca2b0493ed5c74399b73f2595643ba785c1b8 >/dev/null 2>&1; then echo "expected checksum/size mismatch to fail" >&2 exit 1 fi diff --git a/.github/workflows/refresh-mainnet-snapshot.yml b/.github/workflows/refresh-mainnet-snapshot.yml index 822d765b71..64ef08e450 100644 --- a/.github/workflows/refresh-mainnet-snapshot.yml +++ b/.github/workflows/refresh-mainnet-snapshot.yml @@ -24,7 +24,7 @@ on: workflow_dispatch: inputs: try_runtime_only: - description: "Generate only try-runtime snapshots (measurement/debugging)" + description: "Generate only try-runtime snapshots (targeted recovery)" type: boolean default: false @@ -32,10 +32,10 @@ permissions: contents: read concurrency: - # A test-branch dispatch must not cancel the production refresh on main (or - # vice versa). Schedule and dispatch on the same ref still supersede safely. - group: refresh-mainnet-snapshot-${{ github.ref }} - cancel-in-progress: true + # Serialize every ref globally: diagnostic dispatches must not cancel a + # running production refresh or consume a second RPC/runner slot alongside it. + group: refresh-mainnet-snapshot + cancel-in-progress: false env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index ffa4abfbb6..26da00e2b3 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -49,6 +49,7 @@ jobs: name: detect relevant changes runs-on: ubuntu-latest permissions: + contents: read pull-requests: read outputs: runtime: ${{ steps.filter.outputs.runtime }} @@ -57,6 +58,8 @@ jobs: sdk_drift: ${{ steps.filter.outputs.sdk_drift }} snapshot_ci: ${{ steps.filter.outputs.snapshot_ci }} steps: + - uses: actions/checkout@v4 + # Plain gh-api file listing instead of a marketplace action: the org's # Actions allowlist rejects unlisted third-party actions (startup_failure). - name: Filter changed paths @@ -67,28 +70,9 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} run: | set -euo pipefail - files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename') - # SDK-only changes are covered by sdk-checks and the Rust SDK e2e - # workflow; they should not force clone-upgrade or SDK drift. - runtime_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor|clones)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^website/apps/bittensor-website/scripts/|^\.github/(workflows/(runtime-checks|refresh-mainnet-snapshot)\.yml|actions/(rust-setup|sccache-setup)/|scripts/(sccache-configure|snapshot-artifact|test-snapshot-artifact)\.sh)$' - docs_pattern='^website/|^sdk/python/|^\.github/workflows/runtime-checks\.yml$' - python_sdk_pattern='^sdk/(python|bittensor-core|bittensor-core-py|bittensor-core-wasm)/|^Cargo.lock$|^\.github/workflows/runtime-checks\.yml$' - sdk_drift_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$' - snapshot_pattern='^\.github/(workflows/(runtime-checks|refresh-mainnet-snapshot)\.yml|scripts/(snapshot-artifact|test-snapshot-artifact)\.sh)$' - runtime=false; docs=false; python_sdk=false; sdk_drift=false; snapshot_ci=false - grep -qE "$runtime_pattern" <<< "$files" && runtime=true - grep -qE "$docs_pattern" <<< "$files" && docs=true - grep -qE "$python_sdk_pattern" <<< "$files" && python_sdk=true - grep -qE "$sdk_drift_pattern" <<< "$files" && sdk_drift=true - grep -qE "$snapshot_pattern" <<< "$files" && snapshot_ci=true - echo "runtime=$runtime, docs=$docs, python_sdk=$python_sdk, sdk_drift=$sdk_drift, snapshot_ci=$snapshot_ci" - { - echo "runtime=$runtime" - echo "docs=$docs" - echo "python_sdk=$python_sdk" - echo "sdk_drift=$sdk_drift" - echo "snapshot_ci=$snapshot_ci" - } >> "$GITHUB_OUTPUT" + gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" \ + --paginate --jq '.[].filename' \ + | .github/scripts/classify-runtime-changes.sh "$GITHUB_OUTPUT" snapshot-artifact-tests: name: snapshot artifact contract tests @@ -98,6 +82,7 @@ jobs: steps: - uses: actions/checkout@v4 - run: .github/scripts/test-snapshot-artifact.sh + - run: .github/scripts/test-runtime-change-filter.sh # Build the try-runtime wasm independently so its consumers do not wait for # the unrelated release node build. The source check also makes the @@ -229,9 +214,6 @@ jobs: - name: testnet uri: wss://archive.dev.opentensor.ai:8443 genesis: "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105" - # Keep endpoint-heavy checks near the archive RPCs. Measurements - # from BHS showed materially higher per-request latency, which - # compounds during try-runtime's state traversal. - name: mainnet uri: wss://archive.dev.opentensor.ai:443 genesis: "0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03" @@ -294,6 +276,7 @@ jobs: "try-runtime-snap-v${TRY_RUNTIME_VERSION}-${{ matrix.network.name }}" \ "${{ github.event.repository.default_branch }}" \ "${{ github.repository_id }}" \ + .github/workflows/refresh-mainnet-snapshot.yml \ 72 "$GITHUB_OUTPUT" required - name: Mark snapshot restore start @@ -302,14 +285,14 @@ jobs: - name: Download selected try-runtime state snapshot if: steps.state-mode.outputs.fresh-state != 'true' - env: - GH_TOKEN: ${{ github.token }} - ARTIFACT_DOWNLOAD_CONCURRENCY: 16 - run: | - .github/scripts/snapshot-artifact.sh download \ - "${{ steps.snapshot.outputs.artifact-id }}" \ - "${{ steps.snapshot.outputs.digest }}" \ - snap + uses: actions/download-artifact@v4 + with: + artifact-ids: ${{ steps.snapshot.outputs.artifact-id }} + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ steps.snapshot.outputs.run-id }} + path: snap + merge-multiple: true - name: Validate try-runtime state snapshot if: steps.state-mode.outputs.fresh-state != 'true' @@ -320,7 +303,7 @@ jobs: .github/scripts/snapshot-artifact.sh validate \ "$manifest" "$snapshot" \ "${{ matrix.network.name }}" "${{ matrix.network.genesis }}" \ - "$TRY_RUNTIME_VERSION" + "$TRY_RUNTIME_VERSION" "${{ steps.snapshot.outputs.producer-sha }}" echo "TRY_RUNTIME_SNAP=$snapshot" >> "$GITHUB_ENV" restore_seconds=$(($(date -u +%s) - SNAPSHOT_RESTORE_STARTED)) block=$(jq -er '.finalized_block_number' "$manifest") @@ -514,19 +497,21 @@ jobs: mainnet-snapshot \ "${{ github.event.repository.default_branch }}" \ "${{ github.repository_id }}" \ + .github/workflows/refresh-mainnet-snapshot.yml \ 168 "$GITHUB_OUTPUT" optional - name: Download selected mainnet clone snapshot id: download-clone-snapshot if: steps.clone-snapshot.outputs.found == 'true' continue-on-error: true - env: - GH_TOKEN: ${{ github.token }} - run: | - .github/scripts/snapshot-artifact.sh download \ - "${{ steps.clone-snapshot.outputs.artifact-id }}" \ - "${{ steps.clone-snapshot.outputs.digest }}" \ - /tmp/mainnet-snapshot + uses: actions/download-artifact@v4 + with: + artifact-ids: ${{ steps.clone-snapshot.outputs.artifact-id }} + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ steps.clone-snapshot.outputs.run-id }} + path: /tmp/mainnet-snapshot + merge-multiple: true - name: Restore mainnet clone snapshot id: restore-clone-snapshot diff --git a/.github/workflows/sccache-warm.yml b/.github/workflows/sccache-warm.yml index ccce428d32..9ee10b4d21 100644 --- a/.github/workflows/sccache-warm.yml +++ b/.github/workflows/sccache-warm.yml @@ -2,7 +2,7 @@ name: Warm R2 sccache # Normal same-repository PR and main CI writes through while it compiles. This # workflow is maintenance only: refresh deployed-state branches when they move, -# repair expired/missing main entries at 10:00 UTC, or recover manually. +# repair expired/missing main entries at 12:00 UTC, or recover manually. on: push: @@ -10,7 +10,7 @@ on: - devnet - testnet schedule: - - cron: "0 10 * * *" + - cron: "0 12 * * *" workflow_dispatch: inputs: source_ref: diff --git a/.github/workflows/scheduled-smoke-tests.yml b/.github/workflows/scheduled-smoke-tests.yml index a5dbe18333..79b61ca9c5 100644 --- a/.github/workflows/scheduled-smoke-tests.yml +++ b/.github/workflows/scheduled-smoke-tests.yml @@ -1,12 +1,12 @@ name: Scheduled Smoke Tests -# Read-only smoke suites against the live networks, daily at 10:00 UTC. The same +# Read-only smoke suites against the live networks, daily at 12:00 UTC. The same # suites gate promotion in release-train.yml; this catches drift between # trains. on: schedule: - - cron: "0 10 * * *" + - cron: "0 12 * * *" workflow_dispatch: permissions: From 5b0f2ebbcc40207b680b9cb8b76019a756fd0563 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 04:53:35 +0200 Subject: [PATCH 20/68] ci: consolidate and accelerate clone regressions --- .github/scripts/classify-runtime-changes.sh | 2 +- .github/scripts/start-accelerated-clone.sh | 45 +++++ .github/scripts/test-runtime-change-filter.sh | 1 + .github/workflows/runtime-checks.yml | 171 ++++++++++-------- .../js-tests/scripts/run-clone-regressions.ts | 13 +- .../tests/test-total-issuance-trackers.ts | 26 +-- node/src/service.rs | 40 ++-- 7 files changed, 179 insertions(+), 119 deletions(-) create mode 100755 .github/scripts/start-accelerated-clone.sh diff --git a/.github/scripts/classify-runtime-changes.sh b/.github/scripts/classify-runtime-changes.sh index 75827c634c..21029121b9 100755 --- a/.github/scripts/classify-runtime-changes.sh +++ b/.github/scripts/classify-runtime-changes.sh @@ -11,7 +11,7 @@ output_file="$1" # SDK-only changes are covered by sdk-checks and the Rust SDK e2e workflow; # they should not force clone-upgrade or SDK drift. -runtime_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor|clones)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^website/apps/bittensor-website/scripts/|^\.github/(workflows/(runtime-checks|refresh-mainnet-snapshot)\.yml|actions/(rust-setup|sccache-setup)/.*|scripts/(classify-runtime-changes|test-runtime-change-filter|sccache-configure|snapshot-artifact|test-snapshot-artifact)\.sh)$' +runtime_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor|clones)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^website/apps/bittensor-website/scripts/|^\.github/(workflows/(runtime-checks|refresh-mainnet-snapshot)\.yml|actions/(rust-setup|sccache-setup)/.*|scripts/(classify-runtime-changes|test-runtime-change-filter|sccache-configure|snapshot-artifact|start-accelerated-clone|test-snapshot-artifact)\.sh)$' docs_pattern='^website/|^sdk/python/|^\.github/workflows/runtime-checks\.yml$' python_sdk_pattern='^sdk/(python|bittensor-core|bittensor-core-py|bittensor-core-wasm)/|^Cargo\.lock$|^\.github/workflows/runtime-checks\.yml$' sdk_drift_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$' diff --git a/.github/scripts/start-accelerated-clone.sh b/.github/scripts/start-accelerated-clone.sh new file mode 100755 index 0000000000..e3e802330d --- /dev/null +++ b/.github/scripts/start-accelerated-clone.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +log_file="${1:-clone-node.log}" + +nohup ./clones/scripts/start-local-clone.sh --sealing 250 > "$log_file" 2>&1 & + +ready=false +for _ in $(seq 1 450); do + if curl -sf -H "Content-Type: application/json" \ + -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ + http://127.0.0.1:9944 > /dev/null; then + ready=true + break + fi + sleep 2 +done + +if [ "$ready" != true ]; then + echo "Clone node failed to start:" + cat "$log_file" + exit 1 +fi + +height() { + local hex + hex=$(curl -fsS -H "Content-Type: application/json" \ + -d '{"id":1,"jsonrpc":"2.0","method":"chain_getHeader","params":[]}' \ + http://127.0.0.1:9944 | jq -er '.result.number') + echo $((16#${hex#0x})) +} + +first=$(height) +deadline=$(($(date -u +%s) + 60)) +while [ "$(date -u +%s)" -lt "$deadline" ]; do + current=$(height) + if [ "$current" -ge "$((first + 20))" ]; then + echo "Accelerated clone advanced from block $first to $current." + exit 0 + fi + sleep 1 +done + +tail -n 200 "$log_file" +exit 1 diff --git a/.github/scripts/test-runtime-change-filter.sh b/.github/scripts/test-runtime-change-filter.sh index 733f20e367..e46ee24a25 100755 --- a/.github/scripts/test-runtime-change-filter.sh +++ b/.github/scripts/test-runtime-change-filter.sh @@ -25,6 +25,7 @@ assert_classification .github/actions/rust-setup/action.yml "$runtime_only" assert_classification .github/actions/sccache-setup/action.yml "$runtime_only" assert_classification .github/scripts/classify-runtime-changes.sh "$runtime_and_snapshot_only" assert_classification .github/scripts/test-runtime-change-filter.sh "$runtime_and_snapshot_only" +assert_classification .github/scripts/start-accelerated-clone.sh "$runtime_only" assert_classification .github/workflows/runtime-checks.yml "$runtime_and_snapshot" assert_classification $'README.md\nsdk/python/example.py' "$docs_and_python" diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 26da00e2b3..83ef257808 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -425,12 +425,14 @@ jobs: # label is set). Auto-skips (via the `build-node-release` need) when the PR touches # nothing runtime-relevant. # - # The regression tests are wall-clock-bound (they wait on 12-second blocks) - # and mutate chain state, so they can't share one node concurrently. - # Instead each shard boots its own clone from the snapshot and runs a - # subset, preserving the suite's original relative order within each shard. + # Interval sealing advances runtime time by one 12-second slot every 250ms, + # so the block-driven regressions can run sequentially on one runner. The + # issuance-invariant test gets a pristine clone; the already-downloaded + # checkpoint is then restored locally for the remaining tests, before + # forceSetBalance-based fixtures make the issuance mirrors diverge. This + # avoids four runners and four downloads without sharing destructive state. clone-upgrade: - name: clone-upgrade (${{ matrix.shard.name }}) + name: clone-upgrade needs: [trusted-pr, changes, build-node-release] runs-on: [self-hosted, fireactions-turbo-8] if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-clone-upgrade') }} @@ -439,35 +441,6 @@ jobs: contents: read # Cross-run artifact download (the nightly mainnet snapshot). actions: read - strategy: - fail-fast: false - matrix: - shard: - - name: balancer - tests: >- - test-balancer-operation.ts - test-balancer-edge-emission-issuance.ts - sdk: false - - name: locks - tests: >- - test-locks-conviction.ts - test-lock-dust-cleanup.ts - test-proxy-filter-security-regressions.ts - sdk: false - - name: stake-sdk - tests: >- - test-hotkey-swap-and-proxy-stake.ts - test-alpha-deprecated-stake-histogram.ts - sdk: true - # test-total-issuance-trackers legitimately waits ~6 min on chain - # state (see CLONE_REGRESSION_TIMEOUT_MS below), so the two - # emission/issuance tests get their own shard instead of - # serializing behind the stake-sdk pair. - - name: issuance - tests: >- - test-net-tao-flow-emission-allocation.ts - test-total-issuance-trackers.ts - sdk: false steps: - uses: actions/checkout@v4 @@ -520,11 +493,12 @@ jobs: run: | set -euo pipefail tar -xzf /tmp/mainnet-snapshot/mainnet-snapshot.tar.gz - rm -rf /tmp/mainnet-snapshot echo "Restored artifact ${{ steps.clone-snapshot.outputs.artifact-id }} from run ${{ steps.clone-snapshot.outputs.run-id }}." # Tells start-local-clone.sh to keep the pre-initialized database # instead of wiping it and re-running genesis init. echo "KEEP_CLONE_DATA=1" >> "$GITHUB_ENV" + echo "CLONE_CHECKPOINT=/tmp/mainnet-snapshot/mainnet-snapshot.tar.gz" >> "$GITHUB_ENV" + echo "CLONE_CHECKPOINT_GZIP=1" >> "$GITHUB_ENV" - name: Fall back after an unusable clone snapshot if: >- @@ -560,65 +534,119 @@ jobs: done exit 1 - - name: Start local clone + - name: Prepare a reusable checkpoint after live fallback + if: steps.restore-clone-snapshot.outcome != 'success' run: | - nohup ./clones/scripts/start-local-clone.sh > clone-node.log 2>&1 & - # Genesis init from the ~2 GB mainnet-clone chainspec takes several - # minutes before the RPC server comes up, so allow up to 15 minutes. - # (With a restored snapshot the db is pre-initialized and this is - # nearly instant.) - for i in $(seq 1 450); do + set -euo pipefail + nohup ./clones/scripts/start-local-clone.sh --sealing manual > clone-node.log 2>&1 & + for _ in $(seq 1 450); do if curl -sf -H "Content-Type: application/json" \ -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ http://127.0.0.1:9944 > /dev/null; then - echo "Clone node is up." - exit 0 + break fi sleep 2 done - echo "Clone node failed to start:" - cat clone-node.log - exit 1 + if ! curl -sf -H "Content-Type: application/json" \ + -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ + http://127.0.0.1:9944 > /dev/null; then + cat clone-node.log + exit 1 + fi + ./clones/scripts/stop-local-clone.sh + for _ in $(seq 1 60); do + pgrep -f 'node-subtensor.*--base-path clones/mainnet-clone' > /dev/null || break + sleep 1 + done + if pgrep -f 'node-subtensor.*--base-path clones/mainnet-clone' > /dev/null; then + echo "Clone did not stop before checkpointing." + exit 1 + fi + tar -cf /tmp/mainnet-clone-pristine.tar \ + clones/mainnet-clone-chainspec.json \ + clones/mainnet-clone + echo "KEEP_CLONE_DATA=1" >> "$GITHUB_ENV" + echo "CLONE_CHECKPOINT=/tmp/mainnet-clone-pristine.tar" >> "$GITHUB_ENV" + echo "CLONE_CHECKPOINT_GZIP=0" >> "$GITHUB_ENV" - - name: Sudo-upgrade clone with proposed runtime + - name: Start isolated issuance clone + run: .github/scripts/start-accelerated-clone.sh + + - name: Sudo-upgrade issuance clone with proposed runtime working-directory: clones/js-tests run: | npm ci # Right after genesis the node occasionally rejects the first - # extrinsic with "bad signature" (observed: 1 of 3 shards failed at - # +8s while the identical tx succeeded on the others seconds later). + # extrinsic with "bad signature" (observed in the former sharded + # workflow while identical transactions succeeded seconds later). # Setting the same runtime code twice is idempotent, so retry once. npm run runtime:update:alice || { sleep 15; npm run runtime:update:alice; } + - name: Run isolated issuance regression + working-directory: clones/js-tests + env: + CLONE_REGRESSION_TESTS: test-total-issuance-trackers.ts + CLONE_REGRESSION_TIMEOUT_MS: 1800000 + run: npm run test:clone-regressions + + - name: Stop isolated issuance clone + run: | + ./clones/scripts/stop-local-clone.sh + for _ in $(seq 1 60); do + pgrep -f 'node-subtensor.*--base-path clones/mainnet-clone' > /dev/null || exit 0 + sleep 1 + done + exit 1 + + - name: Restore pristine clone for remaining regressions + run: | + set -euo pipefail + rm -rf clones/mainnet-clone clones/mainnet-clone-chainspec.json + if [ "$CLONE_CHECKPOINT_GZIP" = "1" ]; then + tar -xzf "$CLONE_CHECKPOINT" + else + tar -xf "$CLONE_CHECKPOINT" + fi + + - name: Start regression clone + run: .github/scripts/start-accelerated-clone.sh + + - name: Sudo-upgrade regression clone with proposed runtime + working-directory: clones/js-tests + run: npm run runtime:update:alice || { sleep 15; npm run runtime:update:alice; } + - name: Run clone smoke tests working-directory: clones/js-tests run: npm test - - name: Run clone regression tests (shard subset) + - name: Run remaining clone regression tests working-directory: clones/js-tests env: - CLONE_REGRESSION_TESTS: ${{ matrix.shard.tests }} - # test-total-issuance-trackers legitimately waits minutes for the - # queued replacement registration: register_network at the subnet - # limit prunes a subnet and only emits NetworkAdded from on_idle - # after the pruned subnet's chunked storage cleanup finishes - # (~6 min on mainnet-scale state). - CLONE_REGRESSION_TIMEOUT_MS: ${{ matrix.shard.name == 'issuance' && '1800000' || '900000' }} + CLONE_REGRESSION_TESTS: >- + test-balancer-operation.ts + test-balancer-edge-emission-issuance.ts + test-locks-conviction.ts + test-lock-dust-cleanup.ts + test-proxy-filter-security-regressions.ts + test-hotkey-swap-and-proxy-stake.ts + test-net-tao-flow-emission-allocation.ts + test-alpha-deprecated-stake-histogram.ts + CLONE_REGRESSION_TIMEOUT_MS: 1800000 run: npm run test:clone-regressions - name: Install uv - if: ${{ matrix.shard.sdk }} + if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.sdk_drift == 'true' }} run: | curl -LsSf https://astral.sh/uv/0.11.28/install.sh | sh echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Sync SDK environment - if: ${{ matrix.shard.sdk }} + if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.sdk_drift == 'true' }} working-directory: sdk/python run: uv sync --locked --all-extras --dev - name: Metadata drift gate (committed _generated vs upgraded clone) - if: ${{ matrix.shard.sdk && (github.event_name != 'pull_request' || needs.changes.outputs.sdk_drift == 'true') }} + if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.sdk_drift == 'true' }} working-directory: sdk/python run: uv run python -m codegen.check --drift ${{ env.WS_ENDPOINT }} @@ -638,34 +666,33 @@ jobs: run: ./clones/scripts/stop-local-clone.sh # Branch protection requires a check named exactly "Sudo-upgrade mainnet - # clone and test"; the shard matrix above produces differently-named jobs, - # so this fan-in preserves the required-check name. Passes when every shard - # succeeded, or when the whole suite legitimately skipped (docs-only PR or - # the skip-clone-upgrade label). Anything else — including a build failure - # cascading into skipped shards — fails. + # clone and test"; this fan-in preserves the required-check name. It passes + # when the clone job succeeded, or when the whole suite legitimately skipped + # (docs-only PR or the skip-clone-upgrade label). Anything else — including + # a build failure cascading into a skipped clone job — fails. clone-upgrade-gate: name: Sudo-upgrade mainnet clone and test needs: [changes, build-node-release, clone-upgrade] if: always() runs-on: ubuntu-latest steps: - - name: Evaluate shard results + - name: Evaluate clone result env: CHANGES: ${{ needs.changes.result }} - SHARDS: ${{ needs.clone-upgrade.result }} + CLONE: ${{ needs.clone-upgrade.result }} BUILD: ${{ needs.build-node-release.result }} LABEL_SKIP: ${{ contains(github.event.pull_request.labels.*.name, 'skip-clone-upgrade') }} run: | - echo "changes=$CHANGES shards=$SHARDS build=$BUILD label-skip=$LABEL_SKIP" + echo "changes=$CHANGES clone=$CLONE build=$BUILD label-skip=$LABEL_SKIP" # A failed path-filter job must not read as "nothing runtime-related - # changed" — that would let build+shards skip and the gate pass. + # changed" — that would let build+clone skip and the gate pass. if [ "$CHANGES" != "success" ]; then exit 1 fi - if [ "$SHARDS" = "success" ]; then + if [ "$CLONE" = "success" ]; then exit 0 fi - if [ "$SHARDS" = "skipped" ] && { [ "$LABEL_SKIP" = "true" ] || [ "$BUILD" = "skipped" ]; }; then + if [ "$CLONE" = "skipped" ] && { [ "$LABEL_SKIP" = "true" ] || [ "$BUILD" = "skipped" ]; }; then exit 0 fi exit 1 diff --git a/clones/js-tests/scripts/run-clone-regressions.ts b/clones/js-tests/scripts/run-clone-regressions.ts index 1328bb2a3c..5e5587c9c1 100644 --- a/clones/js-tests/scripts/run-clone-regressions.ts +++ b/clones/js-tests/scripts/run-clone-regressions.ts @@ -15,6 +15,9 @@ const testsDir = path.join(__dirname, "..", "tests"); const TEST_TIMEOUT_MS = Number(process.env.CLONE_REGRESSION_TIMEOUT_MS ?? 15 * 60 * 1000); const CLONE_REGRESSIONS = [ + // This test asserts the global issuance mirrors before and after each + // scenario, so run it before forceSetBalance-based tests mutate the clone. + "test-total-issuance-trackers.ts", "test-balancer-operation.ts", "test-balancer-edge-emission-issuance.ts", "test-locks-conviction.ts", @@ -22,7 +25,6 @@ const CLONE_REGRESSIONS = [ "test-proxy-filter-security-regressions.ts", "test-hotkey-swap-and-proxy-stake.ts", "test-net-tao-flow-emission-allocation.ts", - "test-total-issuance-trackers.ts", "test-alpha-deprecated-stake-histogram.ts", ]; @@ -40,8 +42,6 @@ const selected = ? CLONE_REGRESSIONS.filter((name) => requested.includes(name)) : CLONE_REGRESSIONS; -let failed = false; - for (const name of selected) { const script = path.join(testsDir, name); console.log(`\n=== clone regression: ${name} (timeout ${TEST_TIMEOUT_MS}ms) ===`); @@ -51,14 +51,11 @@ for (const name of selected) { timeout: TEST_TIMEOUT_MS, }); if ((result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT") { - failed = true; console.error(`TIMEOUT: ${name} exceeded ${TEST_TIMEOUT_MS}ms`); - continue; + process.exit(1); } if (result.status !== 0) { - failed = true; console.error(`FAILED: ${name}`); + process.exit(1); } } - -process.exit(failed ? 1 : 0); diff --git a/clones/js-tests/tests/test-total-issuance-trackers.ts b/clones/js-tests/tests/test-total-issuance-trackers.ts index 59f6f91e16..4b92eda849 100644 --- a/clones/js-tests/tests/test-total-issuance-trackers.ts +++ b/clones/js-tests/tests/test-total-issuance-trackers.ts @@ -53,7 +53,7 @@ async function main() { console.log("run id:", RUN_ID); assertMetadataAvailable(); - await repairIssuanceMirrorIfNeeded("pre-test setup"); + await assertIssuanceMatch("pre-test setup"); await fundTestAccounts(); await assertIssuanceMatch("initial"); @@ -408,30 +408,6 @@ async function ensureEvmWhitelistDisabled() { console.log("EVM whitelist check disabled"); } -async function repairIssuanceMirrorIfNeeded(label) { - for (let attempt = 1; attempt <= 3; attempt++) { - const balances = (await api.query.balances.totalIssuance()).toBigInt(); - const subtensor: bigint = (await api.query.subtensorModule.totalIssuance()).toBigInt(); - const diff = balances - subtensor; - if (diff === 0n) { - console.log(`${label}: issuance matched`, balances.toString()); - return; - } - - const target = balances + diff; - assert.ok(target > 0n, `cannot repair issuance mirror: computed target ${target}`); - await submitAndWait( - fundSource, - api.tx.sudo.sudo(api.tx.system.setStorage([ - [api.query.subtensorModule.totalIssuance.key(), storageValueHex("u64", target)], - ])), - `sudo repair Subtensor TotalIssuance mirror attempt ${attempt}` - ); - } - - await assertIssuanceMatch(`${label} repaired`); -} - async function assertIssuanceMatch(label) { const [balancesIssuance, subtensorIssuance] = await Promise.all([ api.query.balances.totalIssuance(), diff --git a/node/src/service.rs b/node/src/service.rs index 624f63b968..f7d277eed2 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -24,9 +24,9 @@ use sp_runtime::key_types; use sp_runtime::traits::{Block as BlockT, NumberFor}; use stc_shield::{self, MemoryShieldKeystore}; use std::collections::HashSet; +use std::path::Path; use std::str::FromStr; -use std::sync::atomic::AtomicBool; -use std::{cell::RefCell, path::Path}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::{sync::Arc, time::Duration}; use stp_shield::ShieldKeystorePtr; use substrate_prometheus_endpoint::Registry; @@ -758,11 +758,20 @@ fn run_manual_seal_authorship( shield_keystore.clone(), ); - thread_local!(static TIMESTAMP: RefCell = const { RefCell::new(0) }); - - /// Provide a mock duration starting at 0 in millisecond for timestamp inherent. - /// Each call will increment timestamp by slot_duration making the consensus logic think time has passed. - struct MockTimestampInherentDataProvider; + let timestamp = Arc::new(AtomicU64::new( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() + .min(u64::MAX as u128) as u64, + )); + + /// Provide a synthetic timestamp starting after the current wall-clock slot. + /// Each call advances by one slot, allowing interval sealing to run faster + /// than real time while remaining ahead of cloned consensus state. + struct MockTimestampInherentDataProvider { + timestamp: Arc, + } #[async_trait::async_trait] impl sp_inherents::InherentDataProvider for MockTimestampInherentDataProvider { @@ -770,11 +779,15 @@ fn run_manual_seal_authorship( &self, inherent_data: &mut sp_inherents::InherentData, ) -> Result<(), sp_inherents::Error> { - TIMESTAMP.with(|x| { - let mut x_ref = x.borrow_mut(); - *x_ref = x_ref.saturating_add(subtensor_runtime_common::time::SLOT_DURATION); - inherent_data.put_data(sp_timestamp::INHERENT_IDENTIFIER, &*x_ref) - }) + let slot_duration = subtensor_runtime_common::time::SLOT_DURATION; + let timestamp = self + .timestamp + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { + Some(current.saturating_add(slot_duration)) + }) + .map(|previous| previous.saturating_add(slot_duration)) + .unwrap_or(u64::MAX); + inherent_data.put_data(sp_timestamp::INHERENT_IDENTIFIER, ×tamp) } async fn try_handle_error( @@ -789,9 +802,10 @@ fn run_manual_seal_authorship( let create_inherent_data_providers = move |_, ()| { let keystore = shield_keystore.clone(); + let timestamp = timestamp.clone(); async move { Ok(( - MockTimestampInherentDataProvider, + MockTimestampInherentDataProvider { timestamp }, stc_shield::InherentDataProvider::new(keystore), )) } From 2fbd81496236c792bc90829c2e43910fb103213c Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 05:47:09 +0200 Subject: [PATCH 21/68] ci: trust base revision for runtime path filtering --- .github/scripts/test-runtime-change-filter.sh | 2 + .github/workflows/runtime-checks.yml | 40 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/.github/scripts/test-runtime-change-filter.sh b/.github/scripts/test-runtime-change-filter.sh index e46ee24a25..032f83022f 100755 --- a/.github/scripts/test-runtime-change-filter.sh +++ b/.github/scripts/test-runtime-change-filter.sh @@ -16,6 +16,7 @@ assert_classification() { all_false=$'runtime=false\ndocs=false\npython_sdk=false\nsdk_drift=false\nsnapshot_ci=false' runtime_only=$'runtime=true\ndocs=false\npython_sdk=false\nsdk_drift=false\nsnapshot_ci=false' +runtime_and_sdk=$'runtime=true\ndocs=false\npython_sdk=false\nsdk_drift=true\nsnapshot_ci=false' runtime_and_snapshot_only=$'runtime=true\ndocs=false\npython_sdk=false\nsdk_drift=false\nsnapshot_ci=true' runtime_and_snapshot=$'runtime=true\ndocs=true\npython_sdk=true\nsdk_drift=false\nsnapshot_ci=true' docs_and_python=$'runtime=false\ndocs=true\npython_sdk=true\nsdk_drift=false\nsnapshot_ci=false' @@ -28,5 +29,6 @@ assert_classification .github/scripts/test-runtime-change-filter.sh "$runtime_an assert_classification .github/scripts/start-accelerated-clone.sh "$runtime_only" assert_classification .github/workflows/runtime-checks.yml "$runtime_and_snapshot" assert_classification $'README.md\nsdk/python/example.py' "$docs_and_python" +assert_classification $'README.md\nnode/src/renamed-service.rs' "$runtime_and_sdk" echo "runtime change filter tests passed" diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 83ef257808..0cf9697e90 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -58,7 +58,15 @@ jobs: sdk_drift: ${{ steps.filter.outputs.sdk_drift }} snapshot_ci: ${{ steps.filter.outputs.snapshot_ci }} steps: - - uses: actions/checkout@v4 + - name: Checkout trusted path classifier + if: github.event_name == 'pull_request' + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + sparse-checkout: .github/scripts/classify-runtime-changes.sh + sparse-checkout-cone-mode: false + path: .trusted-runtime-filter + persist-credentials: false # Plain gh-api file listing instead of a marketplace action: the org's # Actions allowlist rejects unlisted third-party actions (startup_failure). @@ -68,11 +76,35 @@ jobs: env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} + CHANGED_FILES: ${{ github.event.pull_request.changed_files }} run: | set -euo pipefail - gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" \ - --paginate --jq '.[].filename' \ - | .github/scripts/classify-runtime-changes.sh "$GITHUB_OUTPUT" + enable_all() { + { + echo "runtime=true" + echo "docs=true" + echo "python_sdk=true" + echo "sdk_drift=true" + echo "snapshot_ci=true" + } >> "$GITHUB_OUTPUT" + } + classifier=.trusted-runtime-filter/.github/scripts/classify-runtime-changes.sh + if [[ ! -f "$classifier" ]]; then + echo "::warning::Trusted base predates the path classifier; enabling every check." + enable_all + exit 0 + fi + pages=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --slurp) + fetched_files=$(jq '[.[][]] | length' <<< "$pages") + if [[ "$fetched_files" -ne "$CHANGED_FILES" ]]; then + echo "::warning::PR file listing was incomplete ($fetched_files/$CHANGED_FILES); enabling every check." + enable_all + exit 0 + fi + # A rename can move a runtime file outside a matched directory, so + # classify both the current and previous paths. + jq -r '.[][] | .filename, (.previous_filename // empty)' <<< "$pages" \ + | bash "$classifier" "$GITHUB_OUTPUT" snapshot-artifact-tests: name: snapshot artifact contract tests From f70fa9e0562bab4b15184704d011e16ab5bd7975 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 05:20:24 +0200 Subject: [PATCH 22/68] ci: test snapshot producer on hosted runners --- .github/workflows/refresh-mainnet-snapshot.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/refresh-mainnet-snapshot.yml b/.github/workflows/refresh-mainnet-snapshot.yml index 64ef08e450..0ee189b11b 100644 --- a/.github/workflows/refresh-mainnet-snapshot.yml +++ b/.github/workflows/refresh-mainnet-snapshot.yml @@ -122,13 +122,12 @@ jobs: # consumer pin in runtime-checks.yml. try-runtime-snapshot: name: try-runtime snapshot ${{ matrix.network.name }} - runs-on: [self-hosted, fireactions-tryruntime] + runs-on: ubuntu-latest timeout-minutes: 90 strategy: fail-fast: false - # The shared pool has enough capacity for the three PR consumers. Keep - # the off-critical-path producer to one slot so a refresh cannot crowd - # those consumers out. + # Snapshot production is off the PR critical path. Serialize the network + # jobs to bound archive RPC load and hosted-runner usage. max-parallel: 1 matrix: network: @@ -198,12 +197,16 @@ jobs: export RUST_LOG=remote-ext=debug snapshot="${{ matrix.network.name }}.snap" manifest="${{ matrix.network.name }}.manifest.json" + resource_usage="$RUNNER_TEMP/${{ matrix.network.name }}-snapshot-resources.txt" + trap 'cat "$resource_usage" 2>/dev/null || true' EXIT + df -h "$GITHUB_WORKSPACE" + free -h # The positional path must precede --uri because --uri is variadic. # remote-externalities creates four value-fetch workers per URI. The # archive endpoint is dedicated to this workload, so open four # independent clients to it (16 value-fetch workers in total). rpc_uri="${{ matrix.network.uri }}" - "$TRY_RUNTIME_BIN" create-snapshot \ + /usr/bin/time -v -o "$resource_usage" "$TRY_RUNTIME_BIN" create-snapshot \ "$snapshot" \ --uri "$rpc_uri" "$rpc_uri" "$rpc_uri" "$rpc_uri" \ --at "${{ steps.source.outputs.block-hash }}" From 650ccd4feff96f29a105a4666d6ce4c14095e244 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 06:09:10 +0200 Subject: [PATCH 23/68] ci: retire dedicated try-runtime runners --- .github/actionlint.yaml | 1 - .github/workflows/refresh-mainnet-snapshot.yml | 5 ++++- .github/workflows/release-train.yml | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index fa03ffb6eb..c4aacd6b88 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -3,5 +3,4 @@ self-hosted-runner: labels: - fireactions-turbo-16 - fireactions-turbo-8 - - fireactions-tryruntime - Benchmarking diff --git a/.github/workflows/refresh-mainnet-snapshot.yml b/.github/workflows/refresh-mainnet-snapshot.yml index 0ee189b11b..6a67dd1b13 100644 --- a/.github/workflows/refresh-mainnet-snapshot.yml +++ b/.github/workflows/refresh-mainnet-snapshot.yml @@ -122,7 +122,10 @@ jobs: # consumer pin in runtime-checks.yml. try-runtime-snapshot: name: try-runtime snapshot ${{ matrix.network.name }} - runs-on: ubuntu-latest + # Snapshot assembly exceeds the memory available on ubuntu-latest. This is + # scheduled, off-critical-path work, so use the shared 8-core pool instead + # of retaining a dedicated runner host. + runs-on: [self-hosted, fireactions-turbo-8] timeout-minutes: 90 strategy: fail-fast: false diff --git a/.github/workflows/release-train.yml b/.github/workflows/release-train.yml index 7869eb3bf5..ee96c6eda5 100644 --- a/.github/workflows/release-train.yml +++ b/.github/workflows/release-train.yml @@ -190,7 +190,7 @@ jobs: check-devnet: name: Smoke-check devnet needs: deploy-devnet - runs-on: [self-hosted, fireactions-tryruntime] + runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 @@ -278,7 +278,7 @@ jobs: check-testnet: name: Smoke-check testnet needs: deploy-testnet - runs-on: [self-hosted, fireactions-tryruntime] + runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 From 35aee6940c0295b978d076fcdfbaf1e858448855 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 06:10:51 +0200 Subject: [PATCH 24/68] ci: drop hosted-only snapshot telemetry --- .github/workflows/refresh-mainnet-snapshot.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/refresh-mainnet-snapshot.yml b/.github/workflows/refresh-mainnet-snapshot.yml index 6a67dd1b13..fbe4c6468b 100644 --- a/.github/workflows/refresh-mainnet-snapshot.yml +++ b/.github/workflows/refresh-mainnet-snapshot.yml @@ -130,7 +130,7 @@ jobs: strategy: fail-fast: false # Snapshot production is off the PR critical path. Serialize the network - # jobs to bound archive RPC load and hosted-runner usage. + # jobs to bound archive RPC load and shared-runner usage. max-parallel: 1 matrix: network: @@ -200,16 +200,12 @@ jobs: export RUST_LOG=remote-ext=debug snapshot="${{ matrix.network.name }}.snap" manifest="${{ matrix.network.name }}.manifest.json" - resource_usage="$RUNNER_TEMP/${{ matrix.network.name }}-snapshot-resources.txt" - trap 'cat "$resource_usage" 2>/dev/null || true' EXIT - df -h "$GITHUB_WORKSPACE" - free -h # The positional path must precede --uri because --uri is variadic. # remote-externalities creates four value-fetch workers per URI. The # archive endpoint is dedicated to this workload, so open four # independent clients to it (16 value-fetch workers in total). rpc_uri="${{ matrix.network.uri }}" - /usr/bin/time -v -o "$resource_usage" "$TRY_RUNTIME_BIN" create-snapshot \ + "$TRY_RUNTIME_BIN" create-snapshot \ "$snapshot" \ --uri "$rpc_uri" "$rpc_uri" "$rpc_uri" "$rpc_uri" \ --at "${{ steps.source.outputs.block-hash }}" From 9b2e9739222450dab35f538b41513cd776229f6e Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 11:36:33 +0200 Subject: [PATCH 25/68] ci: cache SDK core builds in clone checks --- .github/workflows/runtime-checks.yml | 21 +++++++++++++++++++ .../tests/test-hotkey-swap-and-proxy-stake.ts | 3 ++- .../test-proxy-filter-security-regressions.ts | 3 ++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 0cf9697e90..449284fea0 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -427,10 +427,20 @@ jobs: (github.event_name != 'pull_request' || needs.changes.outputs.runtime == 'true' || needs.changes.outputs.python_sdk == 'true') && !(github.event_name == 'workflow_dispatch' && inputs.try_runtime_only) runs-on: [self-hosted, fireactions-turbo-8] + environment: + name: ${{ github.event_name == 'workflow_dispatch' && 'sccache-reader' || 'sccache-writer' }} + deployment: false timeout-minutes: 30 steps: - uses: actions/checkout@v4 + - name: Enable shared compiler cache for SDK core + uses: ./.github/actions/sccache-setup + with: + credential-mode: auto + writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} + writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} + - name: Install uv run: | curl -LsSf https://astral.sh/uv/0.11.28/install.sh | sh @@ -467,6 +477,9 @@ jobs: name: clone-upgrade needs: [trusted-pr, changes, build-node-release] runs-on: [self-hosted, fireactions-turbo-8] + environment: + name: ${{ github.event_name == 'workflow_dispatch' && 'sccache-reader' || 'sccache-writer' }} + deployment: false if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-clone-upgrade') }} timeout-minutes: 90 permissions: @@ -476,6 +489,14 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Enable shared compiler cache for SDK core + if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.sdk_drift == 'true' }} + uses: ./.github/actions/sccache-setup + with: + credential-mode: auto + writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} + writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} + - name: Download release node + runtime wasm uses: actions/download-artifact@v4 with: diff --git a/clones/js-tests/tests/test-hotkey-swap-and-proxy-stake.ts b/clones/js-tests/tests/test-hotkey-swap-and-proxy-stake.ts index 1ce7ab8954..94282212f5 100644 --- a/clones/js-tests/tests/test-hotkey-swap-and-proxy-stake.ts +++ b/clones/js-tests/tests/test-hotkey-swap-and-proxy-stake.ts @@ -7,6 +7,7 @@ import { connectApi } from "../lib/api.js"; import { createTempLogger } from "../lib/file-log.js"; const WS_ENDPOINT = process.env.WS_ENDPOINT ?? "ws://127.0.0.1:9944"; +const BLOCK_PRODUCTION_POLL_MS = 1_000; const RUN_ID = process.env.HOTKEY_PROXY_RUN_ID ?? `run${Date.now()}p${process.pid}`; const FUND_SOURCE_URI = process.env.HOTKEY_PROXY_FUND_SOURCE_URI ?? "//Alice"; const FUND_AMOUNT = BigInt(process.env.HOTKEY_PROXY_FUND_AMOUNT ?? "5000000000000"); @@ -104,7 +105,7 @@ async function waitForBlockProduction() { const deadline = Date.now() + 180_000; while (Date.now() < deadline && observed.length < 2) { - await sleep(6_000); + await sleep(BLOCK_PRODUCTION_POLL_MS); const current = (await api.rpc.chain.getHeader()).number.toBigInt(); if (current > previous) { observed.push(current); diff --git a/clones/js-tests/tests/test-proxy-filter-security-regressions.ts b/clones/js-tests/tests/test-proxy-filter-security-regressions.ts index 2e3768d46e..1736632b13 100644 --- a/clones/js-tests/tests/test-proxy-filter-security-regressions.ts +++ b/clones/js-tests/tests/test-proxy-filter-security-regressions.ts @@ -6,6 +6,7 @@ import { connectApi } from "../lib/api.js"; import { createTempLogger } from "../lib/file-log.js"; const WS_ENDPOINT = process.env.WS_ENDPOINT ?? "ws://127.0.0.1:9944"; +const BLOCK_PRODUCTION_POLL_MS = 1_000; const RUN_ID = process.env.PROXY_FILTER_RUN_ID ?? `run${Date.now()}p${process.pid}`; const FUND_SOURCE_URI = process.env.PROXY_FILTER_FUND_SOURCE_URI ?? "//Alice"; const FUND_AMOUNT = BigInt(process.env.PROXY_FILTER_FUND_AMOUNT ?? "5000000000000"); @@ -115,7 +116,7 @@ async function waitForBlockProduction() { const deadline = Date.now() + 180_000; while (Date.now() < deadline && observed.length < 2) { - await sleep(6_000); + await sleep(BLOCK_PRODUCTION_POLL_MS); const current = (await api.rpc.chain.getHeader()).number.toBigInt(); if (current > previous) { observed.push(current); From b7e089b350b4ed0e3dbfd6c63eb9e17096e44b32 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 11:52:50 +0200 Subject: [PATCH 26/68] ci: drop ineffective SDK compiler cache --- .github/workflows/runtime-checks.yml | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 449284fea0..43c1cca34b 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -427,20 +427,10 @@ jobs: (github.event_name != 'pull_request' || needs.changes.outputs.runtime == 'true' || needs.changes.outputs.python_sdk == 'true') && !(github.event_name == 'workflow_dispatch' && inputs.try_runtime_only) runs-on: [self-hosted, fireactions-turbo-8] - environment: - name: ${{ github.event_name == 'workflow_dispatch' && 'sccache-reader' || 'sccache-writer' }} - deployment: false timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - name: Enable shared compiler cache for SDK core - uses: ./.github/actions/sccache-setup - with: - credential-mode: auto - writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} - writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} - - name: Install uv run: | curl -LsSf https://astral.sh/uv/0.11.28/install.sh | sh @@ -477,9 +467,6 @@ jobs: name: clone-upgrade needs: [trusted-pr, changes, build-node-release] runs-on: [self-hosted, fireactions-turbo-8] - environment: - name: ${{ github.event_name == 'workflow_dispatch' && 'sccache-reader' || 'sccache-writer' }} - deployment: false if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-clone-upgrade') }} timeout-minutes: 90 permissions: @@ -489,14 +476,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Enable shared compiler cache for SDK core - if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.sdk_drift == 'true' }} - uses: ./.github/actions/sccache-setup - with: - credential-mode: auto - writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} - writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} - - name: Download release node + runtime wasm uses: actions/download-artifact@v4 with: @@ -675,6 +654,10 @@ jobs: - name: Run remaining clone regression tests working-directory: clones/js-tests env: + # This scan is against the isolated local clone. Larger pages reduce + # hundreds of loopback RPC round trips without increasing public RPC + # load or changing the assertions. + ALPHA_HISTOGRAM_PAGE_SIZE: 5000 CLONE_REGRESSION_TESTS: >- test-balancer-operation.ts test-balancer-edge-emission-issuance.ts From 1687152c3a58edbc1a24f5dacd4cbb4b2324ea83 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 12:03:15 +0200 Subject: [PATCH 27/68] ci: respect clone RPC page limit --- .github/workflows/runtime-checks.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 43c1cca34b..0cf9697e90 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -654,10 +654,6 @@ jobs: - name: Run remaining clone regression tests working-directory: clones/js-tests env: - # This scan is against the isolated local clone. Larger pages reduce - # hundreds of loopback RPC round trips without increasing public RPC - # load or changing the assertions. - ALPHA_HISTOGRAM_PAGE_SIZE: 5000 CLONE_REGRESSION_TESTS: >- test-balancer-operation.ts test-balancer-edge-emission-issuance.ts From 0fffc8818bd650fd4fead2b9488525c6fb9daa12 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 12:38:22 +0200 Subject: [PATCH 28/68] ci: simplify clone runtime orchestration --- .github/scripts/classify-runtime-changes.sh | 51 +++++++--- .github/scripts/start-accelerated-clone.sh | 45 --------- .github/scripts/test-runtime-change-filter.sh | 9 +- .../workflows/refresh-mainnet-snapshot.yml | 43 ++++----- .github/workflows/runtime-checks.yml | 80 ++++------------ .../js-tests/scripts/run-clone-regressions.ts | 66 +++++++++---- clones/scripts/local-clone-checkpoint.sh | 55 +++++++++++ clones/scripts/start-local-clone-and-wait.sh | 92 +++++++++++++++++++ clones/scripts/stop-local-clone.sh | 38 +++++++- 9 files changed, 309 insertions(+), 170 deletions(-) delete mode 100755 .github/scripts/start-accelerated-clone.sh create mode 100755 clones/scripts/local-clone-checkpoint.sh create mode 100755 clones/scripts/start-local-clone-and-wait.sh diff --git a/.github/scripts/classify-runtime-changes.sh b/.github/scripts/classify-runtime-changes.sh index 21029121b9..521142e17f 100755 --- a/.github/scripts/classify-runtime-changes.sh +++ b/.github/scripts/classify-runtime-changes.sh @@ -9,25 +9,48 @@ fi output_file="$1" -# SDK-only changes are covered by sdk-checks and the Rust SDK e2e workflow; -# they should not force clone-upgrade or SDK drift. -runtime_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor|clones)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^website/apps/bittensor-website/scripts/|^\.github/(workflows/(runtime-checks|refresh-mainnet-snapshot)\.yml|actions/(rust-setup|sccache-setup)/.*|scripts/(classify-runtime-changes|test-runtime-change-filter|sccache-configure|snapshot-artifact|start-accelerated-clone|test-snapshot-artifact)\.sh)$' -docs_pattern='^website/|^sdk/python/|^\.github/workflows/runtime-checks\.yml$' -python_sdk_pattern='^sdk/(python|bittensor-core|bittensor-core-py|bittensor-core-wasm)/|^Cargo\.lock$|^\.github/workflows/runtime-checks\.yml$' -sdk_drift_pattern='^(common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$' -snapshot_pattern='^\.github/(workflows/(runtime-checks|refresh-mainnet-snapshot)\.yml|scripts/(classify-runtime-changes|test-runtime-change-filter|snapshot-artifact|test-snapshot-artifact)\.sh)$' - -files=$(< /dev/stdin) runtime=false docs=false python_sdk=false sdk_drift=false snapshot_ci=false -grep -qE "$runtime_pattern" <<< "$files" && runtime=true -grep -qE "$docs_pattern" <<< "$files" && docs=true -grep -qE "$python_sdk_pattern" <<< "$files" && python_sdk=true -grep -qE "$sdk_drift_pattern" <<< "$files" && sdk_drift=true -grep -qE "$snapshot_pattern" <<< "$files" && snapshot_ci=true + +# Keep path ownership explicit. SDK-only changes are covered by sdk-checks and +# the Rust SDK e2e workflow; they should not force clone-upgrade or SDK drift. +while IFS= read -r path; do + case "$path" in + common/*|node/*|pallets/*|precompiles/*|primitives/*|runtime/*|support/*|chain-extensions/*|src/*|vendor/*|Cargo.toml|build.rs|rust-toolchain.toml) + runtime=true + sdk_drift=true + ;; + clones/*|website/apps/bittensor-website/scripts/*) + runtime=true + ;; + .github/workflows/runtime-checks.yml|.github/workflows/refresh-mainnet-snapshot.yml|.github/actions/rust-setup/*|.github/actions/sccache-setup/*|.github/scripts/sccache-configure.sh) + runtime=true + ;; + .github/scripts/classify-runtime-changes.sh|.github/scripts/test-runtime-change-filter.sh|.github/scripts/snapshot-artifact.sh|.github/scripts/test-snapshot-artifact.sh) + runtime=true + snapshot_ci=true + ;; + esac + + case "$path" in + website/*|sdk/python/*|.github/workflows/runtime-checks.yml) docs=true ;; + esac + + case "$path" in + sdk/python/*|sdk/bittensor-core/*|sdk/bittensor-core-py/*|sdk/bittensor-core-wasm/*|Cargo.lock|.github/workflows/runtime-checks.yml) + python_sdk=true + ;; + esac + + case "$path" in + .github/workflows/runtime-checks.yml|.github/workflows/refresh-mainnet-snapshot.yml) + snapshot_ci=true + ;; + esac +done echo "runtime=$runtime, docs=$docs, python_sdk=$python_sdk, sdk_drift=$sdk_drift, snapshot_ci=$snapshot_ci" { diff --git a/.github/scripts/start-accelerated-clone.sh b/.github/scripts/start-accelerated-clone.sh deleted file mode 100755 index e3e802330d..0000000000 --- a/.github/scripts/start-accelerated-clone.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log_file="${1:-clone-node.log}" - -nohup ./clones/scripts/start-local-clone.sh --sealing 250 > "$log_file" 2>&1 & - -ready=false -for _ in $(seq 1 450); do - if curl -sf -H "Content-Type: application/json" \ - -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ - http://127.0.0.1:9944 > /dev/null; then - ready=true - break - fi - sleep 2 -done - -if [ "$ready" != true ]; then - echo "Clone node failed to start:" - cat "$log_file" - exit 1 -fi - -height() { - local hex - hex=$(curl -fsS -H "Content-Type: application/json" \ - -d '{"id":1,"jsonrpc":"2.0","method":"chain_getHeader","params":[]}' \ - http://127.0.0.1:9944 | jq -er '.result.number') - echo $((16#${hex#0x})) -} - -first=$(height) -deadline=$(($(date -u +%s) + 60)) -while [ "$(date -u +%s)" -lt "$deadline" ]; do - current=$(height) - if [ "$current" -ge "$((first + 20))" ]; then - echo "Accelerated clone advanced from block $first to $current." - exit 0 - fi - sleep 1 -done - -tail -n 200 "$log_file" -exit 1 diff --git a/.github/scripts/test-runtime-change-filter.sh b/.github/scripts/test-runtime-change-filter.sh index 032f83022f..8ee778d3cb 100755 --- a/.github/scripts/test-runtime-change-filter.sh +++ b/.github/scripts/test-runtime-change-filter.sh @@ -17,17 +17,24 @@ assert_classification() { all_false=$'runtime=false\ndocs=false\npython_sdk=false\nsdk_drift=false\nsnapshot_ci=false' runtime_only=$'runtime=true\ndocs=false\npython_sdk=false\nsdk_drift=false\nsnapshot_ci=false' runtime_and_sdk=$'runtime=true\ndocs=false\npython_sdk=false\nsdk_drift=true\nsnapshot_ci=false' +runtime_and_docs=$'runtime=true\ndocs=true\npython_sdk=false\nsdk_drift=false\nsnapshot_ci=false' runtime_and_snapshot_only=$'runtime=true\ndocs=false\npython_sdk=false\nsdk_drift=false\nsnapshot_ci=true' runtime_and_snapshot=$'runtime=true\ndocs=true\npython_sdk=true\nsdk_drift=false\nsnapshot_ci=true' docs_and_python=$'runtime=false\ndocs=true\npython_sdk=true\nsdk_drift=false\nsnapshot_ci=false' +python_only=$'runtime=false\ndocs=false\npython_sdk=true\nsdk_drift=false\nsnapshot_ci=false' assert_classification README.md "$all_false" assert_classification .github/actions/rust-setup/action.yml "$runtime_only" assert_classification .github/actions/sccache-setup/action.yml "$runtime_only" assert_classification .github/scripts/classify-runtime-changes.sh "$runtime_and_snapshot_only" assert_classification .github/scripts/test-runtime-change-filter.sh "$runtime_and_snapshot_only" -assert_classification .github/scripts/start-accelerated-clone.sh "$runtime_only" +assert_classification clones/scripts/start-local-clone-and-wait.sh "$runtime_only" +assert_classification .github/workflows/refresh-mainnet-snapshot.yml "$runtime_and_snapshot_only" assert_classification .github/workflows/runtime-checks.yml "$runtime_and_snapshot" +assert_classification website/apps/bittensor-website/scripts/generate-metadata.mjs "$runtime_and_docs" +assert_classification sdk/bittensor-core/src/lib.rs "$python_only" +assert_classification Cargo.lock "$python_only" +assert_classification rust-toolchain.toml "$runtime_and_sdk" assert_classification $'README.md\nsdk/python/example.py' "$docs_and_python" assert_classification $'README.md\nnode/src/renamed-service.rs' "$runtime_and_sdk" diff --git a/.github/workflows/refresh-mainnet-snapshot.yml b/.github/workflows/refresh-mainnet-snapshot.yml index fbe4c6468b..cc98c77aa6 100644 --- a/.github/workflows/refresh-mainnet-snapshot.yml +++ b/.github/workflows/refresh-mainnet-snapshot.yml @@ -40,6 +40,8 @@ concurrency: env: CARGO_TERM_COLOR: always TRY_RUNTIME_VERSION: "0.10.1" + # v0.10.1 exposes parallelism only through the number of URI values. + SNAPSHOT_RPC_CLIENTS: "4" jobs: snapshot: @@ -73,31 +75,11 @@ jobs: - name: Genesis-init the local clone database run: | - nohup ./clones/scripts/start-local-clone.sh > clone-node.log 2>&1 & # Genesis init from the ~2 GB chainspec takes several minutes before - # the RPC server comes up. - up=false - for _ in $(seq 1 450); do - if curl -sf -H "Content-Type: application/json" \ - -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ - http://127.0.0.1:9944 > /dev/null; then - up=true - break - fi - sleep 2 - done - if [ "$up" != true ]; then - echo "Clone node failed to start:" - cat clone-node.log - exit 1 - fi + # the RPC server comes up. Preserve the node's default consensus mode + # and only require RPC health before packaging. + ./clones/scripts/start-local-clone-and-wait.sh node-default ./clones/scripts/stop-local-clone.sh - # pkill returns before the node finishes flushing the db; wait for - # the process to actually exit before packaging. - for _ in $(seq 1 60); do - pgrep -f "node-subtensor.*--base-path clones/mainnet-clone" > /dev/null || break - sleep 2 - done - name: Package snapshot run: | @@ -201,13 +183,20 @@ jobs: snapshot="${{ matrix.network.name }}.snap" manifest="${{ matrix.network.name }}.manifest.json" # The positional path must precede --uri because --uri is variadic. - # remote-externalities creates four value-fetch workers per URI. The - # archive endpoint is dedicated to this workload, so open four - # independent clients to it (16 value-fetch workers in total). + # Each client creates four remote-externalities value-fetch workers; + # the archive endpoint is dedicated to this workload. rpc_uri="${{ matrix.network.uri }}" + [[ "$SNAPSHOT_RPC_CLIENTS" =~ ^[1-9][0-9]*$ ]] || { + echo "invalid SNAPSHOT_RPC_CLIENTS: $SNAPSHOT_RPC_CLIENTS" >&2 + exit 2 + } + rpc_uris=() + for ((client = 0; client < SNAPSHOT_RPC_CLIENTS; client++)); do + rpc_uris+=("$rpc_uri") + done "$TRY_RUNTIME_BIN" create-snapshot \ "$snapshot" \ - --uri "$rpc_uri" "$rpc_uri" "$rpc_uri" "$rpc_uri" \ + --uri "${rpc_uris[@]}" \ --at "${{ steps.source.outputs.block-hash }}" snapshot_size=$(stat -c '%s' "$snapshot") diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml index 0cf9697e90..a5e9f100e6 100644 --- a/.github/workflows/runtime-checks.yml +++ b/.github/workflows/runtime-checks.yml @@ -524,13 +524,13 @@ jobs: continue-on-error: true run: | set -euo pipefail - tar -xzf /tmp/mainnet-snapshot/mainnet-snapshot.tar.gz + checkpoint=/tmp/mainnet-snapshot/mainnet-snapshot.tar.gz + ./clones/scripts/local-clone-checkpoint.sh restore "$checkpoint" echo "Restored artifact ${{ steps.clone-snapshot.outputs.artifact-id }} from run ${{ steps.clone-snapshot.outputs.run-id }}." # Tells start-local-clone.sh to keep the pre-initialized database # instead of wiping it and re-running genesis init. echo "KEEP_CLONE_DATA=1" >> "$GITHUB_ENV" - echo "CLONE_CHECKPOINT=/tmp/mainnet-snapshot/mainnet-snapshot.tar.gz" >> "$GITHUB_ENV" - echo "CLONE_CHECKPOINT_GZIP=1" >> "$GITHUB_ENV" + echo "CLONE_CHECKPOINT=$checkpoint" >> "$GITHUB_ENV" - name: Fall back after an unusable clone snapshot if: >- @@ -539,7 +539,8 @@ jobs: steps.restore-clone-snapshot.outcome != 'success') run: | echo "::warning::selected clone snapshot was unusable; falling back to a live mainnet scrape" - rm -rf /tmp/mainnet-snapshot clones/mainnet-clone clones/mainnet-clone-chainspec.json + ./clones/scripts/local-clone-checkpoint.sh clear + rm -rf /tmp/mainnet-snapshot - name: Fall back after clone snapshot lookup failure if: steps.clone-snapshot.outcome == 'failure' @@ -570,44 +571,22 @@ jobs: if: steps.restore-clone-snapshot.outcome != 'success' run: | set -euo pipefail - nohup ./clones/scripts/start-local-clone.sh --sealing manual > clone-node.log 2>&1 & - for _ in $(seq 1 450); do - if curl -sf -H "Content-Type: application/json" \ - -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ - http://127.0.0.1:9944 > /dev/null; then - break - fi - sleep 2 - done - if ! curl -sf -H "Content-Type: application/json" \ - -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ - http://127.0.0.1:9944 > /dev/null; then - cat clone-node.log - exit 1 - fi - ./clones/scripts/stop-local-clone.sh - for _ in $(seq 1 60); do - pgrep -f 'node-subtensor.*--base-path clones/mainnet-clone' > /dev/null || break - sleep 1 - done - if pgrep -f 'node-subtensor.*--base-path clones/mainnet-clone' > /dev/null; then - echo "Clone did not stop before checkpointing." - exit 1 - fi - tar -cf /tmp/mainnet-clone-pristine.tar \ - clones/mainnet-clone-chainspec.json \ - clones/mainnet-clone + ./clones/scripts/start-local-clone-and-wait.sh manual + checkpoint=/tmp/mainnet-clone-pristine.tar + ./clones/scripts/local-clone-checkpoint.sh create "$checkpoint" echo "KEEP_CLONE_DATA=1" >> "$GITHUB_ENV" - echo "CLONE_CHECKPOINT=/tmp/mainnet-clone-pristine.tar" >> "$GITHUB_ENV" - echo "CLONE_CHECKPOINT_GZIP=0" >> "$GITHUB_ENV" + echo "CLONE_CHECKPOINT=$checkpoint" >> "$GITHUB_ENV" + + - name: Install clone test dependencies + working-directory: clones/js-tests + run: npm ci - name: Start isolated issuance clone - run: .github/scripts/start-accelerated-clone.sh + run: ./clones/scripts/start-local-clone-and-wait.sh accelerated - name: Sudo-upgrade issuance clone with proposed runtime working-directory: clones/js-tests run: | - npm ci # Right after genesis the node occasionally rejects the first # extrinsic with "bad signature" (observed in the former sharded # workflow while identical transactions succeeded seconds later). @@ -617,31 +596,18 @@ jobs: - name: Run isolated issuance regression working-directory: clones/js-tests env: - CLONE_REGRESSION_TESTS: test-total-issuance-trackers.ts + CLONE_REGRESSION_PHASE: pristine CLONE_REGRESSION_TIMEOUT_MS: 1800000 run: npm run test:clone-regressions - name: Stop isolated issuance clone - run: | - ./clones/scripts/stop-local-clone.sh - for _ in $(seq 1 60); do - pgrep -f 'node-subtensor.*--base-path clones/mainnet-clone' > /dev/null || exit 0 - sleep 1 - done - exit 1 + run: ./clones/scripts/stop-local-clone.sh - name: Restore pristine clone for remaining regressions - run: | - set -euo pipefail - rm -rf clones/mainnet-clone clones/mainnet-clone-chainspec.json - if [ "$CLONE_CHECKPOINT_GZIP" = "1" ]; then - tar -xzf "$CLONE_CHECKPOINT" - else - tar -xf "$CLONE_CHECKPOINT" - fi + run: ./clones/scripts/local-clone-checkpoint.sh restore "$CLONE_CHECKPOINT" - name: Start regression clone - run: .github/scripts/start-accelerated-clone.sh + run: ./clones/scripts/start-local-clone-and-wait.sh accelerated - name: Sudo-upgrade regression clone with proposed runtime working-directory: clones/js-tests @@ -654,15 +620,7 @@ jobs: - name: Run remaining clone regression tests working-directory: clones/js-tests env: - CLONE_REGRESSION_TESTS: >- - test-balancer-operation.ts - test-balancer-edge-emission-issuance.ts - test-locks-conviction.ts - test-lock-dust-cleanup.ts - test-proxy-filter-security-regressions.ts - test-hotkey-swap-and-proxy-stake.ts - test-net-tao-flow-emission-allocation.ts - test-alpha-deprecated-stake-histogram.ts + CLONE_REGRESSION_PHASE: remaining CLONE_REGRESSION_TIMEOUT_MS: 1800000 run: npm run test:clone-regressions diff --git a/clones/js-tests/scripts/run-clone-regressions.ts b/clones/js-tests/scripts/run-clone-regressions.ts index 5e5587c9c1..f0205022e3 100644 --- a/clones/js-tests/scripts/run-clone-regressions.ts +++ b/clones/js-tests/scripts/run-clone-regressions.ts @@ -14,35 +14,63 @@ const testsDir = path.join(__dirname, "..", "tests"); // beats burning half an hour of runner time per stuck test. const TEST_TIMEOUT_MS = Number(process.env.CLONE_REGRESSION_TIMEOUT_MS ?? 15 * 60 * 1000); +const CLONE_REGRESSION_PHASES = ["pristine", "remaining"] as const; +type CloneRegressionPhase = (typeof CLONE_REGRESSION_PHASES)[number]; + const CLONE_REGRESSIONS = [ // This test asserts the global issuance mirrors before and after each - // scenario, so run it before forceSetBalance-based tests mutate the clone. - "test-total-issuance-trackers.ts", - "test-balancer-operation.ts", - "test-balancer-edge-emission-issuance.ts", - "test-locks-conviction.ts", - "test-lock-dust-cleanup.ts", - "test-proxy-filter-security-regressions.ts", - "test-hotkey-swap-and-proxy-stake.ts", - "test-net-tao-flow-emission-allocation.ts", - "test-alpha-deprecated-stake-histogram.ts", -]; - -// CI shards the suite across parallel clones; CLONE_REGRESSION_TESTS selects -// a subset (whitespace/comma separated). Unknown names fail loudly so a typo -// in a shard definition can't silently skip a regression test. + // scenario, so it owns the pristine phase before forceSetBalance-based tests + // mutate the clone. + { name: "test-total-issuance-trackers.ts", phase: "pristine" }, + { name: "test-balancer-operation.ts", phase: "remaining" }, + { name: "test-balancer-edge-emission-issuance.ts", phase: "remaining" }, + { name: "test-locks-conviction.ts", phase: "remaining" }, + { name: "test-lock-dust-cleanup.ts", phase: "remaining" }, + { name: "test-proxy-filter-security-regressions.ts", phase: "remaining" }, + { name: "test-hotkey-swap-and-proxy-stake.ts", phase: "remaining" }, + { name: "test-net-tao-flow-emission-allocation.ts", phase: "remaining" }, + { name: "test-alpha-deprecated-stake-histogram.ts", phase: "remaining" }, +] as const satisfies ReadonlyArray<{ name: string; phase: CloneRegressionPhase }>; + +const regressionNames = new Set(CLONE_REGRESSIONS.map(({ name }) => name)); +const regressionPhases = new Set(CLONE_REGRESSION_PHASES); + +// CI selects a named phase, keeping suite membership canonical here. Explicit +// filename selection remains available for targeted local/debug runs. +const requestedPhase = (process.env.CLONE_REGRESSION_PHASE ?? "").trim(); const requested = (process.env.CLONE_REGRESSION_TESTS ?? "").split(/[\s,]+/).filter(Boolean); -const unknown = requested.filter((name) => !CLONE_REGRESSIONS.includes(name)); +const cliArgs = process.argv.slice(2); +const unknownArgs = cliArgs.filter((arg) => arg !== "--list"); +if (unknownArgs.length > 0) { + console.error(`Unknown argument(s): ${unknownArgs.join(", ")}`); + process.exit(1); +} +if (requestedPhase && requested.length > 0) { + console.error("Set either CLONE_REGRESSION_PHASE or CLONE_REGRESSION_TESTS, not both"); + process.exit(1); +} +if (requestedPhase && !regressionPhases.has(requestedPhase)) { + console.error(`Unknown regression phase: ${requestedPhase}`); + process.exit(1); +} +const unknown = requested.filter((name) => !regressionNames.has(name)); if (unknown.length > 0) { console.error(`Unknown regression test(s): ${unknown.join(", ")}`); process.exit(1); } const selected = requested.length > 0 - ? CLONE_REGRESSIONS.filter((name) => requested.includes(name)) - : CLONE_REGRESSIONS; + ? CLONE_REGRESSIONS.filter(({ name }) => requested.includes(name)) + : requestedPhase + ? CLONE_REGRESSIONS.filter(({ phase }) => phase === requestedPhase) + : CLONE_REGRESSIONS; + +if (cliArgs.includes("--list")) { + console.log(selected.map(({ name }) => name).join("\n")); + process.exit(0); +} -for (const name of selected) { +for (const { name } of selected) { const script = path.join(testsDir, name); console.log(`\n=== clone regression: ${name} (timeout ${TEST_TIMEOUT_MS}ms) ===`); const result = spawnSync(process.execPath, ["--import", "tsx", script], { diff --git a/clones/scripts/local-clone-checkpoint.sh b/clones/scripts/local-clone-checkpoint.sh new file mode 100755 index 0000000000..6a09322665 --- /dev/null +++ b/clones/scripts/local-clone-checkpoint.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" +CLONE_DIR="clones/mainnet-clone" +CHAIN_SPEC="clones/mainnet-clone-chainspec.json" + +usage() { + echo "usage: local-clone-checkpoint.sh create ARCHIVE | restore ARCHIVE | clear" >&2 + exit 2 +} + +stop_clone() { + "$SCRIPT_DIR/stop-local-clone.sh" +} + +clear_clone() { + rm -rf "$CLONE_DIR" "$CHAIN_SPEC" +} + +cd "$REPO_ROOT" +command="${1:-}" +shift || true + +case "$command" in + create) + [[ $# -eq 1 ]] || usage + archive="$1" + [[ -d "$CLONE_DIR" ]] || { echo "missing clone directory: $CLONE_DIR" >&2; exit 1; } + [[ -f "$CHAIN_SPEC" ]] || { echo "missing clone chainspec: $CHAIN_SPEC" >&2; exit 1; } + stop_clone + tar -cf "$archive" "$CHAIN_SPEC" "$CLONE_DIR" + echo "Created clone checkpoint $archive." + ;; + restore) + [[ $# -eq 1 ]] || usage + archive="$1" + [[ -f "$archive" ]] || { echo "missing clone checkpoint: $archive" >&2; exit 1; } + stop_clone + clear_clone + # GNU tar detects gzip automatically while reading, so callers do not need + # to carry the producer's archive format through the workflow. + tar -xf "$archive" + [[ -d "$CLONE_DIR" ]] || { echo "checkpoint did not contain $CLONE_DIR" >&2; exit 1; } + [[ -f "$CHAIN_SPEC" ]] || { echo "checkpoint did not contain $CHAIN_SPEC" >&2; exit 1; } + echo "Restored clone checkpoint $archive." + ;; + clear) + [[ $# -eq 0 ]] || usage + stop_clone + clear_clone + ;; + *) usage ;; +esac diff --git a/clones/scripts/start-local-clone-and-wait.sh b/clones/scripts/start-local-clone-and-wait.sh new file mode 100755 index 0000000000..c1e03108cd --- /dev/null +++ b/clones/scripts/start-local-clone-and-wait.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" + +log_file="clone-node.log" +ready_attempts=450 +advance_timeout_seconds=60 + +usage() { + cat >&2 <<'EOF' +Usage: start-local-clone-and-wait.sh MODE + +Modes: + accelerated Seal every 250ms and require 20 blocks of advancement + manual Use manual sealing and require RPC health only + node-default Preserve the node's default sealing and require RPC health only +EOF + exit 2 +} + +[[ $# -eq 1 ]] || usage +mode="$1" + +cd "$REPO_ROOT" +case "$mode" in + accelerated) + nohup ./clones/scripts/start-local-clone.sh --sealing 250 > "$log_file" 2>&1 & + ;; + manual) + nohup ./clones/scripts/start-local-clone.sh --sealing manual > "$log_file" 2>&1 & + ;; + node-default) + nohup ./clones/scripts/start-local-clone.sh > "$log_file" 2>&1 & + ;; + *) usage ;; +esac +node_pid=$! + +cleanup_on_failure() { + local status=$? + (( status != 0 )) || return + echo "Clone node startup failed; last log lines:" + tail -n 200 "$log_file" 2>/dev/null || true + "$SCRIPT_DIR/stop-local-clone.sh" || true +} +trap cleanup_on_failure EXIT + +rpc() { + local method="$1" + curl -fsS -H "Content-Type: application/json" \ + -d "{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"params\":[]}" \ + http://127.0.0.1:9944 +} + +ready=false +for ((attempt = 1; attempt <= ready_attempts; attempt++)); do + if rpc system_health > /dev/null; then + ready=true + break + fi + kill -0 "$node_pid" 2>/dev/null || break + sleep 2 +done +[[ "$ready" == true ]] || exit 1 + +if [[ "$mode" != accelerated ]]; then + echo "Clone node is healthy." + trap - EXIT + exit 0 +fi + +height() { + local hex + hex=$(rpc chain_getHeader | jq -er '.result.number') + echo $((16#${hex#0x})) +} + +first=$(height) +deadline=$(($(date +%s) + advance_timeout_seconds)) +while (( $(date +%s) < deadline )); do + current=$(height) + if (( current >= first + 20 )); then + echo "Clone advanced from block $first to $current." + trap - EXIT + exit 0 + fi + sleep 1 +done + +exit 1 diff --git a/clones/scripts/stop-local-clone.sh b/clones/scripts/stop-local-clone.sh index 1b8f47937f..8abac74a91 100755 --- a/clones/scripts/stop-local-clone.sh +++ b/clones/scripts/stop-local-clone.sh @@ -1,6 +1,38 @@ #!/usr/bin/env bash set -euo pipefail -pkill -f "node-subtensor.*--base-path clones/mainnet-clone" \ - || pkill -f "node-subtensor.*--base-path ../clones/mainnet-clone" \ - || true +timeout_seconds="${CLONE_STOP_TIMEOUT_SECONDS:-60}" +[[ "$timeout_seconds" =~ ^[0-9]+$ ]] || { + echo "invalid CLONE_STOP_TIMEOUT_SECONDS: $timeout_seconds" >&2 + exit 2 +} + +patterns=( + "node-subtensor.*--base-path clones/mainnet-clone" + "node-subtensor.*--base-path \.\./clones/mainnet-clone" +) + +clone_running() { + local pattern + for pattern in "${patterns[@]}"; do + pgrep -f "$pattern" > /dev/null && return 0 + done + return 1 +} + +for pattern in "${patterns[@]}"; do + pkill -f "$pattern" || true +done + +deadline=$(($(date +%s) + timeout_seconds)) +while clone_running && (( $(date +%s) < deadline )); do + sleep 1 +done + +if clone_running; then + echo "Clone node did not stop within ${timeout_seconds}s." >&2 + for pattern in "${patterns[@]}"; do + pgrep -af "$pattern" >&2 || true + done + exit 1 +fi From 1a830d1cd7adc3c7b336b9105ef26badbb59911d Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 04:53:58 +0200 Subject: [PATCH 29/68] ci: benchmark single-node e2e finality --- .github/workflows/typescript-e2e.yml | 18 ++-- ts-tests/configs/zombie_single_node.json | 38 ++++++++ ts-tests/moonwall.config.json | 112 +++++++++++++++++++++++ 3 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 ts-tests/configs/zombie_single_node.json diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index b46dbcb541..e9c68162b9 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -91,14 +91,12 @@ jobs: environment: name: sccache-writer deployment: false - needs: [trusted-pr, typescript-formatting, changes] + needs: [trusted-pr, changes] if: needs.changes.outputs.e2e == 'true' timeout-minutes: 60 strategy: matrix: include: - - variant: release - flags: "" - variant: fast flags: "--features fast-runtime" env: @@ -150,18 +148,18 @@ jobs: fail-fast: false matrix: include: - - test: dev - binary: release - - test: zombienet_shield - binary: release - test: zombienet_staking binary: fast - - test: zombienet_coldkey_swap - binary: fast - - test: zombienet_subnets + - test: zombienet_staking_single binary: fast - test: zombienet_evm binary: fast + - test: zombienet_evm_single + binary: fast + - test: zombienet_coldkey_swap_single + binary: fast + - test: zombienet_subnets_single + binary: fast name: "typescript-e2e-${{ matrix.test }}" diff --git a/ts-tests/configs/zombie_single_node.json b/ts-tests/configs/zombie_single_node.json new file mode 100644 index 0000000000..eca2afea71 --- /dev/null +++ b/ts-tests/configs/zombie_single_node.json @@ -0,0 +1,38 @@ +{ + "settings": { + "timeout": 1000, + "provider": "native" + }, + "relaychain": { + "chain_spec_path": "specs/chain-spec.json", + "default_command": "../target/release/node-subtensor", + "default_args": [ + "--sealing=250" + ], + "genesis": { + "runtimeGenesis": { + "patch": { + "configuration": { + "config": { + + } + } + } + } + }, + "nodes": [ + { + "name": "one", + "rpc_port": 9947, + "validator": true + } + ] + }, + "types": { + "Header": { + "number": "u64", + "parent_hash": "Hash", + "post_state": "Hash" + } + } +} diff --git a/ts-tests/moonwall.config.json b/ts-tests/moonwall.config.json index 951661ef9e..34bbb681a6 100644 --- a/ts-tests/moonwall.config.json +++ b/ts-tests/moonwall.config.json @@ -66,6 +66,32 @@ "endpoints": ["ws://127.0.0.1:9947"] } ] + }, + { + "name": "zombienet_staking_single", + "timeout": 600000, + "testFileDir": ["suites/zombienet_staking"], + "runScripts": [ + "generate-types.sh", + "build-spec.sh" + ], + "foundation": { + "type": "zombie", + "zombieSpec": { + "configPath": "./configs/zombie_single_node.json", + "skipBlockCheck": [] + } + }, + "vitestArgs": { + "bail": 1 + }, + "connections": [ + { + "name": "Node", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9947"] + } + ] }, { "name": "zombienet_shield", "timeout": 600000, @@ -124,6 +150,32 @@ "endpoints": ["ws://127.0.0.1:9947"] } ] + }, + { + "name": "zombienet_coldkey_swap_single", + "timeout": 600000, + "testFileDir": ["suites/zombienet_coldkey_swap"], + "runScripts": [ + "generate-types.sh", + "build-spec.sh" + ], + "foundation": { + "type": "zombie", + "zombieSpec": { + "configPath": "./configs/zombie_single_node.json", + "skipBlockCheck": [] + } + }, + "vitestArgs": { + "bail": 1 + }, + "connections": [ + { + "name": "Node", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9947"] + } + ] }, { "name": "zombienet_evm", "timeout": 600000, @@ -157,6 +209,40 @@ "descriptor": "evm" } ] + }, + { + "name": "zombienet_evm_single", + "timeout": 600000, + "testFileDir": ["suites/zombienet_evm"], + "runScripts": [ + "generate-types.sh", + "generate-ink-types.sh", + "build-spec.sh" + ], + "foundation": { + "type": "zombie", + "zombieSpec": { + "configPath": "./configs/zombie_single_node.json", + "skipBlockCheck": [] + } + }, + "vitestArgs": { + "bail": 1 + }, + "connections": [ + { + "name": "Node", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9947"], + "descriptor": "subtensor" + }, + { + "name": "EVM", + "type": "ethers", + "endpoints": ["http://127.0.0.1:9947"], + "descriptor": "evm" + } + ] }, { "name": "zombienet_subnets", "timeout": 600000, @@ -182,6 +268,32 @@ "endpoints": ["ws://127.0.0.1:9947"] } ] + }, + { + "name": "zombienet_subnets_single", + "timeout": 600000, + "testFileDir": ["suites/zombienet_subnets"], + "runScripts": [ + "generate-types.sh", + "build-spec.sh" + ], + "foundation": { + "type": "zombie", + "zombieSpec": { + "configPath": "./configs/zombie_single_node.json", + "skipBlockCheck": [] + } + }, + "vitestArgs": { + "bail": 1 + }, + "connections": [ + { + "name": "Node", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9947"] + } + ] }, { "name": "smoke_devnet", "testFileDir": ["suites/smoke"], From c3612716cd72b284a148227f781cca80939faf4a Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 05:19:12 +0200 Subject: [PATCH 30/68] ci: tune single-node e2e block waits --- .github/workflows/typescript-e2e.yml | 4 ---- ts-tests/configs/zombie_single_node.json | 2 +- .../suites/zombienet_evm/00-evm-substrate-transfer.test.ts | 5 +++++ ts-tests/utils/staking.ts | 2 +- ts-tests/utils/transactions.ts | 5 +++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index e9c68162b9..c3328f5b67 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -148,12 +148,8 @@ jobs: fail-fast: false matrix: include: - - test: zombienet_staking - binary: fast - test: zombienet_staking_single binary: fast - - test: zombienet_evm - binary: fast - test: zombienet_evm_single binary: fast - test: zombienet_coldkey_swap_single diff --git a/ts-tests/configs/zombie_single_node.json b/ts-tests/configs/zombie_single_node.json index eca2afea71..e37ab34595 100644 --- a/ts-tests/configs/zombie_single_node.json +++ b/ts-tests/configs/zombie_single_node.json @@ -7,7 +7,7 @@ "chain_spec_path": "specs/chain-spec.json", "default_command": "../target/release/node-subtensor", "default_args": [ - "--sealing=250" + "--sealing=100" ], "genesis": { "runtimeGenesis": { diff --git a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts index a21b61c777..46d59babe9 100644 --- a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts +++ b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts @@ -226,6 +226,11 @@ describeSuite({ await waitForTransactionWithRetry(api, tx, signer, "evm_call"); + // Frontier updates its latest-state view asynchronously after the + // Substrate transaction finalizes. Wait for the next finalized block + // instead of relying on GRANDPA lag to hide that indexing boundary. + await waitForFinalizedBlocks(api, 1); + const receiverBalanceAfterCall = await getEthBalance(provider, ethWallet.address); expect(receiverBalanceAfterCall).toEqual(receiverBalance + raoToEth(tao(1))); }, diff --git a/ts-tests/utils/staking.ts b/ts-tests/utils/staking.ts index 79c432b17b..a43ed5000a 100644 --- a/ts-tests/utils/staking.ts +++ b/ts-tests/utils/staking.ts @@ -315,7 +315,7 @@ export async function waitForBlocks(api: TypedApi, numBlocks: if (currentBlock >= targetBlock) { break; } - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 100)); } } diff --git a/ts-tests/utils/transactions.ts b/ts-tests/utils/transactions.ts index d57be282fb..d8c66dafae 100644 --- a/ts-tests/utils/transactions.ts +++ b/ts-tests/utils/transactions.ts @@ -112,6 +112,7 @@ export async function sendTransaction( } const SECOND = 1000; +const LOCAL_BLOCK_POLL_INTERVAL = 100; /** * Polls until the finalized head reaches `blockNumber` (inclusive). Use this @@ -124,7 +125,7 @@ const SECOND = 1000; export async function waitUntilBlockFinalized( api: TypedApi, blockNumber: number, - pollInterval = 1 * SECOND, + pollInterval = LOCAL_BLOCK_POLL_INTERVAL, timeout = 120 * SECOND ): Promise { const deadline = Date.now() + timeout; @@ -142,7 +143,7 @@ export async function waitUntilBlockFinalized( export async function waitForFinalizedBlocks( api: TypedApi, count: number, - pollInterval = 1 * SECOND, + pollInterval = LOCAL_BLOCK_POLL_INTERVAL, timeout = 120 * SECOND ): Promise { const startBlock = await api.query.System.Number.getValue({ at: "finalized" }); From 7a05c04ba133957e5855126ed76d247051725eab Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 05:32:00 +0200 Subject: [PATCH 31/68] test: synchronize accelerated evm nonces --- .github/workflows/typescript-e2e.yml | 21 +++++--------- .../01-contract-deploy-call.test.ts | 28 +++++++++++++------ 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index c3328f5b67..56b34fb3ce 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -69,13 +69,6 @@ jobs: with: node-version-file: ts-tests/.nvmrc - - name: Install system dependencies - run: | - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends \ - -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \ - build-essential clang curl git make libssl-dev llvm libudev-dev protobuf-compiler pkg-config - - name: Install e2e dependencies working-directory: ts-tests run: pnpm install --frozen-lockfile @@ -141,23 +134,23 @@ jobs: run-e2e-tests: needs: [trusted-pr, build] - runs-on: [self-hosted, fireactions-turbo-8] + runs-on: ${{ fromJSON(matrix.runner) }} timeout-minutes: 30 strategy: fail-fast: false matrix: include: - - test: zombienet_staking_single - binary: fast - test: zombienet_evm_single binary: fast - - test: zombienet_coldkey_swap_single - binary: fast - - test: zombienet_subnets_single + candidate: turbo-8 + runner: '["self-hosted", "fireactions-turbo-8"]' + - test: zombienet_evm_single binary: fast + candidate: github-hosted + runner: '["ubuntu-latest"]' - name: "typescript-e2e-${{ matrix.test }}" + name: "typescript-e2e-${{ matrix.test }}-${{ matrix.candidate }}" steps: - name: Check-out repository diff --git a/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts b/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts index f23e9b5380..dcd619770c 100644 --- a/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts +++ b/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts @@ -71,6 +71,11 @@ describeSuite({ let proxyWallet2: ethers.Wallet; let proxyWallet3: ethers.Wallet; let proxyWallet4: ethers.Wallet; + let stakeSigner: ethers.NonceManager; + let proxySigner1: ethers.NonceManager; + let proxySigner2: ethers.NonceManager; + let proxySigner3: ethers.NonceManager; + let proxySigner4: ethers.NonceManager; let pureProxyReceiver: KeyringPair; let delegateProxyReceiver: KeyringPair; let hotkey: KeyringPair; @@ -117,6 +122,11 @@ describeSuite({ proxyWallet2 = createEthersWallet(provider); proxyWallet3 = createEthersWallet(provider); proxyWallet4 = createEthersWallet(provider); + stakeSigner = new ethers.NonceManager(stakeWallet); + proxySigner1 = new ethers.NonceManager(proxyWallet1); + proxySigner2 = new ethers.NonceManager(proxyWallet2); + proxySigner3 = new ethers.NonceManager(proxyWallet3); + proxySigner4 = new ethers.NonceManager(proxyWallet4); pureProxyReceiver = generateKeyringPair("sr25519"); delegateProxyReceiver = generateKeyringPair("sr25519"); @@ -127,7 +137,7 @@ describeSuite({ proxyWalletsReady = true; } - async function deployAndFundStakeWrap(wallet: ethers.Wallet): Promise { + async function deployAndFundStakeWrap(wallet: ethers.NonceManager): Promise { const contractFactory = new ethers.ContractFactory(STAKE_WRAP_ABI, STAKE_WRAP_BYTECODE, wallet); const contract = await contractFactory.deploy(); await contract.waitForDeployment(); @@ -327,7 +337,7 @@ describeSuite({ await ensureSubnetReady(); await ensureProxyWalletsReady(); - const deployedContract = await deployAndFundStakeWrap(stakeWallet); + const deployedContract = await deployAndFundStakeWrap(stakeSigner); const stakeTx = await deployedContract.stake(hotkey.publicKey, netuid, tao(2)); const stakeReceipt = await stakeTx.wait(); @@ -347,7 +357,7 @@ describeSuite({ await ensureSubnetReady(); await ensureProxyWalletsReady(); - const deployedContract = await deployAndFundStakeWrap(stakeWallet); + const deployedContract = await deployAndFundStakeWrap(stakeSigner); const tx = await deployedContract.stakeLimit(hotkey.publicKey, netuid, tao(2000), tao(1000), true); const receipt = await tx.wait(); @@ -363,7 +373,7 @@ describeSuite({ await ensureProxyWalletsReady(); const proxies = await getProxies(api, convertH160ToSS58(proxyWallet1.address)); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet1); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner1); const type = 0; const delay = 0; @@ -399,7 +409,7 @@ describeSuite({ test: async () => { await ensureProxyWalletsReady(); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet1); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner1); const type = 0; const delay = 0; const index = 0; @@ -421,7 +431,7 @@ describeSuite({ test: async () => { await ensureProxyWalletsReady(); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet2); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner2); const amount = 1_000_000_000; const wrongReceiver = generateKeyringPair("sr25519"); const callCode = await getTransferCallCode(api, wrongReceiver, amount); @@ -440,7 +450,7 @@ describeSuite({ await ensureProxyWalletsReady(); const proxies = await api.query.Proxy.Proxies.getValue(convertH160ToSS58(proxyWallet2.address)); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet2); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner2); const proxiesFromContract = await contract.getProxies(convertH160ToPublicKey(proxyWallet2.address)); expect(proxiesFromContract.length).toEqual(proxies[0].length); @@ -479,7 +489,7 @@ describeSuite({ const balance = await getBalance(api, receiverSs58); const amount = 1_000_000_000; - const contract2 = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet3); + const contract2 = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner3); const callCode = await getTransferCallCode(api, delegateProxyReceiver, amount); const tx2 = await contract2.proxyCall(convertH160ToPublicKey(proxyWallet2.address), [type], callCode); await tx2.wait(); @@ -495,7 +505,7 @@ describeSuite({ await ensureProxyWalletsReady(); const proxies = await api.query.Proxy.Proxies.getValue(convertH160ToSS58(proxyWallet4.address)); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet4); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner4); expect(proxies[0].length).toEqual(0); const proxiesFromContract = await contract.getProxies(convertH160ToPublicKey(proxyWallet4.address)); From 7ab7d2dd6f92aa155414c6a04cc9180ffb70ace8 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 05:46:08 +0200 Subject: [PATCH 32/68] ci: validate balanced shield shards --- .github/workflows/typescript-e2e.yml | 16 ++-- ts-tests/moonwall.config.json | 76 +++++++++++++++++++ .../01-contract-deploy-call.test.ts | 14 ++-- 3 files changed, 92 insertions(+), 14 deletions(-) diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index 56b34fb3ce..72a48f88f9 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -90,6 +90,8 @@ jobs: strategy: matrix: include: + - variant: release + flags: "" - variant: fast flags: "--features fast-runtime" env: @@ -134,7 +136,7 @@ jobs: run-e2e-tests: needs: [trusted-pr, build] - runs-on: ${{ fromJSON(matrix.runner) }} + runs-on: [self-hosted, fireactions-turbo-8] timeout-minutes: 30 strategy: @@ -143,14 +145,12 @@ jobs: include: - test: zombienet_evm_single binary: fast - candidate: turbo-8 - runner: '["self-hosted", "fireactions-turbo-8"]' - - test: zombienet_evm_single - binary: fast - candidate: github-hosted - runner: '["ubuntu-latest"]' + - test: zombienet_shield_a + binary: release + - test: zombienet_shield_b + binary: release - name: "typescript-e2e-${{ matrix.test }}-${{ matrix.candidate }}" + name: "typescript-e2e-${{ matrix.test }}" steps: - name: Check-out repository diff --git a/ts-tests/moonwall.config.json b/ts-tests/moonwall.config.json index 34bbb681a6..983c5da003 100644 --- a/ts-tests/moonwall.config.json +++ b/ts-tests/moonwall.config.json @@ -125,6 +125,82 @@ } ] }, + { + "name": "zombienet_shield_a", + "timeout": 600000, + "testFileDir": ["suites/zombienet_shield"], + "include": [ + "00.00-basic.test.ts", + "00.01-basic.test.ts", + "02-edge-cases.test.ts" + ], + "runScripts": [ + "generate-types.sh", + "build-spec.sh" + ], + "foundation": { + "type": "zombie", + "zombieSpec": { + "configPath": "./configs/zombie_extended.json", + "skipBlockCheck": [] + } + }, + "vitestArgs": { + "bail": 1 + }, + "connections": [ + { + "name": "Node", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9947"], + "descriptor": "subtensor" + }, + { + "name": "NodeFull", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9950"], + "descriptor": "subtensor" + } + ] + }, + { + "name": "zombienet_shield_b", + "timeout": 600000, + "testFileDir": ["suites/zombienet_shield"], + "include": [ + "01-scaling.test.ts", + "03-timing.test.ts", + "04-mortality.test.ts" + ], + "runScripts": [ + "generate-types.sh", + "build-spec.sh" + ], + "foundation": { + "type": "zombie", + "zombieSpec": { + "configPath": "./configs/zombie_extended.json", + "skipBlockCheck": [] + } + }, + "vitestArgs": { + "bail": 1 + }, + "connections": [ + { + "name": "Node", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9947"], + "descriptor": "subtensor" + }, + { + "name": "NodeFull", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9950"], + "descriptor": "subtensor" + } + ] + }, { "name": "zombienet_coldkey_swap", "timeout": 600000, diff --git a/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts b/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts index dcd619770c..36f6c50f97 100644 --- a/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts +++ b/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts @@ -66,6 +66,7 @@ describeSuite({ let api: TypedApi; let provider: ethers.JsonRpcProvider; let ethWallet: ethers.Wallet; + let ethSigner: ethers.NonceManager; let stakeWallet: ethers.Wallet; let proxyWallet1: ethers.Wallet; let proxyWallet2: ethers.Wallet; @@ -88,6 +89,7 @@ describeSuite({ api = context.papi("Node").getTypedApi(subtensor); provider = context.ethers("EVM").provider as ethers.JsonRpcProvider; ethWallet = createEthersWallet(provider); + ethSigner = new ethers.NonceManager(ethWallet); await forceSetBalance(api, convertH160ToSS58(ethWallet.address)); await disableWhiteListCheck(api, true); await waitForFinalizedBlocks(api, 1); @@ -208,7 +210,7 @@ describeSuite({ const contractFactory = new ethers.ContractFactory( BRIDGE_TOKEN_CONTRACT_ABI, BRIDGE_TOKEN_CONTRACT_BYTECODE, - ethWallet + ethSigner ); const contract = await contractFactory.deploy("name", "symbol", ethWallet.address); await contract.waitForDeployment(); @@ -226,7 +228,7 @@ describeSuite({ const contractFactory = new ethers.ContractFactory( BRIDGE_TOKEN_CONTRACT_ABI, BRIDGE_TOKEN_CONTRACT_BYTECODE, - ethWallet + ethSigner ); const contract = await contractFactory.deploy("name", "symbol", ethWallet.address, { gasLimit: 12_345_678, @@ -249,7 +251,7 @@ describeSuite({ const stakeBalance = tao(20); const stakeBefore = await getStake(api, hotkeySs58, walletSs58, netuid); - const stakingPrecompile = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, ethWallet); + const stakingPrecompile = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, ethSigner); const tx = await stakingPrecompile.addStake(hotkey.publicKey, stakeBalance.toString(), netuid); const receipt = await tx.wait(); expect(receipt?.status).toEqual(1); @@ -276,14 +278,14 @@ describeSuite({ await ensureSubnetReady(); const hotkeySs58 = convertPublicKeyToSs58(hotkey.publicKey); const walletSs58 = convertH160ToSS58(ethWallet.address); - const stakingPrecompile = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, ethWallet); + const stakingPrecompile = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, ethSigner); const stakeBeforeDeposit = await getStake(api, hotkeySs58, walletSs58, netuid); const contractFactory = new ethers.ContractFactory( ALPHA_POOL_CONTRACT_ABI, ALPHA_POOL_CONTRACT_BYTECODE, - ethWallet + ethSigner ); const contract = await contractFactory.deploy(hotkey.publicKey); await contract.waitForDeployment(); @@ -294,7 +296,7 @@ describeSuite({ await forceSetBalance(api, convertPublicKeyToSs58(contractPublicKey)); await expectDeployedContract(provider, contractAddress); - const contractForCall = new ethers.Contract(contractAddress, ALPHA_POOL_CONTRACT_ABI, ethWallet); + const contractForCall = new ethers.Contract(contractAddress, ALPHA_POOL_CONTRACT_ABI, ethSigner); const setContractColdkeyTx = await contractForCall.setContractColdkey(contractPublicKey); const setColdkeyReceipt = await setContractColdkeyTx.wait(); expect(setColdkeyReceipt?.status).toEqual(1); From ad25548531f5c27c929c6661e1ef330da31e6e52 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 11:04:59 +0200 Subject: [PATCH 33/68] ci: finalize accelerated typescript e2e suite --- .github/actions/run-typescript-e2e/action.yml | 52 ++++++++ .github/workflows/typescript-e2e.yml | 87 +++++++----- ts-tests/configs/zombie_node.json | 47 ------- ts-tests/moonwall.config.json | 124 +----------------- ts-tests/scripts/validate-e2e-config.mjs | 85 ++++++++++++ .../01-contract-deploy-call.test.ts | 11 +- .../zombienet_shield/01-scaling.test.ts | 59 +++++++++ 7 files changed, 265 insertions(+), 200 deletions(-) create mode 100644 .github/actions/run-typescript-e2e/action.yml delete mode 100644 ts-tests/configs/zombie_node.json create mode 100644 ts-tests/scripts/validate-e2e-config.mjs diff --git a/.github/actions/run-typescript-e2e/action.yml b/.github/actions/run-typescript-e2e/action.yml new file mode 100644 index 0000000000..b79a3d3854 --- /dev/null +++ b/.github/actions/run-typescript-e2e/action.yml @@ -0,0 +1,52 @@ +name: Run TypeScript E2E suite +description: Download a prebuilt node and run one Moonwall environment. + +inputs: + binary: + description: Node artifact variant to download. + required: true + test: + description: Moonwall environment to run. + required: true + +runs: + using: composite + steps: + - name: Download binary + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: node-subtensor-${{ inputs.binary }} + path: target/release + + - name: Make binary executable + shell: bash + run: chmod +x target/release/node-subtensor + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version-file: ts-tests/.nvmrc + + - name: Setup pnpm + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 10 + + - name: Install e2e dependencies + shell: bash + working-directory: ts-tests + run: pnpm install --frozen-lockfile + + - name: Install lsof (dev foundation RPC port discovery) + if: inputs.test == 'dev' + shell: bash + run: | + sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update + sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends lsof + + - name: Run tests + shell: bash + working-directory: ts-tests + env: + MOONWALL_TEST: ${{ inputs.test }} + run: pnpm moonwall test "$MOONWALL_TEST" diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index 72a48f88f9..7328defcee 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -45,7 +45,7 @@ jobs: files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename') # SDK-only lockfile movement is covered by SDK checks; do not run # zombienet unless chain/runtime or ts-tests inputs changed. - pattern='^(ts-tests|common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^\.github/workflows/typescript-e2e\.yml$' + pattern='^(ts-tests|common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^\.github/(workflows/typescript-e2e\.yml|actions/run-typescript-e2e/)' if grep -qE "$pattern" <<< "$files"; then echo "e2e=true" >> "$GITHUB_OUTPUT" else @@ -69,6 +69,9 @@ jobs: with: node-version-file: ts-tests/.nvmrc + - name: Validate E2E configuration + run: node ts-tests/scripts/validate-e2e-config.mjs + - name: Install e2e dependencies working-directory: ts-tests run: pnpm install --frozen-lockfile @@ -134,6 +137,10 @@ jobs: path: target/release/node-subtensor if-no-files-found: error + # State-focused suites do not exercise multi-validator consensus. They use a + # single immediately-finalized node; Shield retains the production-like + # multi-node topology below. Two workers here plus two Shield workers cap the + # E2E phase at four self-hosted runners without extending its critical path. run-e2e-tests: needs: [trusted-pr, build] runs-on: [self-hosted, fireactions-turbo-8] @@ -141,14 +148,19 @@ jobs: strategy: fail-fast: false + max-parallel: 2 matrix: include: - - test: zombienet_evm_single + - test: zombienet_evm binary: fast - - test: zombienet_shield_a - binary: release - - test: zombienet_shield_b + - test: zombienet_staking + binary: fast + - test: zombienet_coldkey_swap + binary: fast + - test: dev binary: release + - test: zombienet_subnets + binary: fast name: "typescript-e2e-${{ matrix.test }}" @@ -156,36 +168,47 @@ jobs: - name: Check-out repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Download binary - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + - name: Run E2E suite + uses: ./.github/actions/run-typescript-e2e with: - name: node-subtensor-${{ matrix.binary }} - path: target/release + binary: ${{ matrix.binary }} + test: ${{ matrix.test }} - - name: Make binary executable - run: chmod +x target/release/node-subtensor - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version-file: ts-tests/.nvmrc + # Shield validates consensus, key rotation, timing, and transaction + # mortality, so preserve its six-node release topology and parallelize only + # by running balanced file groups on independent networks. + run-shield-tests: + needs: [trusted-pr, build] + runs-on: [self-hosted, fireactions-turbo-8] + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + test: [zombienet_shield_a, zombienet_shield_b] + name: "typescript-e2e-${{ matrix.test }}" + steps: + - name: Check-out repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - name: Run Shield shard + uses: ./.github/actions/run-typescript-e2e with: - version: 10 - - - name: Install e2e dependencies - working-directory: ts-tests - run: pnpm install --frozen-lockfile + binary: release + test: ${{ matrix.test }} - - name: Install lsof (dev foundation RPC port discovery) - if: matrix.test == 'dev' - run: | - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends lsof - - - name: Run tests + shield-result: + name: typescript-e2e-zombienet_shield + if: always() + needs: [build, run-shield-tests] + runs-on: ubuntu-latest + steps: + - name: Check Shield shard results + env: + BUILD_RESULT: ${{ needs.build.result }} + SHIELD_RESULT: ${{ needs.run-shield-tests.result }} run: | - cd ts-tests - pnpm moonwall test ${{ matrix.test }} + if [[ "$BUILD_RESULT" == "failure" || "$BUILD_RESULT" == "cancelled" || \ + "$SHIELD_RESULT" == "failure" || "$SHIELD_RESULT" == "cancelled" ]]; then + echo "Shield E2E prerequisites failed: build=$BUILD_RESULT shards=$SHIELD_RESULT" >&2 + exit 1 + fi diff --git a/ts-tests/configs/zombie_node.json b/ts-tests/configs/zombie_node.json deleted file mode 100644 index f073be80bd..0000000000 --- a/ts-tests/configs/zombie_node.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "settings": { - "timeout": 1000, - "provider": "native" - }, - "relaychain": { - "chain_spec_path": "specs/chain-spec.json", - "default_command": "../target/release/node-subtensor", - "default_args": [ - ], - "genesis": { - "runtimeGenesis": { - "patch": { - "configuration": { - "config": { - - } - } - } - } - }, - "nodes": [ - { - "name": "one", - "rpc_port": "9947", - "validator": true - }, - { - "name": "two", - "rpc_port": "9948", - "validator": true - }, - { - "name": "three", - "rpc_port": "9949", - "validator": true - } - ] - }, - "types": { - "Header": { - "number": "u64", - "parent_hash": "Hash", - "post_state": "Hash" - } - } -} \ No newline at end of file diff --git a/ts-tests/moonwall.config.json b/ts-tests/moonwall.config.json index 983c5da003..2b5a5072f2 100644 --- a/ts-tests/moonwall.config.json +++ b/ts-tests/moonwall.config.json @@ -49,32 +49,6 @@ "generate-types.sh", "build-spec.sh" ], - "foundation": { - "type": "zombie", - "zombieSpec": { - "configPath": "./configs/zombie_node.json", - "skipBlockCheck": [] - } - }, - "vitestArgs": { - "bail": 1 - }, - "connections": [ - { - "name": "Node", - "type": "papi", - "endpoints": ["ws://127.0.0.1:9947"] - } - ] - }, - { - "name": "zombienet_staking_single", - "timeout": 600000, - "testFileDir": ["suites/zombienet_staking"], - "runScripts": [ - "generate-types.sh", - "build-spec.sh" - ], "foundation": { "type": "zombie", "zombieSpec": { @@ -130,9 +104,9 @@ "timeout": 600000, "testFileDir": ["suites/zombienet_shield"], "include": [ - "00.00-basic.test.ts", - "00.01-basic.test.ts", - "02-edge-cases.test.ts" + "suites/zombienet_shield/00.00-basic.test.ts", + "suites/zombienet_shield/00.01-basic.test.ts", + "suites/zombienet_shield/02-edge-cases.test.ts" ], "runScripts": [ "generate-types.sh", @@ -168,9 +142,9 @@ "timeout": 600000, "testFileDir": ["suites/zombienet_shield"], "include": [ - "01-scaling.test.ts", - "03-timing.test.ts", - "04-mortality.test.ts" + "suites/zombienet_shield/01-scaling.test.ts", + "suites/zombienet_shield/03-timing.test.ts", + "suites/zombienet_shield/04-mortality.test.ts" ], "runScripts": [ "generate-types.sh", @@ -209,32 +183,6 @@ "generate-types.sh", "build-spec.sh" ], - "foundation": { - "type": "zombie", - "zombieSpec": { - "configPath": "./configs/zombie_node.json", - "skipBlockCheck": [] - } - }, - "vitestArgs": { - "bail": 1 - }, - "connections": [ - { - "name": "Node", - "type": "papi", - "endpoints": ["ws://127.0.0.1:9947"] - } - ] - }, - { - "name": "zombienet_coldkey_swap_single", - "timeout": 600000, - "testFileDir": ["suites/zombienet_coldkey_swap"], - "runScripts": [ - "generate-types.sh", - "build-spec.sh" - ], "foundation": { "type": "zombie", "zombieSpec": { @@ -261,40 +209,6 @@ "generate-ink-types.sh", "build-spec.sh" ], - "foundation": { - "type": "zombie", - "zombieSpec": { - "configPath": "./configs/zombie_node.json", - "skipBlockCheck": [] - } - }, - "vitestArgs": { - "bail": 1 - }, - "connections": [ - { - "name": "Node", - "type": "papi", - "endpoints": ["ws://127.0.0.1:9947"], - "descriptor": "subtensor" - }, - { - "name": "EVM", - "type": "ethers", - "endpoints": ["http://127.0.0.1:9947"], - "descriptor": "evm" - } - ] - }, - { - "name": "zombienet_evm_single", - "timeout": 600000, - "testFileDir": ["suites/zombienet_evm"], - "runScripts": [ - "generate-types.sh", - "generate-ink-types.sh", - "build-spec.sh" - ], "foundation": { "type": "zombie", "zombieSpec": { @@ -327,32 +241,6 @@ "generate-types.sh", "build-spec.sh" ], - "foundation": { - "type": "zombie", - "zombieSpec": { - "configPath": "./configs/zombie_node.json", - "skipBlockCheck": [] - } - }, - "vitestArgs": { - "bail": 1 - }, - "connections": [ - { - "name": "Node", - "type": "papi", - "endpoints": ["ws://127.0.0.1:9947"] - } - ] - }, - { - "name": "zombienet_subnets_single", - "timeout": 600000, - "testFileDir": ["suites/zombienet_subnets"], - "runScripts": [ - "generate-types.sh", - "build-spec.sh" - ], "foundation": { "type": "zombie", "zombieSpec": { diff --git a/ts-tests/scripts/validate-e2e-config.mjs b/ts-tests/scripts/validate-e2e-config.mjs new file mode 100644 index 0000000000..505558085a --- /dev/null +++ b/ts-tests/scripts/validate-e2e-config.mjs @@ -0,0 +1,85 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const tsTestsDir = join(dirname(fileURLToPath(import.meta.url)), ".."); +const config = JSON.parse(readFileSync(join(tsTestsDir, "moonwall.config.json"), "utf8")); +const singleNodeSpec = JSON.parse(readFileSync(join(tsTestsDir, "configs/zombie_single_node.json"), "utf8")); +const shieldSpec = JSON.parse(readFileSync(join(tsTestsDir, "configs/zombie_extended.json"), "utf8")); +const environments = new Map(config.environments.map((environment) => [environment.name, environment])); + +const shieldFiles = readdirSync(join(tsTestsDir, "suites/zombienet_shield")) + .filter((file) => file.endsWith(".test.ts")) + .map((file) => `suites/zombienet_shield/${file}`) + .sort(); +const shieldShardNames = ["zombienet_shield_a", "zombienet_shield_b"]; +const shieldShardIncludes = shieldShardNames.map((name) => { + const environment = environments.get(name); + if (!environment) { + throw new Error(`Missing Moonwall environment: ${name}`); + } + const includes = environment.include ?? []; + if (includes.length === 0) { + throw new Error(`${name} must include at least one Shield test`); + } + return includes; +}); +const shardSizes = shieldShardIncludes.map((includes) => includes.length); +if (Math.max(...shardSizes) - Math.min(...shardSizes) > 1) { + throw new Error(`Shield shards must stay balanced; found sizes ${shardSizes.join(" and ")}`); +} +const shieldIncludes = shieldShardIncludes.flat(); + +const duplicateShieldFiles = shieldIncludes.filter((file, index) => shieldIncludes.indexOf(file) !== index); +if (duplicateShieldFiles.length > 0) { + throw new Error(`Shield tests assigned to multiple shards: ${[...new Set(duplicateShieldFiles)].join(", ")}`); +} + +const sortedIncludes = [...shieldIncludes].sort(); +if (JSON.stringify(sortedIncludes) !== JSON.stringify(shieldFiles)) { + const missing = shieldFiles.filter((file) => !sortedIncludes.includes(file)); + const unknown = sortedIncludes.filter((file) => !shieldFiles.includes(file)); + throw new Error(`Shield shard coverage mismatch; missing=[${missing.join(", ")}] unknown=[${unknown.join(", ")}]`); +} + +const singleNodeConfig = "./configs/zombie_single_node.json"; +const singleNodes = singleNodeSpec.relaychain?.nodes ?? []; +if ( + singleNodes.length !== 1 || + singleNodes[0]?.validator !== true || + !singleNodeSpec.relaychain?.default_args?.includes("--sealing=100") +) { + throw new Error("Single-node state spec must contain one validator using --sealing=100"); +} + +for (const name of ["zombienet_staking", "zombienet_coldkey_swap", "zombienet_evm", "zombienet_subnets"]) { + const configPath = environments.get(name)?.foundation?.zombieSpec?.configPath; + if (configPath !== singleNodeConfig) { + throw new Error(`${name} must use ${singleNodeConfig}; found ${configPath ?? "no config"}`); + } +} + +const shieldConfig = "./configs/zombie_extended.json"; +const shieldNodes = shieldSpec.relaychain?.nodes ?? []; +const shieldValidators = shieldNodes.filter((node) => node.validator === true); +if (shieldNodes.length !== 6 || shieldValidators.length !== 3) { + throw new Error( + `Shield spec must retain six nodes and three validators; found ${shieldNodes.length} nodes and ${shieldValidators.length} validators` + ); +} + +for (const name of ["zombienet_shield", "zombienet_shield_a", "zombienet_shield_b"]) { + const environment = environments.get(name); + const configPath = environment?.foundation?.zombieSpec?.configPath; + const connectionNames = new Set((environment?.connections ?? []).map((connection) => connection.name)); + if (configPath !== shieldConfig) { + throw new Error(`${name} must use ${shieldConfig}; found ${configPath ?? "no config"}`); + } + if (!connectionNames.has("Node") || !connectionNames.has("NodeFull")) { + throw new Error(`${name} must expose authority and full-node connections`); + } +} + +console.log( + `Validated ${shieldFiles.length} Shield files, three multi-node Shield environments, and four single-node state suites.` +); diff --git a/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts b/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts index 36f6c50f97..b713d582af 100644 --- a/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts +++ b/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts @@ -309,12 +309,13 @@ describeSuite({ const depositAlphaTx = await contractForCall.depositAlpha(netuid, tao(10).toString(), hotkey.publicKey); const depositReceipt = await depositAlphaTx.wait(); - expect(depositReceipt?.status).toEqual(1); + if (!depositReceipt) throw new Error("Missing depositAlpha receipt"); + expect(depositReceipt.status).toEqual(1); // Wait for the deposit's own block to finalize rather than a // fixed block count: when GRANDPA lags best by more than 2 // blocks, the finalized-state stake reads below see the // pre-deposit stake and the toBeLessThan assertion flakes. - await waitUntilBlockFinalized(api, depositReceipt!.blockNumber); + await waitUntilBlockFinalized(api, depositReceipt.blockNumber); const stakeAfterDeposit = await getStake(api, hotkeySs58, walletSs58, netuid); expect(stakeAfterDeposit).toBeLessThan(stakeBeforeDeposit); @@ -433,7 +434,11 @@ describeSuite({ test: async () => { await ensureProxyWalletsReady(); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner2); + // Use a throwaway manager for this expected rejection. NonceManager + // increments before submission, so a preflight rejection would + // otherwise leave the persistent signer used by T10 one nonce ahead. + const rejectingSigner = new ethers.NonceManager(proxyWallet2); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, rejectingSigner); const amount = 1_000_000_000; const wrongReceiver = generateKeyringPair("sr25519"); const callCode = await getTransferCallCode(api, wrongReceiver, amount); diff --git a/ts-tests/suites/zombienet_shield/01-scaling.test.ts b/ts-tests/suites/zombienet_shield/01-scaling.test.ts index a83d5a3e2b..aa12f50d46 100644 --- a/ts-tests/suites/zombienet_shield/01-scaling.test.ts +++ b/ts-tests/suites/zombienet_shield/01-scaling.test.ts @@ -5,6 +5,7 @@ import { Keyring } from "@polkadot/keyring"; import { hexToU8a } from "@polkadot/util"; import type { PolkadotClient, TypedApi } from "polkadot-api"; import { beforeAll, expect } from "vitest"; +import { sleep } from "@zombienet/utils"; import { checkRuntime, getAccountNonce, @@ -15,13 +16,38 @@ import { waitForFinalizedBlocks, } from "../../utils"; +type IndexedEvmBlock = { + hash: string; + number: string; +}; + +async function waitForIndexedEvmBlock(client: PolkadotClient, blockNumber: number): Promise { + const deadline = Date.now() + 60_000; + const blockTag = `0x${blockNumber.toString(16)}`; + + while (Date.now() < deadline) { + try { + const block = (await client._request("eth_getBlockByNumber", [blockTag, false])) as IndexedEvmBlock | null; + if (block) return block; + } catch { + // The node may know the finalized Substrate block just before its + // asynchronous Frontier mapping is queryable. + } + await sleep(1_000); + } + + throw new Error(`Frontier did not index finalized block #${blockNumber}`); +} + describeSuite({ id: "01_scaling", title: "MEV Shield — 6 node scaling", foundationMethods: "zombie", testCases: ({ it, context }) => { let api: TypedApi; + let apiFull: TypedApi; let client: PolkadotClient; + let clientFull: PolkadotClient; let alice: KeyringPair; let bob: KeyringPair; @@ -35,6 +61,8 @@ describeSuite({ client = context.papi("Node"); api = client.getTypedApi(subtensor); + clientFull = context.papi("NodeFull"); + apiFull = clientFull.getTypedApi(subtensor); await checkRuntime(api); }, 120000); @@ -85,6 +113,37 @@ describeSuite({ const balanceAfter = await getBalance(api, bob.address); expect(balanceAfter).toBeGreaterThan(balanceBefore); + + // The state-oriented suites run one immediately-finalized node, + // so retain explicit GRANDPA propagation and Frontier indexing + // assertions on the production-like Shield topology. Pin every + // read to the same finalized block to avoid comparing independently + // advancing latest-state views. + const finalizedHash = (await client._request("chain_getFinalizedHead", [])) as string; + const finalizedNumber = await api.query.System.Number.getValue({ at: finalizedHash }); + const authorityAccount = await api.query.System.Account.getValue(bob.address, { at: finalizedHash }); + expect(authorityAccount.data.free).toBe(balanceAfter); + const deadline = Date.now() + 60_000; + let fullNodeBalance: bigint | undefined; + while (Date.now() < deadline) { + try { + const fullAccount = await apiFull.query.System.Account.getValue(bob.address, { + at: finalizedHash, + }); + fullNodeBalance = fullAccount.data.free; + break; + } catch { + await sleep(1_000); + } + } + expect(fullNodeBalance).toBe(authorityAccount.data.free); + + const [authorityEvmBlock, fullNodeEvmBlock] = await Promise.all([ + waitForIndexedEvmBlock(client, finalizedNumber), + waitForIndexedEvmBlock(clientFull, finalizedNumber), + ]); + expect(fullNodeEvmBlock.number).toBe(authorityEvmBlock.number); + expect(fullNodeEvmBlock.hash).toBe(authorityEvmBlock.hash); }, }); From 089f96ced522eb37d92fde819c4663406e1694ee Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 11:30:20 +0200 Subject: [PATCH 34/68] test: wait for frontier after substrate transfer --- .../suites/zombienet_evm/00-evm-substrate-transfer.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts index 46d59babe9..b32b169b23 100644 --- a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts +++ b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts @@ -137,6 +137,11 @@ describeSuite({ }); await waitForTransactionWithRetry(api, tx, signer, "substrate_to_evm"); + // Frontier updates its latest-state view asynchronously after the + // Substrate transaction finalizes. Wait for the next finalized + // block before reading the mapped EVM balance. + await waitForFinalizedBlocks(api, 1); + const receiverBalanceAfter = await getEthBalance(provider, ethWallet.address); expect(receiverBalanceAfter).toEqual(receiverBalance + raoToEth(transferBalance)); }, From 7525fc976763662538476d7d98295c7fa3850380 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 11:49:22 +0200 Subject: [PATCH 35/68] test: poll frontier balance after substrate transfer --- .../00-evm-substrate-transfer.test.ts | 23 ++++++++-------- ts-tests/utils/evm.ts | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts index b32b169b23..20f8affc54 100644 --- a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts +++ b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts @@ -24,6 +24,7 @@ import { ss58ToEthAddress, ss58ToH160, tao, + waitForEthBalance, waitForFinalizedBlocks, waitForTransactionWithRetry, WITHDRAW_CONTRACT_ABI, @@ -137,12 +138,11 @@ describeSuite({ }); await waitForTransactionWithRetry(api, tx, signer, "substrate_to_evm"); - // Frontier updates its latest-state view asynchronously after the - // Substrate transaction finalizes. Wait for the next finalized - // block before reading the mapped EVM balance. - await waitForFinalizedBlocks(api, 1); - - const receiverBalanceAfter = await getEthBalance(provider, ethWallet.address); + const receiverBalanceAfter = await waitForEthBalance( + provider, + ethWallet.address, + receiverBalance + raoToEth(transferBalance) + ); expect(receiverBalanceAfter).toEqual(receiverBalance + raoToEth(transferBalance)); }, }); @@ -231,12 +231,11 @@ describeSuite({ await waitForTransactionWithRetry(api, tx, signer, "evm_call"); - // Frontier updates its latest-state view asynchronously after the - // Substrate transaction finalizes. Wait for the next finalized block - // instead of relying on GRANDPA lag to hide that indexing boundary. - await waitForFinalizedBlocks(api, 1); - - const receiverBalanceAfterCall = await getEthBalance(provider, ethWallet.address); + const receiverBalanceAfterCall = await waitForEthBalance( + provider, + ethWallet.address, + receiverBalance + raoToEth(tao(1)) + ); expect(receiverBalanceAfterCall).toEqual(receiverBalance + raoToEth(tao(1))); }, }); diff --git a/ts-tests/utils/evm.ts b/ts-tests/utils/evm.ts index c4f35c1fba..dcde272a74 100644 --- a/ts-tests/utils/evm.ts +++ b/ts-tests/utils/evm.ts @@ -25,6 +25,32 @@ export async function getEthBalance(provider: ethers.Provider, address: string): return provider.getBalance(address); } +/** + * Wait for Frontier's latest-state view to expose an exact balance. + * + * Raw RPC is intentional: ethers caches some high-level reads briefly, which + * can return the pre-transaction value when development blocks are very fast. + */ +export async function waitForEthBalance( + provider: ethers.JsonRpcProvider, + address: string, + expected: bigint, + timeoutMs = 30_000 +): Promise { + const deadline = Date.now() + timeoutMs; + let actual = 0n; + + while (Date.now() < deadline) { + actual = BigInt(await provider.send("eth_getBalance", [address, "latest"])); + if (actual === expected) { + return actual; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + throw new Error(`Timed out waiting for ${address} balance ${expected}; last observed ${actual}`); +} + /** Read chain ID via RPC without ethers' cached-network checks. */ export async function getEthChainId(provider: ethers.JsonRpcProvider): Promise { const chainId = await provider.send("eth_chainId", []); From e037edaa6e3b599b636a322fe15758efd0c29ed6 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 12:04:14 +0200 Subject: [PATCH 36/68] test: bypass ethers balance cache --- .../suites/zombienet_evm/00-evm-substrate-transfer.test.ts | 2 +- ts-tests/utils/evm.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts index 20f8affc54..e10967f1ce 100644 --- a/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts +++ b/ts-tests/suites/zombienet_evm/00-evm-substrate-transfer.test.ts @@ -49,7 +49,7 @@ function expectWithinTxFee(actual: bigint, expected: bigint): void { async function transferAndGetFee( wallet: ethers.Wallet, wallet2: ethers.Wallet, - provider: ethers.Provider, + provider: ethers.JsonRpcProvider, maxFeePerGas: bigint, maxPriorityFeePerGas: bigint ): Promise { diff --git a/ts-tests/utils/evm.ts b/ts-tests/utils/evm.ts index dcde272a74..699b7a2c6f 100644 --- a/ts-tests/utils/evm.ts +++ b/ts-tests/utils/evm.ts @@ -21,8 +21,9 @@ export function createEthersWallet(provider: ethers.JsonRpcProvider): ethers.Wal return new ethers.Wallet(account.privateKey, provider); } -export async function getEthBalance(provider: ethers.Provider, address: string): Promise { - return provider.getBalance(address); +/** Read an uncached latest balance directly from the node. */ +export async function getEthBalance(provider: ethers.JsonRpcProvider, address: string): Promise { + return BigInt(await provider.send("eth_getBalance", [address, "latest"])); } /** @@ -41,7 +42,7 @@ export async function waitForEthBalance( let actual = 0n; while (Date.now() < deadline) { - actual = BigInt(await provider.send("eth_getBalance", [address, "latest"])); + actual = await getEthBalance(provider, address); if (actual === expected) { return actual; } From d8906172e80df04b3c7622d4c7a0e1582b3481d2 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 14:38:54 +0200 Subject: [PATCH 37/68] ci: restore six-hour smoke cadence --- .github/workflows/scheduled-smoke-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scheduled-smoke-tests.yml b/.github/workflows/scheduled-smoke-tests.yml index 79b61ca9c5..4d4ff45393 100644 --- a/.github/workflows/scheduled-smoke-tests.yml +++ b/.github/workflows/scheduled-smoke-tests.yml @@ -1,12 +1,12 @@ name: Scheduled Smoke Tests -# Read-only smoke suites against the live networks, daily at 12:00 UTC. The same +# Read-only smoke suites against the live networks, every 6 hours. The same # suites gate promotion in release-train.yml; this catches drift between # trains. on: schedule: - - cron: "0 12 * * *" + - cron: "0 */6 * * *" workflow_dispatch: permissions: From e45c8a7222a6706345448cb40863b884ba2899fc Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 15:24:47 +0200 Subject: [PATCH 38/68] test: bypass cached EVM nonce reads --- .../01-contract-deploy-call.test.ts | 46 ++++++------------- ts-tests/utils/evm.ts | 27 ++++++++++- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts b/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts index b713d582af..ad2af9bc32 100644 --- a/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts +++ b/ts-tests/suites/zombienet_evm/01-contract-deploy-call.test.ts @@ -66,17 +66,11 @@ describeSuite({ let api: TypedApi; let provider: ethers.JsonRpcProvider; let ethWallet: ethers.Wallet; - let ethSigner: ethers.NonceManager; let stakeWallet: ethers.Wallet; let proxyWallet1: ethers.Wallet; let proxyWallet2: ethers.Wallet; let proxyWallet3: ethers.Wallet; let proxyWallet4: ethers.Wallet; - let stakeSigner: ethers.NonceManager; - let proxySigner1: ethers.NonceManager; - let proxySigner2: ethers.NonceManager; - let proxySigner3: ethers.NonceManager; - let proxySigner4: ethers.NonceManager; let pureProxyReceiver: KeyringPair; let delegateProxyReceiver: KeyringPair; let hotkey: KeyringPair; @@ -89,7 +83,6 @@ describeSuite({ api = context.papi("Node").getTypedApi(subtensor); provider = context.ethers("EVM").provider as ethers.JsonRpcProvider; ethWallet = createEthersWallet(provider); - ethSigner = new ethers.NonceManager(ethWallet); await forceSetBalance(api, convertH160ToSS58(ethWallet.address)); await disableWhiteListCheck(api, true); await waitForFinalizedBlocks(api, 1); @@ -124,11 +117,6 @@ describeSuite({ proxyWallet2 = createEthersWallet(provider); proxyWallet3 = createEthersWallet(provider); proxyWallet4 = createEthersWallet(provider); - stakeSigner = new ethers.NonceManager(stakeWallet); - proxySigner1 = new ethers.NonceManager(proxyWallet1); - proxySigner2 = new ethers.NonceManager(proxyWallet2); - proxySigner3 = new ethers.NonceManager(proxyWallet3); - proxySigner4 = new ethers.NonceManager(proxyWallet4); pureProxyReceiver = generateKeyringPair("sr25519"); delegateProxyReceiver = generateKeyringPair("sr25519"); @@ -139,7 +127,7 @@ describeSuite({ proxyWalletsReady = true; } - async function deployAndFundStakeWrap(wallet: ethers.NonceManager): Promise { + async function deployAndFundStakeWrap(wallet: ethers.Wallet): Promise { const contractFactory = new ethers.ContractFactory(STAKE_WRAP_ABI, STAKE_WRAP_BYTECODE, wallet); const contract = await contractFactory.deploy(); await contract.waitForDeployment(); @@ -210,7 +198,7 @@ describeSuite({ const contractFactory = new ethers.ContractFactory( BRIDGE_TOKEN_CONTRACT_ABI, BRIDGE_TOKEN_CONTRACT_BYTECODE, - ethSigner + ethWallet ); const contract = await contractFactory.deploy("name", "symbol", ethWallet.address); await contract.waitForDeployment(); @@ -228,7 +216,7 @@ describeSuite({ const contractFactory = new ethers.ContractFactory( BRIDGE_TOKEN_CONTRACT_ABI, BRIDGE_TOKEN_CONTRACT_BYTECODE, - ethSigner + ethWallet ); const contract = await contractFactory.deploy("name", "symbol", ethWallet.address, { gasLimit: 12_345_678, @@ -251,7 +239,7 @@ describeSuite({ const stakeBalance = tao(20); const stakeBefore = await getStake(api, hotkeySs58, walletSs58, netuid); - const stakingPrecompile = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, ethSigner); + const stakingPrecompile = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, ethWallet); const tx = await stakingPrecompile.addStake(hotkey.publicKey, stakeBalance.toString(), netuid); const receipt = await tx.wait(); expect(receipt?.status).toEqual(1); @@ -278,14 +266,14 @@ describeSuite({ await ensureSubnetReady(); const hotkeySs58 = convertPublicKeyToSs58(hotkey.publicKey); const walletSs58 = convertH160ToSS58(ethWallet.address); - const stakingPrecompile = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, ethSigner); + const stakingPrecompile = new ethers.Contract(ISTAKING_V2_ADDRESS, IStakingV2ABI, ethWallet); const stakeBeforeDeposit = await getStake(api, hotkeySs58, walletSs58, netuid); const contractFactory = new ethers.ContractFactory( ALPHA_POOL_CONTRACT_ABI, ALPHA_POOL_CONTRACT_BYTECODE, - ethSigner + ethWallet ); const contract = await contractFactory.deploy(hotkey.publicKey); await contract.waitForDeployment(); @@ -296,7 +284,7 @@ describeSuite({ await forceSetBalance(api, convertPublicKeyToSs58(contractPublicKey)); await expectDeployedContract(provider, contractAddress); - const contractForCall = new ethers.Contract(contractAddress, ALPHA_POOL_CONTRACT_ABI, ethSigner); + const contractForCall = new ethers.Contract(contractAddress, ALPHA_POOL_CONTRACT_ABI, ethWallet); const setContractColdkeyTx = await contractForCall.setContractColdkey(contractPublicKey); const setColdkeyReceipt = await setContractColdkeyTx.wait(); expect(setColdkeyReceipt?.status).toEqual(1); @@ -340,7 +328,7 @@ describeSuite({ await ensureSubnetReady(); await ensureProxyWalletsReady(); - const deployedContract = await deployAndFundStakeWrap(stakeSigner); + const deployedContract = await deployAndFundStakeWrap(stakeWallet); const stakeTx = await deployedContract.stake(hotkey.publicKey, netuid, tao(2)); const stakeReceipt = await stakeTx.wait(); @@ -360,7 +348,7 @@ describeSuite({ await ensureSubnetReady(); await ensureProxyWalletsReady(); - const deployedContract = await deployAndFundStakeWrap(stakeSigner); + const deployedContract = await deployAndFundStakeWrap(stakeWallet); const tx = await deployedContract.stakeLimit(hotkey.publicKey, netuid, tao(2000), tao(1000), true); const receipt = await tx.wait(); @@ -376,7 +364,7 @@ describeSuite({ await ensureProxyWalletsReady(); const proxies = await getProxies(api, convertH160ToSS58(proxyWallet1.address)); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner1); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet1); const type = 0; const delay = 0; @@ -412,7 +400,7 @@ describeSuite({ test: async () => { await ensureProxyWalletsReady(); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner1); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet1); const type = 0; const delay = 0; const index = 0; @@ -434,11 +422,7 @@ describeSuite({ test: async () => { await ensureProxyWalletsReady(); - // Use a throwaway manager for this expected rejection. NonceManager - // increments before submission, so a preflight rejection would - // otherwise leave the persistent signer used by T10 one nonce ahead. - const rejectingSigner = new ethers.NonceManager(proxyWallet2); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, rejectingSigner); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet2); const amount = 1_000_000_000; const wrongReceiver = generateKeyringPair("sr25519"); const callCode = await getTransferCallCode(api, wrongReceiver, amount); @@ -457,7 +441,7 @@ describeSuite({ await ensureProxyWalletsReady(); const proxies = await api.query.Proxy.Proxies.getValue(convertH160ToSS58(proxyWallet2.address)); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner2); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet2); const proxiesFromContract = await contract.getProxies(convertH160ToPublicKey(proxyWallet2.address)); expect(proxiesFromContract.length).toEqual(proxies[0].length); @@ -496,7 +480,7 @@ describeSuite({ const balance = await getBalance(api, receiverSs58); const amount = 1_000_000_000; - const contract2 = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner3); + const contract2 = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet3); const callCode = await getTransferCallCode(api, delegateProxyReceiver, amount); const tx2 = await contract2.proxyCall(convertH160ToPublicKey(proxyWallet2.address), [type], callCode); await tx2.wait(); @@ -512,7 +496,7 @@ describeSuite({ await ensureProxyWalletsReady(); const proxies = await api.query.Proxy.Proxies.getValue(convertH160ToSS58(proxyWallet4.address)); - const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxySigner4); + const contract = new ethers.Contract(IPROXY_ADDRESS, IProxyABI, proxyWallet4); expect(proxies[0].length).toEqual(0); const proxiesFromContract = await contract.getProxies(convertH160ToPublicKey(proxyWallet4.address)); diff --git a/ts-tests/utils/evm.ts b/ts-tests/utils/evm.ts index 699b7a2c6f..48ba6c96bd 100644 --- a/ts-tests/utils/evm.ts +++ b/ts-tests/utils/evm.ts @@ -1,4 +1,4 @@ -import { subtensor } from "@polkadot-api/descriptors"; +import type { subtensor } from "@polkadot-api/descriptors"; import { Keyring } from "@polkadot/keyring"; import { ethers } from "ethers"; import type { TypedApi } from "polkadot-api"; @@ -16,9 +16,32 @@ export async function disableWhiteListCheck(api: TypedApi, dis await waitForTransactionWithRetry(api, tx, alice, "disable_whitelist", 5); } +class UncachedNonceWallet extends ethers.Wallet { + constructor( + privateKey: string, + private readonly rpcProvider: ethers.JsonRpcProvider + ) { + super(privateKey, rpcProvider); + } + + /** + * Bypass ethers' 250ms request cache for nonce reads. Development blocks + * seal every 100ms, so a cached pending nonce can already be stale when the + * next transaction is populated and cause a spurious "nonce too low". + */ + override async getNonce(blockTag: ethers.BlockTag = "latest"): Promise { + if (blockTag !== "pending") { + return super.getNonce(blockTag); + } + + const nonce = await this.rpcProvider.send("eth_getTransactionCount", [this.address, "pending"]); + return ethers.getNumber(nonce, "nonce"); + } +} + export function createEthersWallet(provider: ethers.JsonRpcProvider): ethers.Wallet { const account = ethers.Wallet.createRandom(); - return new ethers.Wallet(account.privateKey, provider); + return new UncachedNonceWallet(account.privateKey, provider); } /** Read an uncached latest balance directly from the node. */ From 6318e45496dd8bc798cc5b08e392afe099ca94cd Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 15:59:12 +0200 Subject: [PATCH 39/68] ci: shorten typescript e2e critical path --- .github/workflows/typescript-e2e.yml | 39 +++++++-------------- ts-tests/moonwall.config.json | 43 +++++++++++++++++++++--- ts-tests/scripts/validate-e2e-config.mjs | 11 +++--- 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index 7328defcee..ebd141f515 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -45,7 +45,7 @@ jobs: files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename') # SDK-only lockfile movement is covered by SDK checks; do not run # zombienet unless chain/runtime or ts-tests inputs changed. - pattern='^(ts-tests|common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^\.github/(workflows/typescript-e2e\.yml|actions/run-typescript-e2e/)' + pattern='^(ts-tests|common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^\.github/(workflows/typescript-e2e\.yml|actions/(run-typescript-e2e|rust-setup|sccache-setup)/|scripts/sccache-configure\.sh$)' if grep -qE "$pattern" <<< "$files"; then echo "e2e=true" >> "$GITHUB_OUTPUT" else @@ -103,29 +103,14 @@ jobs: - name: Check-out repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Install system dependencies - run: | - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends \ - -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \ - build-essential clang curl git make libssl-dev llvm libudev-dev protobuf-compiler pkg-config - - - name: Install Rust - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - - - name: Shared R2 compiler cache - uses: ./.github/actions/sccache-setup - with: - credential-mode: auto - writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} - writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} - - - name: Cache Cargo registry and git data - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + - name: Rust build setup + uses: ./.github/actions/rust-setup with: - key: e2e-${{ matrix.variant }} - cache-on-failure: true - cache-targets: false + cache-key: e2e-${{ matrix.variant }} + fast-linker: "true" + sccache-credential-mode: auto + sccache-writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} + sccache-writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} - name: Build node-subtensor (${{ matrix.variant }}) run: cargo build --profile release ${{ matrix.flags }} -p node-subtensor @@ -139,8 +124,8 @@ jobs: # State-focused suites do not exercise multi-validator consensus. They use a # single immediately-finalized node; Shield retains the production-like - # multi-node topology below. Two workers here plus two Shield workers cap the - # E2E phase at four self-hosted runners without extending its critical path. + # multi-node topology below. Two workers here plus three Shield workers peak + # at five self-hosted runners while the E2E phase is active. run-e2e-tests: needs: [trusted-pr, build] runs-on: [self-hosted, fireactions-turbo-8] @@ -176,7 +161,7 @@ jobs: # Shield validates consensus, key rotation, timing, and transaction # mortality, so preserve its six-node release topology and parallelize only - # by running balanced file groups on independent networks. + # by running measured-time-balanced file groups on independent networks. run-shield-tests: needs: [trusted-pr, build] runs-on: [self-hosted, fireactions-turbo-8] @@ -184,7 +169,7 @@ jobs: strategy: fail-fast: false matrix: - test: [zombienet_shield_a, zombienet_shield_b] + test: [zombienet_shield_a, zombienet_shield_b, zombienet_shield_c] name: "typescript-e2e-${{ matrix.test }}" steps: - name: Check-out repository diff --git a/ts-tests/moonwall.config.json b/ts-tests/moonwall.config.json index 2b5a5072f2..c8fcaaca64 100644 --- a/ts-tests/moonwall.config.json +++ b/ts-tests/moonwall.config.json @@ -104,9 +104,7 @@ "timeout": 600000, "testFileDir": ["suites/zombienet_shield"], "include": [ - "suites/zombienet_shield/00.00-basic.test.ts", - "suites/zombienet_shield/00.01-basic.test.ts", - "suites/zombienet_shield/02-edge-cases.test.ts" + "suites/zombienet_shield/00.01-basic.test.ts" ], "runScripts": [ "generate-types.sh", @@ -142,7 +140,6 @@ "timeout": 600000, "testFileDir": ["suites/zombienet_shield"], "include": [ - "suites/zombienet_shield/01-scaling.test.ts", "suites/zombienet_shield/03-timing.test.ts", "suites/zombienet_shield/04-mortality.test.ts" ], @@ -175,6 +172,44 @@ } ] }, + { + "name": "zombienet_shield_c", + "timeout": 600000, + "testFileDir": ["suites/zombienet_shield"], + "include": [ + "suites/zombienet_shield/00.00-basic.test.ts", + "suites/zombienet_shield/01-scaling.test.ts", + "suites/zombienet_shield/02-edge-cases.test.ts" + ], + "runScripts": [ + "generate-types.sh", + "build-spec.sh" + ], + "foundation": { + "type": "zombie", + "zombieSpec": { + "configPath": "./configs/zombie_extended.json", + "skipBlockCheck": [] + } + }, + "vitestArgs": { + "bail": 1 + }, + "connections": [ + { + "name": "Node", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9947"], + "descriptor": "subtensor" + }, + { + "name": "NodeFull", + "type": "papi", + "endpoints": ["ws://127.0.0.1:9950"], + "descriptor": "subtensor" + } + ] + }, { "name": "zombienet_coldkey_swap", "timeout": 600000, diff --git a/ts-tests/scripts/validate-e2e-config.mjs b/ts-tests/scripts/validate-e2e-config.mjs index 505558085a..7fc8622e81 100644 --- a/ts-tests/scripts/validate-e2e-config.mjs +++ b/ts-tests/scripts/validate-e2e-config.mjs @@ -12,7 +12,7 @@ const shieldFiles = readdirSync(join(tsTestsDir, "suites/zombienet_shield")) .filter((file) => file.endsWith(".test.ts")) .map((file) => `suites/zombienet_shield/${file}`) .sort(); -const shieldShardNames = ["zombienet_shield_a", "zombienet_shield_b"]; +const shieldShardNames = ["zombienet_shield_a", "zombienet_shield_b", "zombienet_shield_c"]; const shieldShardIncludes = shieldShardNames.map((name) => { const environment = environments.get(name); if (!environment) { @@ -24,10 +24,7 @@ const shieldShardIncludes = shieldShardNames.map((name) => { } return includes; }); -const shardSizes = shieldShardIncludes.map((includes) => includes.length); -if (Math.max(...shardSizes) - Math.min(...shardSizes) > 1) { - throw new Error(`Shield shards must stay balanced; found sizes ${shardSizes.join(" and ")}`); -} +// File counts intentionally differ: the shards are balanced by measured runtime. const shieldIncludes = shieldShardIncludes.flat(); const duplicateShieldFiles = shieldIncludes.filter((file, index) => shieldIncludes.indexOf(file) !== index); @@ -68,7 +65,7 @@ if (shieldNodes.length !== 6 || shieldValidators.length !== 3) { ); } -for (const name of ["zombienet_shield", "zombienet_shield_a", "zombienet_shield_b"]) { +for (const name of ["zombienet_shield", ...shieldShardNames]) { const environment = environments.get(name); const configPath = environment?.foundation?.zombieSpec?.configPath; const connectionNames = new Set((environment?.connections ?? []).map((connection) => connection.name)); @@ -81,5 +78,5 @@ for (const name of ["zombienet_shield", "zombienet_shield_a", "zombienet_shield_ } console.log( - `Validated ${shieldFiles.length} Shield files, three multi-node Shield environments, and four single-node state suites.` + `Validated ${shieldFiles.length} Shield files, ${shieldShardNames.length + 1} multi-node Shield environments, and four single-node state suites.` ); From c83cdba3e1db52e407e1a412f12fb334ab967c69 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 16:26:08 +0200 Subject: [PATCH 40/68] ci: keep faster e2e linker path --- .github/workflows/typescript-e2e.yml | 31 +++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index ebd141f515..0fb5c5acc7 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -45,7 +45,7 @@ jobs: files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename') # SDK-only lockfile movement is covered by SDK checks; do not run # zombienet unless chain/runtime or ts-tests inputs changed. - pattern='^(ts-tests|common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^\.github/(workflows/typescript-e2e\.yml|actions/(run-typescript-e2e|rust-setup|sccache-setup)/|scripts/sccache-configure\.sh$)' + pattern='^(ts-tests|common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^\.github/(workflows/typescript-e2e\.yml|actions/(run-typescript-e2e|sccache-setup)/|scripts/sccache-configure\.sh$)' if grep -qE "$pattern" <<< "$files"; then echo "e2e=true" >> "$GITHUB_OUTPUT" else @@ -103,14 +103,29 @@ jobs: - name: Check-out repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Rust build setup - uses: ./.github/actions/rust-setup + - name: Install system dependencies + run: | + sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update + sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends \ + -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \ + build-essential clang curl git make libssl-dev llvm libudev-dev protobuf-compiler pkg-config + + - name: Install Rust + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + + - name: Shared R2 compiler cache + uses: ./.github/actions/sccache-setup + with: + credential-mode: auto + writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} + writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} + + - name: Cache Cargo registry and git data + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: - cache-key: e2e-${{ matrix.variant }} - fast-linker: "true" - sccache-credential-mode: auto - sccache-writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} - sccache-writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} + key: e2e-${{ matrix.variant }} + cache-on-failure: true + cache-targets: false - name: Build node-subtensor (${{ matrix.variant }}) run: cargo build --profile release ${{ matrix.flags }} -p node-subtensor From d823013a7f05feebf9b3dc83e9a30f249baff3ca Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Mon, 13 Jul 2026 17:01:26 +0200 Subject: [PATCH 41/68] ci: centralize e2e setup and validate shield shards --- .github/workflows/typescript-e2e.yml | 31 ++++++------------------ ts-tests/scripts/validate-e2e-config.mjs | 15 +++++++++++- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index 0fb5c5acc7..45d76274c2 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -45,7 +45,7 @@ jobs: files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename') # SDK-only lockfile movement is covered by SDK checks; do not run # zombienet unless chain/runtime or ts-tests inputs changed. - pattern='^(ts-tests|common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^\.github/(workflows/typescript-e2e\.yml|actions/(run-typescript-e2e|sccache-setup)/|scripts/sccache-configure\.sh$)' + pattern='^(ts-tests|common|node|pallets|precompiles|primitives|runtime|support|chain-extensions|src|vendor)/|^(Cargo\.toml|build\.rs|rust-toolchain\.toml)$|^\.github/(workflows/typescript-e2e\.yml|actions/(run-typescript-e2e|rust-setup|sccache-setup)/|scripts/sccache-configure\.sh$)' if grep -qE "$pattern" <<< "$files"; then echo "e2e=true" >> "$GITHUB_OUTPUT" else @@ -103,29 +103,14 @@ jobs: - name: Check-out repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Install system dependencies - run: | - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends \ - -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \ - build-essential clang curl git make libssl-dev llvm libudev-dev protobuf-compiler pkg-config - - - name: Install Rust - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - - - name: Shared R2 compiler cache - uses: ./.github/actions/sccache-setup - with: - credential-mode: auto - writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} - writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} - - - name: Cache Cargo registry and git data - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + - name: Set up Rust build environment + uses: ./.github/actions/rust-setup with: - key: e2e-${{ matrix.variant }} - cache-on-failure: true - cache-targets: false + cache-key: e2e-${{ matrix.variant }} + fast-linker: "false" + sccache-credential-mode: auto + sccache-writer-access-key-id: ${{ secrets.SCCACHE_R2_WRITE_ACCESS_KEY_ID }} + sccache-writer-secret-access-key: ${{ secrets.SCCACHE_R2_WRITE_SECRET_ACCESS_KEY }} - name: Build node-subtensor (${{ matrix.variant }}) run: cargo build --profile release ${{ matrix.flags }} -p node-subtensor diff --git a/ts-tests/scripts/validate-e2e-config.mjs b/ts-tests/scripts/validate-e2e-config.mjs index 7fc8622e81..d0048c0c93 100644 --- a/ts-tests/scripts/validate-e2e-config.mjs +++ b/ts-tests/scripts/validate-e2e-config.mjs @@ -1,6 +1,7 @@ -import { readdirSync, readFileSync } from "node:fs"; +import { readFileSync, readdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { isDeepStrictEqual } from "node:util"; const tsTestsDir = join(dirname(fileURLToPath(import.meta.url)), ".."); const config = JSON.parse(readFileSync(join(tsTestsDir, "moonwall.config.json"), "utf8")); @@ -65,6 +66,13 @@ if (shieldNodes.length !== 6 || shieldValidators.length !== 3) { ); } +const canonicalShieldEnvironment = environments.get("zombienet_shield"); +if (!canonicalShieldEnvironment) { + throw new Error("Missing Moonwall environment: zombienet_shield"); +} +const sharedShieldSettings = ({ name: _name, include: _include, ...settings }) => settings; +const canonicalShieldSettings = sharedShieldSettings(canonicalShieldEnvironment); + for (const name of ["zombienet_shield", ...shieldShardNames]) { const environment = environments.get(name); const configPath = environment?.foundation?.zombieSpec?.configPath; @@ -75,6 +83,11 @@ for (const name of ["zombienet_shield", ...shieldShardNames]) { if (!connectionNames.has("Node") || !connectionNames.has("NodeFull")) { throw new Error(`${name} must expose authority and full-node connections`); } + if (name !== "zombienet_shield") { + if (!isDeepStrictEqual(sharedShieldSettings(environment), canonicalShieldSettings)) { + throw new Error(`${name} settings must match zombienet_shield except for name and include`); + } + } } console.log( From dc374d1fa5c4c7c14286b1a141707829b75c96df Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 09:18:52 -0600 Subject: [PATCH 42/68] feat: one-URL runtime upgrade signing (btcli upgrade + proposal pre-releases) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release train now publishes each mainnet upgrade proposal as a pre-release v whose page URL is the single thing to share with signers: it carries the signer instructions and the artifacts (proxy_proxy_blob.hex, subtensor.wasm, digest, upgrade-manifest.json) as stable-URL assets. The mainnet watcher promotes the pre-release to the final release once the upgrade executes instead of creating a new one. On the signer side, a new `btcli upgrade` command group replaces the manual prod-approval.js flow: `pending` discovers proposals purely from chain state (sudo.key() -> SudoUncheckedSetCode proxy -> pending multisig ops), `check` verifies the call data end to end (exact structural re-encoding against live metadata, srtool digest, live sudo key, on-chain call hash — with --wasm pinning it to a self-built runtime), and `sign` runs every check then auto-detects the first/interior/final signer position from chain state and submits the matching approval. `btcli multisig pending` surfaces pending upgrades when the multisig is the chain's sudo key. Co-authored-by: Cursor --- .github/scripts/deploy/README.md | 236 +++--- .github/scripts/deploy/prod-approval.js | 13 +- .../deploy/propose-upgrade-multisig.js | 4 +- .github/scripts/deploy/sudo-signatories.json | 9 + .github/workflows/release-train.yml | 149 ++++ .github/workflows/watch-mainnet-release.yml | 55 +- sdk/python/bittensor/cli/commands/multisig.py | 25 +- sdk/python/bittensor/cli/commands/upgrade.py | 610 +++++++++++++++ sdk/python/bittensor/cli/main.py | 2 + sdk/python/bittensor/cli/upgrade_helpers.py | 700 ++++++++++++++++++ 10 files changed, 1646 insertions(+), 157 deletions(-) create mode 100644 .github/scripts/deploy/sudo-signatories.json create mode 100644 sdk/python/bittensor/cli/commands/upgrade.py create mode 100644 sdk/python/bittensor/cli/upgrade_helpers.py diff --git a/.github/scripts/deploy/README.md b/.github/scripts/deploy/README.md index a7b5f341b1..1589b767a3 100644 --- a/.github/scripts/deploy/README.md +++ b/.github/scripts/deploy/README.md @@ -9,10 +9,11 @@ member of the sudo multisig (the "triumvirate"). | ------ | ------- | | `deploy-wasm.js` | Direct sudo upgrade for chains where CI holds the sudo key (devnet, testnet, local mainnet clones). Used by `release-train.yml`. | | `propose-upgrade-multisig.js` | CI's half of the mainnet deployment multisig proposal. Used by `release-train.yml`. | -| `approve-upgrade-multisig.js` | Library for triumvirate first-signer approvals; not run directly. | -| `prod-approval.js` | Runs `approve-upgrade-multisig` with the production triumvirate signatories. | +| `approve-upgrade-multisig.js` | Library for triumvirate first-signer approvals (legacy path); not run directly. | +| `prod-approval.js` | Runs `approve-upgrade-multisig` with the production triumvirate signatories (legacy path). | | `samvps-approval.js` | Same, for the sam-vps multisig. | -| `test-wasm.py` | Verifies a WASM's hash and that its bytes are embedded in the call data. | +| `sudo-signatories.json` | The triumvirate signer set + threshold; consumed by `prod-approval.js` and embedded into `upgrade-manifest.json` by the release train. | +| `test-wasm.py` | Verifies a WASM's hash and that its bytes are embedded in the call data (legacy path; `btcli upgrade check` supersedes it). | ## Background: the two multisig layers @@ -28,54 +29,62 @@ A production runtime upgrade is gated by two nested multisigs: `sudo.key()`. It is the *second* party of the deployment multisig, so the triumvirate has to produce that second approval among themselves. -The first triumvirate signer runs `prod-approval.js`. That script builds the -deployment multisig's finalizing call **and** submits the first triumvirate -approval of it in one transaction. The remaining triumvirate signers then -approve (and the last one executes) from PolkadotJS Apps — no script needed. +When the proposal lands, the release train also publishes a **proposal +pre-release** — `v`, tagged at the exact commit being deployed — +whose page carries the signer instructions and these assets: -Once the final approval lands and the upgrade executes on chain, the release -watcher (`watch-mainnet-release.yml`) cuts the GitHub release and publishes the -artifacts. - -> You may have been a *second* signer before. That part is done in the Polkadot -> UI. Being the **first** signer is different: it is the script run documented -> below, because only the first signer has to assemble the call data. +- `subtensor.wasm` and `subtensor-digest.json` — CI's deterministic srtool build +- `proxy_proxy_blob.hex` — the call data being signed +- `pending-release.json` — machine-readable proposal record (timepoint etc.) +- `upgrade-manifest.json` — everything btcli needs (call hash, commit, signer + set, asset URLs) -## First signer: step by step +The **URL of that pre-release page is the one thing signers need**. Once the +final approval lands and the upgrade executes on chain, the release watcher +(`watch-mainnet-release.yml`) promotes the pre-release to the final release +and publishes the rest of the artifacts. -Only the **first** signer runs the script. Everyone else approves in the UI. +## Signing an upgrade (the btcli path) -### 0. One-time setup +Every triumvirate signer — first, interior, or last — runs the same command: ``` -cd .github/scripts/deploy -npm ci +btcli upgrade sign --url https://github.com//subtensor/releases/tag/v -w ``` -You also need `python3` (for the verification step) and your triumvirate -mnemonic. +Before submitting anything, btcli verifies: -### 1. Find the CI proposal run +- the call data is *exactly* + `proxy.proxy(sudo_key, None, sudo.sudoUncheckedWeight(system.setCode(), ))` + — no batches, no extra calls (re-encoded byte-for-byte against live chain + metadata); +- the embedded runtime matches the release's srtool digest; +- the proxied account is the chain's live `sudo.key()`; +- a pending on-chain deployment-multisig proposal carries `blake2_256(call data)`; +- the resolved signer set derives the on-chain sudo key. -Open the **Release Train** GitHub Action run that proposed this upgrade -(Actions tab). You need its artifact and one timepoint from it. +It then reads your position from chain state — whether the sudo multisig +operation is not yet opened (you are the first signer), underway (interior +approval), or one approval short (your `as_multi` executes the upgrade) — and +submits the matching extrinsic. No signer numbers, no timepoints, no +PolkadotJS. -### 2. Build the runtime yourself with srtool +Related commands: -Do not sign a WASM you were simply handed. Build the runtime from source with -srtool and confirm your output matches CI's — this is the step that makes the -later check meaningful. srtool builds are deterministic, so an identical source -at the same toolchain produces a byte-identical, identically-hashed runtime. +``` +btcli upgrade pending # discover pending proposals from chain state alone +btcli upgrade check --url ... # run every verification without signing +btcli multisig pending -w # pending sudo-multisig ops; also lists pending upgrades +``` -Check out this repo at the exact ref being deployed (the commit on `main` -that triggered the run) and build with the **same parameters CI uses**. The -authoritative recipe is the srtool build step in -[`.github/workflows/release-train.yml`](../../workflows/release-train.yml); -the key parameters are package `node-subtensor-runtime`, profile `production`, -and build option `--features=metadata-hash`. +### Verifying against code you built yourself (recommended) + +Do not sign a WASM you were simply handed. srtool builds are deterministic: +identical source at the same toolchain produces a byte-identical runtime. The +pre-release tag *is* the code being deployed, so: ``` -git checkout +git fetch origin && git checkout v # srtool expects the runtime crate in a directory named after the package: ln -s . runtime/node-subtensor @@ -90,108 +99,63 @@ docker run --rm --user root --platform=linux/amd64 \ ``` Use the srtool image whose Rust version matches subtensor's pinned toolchain -(CI pins this in `scripts/srtool/build-srtool-image.sh`, currently `1.89.0`). -If no prebuilt `paritytech/srtool:` image exists for that -version, build one from source with `scripts/srtool/build-srtool-image.sh` — -that is what CI does. The [`srtool-cli`](https://github.com/chevdor/srtool-cli) -wrapper auto-selects the image and is an easier alternative to the raw -`docker run`. - -When the build finishes, srtool prints the path to the generated -`...node_subtensor_runtime.compact.compressed.wasm` and a one-line JSON digest -as its final output line. - -### 3. Confirm your build matches CI, then assemble the files +(CI pins this in `scripts/srtool/build-srtool-image.sh`; the release's +`upgrade-manifest.json` records the version used). If no prebuilt +`paritytech/srtool:` image exists for that version, build one from +source with `scripts/srtool/build-srtool-image.sh` — that is what CI does. The +[`srtool-cli`](https://github.com/chevdor/srtool-cli) wrapper auto-selects the +image and is an easier alternative to the raw `docker run`. -Download CI's artifact from the proposal run (Actions → the run → -**Artifacts** → `mainnet-upgrade-`). It contains: - -- `subtensor.wasm` and `subtensor-digest.json` — CI's build and digest -- `proxy_proxy_blob.hex` — the call data you will sign -- `pending-release.json` — machine-readable proposal record (timepoint etc.) - -Compare your local srtool output against CI's — the SHA256 must be identical: +Then pin the call data to *your* build: ``` -# your local build: -shasum -a 256 .../node_subtensor_runtime.compact.compressed.wasm -# CI's digest (sha256 field, minus the 0x): -cat subtensor-digest.json +btcli upgrade check --url --wasm ./runtime/target/srtool/production/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm +btcli upgrade sign --url --wasm -w ``` -If they differ, **stop** — your source/toolchain does not match what CI built, -and you must resolve that before signing. - -Once they match, put these three files in this directory -(`.github/scripts/deploy`, next to `package.json`). Use **your locally built** -runtime as `subtensor.wasm` and your srtool digest as `subtensor-digest.json`, -plus CI's `proxy_proxy_blob.hex`: - -- `subtensor.wasm` (your build) -- `subtensor-digest.json` (your build) -- `proxy_proxy_blob.hex` (from CI) - -Then verify the call data embeds that exact runtime: - -``` -python3 test-wasm.py -``` - -This confirms `subtensor.wasm` hashes to `subtensor-digest.json` and that -`proxy_proxy_blob.hex` is, byte for byte, the expected -`proxy.proxy(sudo_key, None, sudo.sudoUncheckedWeight(system.setCode(), ))` -call — the WASM must be the `code` argument of `setCode`, and the call data can -contain nothing else (no batches, no extra calls). It must print -`WASM is correct` before you sign anything. Because the `subtensor.wasm` here is -the one *you* built, a passing check proves the call data executes `setCode` -with exactly the runtime you compiled from source. The one part the script -cannot pin offline is the 32-byte proxied account, which must be the chain's -sudo key — `prod-approval.js` verifies the sudo key separately before -submitting, and the proxy call fails on chain if that account is not the sudo -key. - -### 4. Note the deployment timepoint - -Read `blockHeight` and `extrinsicIndex` from CI's `pending-release.json` -(under `proposal`). Those two numbers are the timepoint of CI's -deployment-multisig approval — you pass them to the script in the next step as -`` and ``. The same numbers appear in the **propose** job's -summary table (Block Height / Extrinsic Index). - -### 5. Run the first-signer approval - -``` -npm run prod-approval -- 0 proxy_proxy_blob.hex -``` - -- `` — the target chain, e.g. `wss://entrypoint-finney.opentensor.ai:443` -- `` — `5FW3gUUAnWZFTG3QWijcGV9ji3iySsuCj12TQi2eDtgGvxej` -- `0` — your signer number; the first signer is always `0` -- `` `` — the timepoint from step 4 -- `proxy_proxy_blob.hex` — the CI call-data file from step 3 - -You will be prompted for your `mnemonic:` (input is hidden). The script first -checks that the triumvirate 2-of-3 derives the on-chain sudo key and aborts if -it does not, then submits your approval. - -### 6. Hand off to the other signers - -The script writes `deployment-multisig-proposal.hex` and prints a summary -containing a **Call Hash** and the **Block Height / Extrinsic Index** of *your* -approval. Send the other triumvirate signers: - -- the **Call Hash**, -- the **timepoint** (your Block Height + Extrinsic Index), and -- the `deployment-multisig-proposal.hex` file (the final signer needs the call - data). - -They then complete it from PolkadotJS Apps (Developer → Multisig, or via the -sudo multisig account) — interior signers `approveAsMulti` with the call hash + -your timepoint, and the last signer `asMulti` with the call data + your -timepoint. When the final approval lands, the spec version bumps and the -release watcher takes over. - -## Argument reference +A passing check then proves the call data executes `setCode` with exactly the +runtime you compiled from source. Without `--wasm`, `check`/`sign` still verify +the published artifacts against each other and the chain, but the runtime bytes +are trusted from the release. + +This structure — a URL anyone can fetch, call data anyone can re-derive from +source, and an on-chain hash anyone can compare — is the template for later +decentralized governance: any holder can `btcli upgrade check --url ...` and +assert the proposal on chain matches the code they are looking at. + +## Legacy path: the node scripts + +The pre-btcli flow still works and is kept as a fallback. It is documented +here in abbreviated form; the scripts are unchanged. + +1. **Find the CI proposal run** (Actions → Release Train) or the proposal + pre-release; download `subtensor.wasm`, `subtensor-digest.json`, + `proxy_proxy_blob.hex`, and `pending-release.json`. +2. **Build the runtime yourself with srtool** (see above) and confirm your + sha256 matches CI's digest. If they differ, **stop**. +3. **Verify the call data** — place `subtensor.wasm` (your build), + `subtensor-digest.json` (your digest), and `proxy_proxy_blob.hex` (CI's) in + this directory and run `python3 test-wasm.py`. It must print + `WASM is correct`. +4. **Note the deployment timepoint** — `blockHeight` / `extrinsicIndex` under + `proposal` in `pending-release.json`. +5. **First signer only** — one-time `npm ci` in this directory, then: + + ``` + npm run prod-approval -- 0 proxy_proxy_blob.hex + ``` + + You are prompted for your mnemonic (hidden). The script checks the + triumvirate 2-of-3 derives the on-chain sudo key, then submits the first + approval and writes `deployment-multisig-proposal.hex`. +6. **Hand off** — send the other signers the printed **Call Hash**, the + **timepoint** of your approval, and `deployment-multisig-proposal.hex`. + Interior signers `approveAsMulti` (call hash + timepoint) and the last + signer `asMulti` (call data + timepoint) from PolkadotJS Apps — or they + simply run `btcli upgrade sign --url ...`, which interoperates with + approvals made either way. + +### Argument reference (legacy scripts) `prod-approval.js` (and `samvps-approval.js`) take: @@ -208,6 +172,6 @@ release watcher takes over. The mnemonic is always read interactively, never passed as an argument. -Triumvirate members and the threshold are defined in `prod-approval.js` -(`SUDO_SIGNATORIES`, `SUDO_THRESHOLD`). `samvps-approval.js` is the equivalent -for the sam-vps multisig. +Triumvirate members and the threshold are defined in `sudo-signatories.json` +(read by `prod-approval.js` and embedded into the release manifest). +`samvps-approval.js` is the equivalent for the sam-vps multisig. diff --git a/.github/scripts/deploy/prod-approval.js b/.github/scripts/deploy/prod-approval.js index 2c9571b119..0c2a90ce3d 100644 --- a/.github/scripts/deploy/prod-approval.js +++ b/.github/scripts/deploy/prod-approval.js @@ -1,13 +1,12 @@ const { main } = require("./approve-upgrade-multisig"); // New triumvirate (June 22, 2026). 2-of-3 derives sudo multisig -// 5DcSqBNqCmfdJZRGFSwwcRb2dZdJHZuKK8Tb1Gx8gbmF5E8s. -const SUDO_SIGNATORIES = [ - "5E7RCRrPVS8TckCDjr92B5ciGziwz2kfvxe4URy3L7AgirGJ", // A - "5FevFjov8435t5XC2MUSRpFYxtthE8pZy1toHpgAAia3ZphG", // B - "5GRCukV2rZmSVfJhAXoLjcrU1pMVCf2Ra1ydbiCFZdaQXDXo", // C -]; -const SUDO_THRESHOLD = 2; // 2 of 3 +// 5DcSqBNqCmfdJZRGFSwwcRb2dZdJHZuKK8Tb1Gx8gbmF5E8s. The signer set lives in +// sudo-signatories.json so the release-train manifest embeds the same list. +const { + signatories: SUDO_SIGNATORIES, + threshold: SUDO_THRESHOLD, +} = require("./sudo-signatories.json"); main(SUDO_SIGNATORIES, SUDO_THRESHOLD).catch((error) => { console.error(error.stack); diff --git a/.github/scripts/deploy/propose-upgrade-multisig.js b/.github/scripts/deploy/propose-upgrade-multisig.js index 174d8a9387..a6fd8932ff 100644 --- a/.github/scripts/deploy/propose-upgrade-multisig.js +++ b/.github/scripts/deploy/propose-upgrade-multisig.js @@ -197,13 +197,15 @@ blob artifact provided by the CI. fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary); } - // Machine-readable record for the release watcher + // Machine-readable record for the release watcher and upgrade manifest const pendingRelease = { callHash, blockHash, blockHeight, extrinsicIndex, multisigAddress, + ciAddress: ciKeyAddress, + sudoKey, specVersionBefore, }; fs.writeFileSync( diff --git a/.github/scripts/deploy/sudo-signatories.json b/.github/scripts/deploy/sudo-signatories.json new file mode 100644 index 0000000000..6fce246ed7 --- /dev/null +++ b/.github/scripts/deploy/sudo-signatories.json @@ -0,0 +1,9 @@ +{ + "_note": "Production sudo multisig (triumvirate) signer set. 2-of-3 derives the on-chain sudo key 5DcSqBNqCmfdJZRGFSwwcRb2dZdJHZuKK8Tb1Gx8gbmF5E8s. Consumed by prod-approval.js and embedded into upgrade-manifest.json by release-train.yml; btcli verifies the derived address against the live sudo key before signing, so a stale entry fails loudly.", + "signatories": [ + "5E7RCRrPVS8TckCDjr92B5ciGziwz2kfvxe4URy3L7AgirGJ", + "5FevFjov8435t5XC2MUSRpFYxtthE8pZy1toHpgAAia3ZphG", + "5GRCukV2rZmSVfJhAXoLjcrU1pMVCf2Ra1ydbiCFZdaQXDXo" + ], + "threshold": 2 +} diff --git a/.github/workflows/release-train.yml b/.github/workflows/release-train.yml index 7869eb3bf5..debc440e60 100644 --- a/.github/workflows/release-train.yml +++ b/.github/workflows/release-train.yml @@ -378,6 +378,8 @@ jobs: runs-on: [self-hosted, fireactions-turbo-8] environment: mainnet timeout-minutes: 30 + permissions: + contents: write # publish the proposal pre-release + tag steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 @@ -449,6 +451,49 @@ jobs: > wasm/pending-release.json cat wasm/pending-release.json + # Everything a signer (or btcli) needs to locate, verify, and sign the + # proposal, in one machine-readable file. `btcli upgrade check/sign` + # consumes it via the release asset URL. + - name: Build upgrade manifest + if: steps.guard.outputs.should_propose == 'true' + run: | + spec="${{ needs.build.outputs.spec_version }}" + tag="v${spec}" + base="https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}" + jq -n \ + --arg repo "$GITHUB_REPOSITORY" \ + --arg tag "$tag" \ + --arg base "$base" \ + --arg network "finney" \ + --slurpfile pending wasm/pending-release.json \ + --slurpfile digest wasm/subtensor-digest.json \ + --slurpfile sudo .github/scripts/deploy/sudo-signatories.json \ + '{ + kind: "runtime-upgrade-proposal", + network: $network, + repo: $repo, + tag: $tag, + spec_version: $pending[0].expected_spec_version, + commit: $pending[0].commit, + call_hash: $pending[0].proposal.callHash, + proposal: $pending[0].proposal, + ci_address: $pending[0].proposal.ciAddress, + deployment_multisig: $pending[0].proposal.multisigAddress, + sudo_key: $pending[0].proposal.sudoKey, + sudo: {signatories: $sudo[0].signatories, threshold: $sudo[0].threshold}, + wasm_sha256: $digest[0].sha256, + srtool_rustc: ($digest[0].rustc // null), + max_weight: {ref_time: 50000000000, proof_size: 0}, + assets: { + wasm: ($base + "/subtensor.wasm"), + digest: ($base + "/subtensor-digest.json"), + call_data: ($base + "/proxy_proxy_blob.hex"), + pending_release: ($base + "/pending-release.json"), + manifest: ($base + "/upgrade-manifest.json") + } + }' > wasm/upgrade-manifest.json + cat wasm/upgrade-manifest.json + - name: Upload runtime + call data artifacts if: steps.guard.outputs.should_propose == 'true' uses: actions/upload-artifact@v4 @@ -459,5 +504,109 @@ jobs: wasm/subtensor-digest.json wasm/proxy_proxy_blob.hex wasm/pending-release.json + wasm/upgrade-manifest.json if-no-files-found: error retention-days: 90 + + # The single URL to share with the other signers. A *pre-release* at the + # deployed commit: the release page carries the signer instructions and + # the artifacts as stable-URL assets; watch-mainnet-release.yml promotes + # it to a final release once the upgrade executes on chain. + - name: Publish proposal pre-release + if: steps.guard.outputs.should_propose == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + spec="${{ needs.build.outputs.spec_version }}" + tag="v${spec}" + url="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${tag}" + + # A published (non-prerelease) v means this spec already + # shipped; never touch it. A pre-release for the same spec is a + # respun proposal (e.g. the first train failed mid-way): replace it. + if existing=$(gh release view "$tag" --json isPrerelease --jq '.isPrerelease' 2>/dev/null); then + if [ "$existing" != "true" ]; then + echo "release $tag already published as a final release; refusing to overwrite" + exit 1 + fi + echo "replacing existing proposal pre-release $tag" + gh release delete "$tag" --cleanup-tag --yes + fi + + commit=$(jq -r '.commit' wasm/pending-release.json) + call_hash=$(jq -r '.proposal.callHash' wasm/pending-release.json) + height=$(jq -r '.proposal.blockHeight' wasm/pending-release.json) + index=$(jq -r '.proposal.extrinsicIndex' wasm/pending-release.json) + ci_address=$(jq -r '.proposal.ciAddress' wasm/pending-release.json) + deploy_ms=$(jq -r '.proposal.multisigAddress' wasm/pending-release.json) + sha256=$(jq -r '.sha256' wasm/subtensor-digest.json) + + cat > release-body.md < + \`\`\` + + btcli verifies the call data against the chain (structure, sudo key, + on-chain call hash) before submitting, and picks the right approval + (first / interior / final) from chain state automatically. + + ### Verify it independently (recommended) + + Build the runtime from this tag with srtool and compare byte-for-byte + (see \`.github/scripts/deploy/README.md\` for the full recipe): + + \`\`\` + git fetch origin && git checkout ${tag} + # srtool build, then: + btcli upgrade check --url ${url} --wasm + \`\`\` + + Without \`--wasm\`, \`btcli upgrade check --url ${url}\` still verifies + the published artifacts against each other and the chain, but trusts + the released wasm; a local srtool build closes that gap. + + ### Proposal details + + | | | + | --- | --- | + | spec_version | \`${spec}\` | + | commit | \`${commit}\` | + | call hash | \`${call_hash}\` | + | deployment timepoint | \`${height}:${index}\` | + | CI key | \`${ci_address}\` | + | deployment multisig | \`${deploy_ms}\` | + | wasm sha256 | \`${sha256}\` | + EOF + + gh release create "$tag" \ + --repo "$GITHUB_REPOSITORY" \ + --target "$commit" \ + --prerelease \ + --title "Runtime ${spec} (proposed)" \ + --notes-file release-body.md \ + wasm/subtensor.wasm \ + wasm/subtensor-digest.json \ + wasm/proxy_proxy_blob.hex \ + wasm/pending-release.json \ + wasm/upgrade-manifest.json + + { + echo "## Share this with the other signers" + echo "" + echo "**${url}**" + echo "" + echo '```' + echo "btcli upgrade sign --url ${url} -w " + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/watch-mainnet-release.yml b/.github/workflows/watch-mainnet-release.yml index 28d2cce2e8..7d1ff37f4d 100644 --- a/.github/workflows/watch-mainnet-release.yml +++ b/.github/workflows/watch-mainnet-release.yml @@ -72,9 +72,12 @@ jobs: # Match both this workflow's tags (v424) and the legacy scheme # inherited from upstream (v3.4.9-424) so an already-released - # runtime is never released twice. + # runtime is never released twice. Pre-releases don't count: the + # release train publishes the proposal as a pre-release v, + # which this workflow promotes to the final release below. if gh release list --repo "$GITHUB_REPOSITORY" --limit 300 \ - --json tagName --jq '.[].tagName' \ + --json tagName,isPrerelease \ + --jq '.[] | select(.isPrerelease | not) | .tagName' \ | grep -Eq "^v${local_spec}$|-${local_spec}$"; then echo "A release for spec_version $local_spec already exists; nothing to do." echo "ready=false" >> $GITHUB_OUTPUT @@ -136,21 +139,51 @@ jobs: unzip -o artifact.zip -d wasm ls -la wasm - - name: Create release with runtime assets + - name: Create or promote release with runtime assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SPEC_VERSION: ${{ needs.check.outputs.spec_version }} RELEASE_SHA: ${{ needs.check.outputs.sha }} run: | - gh release create "v${SPEC_VERSION}" \ - --repo "$GITHUB_REPOSITORY" \ - --target "$RELEASE_SHA" \ - --title "Runtime ${SPEC_VERSION}" \ - --generate-notes \ - wasm/subtensor.wasm \ - wasm/subtensor-digest.json \ - wasm/proxy_proxy_blob.hex \ + tag="v${SPEC_VERSION}" + assets=( + wasm/subtensor.wasm + wasm/subtensor-digest.json + wasm/proxy_proxy_blob.hex wasm/pending-release.json + ) + [ -f wasm/upgrade-manifest.json ] && assets+=(wasm/upgrade-manifest.json) + + # The release train published the proposal as a pre-release at this + # tag; the upgrade has now executed on chain, so promote it to the + # final release. Assets are re-uploaded from the provenance-gated + # workflow artifact so the release always carries the trusted bytes. + # Fall back to creating the release for trains that predate the + # proposal pre-release flow. + if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + tag_sha=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" --jq '.object.sha' || true) + if [ -n "$tag_sha" ] && [ "$tag_sha" != "$RELEASE_SHA" ]; then + echo "tag $tag points at $tag_sha but the released commit is $RELEASE_SHA;" + echo "the proposal pre-release does not match the executed upgrade — resolve manually." + exit 1 + fi + notes=$(gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \ + -f tag_name="$tag" -f target_commitish="$RELEASE_SHA" --jq '.body' || true) + gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber "${assets[@]}" + gh release edit "$tag" \ + --repo "$GITHUB_REPOSITORY" \ + --title "Runtime ${SPEC_VERSION}" \ + ${notes:+--notes "$notes"} \ + --prerelease=false \ + --latest + else + gh release create "$tag" \ + --repo "$GITHUB_REPOSITORY" \ + --target "$RELEASE_SHA" \ + --title "Runtime ${SPEC_VERSION}" \ + --generate-notes \ + "${assets[@]}" + fi # Mirror: the mainnet branch always points at the code now running on # mainnet (the release-train commit, not HEAD of main). Pushed over diff --git a/sdk/python/bittensor/cli/commands/multisig.py b/sdk/python/bittensor/cli/commands/multisig.py index 3115cbc3ed..18e36e3f0e 100644 --- a/sdk/python/bittensor/cli/commands/multisig.py +++ b/sdk/python/bittensor/cli/commands/multisig.py @@ -14,9 +14,11 @@ from ... import config as cfg from ... import storage, wallets from .. import multisig_helpers as ms_helpers +from .. import upgrade_helpers as uh from ..context import AppContext, ctx_of from ..globals import with_globals from ..prompt import confirm_wallet +from .upgrade import load_pending_upgrades, render_upgrade_records app = typer.Typer( no_args_is_help=True, @@ -277,7 +279,7 @@ def multisig_pending( async def _load(client): ms = await client.multisig(signatories_resolved, threshold) - return await ms_helpers.list_pending_with_commands( + records = await ms_helpers.list_pending_with_commands( client, app_ctx, ms=ms, @@ -288,6 +290,25 @@ async def _load(client): call_hash_filter=call_hash, call_data=call_data, ) + # When this multisig IS the chain's sudo key, pending runtime-upgrade + # proposals (held one layer out, on the CI deployment multisig) are + # part of what its signers are waiting to act on — surface them here. + upgrades: list = [] + sudo_key = await client.query(storage.Sudo.Key) + if str(sudo_key) == ms.address and not call_hash: + upgrades = await load_pending_upgrades(client, app_ctx, uh.DEFAULT_UPGRADE_REPO) + return records, upgrades - records = app_ctx.run(_load) + records, upgrades = app_ctx.run(_load) + if app_ctx.output.json_mode and upgrades: + app_ctx.output.value( + { + "pending_multisigs": records, + "count": len(records), + "pending_upgrades": upgrades, + } + ) + return app_ctx.output.pending_multisigs(records, title=f"pending multisig — {label}") + if upgrades: + render_upgrade_records(app_ctx, upgrades) diff --git a/sdk/python/bittensor/cli/commands/upgrade.py b/sdk/python/bittensor/cli/commands/upgrade.py new file mode 100644 index 0000000000..5426668270 --- /dev/null +++ b/sdk/python/bittensor/cli/commands/upgrade.py @@ -0,0 +1,610 @@ +"""`btcli upgrade`: runtime-upgrade proposals — discover, verify, sign. + +The release train proposes a mainnet runtime upgrade as the CI half of a +2-of-2 deployment multisig and publishes a proposal pre-release whose URL is +the one thing signers need: + + btcli upgrade pending # what is waiting, from chain state + btcli upgrade check --url # verify the call data end to end + btcli upgrade sign --url -w # approve it + +`sign` re-runs every check first and refuses on any mismatch, then picks the +right approval (first / interior / final) from chain state — no signer +numbers, timepoints, or PolkadotJS required. +""" + +from __future__ import annotations + +import asyncio +from typing import Any, Optional + +import typer + +from ... import calls +from .. import multisig_helpers as ms_helpers +from .. import upgrade_helpers as uh +from ..context import AppContext, ctx_of +from ..globals import with_globals, with_tx_globals +from ..prompt import confirm_wallet + +app = typer.Typer( + no_args_is_help=True, + help="Discover, verify, and sign runtime-upgrade proposals.", +) + + +# --- shared rendering -------------------------------------------------------------------- + + +def _approval_summary(approvals: list[str], threshold: int) -> str: + return f"{len(approvals)} of {threshold} approvals" + + +def upgrade_record_fields(record: dict[str, Any]) -> dict[str, Any]: + """Human key/value view of one discovered pending upgrade.""" + fields: dict[str, Any] = {} + release = record.get("release") + if release: + if release.get("spec_version") is not None: + fields["spec_version"] = release["spec_version"] + fields["release"] = release.get("url") + if release.get("tag"): + fields["source"] = f"{release.get('repo')}@{release['tag']} ({release.get('commit')})" + fields["call_hash"] = record["call_hash"] + fields["proposed at"] = record["timepoint_display"] + fields["CI key"] = record["ci_address"] + fields["deployment multisig"] = record["deployment_multisig"] + fields["sudo key"] = record["sudo_key"] + fields["deployment layer"] = _approval_summary(record["deployment_approvals"], 2) + " (CI)" + sudo_layer = record.get("sudo_layer") + threshold = record.get("sudo_threshold") + if sudo_layer: + summary = _approval_summary(sudo_layer["approvals"], threshold or 0) + if not threshold: + summary = f"{len(sudo_layer['approvals'])} approvals so far" + fields["sudo multisig layer"] = ( + f"{summary} — opened at " + f"{sudo_layer['timepoint']['height']}:{sudo_layer['timepoint']['index']}" + ) + elif record.get("release"): + fields["sudo multisig layer"] = "not opened yet — the next signer opens it" + if record.get("sign_command"): + fields["sign with"] = record["sign_command"] + elif not release: + fields["note"] = ( + "no matching release manifest found — pass the proposal's release URL " + "to `btcli upgrade check/sign` directly" + ) + return fields + + +def render_upgrade_records(app_ctx: AppContext, records: list[dict[str, Any]]) -> None: + out = app_ctx.output + if out.json_mode: + out.value({"pending_upgrades": records, "count": len(records)}) + return + if not records: + out.detail("pending runtime upgrades", {}) + return + for index, record in enumerate(records): + title = "pending runtime upgrade" if index == 0 else None + out.detail(title, upgrade_record_fields(record)) + + +async def _enrich_upgrades(client, app_ctx: AppContext, records: list[dict], repo: str) -> None: + """Attach release/manifest info and sudo-layer status to discovered upgrades. + + Enrichment is advisory: GitHub being unreachable degrades the output but + never fails the command. Fetches run in a worker thread so the websocket + stays serviced. + """ + manifests = await asyncio.to_thread(uh.fetch_release_manifests, repo) + for record in records: + manifest = uh.find_manifest_for_call_hash(manifests, record["call_hash"]) + if not manifest: + continue + record["release"] = { + "url": manifest.get("release_url"), + "repo": manifest.get("repo"), + "tag": manifest.get("tag"), + "commit": manifest.get("commit"), + "spec_version": manifest.get("spec_version"), + "prerelease": manifest.get("prerelease"), + } + if manifest.get("release_url"): + record["sign_command"] = uh.sign_command(app_ctx.network, manifest["release_url"]) + sudo = manifest.get("sudo") or {} + if sudo.get("threshold"): + record["sudo_threshold"] = int(sudo["threshold"]) + call_data_url = (manifest.get("assets") or {}).get("call_data") + if not call_data_url: + continue + try: + blob = uh.parse_hex_blob(await asyncio.to_thread(uh.fetch_bytes, call_data_url)) + except ValueError: + continue + if uh.call_hash_hex(blob) != record["call_hash"]: + continue # release asset does not match this on-chain op; don't trust it + finalizing = await uh.compose_finalizing_call( + client, + blob=blob, + ci_address=record["ci_address"], + deploy_timepoint=record["timepoint"], + ) + record["sudo_layer"] = await uh.sudo_layer_status( + client, + sudo_key=record["sudo_key"], + finalizing_call_hash=ms_helpers.hex_bytes(finalizing.call_hash), + ) + + +async def load_pending_upgrades(client, app_ctx: AppContext, repo: str) -> list[dict]: + records = await uh.discover_pending_upgrades(client) + if records: + await _enrich_upgrades(client, app_ctx, records, repo) + return records + + +# --- commands ---------------------------------------------------------------------------- + + +@app.command("pending") +@with_globals +def upgrade_pending( + ctx: typer.Context, + repo: str = typer.Option( + uh.DEFAULT_UPGRADE_REPO, + "--repo", + help="GitHub repo whose releases carry the upgrade manifests.", + ), +): + """List pending runtime-upgrade proposals, discovered from chain state. + + Walks sudo.key() -> its SudoUncheckedSetCode proxy -> pending multisig + operations, then matches each against the repo's release manifests to show + the spec version, source commit, both approval layers, and the exact sign + command. + """ + app_ctx = ctx_of(ctx) + records = app_ctx.run(lambda client: load_pending_upgrades(client, app_ctx, repo)) + render_upgrade_records(app_ctx, records) + + +def _bundle_or_exit( + app_ctx: AppContext, + *, + url: Optional[str], + hex_url: Optional[str], + hex_file: Optional[str], + wasm: Optional[str], +) -> uh.ProposalBundle: + app_ctx.output.message("[dim]fetching proposal artifacts…[/dim]") + try: + bundle = uh.fetch_proposal_bundle( + release_url=url, hex_url=hex_url, hex_file=hex_file, wasm_path=wasm + ) + except (ValueError, OSError) as error: + app_ctx.output.error(str(error)) + raise typer.Exit(1) + for note in bundle.notes: + app_ctx.output.message(f"[dim]note: {note}[/dim]") + return bundle + + +def _render_checks(app_ctx: AppContext, outcome: uh.CheckOutcome) -> None: + out = app_ctx.output + if out.json_mode: + out.value( + { + "ok": outcome.ok, + "checks": [ + {"name": c.name, "ok": c.ok, "detail": c.detail} for c in outcome.checks + ], + "data": outcome.data, + } + ) + return + glyphs = {True: "✓", False: "✗ FAIL:", None: "– skipped:"} + fields = {c.name: f"{glyphs[c.ok]} {c.detail}" for c in outcome.checks} + out.detail("upgrade check", fields) + + +def _print_reproduce_recipe(app_ctx: AppContext, bundle: uh.ProposalBundle) -> None: + """When the wasm came from the release (or nowhere), point at self-building.""" + if bundle.wasm_source == "local" or app_ctx.output.json_mode: + return + out = app_ctx.output + out.message("") + out.message( + "to verify against code you compiled yourself, build the runtime with " + "srtool and re-run with --wasm:" + ) + for line in uh.srtool_recipe(bundle.manifest): + out.message(f" [dim]{line}[/dim]") + + +@app.command( + "check", + epilog=( + "Example: btcli upgrade check " + "--url https://github.com/RaoFoundation/subtensor/releases/tag/v426 " + "--wasm ./my-srtool-build.wasm" + ), +) +@with_globals +def upgrade_check( + ctx: typer.Context, + url: Optional[str] = typer.Option( + None, + "--url", + help="Proposal release page (https://github.com/O/R/releases/tag/TAG or O/R@TAG).", + ), + hex_url: Optional[str] = typer.Option( + None, "--hex-url", help="URL of the raw call-data hex (proxy_proxy_blob.hex)." + ), + hex_file: Optional[str] = typer.Option( + None, "--hex-file", help="Local path to the call-data hex file." + ), + wasm: Optional[str] = typer.Option( + None, + "--wasm", + help="Runtime wasm you built yourself (srtool); pins the call data to your build.", + ), +): + """Verify a proposed runtime upgrade end to end without signing anything. + + Checks that the call data is exactly proxy.proxy(sudo_key, None, + sudoUncheckedWeight(setCode(wasm), )), that re-encoding it + against live chain metadata reproduces it byte-for-byte, that the embedded + runtime matches the wasm and srtool digest, that the proxied account is + the chain's sudo key, and that a pending on-chain proposal carries its + exact call hash. + """ + app_ctx = ctx_of(ctx) + bundle = _bundle_or_exit(app_ctx, url=url, hex_url=hex_url, hex_file=hex_file, wasm=wasm) + outcome = app_ctx.run(lambda client: uh.run_proposal_checks(client, bundle)) + _render_checks(app_ctx, outcome) + _print_reproduce_recipe(app_ctx, bundle) + if not outcome.ok: + app_ctx.output.error( + "verification failed: " + "; ".join(c.name for c in outcome.failed()), + help="do not sign this proposal until every check passes", + ) + raise typer.Exit(1) + + +def _resolve_sudo_signer_set( + app_ctx: AppContext, + *, + multisig: Optional[str], + multisig_threshold: Optional[int], + signatories: Optional[str], + other_signatories: Optional[str], + manifest: Optional[dict], +) -> tuple[int, list[str]]: + """The sudo multisig's (threshold, resolved signatories) for signing. + + Explicit flags win; otherwise the release manifest's published signer set + is used. Either way the derived address is verified against the live sudo + key before anything is submitted. + """ + threshold, sigs, _preset, _refs = ms_helpers.resolve_multisig( + app_ctx, + multisig_name=multisig, + threshold=multisig_threshold, + signatories=signatories, + other_signatories=other_signatories, + signer="coldkey", + wallet_default=None, + ) + if threshold is not None: + return threshold, sigs + sudo = (manifest or {}).get("sudo") or {} + manifest_sigs = [str(s) for s in sudo.get("signatories") or []] + manifest_threshold = sudo.get("threshold") + if manifest_sigs and manifest_threshold: + return int(manifest_threshold), list(dict.fromkeys(manifest_sigs)) + raise ValueError( + "cannot determine the sudo multisig signer set: the release manifest has " + "no sudo block — pass `--multisig NAME` (a saved multisig) or " + "`--multisig-threshold N --signatories a,b,c`" + ) + + +@app.command( + "sign", + epilog=( + "Example: btcli upgrade sign " + "--url https://github.com/RaoFoundation/subtensor/releases/tag/v426 -w trium-a" + ), +) +@with_tx_globals +def upgrade_sign( + ctx: typer.Context, + url: Optional[str] = typer.Option( + None, + "--url", + help="Proposal release page (https://github.com/O/R/releases/tag/TAG or O/R@TAG).", + ), + hex_url: Optional[str] = typer.Option( + None, "--hex-url", help="URL of the raw call-data hex (proxy_proxy_blob.hex)." + ), + hex_file: Optional[str] = typer.Option( + None, "--hex-file", help="Local path to the call-data hex file." + ), + wasm: Optional[str] = typer.Option( + None, + "--wasm", + help="Runtime wasm you built yourself (srtool); pins the call data to your build.", + ), + multisig: Optional[str] = typer.Option( + None, "--multisig", help="Saved sudo-multisig name (see `btcli multisig list`)." + ), + multisig_threshold: Optional[int] = typer.Option( + None, "--multisig-threshold", help="Sudo multisig threshold (with --signatories)." + ), + signatories: Optional[str] = typer.Option( + None, + "--signatories", + help="Full sudo signer set: ss58, address-book names, or wallet names.", + ), + other_signatories: Optional[str] = typer.Option( + None, + "--other-signatories", + help="Other sudo signers only; your -w wallet coldkey is added.", + ), +): + """Verify a proposed runtime upgrade, then submit your multisig approval. + + Runs every `upgrade check` verification first and refuses to sign on any + mismatch. Your position is read from chain state: if the sudo multisig has + not acted yet you open the operation (first approval), if it is underway + you approve, and if yours is the final approval the upgrade executes in + the same extrinsic. + """ + app_ctx = ctx_of(ctx) + bundle = _bundle_or_exit(app_ctx, url=url, hex_url=hex_url, hex_file=hex_file, wasm=wasm) + + confirm_wallet( + app_ctx, + help_text="Wallet whose coldkey is a sudo multisig signatory.", + require_coldkey=True, + ) + try: + sudo_threshold, sudo_signatories = _resolve_sudo_signer_set( + app_ctx, + multisig=multisig, + multisig_threshold=multisig_threshold, + signatories=signatories, + other_signatories=other_signatories, + manifest=bundle.manifest, + ) + except ValueError as error: + app_ctx.output.error(str(error)) + raise typer.Exit(1) + + signer_address = app_ctx.wallet().coldkeypub.ss58_address + if signer_address not in sudo_signatories: + app_ctx.output.error( + f"wallet {app_ctx.wallet_name!r} coldkey {signer_address} is not in the " + "sudo multisig signer set", + note="signatories: " + ", ".join(sudo_signatories), + ) + raise typer.Exit(1) + + # Phase 1: verify everything and read the current signing position. + async def _verify(client): + outcome = await uh.run_proposal_checks(client, bundle) + plan: dict[str, Any] = {} + if outcome.ok: + derived = await client.multisig(sudo_signatories, sudo_threshold) + plan["multisig_address"] = derived.address + plan["matches_sudo"] = derived.address == outcome.data["sudo_key"] + if plan["matches_sudo"]: + pending = outcome.data["pending"] + finalizing = await uh.compose_finalizing_call( + client, + blob=bundle.blob, + ci_address=pending["ci_address"], + deploy_timepoint=pending["timepoint"], + ) + plan["finalizing_call_hash"] = ms_helpers.hex_bytes(finalizing.call_hash) + plan["sudo_layer"] = await uh.sudo_layer_status( + client, + sudo_key=outcome.data["sudo_key"], + finalizing_call_hash=plan["finalizing_call_hash"], + ) + return outcome, plan + + outcome, plan = app_ctx.run(_verify) + _render_checks(app_ctx, outcome) + if not outcome.ok: + app_ctx.output.error( + "verification failed: " + "; ".join(c.name for c in outcome.failed()), + help="refusing to sign; resolve every failed check first", + ) + raise typer.Exit(1) + if not plan["matches_sudo"]: + app_ctx.output.error( + f"the resolved signer set derives {plan['multisig_address']}, which is not " + f"the chain's sudo key {outcome.data['sudo_key']}", + help="check the threshold and signatory list (or the saved multisig entry)", + ) + raise typer.Exit(1) + + position, prompt = _signing_position( + plan.get("sudo_layer"), sudo_threshold, signer_address, outcome.data + ) + if position == "signed": + app_ctx.output.message(prompt) + _print_share_hint(app_ctx, url, plan.get("sudo_layer"), sudo_signatories) + return + + if bundle.wasm_source != "local": + app_ctx.output.message( + "[yellow]note:[/yellow] the runtime was taken from the release, not a local " + "srtool build — anyone reproducing the build should pass --wasm" + ) + + if app_ctx.dry_run: + app_ctx.output.detail( + "dry run: upgrade sign", + { + "position": position, + "action": prompt, + "sudo_multisig": plan["multisig_address"], + "finalizing_call_hash": plan["finalizing_call_hash"], + }, + ) + return + + app_ctx.confirm(prompt + "?") + signing = app_ctx.signer("coldkey") + + # Phase 2: re-read the position from live state and submit the matching + # approval (state may have advanced since the prompt — later is only ever + # *more* approvals, and the submission adapts). + async def _submit(client): + pending = await uh.find_pending_upgrade(client, bundle.call_hash) + if pending is None: + raise ValueError( + "the deployment-multisig proposal disappeared from chain state " + "(executed or cancelled since verification)" + ) + finalizing = await uh.compose_finalizing_call( + client, + blob=bundle.blob, + ci_address=pending["ci_address"], + deploy_timepoint=pending["timepoint"], + ) + sudo_layer = await uh.sudo_layer_status( + client, + sudo_key=pending["sudo_key"], + finalizing_call_hash=ms_helpers.hex_bytes(finalizing.call_hash), + ) + approvals = (sudo_layer or {}).get("approvals") or [] + if signer_address in approvals: + return None, sudo_layer + others = uh.sorted_other_signatories(sudo_signatories, signer_address) + if sudo_layer is None: + call = calls.Multisig.approve_as_multi( + threshold=sudo_threshold, + other_signatories=others, + maybe_timepoint=None, + call_hash=finalizing.call_hash, + max_weight=uh.FINALIZE_WEIGHT, + ) + elif len(approvals) < sudo_threshold - 1: + call = calls.Multisig.approve_as_multi( + threshold=sudo_threshold, + other_signatories=others, + maybe_timepoint=sudo_layer["timepoint"], + call_hash=finalizing.call_hash, + max_weight=uh.FINALIZE_WEIGHT, + ) + else: + call = calls.Multisig.as_multi( + threshold=sudo_threshold, + other_signatories=others, + maybe_timepoint=sudo_layer["timepoint"], + call=finalizing, + max_weight=uh.FINALIZE_WEIGHT, + ) + result = await client.submit_call( + call, signing, signer="coldkey", wait_for_finalization=False + ) + updated = await uh.sudo_layer_status( + client, + sudo_key=pending["sudo_key"], + finalizing_call_hash=ms_helpers.hex_bytes(finalizing.call_hash), + ) + return result, updated + + result, sudo_layer = app_ctx.run(_submit) + if result is None: + app_ctx.output.message("your approval is already recorded on-chain; nothing to submit") + _print_share_hint(app_ctx, url, sudo_layer, sudo_signatories) + return + if not app_ctx.output.result(result, "runtime-upgrade approval submitted"): + raise typer.Exit(1) + _report_after_sign(app_ctx, url, sudo_layer, sudo_signatories, sudo_threshold) + + +def _signing_position( + sudo_layer: Optional[dict], + threshold: int, + signer_address: str, + check_data: dict[str, Any], +) -> tuple[str, str]: + """(position, human action line) for the confirmation prompt.""" + spec = ((check_data.get("manifest") or {}).get("spec_version")) or "?" + if sudo_layer is None: + return ( + "first", + f"open the sudo-multisig approval for runtime upgrade {spec} " + f"(approval 1 of {threshold})", + ) + approvals = sudo_layer.get("approvals") or [] + if signer_address in approvals: + return ( + "signed", + f"your approval is already recorded ({len(approvals)} of {threshold} so far)", + ) + if len(approvals) >= threshold - 1: + return ( + "final", + f"submit the FINAL approval for runtime upgrade {spec} — " + "the upgrade executes in this extrinsic", + ) + return ( + "interior", + f"add approval {len(approvals) + 1} of {threshold} for runtime upgrade {spec}", + ) + + +def _print_share_hint( + app_ctx: AppContext, + url: Optional[str], + sudo_layer: Optional[dict], + sudo_signatories: list[str], +) -> None: + if app_ctx.output.json_mode or not url: + return + approvals = (sudo_layer or {}).get("approvals") or [] + remaining = [s for s in sudo_signatories if s not in approvals] + if remaining: + app_ctx.output.message("waiting on: " + ", ".join(remaining)) + app_ctx.output.message(f"they can run: {uh.sign_command(app_ctx.network, url)}") + + +def _report_after_sign( + app_ctx: AppContext, + url: Optional[str], + sudo_layer: Optional[dict], + sudo_signatories: list[str], + threshold: int, +) -> None: + if sudo_layer is None: + # The op is gone from pending state: the threshold was reached and the + # finalizing call executed — the upgrade is done (or the pending read + # lagged; `upgrade pending` will confirm). + app_ctx.output.message( + "the sudo multisig operation is complete — the upgrade should now " + "execute; verify with `btcli upgrade pending` and the chain's spec version" + ) + return + approvals = sudo_layer.get("approvals") or [] + fields: dict[str, Any] = { + "approvals": f"{len(approvals)} of {threshold}", + "opened at": (f"{sudo_layer['timepoint']['height']}:{sudo_layer['timepoint']['index']}"), + "call_hash": sudo_layer["call_hash"], + } + remaining = [s for s in sudo_signatories if s not in approvals] + if remaining: + fields["waiting on"] = ", ".join(remaining) + if url: + fields["they run"] = uh.sign_command(app_ctx.network, url) + app_ctx.output.detail("sudo multisig status", fields) + + +__all__ = ["app", "load_pending_upgrades", "render_upgrade_records", "upgrade_record_fields"] diff --git a/sdk/python/bittensor/cli/main.py b/sdk/python/bittensor/cli/main.py index ddc8cbd5de..a5bf86f889 100644 --- a/sdk/python/bittensor/cli/main.py +++ b/sdk/python/bittensor/cli/main.py @@ -43,6 +43,7 @@ subnets, sudo, timelock, + upgrade, utils, wallet, weights, @@ -107,6 +108,7 @@ def list_commands(self, ctx) -> list[str]: app.add_typer(proxy.app, name="proxy", rich_help_panel=PANEL_PROXY_MULTISIG) app.add_typer(multisig.app, name="multisig", rich_help_panel=PANEL_PROXY_MULTISIG) +app.add_typer(upgrade.app, name="upgrade", rich_help_panel=PANEL_PROXY_MULTISIG) # Hidden group aliases carried over from the v9 btcli (`btcli w list`, etc.). for _sub_app, _aliases in ( diff --git a/sdk/python/bittensor/cli/upgrade_helpers.py b/sdk/python/bittensor/cli/upgrade_helpers.py new file mode 100644 index 0000000000..1b81c87454 --- /dev/null +++ b/sdk/python/bittensor/cli/upgrade_helpers.py @@ -0,0 +1,700 @@ +"""Runtime-upgrade proposal helpers: discovery, verification, signing support. + +A mainnet runtime upgrade is proposed by CI as the first half of a 2-of-2 +deployment multisig (CI key + sudo multisig) holding a ``SudoUncheckedSetCode`` +proxy on the chain's sudo key (see ``.github/scripts/deploy`` in the subtensor +repo). Everything about a pending proposal is discoverable from chain state: + + sudo.key() -> proxy.proxies(sudo_key) # the deployment multisig + -> Multisig.Multisigs(deploy) # the pending proposal (hash only) + -> depositor == CI key # multi([CI, sudo], 2) == delegate + +The call *data* is not on-chain (CI approves by hash); it ships as the +``proxy_proxy_blob.hex`` asset of the proposal pre-release that the release +train publishes, alongside ``upgrade-manifest.json`` (spec version, commit, +signer set, asset URLs). These helpers fetch that bundle, verify the call data +is exactly ``proxy.proxy(sudo_key, None, sudoUncheckedWeight(setCode(wasm), +W))`` and matches the on-chain hash, and build the finalizing call the sudo +multisig signs. +""" + +from __future__ import annotations + +import json +import os +import re +import urllib.error +import urllib.request +from dataclasses import dataclass, field +from hashlib import blake2b, sha256 +from typing import Any, Optional + +from .._generated import calls +from .._generated import storage as st +from ..sp_core import ss58_decode, ss58_encode +from . import multisig_helpers as ms_helpers + +DEFAULT_UPGRADE_REPO = "RaoFoundation/subtensor" + +SUDO_PROXY_TYPE = "SudoUncheckedSetCode" + +# Weight literals of the CI deployment pipeline (.github/scripts/deploy). +# PROPOSAL_WEIGHT is an argument of the proposed call itself +# (sudoUncheckedWeight) — changing it changes the call hash CI registered. +# FINALIZE_WEIGHT is an argument of the finalizing as_multi call, whose hash +# the sudo multisig operates on, so every triumvirate approval must repeat it +# exactly. Both must stay in lockstep with propose-upgrade-multisig.js / +# approve-upgrade-multisig.js. +PROPOSAL_WEIGHT = {"ref_time": 50_000_000_000, "proof_size": 0} +FINALIZE_WEIGHT = {"ref_time": 60_000_000_000, "proof_size": 10_000} + +_MAX_FETCH_BYTES = 64 * 1024 * 1024 +_FETCH_TIMEOUT = 60.0 + +MANIFEST_ASSET = "upgrade-manifest.json" +CALL_DATA_ASSET = "proxy_proxy_blob.hex" +WASM_ASSET = "subtensor.wasm" +DIGEST_ASSET = "subtensor-digest.json" + + +# --- releases and assets --------------------------------------------------------------- + + +@dataclass(frozen=True) +class ReleaseRef: + """A GitHub release, addressed as ``owner/repo`` + tag.""" + + repo: str + tag: str + + @property + def url(self) -> str: + return f"https://github.com/{self.repo}/releases/tag/{self.tag}" + + def asset_url(self, name: str) -> str: + return f"https://github.com/{self.repo}/releases/download/{self.tag}/{name}" + + +def parse_release_url(url: str) -> ReleaseRef: + """Parse a GitHub release page or asset URL into a :class:`ReleaseRef`. + + Accepts ``https://github.com/O/R/releases/tag/TAG``, + ``https://github.com/O/R/releases/download/TAG/asset``, and the short + ``O/R@TAG`` form. + """ + short = re.fullmatch(r"([\w.-]+/[\w.-]+)@([\w.-]+)", url.strip()) + if short: + return ReleaseRef(repo=short.group(1), tag=short.group(2)) + match = re.match( + r"https?://github\.com/([\w.-]+/[\w.-]+)/releases/(?:tag|download)/([^/?#]+)", + url.strip(), + ) + if not match: + raise ValueError( + f"cannot parse {url!r} as a GitHub release URL " + "(expected https://github.com/OWNER/REPO/releases/tag/TAG)" + ) + return ReleaseRef(repo=match.group(1), tag=match.group(2)) + + +def _github_headers(api: bool = False) -> dict[str, str]: + headers = {"User-Agent": "btcli-upgrade"} + if api: + headers["Accept"] = "application/vnd.github+json" + token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" + return headers + + +def fetch_bytes(url: str, *, api: bool = False) -> bytes: + """GET a URL (following redirects), capped at ``_MAX_FETCH_BYTES``.""" + request = urllib.request.Request(url, headers=_github_headers(api=api)) + try: + with urllib.request.urlopen(request, timeout=_FETCH_TIMEOUT) as response: + data = response.read(_MAX_FETCH_BYTES + 1) + except urllib.error.HTTPError as error: + raise ValueError(f"fetching {url} failed: HTTP {error.code}") from error + except (urllib.error.URLError, OSError, TimeoutError) as error: + raise ValueError(f"fetching {url} failed: {error}") from error + if len(data) > _MAX_FETCH_BYTES: + raise ValueError(f"{url} exceeds the {_MAX_FETCH_BYTES // (1 << 20)} MiB fetch cap") + return data + + +def fetch_json(url: str, *, api: bool = False) -> Any: + return json.loads(fetch_bytes(url, api=api).decode("utf-8")) + + +def parse_hex_blob(raw: bytes | str) -> bytes: + """Decode a call-data hex payload (``proxy_proxy_blob.hex`` contents).""" + text = raw.decode("ascii") if isinstance(raw, bytes) else raw + text = text.strip().removeprefix("0x") + if not text or not re.fullmatch(r"[0-9a-fA-F]+", text) or len(text) % 2: + raise ValueError("call data is not a valid hex string") + return bytes.fromhex(text) + + +def call_hash_hex(blob: bytes) -> str: + """0x-hex blake2_256 of raw call bytes — the multisig pallet's call hash.""" + return "0x" + blake2b(blob, digest_size=32).hexdigest() + + +@dataclass +class ProposalBundle: + """The verification inputs for one proposal, however they were sourced.""" + + blob: bytes + release: Optional[ReleaseRef] = None + manifest: Optional[dict] = None + wasm: Optional[bytes] = None + digest: Optional[dict] = None + # "local" when --wasm supplied the runtime (the trust anchor), "release" + # when it was downloaded alongside the call data it is checked against. + wasm_source: Optional[str] = None + notes: list[str] = field(default_factory=list) + + @property + def call_hash(self) -> str: + return call_hash_hex(self.blob) + + +def fetch_proposal_bundle( + *, + release_url: Optional[str] = None, + hex_url: Optional[str] = None, + hex_file: Optional[str] = None, + wasm_path: Optional[str] = None, +) -> ProposalBundle: + """Assemble the call data + runtime + manifest from a release URL or raw parts.""" + sources = [s for s in (release_url, hex_url, hex_file) if s] + if len(sources) != 1: + raise ValueError("pass exactly one of --url, --hex-url, or --hex-file") + + release: Optional[ReleaseRef] = None + manifest: Optional[dict] = None + digest: Optional[dict] = None + notes: list[str] = [] + + if release_url: + release = parse_release_url(release_url) + blob = parse_hex_blob(fetch_bytes(release.asset_url(CALL_DATA_ASSET))) + try: + manifest = fetch_json(release.asset_url(MANIFEST_ASSET)) + except ValueError: + notes.append("release has no upgrade-manifest.json asset (older release train)") + try: + digest = fetch_json(release.asset_url(DIGEST_ASSET)) + except ValueError: + notes.append("release has no subtensor-digest.json asset") + elif hex_url: + blob = parse_hex_blob(fetch_bytes(hex_url)) + else: + assert hex_file is not None + with open(hex_file, "rb") as handle: + blob = parse_hex_blob(handle.read()) + + wasm: Optional[bytes] = None + wasm_source: Optional[str] = None + if wasm_path: + with open(wasm_path, "rb") as handle: + wasm = handle.read() + wasm_source = "local" + elif release: + try: + wasm = fetch_bytes(release.asset_url(WASM_ASSET)) + wasm_source = "release" + except ValueError: + notes.append("release has no subtensor.wasm asset") + + return ProposalBundle( + blob=blob, + release=release, + manifest=manifest, + wasm=wasm, + digest=digest, + wasm_source=wasm_source, + notes=notes, + ) + + +def fetch_release_manifests(repo: str, *, limit: int = 15) -> list[dict]: + """Upgrade manifests attached to the repo's recent releases. + + Each manifest gains ``release_url`` and ``prerelease``. Network failures + yield an empty list — manifest enrichment is always advisory. + """ + try: + releases = fetch_json( + f"https://api.github.com/repos/{repo}/releases?per_page={limit}", api=True + ) + except ValueError: + return [] + manifests: list[dict] = [] + for release in releases if isinstance(releases, list) else []: + assets = {a.get("name"): a for a in release.get("assets") or []} + if MANIFEST_ASSET not in assets: + continue + try: + manifest = fetch_json(assets[MANIFEST_ASSET]["browser_download_url"]) + except ValueError: + continue + manifest["release_url"] = release.get("html_url") + manifest["prerelease"] = bool(release.get("prerelease")) + manifests.append(manifest) + return manifests + + +def find_manifest_for_call_hash(manifests: list[dict], call_hash: str) -> Optional[dict]: + """The manifest whose ``call_hash`` matches, if any.""" + wanted = call_hash.lower() + for manifest in manifests: + if str(manifest.get("call_hash", "")).lower() == wanted: + return manifest + return None + + +# --- structural verification (offline port of test-wasm.py) ---------------------------- + +# The blob is the bare SCALE encoding of +# proxy.proxy(real=sudo_key, force_proxy_type=None, +# sudo.sudoUncheckedWeight(system.setCode(wasm), PROPOSAL_WEIGHT)) +# as built by propose-upgrade-multisig.js. Pallet/call indices come from the +# runtime (System=0, Sudo=12, Proxy=16); if they change, this parse fails +# loudly and must be updated in lockstep with test-wasm.py. The parse is only +# used to *extract* the embedded runtime and proxied account — byte-exactness +# is then proven by re-encoding against live chain metadata (see +# ``reconstruct_proxy_call``), which does not depend on these constants. +_PROXY_PROXY = bytes([16, 0]) +_MULTIADDRESS_ID = bytes([0]) +_OPTION_NONE = bytes([0]) +_SUDO_SUDO_UNCHECKED_WEIGHT = bytes([12, 1]) +_SYSTEM_SET_CODE = bytes([0, 2]) + + +def _compact_encode(n: int) -> bytes: + """SCALE compact encoding of a non-negative integer.""" + if n < 1 << 6: + return bytes([n << 2]) + if n < 1 << 14: + return ((n << 2) | 0b01).to_bytes(2, "little") + if n < 1 << 30: + return ((n << 2) | 0b10).to_bytes(4, "little") + data = n.to_bytes((n.bit_length() + 7) // 8, "little") + return bytes([((len(data) - 4) << 2) | 0b11]) + data + + +def _compact_decode(data: bytes) -> tuple[int, int]: + """Decode a SCALE compact integer; returns (value, bytes consumed).""" + if not data: + raise ValueError("empty compact integer") + mode = data[0] & 0b11 + if mode == 0b00: + return data[0] >> 2, 1 + if mode == 0b01: + return int.from_bytes(data[:2], "little") >> 2, 2 + if mode == 0b10: + return int.from_bytes(data[:4], "little") >> 2, 4 + length = (data[0] >> 2) + 4 + return int.from_bytes(data[1 : 1 + length], "little"), 1 + length + + +@dataclass +class ParsedBlob: + """The two variable parts of a well-formed proposal blob.""" + + real_ss58: str + code: bytes + + +def parse_proposal_blob(blob: bytes, *, ss58_format: int = 42) -> ParsedBlob: + """Parse a proposal blob, requiring the exact CI call shape around the wasm. + + Raises ``ValueError`` when the blob is anything other than + ``proxy.proxy(MultiAddress::Id(real), None, + sudoUncheckedWeight(setCode(code), PROPOSAL_WEIGHT))`` — a batch or any + extra call wrapped around honest wasm bytes fails here. + """ + head = _PROXY_PROXY + _MULTIADDRESS_ID + if blob[: len(head)] != head: + raise ValueError("call data is not proxy.proxy(MultiAddress::Id(...), ...)") + real = blob[len(head) : len(head) + 32] + if len(real) != 32: + raise ValueError("call data is truncated inside the proxied account") + rest = blob[len(head) + 32 :] + inner_head = _OPTION_NONE + _SUDO_SUDO_UNCHECKED_WEIGHT + _SYSTEM_SET_CODE + if rest[: len(inner_head)] != inner_head: + raise ValueError( + "call data after the proxied account is not sudoUncheckedWeight(setCode(...), ...)" + ) + rest = rest[len(inner_head) :] + code_len, consumed = _compact_decode(rest) + code = rest[consumed : consumed + code_len] + if len(code) != code_len: + raise ValueError("call data is truncated inside the runtime bytes") + weight = _compact_encode(PROPOSAL_WEIGHT["ref_time"]) + _compact_encode( + PROPOSAL_WEIGHT["proof_size"] + ) + tail = rest[consumed + code_len :] + if tail != weight: + raise ValueError( + "call data does not end with exactly the pinned sudoUncheckedWeight " + f"weight {PROPOSAL_WEIGHT} (or carries trailing bytes)" + ) + return ParsedBlob(real_ss58=ss58_encode(real, ss58_format), code=code) + + +async def reconstruct_proxy_call(client, *, wasm: bytes, sudo_key: str): + """Compose the proposal call from first principles against live metadata. + + Returns the composed call (``.data`` bytes, ``.call_hash``). Byte-equality + of ``.data`` with a proposal blob proves the blob dispatches exactly + ``setCode(wasm)`` on ``sudo_key`` and nothing else. + """ + set_code = await client.compose(calls.System.set_code(code="0x" + wasm.hex())) + unchecked = await client.compose( + calls.Sudo.sudo_unchecked_weight(call=set_code, weight=PROPOSAL_WEIGHT) + ) + return await client.compose( + calls.Proxy.proxy(real=sudo_key, force_proxy_type=None, call=unchecked) + ) + + +# --- on-chain discovery ----------------------------------------------------------------- + + +async def discover_pending_upgrades(client) -> list[dict[str, Any]]: + """Pending runtime-upgrade proposals, from chain state alone. + + Walks sudo.key() -> its ``SudoUncheckedSetCode`` proxies -> pending + multisig ops on each delegate, keeping ops whose depositor + sudo key + re-derive the delegate as a 2-of-2 multisig (the CI deployment pattern). + """ + sudo_key = str(await client.query(st.Sudo.Key)) + value = await client.query(st.Proxy.Proxies, [sudo_key]) + delegations = (value or ([], 0))[0] + upgrades: list[dict[str, Any]] = [] + for delegation in delegations: + if str(delegation.get("proxy_type")) != SUDO_PROXY_TYPE: + continue + if int(delegation.get("delay") or 0) != 0: + continue + delegate = str(delegation.get("delegate")) + for op in await ms_helpers.list_pending_multisig_ops(client, delegate): + depositor = str(op.get("depositor")) + try: + derived = await client.multisig([depositor, sudo_key], 2) + except Exception: + continue + if derived.address != delegate: + continue # not the CI + sudo deployment pattern + upgrades.append( + { + "kind": "runtime-upgrade-proposal", + "call_hash": op["call_hash"], + "timepoint": op["timepoint"], + "timepoint_display": op["timepoint_display"], + "sudo_key": sudo_key, + "deployment_multisig": delegate, + "ci_address": depositor, + "deployment_approvals": list(op.get("approvals") or []), + } + ) + return upgrades + + +async def find_pending_upgrade(client, call_hash: str) -> Optional[dict[str, Any]]: + """The discovered pending upgrade matching ``call_hash``, if any.""" + wanted = call_hash.lower() + for upgrade in await discover_pending_upgrades(client): + if str(upgrade["call_hash"]).lower() == wanted: + return upgrade + return None + + +# --- the finalizing call and the sudo-multisig layer ------------------------------------- + + +async def compose_finalizing_call( + client, + *, + blob: bytes, + ci_address: str, + deploy_timepoint: dict[str, int], +): + """The deployment multisig's finalizing call — what the sudo multisig signs. + + ``Multisig.as_multi(2, [ci], deploy_timepoint, , + FINALIZE_WEIGHT)``: submitted by the sudo multisig account, it lands the + second (executing) approval of CI's proposal. Its encoding must be + byte-identical across all triumvirate signers, which is why the weight is + a pinned constant and the call bytes are spliced straight from the shared + blob (the call encoder accepts pre-composed calls as raw SCALE bytes). + """ + return await client.compose( + calls.Multisig.as_multi( + threshold=2, + other_signatories=[ci_address], + maybe_timepoint={ + "height": int(deploy_timepoint["height"]), + "index": int(deploy_timepoint["index"]), + }, + call=bytes(blob), + max_weight=FINALIZE_WEIGHT, + ) + ) + + +def sorted_other_signatories(signatories: list[str], self_address: str) -> list[str]: + """Everyone but ``self_address``, sorted by raw account id (chain order).""" + others = [s for s in signatories if s != self_address] + return sorted(set(others), key=lambda s: bytes(ss58_decode(s))) + + +async def sudo_layer_status( + client, + *, + sudo_key: str, + finalizing_call_hash: str, +) -> Optional[dict[str, Any]]: + """The sudo multisig's pending op for the finalizing call, if opened.""" + pending = await client.query(st.Multisig.Multisigs, [sudo_key, finalizing_call_hash]) + if not pending: + return None + when = pending.get("when") or {} + return { + "call_hash": finalizing_call_hash, + "timepoint": {"height": int(when.get("height", 0)), "index": int(when.get("index", 0))}, + "approvals": [str(a) for a in pending.get("approvals") or []], + "depositor": str(pending.get("depositor")), + } + + +def sign_command(network: str, release_url: str, wallet: str = "") -> str: + """The copy-paste command for a co-signer.""" + prefix = "btcli" if network == "finney" else f"btcli -n {network}" + return f"{prefix} upgrade sign --url {release_url} -w {wallet}" + + +def check_command(network: str, release_url: str) -> str: + prefix = "btcli" if network == "finney" else f"btcli -n {network}" + return f"{prefix} upgrade check --url {release_url} --wasm " + + +def srtool_recipe(manifest: Optional[dict]) -> list[str]: + """The reproduce-from-source recipe, tailored by the manifest when present.""" + tag = (manifest or {}).get("tag") or "" + repo = (manifest or {}).get("repo") or DEFAULT_UPGRADE_REPO + rustc = (manifest or {}).get("srtool_rustc") + image = f"paritytech/srtool:{rustc}" if rustc else "paritytech/srtool:" + return [ + f"git clone https://github.com/{repo} && cd {repo.split('/', 1)[1]}", + f"git checkout {tag}", + "ln -s . runtime/node-subtensor", + "docker run --rm --user root --platform=linux/amd64 \\", + " -e PACKAGE=node-subtensor-runtime \\", + ' -e BUILD_OPTS="--features=metadata-hash" \\', + " -e PROFILE=production \\", + ' -v "$(pwd)":/build \\', + f" {image} /srtool/build --app", + ] + + +# --- verification runner ------------------------------------------------------------------ + + +@dataclass +class Check: + name: str + ok: Optional[bool] # None = skipped / not applicable + detail: str + + +@dataclass +class CheckOutcome: + checks: list[Check] + data: dict[str, Any] + + @property + def ok(self) -> bool: + return all(c.ok is not False for c in self.checks) + + def failed(self) -> list[Check]: + return [c for c in self.checks if c.ok is False] + + +async def run_proposal_checks(client, bundle: ProposalBundle) -> CheckOutcome: + """Run every offline + on-chain check for a proposal bundle. + + The checks, in order: + + 1. structure — the blob parses as exactly the CI proposal call shape; + 2. reconstruction — re-encoding ``proxy.proxy(real, None, + sudoUncheckedWeight(setCode(code), W))`` against live chain metadata + reproduces the blob byte-for-byte; + 3. runtime match — the embedded runtime equals the provided wasm + (local srtool build or release asset); + 4. digest — sha256 of the embedded runtime matches subtensor-digest.json; + 5. sudo key — the proxied account is the chain's live sudo.key(); + 6. on-chain proposal — a pending deployment-multisig op exists whose call + hash is blake2_256(blob), depositor rederives the delegate; + 7. manifest cross-checks, when a manifest is present. + """ + checks: list[Check] = [] + data: dict[str, Any] = {"call_hash": bundle.call_hash} + + sudo_key = str(await client.query(st.Sudo.Key)) + data["sudo_key"] = sudo_key + + # 1. structure + parsed: Optional[ParsedBlob] = None + try: + parsed = parse_proposal_blob(bundle.blob) + checks.append( + Check( + "call structure", + True, + "exactly proxy.proxy(real, None, sudoUncheckedWeight(setCode(code), " + f"{{ref_time: {PROPOSAL_WEIGHT['ref_time']}, proof_size: " + f"{PROPOSAL_WEIGHT['proof_size']}}})); runtime is " + f"{len(parsed.code)} bytes", + ) + ) + except ValueError as error: + checks.append(Check("call structure", False, str(error))) + + if parsed is not None: + data["proxied_account"] = parsed.real_ss58 + data["code_sha256"] = "0x" + sha256(parsed.code).hexdigest() + + # 2. reconstruction against live metadata + reconstructed = await reconstruct_proxy_call( + client, wasm=parsed.code, sudo_key=parsed.real_ss58 + ) + if bytes(reconstructed.data) == bundle.blob: + checks.append( + Check( + "re-encoding", + True, + "composing the same call against live chain metadata reproduces " + "the call data byte-for-byte", + ) + ) + else: + checks.append( + Check( + "re-encoding", + False, + "re-encoded call differs from the call data " + f"({len(bytes(reconstructed.data))} vs {len(bundle.blob)} bytes) — " + "runtime metadata may disagree with the offline parse", + ) + ) + + # 3. runtime match (the trust anchor when --wasm is a local srtool build) + if bundle.wasm is not None: + if parsed.code == bundle.wasm: + source = ( + "your local build" + if bundle.wasm_source == "local" + else "the release's subtensor.wasm" + ) + checks.append( + Check("runtime match", True, f"embedded runtime is byte-identical to {source}") + ) + else: + checks.append( + Check( + "runtime match", + False, + "embedded runtime differs from the provided wasm " + f"(sha256 {sha256(bundle.wasm).hexdigest()} vs " + f"{sha256(parsed.code).hexdigest()})", + ) + ) + else: + checks.append( + Check( + "runtime match", + None, + "no wasm provided — build from source with srtool and pass --wasm " + "to pin the call data to code you compiled yourself", + ) + ) + + # 4. digest + if bundle.digest is not None: + expected = str(bundle.digest.get("sha256", "")).removeprefix("0x").lower() + actual = sha256(parsed.code).hexdigest() + checks.append( + Check( + "srtool digest", + actual == expected, + f"sha256 {'matches' if actual == expected else 'differs from'} " + "subtensor-digest.json", + ) + ) + + # 5. sudo key + checks.append( + Check( + "sudo key", + parsed.real_ss58 == sudo_key, + f"proxied account {parsed.real_ss58} " + f"{'is' if parsed.real_ss58 == sudo_key else 'IS NOT'} " + f"the chain's sudo key {sudo_key}", + ) + ) + + # 6. on-chain proposal + pending = await find_pending_upgrade(client, bundle.call_hash) + if pending is not None: + data["pending"] = pending + checks.append( + Check( + "on-chain proposal", + True, + f"pending deployment-multisig op matches blake2_256(call data) " + f"{bundle.call_hash} (proposed at {pending['timepoint_display']} " + f"by {pending['ci_address']})", + ) + ) + else: + checks.append( + Check( + "on-chain proposal", + False, + f"no pending deployment-multisig op has call hash {bundle.call_hash} " + "(already executed, cancelled, or not proposed on this network)", + ) + ) + + # 7. manifest cross-checks + manifest = bundle.manifest + if manifest: + manifest_hash = str(manifest.get("call_hash", "")).lower() + checks.append( + Check( + "manifest call hash", + manifest_hash == bundle.call_hash.lower(), + "upgrade-manifest.json call_hash " + f"{'matches' if manifest_hash == bundle.call_hash.lower() else 'differs from'} " + "the call data", + ) + ) + if pending is not None and manifest.get("ci_address"): + ci_matches = str(manifest["ci_address"]) == pending["ci_address"] + checks.append( + Check( + "manifest CI key", + ci_matches, + "manifest ci_address " + f"{'matches' if ci_matches else 'differs from'} the on-chain depositor", + ) + ) + data["manifest"] = { + key: manifest.get(key) + for key in ("spec_version", "commit", "tag", "repo", "wasm_sha256") + } + + return CheckOutcome(checks=checks, data=data) From 5bb95ed7def82699bfebe394fb21eb1dededd687 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 09:50:14 -0600 Subject: [PATCH 43/68] test: poll NextKey in mev shield e2e instead of single startup read Co-authored-by: Cursor --- sdk/bittensor-core/tests/e2e.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/sdk/bittensor-core/tests/e2e.rs b/sdk/bittensor-core/tests/e2e.rs index ec822e09a8..25e913c624 100644 --- a/sdk/bittensor-core/tests/e2e.rs +++ b/sdk/bittensor-core/tests/e2e.rs @@ -9,6 +9,8 @@ mod e2e_support; use std::collections::BTreeSet; +use std::thread; +use std::time::{Duration, Instant}; use bittensor_core::client::{as_str, as_u128, field, value_bytes, variant_name}; use bittensor_core::codec::Value; @@ -262,11 +264,26 @@ fn test_policy_aggregates_spend_across_batch() { #[test] fn test_mev_shield_next_key_is_mlkem768() { let ctx = TestContext::new(); - let key = ctx - .client - .query("MevShield", "NextKey", &[], None) - .expect("NextKey read"); - assert_eq!(value_bytes(&key).expect("NextKey bytes").len(), 1_184); + // NextKey is None until every localnet authority has authored a block and + // announced its key (the rotation inherent kills NextKey whenever the + // next-next author has no AuthorKeys entry yet), so poll instead of + // reading once right after startup. + let deadline = Instant::now() + Duration::from_secs(30); + let key = loop { + let key = ctx + .client + .query("MevShield", "NextKey", &[], None) + .expect("NextKey read"); + if let Some(bytes) = value_bytes(&key) { + break bytes; + } + assert!( + Instant::now() < deadline, + "NextKey not announced within 30s" + ); + thread::sleep(Duration::from_millis(250)); + }; + assert_eq!(key.len(), 1_184); } #[test] From 3cd53e813378b8b1de9d4057439235a194839f56 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 09:51:27 -0600 Subject: [PATCH 44/68] chore: bump spec_version to 430; add devnet endpoint; dynamic SDK version - Bump runtime spec_version 429 -> 430 for the next mainnet upgrade. - Add the devnet network endpoint to the Python SDK and network docs. - Read the Python SDK __version__ from installed package metadata so wheels report the version they were published as. Co-authored-by: Cursor --- docs/concepts/network.mdx | 1 + runtime/src/lib.rs | 2 +- sdk/python/bittensor/__init__.py | 9 ++++++++- sdk/python/bittensor/settings.py | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/concepts/network.mdx b/docs/concepts/network.mdx index 0134d04b76..191b346add 100644 --- a/docs/concepts/network.mdx +++ b/docs/concepts/network.mdx @@ -20,6 +20,7 @@ The SDK's network names resolve to public endpoints: - `finney` — mainnet, `wss://entrypoint-finney.opentensor.ai:443` - `test` — public testnet, `wss://test.finney.opentensor.ai:443` +- `devnet` — the development chain, `wss://dev.chain.opentensor.ai:443` - `local` — a dev node at `ws://127.0.0.1:9944` Mainnet also has a public **lite** node at diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 017a71a855..399d60658c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -235,7 +235,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 429, + spec_version: 430, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/sdk/python/bittensor/__init__.py b/sdk/python/bittensor/__init__.py index 67bf9138b4..c7f7c81788 100644 --- a/sdk/python/bittensor/__init__.py +++ b/sdk/python/bittensor/__init__.py @@ -31,6 +31,7 @@ async def main(): logging.getLogger("bittensor").setLevel(logging.DEBUG) # just the SDK """ +import importlib.metadata as _importlib_metadata import logging as _logging from . import evm, http_auth, intents, reads, timelock, wallets @@ -194,7 +195,13 @@ async def main(): *sorted(_INTENT_EXPORTS), ] -__version__ = "11.0.0.dev0" +# The single source of truth for the version is pyproject.toml (which the +# release workflows stamp with the rc/dev suffix at build time); read it from +# the installed distribution so wheels report what they were published as. +try: + __version__ = _importlib_metadata.version("bittensor") +except _importlib_metadata.PackageNotFoundError: # uninstalled source tree + __version__ = "0.0.0.dev0+unknown" # Removed v10 API names raise with a pointer to the replacement instead of a # bare AttributeError, so an un-migrated script fails with instructions diff --git a/sdk/python/bittensor/settings.py b/sdk/python/bittensor/settings.py index cf918d9074..090182a08d 100644 --- a/sdk/python/bittensor/settings.py +++ b/sdk/python/bittensor/settings.py @@ -34,6 +34,7 @@ "finney": "wss://entrypoint-finney.opentensor.ai:443", "test": "wss://test.finney.opentensor.ai:443", "archive": "wss://archive.chain.opentensor.ai:443", + "devnet": "wss://dev.chain.opentensor.ai:443", "local": os.getenv("BT_CHAIN_ENDPOINT") or "ws://127.0.0.1:9944", } From 59ae9224834da0bfde4d4137d22fe43355a33a82 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 10:55:28 -0600 Subject: [PATCH 45/68] ci: print CI key address and precheck balance in mainnet proposal Co-authored-by: Cursor --- .../deploy/propose-upgrade-multisig.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/scripts/deploy/propose-upgrade-multisig.js b/.github/scripts/deploy/propose-upgrade-multisig.js index a6fd8932ff..3787da5513 100644 --- a/.github/scripts/deploy/propose-upgrade-multisig.js +++ b/.github/scripts/deploy/propose-upgrade-multisig.js @@ -52,6 +52,7 @@ async function main() { const keyring = new Keyring({ type: "sr25519" }); const pair = keyring.addFromUri(seedPhrase); // CI's signing key const ciKeyAddress = pair.address; + console.log(`CI key address (fee payer + multisig depositor): ${ciKeyAddress}`); // Grab the sudo key from the chain const sudoKey = (await api.query.sudo.key()).toString(); @@ -81,6 +82,24 @@ async function main() { ); } + // The CI key pays the extrinsic fee and the multisig deposit + // (depositBase + threshold * depositFactor), so an unfunded key fails with + // an opaque 1010 at submission time. Check up front and name the account. + const account = await api.query.system.account(ciKeyAddress); + const free = account.data.free.toBigInt(); + const depositBase = api.consts.multisig.depositBase.toBigInt(); + const depositFactor = api.consts.multisig.depositFactor.toBigInt(); + const required = depositBase + 2n * depositFactor; + console.log( + `CI key free balance: ${free} rao; multisig deposit needed: ${required} rao (+ fee)` + ); + if (free < required) { + throw Error( + `CI key ${ciKeyAddress} has ${free} rao free but needs at least ` + + `${required} rao for the multisig deposit plus fees — fund it and re-run` + ); + } + // Read the WASM file and convert it to hex string const wasm = fs.readFileSync(wasmPath).toString("hex"); console.log(`WASM file size (hex): ${wasm.length / 2} bytes`); From f9a06f653ab9ee028260b9489ec5036847f3c5f6 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:01:44 -0600 Subject: [PATCH 46/68] feat: add v430 release announcement page at /releases/v430-upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit About-style page covering the pending spec 430 upgrade: conviction subnet ownership, price-driven emissions, bittensor v11, Ledger and extension signing, the sign-and-verify upgrade flow, and required actions — all linking into the docs. Includes an OG share thumbnail and a production toggle on the Deploy Docs workflow so the website can ship without waiting for the chain upgrade. Co-authored-by: Cursor --- .github/workflows/deploy-docs.yml | 27 +- .../public/images/og_thumbs/v430-upgrade.png | Bin 0 -> 365963 bytes .../releases/v430-upgrade/page.module.css | 105 ++++++ .../releases/v430-upgrade/page.tsx | 307 ++++++++++++++++++ .../apps/bittensor-website/src/app/sitemap.ts | 1 + 5 files changed, 434 insertions(+), 6 deletions(-) create mode 100644 website/apps/bittensor-website/public/images/og_thumbs/v430-upgrade.png create mode 100644 website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css create mode 100644 website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index d1d8834c4f..87a095e199 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -11,6 +11,11 @@ on: - "docs/**" - "website/**" workflow_dispatch: + inputs: + production: + description: "Deploy to production (bittensor.com) instead of staging" + type: boolean + default: false concurrency: group: deploy-docs @@ -20,6 +25,9 @@ jobs: deploy: name: Deploy staging docs to Vercel runs-on: [self-hosted, fireactions-turbo-8] + # Production deploys are gated behind the same environment approval as the + # post-upgrade promotion in watch-mainnet-release.yml. + environment: ${{ inputs.production && 'mainnet' || null }} env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} @@ -43,9 +51,16 @@ jobs: # CLI resolves it against the cwd (running from website/ doubles the # path and fails with ENOENT). run: | - npx --yes vercel pull --yes --environment=preview --token "$VERCEL_TOKEN" - npx --yes vercel build --token "$VERCEL_TOKEN" - url=$(npx --yes vercel deploy --prebuilt --token "$VERCEL_TOKEN") - echo "Deployed: $url" - npx --yes vercel alias set "$url" "staging.$DOCS_DOMAIN" --token "$VERCEL_TOKEN" - echo "Aliased to https://staging.$DOCS_DOMAIN" >> $GITHUB_STEP_SUMMARY + if [ "${{ inputs.production }}" = "true" ]; then + npx --yes vercel pull --yes --environment=production --token "$VERCEL_TOKEN" + npx --yes vercel build --prod --token "$VERCEL_TOKEN" + npx --yes vercel deploy --prebuilt --prod --token "$VERCEL_TOKEN" + echo "Deployed to https://$DOCS_DOMAIN (production)" >> $GITHUB_STEP_SUMMARY + else + npx --yes vercel pull --yes --environment=preview --token "$VERCEL_TOKEN" + npx --yes vercel build --token "$VERCEL_TOKEN" + url=$(npx --yes vercel deploy --prebuilt --token "$VERCEL_TOKEN") + echo "Deployed: $url" + npx --yes vercel alias set "$url" "staging.$DOCS_DOMAIN" --token "$VERCEL_TOKEN" + echo "Aliased to https://staging.$DOCS_DOMAIN" >> $GITHUB_STEP_SUMMARY + fi diff --git a/website/apps/bittensor-website/public/images/og_thumbs/v430-upgrade.png b/website/apps/bittensor-website/public/images/og_thumbs/v430-upgrade.png new file mode 100644 index 0000000000000000000000000000000000000000..5770ca85b793b5c8650aa2e863192dfb91e23a3d GIT binary patch literal 365963 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�M;Ts)U`9#&d&+8P1x6 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�a?9R zLUqYus4h1e*rN8h5H18EMwTFjDhN&E(NSL3t~we*Qr^IduWQ>X-gTWZCm4ZhampNFz8AX5oD@Z=kCJ z!vfkpY;9Xn>a)Q|7Z#TKHsIkLnTi zpuAP%X+u*s#l!()QU!z2*OBnLk_D^Qny%xRx zcYKVcphy#X=luNa;{4*p*$Y(A9zI2gPw(KqO92)(xd;~pgic{3(1r4fB67m4rsS(Z zQxny39|b%ur76OjOXcB4EIyA8xT3qcxyF61c$XH=#@+4VoE=tp*rWFb``zW`6?hH~ zj!}xfn(N7%Yu*NnlYPAGiFbo>y;@F1lhVzk5TRy3lJ;29jHQ^IidLOcOSICO*r7f{ z?KWAuiQ0UkcfKp=hG6xPIfC>Gnv@gNg`Wnw7w4hzd3Tz&;fS!Kw#hJ)m^IyqJdFV} zgei$T%csRx%`wv8sY-3b6~gTB@D@AIlS|IZ(pxENayp@@B$Rp^3if7^5~H;fZhOEc zTEy{;nrS%Hnmgiw?!i*5x!5I-)Uy*kfY44^hTE^f(S%*L2pK*Hg zoQyw?2Gu$K8A(nuZgF2IpPb<&V)yQbCzSC5t6Q9MJ~}!$JA3}wXP@8=d^p9-@KHbe zSpLVud@>UeeNw=(k@-f`_$D!PLE|$JoQ!jB#v%{FCcZM@w^`a$=P`R2JipUYhs8Go z1FqSus3*}Q-oxW{shydW_t99JL@V_P7Zy*>(jz=tG_KB8G}jEZTKg z3JSeCvtb3eIfU0@B~D%4fYC^5B1E&s6QPLvrcK7)B7zVv~afuZo=toC~FzNE7tw}b%<>8afgVjoU__O+tGQu; zl!|!4&_rX=h|&jJ6xpRl2W*3EBH7rg_WWGO9kS_2@n& z0?WFs-LOXnevw^yynNrka_|QZ=xZ%D;?U@eZ0*Rp>!sGX45it$#sM`rlg(p14wK@A z4dZB$xsNMy+n#faLtgL0%}IO#J72XIPkQr&^v&bPPtlLzMkP#*_VK=gJ)RAhLlE~> z!IiuHeU5E753Lqp-hpC$T75yA)_;JT`jB<|r0Lr3VnvBI2H;~QWWH=)P+$wDWc*3HF>$y88C zjAk7O`6fXMeuq6=;+Tr0sw*VD4XojzNEBExt7sl1mbCW;XVav%8IC@w=vS`#avNe002jaQMl`pWu?(#RVS2vzH-iU#Oj* z;z;Kw%NH*7@P}vktU+^`!ZIHCp%;{XvoJxj0+f0P6_8sART67a!NCc;yKB(*cMqsY zPSxSI{=Eat**Jk?7k;;YaKJlVuU=eIIo_hmC82t!*2x_p?4(X z_*xc(y2f3y6DmM7yV|;wK0EZ^O=MFl{po5V-c&S6EP|lc6V8`z#aeR7DN5fAL?Ydf{elZGc!cmEp+zxcUD8uaiNCytj{=UN0{%u}VaDeSCBTHJ4X>E0`J}V2vLi z|5+hzV9h6WYWHljlq@VbOG^*)aBQ`g14cQF_P~zv4C@rJiQ-Y{2UWOMN96Dc$MRgS zM+|GWOe2m6&b6VRe=Q5be)U_E-4Zm3Naf&7jKL|n;_hAA97xM0|dEcpQc5|rqhv+BC1bi*lePkxBnDz++ zP2&BkJ9j+wMkL|GwLw`Bstfx^I1h&xjiO8D>qniVG){0d0>N3J2~efHjp63>ZBoWB)@R-{Ly z`VAA1e)htIl&$N+Kr1_~B#CQku)rO9UdAJ@B%~f3#$dvM2TL%|z?1@Wh|4QJ6@$cI z|MlPe+yDOG{x84tKY#Sm_uqZ*-7kLd#ee;8e(TLQKKJ8ifloNMhp~FN~W2cimUG)s)kW@s=_!$VXc(q{l<9A5o9Y$wwde2_)Cs{{Rz*f z8tRq@^$J#+ZdzMtu!uBkP3yN1Sw3q@n@~Mw*Mf!AxQbeQVM?KQ2xlGk7-pvpW}69u z(v@yZ_&J z|I>#bp1n9bJY?sFE{=~B$|4OSd-8!moE%fzV76J##hh%sM^iTDGW)bIP_Fb#L5gbI z$4Zaa2+`H@lM?&<7zW;X13V0?=&qp-H+SF$*ROo}OaJtr|FiG>lke>9aD8}ndkOAG zcnJ;e{omC`8SYLlf0pYk)cQ;OM8|ywTj6Ge4JEKxV)H~K8E8Om9wF=C04=? z1h8Q11aB#vpqZi}FF+sBZsJ2~CK*b>fq;~3E6V z6i{BvkVpr;X~}Gt=R{tO@_H9gS9{hISn{MxV>L!T5ElV2vbD4^EL1fJ2+VU~!D=Z>hS20WBBmB2s_}X6ajTose(sf}wnz`}X$e5GL*7I}_*U z=Rf|@kN@mX|NQTN{F8t5kN?rT@4mY)ufW00qPVRemo{k}-bS;}`@gW1v`8X*zgupJ zwlP=~LQ&Wz%*JDdj1RUP%1Xx)buCD!YZ0?Y6SWPsNEZb**EV!X4l`*SRt`I?a9t&H zE7bTm?mJq=vQ5H_Usp6oaR;>1DL$a=We0+*Ja(LTiA|(omG_}A(uyPjQ(EiS93|#?YJrO4gL`leXl#pA$c-2lJhSlIS_i0JQPYto(ZIa85q)Bk!86BnBOtM; zv0qFLjZ?dRYB-~)W@Sk8#cEGRWM zrbL{^>!eU^o(fKapd`0;WY!u1RlLGqZ*4OIo?%J7OG24>O&lwQkxhoC@m{JBEI+4^RT+YvI@(mH;&%GxvtyYo8#l7_uv1*&;H!c;#}BA-~SLV zBgGO6^9XctT#DVw#|Z(z+bP%+YUii7NQgmPrHDo(8rD|CiV)obCKTX>qF9FV?Kg6j z@&XqzFD{=xePjP%|KjZI!|#9i@BZEY`XBzozyH7f=>PoruYK+FZ@+zXaD-JZd^+T5 zy(6q2(I;cMi;&~8qC|c&+R=jb?voi-cddZI2yz;a8WlM;!U<}Q$wlonprV#=2oyHV z(N*)R&4mp$+bqgF%$2$`%g#w=I5MkFjd`{4oV0T#3}*aF(B^_Pv}?oI%rk`S{%ra< z*Y=uUplZy9leMg;NC+~ip?+HWh|uhsjx>p0*OKWW!xVNz`$*A}CN;$ibwxIcXv|lC z80YGOmYf&@Cimb3bBv*G57FRd_6A>qjMpi+n+!@E1TB@c^YUtS;^X}anD-nXo!}Rz zqMtr}`fI=TYk&K93AH>NSPIsLUoozSJZFzpOU*uCGsU_r@OX=a=Ih=jqTn^ZOI% zW}obR^{Zd~SO4n2``zFD-QWNH-~aT}Pk-{`pWrb_+|h5{I1Hvnycb8F3jm1ODM~l} zvw-%Iu~-8r3@dh|X=jtzuejMny|^{+S`j>5+D(HGI!#MaAW;pC*_-zO(`LE)V%hbZ zIxjwxhB*d3{4;zlwfdUnogvQ^-^_w=mM<;l&|9{OB8@B>&n&93c)Nc{5kO_FwU!!( zS9D*C#taxMizqMuUIzf3>)^A5a0utZaXs+8_ul)$7e2tE?`vQC+M92_sSKDJ#St3O zDFTEnNc@$}5|Qxo?q4BSUfxPrqvcw^Ywd}|dTU4PSGC4yy;RT|atl1VC!Vyz^)^hE zu*$=?9IygJj~6*OaY7L`yv7qZPO*EGFSY7|kmJ?f&fy*wghWAihYgqVEGE*OGN&_m zl*S?^by^mGQKAh&3DR|vpCTXuYIpY#IXnso626#-wH;5liO}91z9M`4=I7pg_uZet z!-Vh!C-rWztmSK;_s~ttB|#dpBi~L0!M)uk8nxX&&3IWH&h#}{lTa(wwAs+b z%Esf!1ybR%&jP9jF$PoQ0X@+;mBANz5u^*ws+25B)RAwPpGvfJL|wv{!zyK~Q~{#d zVeLuY1X-!l5bBm7gE>DOtmS0%gE!k*Y7#zeYK*@swUWRq3>B9FO(hggcGP-NXE8xXo1X-8H{8aM9oKi^3AoKmXHiLqi}QIn`LrMyrJ{vxY_ z&0pm-%wa(r4dH+7X#?9qxnyMY5o;7nRKc72cM&|m*ZlBhoojYdxE&O$r0Xku$U(}| z5AoC^^k%dby`x+PRK^=!RbuhQycbp^WG^R2f&kNaJ5=@LZ#@%5&-(n%aaVuB0U%vvg2-&O@j zV^S(iaVbkmem#ziD-JXke`|xr(nt)cQ)?B%^E5#{gXw5;TB#TD>6#%l2`i8)tBBh6 z5|gA*zhSG4Y3d}i78pNU*}&mB2*ECnP{pxE68bx-AAv=7q!TEMM$`#%`swFPG6N}# zgSLk@vK*c%*3riTGP)dsW2LPqg+iS{=`m`Sn4nK+Y1=%LD; zz^1_g^QdIqg(WuA9&&X$v(`Q;bZd6X5S(?dB`YdeYA4JjJz%=(^1lO& z`2Z~CRqCgf>XwRld zr8Mi5x@3;KOb%KGf|=B%)DoHiB6E#+q8e8T*K;7NKMPyx#TRhgi#Kg>3}t(0@^65s zCaJSXTf$Te5xK16b&eS08WXUp*--rv)Ku@iYcd$odE}B->=M3V__Sl;iES(mc;zmJ zt;uPAHA@w6vZ!e;q!=^YgCv1-AWimUyW>nmpBk4R5g!-yD#p{=5=In_i+7e9zHfL< zVhgYidyC7_fGjPgJsdd3GHE821#(p|9=3?RhCj?WMGEQII&)#XJO+38e4kEKt7vXizj7pu22HZ{8KP!SqgcamG245SK$; zTO6AFo-=3A*0Qn{%amW?2$Cj>kob0++tra9cUb{UMF-vnSm1uG#Yh^Q;@}OXO-Gu< zLP2r0+?RTBq^Ii6i>CDA3&Mo4=9KoqYgV#_-$o!tpg4ph`JVUaN#w-#^0)t>7Zb4H z5Zx7*qu+5KPEFsl-}kTH;&K0`Y%M4w;D_V|M~ZFzmO*_@@-}s|#E|2zi6A-2-iC1? zi$Fl!Bk0LGOgfFuPF;owp5vg6a;}3y3mV*ZV@q9U1e#T)RhZTOaHTd8)?~8jvVZF;8t|iCG(y#;%DO`V(k@Az2SJ&RI#%WUu6xC)8C5%qB7FqPyO1>62rj+Y5-x zPbGaAjTfD&r8Qydl)ouuBD6s+WETdD6rOVqx4=ezJYh)Zv^Nk%^5PK}Uw*mZ_kA z?Sp1j1s=0$vuCKJ+k{G_B2ccWtqr&^wYWmq?l`n+wP)1dqP;Gx-V2Uns$S1OD?K2t z-E`aQ0>5cq1)KHzUc@*(5h|3fMB`D_zNj#!KkG3I)%_rz(?C{^?6q{J@5u&hFj;kv z+H4}DToZ+bx)osh0HVKt=tr@#0BjUcO zP}&7Olc`6An$*KNQ~4&@i*^guJq;#F`Ff#nE^UJ+r+jj0#P~r0zh!fuQIvoT6A8JE zGZk?%F53W3j>=q+HOwHL95NBwE^JAiWaU6KuEB&NCfPy8j)HFQGOQ`7)!3G)Ty-X| zrUE@{B-RqZH01aY-Ef_{$>6YlwMm<}OQ7-KGl}2;j&hP6|Dht-&DJHKpdh)q4Hh)H z9};mgdYdzLveK6{#kHA9BwwKmhsc9WY6L@}NV5vk3uQ6Poh^`ijCtTZ&46^dDUl(k z#Y$W?KuZ)1GXjzpgn2a-5Jd%dSzkbOWXEBXw;fPTmIeYMJLt9#tZvh@4F0u|eeUZaciqJkr7F)zoQTQC#y@C~zTIMDd8Y6%`s6QPUyQqeV^X z;p|X&Pb@{p*wbJ{1ir&@s}!|fDJdYfAXH}Q(nW-K&dJm`ws4UTXo@eg3Kaq43QhBo zTU8g9G@_Zi7BHRr@@|k|50vxdiymLx1D+D?OBuB?@w6r{Oquqs1wAH}N24Wu|Flp? zwINn(#t~fQZp7=MB7+I4CW6CDf}tq{fgBj(f*@apB`djvMI=E8ZA(sy3>a)a1{E*Q zZX$a3;7DFQt297cPSJk6x>V#Vbm0(rkRCq@MVeKRZYXWR=2Y;gK<+W-f%7y2(&r{) z5;;SlJep7ISQ43Mgd{BpCx!H+rF1go`Af^oxmBywNl5lt(@aZqn0X$_h? z0ML*QwaoLZ9EM2DDlwA1~2qi<6_W4=r(tCEAA3 z{0NjZ3755^!sgglQ;6^QlGdZRc%2OH@8^j-esK_I@bE!G{R|4Tm*q}hx>=nVI zDdpaO^>sMohUt4XiU9UluCevt3xG_9&f|l5VHG1D`94J1#I;hQu%s+*JLMuxNA$+;8V%TKX4++MPj*_KS>CCGmVkL1KHf=AwQs(H4nRphZkoCfKUL)VLBdm-0nkFGvci zA*hbNMxTzNITmtgOTLw2ZP;84M)~dj zC5V7lVXOK}QW-r+!dWJeJuIueRb>V}iRL+h>Yh8{sRaS;AqTN7>6RR~=(*`9fh=U4kTdx&h zboCvTKV_Q_y|vDg;%qQ&CC@-U@oUOKQKCs4fJ*!#$gF;Vo%f5%c8g1dxA?Yk$gX-9 zdXmBg)zWk%IlpRC3OrLZ*_>3+T+sfZKwC++8IT&;EU$|mKE=WqO=b8wvDGVcas)Qx z5YJphIY{yPp!~R_7P2W9Qy@t#-;xlTdg|fDByFfnG9{PstSeB&2o?;uyTi9TcX*XU zw?1vqpA;B!ID{-60Yusi6Zp~)F#|uc zk8<1nHUbs_YcU~lb8~Zbb-llj*1mg?jtw?`zAt)*kAjm`+O(l;6E8yqP;cx?I!x_k z>p)sDOTP=ISVb&bCyc=oHP6GLn30xvIJOO1GI@otl$1d;yo0bP({Z>U-fuPx@i?|n zDe(X^BFUsTHsu*Cb`lQFQTvjR)w#k5yK?kc(gacQX3@(+3^59GvdrqjyCq|lLC1`Y zuc4I`>XgM*O|v`#jkLDJ(ntYR&h|o2)>yUW6vT#ni&RL(xPcV|j9eeQ!F+s`n;w9w zs8y5g(|(1WwLy~%wMcZ<>J%(t4WZ1+Pz$bzLX&e9Hf2lo3ns1YoRcOLlpp`8~zb zR#c!VLz8H-4?H39v}n`V!X5K{#1RGQ;T#3Y%&0kg4D&X`H1=HJ8T$G8*=L`9c5rZn zhY3$lPjTr08`(5xhS-pJs3|n-E1_lX_#RS3teP^Mr@M@G&pG3asl_vru1tdj4Nne-eu8} znGxe~@B3g?Wi1W!M5(GqSaP0qxzp4#PEFXNX{fBAw6?|5S>o z5~FAKGhuO#v1){|Wx0X03fD;MG_SNE`d-rOe5>6RTxP?HgP=y50eQBe%Qy@BW@a16 zGZEIrZgFnl5BQ!$jG?u3gv-jA`L4%8-c*di5<|UE!^ncvzaWHhs?YfoNeXK26*@Yy z3A2NFn#IGVAcgpvxGOPQnNXxAr&sd-v-jrDdL8F|=iPVg`${YX7f~Xqg``NyiX+*U zWt(ywTdvqnO=49lPVx_oCzJd*u5l_gaZ0wwNgR7(j}tqtNQsx!#E}w9;$lgpxJlpw z5FkK+ScrYQ`@Qq|K2LX_KFhmY-~t3C9$cKamuKszpMH8defs2R0P|OWz)~Vb#E9}+ zQd>-fAi-8Fa(2~liCi_a5;ty@w02B23M%CC<#2oZ#S2kPTPlP@Y3<+=r(XeZWmF|0 zUeZ7*;`?C=#5~i(RUQrG+b21J5*d#qUZuP;u{K#{z_u|s#lqkUa+*now)ovCvRp6`!rJkS64E3s6|~BzLd2s92;zx;N_4_CMCl|P63|p3f{)G6NcoT` zQjLWoUXdX}H2gNn)&N#b(x$5FWeQWG$)4<0DU63q4dECD4!UML*Fe8)lnx01WFKIO zvX_yNK9|cs)7oOc-f`&d>+c29UVG4c4-A_`)GjY+xx6ZqyMA9j5uoc0Jkc(LSEFLx zns|UtFb?S|&*>s?uA>TkB^rt7*Ryg`d2Y;CS|dzOnX#wrw|rHwa`;v>%LI^+1U${iZcf(N z-`77gH>c?qJDF(@{R8uJv*tfTvTIvJe zayOVfVJ-p=BFQ8cfS{q`UewNSUvIyqE5Xx|6GRcwWwSp$4-A(f3ld!Z4-X9?1Fet! zeggwqmUUEZEY)ULXBUyP-(cDT_^+qzdVsJ@+aM9fJL!to}|d~kP}I%m1s&Qkg|Mam1ot$k{Ju5lbu_b8(8RD zSnThU!|v_Ja7<#Pr4%GV*oZ4R`5993f*U~4XPy-`psYstl$eBfNY`ziNcxhlMA@-A zk!qRm)rYR21CT{ScN3Oy#DL_OXKJW$cNx;%XUTFR{loSO5l=X#23Os*|mkxlYc z5Y=&W5e}5nLgPGviaA0^LK^7}f z;?i`v_EKMiz(orx!&nYk1`RZAU$pp|l1c)kcG`qqCRUDBD#+0*iC!md zC|BUi92#N<&n@YyjXnc<-sv|`EF~>f93glAgs{YlNVM2$`a2$vYOr=vvX*yTv?u_r zGZX9`(xi)!`5;N>%SV}TQRMKe28R;?OGa#7Ri8P+v(P7*V^v?opB1W4JExQ>Y#F|J zv6>GNHT%n%k>XcaIii*W->mSh;nH6En;=8dEu6h>yssflrK*~|sua@|w4FaGMk-MmV!mQ;c1% zT)DzTah|Cnv%0~4O$TRZ`}^s?dS_>6WTQ^PL%1Vtp3_PPSAU-tq!{<~X+o%=!5U6a z569_pu>sFeeKP0MIWH;`Gb$Doae0+RhU{IWlmYzpcEl*ufy~0q2yF1LK|N!6*^d7F zM%S$63|sgjoU5f2$uqvE!`|LYMGV?B#g#JN(MYPE;80wc6iL~nr!u8n19tBgj3KN6 zFw!$jAT__lh1%HIDae-zAg7(_AW`>xzo*1pjtoh`3M5hIR{>j6)M+N-OTxXKfp4R7 z6%#_Uh$Pu)#ti=9`M8@u|H4>Z6T!=NFfPaHJ)O2UVz?M2p8-PrYmKdZ_gW8 z2j-1M?tpW{*=PwG8sDIK90)6tB+csC;MDXC>-Q{kYrNCbGdn#4B$+Hn^|A6jE4?!` z0!e$Uv}~zaqF_pI_uR0E839*OO2{=<$#R$sKtV5TBV?Pb#IC!5Q)&s@A-g4Bml+!1 za>c@Gnr_jeSy6nyx}kX8(mLC2SiB=J9jiMlINB=7SdCkz!8hiVaAQQ^ZDeplnG6wc znSLvRRwTI&zH$PCTBV-lxt|%|0J{hJ^cas32~#rLYJiK-G;eZh8ja#qzO#S-i!Z#m zfB*iA7cWjtU(+(S=30cI(6H39Fr)H(k6uX|UOp+XyQ_lO9Z~_+5gh;<3*_>o*_7Fn z*@aUysdLDJQ64gyB&g%d)9c|j0G1{5br=glV6lg7IXqh*n4`g9Z7j%QbY_}*>=XHE zkW*(@NlBC|>9=#VQD#p>><$%VqZp}(dMVTsG;DPh^;cE_^ljKE4yuAKu5R@2(so78 zIO3}o=YmNwu9C-E&@ObgEie?rz3@d6_dyJ|@O|2_hTwBD3mh&>`dAKrOwMaykiEx~ zlT*McrD`qmv4K+;g{^@E7fm|+Yq{+bYLYYn8_A;nx~!1~LM_BgGKUJX5}@Yxj54fT zwnnziiB_(qsjHb#IW-AZP9|0;yO7)r>G$LFoC~PmZO;)CL#q~c@uy*0*2x3g_{&&e$Def6k)ys9y`Fj&GQ%x$M zjUiIqpg^1bdli-|GNH5!R1gz|b~x|leeY`kL(m`rL(`wysW!W+fs&}DG?L5;Nt&9R zdi(V0g9i@0^2*DH-#C2b%H=6$iM}0Qb4V>w6`h=DSqeWEhNKU1G}6(ysCPgULb~$= zA!#&r>RDh%Dx~z@pZ{vUX@Gb!b9UTW`I4Zbq^Eomn=zFQ!X1=|xkq!6qRV7cA|+1< z{2-g`d8#=zGc`SR`SPWinHiB2qK%n~@;J%&?(d)mf(4iadH5X>$pz3oglGXW9JL&? z9s+K&Y8syIy=}B)PyIf;2YnPJH;q}dE7GD~NvXl-P$680@ya0G>!Sgunb-oMxSPVWAtj8T!YhK&ED1u61QcHgR%e`UlUv{pKJ1`#*U0+t0nce?KqU zIcqS7M@D$@4kA6*6bWkr8lh(r=f z|8$8;@MDk?H9i8a9izyIEIul?3{g{C(Q-Yc^u$vE7T}H8;sBV4-LKbmE?>AfJUqz0 zSJvf6M@N|!0z7AXUt!Cdz9?r+AP-BZyfsS7Wxgux+EBkiNxEm0WH7BrKcrN+=hX6W z!fhYODLZ!FBQAp6I;%#+ajzw<gQU{zz5m+4md4wEG|((5+S z#7;h^z-m*R0_YtJwJd#2fdwQJ!en+W1<%dBCD1#O;q_4wFIJMqNim#SY+?49@f8}V zkZMz=PLKu7Hbh!v6++ek$SNo|7Qcpaoe5q*XMqF4hHxSbZy2XAsG*}_C}e{l8}%$M z4h{`jBDNJI)DL0)S%H+UOXI?*a=+)O=ed)y)PoFvkh@i1To2s!W#dJ$io)*UAsesAr`2ExA_ynB!{v2a#AAB;?Ff({-}!*6D9PcfP0?E!sIaaG z)ht0c8jY}CJ0l=W*3MQBB#;Af!G4aZ^^ zu%%Gq?kJ_aNGv9HAcfK*Wbq5x#G**OJwgb^U#;K`rO4&w3pRPQRk#C{R%kary}^8C z=f#zWy`b2a-f|~$~4P(ono~r3<#cHrcCMDiP4so`=g=hJ092>H zkswCYsy{=qpaB@Miohw_M&?RZCEB7?4Kf>K(s>=A1*MyuoK>!ilq9S(uE^X5vW{C% zv4)h71M48mnNkt{dPp-dlCjCDFj&#EK)+0fvO`8NY(SWro_^`27r*n}?>_&+cQ{a# z1#E!84*iAtci#fdVs^MnKp? zfhb{%in$Ujmm81qw6j8K5ysk!Ae3ac1P=$6^-+BuT6Z>RFZH6{}pYngqARfOAhw zE5fuPX2vK%1jEDTk+D&G2Um}sZHP~@@=QG7N)MTZb91iPTy75a4pd!@Nr&BP)oaj( z9AeO-2#IV5AuKzwPJwQ)>M-sTbJkUYbj-Gg!55^o%+id~vwGgcrTxTjGjTO|G zSf-r|P%VPnfWx;Zl|yFq6TzHImcPuw>tL63ScpkGMQCfs0gDSp!e z-80H1tYD=qGneqlxB}C$1P-4_IeaDBkw^s~lR?4jqn4ifB%`Qs$)m90L%8aG`i68! zanIt+^ehc_U~p(bpFx_Rp4K4{ysV5c14DznpUgiy+SuNr0TQpHwW>Zgpl0%m3DKn% z!6`Uag_;tr>-3J3Xt@oDofoZ|QB!1s%vF)Xj$VPO(G++ECQ-PR0aJn(oC*&bZ~-Kq zbq}7dnEmNTko=Wwq_K>RiOXAXD?&+ zOiMX#IHNu5Jhxj#u@tR`cARc7*U#CV3|F+LQ@frpF>P(qRH>H<(zPpBzy18T4;?x@ zFr*_w8SS!BoyjD}asolg%)78>LEACy3-y?h zyUEMUW$eMm9AU9f125ZhWkao)>lBg~EeKl3pr8|$?ECuX8MTw>b-H@}dS-+7+;bPF zj519OT!q7=R8o|gRA_%MMBQd`D_T;imWtknQDc@TrYA^CWmhSSQ*$Do)tYGzRUv~} zkm73)`YsFYN468^0SxF3BG5Jy(rf?VG@4W5)F@2Kbs^MB!o+x)yR)28qa-fCb=;kD zg{Bn*VrpyGL9>R!th<(GPNW6>qPP{ota7V@S%vFn)Pl_Jth8@S6)Q5fG9@rp26X2u zI*W@_Gt=Z+Nd+`CG)&_Bj88bV9%quvVH&u2bh_Tj{wT`UsPLGn-$KKAYWuL)MI&al zn%Wb(;oKxcjW5dgDq8{%r&c4CC^*f+f^YhSFmb+J1J%ZMyUMxza%tMs1 z^q<8g!c6gGQ)-23h@Q2Bj=L6%>U2u)8|JIHN?6!x7}MRw`GLOvwd-`PUbUJ7Z?s5nvNuRqA^KkQ zmTDkXEA}9(x~0q;nW+N}g3vN}CzZX=YBn^GkdcH0E`m0a`hHJLin>8Xw4&20y&b#E z14MNF#&8j!W=og+he7G4n%Bc&)&j5$!Qo+WqS_Z~TjmQkfsQ%R#HB>J9I5inWGe&S zXv~Fa6p6&T7&ppqDfo$dqj*c9v^3aT!S~%gAyhwIyXuLw4TthTiZY}~1U#{bm&qDf z*np8>Jh2Q|iX}#+)J~Cvl^8ij-e^55L8btZ6|7F&JikE7Nd;<)Ju~w&bU&;xX;ikb zFwN({7~|+|c0Nq6t_U|XIHXe_NaoG}Purtr-TUZ~B2Abl02+CDnmMdowpc|ny9cZUTwietx?wGQNa957y-yRMzt_1 zlZ&(+_X|AOnOL>zGe7#Hd-m)Z9Uq6kiDFE7Hyh>X!>)1jR%9AU#kl<%7iU#Ue^*2r zOVm+vD{X<=rtk$&N1|nb>qc}kHIWK>9;yKduK+3w(;_Aspy(hbaAk)we(YCA9knpm z7NnHyA|>(aF0Kvkb!0Va!0F_YG*K?})CsZy+nQ;$sW!mHq=H#gQOpS%^JorM2S{3t zSygZfvQ&lJ4q^63$XT(#gbA<*RNE!A*M12OBJQ5J zn@_3cz87!OfIBrQFXeg#hI?&b0m6IgO}e@*0;vvf3u!tFLGuU5JO`rNb3!I~3}v;i zK-=5cWPa+@sS|I#HEAb^vbJO&FrCMzB$?Y`BmjMVOMe9nvjudsArK=$Cp#)H5NYn~e ziG*cF->UZDzy(rHs)V^r5jgn(qLE%9M`L!-a7U;X$T7K6#)>ip#p{3esc0Mu!!_3H z^#ss9>P7VO)hh#>?JzhvZ6~MmbqY4J5M`{#GL$w=X)vXMl(elic+T0=nWZh{)&_U9 zBDSU)0$p9gfP?)jSGqIGsa05K01D=iU9mPGu=1+RSQIqdP|o1H=XsMLQKkO;%6M3! zsxwQ=T3DOX!pZUt2|SdXH>IuD$5#9d|Od% zFNrlrbFHjr_slb`8nmZixCfTEX|ertY@*cD4juqryEggUbIZZuHr!WP6KZo zYRyS++OfbS*TMOmGc$aoaB8e?lyz`sk_hH9z(!5jJ&02E$18|#wg%&KwOACzzyL=n zzV}X#Ag#>qxWMu?EP1_X{N7c7B0m>>922;iWi7Rh46U+^Jl?P1 zn;`SV8mkTo5HXSk*$xOXJE*8-sgrcg_T$+}@&Y>2=eJFfGpHiiNTkTNkkd5pQORkJ z${q3^_5vHVWDa;I4jAf{yOF2Fd&wm(_Ds*t@Dz~efGbz6#LHZ{at(N9jxb`2G4F3f z-NiFUC)>V}%Sa{VwWH)rD-hjacB)^IbY)hEcZ@cQQ|X zH*Va-C}&`BnCT;O&CO~XgRHQRv-x;u#`lO-xn%LBD=WZ6kO;U~@JjMHN&@ge$|c$eP=T*UozZop*9lt6^~utL*zKQ8HRKH{ z`GGE4u@^4YV^CZZB9IBJl}cIyxJWI?wxV|9=~7H17366njBP7mW+V_sFSfeC@CE0< zbZCy*;Khpjw|wBsdMvX_^0lpFXk-=KTCD&P0c5=+JeM3ZghUBJK)JJ|Hbw zKpHoT08>lu{6_?(c&dQ|X2iryF3T^8?XyEO2O}aj&|nBA#3X^lw5O9sSETKN*Tzko zwr<|?=%bJB+O=!ts?|e-gPy~(8Un-CXSR^E`K(gXmBK3Rdsm@>iaH9FC5<_3)SFj) zfv@T!Pk03`5rZ$rVM2>n3RYOe{Eoi`sSGsd4uA=irNK*l%`An_gphYK?5mz?Ke}Y1 zWsw>g8SJgPC0*S(!*bD7-b6DfC&gn{j#87vl~WK=Yc>jv_7G_c#QqnykxLT60+dBF z7Fl)|!G!2?6&P3}!gAaeqS&-h@8jT9iE?}>rRQwF$*IZX$4{Jo`|a}=FFyL%W9!#% zkeSHOKo){#^Pt;@Upc0hcy8ncvxZPro*V?pa<#j*asUgbqSbO@LaMJaHP3bdIunV+ zZs@lxNjoLEs7(UjkN{?2?R36W>ZwE#u^|2~n-JDY(r6PZUJWvqU5>K|XQ4tVEfSm2 zGT@Fur%+;4m$QX<9fC0veeXScG!KMt~A@i^z2&wxIWYm|SmAsd1HzB(JZeraZA+ z7bRSW&w&$#6lk04A~n!V=*u2Jo^pv2hql7t0BkEK?!#ROPYdmn$r^)kjOgU#)TK+8 z-hA`O(KnBdPE4@dKrbF^G248_V$W=^ww=$;^5n3e6Gt^6V~j)5W3`9_SBzRvX`XIV zL5+J%S^DLY+bkt%96B48XjkyI}+Yq=e zXAMfp%;Er;pV8;(ZIGo=md+W{yVf&24?KGG$jh(1`o__td-v{Lxq^p;T6mL4?)I!1 zEX>bo9g3H+tynci%?T}}0$5aHjVROz*_}?Dl!)X6i8#&fh#2;b-jXC$OREuf0KeBV zZ=-}%BFSX>3@N2|FuD}jjVcGWZWX%W*H=0-08h^nVlXVP#;BT43uQWqlwFr0WhG*C z;0!k?t>n_O>jDS$7P&Ubq48_sL`s3N!0d%x4McoL6HmZKNBMTJ<~7Emq)J0fB%KjZ z0j4Gr^jnv(AX5rC5nms!w+ItJ{3pLVfTbh?Dman3Ie7#fVQgailDWYP5`}>W4n`!KtaK z!GXbfTY)wW1c7rINb)O|*IQ6vokP~7DOD3hY5*ajmeoMj_0W*fKveW39u5&Xg^E8Y7ddpBpAOu6zo^w7k zVT(%6j(gCvc^1g3ta5Jn{J!ClVZOdKJ2ThMq)VNGKFW+oCkw!tSx||wA61Z?U^6>A z>*Fe^kSz_2m&{{95__O0zk){tib5s`ixX4~D&jZh%dBDgW9v&GS9NM{k+p1No z@45T#ef##^b=Tc%)~sPFOKH5q0RtAo8qHxh;W?p{CP$K877a;BnW)+*6mTialPb{y zuB~~1g#cFE9HSI=%5CTi->5iRf~-@{!3?P=rc`1GCK88d3|L>Rx+B>(*~%*FG)uBp zTdtMOa3rJ|i<3w)Hs)wCUj?)lWHyx*w_;RID=!GfoLgF!l@~&IDV__L5>o(3Z=235M@%Ux;NuNw8+k89 z$jrHrwb2KpFBWGKHEw2*4RRzQS>gr@ZpzFo{z|>>M`1kO+T# z2)~0v+Bm>|V0coe?#ayM1n)<#*D`}K6eK-ZbAg)ev%r-toIQW`;DH0b|NFmx;nKxx zlT&~GU;q5ByYBk*k9>N3d`!<0@u%z^<;!5unw9h6*YxBoS`vl}Lx4N}3>QQ0+zcQ7 z@rEfEDhNdZ^+ciyqny0R^02M195OYdT-B{nTjm%YFHta2Qp~DRpoAferqCO`!~DXW zUOFH)&_@r%COF&*nxX6zOQ$puWfPng%_=AmN$eq&!b=UP#tge-ku$9(ZHLmeW?B_3 zX}gkk5Tp&}11pHwC;!SRl~4 zjec_5>P9RT7C0+k0dK|566J1iAT$j1z#50~@p8NsZK$7-)5QxH-g=8u0S~c*Yt8D_ z=P#b;Oo-#hj&ri;@X(0*7Q;i2Vh1VdSC{KI++pv0U5+S{_$kM3g@_FyFMG-e(MTty z-{gRoAumZI!;S!rSj&=FVqH-h45%R7<(f5XHNa(`u#GahV9bzNvtgs&xw4WFx4M41 zC`k@d*H50?BJ9PNrJIT0=0$8#h?|+5YTp{S^rOW(`Gub8nd!?{F0Wd(f?m64NXtny zes>z`jP)!{lQFpxIc5!2xYMB%_jL{CV)!K=g@CF_V1FMgS-fz_CP z(g2DK%?kS`CdN6_Z}0BCd|~4L`|sar7q}G4robYn3&J=>71X_ArXdITe!E+}$3mOd z6>;?wkg>=zJSD<)`5S66QSB1#wgNNZwb+fRQ6Bv@*^G~Ks*;1;1Y8AsXX4jcsmmm+ z+eSxXfXM)t^Kx@kE#f*bZ2SY{x-c`cWrFwd{l&G+@Oo=h{F7+b-5%ayBF2YDfm&Dn;~ zqz0%;Id-o|C*kv}G278|uoTWpSA}XMl@Rkl1yY2xeVh-?_mAlLr&e7lJ}6{R-?Sgo zSioZ~3KTBIIg6bKM@0I2-#C2uKm5VJKYjZ2rAwFp;XnF^J9q5(v;X?%Zyr7N{Bz&_ z;ZOg_kNwz>?cA|*Y;=sn00F;p;n61oPhJD?#wWJ4v)#vnM@H*lP_rQ^r1OgHg_cs#?IT3B#}TiXmUBBLt{Z$Tq%RFkuc3Wa*q zxu$gIkfM2@0_j;@GBVr(Rv~G^iX^XVQ$n?)!YaI385gbTp`0iH*p6w+jaC$Mg;8)a ze81%YS#sy#(x_Qehp4eDcs3C!a&wWJAcK%OLS1gp&Cw&g)CGK-%1yZuC2=GWd`L(t z|77qA+GZ?m-c;?XmL-nr&PJz>it$fRPX`4Fu$S|=@Ljxp%{$5X!dG8=nKyU-)xZ8% zc(%`e_Ot6ZY@ArJ@{j)L4^N&r@xJ@s&lZKx{mf6}+!)cR5%u(p4i3SFzFuBW8ytLv zp%3<2eglk^FI#3C8rGxBxmnwnZ0*fOLOYpqauq5+E2D$!UIkK`1Imo;lFsW_6Ji}o zDGo^~=nZ&x3B9S;u(L3vKsKzgw^lO+YYyzT=JBU}goCmGtEYzQz4fS&6+#;gb5^VN z=%NAHX}lT>=;|@GM7a2y7Xe&{3`6e-V~If(Ieg4-{%w>b2Ti2fKzh>`yX*LPegwRz}#cYZLsoLx6@|sQc+ZE2c4T#eD-@8rln=P-*3m<^n z;Y_^fQ}vRdCIE|L<6|2)ZS>wzXXZ&7cR6?{nJasI%mLx7%vE!v+qOXru8UudUF@=g zu)3k~FFuE}YFN}89@6M%a(epmwX3X`uUfT=ncuqg>qkdM!cMI)A%u)i2*LK5spu&Z z9E2$22Dz|%(ls&Bai#;b1ZKJ%vkb4*t5-p1kPj^O=|f?*u~dt0c8OT)%NtX-O{z7W z<;YRv4s~^l+@tG86<)}1VJ&2#Xlj~_EVBUnz6iN~(1dKeRf&^&2si?ec_0C_qHx3M z)`5s+EoO||lmcBME*Z=g;aQZD7I>f~e2taN@Oc5j>Q3`}wE?+5 z%fSQj?5mgO71Oh`SFc_?aNxijN8Vugx_Z^hy?ghrUb~hR`RzNlvHE}f*fFMpM~@!e zuzvl-#EON*8Bs^$xG+tPET)keVV!quO-;^I%^xruv_+JbW+$<)=kf|&W+iJ@7ZfE$ zY6GlI^AONEQD|vY5%nqdN5l-*%WQd`!??AJl1W*1L5HyOiD|d}Z*@XwMpj5|+6YC; zve0S)lZPz5MP3dU!z~F#fW}{wq(IFC6S$2hsCf-2{K-2hSOs-sYa-N=%wlj*j|9~q zY)L?SezbZf;c0O!@e-V#p5Z<=HflyAV7oe3vx`)S=y+7^N*$;{xuuZwa3jKcU6Q9_zYuANKmk%F4^sj&W->zJ}=H7dI z*)gRNmVH{9$FHoo0NLNmqe6UhEhD5b`L zjp;;J%qoyAOj?v8x-bcww82D_wDc<{*addA$Ym7oFe>T`;xVA&kqaw})TxHiyb47L zsBNmE&v)3P3ne;D6RcpDLeYAGYFBWBFAA5?op>dhm%_6O%slV*3a3JZC1D%0B6vwH z320elUn;ewDl-Y60!i$8if7duO9_%)35&d>!D1U0hsEHPwE(5fZ@gVRH#;^q&c_&7 zU}8Wst3%a|s*&)Q2;F1LPVEx1UHtMb3RyD8<=IsfR(8F3cI+$;jm0V4*C94G9C_pU z7oOX;ZQGtbyFc@p&#b!Rj;&j^vFRP_7dKlFG|5E9rkMDK(BPdil4w^2L}&Rd1}FXr zYH)ZMAW|h3?=G6ha}lFZQQ+3?1xPDafb;@Z?nUWJPN)RsFnN-S*4+iJ!5X{-Ngk4L zEP_edNVCpbXa?Vrzo_$H#=(zz=sCVrq69tApa<7J-vNEH64t3p&dDZ`>T2v#hMZd0 zf|rr5Kh{CUl&#SUTA_+!Pm~I7S>$qIOZnJ6?@$GhPz<=`tr09qQbWtZWkNKo9461s z)0Y%p79!nn%o*k3I&Pkea*7sI18r0ti?hrFr>5oi25o-?H8jJcD`sLkrk!D{j=;dn zSqtGPQSIBB?_Xq9YxSBn2VXzPQ4W9jAODygTaSPCab6ny=wpwvI?t}RpZv+c@#fK^ z|NNi-Unfqod_VGoKlp=>J@(kbReUOsyYw9(Je4&x9G6@x8(bKau>ro14P65R+NiH) zsIi$In^2!NN0dyu%mj7kDoW4L8TBKo*1yqmtV|B_<`cQ-lG%F3u)@Jr1RS zI1V}<{5&evmJZ4-RJ~KN<*E7W11ksftRW6vN zyZ4ULT-R?Z!jiCaNenTGxdbx(|NH;;%MNZrR4~1cD~a+WQ6ZJA5WOBjnkm&&y)m?t zfqDjwmqcxvAz9EA_f%U7+p}ZRZLUa;=WC6bv4}xp;O1@RG_jrBla{24dMk7VEx%I0 zqLSPx$3QAe9%#fRv7k>#(_57&0P6=~E$Ue*!#UbLaSMJCM#*eGCy zwH}!SRMkhj(Evy<%PjDQ%{RXJ%@}e@JTAFaIB>pQp@voH!OaMa3uM`g?3-NL&g&i`2Rda8rcq)@@ zmkr5{xtOKQ?)Oro?|QoEqg zBdHv&BAsUm7~9R^+Ju`)Seu1o`Oj&#q?~{yfMf`46#ywjg`Slq@aM*3u#xu-}3b`t(G7Eq# zF=99TrV0&tCOwvIbSx96OsO>7)qs{Hifpq)Z66g=%0&u#ps?iP$<-HVy^`6t4zg0D zENw1`*P`Doy;Ou8(!)R|Vg&rJ4n|7*+&YTEx8z=MtzlN45SDbBYCXq-(>r)|!yU2t z$K(b}P_+}04fs$YNKYaS5BsLTI$+3J0p*ICGNaMs5+SoNfE_SSXi;z}O@!i=!YM#b zk~{?*yb>kRuw2f9XhzBt1&}2=u_DXkl{>s53PBX2Hm=uoG#QW*(AeCxo%238sG$wZ zRGND(&hk{yR00fI`_{L<`RXgLvbJ>hU3Y!<@yEyQ^_bz|VHyv}>({OS_{Trayph@9 z*S_}Et5>hS|NZxE-MWS6gzOU|T^>!Eu1KpA<=U{yL0(#>WQmej%G4p66YKyQvvU*+ z7qyT;%7HpS%n!f8>L{NjbcAn(UP)g6BXCQDvRvy?T%{d(FV!9z35E)CwKvvC zBiiyNcHoPVX_GqrDx(?IDZ49iFt`I$fUYpug)GQ_2{O5IXO8m96tXE;D3N;DUeTGT)1$7!`a8i#@Os|@4ff#yYo(##90?t z1xD2%C=LiN;0n<)#FHkZb+jR*XKD!oEAHw}m>=4u(wTV3VZw-Fq^sf+i$1|(OT0DK zwI?=GYLiumZzS$uZRoVgs_!c%3RlJ+Q3>9rq1sh<778 z%w$+vT~JapK(mI7%2I-E10{?$YL9RUE6kgtMVU1PAeSmZda6g2mAIrpO7xUKrZCr( zkOwOCJu4|B1(MC@xLt+|BHbY5#i_u%mQv{orA2L2;In8+Z#iNH(`B$hV}(dQRt^1Z zASmbNI#9dNh6>ylj>!5K!C-`!FJF1_g%_T9;>oA}>gk{T+~@AQ@4gRz_(S+K4wzvf zi@ob3>(;GUvGVc9fBc0PUi!6P`xgw!PM$ouZR?iRt5yl0*^WNsqopvpX0&2;q_+cIa7dF?>NJT=Y*+ViV{(fB3YspdNVndlwYafd7_02npswfT9zJyW!6eW zL~Ef6!wV;Z-sHXIN66KrS1yF_+46!6&k1dcC|HvWK;HeP5D?*TeU!=e6{d%5oD=pk zUwM%mi6?-@+`2RgzMj=CB|ITaipyc8n2fAN6(`Zdt?oIIgE%s7N&}yGX|_=us$2nCbD1*s2+9GHuwP)=7^K77fL5t>Eno)L2`tH1008O3?aG z>dmim)dc!RF+oKH`?Zy6XO!Q?TZ~nA0a02oInWL(Au72RowQ?+BJ-4;quMo!Vh5Ba z%-US~%(Kt@*`NN|{{1hL{5OC4ryqX!pOL8UJWx*^S%A@8D zc(@X@#|xY+L>7Du$2kNoDLr0<6Is$?melBf4r}S$s+VI_Q?qy?o~0FHkmyV;P%us0 z^FW1ELX0$2M*Gu_2y&@+o|91RgjJf*K6+<)JScCdPuv{bA>Q_aE3_t`t zN^45t44?=GUJ>3iEU|#O%|fw2>b5ocV7EC*bg`}wFLG<;bSO;)(e;z#MD`)g0W7h( znsVG$;e?*3S&G2(l$6+-P61D2a#NG)NQ9m1&6Jm1iXe@ElxMg_i&IkPOC0mdG4hlY?o2E~7>Xhalssh7a-D3l zOpita-TpRWQ7fuM>#t4aFwq53t=HQ{F280IuI&bnrdTV0t2rsU0k%z&DAX<`x>3}! z8WI{wG{6Od(TxVHF7U@2heiH5kC_NdPU(k|e9 z3?dN;@ECxTa+o%E5HdPByDH5_MvhPc&DbUKMH&n%sTK__I%N^CuyhjtTrax&G)#Lm zwO>@`g_FX>wei4)uk_38QfJ_*Q5ky|bskL5!9xeW``z#U?!WtYJpKEDkALjPKl5WB z_`v<6<6}53T8Mivz@g6@+N)Qu-oAaug^TB(dip6uID7Wo&K=v>qVF$1vkHtS!xu{e zmi~3Hvqr<#&{)EQr2PX(<e=;SqNT zIq7;>UPHrpT@*@s^KWVBH5tCA?Gprb*!-icPyjOaKfW^#3Od4yF)}$Sdgpxssc6zk zo6Cx${1ou8o0B8vMMjJO>#42pj@#-EUaPFi6T((t@2+%PHDq+aKeOScC~$!$dG02Qx*Go?3(@cuWjy7c59E>EElh2I-UvYs-ik1(PaZDbMl? z!YPyjg7ru#$w-hAN@PYwGvL!V_#|)hCNr+U-Z4`H7~tzqb62lTo;-Q#_16z+v1DR= z*G^u%*~<8dfe)(G;7BuYg8XShxN^k`hE@R4KrX+W_P&4rci51{DGsEsU$<8JnZS(am+)a4#qy_C9p8X+_lO$E$TnODv!&4xZyM0fDNEGfr?BmqOMry-J0b0=UpOs3Try0vTXx##ZheD^z(*Cvl0 zJI+>8mdQAtba-ggnA!NPFK(~X8-r?NJS&hh^si7j#o)YL@+g0(ygV8;mi>n897>49 z0jj13lHn(#5`$%c7>i_p(qba=GuT09bLTxh%z7*?pUrGgo|*|ZNb)ZRFE2wP!&&Kt zFx4Uf%^S7no#*D^C|kp?i|cn#2viU~#Q_@dEvbUi3amqV|3Cia|FtcM6jQlO;#Nqq zW8hn5Uw3*gLwh3Byp3pSRr*IvC&)2Mn_J*G6BYx8hle;|mqebDV-i!7Qyi)?JjTQD zzJ<9SzEU+jHo~F^56x^yVrB6zF%(NbDyLl@uy!Yd$*WR&NdIC1loIt5(~93FYw(c~ z?8J)y`iYl@@Q4EeQ#%pLTaXhkXQyoYb4(Q>!4c;3%Dh3#H{b{OiX<(1fA|GS^ z;0Hf==FHnj^`E}@#g|`x`Odw2_|OF&$PR1Jrfdmp{f2a>vY5cQA8VYZ5HBHjHBE{d z^(RQKsEpEYN;i;Zm=c1tNP>eG@Sa$KwBA&ZQ6$eVuo8S7-LM)Ur30_Uh-{E9knT$6 z-uM?#8ps-wNDI#**blGYVBx$mPRFAmD=U)fhWMl67kRaSuNBQs&tL?y5=U0WI2%aC zG)r)U3=%p>ybIJWBOX&b{es~jk~suB;dGtfM%u_U$=o$j0jk&JXPybi%C+2%IyPNf1aTgH zJbas-;d?)}O&n{(Hs$v8N4M?XPtj-+UX|_?!#*HWx8$)7e8E1K1G{UJlNT>vdh+SN zdi(7&)DPVKSrbgn)3e|;KQ%S=)?05e$|ArP_mwMGvD$@$VvXDx+9I*us>L<&B_o$d ztILb*ts2s16aLw05LyxF!JDeaSy@KrWCKZhWRwzQ;o)+PSwuM=^}ClePpO9^8U5CC z^OTN|MxmgS!HbMXouCO;K;~RphYNa*Fv?c9fZDW`M5nzm4ko52!+ji*!JD5<+Ud=C zoMKZ%YhyK9LBT;8uI;6Y3QBSfMYV_N0zrB0UE=I*e!^5~=&JUoS(co}*v^o46tx-#2 z+UU^BAexp)iWC#7o}aRsqzPaIV0GuiB`d8U6)?+{(-)BEL_-opB&|R&AOX20$kl74 z6mm3YRXeb#5)DV(^@x;4q6ef7&x>Q6E;49u28J=Kp1vxTeQEvuLqmFOh+`2NWXuKW z`&)WK*vA>y>}>-!No1_V*)Q=F^K|9mlvo5JxPt|akqr&YDyVr%%mUJ3!o8r_U8pQ% z31_KK2oXK&kQKg7FOw{&7@_H!nV(2_WvJ5VXn48y>eXw94(XBCzJ2@BXQ}k)*Z|$k zR6;{-QB=25(Y2N297>61r+K1Rf1Xl=C7E5pQM$G$oC65~(VD770tHM*;E}Es z#p5J7w%whR9&T{iY>@WEHc@?H;>eLBhu=7S{P;0cv}4DPEnBy2*syMRcof5;g}{j0 z{Gx_gv~auYeM^!jR<0Nt8NPGhU3?7ZyD#rQfByV;UV4e61X+~Q`-xoId*#x(iwLq< zp36r{lGFpio=xv5D`4E(AD7K}b|Xm(7&MjT5SHPQqKlspB9KC!=l5Jed3qKGM3SJhtC!`dK}$Ir5OdSb6)5CWS0kjDf-Z~8?_1%PbVELp_8 z**KKa12wV?27K~d@e0tP1tm zgfg*&&Od`=9WCXM#%Kx&HJNdN$P;E6J0ZLmh#F86Qr+m$h}RG?6e-iO!7+=HgqVPj z3~9Gp#Pi=kg`qUr>Cd*^7Em?hYlS&jE`Eo zX6?Q^_ioy}iBp=v!o%5KQ4MkZVyXju@#a{F{$8FFvX^wzrp-JHdGqL-Oo{l4J)aF@ zL#bwH8X~CY>W?!YgkeJwC}>jCq2dBcO|@=e)zL#JrWs|kIl6LOHGVfoDMbEfYVwpU zLCc2{0pbDCIS`SpIlRc=Q6vDv8(?LHaxQ%+b82ex$dNbM(X(mOCRW4Q9un#ZT#786 zXo%~yj9HhrT*@{mfVt%D;Ztq`mRxPX@rC<7`J-c^v z{0S|QMPg8-?8q+{%nIw}Rge%jD1*X!rcmrs)Swzw35Zyf^&HhG&W3LI#(|IxHUKHI zA|LxTHk2-rB;W!>YlCF@GEhA-H>$$qq+K4OGH{xm8>V3l4_~}^i8b~A^rbK2I8L5E z{n^KV;-`P=r`N7q!)AkiPHESxJHlq1b?xdkK4QvIr8K9ND_1i1(kXs(Y@8eXiJy3! z{i3M)-1&3A`CGrWVZ+AJ;n9&%Hf8m3CIe$cE%PF|&#*FNcUo=lnXU@B#>9$X1$3=kQ2|LU4lAmu zTsitu!?jvoDuG6Lgu{Zd{28|5@KT2#u^9fj;!&NTy@%0(GEM-9xJJj;6)}o`ilPVu;pQljJE_LV%F4DnA zKwEr*lqh?Ol%pA(Y>1nT-IA1Ri4MkH zh>esP#yADa0`|PVEDt2FO6ml$<8Phg5$mu0%YVTK8y|oCagOBKcjujfnGNT8SCf+$ zXXobhMkWl(0#fQE!Sx2za9w`NWC6vqt1!ZhTaE>61tr5?N{~ZkN?S%%UQ=w8kBI+4 z)iQJ|uGG{mDah2lj=G18EIZ1OG2#$n4#x<6sjSAN$r=EQB z>2E*x?U!D75q&)N=wl!G@P~Kp-mP<)@H)1y0Lj!$`ud)I=GlV>4y;(Y0xGx_JQtU8 znKZJMYH)z3i|f{`dF!p?obZ0=@S&~Swu6PA!%4AkR3b=fDI64u?1Gy{6}5Ckwp<5L z%yXiGh(>JBX(&Dz!BB-u-2oXBjB}!Cv_(-wU8s;`WXmZrr~K;#{T_=HBr5==@*+l9 z¬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�gfQA(7U_~q$y6$vm|r_V-eaUH9B=E&u*0Y#e_E1 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�lR?dO<|lG&maDvXG#tQCs5 zI20N-w$taESJYq7BXCxw*9Dd^U%b&45Ei?*)QcmaPWJD`(Ow{2+yh>Xn#$yOsx(Od z6t(~WKmbWZK~(9Fs|dli*|njIi+;utQoG`ou05Q_0z~7b#hlRb-!pFWO!e$xJewO+ zJybJT0fW+`jZ)N=_h7obQWtd<%t=kw z$L=@!Q^qgo5!e^x_{e!!wXmt(9v(8W%_YR2jlQhz8i$5rY+K#r$+14y9>RN({+xT@ zQYM7z*Dicmk0}(ZfvT$+G-tVx<-`GEh#7TSW0yN9j&>wv1?*U9xM3ZX`u}hkCGok^ z5o!oslmI9!v!`%Qmt-}uK@arcqsItK`{w2!F>b=Mo`Kic#fq2gS=rIE3Rp@8w_C61 zVNrVOATHr|9{Q4Y~Gvs)@sL#&Up)R)=s!NK`CPj^Qtn zH-d~#sw>UoHZEho&BN|GW9UjOa(GO0LvqsS7WEFy7 z$vPD5n2Q=4_WB1)6+C%%^XAR%t!-YBLuuL-t6qM=cgBcg;X-9~IHg^JZGBiMFp^bJ zR-;iL2SWI+ls`es9qs@!1V>y{HP3#m|eq*l@N!u$}XnVRQtk|=bapzW6)XH0hx6Zx;|0G z)0%oy*}HO+AZ@Lo?P7S#eQkSV1o(j(uF33D=C{;1?PDni(Q;`KtM(u z$X_lQ)Tp6HT~tg5yr7&RuG0W%o%EE#(EzQ--B0O}TuQ=-T=#+*o@8tA-*hz2kUE?J-%;7XA8BQA{B72Gzc;Ne+zv*64XoGtE^ z#b)^P4l~@mDA$IgqMGLpW(4NT4SHdXJBrP#s6jlSjyG)IJUQgvy>}lxc<_gR_(wNx z+ohDqjjY)r*D`WJq;|1w40mKc z`N>ay<};rqeec?}mtTIFP1N|(l5TT~P-i{jtu9G#4;Fd{L8}Qy$7G{c1HCeo)(7Sx z72T++sYIK`*s>36UrOom?2v;7#2gGx%x)U;5KktXGqo(lFAD^f=MFhp6TFCLA42;LtBYCJJYG?1BX z%z**IO7apc8O3?$XuiON@FEIj&F1Koi7zRz5<@OYw_(^=ccD_$hqi@Fi|8=migQNf z77izBfBLZ-QtEfGC3XBfr%tcr&#c?Ub^L;0W)zLtquFCcuB6k~)$A`Rp)uVuy$FoG z#(jY_FozBt>$yFesIB)_s29*MY z`D0*4k=Z$4#ex9FKsmo`IVq#%^enoQHbQUtQ0g+^smbg-1k1MvZ!2dFPA*=-wh)c*cHuW#+`?lEiL z+VXvOPVe1&@5ev>@wdPI?N?rT^=JS5&+;(<`(U^}g_;Ycud-)dp5ar-gJ$+solNPQ zIVC+pu2Pc*cLcM@s#fR@O=DeKJxH-mfL=o5U84TyR`+a=BKpX7e?d=&YMqd_W<%;V zR_3?2ckkS}^Tr!*eBle9r$+HQ)@!f5_R7mI@9b=2CJj^F3yfO92UZgw3p74h{clHk zgO8@IUfvRjUJyOq(6g>;E-VkokyD4$WX37-obL);sE7uHW~HNzNn9g7W|8JBMPftS z`E6;U^uD7kMX-#-*6A9#(Q&@cw~MH#+uK{ZYI4_tO9IysU1h@kl8(5GL`M8jz-G!K zec@>KLv+p*O6$@o$|66i9m56LNOUvGT z=OlmeXy2dw$#?(LcfRxWZ+!jpZ@oorVL;C?h;Aoa`jqv4Ud1n8K{BT`*v{3>oRRY< zT{A@K=~~lMb+Ds~Wp%)b+x#+a-6PjB-EZOfIvzNt*WsS#`ufdVxBt?w{pFwj>Hmy$ zu6D1!`YP+sY?g*yOc9|33K6Wpz)g2Sib?F7+)AGx@(SE&+w_C;`JXXL%(40~N3E4O zP~GSpVe@G)z%L4sr756^i)>`^urrQ3Wzu6&IyI3ucc{GEYnvF)J&wnZ9`ElzzJ2>8 z=7f4US{}KKsbI&rs$J0#^I?Z&iAXkl3EpK{R8~(z?wG!XJ}USX*Lv$^QDJ4UD+3zy z%L?)eJ(<}IIoGq673mW|uyJuAb6Lozz^BHM`bU0E0UN#ZtO9RWFtR0<{U>85j_ipU zv=dPM>;t6%GjkC=$sxbtt;uN<@ zqzUxPDYPh?3owY@^|B0)0-Db4lCqU7 z(Y7!lShRYq!jz8;M(fGGNnT;P4%U#06k}`h8_wyn5No<@3c-5{v3mvqVLGk|W*lHZ zuc5xqcHOddN8M6jr{7P0^3~h7Z!wI9VryHk3m%^w5wTTh)k2;)B@1eQr~!%TYB)po zOp?|Tw`?Zc(;~}CvXGk!BNxxKPNnx$EXI`sb&O1;?Ci0uOP%SNPR+V!YqGjAi?9gr zn}%e%gr@$@ZjBxD-4sm)=@IP^XXf^#iSs0NCLqvPHKJ#N9EiCu!qEfsUDTo6OfQy@ z*gI4+mOAtWWR>kSyH!E2h(4H;yh5&Bjv<^i`*FY_t$VqJsH92OA!PSh8yPK)9z?;9 za%G#?kmcV+vw~m?+B!f+Q=nFM9fHwF!0*t>lm@!cIkFU49f;8gAY2Lc#R*=nw}Xr3 zCf}g<-Qmv}k+FWr>rdL1dk86H$uE-?HqZ}2db+Q?Nv=KA+OaZ&bW(Aax>R&%wS~Y` zsy773@pGbU49FsqHbB~Dk=7Pcu{d%9F`*7JZdlHR(GQv_Bbd}!8M!bHwu9>dztCis z9H=I`^qiFga21MW5pF`k=u(_L>OhL=lAH+_Mr%mN=$?U<4&D#jyt2Nbt+Bc5&NG2j zU{fDX?!nt;A}3pHr(H7&xkY;O$fP8?7Yc$LwZ~M&t}3vp+rVsFVv3$`YP{#`a`;xp z#Tugcz+pO8oS(^MaG|p&ka)hfclFvAzW9Y(w{F==JFRR@mvyOTdSw&~Nbzt~kVCEq zBTHeIvlnD8!Rk~G%Mv1BouWw{p|q?hzu{>_VEh?F^5dfqA3S*X-FrWL`|aQU?ce_4 z4}WxR?-~n(zxkWL^|{Y}_KnwH-?_TOV$SW`w*lm%6zZwg*11hi;PK%;0!Sx?&SAu& zj%-C*#j7gjMCdYUPVAwn>PJc^$n+VZ%b4@qzmeJoF5$?(KoSG44?Xj z`0OMZqPgPc7R2P9uASn6^;3Q3tvDP%Cq^9zlUoo#iB_DPCDY5px;x!%`btf$K2yS& z6AKWJ#@xu|xr?7G80e&BDD~}E*6BSr+2o1M=cJe)QA&kBWJ3>*KjkPN*rCMc$YD?O zwk#>St~hlndKaWDai@ycz@%T#D-u1Lv-<3UG|VdFNMgnuprMAi6@9~%6%G@x-psC5 zn5Pe_aOaHew@wZZ4%AeeCQubxO>}(SRWnbNa(p6#zYbIeo{GgOFz?=o(ugW3pLIlK(O1@(Ww4Laowtl2jz9 zbfgiIM+VM`G}O@Ukg8=fO0T<(R&Az*yl9pypaNEKltF@d-vYC_by!w`+scTxPJBO-opnUvS4`Q`gOfIv%7Qa#+kk# z&Jl}``w;d|g&uwW@$lh_t#k)AEv*4J*OtSXh6t9W-42GDX`0M2RUqs~D;jX?CV~(p zRiqk4Qj0J^LN@TGpAl>tOebpP=qukEWb4luU`*Xpf@{=S)i1Cn-y}BSRe`S@$DGid zq`DmYBoCAQBn4iAtZc(9*U81)WuUif$Y})35ZMh^*UeOKnwpcbFG25N$VRg#Tp5NH z`sn2>u&K;t7_Tn0bMz8Axw9gD25(}1Iv?2Ekk2-&~o+j1EEz8=} z(`i^eomZAnF}8S~_}9bB9%-zVs#j40z-x!!izRunP2Di)1sl_fs`^;}lX11e}cx+Ox4xu9b}y==~4pt}hR4BCv|4);Lz6?$@~jkN;p z(0znJU07`{{ApFZ2#+_r5DTR12^ zk^^mCH#A>Kzq4L#AQ_}-8eJKyh-DEIVCUz24V-)4@4f%t+i$=9Tfg;NEDXMP@7}Nd z<-h!?PrZ5T=FJa2_~0Ag_y!M-e&;)X%yy%*(b8@xeYtyR)%H z(XuBED~AB`n)9S58^tCx%{X2}6VwgpYJXh^8c4tML{K%+6c2i$`QK(h0N~6veLC;1nn8s#H+yP*{_lT$A^kntn>G82O zZDH7;7=eP;;UW_|wX==AjH07094t$uB7 zcYB+iI;d>6pDoQ*y$q|1bQ;*I?xK+!t!uTgkQR>t-Jq-0`_))WX+Md=AsrnTx zl~g*3ma8D|L?iEH&(h(X7m4^o9CK)fcr`;wb%rY!NEq?rsCXbdIR`|RKxu;?v59Jj4cQ1E>@m7y)+APU#&pI0Q%UNqGC{x2yjLv%>m zTIy{`UlVn=tHD}B5>Ot)WYlUU%=Ex&6^<6cd=h5HzpNEgxU8UC8NBZ&vsb)|Ck}=EkF?7=5FoK8$pe=q zX?3_(63fG3ro7x3J^|#U6GYEvwwu;BFr`>^&|L}ssl&6YnyRWD)M2kVse#OOL0?>- zGt=ZipT<)%K@JR2&Pq2ERXwaN&SH5^h}=JOyi`&?6BELI*WUGe?$cwkn5r3($iu_< z0vOIsNHe8$<;KGB@bl6EckNhxEceiY#`(=b$CI6weU$@Ik68s$5FsT)5H50a*6cvU z>o8R_T%2cFSiz_RaZ@lW*)hDERT1Y<5wJtli@2~}awWkj7ecn~wWEIp*NQ%-7lMkY zQ-(5(th!V&i08pSwvlAC$NCRDyx+We%heW?cJR|4w%p3s&CX_!U{YLE2aw?dR-|iS zkrO%$IK60i6Ut@5!j4FswQfLR>OZ4Dec8!or%I^GD}~hJe%&&O%FfnFRhRGJOjq<- zGd&5(XqT_Bb2(?h(w_bC@vEwttKlFuX$zzk7DddFtZ@N_dJ)r<*J%Wypcf^jaD^qO zEp}f0)TiF~nV6gBYqvSL`-FW4kN~NW$Bi$Rv$-er^ zQlVtiezi^rfe)TWbXhpdia1$U^+QhdD&YbOS7(|b@#6$hhl(?YK)dPASSaQsO_pL! z7(%yN3HIPO6_@)w6v&lhZ9uJ8gVnm$^mPX+yH@0^r-|AIdRqYzhMhEsC8#J~&{N_}aB=f>5dy(%XXu=pQ5$?An!s{X}JQ9Rc+b5$`Li&CNV`-g^|dQ~sDhTu|u3lrwch;~&$BEfvv zOu4{>u3iMfM=nE1CCVPocsEO*+BYFc!731GGpa z3d$aC0KBhH-M!~#dhch8#D=l`&6v*U7jG<$#yWMTY?(VD}$AIAq9@Lfvmw=3#Ntfj<@qeSrSXJ9ls0x=GZBnyw;eKNx|TIl?2w zhhbH@G0`X`r5V0TN4&z>W(0azlt)rg#6D5Yyh|HM*r6-VraD>%f2p2}d6C29CI-S* z({fa10q#>iuHwjGI8C;3af#Q1ZT43YiH4O_OLCKka;xbkQ9j8BAhBKK^pynq%8%-7 znQP~V2h0IjK4loMzpsAvU;XL#|8)P+{-;0lY3?JitjZHpZU_NHP_%k<(Ux1y_nL~x z&l(`f?M6IpQ1nAp&z$I#ee1FeryL>3l-0m|#;j5mhr~HhBii=xsf{!f)n04AY}0aT zcW|`O4KbyreqHk}n+j@Unz91dFt70O6}Wg}nBugydB**bjdea3$iu&UDV{<{5MpUB z)_`gT$F@SDE6Vh8=PEk$XsA15dwYjLHG9mEhvvxI-{+Z8^#%~bwY9NDb7VG1-Ah7z z?=*p_of>j$lqHth$OR&iPs+SXJYu%=1+ODkxI>cD6pSH%?%#i(&uP+8+Lc9*M(ASVRn~he^ z?N!FGGFhiFBF19B#psuq#48r3D%`yBCoB|OEolzv*Y=2FUf_4(Qf>+q-N$4AHT(L$)$Ruoo!pQ}O`Qr0bbZtc00 z`t+11gx(Y90<+H1YiCaD1gy91qzpw0*30r>3Gy+ntBlmw=3h1 zm>IG%`67}UTFM|pMWAY8!!+yHTXDvLhp;Ek;$Mo0XRh|H?f%?f_zMpoJ^J4FzyGFQ z24lZ+&G&c_j5A|02iAjsUa8ch`Vc?WnCJ&1mezSA84))=i**b%WbuF)BR?beu^nSS z-=Wf^pn`-XK?Xkml$wCMv=QCvjM|814TL318`qw1Z0Q>t4<9__)wrv>JNn!(cYN)Y zCI=ak9dmo$7-CB4kvEcBYwxt;n7c&S6kAj~ZWbem%`M6eYX2MSs zbwD(8XEz#XcqNnvxQ~vtxf73Zvn>4h;r`L_Ayc-kEuM?x{tyqQ+C!f~O9RhxxI5s| zY*NbFpguIe#aZbHH`)?mdj$zP*J;%oEMYcmT9> z9%kkT90;gVZb%ZdqMs_JC&o%b*bHoptVwtJTFyn&s!Ad*HnL7+w!m?&s@$2_W4x+; z(tV6ZV!Rp z6DH#(YnuU9!JZ~X{)(f8xM*xPCkG~FJNtb4Nx&+>s_>(+)=t%D#>7JUNLHY1sh;wL zFzED=3)+=PR`t8qob{12dI5vvma=R?@3at~(vxoSI>p6vz~`Xp@$Sl!1P=^hh%tcFs|LhH<)D zn+isVM24ztrpUNimwJse9HC>P9-H%E*qG*rbjAJ~tD>`7`X9eQPF*$T_j+G*J!b_Q*p* zm8+TZ9l zaK<2X2&rY63`GbF3qFX@Frg3P1UW0x8?h2{Zl8T51*Is-5`t|F60YSqq zm$(Z-0W;z^at3}2kQmK?F>wH9EIH%!iDo17W|zwItUc3Ao3OLc{}u+i-*5}mD@SV5ed zBvJD*=Tk=cxtN-2t6G^8GbL?@uX#{FTU#oT&9%)_7H8M5TwA|R*`2PPFh*r<%|9OF z4WQ6RQ%8FoG-FcnL2+VocXoHa|NZZO=R4o|`+xucdhfmW?tO6o^@B(I2am66IenLh zJOL~eD@0i9XP=v6+8T>{=))_fY{kL%EkV#tC_O}adcbBy+?jg#5CRfkd6nB$>j(P> zocFF?Wp^Srx?0zx%WL{v1$~E_uC(?a*CA^Xbs#)hom%rlC?aP;A^+J5;*F-Z$R{~G zu4VR^lLfXSXB;M#f)f>-MRX8IrD+94V4&%v-f~4bC^TiXtB!_GYF~*|&&@@uhg`fM z^CwDpDB>E`bceN#$1Pnwq;@iR zc)vW*I}dWb$an@lkPDIJK^s=_ZE^OB{Oo5xOSSpaKmFbx{^1|;*yN2{H@VK~PN(R3 zF{v4L$s%1NRbBL`jMgHQW>*kpgvw|01+p}w&_6pukFSf4wx(o&h#=cP#zujRYq!PA zW2)Mgl2caNG3K9Lw+g*M3fA$W(c9R%- znR2QCR0G0{b=Xs6lkFncC_9bFwVo;>d0SHpz8GN7k6Mxvy%>}shksmLhq0?TaIF^s z-g8G*P$?A1ntIE<7PW02&ZVAHPGa+#oB#my8qV38Y^v@RWJ}UBneV;#{&&CoCrsg( zlWuQr@Cl0Ve)oHy{NyLMw%M518cC`K4w6+(0h0ik6s1ma_dst7YCB=6LV#c5dL=aoB|r3Kwbk ztr2+V=z1yjTrsg~NG9-+K$XT-Ep!;se*|QR%c^Qx#>py0;sUdX4pEMf!7`p3F^jMb zo>C`L1*s+$ARe7V8J!bATGr^XHZ_puU?taXZR+0t_W2jS@cF;-H~z-|W5+kY`Hh!9 z@$y%Gv$5$&sdt9|;IoY4p6HIu#W- z3&l3m+qim_ce>6W?eCMwN}vyxU`GMI4Palb(+iobQ~BDm)_(vvvzdwDG|gY_VSx3X zB-%wdw&D>TBG(YvWC6_q83HHW@O83^oeIc>MlMqHxfClPpINbN#{wIl27!#ZInR$W zH`JSiJm^cE*48q#Q@zc}w2>J)TLQ^}%<$Wyk?vOVny2x}`)_>XcmBaY_(z}r{9D)d zuJKus{r&yF_xJwZum0+<{OYg%McS{VSl5U$7BF_*FYucPO4jpq_N@qN22;PoL+xV4 zI-zdCTsdYn{^(u9&tOyW=Us4zkTN&_UhwEU8vc0PL z1yvcbu*h=h^TieP5sfUbV>8hr?`)QqFjpL};W>l44KmM7a65*M5!bWyeX_Q6Q| z*{Y3ZMgBQ!+E(*A)h~m@fa^JAxT!G>5&?5~2s0B9=+X=cavdYzD#H=ItIsN>WUT1$ zqHr_KK%mw18JZC0_D^YMz1^@}iF5kwK<$9M84ut~iu8>&OxlRH!+vN+Hof+pJkaE=nXNZ6wzMWy8%I zq(_sJQLwZOZKyT5%hgh>Fywmn9Jr{2sPm${ReH9nKV3gtV>giP?OjG)zwiscaQn_3 zZv5lVci*}D&fU9y!K3HXPW2pSgB(hLKQkROA|?2>d*NhMQ3r5r|~Giwsqi| zo-kxWxU)?P0I$6AGM_QHe*GF(SzTgzkn@=9>!j_%?Hv^^FAJkIbtO|DG~RI86HkxTp zU!ly1mlW{&)VOywLI%*>y4Ewa*>~Q1XMg{I+Zo){VCVZ+UVVkney|fPkBPEPi*0)W z7w*^iRAp!fHQXF%)!)8ap+*Xs9jCk|NU3QadBVphzy0lRvjfvFefbx6cXogO_y6O& z@4oxBul@F?KmF-H_ve1*rI%h}@&-7328c6MRn)vx@Ja-kMLxrCdIOVUS=x8@?8@og zyFdPC|MZ{!+kf}(zW2TFazla7QT)!=zV_xDZ+`hJU)fmSuzNS+@eWD*PbmguTihmH zseW`F*m|tV=hqWkgq&qi{q)=+#H=dv7W@#-DJ;kOG7*GJ1!R{7d=Tf z06&@RCbVTbWGMAkFB^cCfpT4roeFE~eT%K8y@QWHKlBg`x- zQgaBVulW$=V0u#ug9Hm_XyHpAuHIVP+TGn|Lio~4cm9h%|L6bHpM3Y-d+&Vh-~Z12 z5AMJ7?z^nWzfmR0E^GEGKL;1yuvz?CWPC& zJ6cxSSpUptKEv$ebDw()P&gR9^3oAm5RVw>6}2E3@K{?lTxIm$WP?$yJRn7{B}rGk zI;e>#17^3_!QG;^sudy?ZSgCaV@Z*5RTVR0X4$C>bKC=Uci|m%IBQ$$+y!UoqctU) z4W8f%)^<2+bk0^Fr>ypHWn{fc1**qLRp45N#ha|AQlLyPa5D)k8l}FgP@)#ige$A$-k2m_RghHcqm$vK|QXi|6UM!&{xlBvl#{fbCG6r+k;E|Ltdf=4bEUfA8ka8|U29QEh=Yl`TIy z(rgV)watyM7dEX~l?M_vKHA&y`Q3N!{j-1en}77jfBd5#{fO$ri;Lg<=C|H||NfW1 z@?|R05$lLL=oR!sRf4Ng)^xU9o!TR(C(1U*LaLmNW4l=?HD)$gO3Ud@Vwby#4dXWy zm-CBySwl$}*K$yYakh?xjFD5vnKOMYFv*ILNtzn)yfmtW)hk4}HBnYdq}oEmpC*0u zaQ5%`=#V9D?xk`u-QC$f)!PNO9j>Zbc1){;rwvt-f+G`~?b!nr*j7LraucSB&G4Z{ z(ZW5EP}Vm3e>uqxl>s}&5gfY=xZV?WJ_BlHkeIWU0iMGnX;iK*sXFskavSPWtti&Q zdlfG#I-R=N3$zw%+t7+fEi)G>)DRm*90Ob6RA5H!LRKqEA&oO+g~E0?rAIAn=^|>JuK~S|hC|_mZ&+hYA3(I>EyMMI($8~p8M|-pUEBNO7r*e?&wS=f zU-}ZmK z|HhkdLdlIO76~~}O1G!%QCM*q3W0c3%L3c2*fF_vGGU8l<&|Z0N|(=2RR0Y=^AS~c zH?h&c#W*f$IY2mT8xv!=!H^EFSWY1j6m!`zlU)7TJfD|)>5%1Lq z;=0Mi1~HP>#$ft8tLWV=J*dG1mr>;+%bior9Yh?_yUyc9(7M{{k4m1y8jeDZD%4@! zL*e1-hmRiX!3yqJ=-Q|YW*^?Ua(awfjc$d;tW0l#RFhq+oK>ZFohhA6Mj&{vWy)35 zDFaQ*>WS(f>@Wpdgr~qsQ2jXg?=Z|DTP=Gs2HE{r~q|M^W4Cl9VeSi`OE;C4h z+LfS?;pJTGQTcgLC zgLR25L8pgo5k3wfyHM#uZLLM)KW2qnn>%aQukZ{Q9x$)#q>W#wUDmGhWx!p;^@BW? z8%16eQ>u>UY{JRuR#t?mxg%#D<7`SvbC;fUOnRx}DczJz z(!Dl*jH-<~Q=T)l!qG-V3~zi|<(({ZX-bK~x~Qy0%btAIzC_skq^hENst0PUtu9?fL*X^}-QP@*1Ks==u_x$(r#c~=VW(YjXV6`M zB#Ovk@1Q+8-dSgs3#hP4*7^F>>`FHmvqCkRoVie-88G`$nLQb7^`g_+FNF6D5Esr_ z^!gD?Du(A+R)}7deU6CdhsLg7;kstt;KX&#h7$yJt+D@%zesMbfO4a`GErFNl;ZFQ*i8BwaU(Ns#vDW3%;Ydx)ezSU=)ZvIjxXwvjZicWR4hOt+i zlNB4mtwnD&X6KEc5=qD%{oj5(dkiLlS@hyn@=0agfk`_mpK6iE`MS7vQv zB`p)CgIs-rk{^A$LV=q%Z@u~HPd|M0h{@?|ufM_M@Y>#S|nTOr{ton-B`Afs)uKdt}jO{*#qm zmxeEJuY8jJAT!_A$zPx@`L~}U9I_p`YDi{|fr7GX zq0^6KMZil5Yg>P=ce1+il`4leuEcvxO&HnRLN4Y+J+pN&aS090wYfwDAi1Va4^5)7 zTyk^xf;#jBEtb$XVLrbmLafAYZDY&2_5aKT`Qn3H#1=At_#gk^%m35=_>({RXKviM zVG}3HMGtxE30^BTD36bA4;I@*6sPDyVmf(MmzQAC2PdwaF$H7aVI9QuIZ#5IJ3D%Z z@WF#O-gxt^&wu_aU-^oft{vd;SrIm@(My2v*bN{Oo!zB4R+FfY(UZalBuK$Jr}S@= z@X9a;lUP=pu?0$P8-qeJ946b@-&QQSlf^$53cWoQn{0H&p7>cB)`YqbYB}AsY9;YR zcVL&i6ALm_E^y^PD`FCd)a&4^FcMU1Q6~*)NxLDhB;B-yV>6vOf@=uM_kd|tkjrj* zL~CJ$RUG*kH@_gxTm(F8oITQ4!v(&h`8j9hKo#N1B=*d_T%{a62o1qugA1aW6T)f- zF)U;iIo%qlE_$J3xT4o#=HnNN)#2u)&WDnE3wmIl3VkYX`B_*FK-a7azc-RJC4IB7ve5uW^n8_p z2jvG;nUUOBmsD+{gH5b~ER<}L?brRh67~8;){*oSB61Y}SrKIXt<4?R^bjM8D5gzL zm>ZrhVte#tTh`gKr9CiG3K4U@s`Dwc@+%DBTYSxg2XCXlE&VYwT4zSiG)z0MTsddq z?#`V%9K2@CCv=D&IR$Z#a(x10lsWBIXSYso>dbXGPt~rHVKRUN@(cGE6jFXAiZLH zRBu_AW{DO2oHsvkVKwTadPR`ku0X?+1D>!NiyH9B-%07{)6y*BsFrI|iAx|@q4W&Z z7ERR;p|~^4m01(QD%>SFrGRX;w2aogf^Hz{?DM(iQx=L1$e)!w1(lUreHkD+qqn+x zEy>by01CpP4z(;ZL-00#h}L5^c0>=Pt80^$3D7f=Dg7dvT$tUJN#2Vta*b!7OQWic zRs0K>Un%se+(i?sdR9St%11v;suziYPxPu<_QYqCt@)gC`O#RyV$#Ox8>*^)MgzC8 z8AS)$3~f)|E;4?J*fkdEQFKzi=wex>>E3vvo8QD-$(5)V?QP(n z&M!`qMOqeQ@O^r!y+pP8q|vzcIN_DHtxYzvWHU=vrIMGXRE=cavd)}Kdtjs#RNcLv zW-&7YD=E+!a{45^{>S@+%=GNjpmH`FwX!s}zQNR-g*moeVSaV<=FQu8Ztp*OOc||f z-(V(Beo^2xJM3j;%^Nfe7n)dlV~RzP+BXV#QM(IST0GOoMf?h;Lf4%0^x*M6g!VS6 zQ#X36rbN_E30j8eZMhF6^i-=qK1#rX=n21!G(EJ75%WP-2T9|e0|6qoSa@aI-il)eMn+TB%m><>i z@WMJ61BEXk4fB{j4b8}}<7BixrQ^?+5-A01g+-#$RRm2SN*eH3Bw1zHRB8bmf1+XX zgX?3p#mPqe)8Y^+kUM#oA6cRF2xS$nbxzF=6)s2SnwSta9(S%MbPmONQHc)a4Y7t* z&8f<-Cbp0PXo9lknG;fJTAdy*)&LbW38|H!jgwUsT;WtRX0z+s25Qw>5~pg8k75DH zEjF9kUW=3Ixwa-Nsxm7Uel(J*){YlbmF2R%v6Rq~$>UY)O46=9{w9wP!N~ENL-lOx zmscuPavR2VnFnIyHN0V>-tvcbZM`L{&^AP?#x-ElJhcd`^{G|1*~20D06VFzjEdHh!jl-am%_2P>Gux`lLbG>x_fX z$TjRF!pR#col>p}t|UrRXIza-W6@N^%`kAB1oKhXhm}rAg=KRW`x*>WCjRjm30Crs zc)}88^b{rZJhWyLcyk;O{%TJ_BFpt#|C|XL-E9>l*qeQI=%6!cMSrDam z5vH2iyN=q8Mp@ACe5r$&h^Kbyn;iX|)w+yIlEFQIhH4>VOD zZYYyzRS%T1YH??5&QM4p*Sb*Nqw@r{R909HSm0r;8$9KrpthqmN;;s|R*UuX2F>Q<#(h0^XEknm4Y`@IKC zRWZw&m=I>V7lEhaf!v7|rMUua+9tFDunRz7S^Fn5`=*letY~`yLtn5y0S}~l!Uan; z$tsQ|d8P6*ISW>&g1cjfLoEo{LAay{D*0cF?+$6>AG45+$gQ`*5!&F93$bQ{{Il^5 zV|pIyV_C|_@y}qHwgu+OL^|6F|MXOc;~>1E6LtR#4^qt8OCXz#y%cT7~S&`gq6W*XRL{xc_7NKp4MwIOOdC zQj*7WhxsOQT-!qD5j7^@EZbFe%qo;8=By$bMP!lzoq8uY7aDu?q#k%p(?I0In%gv<|`3#GRRE!Dt!bT!g0nq zltTzlGCi7!&Q>o{UtgGDZ|1bBSy>Q;xO%~8DQAWJrenU!bRkhPDi#2xqvsXc1{wvc zH76!INWnEfigb<(f{ci}Jme9f3uo!0;ajQ?w#Y>INb7^A+nZZd9uBPF(M48@_%Qw1 z8E>(}{kJl|nBW64f)^L3UBB}}s_rb`O(UsJ~QwPPmm>5w(%53A?K z(VniH?;q;vP+yc22SBHKLYN65(?iMG+&n$vRmM}TOlo!}?!q~hDAG=lj3wH@hBd#< zO?I!*cO+QN^8Jd?j8ATWwq@U$p!&hEerhY!r}~s4R`HlJIP44SHAmH}E~TVxdi8F4J(lHKHux3kku&O$t&0Rr zSasBnL7gKMaxLmS1&gxW;f=Q6zy^c>06+jqL_t*iCQ8dF=p{DNyBN#wLUn@)t=TM-Sx6 zN1#@TX8Xh&u+DVKHua;r6Z5|=bd1IDY+)}5KSmy)266Z~n%z%{xwFGV@UsCfm9S}6 zXvs2&HI>DlBSxxqP&hoshK9#OQZ;PK0C8ChF*b)L($5dON{iwzN?+&` z{E(57-*UWQsYc!5&f=Il+ZCNMuoVC08qJ>{5h-0n4WTKdn19LPY^+t8qS{vN?~vYu60Bts~Uyf?Nc+IIvpC8r{zJjuy?1 z`7R_ceIBo$*;1iCBuEKLHzx0z$x(>?v1_dO*3_jT|I|QOh1_JJ^6=XT(S0aR>AyU5|U58Ul4 z?*3uOvc@fHH0cw@6lp8xrz2){!e&;dg-rRa!#*=Ij%gLVqXV#}7<_e~%#^zlvUv_) zMwDAO8$VYSmz7%qu5&N7Nc9P!`E(gY7w3t^Ru=8zXpk~ zS&&iy*-Ssa|AQYqc=+JqhYzmbxc+DV?4MP~l-%d~xHF@kj^9rqy<&nlbLY|GkU!bj z)iKie&5)cjTAjKmD~O@L+m!<=0Cfm?-Bw?9@5bJjLzYjzcv6=}xv7UkbxY&y#P*o6 z^*U>wVq^-)*$YR`!7n=N+H8lb?cw5Vj9MM`k6y5ojUE!_8xOzx&Hq3JxXQ76^~R0s z+5i+Pe+o6h7Ep>xvYMEWv%TGEQs{@RLa%L}`N&G8$GjHBnGI|D$iuGa%o~lH+e{s? z8p|wi3*G{AgxIq>%I6A9-qLj#kbndJphZqLAV^%6 zMIaj$p*o2JP>~+I%;L5WoikYuv^XY6>FRT2X&Sf(tf+8EmN`CeZ_#45n-EhL38Rw3 zW}TforxPu*7J;8fJ5ZD@b3@Saz{L!2!Hdw!VH@QOIi2VXp@Y?<1C&a$<0vDDDXBo~ z6T&A@-mY=<%MAOwW-cCcaRY*)cS08?M=489HXKp&ko?RBv+}K(*@VWjOygGMQ4>Qa zx+@Y}-XS|1s0P7UqT8K-v;SF51KR$8mz1Vk)bLG557Sc9X&-dL1Dog5 z5?NF4I!)di004JDh`$szkH~-)K(d!)1JKQGV;Q+kDyw(QSs)yolU2!q1#p%lEs8!U zGl3Tw=W|Vf4rc3pF*0rFO+X7lyutRJ?|kP+Kl;%-ckjOT+G}sU^@W|C9cqawlWJ78 z)S~8D;D8CaAoL7j(VaECnTGt%5>3+}8^Yy(5#drsT1w8!fk}B;^9dH50|m`TpN`l5 zDPlA?p0j|4)vS`4-&H7N+14_eJmP;lMDE%^=Hub!fnOz8OQcEH(jo_IbOg4y^(raf zc=*=ue)IU~_zOSr#alOTUb}vc@7%L5sOTmRseuCYIoM#xq!5&vwzRA@Pr}RuIWh6G zIWSXAtpVvH^m=1)gB?}L;wy~wEt?zLVs1)kLVhp1sCXV7a!8N6OmM~mvn1-pdtA9< zK$;4!$26D#gYf_&#|pxF>`M2WFkDH zkoR5)E3^!Ikr~NMhcMu=*d9n-~?%Y^L>qR+(refl0Ni z1lheS(IJAgK8UIF!kIVgoC6~OnX69YML)kRnyOO5Mg$&I@Ec-?Y??!mZV*ZJB0IFG zz+l0Gvdp%471=aP>IQrT=U}>^`IOP*AWkqD1Z})0a`20vGxAKRMd`C4&e)ujwA8Db z_Y36al9}oQm#vL0Zlf}6^8OeEIYU2IPA;ebaWPaX?)7qMJZJZ9p2j&nBAuPy0f63w zb8+N>Al)4G12o>eXSZ%{(egqC(G#we5~O^GHPJb!Krlz@M9L1`B9ZwZeMzU>+C0}dYtfEF`2EYZ!xO?$+QZjTAI?Tl);1!t4Cs~ zWp?Uz9=;SjRf8?j3Qk*hXh`bs!sYZiAXz%ZRVf9?qR}~+P7^g*RLTH-dSsN(pyP1b z1*=1k_xHd4^{@ZIfBb{H@812=7r*q&zw*oL>$hxEnNFV*3#b5}v?v>*0=2r55i)P4 z@hqZ6N~X-bGXs%P-NQsDX%vFS{8;_7w1}s~rguqQai;4BY@o4bA9$2+ZN)E{xwXkB zA=zM4Rn9heA&j$<0!?#-DXXXA6NnXz$to4I#C$@c{tPp19R%~i!6C<^4?o=9-rCvL zluUCxJDfA)7wBFeOb4d9=;RmN7llMl!aDDBI0g`DgN& zqzpP|J0?-3@idW93{g%h=J-SGx2z{IMy|BK0V*v-Qk-gr)&&sOU;3O0qe49 z`LdW(ia@|Gaujuu1x?h0Ec1g#G8p6#mZV<5>Q738fT%mo&YnJfHsnNR$SzSShNI?V z$a4$Suu^3vl4Xid^dZSPhSr{3xvXLWYV#KeMZ;msn0<(mtPJ9TqCd&G zj_=y`(fTRSN8U!Iw$OXI6$}^GqZC+$x{)#GH&=`vajGOkD}+7zk8#36t+e%%_XBE` zYEaL?;i{I3qI%BAfs&|=8%Z(lF#z|XJAxij_f5Gh`YRUH^5kR&0U-Ew+d!WS&$Z@1 z=b>D{&%WDLO_uYvvt?7419>L7a!+OL$ya2oEiUP)XVS%45`%CAFcPv_v46&Jw&&iY2#-99W@bxzv~HG$gif%Gt%mCqg(`!zN7$IrA&Y z?s->61Q*c8cr_;mhJ`$Vamx~6EEO{0pvibvDEB$ieAw11=7YJ0vrU&VZjJJM<|+I1 zPrB$|a}^ZD$M~@Fmv7u7lne7`-8=UBV0EK)<+JT5uK<2VX98LCm{r!Iiy74yOmxh+{*2cE49;Ruj z6;gkeMio{9sTiv4EqcI+lvRR2+Xictbg8*56YN(?^|frdjl#S|VSNCXN#<)Oyl%lY zlf^sPLUp!ND@-W?jaTZgu${J^hk=EKYf5KjKQW02$aKEjMSYh~%<1JNB zdGjc&o`xk)3lcgS1gC6b;Wt6Flp*hV5878HFk)xtD&z5kgMD`SzkU1Wum83G_P_q} zFTVfoy<0?XzqG-p2yj~PQ1Dyp0?V)Fv^38`$W=6LsSU0XX}Kn}I#v_g?0U#C)ma#O z2^+j4y#PERQuZrU8evbnLtCrVT`Y-^rA>~oZoJz%xCQ6tw64HC))#l#P5}SK z3gL>KT)WiTT*~Fp*zAJ1VyP2w-aE&$7`)xu%U7yx4GmSYfIJ))#w(1`H0ifpQwM5YxLQPS6+U3Z|@rK zkX_xo%8WqkQ@IoTNO|Hi>HwH>m-%TLzooDn6~@J7M^iOWbOF$XLO*ulS)KQR)P+eW z{n%-JWAlyI-(ae7>(IH|QHj?nn z=jZHZG{#(_-h~MFB8jTtLt@R}Hacx6)IaKR3H=Ge9N%QV_+7{YgKjsJTrN4V5co=n zlZNdIp;d@+y;mZ*1ES^~jsYDCs=%&rEpsqJt%oZ% zE_V2UFZZK(l2C5}Jm0h@4JREgh=Gf%$Zr_mw8nuoSW7LKqW7{GN$JQvHLzp=?5IbA zULL@}NcX7e*UMw`yd}95NKX{yyo@onYn8~f!>{I?bvVOx+&WB`8Y{V0h+FmC)bXH* zcS`h2baCtw-kPGWhBw7eyTH{%+QM4C z1#x$O@4x%)-{w;F#^X2i;f7cyVmZCg(k4p?Unt}n$);5cF&~X4O_#9ni%g?CBZPXS?~FT(~|hqXHX>zO=3; z<4Scy_0KO)O~vGDAZ2aE-SgswmV1fB5*v(0mV1qjdg~&VNSF$H4NFu)nzpVl{x`u@ zV_MLj?FDv%@w&iaKu`5m;xK0!HOHnKXXm`?|AJRddT`2D0l0+nt$Mx%X5~}m^bB*L zm)cDkSruRJ?}WA2Qr1z3B|JGwmPQLSt}BQIv;q-Ht-a1#T1T}xSRzr#*7@sq6pdDR0D9B<;GO)ZuV+bmA)9in&G)pWG~hYW2Vn?h7NeU02-kf zQYb$s0qnNDdsYvxm>919+nf4lhyM`ab6!20g`;}8(y%j2Xr^anHaQ&F@X< zj`He0OE>5TMz$XQjO`H%DS8KnOo0a03RJ6sqgwVm z)q3aal~#3UUvj$Qhrdf`;L*HVKO*l%QcydZajVF|??R;muI1TcAuEL9xvm0RuK(I8 z!-aLCd<|BN)!AjGSp!xSYKRJ7!oGk6O`s+c=*l+av~gT)@DK%;WWB~qZ^GW>y$Zf{ zQ+0TA!ydIi-P=3RByDJT&i5oU4W!FszQ>CWVYH{0P^bQ!Cw4&TiGZ^+bp8D2zreuV zectZ22Z8F~g^VG2Zr20IFheuUlBw8U4dkl z|0!bC$izCCj0n5&lXNcx5ss`I$)FDj)Anb_BN{#Oc_HvK0I=bcrstG8o0ym)@&;P| zVug@JlK#eC*W1>J`jOIXXV1J*W>V&evIC6NIm$&9QzNS2Cg=x7_AGJs3C7M6$X3#H z)N$>|pk5jqXd(iQ#15==ws5&;d{5-snG(RY`KB4NFF8HMu?|f{U{aVPa(!r$E>YTK zTo>3qLue|?TJyS6UMzj7q}7O=wf?;#8o$Cj{VK{VwI^r-(QF}NT&2iqM5t6fLQ5pT zED==&G=A-=32EW=dMDZIA|MP$T&mX-1K93IaL10^cRISsY&7#|^N7{rCxilwfoY6aj>f7~d#_A5mLH*ZS!mF1f8!**oFUWz`2{VF~o4N2v&rW93c zsQt=5tt|kv*5`aVh( z)rXt?sv%&rI9yIBS`}#K71}Lq7ba-g9mBnV(~~2$1)gW%2T{yMcDMBP3llDsYax*p zIX2r5K^$uu7JSbs;f{n~Gg=2URxWuLoLId#%fShtOI{L<%x=4q0BFxJqXqUTot&Og z5)4tegRHy9`@1|%0)@xFUt3(~<H_Yg_PUC}G}tX0iRW^Yvkib6&@qO zM{Z|l>-dCzM|)|EvYedFGhjL-2;Lk+m6Y>5r30fTgxr1QV$AD9(ZW04-+%x8pa0?) zUwZqC_wL;*)z^e@vf3!|meRfjW%U2{`20nHNVZF5MQ;@=$xkJao;tm9dy4s&ORAGAQYws1hj!}gL`yJ2}c-O=}H=u{z)rCps)AiKxh=bnHYgchy z&G}!p!*!01N;%BwXcneI=|cJ!MdlwfM>__y=Aa&sy$cuHn`fJ+=iK_TB_zEAdCMLw z6n<>#OXaNBoOMka`L(Wqaa7t6ZpOyGWqd znm|MUH5n%HSzSZZG{|kxUR;KM+EL9YOs$oJ26^fTR*)Dg%8_Y7eGKeC>lAo;+p3Mvd_aq0a|LEm>~U zw5WX?^c&oT-P_w?Hpz^T1gKwr{1L-1wv6()&clZf-+27^(WBSy-n+*~4p}yYQ?pT| zsD+k)F%TmW>E|1uTWv2o0@MjFeov@|@ZU44FSo}y0<=MZ*2`OKQiHNTkwzTHe4j4} zotW)?QfFqJmbNlj)U30y)8{K9H|T9NRn<+ao*K_vV^$7W8GBlqthSk`t~xE2g@mhE zDIrxm29U9*)9V--2(5;b_cB+}bjy3`2w1tRz%`iAW}^+>Mex>Jya9OQ(3&^B2=TCY=uv85 z(g;5UH0iTa7+gDN%APy2YWG|04n3ky^7if1fGff$|QIA zrXoi-QB9dI53oMHplegDex>ii$jB z5l__*5^L6_34}>jH_@<86*XF9+8!9cM#u(kM}pa6iCAUN3ocds7!&qKBD7bZ15*Ga zf=c)b{(6L)hBw1Lj75`k1Z;5WHRo*I=8bmRB}CC|@>25conQO=fA6D@e)&g#^v8@J zC?@VTQ%UU2=p&)ifSW5IlicuTYzve#u^rPdfiz2GIb%>FM@Ma@v{9C6^*I&>Z(2g% z)S!%}GdCPVn1UfOqvO)?ACI6$vX=m4)Jhv5YO2en=H9Im)r>)BXR!N)xJjKA1wk+( zi*zJ~`Toq`Dnn}Nfw-^R*A`Bb27{kK;C|#57v2ns;P?4mg-UYCX}e*RInml`9nmvq6UM9Bu$pK{%Ng-Z|Lkxk2`1k=3Rw z_U-UUfcEWR1IqchkUlrGv%z~u85yu~RF3z9s(d=6T8fLu7@3ealXMj&w%4YjIuHW! zt5@hF0>wF~&2jQQ_l2%QuO4y7_G+ZES2#ts*N-_mM?-Fj%IiZ4Q$f5yw>w`3b#-X} zdNx{F>+Kl2rjy~skI7Wd{z9UNy=eugCLC3BpQT@Urg+fZSc)D-i&N^?cDDC;m3=Lb z`dgeQYhfJ3;w1`{8{9H-%I2;TRtj?``W$j&l1e)JI}Qu=P%jnpe}&gWT0mSbytdNi zldzAWss2LC|2b0zE)NQ>vr`?Z2DO@wqSS|mlFtm$)aWS*T@CFXse3MX9DPMSRM5QI z3BuC5++4z&+G3q90H=9i#}=0+Q)n=AzhKJBK7LGp&fl319Lr_%~~by zmB%-o%|I(NcT*utAsPpL3~|O1bTT115+O5Voz2cJvy%{HgR*lL6PwKl#+uN_msuNR z0dJdEuJMfDokw?1PEX%`=dXVL^Y>UC{J{@?@M}N(;hlp6ZV~Zi2-`g1w{?kxg!tL6 z+kWwuW`JZ6xXXV3-QB~(XH5M3Lqd93(VkB9>%XR>dR~0jIv*b4IO7?T?Y(`QNa~3W zJ>(;KrHQ&<17{H^Pc`2y_?o!UU)&Fk*Q3bFYb?sO^dgXmhBnwe^$zbcZHNQ#`fcts z-Ec~Ut{Rsc9l3Dda9uWABh(Ur;;h<)Yg|#7lQH7>m}%qg?E&lrBWB2@#1 zy|TgV(Dld;EpF2fOAN$0wXf8r@8T*%qWwJmuYu8l71{))I_Z|Z`$6Ht+Z<)$Woy0d(pDv}$+uw}1N{qod#Z zz2AF{3r;_|d>tI6JhDML6DzzW>w1fo6__cLVJT@8aLqVEvB%L8 z>`0doge4K0sD4x(o^hu#aB-rbIapLyE!5>n?}{)*C`)e*f*r)!uTfEfNC$0ot`L0M8?{Ynjg1(cq#4!3IPJ$2KFXEV--g`5yYhl2q8J& z-T0|dR_gw*69CIX`Xtwr^RR@vu}~*y!GT#cIQeAEa%0Ms zkSP_;Xc15?Mf}MbUm?4-og3KXaUsw% z@q?MR$=jWN@$+B2|Ne(ho;*1`JUqU0*E+cXw1RiK zzjx;=U;XMHH;8t&xj=Ih-I3-Gl}gp#8=*LA9U0U%vUEETR5LofAPSD@w3Fewaab^h z@M=K&x2kAkUtQ?DU-Q^k*XyvHn#J3U*h$PAYqf28>%Qs@ZriEWJ0n+hikcPzteU0V zXEjME^rtIzZErT|@s)F$tKhwOeWoJeo>@mS?^n5AD^MC8`OJjTqD`R-zs2&i8>{&M zfxW3-h45O3A7S=wqN=s(% zjnHjl>$+4kF6jT?e*3KtK6w9~cmMi(AN}(F{rh~7kU66+1suNf*M}vks5Jnwi$!*< zSH1e>-$P}dl%|0+$yz}3W`rmJ+{CtGfKjqndZG-nj9@2Hq{U-g4mU`x_(;{A6(iGn zxTQ6uv&Ml_Fj|P@pZMt&IqSv0Ob9>z&2_xS9!xdrBN|w&}~;?JZsd zO0E3IzxA6uNX!RdxmWk}=!8!I@c<<2&e49-r;fIO!{?2&Q)G zaTP5Fo}cr9B3l~bQvy6Uz`RgH4?l(PWm-UdFB(XET}7{NI}@Cmt~Q+1W3c{BrI)(rnGnG z0B3nX=akpFpPXoq!^Va^;{(z-II-@>#s^z&<#l6PA1wFvy_uPHX6*41>-NNqce+0N zgFpC#?|%0?zxIP4JbL($aUdf>EMlUV?8*);Pv-$-aJf^R|AClDsQvdSJdG@7p@+CCsrHaqB8WWW~5=H!#OX1>zo0K1X_{(?Sd52jIuN`NKNX^tc zu)3wp|F9q0>~RLced}|zx(L*M@$a4pX?l;9rLOBC!eyLR4+f#9FR8#vp3$hk0OHO zRJR*6;q=O{ra5fQ1>c4lFcbFS1`=XnK%g zzm~-=&KUy{Z-z$<0r(VXSxQx#zTg(G$kcx2?-e2zYTJpMlkECJz9(|o1u9!s38<-2 zrV5uTtk3?SwmPu0vE$!JCwy7LM7jzdp}qb%dXi|wl>nDwO8isFg%!IQ#-_|LbOB50 z$=!xb&*C;?`uq1E5O!bfr!MO;rI22A16b%n<(T`;Hq6nfgNvHu>5*mg{A*ydsvg@U zqDOVr%)FL)EV`Qn5v)jLD?65pjnj)$HiGVL?`g_HZ^oO(x3(VMzkhmq`pGAs@)fJE z{>s(t0p|D3xW z+2XRRP0?}ytN-F7jXQ6@_4eMrKIrxAnZ6y(Yy4Pa*Pfa4^F#K0nU{KYoKc*=F2o{( zXhU@t5_bLD-sM{Q(MKOMB;=vZum9@T-g@)RL#CQ1C*CC zr8nMq zBTzm&fp1o8S1&6oA?a$9_~sFp=o5+LE33n zT0Y>qN|p$qE>;V;5BHaU`IiU#ciw*cZEm;GzwjXnI)Z%PD(nosIgBffUr7*RFP&Ht z>UD%KmnX{XV5GAMO9y5hLsrp-(5$Y>w3497H4W@tTADpct|UpjOitNe^tjIb^Z)6e z9~~b3#&7<{-Me?b^PTUqYv|;d+r3=U_)e}i>`*!NkSlPF0lb|~n>mef-U#3{;q_y} z>?>UwbLS3S8Lu8XeD(}mzQT*|zPkDM z|NgJJUG@G#sWn8w4!`N3Sy>#}?3~sFP4_1fY48ja$cAiixe+^>yuOn00`j*#4+Zot zbOq@ak0=31ayHCmvqzL>w70Wo9oG(nx6_kTyTzggg?3`$GHs6PD%JGrNK>lZN+Tnu z4~D_S$0k6CRF7qzYF@p$`AjdE+2Fd;~Ucz2iEv|1aaKhWEsH_G?rr-Z!3aD{Cg5TIk= zOYFsqG_4t^CUCixUYSEkB(el(C_8RR?A+b{{`bGnFYg(50`5`hTBob8_RMe@vpdR} z`;zH_VV9J$n9bD%`Bk(VufoE|IcV05oOXBjzwwQ4a37slGVq!xMk+X&VWL@rGrUc8A;c4Q4?fHAPO=Qw@@P0q8whd!OaR1h&y%1MA(Sx?2X9vAwgT%^ z)?;M7*ONBG28OIBIlH_b=LB`cYK&iQXb4%zVqU0ErW6P@b(KE`tkl~(2Y0yKR5x`C z!~RN%4~%rZ3zZIdEqevlmbmMK$+H>bBH-7ju_r0yH*E+kJULCaPao}6i9iilTyX|6 z{eYOcA60^ON%u}IVBLzU!h8oxz1(Dz9_-?b@9U(;1yRBuBN*LgfXA$}4Uo_R^4`N0 zPM)noPGU2{JnBXfR%rt-T@bi5=^iO(wq*yw>2QT1#Ec?bF?xR(H&s|s>2233^R>Z=92*&^R+#4H)qr}1SL?8(m;r3o52YI1D1eCBFk?9R|*KG zE-{=Cc9`3gM?m%eQb)9O5|w{*7= zxJm@5dF+YV+}?b2aNsvVRZ-RBtEyUkR8<9{?91B7t`dQ@dG&!ty;ARN^Ng|ey?gig zwkNSEQy^ZrK(t)Oi&cFXQ=J|2idMZ}Plu}B`jT%srX$b~ zr*ay#*V42xyv+t}EKdnDR`S1Tk++zaGXgQpUE4ejNYntifyLQx+gd$PhGwb_qgqnd zHu&=m&3TNRnqtjXTSv4XvbJIC_H3aH;DW>X{JfHUOeTmfOMYpp0^CB&QQx6at#@ET zRqFPy>(ZI)owyYY_3D<<6_7iCgoVRGL4PJCwRNc_$tpz&TH{Z;O}d>VUP}S`YAus( zKF%s*x!h`=&3B?*U)#o8=o%5Q^045Jn7UFMp`HhzObAs?79U2t2K`uYsEfpyW&l}9 zX-!2;D=Cv?HGoWSS;+_*TdSx1)H2pP>V3X2Y(4gyruOfB)2rx<%sMprLa3`xN?tbcDPiV8B6IoixRw^?~_kH+1uT}cmE!BjJj&$ zsuHS!OPw=j^8gEMKrqM}(;BBMvS&>uYHV*NxqLd?+0?<+1?e_aRh!=3*{5XnvLn67 zjLmL(YrejL#H$k-^s6;@)d9wk*@Ir*-UesxJhM9u(i;SYqbqb|YZK}7Gu#Z7x~9c$ zho5H%!gA5D49KT*Hn;ZZ$%-Eup7Bx>%~0s*p+4U@JUXTu(cTcQ45i}{86^5@1vka? zHcd726FzgVFqMW3cad7OlFL|0NZm~#YW_H>qa;O)offODGC=jx-KTLbgh$oS{ zDR!=sg#{qFCW*~lv=kNtG3NM~uR);8E|B1jVrQ2(1!^`|bVwS12v8VuC}@MaWNdHQ zwe{l8{sB~MNktss-EB2hJ^{k{+3_I-4ryoiT*@N@h3l=BRF<9%#o3Cx?5~L-KwU5| zE2;2#Fl#AXQW!!!?8fgkfn*7bSzCf*l#8^$<<2gmUPg4gLbxhUm#$Y)`MuCr>3IQo zS;&7kl%`KL)vYJ5yRxTiqE7PyY`j(ZIgWr20SJIDf~t+GhL$ds_H!JYN!Jv)e?5|uf?hgAbV4MmC+i#OQo1ueo<>a^+e}BejedF2>l^cV;_$eZ zYYIbG72+9R9Q1>$ju!e=0II(ZuE^;yLE%L?O>&EQZV|Y0QK+-h%ZIG|y=+w-B-cNS zPD-vVd5B zUbUCsP^W}~S=AU<&2+zxx1bSHrh4@%YZ^?C%A#C3ydXO6)v2sDr&n`+4k$bz`TXLY zUSmSIeCE2`t!t6ljdMndU49TW)dd!A2&@g!CB69SdpR2hT-m!pc#INHbz5nRmosyh z=zQzx;Zy4*HIC%w5VzRuK9-^pq9Wk}EmS33jvCmI^@4Jm0p(bTaSc>uNw07^JL_&V z{hSG7)A^u}M3ZbD*t7(e37e&AP?R(BFKL*yO1`u1l5HC#Hz}3Vl7e6LOSPI&z80$U zYb~~g#4~Yl)Kn=<$%kGY7t8*qCj?}7YwzjNvv=Nk=PO_N%Il9F9xF^2yL$FUmwf1; z&NdWwf81tD$0~RTj07BRR#XzpO3=7E1yD3L3K6^hWqIP|aCS$8OoKQ2?M#43=VB7* z5o4PfFS|c_dZZ0!=V!XdZr1?QGj}qomT_Y0WHhXwlj_Wpu!?GJ{JbR@+gZO#36$fa zH1;g7lTszKxML;7SBq8qCNo|*WRuuBQ>Zw!0+tuGY;W<30kn)+}2x0S@+XthV({F>Dt)1 zbLTFl?<-?0IJ@?`RH*E^Z6+nodcg}PF)cd6u$9zKX_c@o+(u0pqv!pQ=JaG|B)ba{ zOsDj$pTj(h+lIa{n=fZiGJGTAy7ZmcmOTubgv5PLIFd3(^VO-H)`E-OCDvRU4jm3jkw zR2q5CZhTMh7~$qQJpx^^g;>3pc9e_e2Grk*y!i;I;6}Ck@-4`u>EvQ>-2P&{RX?>R z>+8$edqdUS+uCUbp@8VY5(e6uPH{m$Iyz?Kx8g-0!P1sXWl%69W9gUd6qVL3owlx7 z8=F9rQRFIA5Gfp`tJ5-Jc_F*^i>nlyxZBh62zbC_;I5S$ud;l~n`;h#+<{c=eT`Pd zv*0KNx`6VQWSt1mK*pgph-v7CZG4=?%b#LcQlzE$6)bv>L`ZA^#eE#1*W zb>KBq)_%OtDV(NMA<5d;4Uw_0Y08a}XXCR|^p(v0z-p@$Uo73+;%$4puIk_}um0w1 zZx}?7wMfV>aE+h@?a9biXGI1d0SpSiBPIo9p|4ch>v?uN(%D&y0FK>kyGxO4Dq61v zg(KB$u>;I5XCJ}lY+{#PgRDRkhiV+FUKe^{y&i1UVWbxu$EPRUHRj#q+U%z%2%BzB z$oa9k{q`h-8ds8_ z&*fmiTD}=!RVlNiv~;nggkJ-oTNL8F+BgtOw^OyLjHAl4m*i|-Vz=fZE_K`K&_lR2VNg^0;{+tux&2`b$n92|lMDAY$HP$a@) zTt7e2Fpm!oK?y$t53-C;=-F5j)D44;E%rM=sa0ifywXz^;z)6L7iZNE9Q{vci4)A9 z!=8CEu+g7n%Dy^VaZ8~vcYF4PI+vD31u6qc;~_;wuYOfSUngy-Ga=ZlVm4Zhz z%d9&wRF0j9zL`zKoH$TfW}wDEL-nN_$mDbe084RmddK>?A;HCP4wFoXfft>M7PmPI zG43giJ<$U$7gd6w5uTF1Zro#QWO|{<_!Jhv1FOV^4mYFdHA$9MdQJfe002M$Nkl zDS&m8aFYa?L2+ccIqk$6FNu%u>G8oB-FZQP^4#}j1XW~Vk1*P!H#~}|5@ST1i$S$c zIw}I|U4#n;Rk1EN0v-v6>s(bh#PtT5vVBnnAggM5)oqil7$sxO^kWF^2g z3>Y2QKf_gs#nvU=s!pbh$^=GFcmvGg)bM*3lW`n3HOH};owWHfvh!JJ+ff+|5$Yq|rXh(GHXMQglJ6#`+cVVpnMK4P+!55&+Gh2PkE|wbV8v$fy>E8fW*y4q!)b z(wrt$ln_@qGP|WYg1eFWAu8&NlQRvy)ju<{w~!meBkdJad6l>Nsze4FT)=#oJzdnu)cg3fhbrCS4W)FMj%EibRrRCa9E9>F=;^FWf5DOr`u4k)YYQ;TkBiLnp0Ud zaAzv4^OpCd48$bdAS?-!?XUcu(ZW){>~giax2rGYa`UpXC%OQ&%&j4Eq6Syxp8M?! zS%VUb__GCfxF{edZ`<4#=Nhl4H@dzH=VszG)~N9{{94gx51yK}*8gQEVwiR6{f>Hr zN430$w@RvT&uh3*`<@7qvnU&unF0VEDqIMfs$u*qbDTMJDC@$yP>4~|L~giB?J6q0 z$dYNE)!7Kd==8NxNJ`l6LbPv@($V9EV5=I*@Iw%1hra6V$g1Q@zQ%9yxpIU|5}-Ug zrU?{$o>q75hnp8!QFH<-QK23FgpU;woMOx}E5X!nr^vW8*>*G7BZ@_=MA&O(v`$Fa z2|r&V(HEX`sguQui>--PXQ@)1A+`pva3aMzocvB5tU!16a6shJSwC} zWs>kC5*bTcoH}QGH6)W3+cLQL$6JEN{RU7&?uV=_?=&&w6nuwr?Vg7X*SG(wSX#VVJOHx)N)Bn18X4H(ut@R zPSJ-AVQ`O<(9w=eFDGaQQ$7ZC0HaV`Z_yr2rf-83AqlaeDF{6B8I`L<&`%`VE;wMa%ms1I^f{Y(2QSWJYhVngROB(5UUd9(c( zG!WRH5MG1fmlWX_fE-_|5?xES>QO7nf~I&0J6bkeLdVmBOc;}m)u*hV9 zrROW8V@p))mA%Q*1dg-~0Rx*PMCDv6m#F&uyJ9QC8G%Gx3UNUKL0^=AKS|jn9@%glg8S-A;9*aMlg-o2nqx3r4)`G23xs$japOERODg?Jqb3Ok7CK#ap~*Sma(d3= zhz;1YQ04jWaL&n5a`mDm^5I#xvUwkw|g@LW-54cdh+DSlLH>uKj3xV)?iIHZjy2P zml&hPL9112u$#O*dW&Z)@7!g*f1kwJ`H`<2YSgXa_fVwqPR3~xQN_#<3?wi2sLC$A z(%4Ji-F{U%3DU`5K9V9fPb58i*D_P{!}Jz*Pl6k9rVViG2> z64N?z+`zkxPL7G}vQR8>#H9pfkH2Na05))BkX>N|%1rT?tFL8<4{Q5mz1)xoqz^pY zLksq@u)A2vTcWpT#H!39?^g-A3A{z`O741W8>KL9?v?bJ*nxy`if^}q{5BteVg#x8ym3&KG<7KzWgWH8f!3`G+amECRKMCP`o;cI`=UbC z+VuvXPhd%He`{OwK$|vcV*3ht(hIX}x6Y2U&H1#?lXk`g7cdY#1*r7mgAmzHRA;cY zmTg|;ulGgS{Mj^G6H#kMc0I4DXR$%A4xvRw(}j{yx7V}YBi4uY2yQqNc1`JV0>|d` z6#WZ=C({&d3#%3ex2d{(R8F&B?)X@vUg>hU5LFpSQXu?T$bcY(ydnv&f{mLwH)DTQ zw^}-J^^u#m9m}rfdfpUnme&D4V?h)6ye+#9ec@3E8$$y13MJf&m*1C{-@=uJUM_Jr zG9k3?UK5IyR>bzzLM$dWZ!tcQZWxw*T&_2~6S ze$mkt+FvbG21}K+{?2$bg9=Nkm`nUh{B_8=gzEKJEDjAzb1AsQh*(BBCnyKb=nnQ7 znksFEk%}}+C0P1!)r=2|%9-bWSfy+)d0QR)ZSmUVW zA?V3nkQr2xrlKZrf$UNi1NIqb2QoWGLTK@$HlCiI^NAybutix-SGpt)VV+lPH?JuHj#O#_+>|eolr8kg`QM1($_5bjdks?#P?KK(C{yX>CZl@x;6% z*RI|oGb)^`sX?51QEG?>e_qEr1+>(-Y{L?snaD3UZ{E!%W?UD|jZ6sLIFD3x*)xF~ zIpcL=b;mbiTmcx_&~;BUoZ-R@O&_(E<%?|S%b&}IwS)K@4~9uyrBNC(tGJwGwmm?| zUE8t)&9S5UI%#v2EG7JJ9Rv8JC^OZG{X9e2(sQk_Z6KxCh)W`=5)>qH^~>-~dU0?^|>W7tZ9Dc)MLQ}}Cx<3)HkA#`=5R!Jy=C&;`9 zXew{24xkuE5e{rq;99_*VcO<7c?8N?{2ai{2Fkw|r}aQCG3;Kpx0h)`c>n(WFMa7N zJil&B<*>Vio*7xzI2{qLVHxl?ObV7NO)Rs{Rzm$k+>n_m)=gtiHNM9>va^SM4w)Bj zK;K6%*=r8Iskov{XCG9&zU`B_rs^qCP2x$>H-or}v&$_CEh}A~ik0gMIidRIYEh`T zxx~D)=#6lV9Uoei`RxpHDM4_9%@@5?olgogEwn@P zINWnO5>`-t)3or;{Fqoy&};6;n;Kb!IXYrK$cH(2-KcH7)tfsAp6yMYk<~-N;epj4 zUfenyLc&X;*lehLnu2GVW_qBdNpn3wO}_02m*i75+#hN$UQR0PSSLw@XpngiFy^YY zpohsoZ^S(`R#&qJ@JnO|hUI$!18JeuS^OM85n~mu6KLw?hJS339QP*la$W9L=ry=L zA^h?e`69XRo(@;~mLMoF14AObuFsoKN7FN@0|!?Dp>D?6;A^-UxGN>xo~DmOQ?I0F zZRVui0~`_@6Zl93!F4BYEcjP#L0-rDH8zSe5{+<7RL(N;X)s)CV{gh|0>tdTvrCxP zqX`vLAqsITEeTQ10{u#vA^c@aEOFLtb{|*LN56Wb6GE93L!41e)k5n3gcQg+6%yCi zxma8zdg`#@PG_x~?Z7FP6>u3(i^){IOhzaTveeL7vLV}KJ{zCXO1gKwq6zInJB?Oj z662(zCSoe!mlo(+dNvcYTGQ>9(ePXmH|<;rbjd5p1_WVPhKG4$nNA-Bk;v>T(+HUe zyW}-ZS}F6=1n$CRl$IVqZ3lJ^L~)QXV|rI1-~gRwp{Z5QLh@@S!wKer{>Y!b-52X| zf*UVegPSiCMcK9or%HIh+r*Mpm!qRYRteE{#)rNIv<hNU#O9rXFZ5SVE}#FIDc>cg0)r^h_n%5kx|ha1NyCjfW%_PKxY>8GEboSb^onDC`c z^KQcX@Nq(Q2M)?ebhv&Q&lFRWH_N%MG>T?QZEWEfGPI-?sM-jJ+DdIK0IcZ%&JKpm zCeY^;1Fkj^Y57doM2&>Y@uNsO*eWhcGuA~60w-K8+s2~&Q zRX7-djXHrc=4hG^eT_sDf3rZ-?IoiAU4JNikrv&ozqis~)(Au!QEk+rm!s&b40d=G z4VqFzE9t9HVQE*^wW$xb)x6U)-T=e*xZS}^x}XSXMOBwi?^IcWVfjUq&InvhxT>nK z=me$Me3Lo)U@eYFT!fp&#ssoH>U zo6R7$Q57RGAh0Y*$9G|Ek=?>>BXGkJc##vr;+pO*S!-Yp!Y^Hm&Yj=3Z%5bY@q-Kt zr^_+@?SxLG5D4*d`Ux?s!EX7yqD-^U)2!HqkA!qOJDGOl)%TK}ot+?rHx$~?h)cCZ z`Uoit@25MNeRv}WX{Wb=AAlrz9r^+LHDt|qtDhrbBg48lj8Hx1DVf352}*npOf=wz zhS`NVHJ-vQR%agwN#buLxm=z5<@DJUUX=Oz*CVr_P z*hzt$9`m5z>652V5AGcBB%wUuixHdZli5T%RY}GnvPy8y{!^A+d`Iopj-I|eJJClw z_`(IRZ)9k*5%S^Y72YW1SCxanIJX3vT!PT1Vrg(r!oW`(R9Lwf+t zWu5r1AAQb*iJ`1h(9Fuqnc(u_b!nFqGQ}0bE5T4n@~)EH%x|(pHmuZJJ|ROD^fK^s};s;!!K*wvV`NbQLAmYfZ&1HICgI_#jqIV?|-i~UoS z4tM%s9}ti04D{Y2<{7h`u@JBm&8p+ej&x8U{lwKdRH`746jCrZ@|h@`D&DIK*RGAsZo}V@n6D9IEsPd zZR2&OOT2u=@eVAswP&EZ8f^i;+&`*KW-sk)^X&YBK`^faWjbfiNea8hVFg-xNG=5o zrhQ+ihTR)Ho5i_&Zj?yct(Y!SO66maR~5ldH&rE3`hx*kmM~1G4x~echgEWidLb$E zQMSxD7LMG!&5HBL7KfC`xiPY2O!z_~xJ0S0CbFN!Klx zV*6@JRfE1C@DhQl(6a*|$T6gHyz0NDy%#t7!Fknv`<&Px-At;bkqqSLBp98W*W!5y ziGX$rZc|Gl@B$`;bwFz7m5bX%AFlGHXmu&kZS5%T+7bFSW@hs41oMHIVV`GQA8XaL z8oxgKa*odL9HI_7l%yE=6HUh#r)2haci7#d(6gOtNIaKs2D1sAHfB!#0GGWRpsp`F zS!$PaVZhZ4jspjjD?NhVDi_?nLKBz?f?5$WCf!*n|I!5J$iS$T!pzq53zlsjKYpza zkpcF05U}G+Z)Azy0q1NExl;OzBA^iX<6{t5y1jM6YZs4>kMHrq{hb|Nu3_Un9PrVz zg>Bx@QlO&LA8RX0@;u@Wn?+fFTd0i#3aDw1k2J*~x&J}^;?WD1voLxG96pY%BAc!enB z0dHIfT|!nzda=g7e#?cZEF^%DXmA<`E?MeDUx|2g3}NPNx&-))i;I1Eg3sPQJ2`#- zgAcZMcHaKtm-hGfO5jUTbPLmA$yjpL(EAN~hc<1_z@X2-p2S+9K{>##%sOLJ{*25X zz1PE14ZGUdqp5bx$gCt+`VIL_?OC)Dh&))p`XkdM9O&njqO#amBJCa}$JM~C4!6x8 zL|`H;NnTsb3xGx`UrKlBuLhA>Q|5};UdphJX;3sBqNbeSxs}aId#YV4ela*~E)&8* zF}Q$wP3-y|gL`&(#RAn761G%n8g6j4>d`lZ86R-5VXdXOl7w<~F3@;BprLJ;2Hxz1Yz&=VqPV$<;2N*&`WQeVe5>WTp&Ln?RQp)^^#|N-9ONP@o);ibB3? zmoY(LZ3XxM=yILj(-3s*4w^YzaD@*=q^l)8zS>qJL-@n(Ib*h2QrVp;_5lS z`m;)C?~l1)@Xa5KFq(NXO|+LpZLu4CP!2`3LkkWa*6UXBrB5i?W@M6uo~(tE5KMaM zV)k~oaom@QuvzOub%0oW>sT7G=#V9~gvLqfa1{xhw0=@Iy@(E9{a|LoNAM8D4KPsh zUePVlAjRPkE;{W8$jVSiP&p)p8jJ>2;VL{|*WG=)J{G4;zNZUBE?8(OATyv-fc&a1 zN@toS$lL9Q5J#pMB%Cxz^t)Hf=gupZJnxc=-+$bxTiHt19rS zR)WSZ1NDSDWR@GA%EB=HQ_KFK==tWwEqeaaMFi?uAt7y;7;(w+Ca`k$?5V&(Jlixz zTp}EdEnsH%`1Q!mJf~?&6R`&>*xaKE6j!wF3Usft>Tw);*br`s8I)Y$uOHQc}#r*YC+w;da?EyzsiAiSxGx!-=dWXgdV z8I{wEv}A_9VXkGMt0^;R##mwl5Q3oTD^$+8oUCADg>WnpiW=}jtRG4!;+4RjYzhgg z&N3I`*Ofqz6QwqY4@->QbsNN?!8L7FkaPwZs94)#98f}DU88~d^$Ng<%3weXLMHQv zf&wtLIh`ZylwoXW>@j!feoE%)%3uQ;L;0jGsDz5cDXSs+m5n|ZG@`hclo8?6EOqHF1VaM!XuM9ECw7OpV0>I-MOpR?%ViS zQxhHTkLpoj;K%wLH@_C28zA>dL5ezRh=zn)dW;Z}@M-OogHfZ{YRw!}B$p&*uCd&y zC2vzEAqq++B~u*ZT+Zr{ny$Zv&ZKBo%7k=ldyhAt;xgu|3uT}b3N+)j( z8YitqgHX*zY)=FO>+P-W9cF`UOSXQ`rz(nzLe|AchW3?vcBP3Om+h2P+3ajEEk`^_ z{yG9FH7#z51zb9n?defQFojP9tbhDaOovcB$zcDY%iC!+q-&Mt2XjzMvbkS6JiXOaQ{b2V8$*v z3y`CyNPylkiB0t|7_db27L5Q3kTZ+ZJ+2yoCQu?rO0%9R!#bKOzk`q(0J921dE_%o zaeu&=DUjOFx<>+5t^olU1zT05J9=@*hIJ&Cnn5AYL?X76)>G1sD#UI{NjOVZl+y!y zn%Ob^SSEG2#XIPo6JAaYag}WbRY{)rZ_}h);Z_@D zRWY}{)hz2%xSb8diuR53GhI;QaLOS)mu4Ng>IcV_5)sC!8kq=^l~>Zah&l8wqE1A$ zv0-gHTrQb27h)UZ4akPEkGvIyQ*cEOBMjOlBC;cDzF;abZk?%_W;c5(Dx!nSNmOM<0Lk z?z`{)`+xrjzy9k#{EgrEO@^d)Q${t7*KAmQTiQkli~Visxj}R8=;-*p_ul*XlaF~u zQM1j>joqC+&TJx0#-Kzh%w`#;XSghuRyVpUGz3)dN~vEL0t*So$DZvF8fO<86JGFJ z?3KOPc=q%u>lw^cIne1>89vFVl_S<|G~QPwpHXFhAegvM1S0v=e6g`x>V)i9NC4>*hY&g*UfO?c;mg>F`xtr$HSO!H~7E zD1Zb6JC(9<*6t``3zoDjb+4EWP^QI06^{M4d^4m_zQfHQ6~lI{ML%{@*80o|4|*KX~}?HR=OK7VCkg zV@0eJWiu%gH#d0}nQF`zS!CYD=AZrff2E<_yLa#4;Lh7`zr{x{&Q96IZL>TXE7$np z;PwSGGI0q=g!SIF605O30tKH3%dj3&8lOTEkdo~E;Qr|bvt`}%Vi^#xnXlU2irvFQ zeX#M_(J^v(DKV5B-YV5CS`k|TXz3E6+6$l~*xF9KIV4~Yu~qZ>x|R8BKN!#p?z%F$P~YJ zTwjA9DfkQmYA?RTHK(N@Ub*VM#JlH4V>25@Y#+S;!Gi~uM)8X#M2nO1P0 zzg)+jn_*W{MCV&>;h!F#Jo)s~zk25{zxpe`qW5ZS9xx8n7}VAW7PRAPhqq^qHx(HM zi3|@JNj`o0?8&ES_=LF^QxX19%^)%l5u@AiE4D`CF4YkVwvk&|RDJdf3L0C4&`OKd zdB1prR^imEOaLKgEd~?M*aXaijMgr-zM<6+ZFtjMfj?7eR+!9AX|b#Wte0|W7(o>x z$Xc6JS+ISVui+gZ9We?=`%U+XDCW;CS84HPSmqZR*;@#iFtKndyhi+hY#qxB*23EN zS$vS$U`kww*8W>BL_vL+*y~XPY=*-Ad6}A{ zyi-(^CfNe4I(Yu<~_wU@jv$wOWDBQ^5u^}1+$EVmlVSD=D{`|ik z9Ur~%#v6|wJ$m!aFJRT_*|}~~=#{)yvuR1hQ`}Y-=TsaE0s+0C?oZa1QK%IZwXNb` zmm0XM0BO!(^kqT07ZZiu3Ra~dYQ>gGx66Ul|5OYsKQ^xd&Mwx+p&xWWO8wQkFaIk0KAiacUz~WhtZDt?1(Bx_YGh-ImB<=$< zYacVpO_K_fv0+07nmh;iO! z4NyZ+&}0d`k^b=H=+FP+FTU{R7qo7qw`OYTiNnWjw)2n?vF_(Dh=r7l+!z6>1x=GG za=qUW#Zm(gMRN0=!Q9Ek$?@s&vrnJ>>~H_}r$7Dam%sGoyLa#2zkd&6#-^IV*%WT2 zCLerX=&4G^+NZ>5XHQ@0c=F_vpZw$}pM3Q3-~ImIefa2+4N`4L91;mx^kV>P2+dk$ zyqtW{>0t^-7%7LCK?IfMA7-U;w#uW1gamn}?+Z|dNBFqAzsJ*A`@8HTRq1ko6LYUb z%^s5iHdL>XkU!!tPl8cp$Xr}76@U8dDKlpmtEsfrE+iXyr@^yMHb8YmmNEl}M#6$? z%s|k~-bfy0+dJSEfNlbO`b^(sKRr2yo2fD?n96B8@!2_Gjr-x0$`a6?_L%i4ULG0? zQ?&#apui%>OtvZVkAtm@WMwLqkUFSJ$qFnlO42&4 z3SM*FbE2()KY99xKlwwxx$@w_gE!xN^LKyecd?tsu3%ULuL_&Lo%f2fQFV3HR z`YA?ZEcZ!%_OqY;tN-<1eel5tzxvf*{qA?a_b>nD|3YQ1=$s>E6_zjGG~Fx7f6DkUJXYUcVQf%8umKP zvfwn>SAtyf5=*kEb?Gzj&8Iu-QrtRV7?2xFo)8AXb4;k;dXDO@t}3jJ#MNcqO89I= z;I?X9S9x;#o6#gY`i`|xN^!L@F;fX$Wk|F2j^9@<;98{ExY*s^xqIi%n{R*d;Nal! z@R0p#jPn>t(|ubHUfpufYpxMnM{(8=2x@iudwcs|`N~(=YRg&&pE_g|%W9m)i)kWf zqROzzFWUw&m!a#Ws|+gu8`xn086r>YnGHy>jK{(ilA-ZqC1nUgq$c^ADe+bndruO3 zR!@%JdFP#H&z|19cVAPh-CY$p6C~yV>=!&ap|+@E_%fRpvgTbJWw51yVz-?=&Y+ya zf##QPp17$^gSo8|2L!P554qu{VHGVmfnUV#GD%@-a<;p3rf**=l$sD~3J96aRnDo` zaIwV3M1`_u;!B;pvw!De@8Yeu-+uh~4VDL?aYcRcBr@yCblXc*zXBHRr}6Fq{yMI+3nFan!n&47fUR%LEPBmoQ}) z=GncMUun|3kR>KPPnxE+O&^<-*K2B1?^RTUz-7jFRKd8ecDtsRp0N{`q>Lona25i; zX4!S$8Ap^!N*o!iFrmLw7c2e3u?jrff%3QVej;&wRKB z8DmYB5%#usNo$0x^_D}8q4`Q7MO24aa{@;ieDKuts6i`GQ~acW0ksB#o7;H7!0`Uv z2Veclul$ey^nW;Elzei+fEbT#q{^dqJ|D7*C)ZM?YMI5*oZ%zFnNJcKmX=<7{q^7b zz2E=kFF*S5mmD8`_~9?_-@6AzuFpss*xj>scF<*%oNRa-p?BK`LaV_z!TJYC2HM(Z zxViD{@Q8hbKmPHL@7}%l5C7pm_|liY_~sYhq?9T4(=&`db>rVy4} zw60}x*&VpC%h-QgkFmK!6(uH%n+Us<33Hhyp_~#|2CLq%CVH@c0Q8fOKVhE1t4%-t z_~W1b{OAAn-~PMzfARkB{P+LegZmG8nobgIL67+$GbjpFlcny!7F7>QT62ps^ea|(lrl#Y#WBegy^ca4WCt`1!u3x~Y?<8M*}Z@7!5(Y7yWHH^ zr2 zD>>;HWD{m#Qj=i<&Y7xr_s-qdUVH5y{rW#*g;0&L3=`&(+} zhaY?hy=G;6BXOIjOZjGouVLdC3-L5K&CM((4KoC{1D7?qQ@r(GKbG-yJ~Kf@azWaZ zoAADUeEzoQkRKJcC+K$N3%Os0;V2LvhfUYA{6qV^`fLgA7 zCFMYJ(s}u0cxR!a;t^H5p(v50du=e(EVm?_@ zS2S-REt>$!3n&AZr<;Z~fx`)5)#2S%yEass6~Er8jauw-$xgXK%XOeHt!O3VIDjiz zCLQV`$t4QGOC^nMm{jo!Av_v`tLh-zMNEzj;^ko(0CNBwwN~`hqNOyXYA~U{!Qqg; zc>|K=*_Dng7gZ?72Cpe%C?nv&%x1h`F5We9_8}2OgD`ShN*b z$;xj-f^20hZewoB(+u3^9UjjPpMCuCC(QZ(@W(%f;*B@-D3=Kq{pe_bMl=%uSTUDF z3C%fG78+LipMJF!&@42%2{{1{FFWck6;zQRUi>lnM?K4Gq;xJ-q1Wt9csfbVUw6qJK?sS+{SmTyzA&V73(KDyjcL+fc z=lwkv!?ZyZb-dt}AEuHdh!W-Jcdwi^iTQ#A=8{I!p5N$KG#a-t6;X$dn zArOBdb!Tf2=HVG|*nU|ei*M9MJ|BwKjSXz%)jPBco(let|MCB%l6?Ey-+J`uwR`vP zY1+5BrG3Q;nFjREB{Q{xsfQp<#z@ht4d^2Oiuf^KpVxM0qV`bYSC2=CXjbaW$Pa5h z5k>|)3mwp8)JvWoxD#%!G<(3{n(GB#MbSL( zphC9Y4947CITB`yAVxK76Ofg8KZHwBQ8)H;)s)Y>tMWlrthwIV%M>hiY?|!XZC{YemZ)G4f3Im7sTqlG8-FA zhMzro_U^my{HOo)|GfDHW`lp1;4rlHMXgS-rNWp2d|@`TdlZ0Rp~LynG*uITOWs__ z@>?b;l$128d9x*FCWL?UH-G(;AOGPuzVVGW-+J?#-}vU8dv}>8Sqb=lOFzS>vA$V| z5HAvWpt*yVI(@z(xS)21m;@d{u30WIt(ysfb2tHsu0S=V(4?7qSS1;0cM9=ZC?jK= zDm`xS>GqB4+9uz?)c#9p=LIcH1*P4mE2{h~c(M;17+b+1WO}UanWyJ`S_Z4QJ>oWX zY8xr9rINq;AW#?jwgjq}pHk(}c<=6=fAUZM3ATOm$tN^D-Yet%%;WRNID$-!=k=bN z3vO*=u^cexFc=T{-CH+sJL&7Id?d+b$CZxXifQWKba%1{ZBSk;VDVUm{Hd<}zy96Nk5p3FKwKR~MMA^>oP0s4#9o6Y!TmF>oi`Txt1$af%+YR z#envN&^e5Vj%Cc>1P;ko5lx05j2lW^C+|X5*|3hpS=#5Nt{t@Ord{>3tsTpJU^6xg z%(B)^--OKNHdrO^+=nsz%7)Ddgv{!{Z>7jr@Q~C2y-m4fM-Q}QR48o%1G_NMRJV|K zFTx4Gs!aWQCr?V5bjdO8#Rf)|-j z*lb39u89n&d&7hO{N64P)eou&7Uxe*W+W_-2|;BVXs9h-QB%&@y8nz0`9?v{)5{e5PkTQ;Xs1hg2a?@Z_sLLz2* zWI9z_y^neuMN^NR%C`EyYOnUzy#Klz5%F41U1?Qv#fDcc9UY>YeosNOi8p&zzyJO3 zf9u=d`u_L6|NaLb{PCasF`dTUdv{-Z^ct_0q4J^^8kiB_y``dSuTb(Ls~TR^uZqqK z(Je?t*_q_POxEH0si+uzuyU{)wp_Kx1WO=I^Newcb)AN3IUNnPXb_x?TPr;>Zc@J^ zoIZ#$a4o%S5k~c8+D_+YRh}Ele@EDh=&_a*?7==k3|MYzh8x?z` zAajHdM?8#zrGp$XcX|_2aN>1tVbNgaWJryiO)+0eQ};uTtdTihoskSg_wL_i#=<;Avkgj+V$`sflMi^#C!lV^06I{ZR1r_1an$>qX5u0yzHbY#NSbW8mj}Wr`n=d^wY2m-z(|r$3 zdDIYOyVcQcKB544)gh7vWD;W`mnO5lOsajNt!eY+K*j;oywT4<0;(?csxmcmDeBAOGYh zpMLu3`|rQcTd7!US2OkCi}p3TdmlUdd(mx($y)SBf7E)=;Vkqv@DA_%fH(&IP`7(E;89o7oenoVgt zS#n2q!>J`oou=VJwQUfGB{E%_&b(#)gxmEOJiw!H`p}W(j*Bx^4;fe5V>dn`TGV~a zXm_PSFym46Ouqx~GOCqEsvUfYeOrqpj78Zj#~wNx`}3HSR`B!*c?2(OFOcyn2_{hl zYkzMacHWS|LczVe_ei7S$+IVnxyhnfiyq~&Y8{53)N|-R1f2bdI7FgihQHz}e3taI zDlms54YbK?IQAGz^Bf(^L_2%CSjBwjoL7nP#qGnV_m1x{BVZ3Hh0F?4&lm)mxFT_A z!s5I)EAtima1AX|q*b;}eJ_Cz1M2dmV<1#y^{})^GO!}szq|qj-&xNgSf4(7hAftY zSeW7)4BEuFzjv^Az#^g+v6#nE+kJJf>XdR}Lbx)T#$YBV{4#^fRluo)Dy0o0QjN4o z5Km`*c2@3#3Qe9c-I*-pkWkS0@t5@QMRd%drXfd%=Cs`Runn$ix4gRZq*hlo0fNAU z$KLp2b7kEaaYd2}LD7`Vbx1}9O*4V~sH3)|$*X!4XuRB`8QR;b>?5FCe3a5750*;G z8ON|1BgDcD9L*m!L-Pm?Z7^d8KzG0v>SdiGrH5A=#~H7eVhK{~{d%(whn1T)ejf8A z@cG5={{G{~um7Xp{EeUd_{aa|fBQGT|NFoHwO{?(ox2B+(Uxpo@t7Usdc@jhxil@r zi2vn2a>PiRy{m1`T-iTopTcI!oVKFQ0Hs|>6E!4lwV5Lyxt( z^}QT!F15nFT1x$@(^;WXmRtmMx$2Ib?x44+OGIF%M%_S^KZ8wj?U>VNaWIVLWYt>Z zUv+5=$V?bndM*Z6R{z(Q|NI$byXG7C#F$cZ98HOIaJCV;XS_l=#vn5%qb(zH@5+5| zBEwuAjAHAE(DE*+D6Jam?~Ix;Dtek;^(SM}c%OyAdq_tCssnxf@_LoC^Jh<=9^HSy z(uk({77eEjwek?beQ|+Znveut^qgLXa4JXY8soeGJ5C7=Jyg^u|O zB4>T7K~GLvp_!nV6j@FuF%FIqr~>#cItCmmD+5|m#pvvoa8WL}iZ|hMM-`fCWrUo- zi(Db*$Lh8Rn;Ngu(?=Ebs#`T_H3j{O()m?CYuX{#b#@DN|G@@4UJOjXvDTJdr51y* zLmuga6lk{k?H(@B?1ZdVF@gceXp6VOAYt9O$^sUPA;3Y7Ox3w=X)XyeSdf;o%Wlm}VUCw*9TGQ$B7cb)39=7yBxA zWsw5SosXu%&fkC#teW&MdFVj#M^|XN-gS1mp;gVZC>_vh~`^hJt z{_MT?zVhWSvwq7%YkHn^Ym2)D9FneTuCoqi2I@$3C&Z&}2_TLZi4Kc8FsZRlNie_4 z8(!HQ*$VXEp)^5DzvrT>HYp{|q@N0Aw`@eIgF0qErbI^gQ>!e4Lwx5BpB%Ke%4j#{ z#o4j$8XyK@J;|vK^xQu?AFlcot2Nx&ft0qw0+c0zN}kOPb~dFuPD9jEEY_Dnq~a4ja2Up#@cR5fOENe6*4k zRMbCv_5>Hc_{A@?7vR%RKH(+3G;?gB{_gMJ(NNF#{=}eRU`ii?L{@Q?e6i(^$I0#mMn7RzhW_?}^))WLDWJ}Lxp%mH+uV#bG zIC}+J2FjIE1gnlYX_dF5>08p%U5WYa=o7G)?8}*<4^$+2a#Cg(Lqlb*u(nB=Bn7A4 z?^NMrF>ZPx51F3tBu^JXM>AB}%16aAWU(WgV2UKO*;*#d$o}-SPSE#gnwe~p$7>|2 ziu8BZ(tl3B-6DnM&jJ@9( z^ITf9e@Lh>qZ6CW7_oO`o^k^?Q%N{9I=1mKot5t1TmPe5{SrhpZR|6YF_Z6tGeZQb zZ}U8D8*45|lG!ZlF`AViS-`c2L511k{tBe5{32a}F4^eJanIY?#V9F8+hb@Y%gGl| z1?PDzL@}zgDUc+40IPg2tU-~vx}<8DG=iY=Tx2V8RiV5!4@}W|E{Eo3dQy>L{%_v4Z1%d5n}Z!==T&Fi4>(>8!DU5`nN@sRY!x86~{1@nC|a{h0q)jsj<3c%m(E@q_RP4`^H=Z+LE~w7Dz8t z2J2f@$nwva1+0X+N>woTg;47`LK^~kKuk7`#pxwf_BnOHI{ETKN20fUJvp}K_!XeE zOx3OqV~U-eq}qkme^K}d4G4XpM6i`qZ@$^O_u&5HH{W2hIV;S%<6|$9wc7<|P$&u$ zJ8h(r?hf9wH=9>UdIS}?e8KvSO8x)&MD>N>X8!iS7#-efR zCo3H=hVhXwf+D{U?1>-A>|s*|QB99Jk`NV%4%bh)Jp;1HW^0?2ePQHut@A5~UCugu z$OM$dM%D->=IXK&TWU)=R<3An`C}U`Pm-faI3n0c)y3Us3bFo%R}{o~Vdb z^+NP+IcH-5(g|U3iNhRKYfqZjoJ#LB1A@wDzg0{~6V!oWSdYOnfvW6Il7(QP^x8#Y z#()`=q}7IOVDHm-t{7lLID7QS2AfzPm7MVrhIj{J8(zRd#QNpyyhUjrRWvv-svL4H>|!ZnM^PwX zo;5a2z3P!tT~bJwT*(^069hS2!IeZ?-(*A{6RjlSx=j)F4or#^capWTMgRMP-Gv0q z=%g-8=-Me!W`Ga9W~3AZ!)%sTSd+Ya_l~}KAYH0dwm3t&DNS;*!OValN}$u%OkuE; zYtcuQz!Y)u6}zUUJiTa(yu5aWzPwx+F-X>m$oy404v&CG1E6OeTWt&4ULsTW5M4%o zMJ`kXBobcdI^#@qr7sdZ>q=P#6|e5=6u&|g340_s1E%8N*_Nqmy1`W}G$V+%!m2fS zQChKx)h(?>&CwOV2dMz@~zc|{XB)~=%FlVPDX7dk+Ghhac3YuvK?dlEu*zzNZ%;jtG&Mr&q$M(c8D5eQ*$l$ z3IPYVDoZIyaeqsh78Hqe`ZL$HArV2UZF-Z8)js$1}2SA zlBfUL4akfOX_}Jp*+xF|&F;Iyqa)6IfdG5>>{}l33?%mrTVYi;g9)#;^Xj!En>)OB z#pZ!*Rkh_EE$=cF6g?IcsVjtgr3w}QKYMTbY{_xmiM{vk^0wa4-Dse(kpM`Ogt&;? zNE(Vmks48Ch?MQn2Y+yQIG?59m+{|Whr^=?FY`qXFGGcbl4C?oge_@25+#zM5edz3 zk+=gO01`kq8oj>d-uJG5zjHEAoyy8u?)LgM`c=QKn|bo=c`_@rGOH@dDm+o%kQbFG zdt+*@s@|MTrj|;ALgunvG?RL@L~JiO%1X=ECU|>A))lcAfOq+2`BfX3ker-crK>aG zFzwH)zLBDA17zf&v`R89BwVtSv|P=wD;gzHclsTruHuHPvWcgrc0WMSkQ-A-Q@b?QtTGl_H=JB3kF!YaY6Q=khx zF=T<9yGU0Y6`I&MEqNBdGfxg!!NJBE-VaD?T~xRc;4!m0bOGwzfp6B6RU}Lu@K5%M zqMdWh4oP>VSyfD?HCdpVsosFYwAZQFo|0z0mm*cTH0xmK-90AD$)U^YBHcvPBxEv# zrzmv^PUnC4Hr-_08}=?;y0pE$jTJ&HcVGnDU0l{z6$OZ;JY{no)=>Dk0-&S4{k{F& z-Q69;7`{Y~m_m_0%8=!(Kw*_FuShNfK#1uel!2vql@4OKHyltUX-mLW^%&f6yhOM% z&{|WpM6l7CC?F&_R%8i9NN61ulSygRq~$8+n3%M?oX8rYIyDs4D_!fHdXi95?;PpM zSn;L9@HN3o!8?LvS8;!UjklN`PVlu4ymf|;M9QEB&*zF!tR_$ap~Ue%%QV-oHmj^l z%=6jOd4O|sZnkL_)$~%{s@UQM&!TLgk-9^g9eI|GnWNTuoMUB(R}ATfm`pHz)>|Zo zdjNt!eZPQ5Ow-;u`T(I|z@_?!T-Lo=V9NX?-@yYXp6uYH6Ouv65$K0+hWUTCQjF^yZk{ED)u#D@_)u6cuAjUIUD-Arbaym~^T7K)KrMK_V97uuGEP++at@V>n!Y z{PD-}adwPxgFw>De_{!FjdYL$#l-of7AZrr5zHQ@pR!vNFBch&4{qJQbLaN$H?F-t z+TI@RAK;r8cy9+V6QtGUpirXB>lyAz^@i7ET z0CP^wl~tArfD#dKJAdQP1TWx0tHFDb#v?8aLI(y9Usog_02NCBlqySNq|UPY4(@C4 zD2>ln^ie8MmDRCnyFgu*^IG|K@t)JOYU?qDm|fZ-XZ?@`8lf2`kOH@gV3r zBBi*#pnEA@WdxoDCj3fSc2doUSRVVV0@ce_ZHltiR1MFR4hJl)S;ji+oiO2}!$#}Gnr})C9OV-N(kHub(GRq>T5{U_W zv&t$^JASPuS(sB6AeSAjSxU2}iz0lm zi&@28WG!c!#>q8+)Phi^}IVCRTRizSW+RU_?NX69(L8*LJ zO=dz|13BE?+aC<^Ikn(7{4&V_t-)%!p+Ln6b=Ln%OXdtCp0qCb@SX@j5tk>yL0L5k zo1;uVIT2=_g8{C;s|RDed^9Zbh-&=!#vBrqHq>|*PHL~mG!J+noR-rn3sl6!iUv$$ ztT+jwmCln`0B3VcRgbC6lo?oB_JG+~gk|(b0a>($Q*oXOIni_;+Jx2o zS#x!$0`YSJ6al;Ax;kF_r1r^K|&T|%PL`$`z;gu>hQLLk&*{Fy*n#!0rvs)y3Lr0-}A!0Nd-NA?Cc6Ro5_n<_dpCZJLR`+awMuidx zZ0l47p4ZlVVhPVkfgT^FS)ix7x6;m>!UtJkVpoBk6ilCW;!19_8Sc?E>B+%RfZ^E zAkTVfEhWvt*+M*Daa%Q|+_U&nmn_lJcqu8-W(i9ecPV3$0Qt?mdRD>2E)6FIYKTj^ zCEAM#q1UE5xMDw3+ec;9I|0^vAUCS5$37`Jo*uCFfW-ZKxjhbbL+=+i!;XVSi_ig9 zX;^7|S-35N_ub;5*CZ^&VdxvKRk*O=%`OKAH*eqG+usk0Sr55LtCg4Mh^r&McY(!9 zefA2UzepSt^PVU{nLR7R7Fa&Ujz5``P7X1a=L3#Q5!YE!H5%u;t43#i?;}K3O_<#A zjmx-~f#AVIg!1Sh$`Cn>XCM&^)60`8TvBwvvD!|S#Q%iti3je22jVju6s;HNLJ7Gp z6~H1@pe!ZrL(AlHarM?0;@$m7>rQ(zS-JIlC(N zI3?VJL(k@uV`P)pPt(Upxi`Bi77+0KU2i8o60(jzitxx!2#X1$+<3C-W>(F4fLYF2 zph|IyrDBcB5*|aZ{|)3GIhXE43M3igG}W9k$|{+Ydn{nxwe94lyJwRcvLq$r7_-(q zlk0<|yKigx#sXtyiC8l4uV_xL31{D|Dn;yV9ndACI!c}?hzRZ%Lmo6MwyX-iutAA+ z?~ZJqq}7)(sff0c0@rYSEf>W`;S#dOQ|UCohzkzDTeM+LboZp$r6kQ3RoN;zp20!U z0p6mb=JInQE7-N!@AWTTdieC|)4~=;>$8PbHYJGa5P6k~6sQQMs=UTVTq+*Lql{~V zA>QnQe}jIn$2^!|wJxyCa&}@^ZpLw9-39g6ZrPGhkk`JXESkxG1@2XcPu)5Ya$xe2ykznuu+c7FSWC!Keyu z6F|L`<(3rjCV?LKl`5~}i&(;guZUIji@iaQcNIANz;TF31{;|o;u+aG2KNV?H(tUr zskErHSuNWOlt=LfO25URZU`>c^tYRt#nA?2Ru7%CeZ-?39I zQLJO7^NKB|WbyUM{%ld3{TLL0v5;Da)kc$+Q#t-m;rC)5fi3&uU<}7Rc@yqsOS8J$ zT!W=qjJzX`JCwD_;MOg37_~A?c~Ehj@W*LcTrgXt{F=IpwY~1>#S2* z4QzqXP+G@!8SqwBmmze1p1wBc_hbWyhN6X85hjG_P-IVg5a}kYl%!ox7n#~&d3-kw zEf@enF@32~S)w3_DUT1B$Fy{U6G^kbB&y&8h=Z=+7onuJ#4B#Wq(a3a0fM$bt4k@8 zp=P;xT0=0BSQ<+(xLD=Qz=qdMS7;-T`rjEys?3B5PU_O&aL?JNtK`wbYDpgA@Qb7u z;nNaCP|WHKB%#v59hquK8@cS*DRj^vku{Kjf-AMgkLFbyIUA2s3@d_g3@uS~2qKiG zpe!T@m%7K3l$=HhzGEy@09HfV@nceH5^+hk+yqMuQjyNkN)ZVY&V_U~Q3Q5)*bL#V zn1|C;*sjinL%of` zH#ax2Mv7TspSJf@oifAVHw7B!Xi-oNrJAxt#gK$65yj*HkDSwJtdc3j#ML1H9(gvP zl(1MO?nua`&b5CFoa=PZI#f=|#4vr46O|?t(MhpB0ju6KOiL)TYO5A$Q3WB`qWC(G z3phojL2>P|(ftK2fP**E;{Brd?1aQMAC1L7T!8e#gtyfK6|G8xB2f4eZ&)CsLGkMW2$yY-tu)B8#=A5^KLog~ z5QaLi_&~X)#o&U=G^wIqD^w_IsEH~ekgGuISm8VaKs70`@_vX?N;;@pfHsGJRMnDP zn$($RSxJyKxO4`$EPy0Bhc-HGkCmRky+(eEp1awMzJyu(4Cs#pnp~7 z^FyeNwvLK36XtA63QB4rHp5UDd)=&=9;luPQgAWGBp4s29}cl;wLiqq`mnbu->ILB z>5?frbM|1ORiSfYjM-9w15i>-5x}j@KRHf~u(IC*ssfRT-5QmdMzr^EqxgHPP|cEC zsEOiMTd4KJL%d{kJRXgawki{s3YMne%^aVEZCf_$XVXQgvp8*x%jK@>6Xw8M0ROVI zG^f$%bVw0u@=(?5R@J;jr&3CwxiTs`5gPRIBY{LnP7QBGg1*>nyP%4CSu+=?RpP(~ z&R%)6J5PurU)sRr_!N2lSHgtQdEeWEbP0PZBOK3^PK)XqUP7p72~as^fvK>zd~Pyr zPGAC3ayq~>iZ;88YOLta*7RgW1wH0fr_Eo}l~Wt3rf}08*}8h4j`8BKc`bE^-Z)V} zR@E}Pqb!ofFx=`cdJQWnkSnQJmFQ{=U22uNf>ni16w~ULUJ6N2U^GzXQlUEm98V8e zwV8TaxgjgTsdcPP%+QvY$n}IJaX|15glTU_xl}1JIYOh8Rg`$QQksCLE6JEKBmB@B z@Tm!QmL+EgiAKMww~PJ0r~cIZ6qNWpSd?-G7U!Uk?9fO|l`{ zOp*=`6SXVEMZi~zW&x<%6QRB?q^Ln?DDot;=rIjVPFB(QBd3ziSu-Q%0)7(*HgNRH zBkkSmnqdQj<5dLxy$O8bbyUzLBZyZ&+FZ2KqU5Mjq9D{TJzE4fXsb*NLCtF5tVMDr zkrFz5x#Q%QjD zYpoWO17f|-XWCT41f1ncE|?KL;hfb|fxr|=I({Xy$iu8WNgSyMlDeB#wvwZAwSI-1 zGbkLLdq;L+K2WndIR4r4DXs{jbDZRX`lp|99f z=xpk#VqQHfatvyd30vSzMPA1j`iT2uU6k>>w&jwLOAr#3l5CcP2P(IODiPuFOKG^= z)ss+>ljqr$Rf%t!F(yGz$dLOP^2q5brh}u=2sDfwyh?`qpRos<7?T4AGvMS{1W3w1 z$(uM2Ph8}Sx}>u_8we9|M!uw&Y|zAtR0mVcR04tktt3!*1Y=@}6x#~ACDad|Nwc`- z(kQbQ%L|IgU4OGoa}4#W)WTM|<;YtI7g(I=kTtMcK?_u123E};6Kz9ZW?M#Qka9wD z;(-$nw0Xd;#BTJ^M&rs8DjkJ%=Q%CDlYDHFu)+Tw8@)~x;fPox%=-e6%8|evkqmAH zpR+C*?fQXsh0`1WQ=%l^qycIeb4Y0>#dL_Hb(#u9JcTws#A9IcW%{vF>XJ?~mvZUy zwJys$$KRGNB0n7YVVS484hj5<(LsCQ(GnfRTgyTB~n6^y@(wu?n{~CPP(XFjy2vON*H4o0=X)^ZrJ@LE`8^WdDTD&GDMH(f z!cmdZLOsl#qWK0EY?iDmer^yVC8Q7c5BG20zIFB5wNs~0_t$#k(-VA-qPHTOMH@vp zRhd+WM`a6%NIA@+T=A2lER%w!!YYgJcxKDVPlBX~85NBZic*wfiT1b8j5Ng9zfmOzLeK_%tSab8EGRAF{17hIlPu_kvbecTzMb z&l9#v$uS_zGpWN-coz}VR8X_Dqb@yJA%w?62YS@LKauxl_5d#4a^)5qi_u*GH(HMQ zj}m}EK^aIYfB;8_UP6~F%Q#*PIQQ}ojMKk+*@(sK4xb#&ffy>qSH0AXN+ALn*bZV(*6{33&9bWqC4a@t;!h=B zJ*LAN8|4UxcrDkmdZtdjQgtZjSX!!-vf2>^BFMlq0U`+Vf7JR&V0xcqzMG}?DEuQ(xP5m}J6sV9FoEMma zAQ1&={_yj|&@CQrc) zv%KgE4Jl!W6AsxBDo@1r(O>arG{O|jbh3UuSJEJ+Oiln*3r7BB4B~>K4JJ+ZqtFFH(_pWs9cnzGJHCK5;!~(;9^Gh4rh#XeRlW^H338N@fRQ!1X^}Ed2rbJ* zbRN?>@D2+R=v`5k8j=dQ%lX1^GpeGI7-n`>inD-(TlYH3a_bIEHrte~>tRz#8*!vN}(Tm~d2Q{8r3R>1_;K%IEQL3v0$rG+(AGhm*rKe*27Gz$=Ty z8yY!mg9x06oo5W3#!B&2&73@@rW5>j=X)0FLxVUaJ=+q`WxCxIsY{eJsY6;Q-1{NR zSP>WoIGh8s$N|_5jU1B-YG7K!vsHg%*^KEu-;8Mt>k`wFd(l;%%B>lvO0^8gbatf^ z!lK6sy4@9%GB#*nRRYmtnyQ-cMO_@-m8zmc7v`-vs;!yOil0T8lRV3FT3~Q{gO}=x z0I%sjrqy9O{H#!~BoKVI(ne~;X>C}Yl;Y8#jyA&h8`G{B$x4s_P50m_9t41P=6i`Iyt$jUYg1oNm=sJe2 zI9bBQ0z2CkTAZH{;(8R6(Ue@P3gFC&ieG}f8Q6(dK{KaVp^oTTWHqJ08N_MF+D#CJ z%p@XcE3}3BCe)kk%|Y6WD<}iMS$^u6Xsvm1UAUVG>P96=n5e`A*oFB5aVpaFp=!CP z$}mc0K~=dG8yiMhgDexNJBfA>F+POGnpT-ZDMw5pl+mFTHIXWbiU6r(t+{dBhygc; z3l&X``$=Ls9;j?MUEnr zIfD5?KkoS2*x1BJ`AnaPyR2W;0Utj+IGEsjZv4d$Cx@661{<9+R8$WKY|xEZLPP6- z2XZ8E@S_sJ5%dVZGJ#!xQV4uZjInkKZGb7<{Zb*3w2NR_U=nrU*k8c5Xcnx9T@VqY z4Uvc-8WyFM;CXz9V#y4-mT5uZJEI743&G;vjCziO$y!N*mz37=OwbY$-Lq;}mYV{z zhPUu%ug&I=Lz60(z(5KyI~tGs3?Bvz0+_4ItN7*wyhLi;15_@^CQ4eiqn?JJ)2bbo zkk?pO@Bu=!l0c_q!l@w9l*7yyrBLQc)T)vrY|+7CM(znH27d*-0Kjz01%aWH(XcTk zW@2U3O^IgC)6_+ilqHoV%KNO-+=G{-$imo4y@w9RPY4$#;Dpg#@Bpp>q5F@CyP(9q zD^f0@=^$%bbCiivu}5G&!b+ET03+OFlmV8@jtnA^U>U0CNg%It3<{oAJO_u;{0o}M z>UjR%D|+5F@1c5+&PR;JN<1pC7)*#dIAI^Acm-!FsR-_psV)ob0Po`&I!=d{djxHQ z5UvPf1~^$c&{n~PLTtPH;U3va)KN5qlIk2 z(Q~2)Wf_-}V3|fFv7ovuTFIyqgDN`ZeqaWM-ko7Rgb&w}bhQq$dP=8C?z;N$gE2|_-;;Y)?BA*hj zWQa>56)80@s{%q1fJB7C7-|y=x$?E*dts0u2DmE%^mjIi1CHfsaa!g%p*C?u)AKiX zW9brqoeqT*RFKvZ&RDaE1vEtQjS3d15juoLd=7>~Su2Y~%lW0_&{UN<8V_`oKPZbo zQ6BR{ZOX)w|2P#UBzm!+awxqPiOIZ@%B*kY5OYuQ5(ut<_MkVw z=GoDBA0#-koV}<7!HFZ5T)oA+83h#~xIr%(o*SflK?_V9XD=?{Z#W*F#aGO1k*B@q z(!rvl766Df!rH`Jh*l?vq{D-?)s3~)zK+oe6pvuZ8y5y7Id$aG;sC`EQVDd2u0RK9 zNluHOurnQe3sM|2qg8}pwqhlc)2xY03nXuPYN>cQc-aJ3O)yqj=0<2}^@%Kl0^Jph zYQj3vxxjWQN9dqMk4c~eVvI(Ql}jfKn{8B&2^+nx6@l9t!#Uwoz!E(>QVLbkG)Wb& zbR9VyF`D)$4aTMuLebAxc2OlqrMhm2_d1y5qgTd6ANvG+qdv2Q%@jm;4TWWz0rB|L zIwK!n z1tz!skZ!DdW2Z(1vkT4SjWea|MtPpa+Txs_9 z_74vD*=wK;hr{?hp%xzBvw1=M1Y77c{P*5CzS`gOn$g>41btrQ3@kCMIUwNRoeowK z!Vfv4Kt}I~ut5L|w=u<+4}cGp|uM^SR9)G9od zmwyEt$Rb#tzQux|G#21N!N>={Y?LUG300X%w;V*NZa7D_pp*_rIItAl_xDG)Z{Ob8 z-Cf_88Um(Ck#=qpTTLwL%apWTM45xym=z0F zT3V*Kd%3KZ3sNWR2X$tH6Huf|P3!iRezHp^xga5n>#>b2qmcz5bVE{5KqFrDH^EVI z$0Pu%gweuWiNK7rT4dQsPzZ5FE*bG4C~*Qx1XA@wLDQPbFby+2YX?(gppdP7`9agUXKtF#@nKMbZs3LY$y$`?a8UgP*G z^u`!wagfUcxZx)nt2)X<9Zb>;l4A{mX<4yb8;?iwRsblnwy`V=NF!`3SeeYrZL7dCWjQ|6vT`0igdlhx zkpoqvEnwE+cXdw2BL?EyfFTM^IPd6TfGSGK!*Z%BaL8?CWqf!D4ujzUb-F(qfgM8e z#v3=j`@Qe}Z~w>t{^?JD`ti3ufp^QW4WcRY(8d)k)*o|Jy!8VdMNX^9FoR`mv20gp z8_)-_eQ+>3JU}zVN{f8o4Lyb4Ns^Z^Y1A4MO9XDxrh}Fea0&;S0WQh0_=r7 zvuR3q~%94#gq2pftLw%Qxjjm_GQQfJoLBtlSy ziFB-@UEScUqk7VSi^kcRs)T5&HdGT$U^LJgAy(g(_#_!`wTUR>6igC?NQ>z5)&akW zw}?|_xr7`BLbz%ja4*eDuRq|(YIS8i<}v_MH~>LMikT4?;|^B`>qBU)7<=#R>;{f>-sBLPlRwp7>+w@%N@z?Oup)rt=flI@J&X)Eprmma7>-7=aqsXO z-}uJYzy9@8r%r8dZJ&SOfwSk%t*>uH#Z7r&f* ziA9W2{Dlh_a=0-eqshsj*V`ZMf9u^XJc>J$rV2I0PL^qZi0QTuPou%b^LK z21Ho#8%YaDNKNq87_P#h`EjVCBSn-khnq*-RxP&aMAOKFG>A6^tSq0<7)Z(h#2*Ik zaK?Bv7C)?=JNLlDmmm4|x4-k9Z~r}do-?PL4#yfj^IAhR{UJQr2 zS9iDKj+B}TgTQ$_-lvay{nMwm-~RTu0SAJH{UOZb>vBZ-YFkT`Jjr?wz^mS4$R3|V z`t}3N{BhqCS1bG>W^kpzEti#*5nIp!r#E~!wYEXzt;xo@2co?wWg}-V^=>+@QAzWF z*VcgD6U#PG`BTZMg3u#p{m_b#b%CeKBAfv4Q4gdOLNR06!JYa_!%O0I9?>&5X)m*D<#p|i*tX2R9Ot{q3l>JwUcB%i zwsG>-|KZB+{vHR?_>)N~ULvP__|}QuT7NL)=c}=(FUx=;88?k70_pL1jJ;>5A7n9) zgmwFOcE$(ez5V_F@V9^aCx7xM7tUWeckbM;e*EJ@47#xrQC3w2<<2GLs|e96l-u!8 zOe0MEBTQnL#ovOnL6GPyu^>N+l&Uzja_W%oZafN87^et+94iPsG=Hq>Np}B7KYH%< zYgaL`LE!F=_CjqEF&u1mBxjop2lCQHxkyIv+Qx9Wv%B-xfBjc*)?1%^>cYi~!}WDU zEM{)-L*zx4jxph7FPY3Bx?CE7Xd3bgnMq&P0VrQea03M+X)H@(0Ue8*fI%@1e@%RB z@0rdo^3MIHT1n^0r5s4!pP)yA4^}RmKmWuNkNx#uefjE@D-T?F0KLJb2Or$r+=L&{ zocH(k-?;e(R)KbI-;s54&c>mc&I}LfB_1#Vz#BJiqVIq)mma)y`SRrx>;>Am{E;zy6dQfnyA`abxLl&h2vgp*PGzNN@s!I)$W6?9G&qB`rJ_gp5#)C{SvttEfz=xS2W`lBp$IMpQDwd|XbOSz^ z!D6t80WP3>o0~|3+fz?I{qCQ8&w~$MJa_i&#Y>k?pFRUsfw0&^R4!OXuM`lZ6i172 zmXS6M%pC%+stE{v;{scVm8a>TC|MmUvN~aT%_&_37%S7rLF(*k5tnc_hRaSQ9z%d2 zim)>o0VG?mxj791y|J~9+a`bajc331wXa>japRx-@<%tfHp4=dst5yw7o%Kn$$*#q z@kf`_0q1fC!k_!x{;LNcTwh;*`G?IhOUl)gp3-5dR z;RFk;=gvRyo-;p>?Dgw!Y;T{!RPe&Z3+Rjb1Kj%QVUfSrU&DF-)V7+;J_g%&Wx$WB z5gD+8*YR67Z=g5((?9#ujg5_$fBN!gKJy!wA9)0}um@su!Q~abQ|;m1SZ6{l@tdeX z{a_yOk>;!-~IIe0qF->i^ zf>{%V;>Co}@ltsUm+B<6Cr!E~+G*}<42v1xSTF>G|E%cE?jVho&}(ogDf1~_9OZ!_ zTpjsLAQshndwaWh_~Po-S8v|Db?w@9jMUDaJw50TuyqrcP>^9>1#f`^hhBkE>OP(E zh2=twartf-dY>-0387qtN|aQ!VbFO#sIf;^~!|eqJfGMoDj+ZIPU`6LFKr%#{W+T^mN(v`G2x{QS*b#D;M zvTC_NaJsJoyTMCKFJd$z_*Stm6xD$gRs~;q^_87FJ6OlwK8>Bor?Bi*QiC@#q#2-^ zP|0W~{N+D8WOjtNBXa0^>C&amjZHj3hcw5xh*}1COL#<9%G9xD0RyBGJ?I7)JzP3L zBj=l?cn=5f`NYTEpd4Ba=BRk)Di$e+gmV-->o1Ih0uT%f(N^&;ne9`jo_hM}+p=XH zZ4yki0ryAP%|KsDU#6o3DoT4+I3WZ(=}-Fb-|gFXe)Pj1qKhLF z^`%zAtU9029S;v=LXqA50iF_M`$Fh)#|VOi_eCYv(Uy2wNte`*hdC!&yy-n4{nIQ~ zDq)r!v7Lp_BIelBH27h$x`)0coe=W+fxd*7o_pq4p0?_Z#1SekTa;i{vIWshIyVy8 z-JQ_+>!2Z2Wg)ntQx`yY@-CPjCo#zvbx|zAIT&$SPv3!o3nLaAu;73YM>Jz+8%AK5 z@?mWdgRuYSPygRnUU>zda9ZD3|IOe0jp1-(b8}1fQVwxD9%|@zEDT$rAzMSX3z8vj zF?&PX(OdR~T5LzYEs+h-K!~laGWL~GDPA1j$4y?SPHA|L_!`H`x|{-%$U)@+AcIP1 z$udk=DGv-chIlLrqglLv1tS6(AK=-je(#q*`pfTr@4K->73N@BQU(w`A(9iRYY!+9 z%QBTCq5~6$87cC=!C@J8dHi{ySwUG~tE4(AaZ+$p9ZqCkly~-6BwfTWqlz`wQuWXh z&#>W)R%q=MeERYjm}@V z@Y0Jfe(6uX1e-S3H}>{MD8l>_Dix+V{krS#h?bia+d7F@tn$7UE9a~xQwq)UsI{)D z15jh!p~}N$9Y9umzmIAE&h4EE-cgOm$oCI;>l2;-TAvpHEFDhJjK;XkVe4qE$PNWr zle2~yu)%>10v$Q9issfp(pr>NKB4M@laL^5+E$BZfXu_K3vFU!7m^vQs3XaWQ=lm( zU2id-iqURi!1!kuD1f4$wp5+$g~Ah^7sA^-un=z3GJ!ws0-jC?r{#5me_T9Zt;Q0^ zMYE&R14AEl85sP)2Yj4<8%7KMw?DC`uS}4X4m&7~}brN`*le{cyVH!sE z@;+iguqXF?V6;YvhLZ(Vl)_)oI#z5xY(reVXX9ci^`!DJOcvh75N-8^Ia%IbPPa7N z12Zv3$NWtc0~sG$gi2TuoUo-518#vzPy~GyF!~d$=QB$>QB|`jLQWmq!wn@)q}MRH zz5MV)cm)k!PD4I`im5P8NF{%Ax@SGW3PmJD{ShvRCAL0!UAA5~7~G5&{4rj*ZH2Ai zkGbDj`%qD_bsTwqCrJWxEcTRbwlm!Y* zt-ufwj71JqBA(2-b7%LZmtKDD)mLubc;iDK`XEM5x9{A>h!=O)fe0MTsBj*x#Cwy-2f&&p2s&WS0jtJn2N8;~bTC#3`ro$KVwP0u2XNL@}tu zMpUdk5SIgNe6!&YkilS^C|Kcs|@|MF*c-O@`p{Ld?_ZwOyZ5hP%Nvh+k*-%8eay09MynRksdtVweIfX1wm`y9bk!Nvc z1A3EZ;{d9KFv&_!IC~Gcbb^)TM4&YOOhic$;awTk^mvrfcy!_41*cKSF)UxWZ!%9FD9g zr)pU^rwN3}E%I{FTsd=y=hEt&DQdy<&89Izf?4vQpe$?PS^J)>a$xa{uam%{+~oT8 z8(;tW-{Qe1e0>40cgOf^xW0k5i*|^sDVOhri3}6jq&aY|gekGTxx96P>#w*pXqL5$ z-jI%hBRq~bxiG=urX17&)rUU?KnNu3pD03N>JvoZCoIEIJ_w}?;$#pQ_={>|PKX05 z1^{5-SiTBm^>hN9umW021?@|)MP$@j6;2t22%;}5xGA8;*c3gJH|eRcM7pme#|Bi! z!GTM_l(WY>nlzNhh!QVfK}7ZkeObz$4A;5Bi~||21sVVVKmbWZK~$l(i|?bXVa7P% zpp(N+^*&4!1EVjHu|G6HD9`UBOeWaJh}z;IkuLKcZqh~oV=BZ!EuBfl`F2jfV*n+fFMN_}z2nrKXEKQ0-R>_em=#>@Rgo!jvgRL+U{S|D- zf8tR(`v%;E@?7aJ9?28k67 zWHcqTB`gj@qm){eOrV;rH7e^jXDtaG4zlsUV)>9hTuP1p)*L+PJ@J4ktjj|J}vt? z6eTq@gTQ){rs%1sN=gb7l7fb8#n=X1EhaQ0Y7mPUNzl`&`r4#lLT!#Y^Zj@jt4xjmiB7FTQX=xGVmF+6& zl;IyFn&>h$_>nPYYVZg?FoXL}_}YaGl-U)?LAT>UM+k?eGEaduC;Xl-ATMqPi3r3rEIAOR?}c2 zwc;Zz#Zp0ulqem}B9D|t@)5@h6CMViD`8SJp)wMncKS&}ZP3QZy=GZ_hnbeUA`Pj$ zmrR7iH`u}wU=H)K&W5iJA|(rk_<#h5vrKbHMEx=Cd*X2%Phfm>FdFyQ@OgSHhm0`_ z!`Kj0L@2bjvc`Fps76Y6M6d%Xn#1xqNsnNG6lw}on6n*>MGD7%9DGl>pvezqb2=Bk zMyT?vel?_n(`g!qwV=hAl1{+-1HVwr0|P_ZO3Eqm?#>Q4_j(wMOPQP~#UC7q}B8QHW3xH7J#}@k)nDf&c4WaDW=o8R(@@P@hT@AzGGj; zSx8Q~J8>nhm^&eC&~)%613rM->+SCC!&i9N7S)Fj8z9UM4se%Y1EGi48zP43N%fq? zC|R<$WrBrrOd`=b*Eaf`3i5$lndgl5Mi6&ua|<_QIQhheH*%86TS=*WrX($oqPk&c z9bc0}(fEfuMN|t?*i7&Zc$~?K?E?S#raKWvHP-V#yHRES5ukYqf|hrlEU# z{KO(}1Ho_jb{r;!L%hgpgjq4a8Mm^E054L;#2bCzh>w6m&!+1jXhrKc|4A&hYDw)){2;X;HNjdi z7cp7OqJ&BQ4VbBTCnTYA(n*B*|Ebfbc6Ud&Z{At!^YM6oy_O{tG*Z3xA%jid!Y7!Z zJQG$Vibu9cs9H>p48pIvN@F-F>Yi0)565SSt@eaq}g{hG?kjNF>09)~I#PlP=k&P<^@Y z5hy{`6N#b#VECmd=`2(UVP{Dc;*aAJz@aWi+ZbaiJ~6pUN-JoJ97N-*O*tVX!d*IG z05zuSGap(>#z6Va%wI3_Dl$q41`%0giwPNJ;+2fJ-6Ph85Cke4 zi-`6Olf{9Wtc9bTEXPDVw8YBQ5=TxvtmJTOzy#k&C1)b5o0-N&UCBZ_bO=C0XXKFF zs38cE4h^$MJbNrNOiDl&gqUQ4uQ*sV2)!5)PL4N+2e81tzOl8lb9HAAtI6COikpXw zsLtq zaxlV`@o;x<2lv#sH@7!8@FD6AEDvHBhV=~$Q1LB5rdJMzcx7LxaTz```0=l^2gQEX zBs!|(Mh;6u4KboH4n{FPG=cTK^}#wH_rp_wLV-74;1{=ksR93pPi6E-R08;rZyLm! z1J)Zc?$l*cSj4uA!VzCt&zIUq@qtg{kJWNbJQS_XFdO+5h0XW{--2TCspUzjTBprK zDppR5w`P$XF|WWbPQ=pIsV#iX1Q0PYR?vWz9}#Hrj? zV&pJhLm1;nu{cG;S_f7gv9z_L(X9j8#DNJ<*WZ_{uArebv=d0j^N=Lu3Od+xy$=7d zu^dP`|H3zE#7go6j&akLXIU1Lk(}g50Ma&H-6r^G=~(T<4JeSxw2-ShqXRtUht3DB ze+}Qp!GsadIa4^@O##A0H;(GUA%Qxf?XUFs%m@eCkc2UagJt_$XuSIvVshF?#!7-AG!R4?|%<#fCyiF z7iOP}-MEh6MgSg$hqAm@U^RMv4Ixxpu}KF{pDv3i+y*)n32lLL#BMAEH>m88?ryU1 zo%67|dU1Y>RmX8o7%nd8!sOKtj?8wRtxk?Awj!P&%l;^QGA zaKf~RD?I8Lk@O$G-_gMHCV z(u2ZP2k&d>!2+Q{R`Cdzr85BtR%9fZrjx3M$yMMQr07q25YVGbBH5od#g?pR=P)FY6qUI3#= zju4oD^X~xv(6_B|dGNx8i}-?$OgjY=qM~lWy1;Y!Kk8<>FWSC*nQvMNa3*Y%|C?J| z=g*(Vu46nwihG(U;VLmhgN;uAq|!BzES8WUy&Xlw8i*`hX5OSqG>(v->fj`3E0cSs zutIQELtY@|(Dsh*2XH+70c))YT`^E%r_&(C3B1pe>}+EFGf1X3UnwGssmT{kV6|+C zg^O4?L-n5bp-)4cRSV!*yRyIq=*f4KflatlmKgFHiKc~nJh-t)9i@}d)|l-tVmtJh z1xG8b5tqj-pt+Ri$#P!hMc1->Q#4@(pfrp!s+YpLV8z%g(6u)%+EYL-;dlnE>73j1 zM0~mgibQ1{p(d$AR7@`Z%0|Fa5-Ig!AaJ^c9&j)ED5stU>3 z_*()9DMO(#gluWT)@QlG7CC?kt{KSjsT_{%pd9s+hn2>7?Sg>S1t_H}_yiN5-y@pf zYYMq}548yA=-bE;dpPlrFF_5OtI2Y6SG}pEyp&T#t1)SzkqdVRsda#8m9d5~baVE z&wJhtPv8MFa@Xhup^mRTy(s3yg)-_YhvuUWp%f)ZB|t696bUX%g)07*%~0ZaRwk0m z3W<0%I#{trow9H|=@G^8+zn_6REA&)&#J)o&_cTsG;o9Bw2Gnh z;DZltY;FN?>&DI9{eARHc#sgE8bU}QDm6IK5EvndjnHcrDn;%VbP>ScOzo&MRcEKU zO9}(CG-3j(0jy^tDNaJEK=d?7?2pj6aB#Z|@X_BQtFKBzwCfT&9-)MF`}?;6$Meq0 zR_}o5dN?Xs9!91_J;<_B^#Ul_tD5K;gi4}k0M!qXW&5v5jHm=ufGjkL5i^fwrJ69s z5_rtAJSZImSdFvCgA8EG5LYY{vZ^M;QA}}_c4YuoFbVM1Sy~ene9B9TjD%(Js1inq zzA7SUf{;*(?7${6tNl$xM-c%((GyTZumpg{#1c_nfh7(^IV%8ch&Q31BFkBU+&h5Bwf98FiQ2=IK)PX>KCG+^ zcw3>HCs`MA84g-Ry+gLKsshdt{lFqjlyHWA z`WN*>PA@Y|M4WaaYCs>Y++>(@dtc4U&Hi}U!#$4Z;&fqIt6bG`L)jpyurwzEE#4| zc=b>ykSyVFl$MN9TBSFLB#2x(1VBx}08xoD=!+o5+!atfR2*)O>Ftve>7p_vox~NW z_%bkKSCe#B)=Wo~UQ4V9*Z9$Bi8rR?7L%COrw#}fQ6EW-s9Q z(<~E;KmmqAO3N^T28A=8iIolAxIKd)=6wwO!wnA94q7zset;K0UBlPs4A9sS^ZIEV zbj2XYq(J7AXbW~f1>kTb(oKI9H-tx|)?c%$4DOsO>(H5OCPXg_dY2B%>+sd8B2;~m z2bJ>a>>`t+Of<9-AhOiJYiJm=Ce)-5Y}0C6qX|q{xSrstY}G${?rt>nQ4Szcle^P4 zVfqOnE5>`U#yd^y7F4F*na$4GsG1a4KX}hHVK1kwTV2q0)w3Rd@&1|B;?_3Mgivo_ zRhj|xzcY}eEauL89SXyn+19wF zD|s_>EyZ0%u-oHIV>0Lspe2SWoFK^SPcUh~$JH@u#G5wx+@}m>y0EpLBr(C@U6?E% z2hXk1V(|fIgt~%bHOnjLw}5`2=X^mb}dQn?ARf!wporD>@AaUP39w8h%sE@r>2 zxORpmc14$#D&3E{yD=`2cOy|OHL!ua!2_UbC2!FMevS?)bcoKkB4eUB4JsL?PXs0T z-In3%`Z^vI?Y;Kem5q%}Ob8|P`D8D)G2rVexOKI;v5xP@B(JJ=>gwj^CIxW8pasL2RdMacmL(QS8eAEQs+m$l zTWlg)(atscupP4thHRN*H3w5I(S>*_9ObN?&Qyd|WUvx1Ok3G0CuP#X(MN*fl2C_BhF{7OKiyEBzg*oMMV<@MtU;XBXX!+-c7WeXWYcc=4U-slL!TjLiI|G)?V-MWm3dn###Rx7If|*pIM+m@=e@)|?)z@g@x9J~Yr_ zC*}knFCAl1f**3gn&4z*y}u4|*hveSKm$V@Q6ozzK(kY~CgE3N1y!Sn^cN+V!K$Sw z!<=?!XYZ#!{n_QqmoXV&i_t|-GN@&eGOf)wC{jz46sUD`Fe?15OcGX_HKZ&~qEgHn zQ3Oo?n`42hr00E+>Fj|bjU^svn3~Wc;Oy@1?2q>U$wxo>!du?*`@jGD4?q0y=RWtj zM;^I+?%V?r2SO*993pBF=f)?|1ldpoxriSwb;}*nBC16%#-rSE5qhwe$)^;_2@Q%z zxGy1Tcar!G;peRLO3_LD9ul_Y%hDj<@WgIbb|IiM0vQaM9I|1q^|sHR#>|i3YmtT` zn>p5Y$M`f3?$~i-Kc<>AJ;H(hKNgSg{$1tUUXhIRFQ!`i|0oxEv zq;4{+5G8TqMF>+PTyB_w*({L4(x^)tl{GCONX?b>wNguziaTbwd`V5HE9QP8h--Y* zUlc*9PTTP<$0|^y;gxz5MdaKl#b?J9qBjE1esg8zMK*03Xw0th8bH zD~pF%GVQ_k(PSUnG*R#DX@DzYg+{1Fo@Uv-P{Hcq(S5p#*#HTwX`?&|#2s^oqdRwQ z<1PlsI4D4w)?)*kfO5cq3_D&MT02K1O+^w2=UWrJ6a!V0~} z_`IZ1r}!(tT_gmYj*2CLon))>S`m!ITx5`YJ)vWP#0sfMYkNsybC$mB<|Sg`EhG{P zt2eIS_?N%?KfZqT^_O0L`C}jZ*suQTC*Jd(_pYyR;4KaqgkwjO3|LA*)3MfK_tLEP zT_}Y`cryiB0{nnaBcScjOl++juIUHR@u`X4aDW*R8Vhi6$P7^vMeTq@+C}Pi1xiaR zkkw9#B)1fFTRMSiH9G42$Q`JfB3`i`}y~A#~K!dcSaa@p4!|5OY9T{C#WXNaeNCD zmMg}Su~t3%793E#Mr)WOW^q_cL}r3#goc!zO{OS4zz4%-ek&47+44dWOapiB>|%=h z$Rm$zZ*4`y8Gfmb=-EV@)j0vGb6Q#D#AJx*NP5xSn4=R93re*Nm zat)do=PD-@Wo#o}N+kIL7b$5T_!j7}-y6L3EpNpqM*f@s`ky`j{PX|a|M1^G^UTlv z{CnT`tN-+$KJ?Hf5aQtNDljEY!$GF}M+!`$=)M+gVJ$R5${^+}4&XAOD66;DLVh+W)h+TaJC(2X}@*uV~PTW7p-aP#IZtp8!N2JX}0Pj`v;4jCC^ zc@-3P(h7S3pc1vx$Z_J*q+q7NY+frNTa*Aj&+b$SlLclI9lUyp6v0f(HJ>dqa)*=e zK#V+5MuJYm!j8f!U(_ z)J@0RY+Mesvi#KwjR`VTkkKOteyScPODt9)ICE&s3|2UI_I9scyZWIvv0C{0 z>(@}fgMqw5RdnQ70Rv5WU5&^Qc3N@9Suj-*O%|PsVg{)HQQ|74$H}JE-+{0E6s%w_ z<^UW?LmQOoUs0ZASO%I=K|;B7+8nqkA4^Qf!7HIGRKMxCIBa7$Cm+YMA!*VYaAqh% zgv)e&xIWlC+jbh+!fWVDo9x zJD~E@D@-aXI|gA;wZ_^`QY39LDj_Wx?+BJggw5Ck{6ohJrUNkq`$T!aM>dhhCT;wH zC4V^aP{o)L(V>TEv`OerPA49?3m!nAYAdirR*4YYf~@JH?L>kqB$jmh4HH5(>-wX) zv`VMbYlR~%yaRt`2uGEn#ZfA~?cuCvriz zne?-<KR3`Y&9!OU8hkQO-L3qHasaJ4cPl#U7@mez8Q2z9?|f|A#A z$0bhivQiSNEKZ$tz!UNW)0eh!X1T{tSk0N0!F-24jwtFZ85o32sUF)5$!Myt?7PF` z_H!@1@cQd`){Jw)7hih$7x=D!KFY#d(s+l#6fP(fX&@?)GI|{m$n_+w2lXa^vMrPE zn871Ha5~}m&oIbnEuaQN=e8yG517t@*JxkgC&9%)NJVhsVIqFCoxRB9V2`^f@h*GO`Rs2#_4JcZzxAmfKlj}K^uPSiKYQiX=bnGzQ=j^^%MU$_ zH+I0bF}7fiu`h3gmmks+-OfgnF*rr!L5NJfk~k$yds61}nx(VqRQ{$0BWK|RDnLG4)VU+}IkhF%h;uCYGLkUf&=V8Fuq>DlmXE;UK zf>P6RR{TUUvyN>-n@%|frj=sGnK*_t9%CL3&#!NM;u9bLzz06?bMJcQ%U}NTpa1!v z|Jk4YDIT7C-}~Qx_RN{{51hyJh%3mI!{Q+Xsvzi2V@szIx`&>Gk#D<4-(^xC z`wm_*$_IW08W}-*wh_FS27G!U{Lu=LD^~*vQc`6CsG6W>18SoCInUkE8-f?)%I|KH z+Eo=xAZCIejyfDy9<4z|z`>b-nNeC3?bI&oRjPC(=71%Xn6D@4vh1eLX6tl}`c zq`8D9V=AFs(HPvZ5Wq645VWdr#4n>{a)nKp5GqF}mzo8l&KxsBI)KuEFu5IQQxsKo zS`OXttrM|?N{?T?un`hFrV#ERx^{g2-WDds>i)vaY9A^=)1tqT$R@= zA@6DeU(|=!yAS(k&Ytz@13kDhz}`G5cK{{k->{l#DU#~=LQ2OfXyv5S{3 zZES6R>eoN@+H0@=hrjuo2hN>+;)%!K^{!_&H#cOs8K27H0GN91kNJXG5h`mMQPYYQ zg^yYd>5Kpk!XYLHhl^;K9;%7H=RjQ!|BaRas{TY9^cUwyT}5?}5|Wx<8g_HGAskDH zj)6-$@6}?ffsUw}1T7^-WfBB6&jV{^d8Db*W{BJ>n2MBY`*x8f1`cW(CUUnhMvA@F zt*y({SiW99ja529cBV)kMB}*QC@lnAxALCeUr#5{~Oeh?cBJVvk7_}&}MIc{CCfO18;H0+i~z> zCrlClvv<4`yJR1K{Bdk6#dJ~oB1{RP6FyCWGg>7Tq+{@-O2}EYFDVc}^d)eE-!piT zNZB%lJG$d;1b1`zCb1HI8%+$aGO;Ja_KQ)hn+({=^g5JKFDIv5|8> zSqR}s9E&4(kv|tE*7#I%a1&oC-rg6_qJ~AXjd&sEyzBR*C!XkK98-|ES-+-20Qm)FS z8KRu$WKt199jiR4Zf9qBlN0_`&Iy8KiPc?Pj>8nN8g0928s&H<(@{nai~4Q#TPuQT z(u}EQ%H6_10el2BV;b?!KmG?Aev&}XF8-J}k1RmB_Ly2^43<2qu8t*Io}RO%2@}By z{FOdINSV64Ddu2BGF$4!DT0a1;uq|uPbou4M8X|eMyQ066S7FN0WkppYutm!-pD7P zdJ40RXP$XxYik>~p82_FJX^@S?-Dtp<+M^j6iLjN0g&b+-Vy(*^RYP=KE{U}IP8-{ z)PwfeU&c?}VJ?Ok=YOYPvk4wki2@`RB;?LlP6m>b$v&3_R)^BUMLR;VcN75VhB+G^ zrotw)n>TKK_j})e;U_PC;s5&A>l^Ez|NQ6Q`ObG@4e;#QGyJm1!6A0vJ@J;eeC9Ks z#hW?4^re6E{BzF_H`bqc{PFj{{{txEe2^P7`KnR~#RE>5abndX%o2lA(Fp!VOQa~P zzX20t1GDxYiZCXar86rhSk3C;OI2kBDb!6#rFQm?c^BmQa0(y5$#qB3VjM<-uUW%X zzUrkyh9h@LmYY8vygeyY8TO*g6k`&VG-iX=(I_{}AvK{t>Td)$v#PO^n$X6|!R-~nW?o^Ck3$`(DYQT~ z%P7q;O_L%7?~)nvbFbK%0S`bQ9Oz>H(J%ZWzofXo|Hk#}uU)zFm9Ko|8_z!bgCG3h z$tR!u^rt_qI&)UMy0Sjxhejz*UJDFz$Yz7(H3CW!pobu(N;KvvN0>BXbt{}+dN;lq ztkh%^T@*wr_)SBg!AzwxCXux3Z-+?e%BmiZI!>#h;z#RByip`!gH~LEFEG=}*$_m} zj(SvUxLxIbXeruWJbo48OYd454Z5Q{x~Z6D7JH3uEKabFnFq3Un$gklED?}+=~!@_ zCE~qApH?)5RN)Dem<6IntR-T}o9Gv|$)B}NH~U7xat>Y}Ii^`%#qtsQS8OuG%O|-@ zD~>EQR3Lq6IxNSsPF#6iB_aoP;n0L!arL5W0FRJwJyvy!YCI!vL~%f&*E_oua~?DR zTq!y7q{8?^RwhDxu8Hd&7#ZKbv-8qRKgBc;Z{WmBINtTncVYqX!iDpk(TP|Lu<=^X z#~ynWFYf=h|MpAQZ(M)zCokf;!MDBr=IPUTmIr7}uaYQcon;B1D*17S zv@4Gqjemm7+D3h|d%`|v&KB_8{^uC8xxV*df}v&bXbjM~Y{g$ozK6o$Qi?b?}_U;e8vfBE&R zS6+DGCs-cDnju603!m3Oqvuuzxer2f(O*ADx}Kzlx+1sYsj3ZVtq2XY*`fyLD85Hnxb%`reGGSkJ9($OD3cxp zCv9$omx>F-MW#f7{OvT=P5YPWdYO6!(adEq!8T7f1oJ7XBBIuMEvVqvOP#4?PX2R} z&!qAcL|OH+9?xPrNpckm0qwvsDIGgolvI7CX%Sj(27)R5WMrYgWKffh&?i7Oe!Q zC_OPc34^4xl4U$Jk;IlxW4WJK4!%JH4nVk~KDb`Q&$5YX_(%qR8N!P=R51-%Jttff zARPq9M4c1lhOERb%ZjwXiaHWh5(h58gaWc^jYQkAy0dZzvS3;t_ul%weY_)TvN9OpjWVl)_4Nx6 zoZsHs{%`-qzqoSs$``-*N8kJIcYp8qe(ysc{NM*a^np7&yZ8tUUOl6lf&(VTSZ|bf zK~p_#2&RDA8nl&Rt`%8=IM!+k6Ro^c!ud||RdOfVr6Q?fstwLEtq#>}MG^(GS}xj0 z58J37+yD!LM0%-cDw8!xjoVbTS!A&CCL3qbHys&fIm`w$K}w$0VS3I9*G>$IEu)># zPmt~_4_H@ViF?3&)727DY4k$XKPlujRj!IUb_v~8q&xY&yB^R=3p!Y>T0#qw>;g5W zcpeyXVSa>b7)B=yBnFMll_aw~`3p3qJ1c1s3A)%ULp{t1nxgj4B%++_`=G+Ur+eedTBPUKz%jcvT@j=Z%L3 zF|S1x%9}+prU{3YQ&wh6G%9R^8Ztx{Tdgnz1nkdef&4{SW zl7HbV%GYWV;q)F8;zdl?S#Bz-ajImiW=dtWt@ukuaEjDv=1_t%2_`HDq@(>ywr4@? zQP1d-oNyRLG@n7b75r=uf79`#_(%@7~ zA?SQ3f_7*a3w6j)02IR)P=}KN77TG8=Ya>#;TcIZ%-eVF@J5F!7F2->qJ?Q@#>|#b z^Jb%63Kg6Y>r`}udqSQi66<`yGH!5TVtgQXzHo<#L{vvkOn?kP;h+Gegf4b{nw6!Gt5D-AHiMmK(i)U=b4#(oJLmd?~HON5F zxb^*%qzh6_8A7}pMN0}K;jcIbv_y^Y0xZu$%T9PQjc`jufc%;wLApsP3Iq>!)fFi} zkI&2Yl8?kW(ubF#!L&^j2^~x6j!A-&$r`t+pbT~d8I>hs34a?rrbUK;Cx#?sg_;_$ zE?Fx7kidaqJ^rE0{!n4^2M{iVS|cE&m+V;!;pk3e%>UKHn*ebdtyy4|)*v3=fSM4V zWmsHHplza*k~=0$dP1ZDB2*4vWN>{XNf8XQEVz_Qa6HcvUd#ZW?h!vnSkZP2!fHa} z=T@c?OUs1H%h!gnsZE49mvZ9?nb1uwLJGq7@le_U_A>2Z>ma{xE-}N}4{{ zl}N{djdGSV86i*J=<&4Di2_6cGHD5Bss}kV9Fvv35uT=6)zv}>1wPI@dwbVjzxM1m zzW(q2-T(L1uYC1C|1bW_cf8{rzw)tx5(^`fATW7Hvnk z^rOvTaG*-(oSzh_)n>aF0yUT|neNMj-%23KM6p4Q1OaA2ReB+FC`nS7YM>@$sJnqY zxuv}y3|QrtbrjH0i&Djj=2>)1{(?mZBhI++AR%Agg}Q0 z(Q8FYBMKXn$qy$`#AN6UBaPGBk&7BujYm~*OF zC%S}_Z?Hi`ST;UwRkPX_YTP zg=1&q0&H3^i+jsO(`j2ol=Lj+J0c7A<8s--lIcuet~!h34#SFCs$sh{%h|W;v{0^& zxWWN~vt15V@|)86G+k$Qc(ac9LFN{)7<0t__VzZG+UN%YYU-r~W-ZcDVb>yPvi0hL zK3(FK&8Q+64Resp+unF1|G{go^7#Jue}FAyk3RM&o)3KZ@@3T=!!Ou{cgcticwzlW zuAiY3zz)>@`tZWJa}PiC&{I!7jWHK?kpAR_7cgbK{OF^2^C)QP9}b~peAUSbS;c1n z7M^jJcBn6-*8{#~5s$p0b~rZ(qd!J+*F$Ytphfet;s-@?hl2BEH361mbG%@6<}_<5 zRKd@)acJgWDTOSnR!acOrz=EV0h~;rC6b%+ivbY0V@8A#HnqA)3s zZ*_BX{n1As!+TaX*4M`)u1zJTi%+sfo0@rtXK6YCIWt>Bb>Urv@`xiIa_sHy?cigB zKmPHL{_qd~@ROhX_22pJ-+9+Fc&F$&R3dJcL+8Y|`0d~M9SlXW4V6z);w3`|V}vL?aeM1qpTwd84Z({=CM){E%!Em+8jC{ETAxFD zU9`~pj!LQy9g$Da;DJhg6)vn4(P3HOw4O8>vUdQN2G*|vt%X?@Gr+gK?QQtR#+Uy1 zkALg8e(Q-Rp1?b1&_xXggR#tmBDYMCWVMa>C9M@RTU5fU$wX}nLlQ(FOxs<^1Cod6 zexVyatcn-0?(U9p8wiV}7=NKV7-7lyV1fzr`ugU&^aGXJNi@e9O2y+7G0>-ur1 zg`!L=E$N%jD3x($!e154usro5p{BzknRehcm)y_`lzFrTR!}NIo7>e%0F-sAIKrf7 zO%QZMIC(ZGhN4&sacmJ~sRYg%nH7zX6i{-e`l`lJYHF@6Ol5(J($$S+hL!_o{9zDzxIfzahky9- z)z`27&EI?tgQrh?;u9bE!293wmM4b8bxij#p9db`dL6ua^(sEvcIC=dyp01BLN2WF z{yM)cfqiILgn0O&OCSF5hrk?frupVKzlE0^UVQMuXWsQL%m?Z3N({Jtu&P_F$*8qR zZdTMK5+;SOgbA~%3MR7nTKBng=P*8GQQTEn$F~`-aaB?B|7Y*bpLNZy@~|_XbLaW? zAhpy&-7VDJ7E+8v#Ugew`v(w?g&6{*fP{i_2&wqbfGQjtoQVp=Kk%DTDK6rwpcoKJ zAdVq`3`jz)RuAfVzS9}LGvs;J+I#J__q*TWo6b4+_IdBQ@3;5bYk1b$!@Ku87GRPN zVbE*ADI%lUD7Q0B zJmV(Ow$S0M%>mBHJoM0ezWBxe@U^GE_C4S8J-7>Ce}5PK7vBj&|K&TCcn2whZ&Iyb zdJCp>wrDE8gG9|^fJ7h1rkDIMD&tQ1RowY07Y3;fFW_0^k{H7po|L|H=`sd{<<%v8 ztWplAyp{+67#|>tdvjq}F(4JeRN`Sgm9p88w3}EQEYZ~7QPgCfIprv=`1**NCElOKen}V6qJ%*Vn9iV6yb~RXqv91;q zSqq-2Lfx$WKG(eU(uG*2XB0btg(BF}b7MSOKo~=%-2DZ56IgWrxc>5(AY1ZV*ZD<#_Ves?;TB^ z``9uafo0&UMCC}DGKm2hv5Xxw04QFgu(f&TPyh5!aOvRB|NJi=eDJ}a`I(=_Gl6me zVRW!}u(Y-eaJvh4e`j*>1CW~|MCgYcAKZWc{U3PvVO)q@JJ+{7d=6s)Y^A|Xy;z=uHXw_tdSgakM5aRr!MAOs;N_T*GybV@Diz7T|R&?k4M zP&aip3r;gEphTyVsU}-0I~J+_@J1K z2t%(GQ|-Gd7m0dkbHz-enTyX^2&kORh(wdp1dEPlbHD|_Oz@b}sx~Vf=&{9WAb*vu zFseGM@P>$2d6YOrCl_=avUy^W(^lDHqo5ipaYH)G*@O5e`Y|Zacf3Mhv~y6Ht-tk6H7%39sE`7$TFN2s>j)9%u_!bV+aBbm}lQ;F5`6q=Kc zl=A9wt1*XgvajLwUhjYS;ft3p;$1X2Teo#*YvarY)<=6d+k-ntS9r3f>*(SQNxG<} zI;4iw5W);q!5mk+dz=#U_{3|cc-t8UExu3`ul$1R8w)E~EnmKP@!Z)nxNwRw4c{Qy z%i%5)u7%O;FgnVDg+$o}AbR?u`wmwn44sqV@oD4-5Xms37 z;mzLpad9|)A2COsBsrG_;Uqw&kr}JgPh;pr_Si%*#Sy65~?|N5lv>T-*c zCGKi><_+FO39CLFZg)qRU-OM@;ST_;)i~yP<2nrem`w2$31(i}L)6u(sJf9?2d9vY*_~_2)%{Sk8`K6bzD8kLX_`t;V>(_9J4|Ru)%SN6N0Cdhh zFoVK-_wXjt&8;o03Gs(D;lUcFMq0&W)jzBZ@wnjC>(_Ag?aZmuckbMI?Uh$>X%XiL z@Xs=ilwF9Mc_}M zP7?v^N=Ssxg)>x|Mmxtz5Hxc1KD_e@!!mAJkk{g3fy7gdY07SGXBsTWQkFhS2j_)9 zJlDRKeQ9C~c4V>7grky)f1}RS@g`Pff`TwpXdbIM6a zwm?DRhviI_&sY=n#Bm+*vZw{qgVr{_o)n6aU4}{Fk^{^jkmv zZR=}m(9i1P=ezmZ6*zxRRnKm5M;KJ>Fc`?D{<^2%@g=5N0B_S-MK_`>&n-}mAU(i~D( zZiutZuHB{)m4YcUZ4p2%_K?Hj zk$CVI^);?e5X3P(E+BMg7=@%n`kE|IOT zuAVt_>dg6bxQvfeI%m(GK`HKK$KZvHBNIm`8}Visl8CYI*PHZdFQ@%|c#z>ZHjd1A z%p2=EJ~6p_XA>{Fx{bS9;P}Bse!2t00+xt;t`eV(m6t{D?BG_H8(b6a?_a)rWsT1k zt{`GNyZmMkZ+hw5oD0P3vVD?cmb++_hFnpP(s{t^)oBdFarU^;C>~d{LGqeR398*{ zezFCjS9{3mFeoIe)`l&S%1%-z5$KOVt>K`V)yVzIPB`we2ym&O8-~TQ+GRP&qaKz* z++iYuW`QLYzA6_dsi4?!8dcr>zg%bV@e(+WS(TU>vv_v3c4MpEYRM>!#th z3W($}f5Y3<@hQTs9Xtwn^Tth7)_wO~!=HYf3`ahkI26cG&FX>(E(+p8-)-Iq42hd~ zKOwf8H*R2dg^PiBZxG%Yqd-}^bd}E*Vsd%u%H{2yZLrha zFtSBrlVdy3p)yb_|(<@pNnw&PsoG`URp4_@hwqnqNi4997go+rhq>5&^co7Vbp;43?$;A3qC&YEj zO(gwB*D=?!<0fFGDWXK#SA*A-m=&eRoJZTk>JWJ;kcSIaISXxS2b_L$#zPXN#%ZNh zRaRO{SY>4`LG<7aZoI67MFLg`a%z@KLq43Za}azFTCO{p+QPaaG=}LDg$XD$D@BnG zvlCc^9^8e5ca-BDC44=}0X`CqJF|GM9o~ZqI^Hycn=V1&7mdTrBbhQJzH7(o3T)a- z6uj{#6+==7_=Mmtssgi|x88d55C8BFZ{NOi-}U=G@-5$T{l4oU?(FYlhQ#ZgVjEVl zO5EAm!X-hh1hJxf_Sxs~twO8@S5{Y^d-hq(r?8YdfBrn4tAWe#3kzedcf4r^QNai6 z@k|ch`Tov3?>zO?S2s5|0khWKOY{@hMX5-tz=&9>%GwVNbp>(2bCB>3FHMz|Dk=+A z2B)oso7@mGlbEszQgN<$%j(47)VVlT`Tl z58tJE`}Qr~$-lIKTl)FHGoPd5NUr=mjgyV*t)2}<(yE}fVAh&BI&<7xJiz@CxIBpA z>(y6ZfBv~=@xD>KLI%SZ-kFSB<#%=%E;&H$Z*A?|xN+kf&p(ex9}qu$OBPobPj8$; zaPZDFMr-_y72%FP<15G{L|K3lYEVdEQbWo;JeUQiSCSc?X=big##LTKz&mOYjxlE8 zGz&uCfGQnM%}B@eFqrlT%CgpckGaVjbgQ-Yf+~3LrcXnS^7l%$8)QkLv&uY#Cl}OO z^MMV9rbL@tsprN=8*nzUHB?nvqXz*E5D}?CoL=_u(}j2Dz9kG&ynnjVznqdvrJII1 zB(l~O_4}|_8A9!7HDTm(_@j>W>$NErl)Bm^6jBA7W`jbpik)0~Ns<_o5qogr2b{cc zBw zOUmEjoTX$lkB%%C|FF!%*I5Lv5Zve*yDKzJ3Nbn(`4r zRx!RqxV^RcxzGKdufP5}rdy9c@x(_z`cb^q9qw4y13<}op#7Du0m*9?p;tk#Dx@ud zTtRp))D%~MOMFl$w1|G|6$725sF_S=-4jjPN{!NA_A2*Mz07lEm>Nkn&XsEI@In`I#tBb^Ki?$GN5DQYp7V;pr7hfb4aoi z=*VDNrb`rI8D6tBvmr#PGpee_ttKra3}i9l!9|>s!+?xuDX(9@j{Er$6x{2Mmy6~%A^Y!jYAhlAuxw*Vck`Y4+^hZp)0EWct}N-z9tPVl|Bd5qS6C2 z8wRIZ8rcLhShYGMwfbucwRE75w4MUPK!wVVbZH>698`^9mRs~7$Rn)eP=eJFDJopW z7l~jV!WP+^zX4!gK%y*7bMn+VF-FJaDwC+=eWW-uAD&6WtLlO~A&k8tPzKND(-R_A zc({0p3xl8f0H;7$zkwe>i+S$3=YIbWe(&<7%TGS}#Knsju>8ZUiC-?0&oX1eg!vQB zprbmlaKbtDS6_Jrqs8@W*Pi(Bhw;&Q)H^OMV%yy0b4ol1j|&j{3%I%T?YH0l-QWEj zzG8pvzUv?R=*Ry4-~T>7V2FsMt_LsFsSp%xJAL(@i72xQ)`FtU+zXGsF+$rGPfy4# z&6wv2gTPUNnySfYUSDuCdR-{A(t=*e!t0vLYRg;nvTbzF!*pN#Oa5{4iorbfkc`-? z&r9_(&y`^qk{VZyCUuH}o=BirrOP2w##$M6;WRV+hm9{w!Nyl^Vo8X8%6lcTN9V;_ z2n~saVsu7TukbALJ^mP9k;uUWmvdqvQOVMAd=v+F1S~F$c5sgd7KH0q5Z-_Pbs3(i zmXX1u-v|ru7bWG?sSR8-ym;}v9@oHv4QpwXV$2M)CPh@S8A#eg3&5JKb>4PGjgmrV zEVqUsE@{*Oa>DROt@HW~jg#sL+~#BRve&D(C90*Iu}!rgEVa%k&&KXI*1eE;z_5m( zH&=K*r1^rWf^?j-!_hj&XY7z^_FzB^5f^L5(`&nz(-^$DLT!JMGzm&cdk}tAEX^Y{ z0i><9$lwP)*+dTfq0|xujAN2Dc)+g6v6pl>;2BO92j5*0-6Xr|G5(1Yilh)P^jCQb z5uf$^rV0s3vnnOwxxDlo(_9Pi!Wf@b!A+8QSf8h6$iRji?{#rfBGLj@PTjosh|2O%!_cQ9RD!!8Q~2Im?-hOF1|O& zg&$<`?wPgK6~2K%K3FezRqbQZNXPf_NFE+I-2Cm|{>*dFJ&TayQx-q^pZ(ZF?|mOk z%5+Mcaa~Ls1|lS3nQ&JPOI4>t0H4RetABXoC?|n&!=x~uq{M<5{Q{3V+o4Y}SKU!! z;Z&7V^%tBSMvqBRZ*V%Coxp{O`};s!DgLR~J5luMtY^ETo@AKL@lMdjrVSW@VnWAS z@ZmvVoC4+Gi>G^+c>@4mpaSy3;%JZGmBGgOIIo$f?$FSk5t4UqP?|{v6-sTu&DwM94x2%qj)bA)s-jGCC#mO^b3`mYcwl zOuje6N|mEVMB6J-Fm!lVa1PIk*N)vlf;K5mr*873A9<>EXUI`%4AU(LwcFA2$#M5e z#)(5)bYWXrEYYiSTn2S70vRH0yhzM5I76`9t31`vGT1x{$W?3ad8`rq>Yp#XOViOO z!Y^8x78S(1&duP3-2XFs@EV&oDbnnv$gg0b9hUt*~j*nQ$mfLcE?E zT5lvC8P*73F299WS|`4lfx&0T1mQ&sQ)6m6YIa5M{V_xxMaB~nkgupv16pS@LMHzl$(C#QE~23fH9qyD5QkTVE*H#OC^$81-dO_tRZi2XoCQBbh>&-{?BgCkyvC#8SDlbTLZ4M1DMH&o;C zh9vxWtdXrBS1gy-msYP``6vJ6zr~Y&zx>O;jQv0Tm4Eu3-}#;2{axR+vW(Zn;A8Vx z<8s6L5Fes|FW0VKz5l@n@zx*=4_d9LQ;gv9C>}ogy!}Ui{KudB+~@GLAy%9J^)LS7 zl`B^^aK|Y;m#!xb3B4k;b93Cg3}UY%Rqu?b;;Pe3;IcKYU7uP#1-}mV`EkfCpd9Up z6mTu#4KongSU(MTyoHRCS^GASs#EtyH0x~%<5a;aoALP4Du0#JOMxM;dNxt0;yOSI zjM-Ek76}ss-K8b`tYrM52cWR7t$?A*ot|V(IUD>WRQZ#nq`+frEvkZqN2}4W z$&A;jMe_b+PtMY2k-fep0+|3Zh!eU=JzC@yow1l8^mtaPDe@}C_eCx7mPlM1L|>p4 zz>;w2FOms!8mNLYja$T!^X@%Q*uw-i%c_X)?^@JQ6$ldf7=ap7DNf(O zXGS@;2n8gB?_h7l<0e*vxJ%;3jhpX%&x5$x0o=TtiaQ1vrKSA^z9Wmbcwmzz$N7Y8 zW^Qa?D$l+4Q&wf@IZ^}+@~dcJhi)ix8I_q+2HQ(qqm(;d8%X6e&6?HHg6NA(e^tDL zvnVQHur#(`Rt3E)JyH#1jL|SkZ$d>(r^c_T2Mk)&t56sc!ZgjQN+!T)g77X0=6VWx zRoBGCR^iHcPIU^sE=c;~U`-AXcb6KxxI`F}ky7&+hK-KJv(;H*UQ1+H0@9^wNu8`J2D_ z;0GT)d+t2mj*1OlVD5{_B;Fs3>Crj9i;t&^0AJ28@Vt?J)9=}5pMCk|m+&^x_r33- zOP4O;<9>K&1MWSA_Xq)1StC7k-%&YmR(z4koE)Z6A#&COWI1}E$pV-}hX6a<{Goer zHyG3kW)LNMn!Ou<&k^HTt*D>bVyaPY2X5uE1i}y)lqu z6=2*_%KA1BQZg3e2}68=1~-#qxs0=AIBCYSn{p8~X&^VE==DV1lmE_Kp+M1=wEpxO>{u{=g+ahZ_6>Oeeg8clKFVHKlR~j7i+gCLGw8@kFs~94UG7 zf6NWCxkO2jds2APvz@z=Wn{9$p;%wg(o9??gy$(eS$awq?QdmD} zDHd99z4aEZm0>A`B~+Y|c~PdvHCM{Vd~<}Xx`4I;i&&}3x{mLi$Bmso|MNfh)vtc_ z*M9BSKL7bY#5e2z@jw2r9{=#;7cXDNk`SD#8Q1!7t@G>|+%HPV_*w%#a*Zbnk%60d zVd;PWFa8CVzxZ1Hr$7CVKJ=l-pdZSy-vi_BE@B{fQvB6%z%Ou4Q}t~+v4RB_zyJdm zfm{$`mAeeIa$DE|-n4-t1V+OtJ*d2x7K;wMcf}eW^jic|WjwJM9N=?oIfL{c8k5I7 zjZxp63Z3xXG)tQ0%Hv8x4>e9(PAJ7IaXg2Wlm$)1oZNwVc645V;#QzhB7N0$VNqPm zNw#pNM83qS5(=n@z8S)C1 zIJW5sKB+2vG{*~i61YoJ4Nt3KoX{B!PQyg2HqRfWtnMrUah_ls9QovtiA3c@V#HQh z^DppthFSB`=$0_|Es^-W?-7Htnf{7YlJ84l$m3~VZeTT?6jRgDXrH19%K@I(k#Ojg z+iCdlAT;q%suv99q7=$}940h1t(XPJ2Rx)BJk1(s(U z`r5zycmMB;FTVWU-~C;f8)5l`g*^U`M%xh4M~_g;_`@m-->koR^VaYG{_j2i{5Mee z#1l_`FaEoy9x@{cc=wf9vNXo)a}ooQ^HVQTSCCDT4*gF-Y&YWpemf@q|$2mDzVA|7=sr#B6%ZnreoZgb{qJ;xGRZCRZgmxs16_4B+?5y?8D)D2J-TVGSB(D@4@pc#an; zdb4~s1y5aIoxn9A4yAZf3s0-yUPG=0*@AFp!S0;wRW~;Vi@6WDO9N%k&F9K&dN4p? zJ`ss+5STy!g_e{>i`p>Q}z<_!E!cfBibX-HgWxfqS$w!dE5m zk{oz~PkK>oX$jW`UwZMy|KoRm7q62!f8qQ`Klago@Pj|7?<3XrjrA7R#(1(7Q!L_% z0f@iqrUpmTy3fmFNvn5}0l$t1N}yR)!RaNrc@+>O7JOEabtJFF<>U|l(0n7mYAk_i z-hpcn9FoIKIXO#FOpQ8;Os>ajJSR&|9fyyp zUu0aYO&<+cm?}ETD#fu(nGC>_p+cWj4dP=A01_}*!bI4#W2q6DfC4#CEXixXvZD4( zbh+R#BuWopBqM2QS4iwy;^ zY{bBYKVcF43@F~t%9mWII;oLTb4#oen=y!Z2$~TiU!aGeSx$u(5lLpe%2 ziZUHFSuza7P_Wgs0ZE8wX1I}ozcuj&-}r=a)P|bi{UmimbVvPZVW^9ZM{TvvilJS! zW7SyYtP;mVFg|x-R1p?L)!HbMNJftaS+-hWqDP{Np+{zR#SLetRXu?_Dxya|NfVTh zw`6s8%enwh{pd0d_L)1bzzl-uSIK%Q5Z#5%tZq;{f+lX!WZ0!M;Q3@6JD(%hfiLS3bw-wExJZ3!&MDVm1rV| znefr%9f&+u;R2MbdDyS`*$w<*xxq;V?iXeM(OFR%t36<^(9>s@#+yZXb&&l6OQ7A| zZED7yQSzi9Fe-qLfBX~pO5tyR=Cd!p^wR(G>%aa@AN-~t|M8!|eU~It0TEtnu0#)}PBO@_`E1+WSUn zbsVyqm}t*so@{UqcggN!j<=*QS47i99-iQqumgU35swV9etN7)F`RI0+9s#NnK?IH z)oeLcn>K?C(C`c+lZ9wb9af;0NG-1k3r;Tv<-{I>Dm%nur{A^eA(Cx8jGU@+&8uV% zO+xs zyzxQ8#dm{AV_w|b-^FOLvVbS3`7SQ){8is4f*lWzImD{|oMpnCf%F4cVa|A>jYOz& z7Q`r({U#NW+@!>kLB9}}1~{}44pKUpN$wz4?i_%23U`VvJ6a6Zf5MnWfFnk2)X128 zCLeHo$w|6B0($syb1;!75=tCQ)POE;YaE21bQBloJ-&{AjUUZbk5w+AB}v>Hq^ z1Yqi2dtlOKXJ~CqRhi=)0hchW$?c`M2*|UV9P2ntaJYyQd`4`xTLxpcs!z1dF9Iuk zZM|&LBFbhVg|P&TW%>Mo9foG<1;HhEVAta+0XX7#c)XydlTxS2vw>XYaqO+hi3M9V z)(Q%=HI~`X3U_=K>1M471JNmG182D3jT5JRUd@re2$geM4vXGOEYn};3#YrB=9Dy} zI;ghm$+eE3o1fx<=Fv54 zl3~!W%ChF+RCXmzu#2ftR+U2Bv|Te{2k3#rIN11g20WaH#*cd{aW0vU&tZLm=?u;{Zc-Cp9D#!JF@v~SvR9sBa-%huo5Qgy0%;4?2l-@IRcGks60pr(`?~h09{1ez~JpuVLtR!}J)x zQJRSYo@bDvWXf?SqNdDHDrm&b7kMj|HYG->COC&|?GYvsBYeO!IH2me4UZw3L-Xa4 zUL=vgSsdKO4g)wp!#zNmQ*AV=;JCd69*FgWudqD_uJR;5Us)W}^>omQ3F zg{=PO4m~s$HXt+`EHN-y~lL<@}al7x@#fw}Se(;e;UwGk#=bn4^>90TYSAY4X zM;`g$IeepHaq;Gjn_qkCtIt38>>IDY{=kC|+<)JFc$4ad3+Exi3yshzS44Rck`!4V zvOOIBAxtY0mgyb-$y2N(G8CK;VBva~Oq!6WWS$exwStpMYd z6h$4`Ar&+*fPf@`>y5l>$a^u+N{26F6~UZFY?zcsnn`Fu0baA%YXSn=4KqoXsVZ^h za{Tl80cq9Au8*r?xIDCv?-A_E(_;&GMhgn0vQ!aqdJiRwqOy&F0(I(??oG-Pq8V+# z6VyuA-q(d{7H%e0ku&GYTU4^29N^U~vb4sTw74+mi;_Ss1##PCaH=uY zX~`xu)LA;IB1I(~;=>*3G-ZWtrtBC5N7y4U65{e8j$3^E9Q zP{)kPjP&4G>`X_J<35t4g{5ZQKw3h%#;U|Zwy>kBDm-n#5OLY~nK;afIcFRTPPFgz zt95DtvC!t&qNG%*WRjqjwI{&rfflmOqIOL)Eir#{Pd%QQ+RKz72>J)A1|%zvnIcw^ zJkc*24@~jR9Q%8?X%O4?&Ngl=^*yd!zZTynWN;L$&NIgicM zM!PdKQ;l<>zsl3)XvA3@n^X^#8-r-c^z$G&uPtcScsdMCU>z7xgg=Pj&)76!d$t5z z6pJHj=3vdaf>L33?$T-w3BK5fDU%_fGI|gCY!- z;K3@9B3v7C3sUNiR+biE@3YZgE9^;t--{dfa6Kv>`2*aXs9K96dKb4)6tiReM!P*C zg^#^bj$d(R2wTK69uLTA^W-vJWiwlRZMNI8jM9y@s>sO>NQU+XPG3)T!MeGmAxas= z60v0PuYxh(HLKDjA_5kK!8++;(kn-7_NidRn~0pG89g4w@rz3l25k)U^I$c2PC00C z^d(MY9op!x<8Mj!Mp#v~uti|{>i|g1NsUK4l*QsGRA6oZG``Rb1WCn`2qtDR8k8yQ zA<<}Zg5m`&ok9Vd>XRiJKFop;<3?Sr%=kFrsfQkV=!ZW2LmTTG__hQ- zPKe;+p*x7LtgSxz0G|~cZEbC>udm?FQ#?Zo3YLg?j1mjN zUA)V)vvv)CjGGcdJY$ql-cETB4wNiBn=c_bfd!E7K zYDZQ+-er^2gwsr}gutO75B`LLG|?&_R0Mj+tG4J4Eu_g?)S!0fybln8go6NAeu%^( z$1=QMYKg-apyKgwly$iA7hXeh~24d?@+A~qe*q>wUmzPLRdeMy*kra5{F(2+q8 z3}7QCeUV@O7_$>A44RUHKJ#p0D9<{mbir_sfg)E}3i=8v>cRs zD_M162t)|t)G5Bdj{3k$x_OmoXPX}(L?f4G#ar@ldNrP3Ep$p(7`u@aN``0ETnB$q zUqu^h2+Bt#=s5hPo19ZDF7PW2`1S@zGwDPPu~`vRzhSln^qXlW7V*LqJ{h<@8nH-k zY-OczD1sEGL|DtM%Ek%M{sF$n1#fWO?^{3qtyiyJ{onu1zxl>DzJU)9KJdT;|KLCU z^oMY#=$W%vWI}dxa}&O=tzu<}x7Dme7AFN(u}ValT#bZ9B(R@iOA5@drZyDmAfVjF z2}szmc10WHwQHgR?Cn=xe);N^tGHoIIU`0H?x0<(E@;#-+Zw92?6%6KeY4)gITH$P zG`5OSU{n|I%T8I5f|{zfhWdFE+)<{z;{d7@wgo=}X|9Lb40W1#G-8YkH@Om2iBPI; zFhRHyXHgl=CH-TiykBZU%|S`G-Lb4p#moRQ0`b377K;#`7>5)$$H%1kMNKByUg?GS zuq=(V)fDVTYcP)|!MOEo5FL`Z zbe|B))mm-#D7W(=#&9<#yw`Ul{v0zKku)S!jt*tW^|h>dguvOv#2Zs>zGqTqRtksZ zjz==m=5l|IYfho-z5yF#xsCyM@gyE{$`N``JZwk_iW=gF=QC~srG0!|9w2dlr__Sr zl@sGQD&tHzJ9@y(N(4Hrq3|sVe1q`8zxZ=35pUhPdFASr#~yv;+O=ydE4c25dgf{p z7vA;{FrQihac5@)T5y%KD(a4bIXs7ROTdGBdhv2atcQRG=-}=MOIh6K3{Hjy5n*lO zKQ|lk*t?YE_TX+3UHS1!-bH9x4YJKVyr!AzYudq}Jheg=Rq2ZMIPL*A_^h5W1Dbe& z?x4i+J0>JWGxdlr?>m$hv?j&#`}A;5FEwqNaIvK0AVuhM1`n43vG(JlmM8fzSmNed zzKU5{rnUI)l}CJ0$=ED$x5k1mqV2^VCS}5iV=Fq29%dK-IlOczDTOjfe#C#>q!; zR)k=My^~w!f>6><&TF{~BVy<@}EK+x<~5*qGA50-8GU6BI>Xx^{AJucz!`FZHU^<5%AIKYE} z6gW6oTV1|<;oJ}ZqaWJAoxr;zyoKe$#S3d|%OXWiv^@gg`E$ILVOL(!f}2utfiWC< zI6})Qb}AfyV|)j1!dvpQ67U{*)Cyku!!eDwH=`=xH7?}hvgp?4HeJT0!eEGM!jVNN z!i!W`FzZN?j_-X4t1gUN^;IQ90u#DnsPz6rx$3N%1LkH+s}fCI1sqN*Vf-B(tx`sf zVBVK>rFD8!sB!{gPVAICQrSx6o+|<&#YSLC0x(A}RS7FB`Q}&fXqoCPij-&!T3YZ} zDVTX$!)*m88Tc25D{Y(Da~g;OoV&sGu)TxjCEf-}U2?w0>xYJ>PO24M8YwVHJV4_w z`QyARsS3v^lyehC5z>6vlMH{b7d*j=u~d<0X)`=6b1o)lZKx0(?U3@hAj&|^CX0k!J8wmYz46y2LYC&6Tv1Y$evheR&y0aTF}_5 z_;CK=oHZoc8g%?YICTRN?XnE1H)KABg-n#}_#Nn%uz6D*Dzl>WQkjh0nModl4gOlb zQz{f1vj)nty|T(#hF8Qoih9+~m7(;miarDoykWs;kYwIsx#O5H2Q6_NP_}|Wfh*Ib zBOC|p6JoeGya>*Riot`w9=`NXm}JJ9IgP?BKVcxfn08mrQKDu70SEqvGS7am zr8UMpNUO8rpPWGnv1TX`3zj+#VLQy7;4CLw07kkDsd`}4$&DZu;IgPYSXsGv{sQkL z#bra>RE2L$avrINUDmU(B8y*{JB1Rek3N^f_F?+2b0`4?JocAny$k~TPQLjgOHrt? zDTulBG0G|0V+5!L%KUH+(cHn)QPkqf5t9Wl`*uLimZ~&KAj@+jaYSM#W%D1$%?fXm z)TC770yY914?tDZI7BrW2Phq(0}c%bl_^vBHJsy^IpNJC5W?<=F~*K`%#$m2fNKJ^ z1*Y&=4?z3U?wICDP1%`KI&gYhU^CdBK6ScB>p*yQ7-%+fZiIoAF#9tlb&*+63zv$l zTA0eO!5|AOhD3(6F*=nAwpm7CHPXtU{Ayj&1&}s1M?-fw6~GaoJ!F%-OOt=N4Gw=y zH~?%IntT_ znitQ8dRN6{l`bG@c6imy3WnEHK=MW1>9H!POBH(_dofzG^k(LS zkZRw76r%F0OybInP=uPLXN%ryoWUl_1VE->yo^IQp5T#i9~xS1BpP|$S4~%mM-rK6 zG#pH=0Y&=V_lpH#2|9HjJ#dC{)PkN@L_gI2Am?a8*HA6Yps=a4w2^7HLhX^=;s%$t zazj>R3MWnkBvpBfPj0=+JB&z;akf&Cn9{FMLlwo2WKKC6lWp{ZZG!CJ=vDDoiA@w5 zI^N+XeN}$s_#u9o5=&%k$aI6$()opmaNjjsGmHptZ2G)iQ;yXVa-9i;O5+cPoTm7p zL{0%(`-ym4TMihMS@((Gp*3xeHv6ObHCQeu}qQ*H07QHz5BaI6l?DOXFh2?`4%5XzDoK;ybLSJWQXAFiYieBDi6k|JMCJl&Vt`5GEJeGv&1yoj#3u4j21qm z2o}+g$4J7JdMY`kNhz_=S$bG#)?y`!l2&mqSaABH(%v|E2cu0aIo^m~n@B5Nys$>^ zH3yL?{Zjp{=0icnZWQS#iYycUw$`%gtAei*O{7R5dbJAWN?*^%*^9xbo z&Apv1!id<0;A!Ka6O0=2on5?r?W*{fK=sGs7B010zy5*5Eg{s;1m%4;Dw}cM54ifz)fBK zvM#7F`j_8&8L6{%bvkXpi>GN(O>|D450mE#(+t<*GqIgKuo9n!u7XF)r#IcOGlXDc zMOI%`ghSLho=l;mPnA?)!(!D6V7PalvudJ$rbiS#%Ud>E>^#$?}>|ohDXf zjgEu~>e%Fq#!a_dX)VzL#b8L9+(Kcw@;C+(TsA>AZHd(Bq2sUWOI94G*FAB%ZmvaL zXg?~WNYJus&8kI#BhT6qr@EHB_slhGE>uM7D;^3(#{MT4c@ zMpEUd61yzTvrJq_!EG}okwszPM9^jQjRA2U@Ba zwCi{q;&`d4$j>A=swDPP4XvVuyczZ^%wK1@%wIJWfn5^o<13?7M2wvz}P;|PRlEr(tG z%dw*UY7>rpdGSf^5F_BLfERU;$M~yTfPn$Wa~&eMcdIEI;F-W{dQm65XT)yPI{tAn z4{!;MO{#{NZbiCH_e?OB9J60R%pL)tyn@RMxOguwF5>dqC()ypj=K}bET$)L?uH1k z!N&u4#B2RG62gJqvPej0D0>71m1GGoGT*deX`-L6;p3_u0nt!c; zFUg2+l&OMDn!sG;=pc?iWD!z_!r?`V+q5M&hN~E#voL2y#i!tx@u+&?(GGI3w~JSm zFK(=#!gql1HI5hvJ!XuLrd?u4a14CA8dnl;f>>Ty#?0?PU$pMZk;F$uWaR|6q+Reg zarm$-MbA_56e+KHVl^5K;Kp)rZG9bkP_Tz`ypO3@tzzg1sT7q+jlV1qdsflqi&QhNc^?&7NT14TtTAqgFuZo6v}}9#Wke95i&)q4ZiQ zCN=BDl+n(Ma$f|oum~aDbgFfiWzsxq#dcCKw+OhpsMX#(S@zE1w}uOT~tVcI=b`kES{tONF15cW@}aZf>{`v z9H&Q8)gla4j8$UNR}>f7u^LH3wu*Epu}>Ra=;JAECAZ6xLHVkZ4h)A@)QJg9Ym_7sHC0uUN5OO zQFXc+oN_@JTV(a{s6=bBnr&VmEEu{&Zp__m<>X9T3x>JI289)BgRM#EM&e7W`iQuaGRD<8ZOfvr4#is0*(Y!5%>cgZe*WRKzXPQrL zi0L6b^FcSh*5S3xNxNLu<+6}Tc5aCCPdyISrC@v{TJ?|{4v9w8w8ANxNyMNEO}~+2 z;^==H*gT}*6^E&fmrIWNoz^j`e-3hNe97O$g#Zm!>Y7MIR zAf414@`hQf(y~a9n^c}>!*3`m_)>%E#uI{d zijIBO$K9$di^-74!_7krg4ErykLx%9%LSo^vKsfIDQmXi^fgfwRZ)5VDuW|tEko#_ z_%=k^#5L55#2Bi6d7^PT@JE;|L)3)g!55_+MFr=Kvi)eIp$ex)zKR(|^2Zsjoj8xG zv}y4yooWC*j?sZd=~c~9$-I7Hy4UUz!kftLO5IiUz%Th@t*MW~266pkZaUv>R4E}j zb5&?c3@xX|0fnYKl=s$SP0##jB|NiaIFayiheNo=j+Mr#Q>XQsVSm#!rDG>r;YCeV zhN2!d8y1S8f5&q@kT(^W4Mw$WQL+0ML}Nk1icE(uJ(6^A77q`ut*wU&3P4yCo$AL{ zZT!|Ma{1%qevYfm_2cwjM`x{;lRD3qs54EeiHM=DdR!P@+qklQ{60#Iv$rMaHTa&b z?$ivah9L)EGy_6%47~^;-piv-C|ejl4UCkb$itXzaJ=RjhI8(!@(`K1!kr<(FGNh$ zJQA=q1kyZA&qZgsAXFo65|A#x7WS0~&#W6=C13z7!>=AcX-^(#{$Eg`3roXn582_8SOQ2A2xm|Ew@?gPUnZH?Uf zj&(&du&5@y!SeL!Q@ldRTA=8Vx|_lokJi*Aq*|yY@ol-jX zx@tV8UI@&&?*=*#Hm&lx1A4a@n0G``U7US~UaNiA$2L``ZvuLn#22> z*)&0v15;?eNm5K{)++(o^B^t9GO!eyN${Ks9TZP&NF@isCQ8HEBR?_}7?O;nRHy8d zsSZOXu8t%bE65XtD&;qGnl&Y=Qx&M76)BMqU$t!UjIzpQ;VqPy0&h-<1zNje+`(D6 zV9Hw(;dU>*->G@Y0#fcOHLG!VcOOlTpJc{cFZTEN8g{fx%?j&-@9gdES!D3i z5_V7s$#lwKfC!0jkT7ajq6kK6pFW;o@;$=qE9Irk%X(&?wvcMry&e>Kb6TV%uc0kH zgtO4utomHZ;_@orP%_#X;ah3A(8ODK850{Nfdv<8KwQ{6zy*#yROQOb^6JV8X?V@$ zJ}!DJt}gRJ4=4~s1tT$y3o5HPEqa((iwS|kGJM<(AK>G-!T0#%Qp^&cuEPeXO|Nw% zQ=Q6I?A4H&Q%>WrDJ&E)>%@^G;I>UQ3%?nGGa^!v4-S8$yW(M1PhFTvx4%cz9i z-O(DJQd?XcadRrUDIC!5xT6XVtYvvDcbO$hjb3Wy?XE z!5FY7-5zy{*K(rCZS8Ku6MV@}Il;#8gh7Y(?$v3xYFPIyDQ&V1ek)1LsyXGQmYYPO z5YpcME@HX9vW8lKy0z7{`>tQdnhu#1l(4PaR^^-+xfir_)r0}ignH*W zk@hYtn`bZ$X2>W=6hxne5{Xi43o&uyQ~{^o>(f&zXnh$G%qHavjOet5q-XP`IFOI< zCMCA?6?R>gaikJ>oU~Ff^M?L$qM$?+lbxjWbrva#1yUH8gtO_4kgMRe;i;H5zo4R_ zM9ry^swx#EUh&WLAW>*Gap6?=0=o)(SrIP+lw}L%X4dI6%Yh4dnP_JiQ09T@{Ef#L znhmC%&SbFEo=QThkm}_(1qEkA4;bq)DFjU+1;n31l`v@MMG%6{qWZj&YP!(ef}v*F zsncS}`mm34h0y8hNtU(W!+uhH5`noy0Nt(Di!)l5X`r~9TJqh46$fcOPTNtL2WY7Y zS(~HQc);c{8V7hB;yHH7JRR#W&^b#F0^<&)jT#<6kb{??p>g7p%@WLYHzO4|mix>L zj}RW!%0T1F2&Jq-Ifk>g?#>OkG=exa31Lw8*vnKNP{wWO^3AaT-LT7q;!zPBSVctOaczx0xx z-XcsrJSn8QIMuwQX~YwyC=mjNq+pViQxccx@T?QFcvYSu9@n-i8COdA9cB_zM4US1 zF;3;NtYhp-I({arO1yQ)pVWGWM3t2BcktvQVQiMu^r$5Km0LK2AIZF0fiwJj1WzlA zyr86MSjUDdPV*b`X%X6`vXQQ~9AT+A59WOv z3B0+R##T{ z7WWpH_HaWS)bOezp7%P~Tj15Jg%w=86T@O5i>e~9rrH!qUNS}wN>tAP85x676+H7(>(~QS3}(@Wr&h$I zYgz?h1=Fk%vt?)zN$J+A5ap>|^(rxK^GO)SQoS#Tb)~Sa3g*@CN+{#!rI{Gcq4HY4 zu9htZI&_=DvSIWZFh$C#S!bwh*uJQ#+v=>A>$}OOH`#Q3`Bat5-Yke`G}PGM4daFO5)CiE{(&Ii&N|#$dF$4#3m2BLNaypo>~qqn8ZkO{1XpPo z6L8I$pI79IWcGJ=u>{mpMR9)Rn2((!1fo{Opgu^I*&;fLREq-Fs?+qlb8jtF zgVr5{Aw1n0?v+v;FVG8OJkmjP9RK9lTOS`}2&b;eG&>Z47*RTESF+i=Z6?qx>QdBQ z22?JBIfVJpy_t(a_0;?8(*sRv?OU`b&`>cnctM!S z@}Psmr_tn?WVx{uPP|pKH&->#%K)i@M?6Sgs9G7M0HcU+5=|+GLx~M3a#Z=9Xi7TH zQj-<=0!g#PN}ZJyXj_i%L=Ypsz7&k%0zV!yh& zw#IeD>gMLAUdO<+@2$7qzJ2=+UYeg@(_kB@>aP+T$OAz!6s1*S!DHEKNZn(ZMVq9Y zM`%|7RF4*!P9~rr+_qjed&5v95VXQd!O$$Ny6lLHt6Y%tIxemPE-&N4ARF!GolPW8 zpTYMGxYpa=-rgCFo_^}-ciwsD(&bC%&!3ZeZuQX=$p@-KR54#xP)}Z5rR%S#CH$kg5lEE*5>YLcYCyr(cVRs+T5K5+7gMTzkT7X?J%MGe20tnh@6pvAV!T!iOGuXl-o`QrxmBE#Z(h z;WmLcnWZV^3*?$LolKxzbBf<=;28{`F>xgp*+}#f9H(RNaRT*T#%&UG2 z3md04pqzJ$;#ELYXyu7`${axSr-4`!iJagtW-vY9`2NtU#RRue$S@!}m zSxzY__AVeTwVjE3RYjU1ls|8<&|ipDvmGK@ z$j@g#BpLoFS4U*c6M;PHIVP$xqavP=bwM{(MQgnJ+nLF znG*1~p+t!|o4Z%Fdi;p(ZY8YBGvvgU#$kxgq&=pjJtWbT3Jeg4&YeGx z^~Svz6dU?mD?N7oR)zs#@olV)T^nJC6&|zn*oNgD{^2!`z&6?);qgGcIBo^?v$$~Q z_9i~0_P~SpojrRtzS49509QLyIJEPaYr`@#Ivm$OXw)FgO!e_oggSwbI?H0KvEw~z z4rf(3!$vLOGGc5}$Ipc-B5_Kqk#$I4@E|KLP-AHYr`!X^9j&*j`3Q&-`!IvY%8z6J z0$#Jc`_@};;XDhk!-Isn)2ciIfF~wH1av^W%ff+{1H{r}CPvP2qopU=kTRKa3`A`5 zvDwo|i=oN%A4@5#jN|620XvpqI{`H;0+UUPc^G0pjN6n`0u>j9Q)-HUc%V=45;ac= z3l|SLnYk*l$yTEb5>+Mx*^nW@N@2y)4bB1r+#x&ut6O%hrr;w}dx)$^6NOQq^=Z>t z5SylrVup;8C#cEuARke~Pn6J8W1zDK2e{P`Y-@PWo|LZO0|j_p#Ns+~&%f}EGiT0x z`zJqn;R5b<>u(`Bn4X*jywaAMVv4#+pORv4xb~H9!O=?5Rw4I+4zi|FIcsWTLT5$f zvj#G#-I_R8?Ak(6FLtv3AL0@M;-@BVTr)1;#FcHJWaOHKO{gh3f30|<02`ncZd(?9O6PW)T*^Z z$z%({Xz}FtXd=*@f&<4K4(Fy)j$~u{8V-9=N>!M~NbdSI4eD>fT2loY?eIUjb;o|e z_eUUi=guaU7H4p|2``%=FLpEB#2)p{R_#E9upS)`7?d(ld}20rb}W}AP?TWRaL1|; z4Vbbcx44K~_%=6hquTfwC>}aoUf$Wk#Y1`R4EeK7@^W8aMn1_*8v#}Yt4HcaqmB&j z9^J*eeNbOgKCO`>k_UFVeuf*653m;GCkc5SP5;Jf%}3`zfGKs9&XM4v`t3WnafMKT z!J#7>kd7!W$JeY>#qouA_aW2v&;8xdVP_KGW9#U#1vZngzgv_{z94K}es+egSep)0 z(#qwv8Ivb=VreThO>GPrUAc!D9Zz#+Cd(9;Xu@1tSoB1%9eEU2y{{S?UUiH26q!4E zrUWBx5Oz8`oW&XIh*Uww92HT8$(r@Vk*D%$zj>4~6-scG2{z80I)iB)A06D=$Jew? z60eSU=9yDev9eJt*h2d;ap(-;oG((qTyoSnnzA6TbnCG;#FEv*+Q3PDG zau->u1=G`(@cJDOgIYzaJR;><45^777hu4!d$7B_kJf_MzzRD#pp2u9JQ}@+ zn;;ezaNYzjmBGuI?%)~1XP=18flL48J%Id~*I?WAb z+NC7XEGMPiMBwjbsOxQ^sy4%83D2%XXBUSmMqWo_&*|Y8nsc_XbIy8)EP*w)3aP0>(X#ak&>t@%_-JJ+HP3ipy$H*;Hp_ zQ)T36*oiFpuYxHcDT}I5E4k3@a&mPEV7a=L`@BFnz!No?0>1FV3pZ}uynG3Z!Hbwh zMVqt5V~)>OZ3Nr%TSQpF6hx?)3ASz}-KAl)U0zy|YC_Y(ocv%PwRCX6`$V_5Uw!oz z49a+m%;k%hFP=wzVeU&uc*xN3sOU-GSDWo1^UGQm;%KsghD|@bF>|zDp-v_1}PHC z-*J=@Td@`5E=!m6`Xp9>^hvK8EiB-hX*X})`e*f$)l2{8(c2YWYD@Yxk533c}M&Mq}aw}rJ6_B^fs(hYlCR)A>mDz zWrvbS4f3{XIkkeD4-t4|_3RVCuNnG*x(S*;^H7W)LU=7f$o9Jfa)dsIG)eFy}bnk zYb$H0yDE|bv+5thse&F)p{mFhoWo5NddAIaZ5OrVBU}I*uD@5X!^e)I;Q$75dS|_e zVd!%+!48oGJst6!s`Bb4CaKq$8b!ET&S;DmP*qYRkJD!tO|+8`8Kf(wf@*jo-kAo{N#W`Hz#~7VLfbTPV%@O4v-9@bm>|80x9#Dx zX;^i9^jkiKvB=c)CY(Fam{kEWihNdCrculxmrCW${#d5+WAymk;7c#P zw7IqQ`fIP@O9ps!5X+CpAOG+pk35Q(!Q%Nr2x5W@$dG~vEb+ybpwb8a*Xudzn(oc? zj1sV!+;7p@8i6V#B?5eNtMWJ54uOKzMSPPe78ks^5&_3+VDZV8C!Tm5*KaY@V9DVs z_7FfN6O#ujwtkK!_=gt6RTVb=U>OC?)v8IhV+sNBqu6jr#u*LuHn3uJcuh2C90UE`Y88^< zY$9w@2AMeG1)Z;X(Hg)_i_SDV*O^qk7y>aN2|PIF z@9b&au1L_SGs8w+-iHNi+%w0?rW$G8t6~-hi`1JCGtEM50Rn#D)xrMlTeqKk?)k5O z{h8;Ve-4u&e5C$+zxVqtUbt}o_3L_3N&QmU#$J{7M6V$QLPVnh2pmb9pjW6MCT6Fq zVU&!**(ga;YzU!>z{=$lVj!^w59B!OmAI=&tgxgYr5d1wpf%>Wv>t>2{j5?Lva5BZ zXNqW1YDgpy0Xl=(D&F8ROQW3;F6Umka^;hs{3N~=i5abG;@JbL z$9sUV{*yqko`XhMhkLSIDebDcDfjTi{=|0Fmt}&(IZZjUP()!R*4XpVMgGJ2O?XhH z_&lV>7GDIB!GUdRG&)#c-+1EjCpS)?l6!r*AdFYks-DgX`Q*0fq%={Nb_-Q<$QJX40I8KZ_@ zGsI!m%s!MW(Nju`c}qaUFg<}&7`il+8e={z2&DnF)?HB`t%PSCqY?vAQVIrSarDBz zDGP_xt7a3_q9)yx7x0?OjngLzDPPo`E(8m6z-dq$5h@k8t4bx3!Vb!K0vlgrV?Z#z z;+-i6dwk4iaT#+OTxt2tXZ{tghQ0s&596(jm_4to@V)SAQjus505KDS4lEQPjSDY0 z<&TfKpE+|DdrT&voX9EE>N*DK#G@LSxp)Ttr~&KIzl-%$qI*P zL#VOghD7W?^5jQwcj);G=g}HbaoAf-R+czSNh?CsaGj7aaD%WCk)<-VWF~?o{v^AO zSR@M`bwPQ=5Wa-L){EJP<3jHeW76CcQL;^Dj&zD8!HqSx8X$f5(mt8Mr#h?-CrN@?3SISW-Ps6_M&dLw-Rg6J$5y1(fIA3S~f6!7qN>jiv+XBADgszg*3 zz}s+v9(B+UX?`&{v4FBwbvkfWizp+TD(U8ly!)jA97))W+1uLC!2uhyw~l7!aI(b5!7D*JKlq12KQbt(P^iQL-jQD+ z4CwJ=(MhD3>L`jV_w+9sDE{2Q$_GVw+L!$c^K*&M!CP;=gICO5zI+9fX{;NUxhxD# z8%=?!Q2F>_ZjTR~EaLP(ZqkFI)zx(fQQiLj=H}Mw+WH1A;$Rs#+S$6Zxii{9hF6*} zDU=2X^DvIZr9NV`zP`4%ynN^OZG1ouvk)vr@VFP4Suy;PWg!5|ydGL`UYiW z11o)2wX!-wmOWhx@d`w^cI);{JR*4h{DpJp&Oh+L0}s9LeK=VTr+|Hgr`I40eJU`( z`f*@U_ZWr<8%MK{1Ql=W+(X1iTnldU`+e`ceFIBCF5~z1R#uj7-$J$AmM}sA+#vS;*g0TflLyQCAR)SDdM~Zcj@z`oYHLR-^DC*Gi`vor_{ zQhqc{oJy+Y&^WPBKX>jdpmW@a|7Y&UT@f3Ji&SS`kuhRNQ)BNcs|>Z)z--($T_J%X z3WJnPplKi+E6o;Lkslso$0;RuUZNF>T=_&7Lzto4;st0x4?F{C&Q#e+eO zs~DUJ=1V#DM>r*n6ZSv)V?TnKI?ml=uEzHX@RAMZ_{8DNd!tmc zjF%8_wS>3%V{Q*Yn0V~5$8gnfb!`nxLWsWq;r9bH5@@`T zkbmrv$FS1d+uOyG13ICfv5>V0#IuNTJs5(MB2rD2fhSYYmFvXn0vN1Fmob1h+QEe~ z+QPdRC@T&RwT^Jt01(9dH@LI9vX0gGXm@92L2e0K*he)vQMN;WO?JKm0J7#FI}xiED1xuU%(Mu#8{fi$M1A#vN23T9Mm0CaI4ho~cY~NEZbu z47!*{5lAA*5J@{T!Bn%NI7CoGi7VTLfE6Kr78mbq-hm^yDu{*$2BdL10cxon{||lB z_EVT;V5IpL^zpMvP1UXF# zCk7ua6e-;mT3tzdvMN&zPP>jlC92bzLyf{s?!)=DaN9#_y@T}5nn@bkAPmw+c<~$} z=sD<-JXwP|FgJy@hDyXIJ#l3i_xjEZDOaIl05F50q}xR}5$Bp39x)F?aayF<63j2* zD;z2Ki&C%o5Ypl@?69jIEfVkSm`KR(;_o%3wCe0-O9G9QjAuH!g~mE2&ZM#0(xfXi zxWU&BVHhOa1c|on zfMlq_qvpfv3*a@Rs2bcLBiHg|7&+MAyoI}ZH=p_XGq1h=+NCR(@l}nhSFd7!`pjwE zJhp}x>1<$;%!L+jl*KJ%yz>uftx)j|6^ft=p;;+Pks3@QG59i^Q3q7;q=PD{1uV#7 zMT&NeJ5Iqa=N~}lGoO0bC_qYmaeB+P?b=OE=ONOtVuBr^Ox=)d$+86<~@|yrRKyJzA%=bl$>Ri zm@-SuQu~Rn;Spe4eRpnX{QBqIZ)uJaB<(C9ALBr* zC!wiPUB<1YATQ&xWtfZ5FSVyVO)fi_r@Y-Pj!@(rw!tjKxF}Z5k`PhxNinSAwK}}k zNo*)Pz*S7_@eOq5lrpF)Bcw*<#DYWk#kI_xogLhcdwSzEUd4$lKR=DDv%Jf9A5V2Y z`|Q_WeDTH4e)eBqyLR;_fAS};UcJW4#T&d7fKNC8IxY-KBo_5c8!P*GH47eyV+15T z;)FymqiCB1*Am!;C;k*{Ql=bHi?YjJt3dZjaG%!(r7Zy(?p0n}Ug2Y>dy8ngToZy5 z3t-gX9xp)!^G@r$lGU09QRskSiP-=UwH>M~Y;~TV;<}J^z^s^#u|xAm3y{I1$Y>`R z7Q_(mE$2AFDBy)ayeAy1C_dil`g^9dJ!4kdS?VPd3pUlMqfm&It(`f7bK3CL<)+j$ z6tS4XJFY6}t(7hY7ceH^K!BUZVS+u2e@hrQFkHY9Y-i7$K|jK9(PETw1zSt0Cnv6s zx~LWkrf~sf&QZ23$lT1|I{Pk7oN?S#%hMZtMsZy;Tt6opoLTir$bwEsgjbK1PUXmph)=;6-6gm z8SoHej>!_JX+u)fn$HfYFwbvLd>a7Z4xAlDrmdQQpusB^gyC?7_=qHX4!8#$DJU>T zXA}KLwF_?Q66L5sh+PYKKMJYu5Miki9N zNLa&TVt;>xHiy|cE<|C?w6eB}NjeI+VHw3ohS&zM!@*Zqm%Jc}GHI%ud#G;N3#T?1 zrP41Y;+ILP5?Z`1P4n%0=uhsbhNz?z!W0%H;KY?uF$YXW z#I-s(u*wgwN#*rVK?|4BJ+;o9&iN&C4;yv>Q+zSD*u00gqlQi;STHqKEnFlUgNbx7 zH;bZ_A~r`7qv@Asav-;hqm-wL(062X!41qStT5bRju$yBEOQaU>f^F-_!f~Qk9uVt zwqD+W3>pEr;vulh#PT_Cx1rM%zQsU@DI}?unjx;&eBL4z-G;U`S}C0NH{1tJ(_X%u4MC=CmO6 ztr-Sc&L6csNVRG>+5xNLZiD1K6|@86+fXz$!6WHYhAQC1g_l&AT*8`XcYlPJsK}Kc zOwMqD&u;VzD>~@ZDYeKzKGdVE5;GHZx+6RKBXmJAXqQ7BBr+=|AjNtgwXwUudwP8x+3nlg*yHs%czX=KviQdhLSk|>Vz7yQ~e|KnqWfCLv=GbjSnfxHA(TYIuI$5e8-FcMv6i!g6%vhVaF_=gjXO&JVM%UD2T#3pQ-oUiCiwpi}KYM(@4kwOy<_JRt z3-=G!Hr9|uBSc&lSK`Q$DeTA?{$#X+TZ)Erc4~@hGzdIRBqN=SD&eG&lQ*17i^74U zD0OoENmbRR!5eDhp$&{`7#x=N_{{Byr-~NvEH}o;(FkYq;37tHvh6I4aNxjMBxYK+ zyj66=QxybeJs=lHk$0iR{5zZtxq*7*L zs6)w2+^pA2hgB5C8c9vA>%u1aKq| zssd7?FjUJkJ@d{a=b|OUWC<cBOwMyLwQUtRT`P76^!Rfb%ff@{_GIZ90 z@&g4!4aLH&M3oME6pgqL!dox+pq+$^3l0E%7b|+avEE!c}tj3Oh8#` z1MmfF3}D;eiHj_`IXN;bkl9By>W6Dcw(A%B5>NQr&SvWR6RpjuwIJ4o=~g73Unrhn zysZi4E>tlKFHweG&*=y@zHSOjbgVhB;X4y-RQSd6V{s8*pMZdL`bmNV4XyZ6fnrSg ziBnOLLS;Lb%hYTG|9^W|mSo3qUC{#?#2`+ffCeEM6fOM-pn?E{Bq$Tz=m9?G-prdfhsvsYuU-#k7aH|4?{MyrSy`1e>YpM{ zacY|9Bc8$^u{9+LZ79swd22NRw(z{L{Nh~U30@7&8wBK=K;YqY5{JJ{E# z_^Kge?YCL#&_nb&Nkz(s63Lh0ObXLhO^@q5Oar4-0jPoW)m5fY?5osVRbLaNhP|Tn zGvE_0<-rE`l7{J}QHBgw!d+!oYq0$SPxpoew(L(ywoyfqtKW0W0QvwA9(d4<%U^us zyF4ZbfAnCTy(J9TS$Cn1wxc#K6~Z5Sn1ICR0wp$O%Jo5X2OMh9BGy>QR}dU(@Hj+} z(MRPpyBg1I3t&W*U#g2k!KWnAEnyeO!_d(MsgP{YTZ9H%sPkdFIR8=iWlSFhyD$RW z!83Y^1#>A(%b{lcoh-^pvkEE;Ll!2?Hq+DF4QrL)evyEGn3|zVoO0hJF ziG5Z2A!k5)4%e{{oqIMqBNBYnzYRK>_T_SZ9^T0AUXQ+E9lMf-ha}=aq43KnO`Ssc zY^vtcCd^4@!YMk#m#l=$ER_eq;Yt^7_`$)~;fn*zZ;p14v4_V?Wn}NKPy28ZhxQ_o zjpKcgCIksn3ic^kp^1-q)&aL!@{57^CLtf++B%iH4e?$UojK@-r!_ix6A7jnIP8$8 z(d13-C)e1Qa@bM0pIr?I3ED6ImjEWI=a7#MQ; z1o7dd{|2iW(cB_00Tt$p^Tc4=__XJ=nVLXCC_BwpJK9) z1pxGBOhR~(Gdd5T= z{$w=S&Ys*Xdct=O<2^w5v?T^5zP@Ov!97+itGOu&oFWrZSNTQM3wY)B_U+sHQkj>BhlmC{1%!i9zHa5F z1BH33N&s;bXlmv}Q_RV%c@{#b1&rb)jaU@wxS-YXR^;o^gpN!xr`8L}0ds{^;dou8 zd_N&xN70@9Tyq|5uM(=I8|5gak$OL!QOc@(0(hLGr~Ao7C(DD%+K_Q$Ee>U=t(e2-5;?UL}dDbjchAQ6?m$UHnB{=NQ4bq?OP@c$v`i=g)E5 z@cjq(Z{50y*FS=VG@(UgM@c0aP$U5y_vCsh5ZGpNza}^eJ&*!n=DY?e3Mh-#u!JA2 z__Z*&65rqyIupy%sni|z8dV`3|2jrJ^cj`moBo}Vy^CN*`ku5B-zmjbgI62|I0i@) z(#xy|Tm-iVeUOoEkgK5}nhkY4>D?HRn8l?`D4K@Et%-XEoSl(70k%#~hu$pO%fS`z zDgzff1F{`mXb&%wb3fQ@roefyakbzz990rzH-+S2uYx_kTX^^;hXRgvA&TnQ6jzFf zrl<_bG>r-|8EMKWU=kWK;ORJyKRLOE>qFbym>Ob?FC7?1WmvA`i}kpx3!jL< z1tnbK!+~^cI^Ihd4#5?X41Jd)zg@vYI#Sr$!k1?hQQ^l4@vh1fzUt%J-ZnpVK%aR8 zh$#tXr*b?)Lt(+|P6>fdLM)0!9~2lwV#uK-3b@X|HU8@&ohw%4m*#lnnGvlb+)$%2 z>6^He0VTXpftNfnZ58j}7~~WO^Iy`oFv%Sgt-Ot^#5~83r-N|6 zDBdiJ?@avS-Y;(7xs7)V;iZoZk=N?*EA#bm%(;b2e0T;_j@~KqQobe54xo!PPLa$o zg%$z5?8oyF)k1qrYe=Jlcqr4*=0R0k0nH;~a@7${b-V+Q*}vFzs01!waT$>fXVV4q zvo-`)Tx#Yl3SbyuUk5^v!9wZo0vYg4ObbdJ(i6xwY=MTm60oSVXc06IzC#1o4i8`M z?C^!7C&vKOZJGVgTQrwOiB6!Yl`(|5e4qDf9HWD`Q&XHc`%Lz?!I%0z>k7mI#oG30(`tlXo}C-T0Mi?|wK zYss1djobkc8ip#FXbCTP*yY`!xC;o;;Tqu%za|Gz9{OWZEZhkhVPg)$p3=;PJ||c3 z@GKf8kbFrAkvM;#7QS?k3azkn;)FqXeTcXsM|?cLP4D?5(+Y7L*Bd!!(7B>sf|ER` zYl0;Za8@Q`+618?V#ZV$a#j$XPz}PGrKq9;$_g`8#DF?ygK#o7kWOh-OTLc)*8`wJ z46h4kh6Ojn!P$UQT-4;NYBmIESOiC+af`xy) z7|3RotEFrd358)JFIn^{M%QYLN2N69Rea$nQ|YlqGNM-r0z4vTAHwDCS9tvoEDJh4 zAd5cpHAW<6E((-VlD8gzLuI%fbF!G-m@B$$x%I>i#UijF3UAJQh<2H0*`nDF(iZGb z*ucK|=9}lwzx(X7&)$3Q(VZXt=$*Uo;G;K^pw#XWIcqE>nxQBY4W7|2ySy75Y&{5t5eo&|G&9ri7Q-q2xanNmMU#E|?YG}OfBuI*eD?F7|K009 zdi~Dpw{bH+?%WW20Ta?#tSvIcUM2XT>QS)mm~y?OGgpKLSx|a7(N_68fS<&?fKlTP z6NF?`BvIwA&m>WBZtFOZm<}fq7caqqW)#s-3L#RK`Ry8QnDFvGX%H={4dvBI+}(N8 zLr5m0&REsUH;GqhWgv^sf&Pv|r&|#d<)`g}(Ww;lkqHbFhBTo5N zUUL%$)M%qyp1oal5!Un31|i_jTEPk-BG{@yT(n+(YBroYi+)1r;R-QUWtRhI3$dcw zP&{)Ilh$%8&^k@%(mf@i3Rp-MRf|6J$QhE<2B}QOG;taj*LaRk@G-g*?A>uB&G$I) zPyolxI1bZO-F$ikD!i?<+|LmSZmDN_Ohie1dk`;|KgLT+c@+T16=>oeJf()$#Bg?p zJduG^p2~FqqJSvZb>JwD=!K$vR3{xG!9&R`2>_yeI`L*eourtVv>-MMq~ z<}J(#@zue*ckkjukra_Dc<35DHsth?84#9)%a1#p;1N_1)#(s=j35tCZ3bT4-h*P) z@V~}lo|$HLNiU#fa$|{7a8mCr4n%PR-Yb;>fKlOCTL-MO_>q{B`v@{xMWOOA@&Sjk8x#KWm%luI{N$5Q{_~f={D=Mf z`)|GZ7T#^k`xjC?0ap8fGgrsr9jLi23N$iLMW#t?X-Ag?!WowaRt5U2oZ3zcawg3{ ztk(Ro(ieU{gT?@J@NipdpqS1W8v&}j?m|tSMfToN>3ss||;JH@L5d*q$y)_O{ zSv?7BnhQ3BO;O)M4eGu&!gOdeJbb44^B|s?i<@#1Z6%dK8N-SBX$R37u7`*p!-PwUfB5OrVjOrdmZlgI(|A$k4glolV37Cd@4Jn zNQ7dc+YpMv_!U3HR7oXGU8}O@=K(R(lM5#dPWz5X>TD25c*&$*%|k01MzSPrsYe7j zf?zEm(yzksoD-__xIkBW9T|*K+@mdi0;R6*Y z)6b&Isl4-EjgV}mINyL71y&`l^~z+bOGy>kd+D0&G*DrDbO?(fYgN}u#2U^xe@rAP zrBW;HreFog3u>@#bYF0oIE=$5H27IXe%J$VhB!Wc`SJi40dQ9s&JG+OAL4Lu7x(n) zG@i3ZeoaL7kpU7@Ux}B|Ylq_jWYtV6jbQ1X_hwqTl_jqk`M7NtR|fIfG~CC=955Y0 zj(7BiE7HxT^l)S4Xl_hS`6^xL@U#er0&Wa)`arM8!*_ZO#rR*78gf>El=hKb>QcBc zs@0SYoVdtOtQ1fxsq22aMqCnH3rHPMFJXf5YODT{0s( zrd>X$gS6?L0yukuY;yqNFq@lcf`F(Q)7&;?lx2poS!uUHGkHr!*(^ zur1GjCp_RKP4 z`VP~;saHrwyESA+fs4hZC&$(h%wg>it$yZox;4kL9`YO@psc8AO*&fRdvR9LPIw{jdB4BNEZD{Y2iT! z7iPbk$=Z3gm+IEM3r0cA+VQZnM8rtI@zL=g|MWkPpL~HiAr8TC^XSWi16&^D1@{n^ z2Dgz&m~8&fz$-$=9jeHlH8BY(TauDeLno1CN(niSVoh}2YVgqa!NI|&pZ@;Yvu9sE z{qn6py>)PKh_?#!><`a|OBTc3eTnTZ9cXd*g#fjC>Utr_3MNblu?^7?uVjRMC_J|ZV zyCP3yh^?@#4fs&DK-3y?Q;h2tJS0-lff+ZKRH~#lOE3>~r(jjJs>CTCFaY_E4%T~n zxKeoiuit%#mrmiiVmwlg7f_)q;UwM8&R)zEQ6VgY9*Wirg+>lpxFIE`D72S%rfek7 zTdAx@*>!?dZ&Zour@D=GxrknaUdSoF_#aIb5^>Yh7$L;{w&X4fsv>Q9x>BP|`jxFg zCMk@LO2Q*97Lzk`M#&3aHs?&Lc~6r9K--KYvXN3VSdDP5KO(OwMav<%D9Z`dNb3h6 zMx)K3PMpw6q8!eu`NTMQ!lj?#9kFn z@{hh^3(C**b4!*#tge>Jk%3FrRC*YI)be&J+@kz9kA8Z6Z}*#Tzuw<}_no_UF$sx% z(zd2%I~dmrO|b?s9X1c8ZiwChGZNgG?TIO0qyifVQ+Mp+asMQm{&*v5f-PIEi1r;mW^VC#jSS$3!_D~qv)p=&?*w% zRlH)(%|I9jWL7v=7xLCP0cQn>3#DRV%;~Np5iAHbV(E%~E*aP@CX3#Qf;DfZ%id_c zhlTjxThLRL8D&bPa7u-rDWEF(c?3mEHpaA@ z@~Xb9V;x62-hwqJw=^=~cEgAwHyF9E5XjCW=iFp2Z5K9PXQFi=&=qx_z?20)G$v0! zv5Y@#T!EQ^UC@ezlbv6$9aWi{Zj@00lWZ26>P=!(eGOvjC#fWGYTfxz@Ow6$R!?je zkI&PwoALAXm~r5O6Rtr0>K}jg#gi}Ie*5i@KKkhXy&u2+#+&#Qj&$0D-SK14cDQh8N}&whgiOSw8Wn28>o@S@hJU|V3~@X}W1V`*q0JTuG*A!J^E zEYOO~!$<})XKYW|8=Q+*Jx<}U&3f-j=_@P~qdC0z!(n%YFxr&SAXc(p9r#8a z4dWx^Y@~ELS~N~(pj&CGIN+}GWMd4Z#J8CH0}E_lu8TOb#UhQOH&?|qSfWfjC2 zM3uLHw{G3s-Q77oK~wg+u?tY6>j`VGNPs5WNX!(p386+2=Vv6X^{cKo{Enz? z)*Lr5QRlTmym1O|2fMko_5S2?_bl)F`8pM5qIdc`NXVCgVdvKewv)vyS52+!{1oH;1)p(9wuC77NVPJ_OVC zgs^nZ;+o>6Z<{zwz;?paAagOZ&?_`m7Ef8zO3$s$@&=o2WwQ$fm>SePQRKy2MDQEt z%&sdm#o9Tn^B|r#gcpfOL84LknOu!T7sp_cikuE|KFBMC{NXaFaFH3V!}tJ-CQw#E zWLReyHPfztOB>a=vKW^(v3eJf*&tWA5fPIO0`2VFzJLGTz59-YI7GHPtPWXHLj1y}K>Es}HQ*IW;8D|g#EC9SP2RJkN3gtRIo{MYO%=t!fwQbo0S4z z=>;))Uo~iwoYoc1Zo(`|(Zhnge$`ATp9|0`x`N-RuAm|)$p^%%AO*T7R#uX_jm)_^ zg4I7IQ@&~-4??~n3^YL!Fv+=v0c65h9y&5=&LrnrpCDhvSfq#mu`L>`3HTcl@y-+> zY{=s3u)bphWw^N!)bg>Yxxxn@O!({scKbpN9u&uzkxLON7yJNdVxDcbJrQN0`-Ti4 zlYczkWTjkCvXdpV?+mRnCHb(2_iIj`oNh3q_FuAO8rMhBCFGO+H z2w8wcTaDjNG7D))UcZj+bsZnT#3w?w`OyMK3R`$#$quePUEBKp;CoQOLiaOdjlf2* z0v1)1tXad9SxFv-IX{l1r5A;zWmeN8oxF~Fa_#%0Beo6Zh;qhA-U-B3!&efH`Ju>> z#jHaPFtxW{URy)rw}+VU)$Fr6X|01qvbL5&^R5C@y)B4K+1fPDXipe(AhlJIfyAgF(iK7-rz(N92%ErM z%b{7y?!JP{9muSx*AT{-I{9ff8^^7%hT+Su%EZN85@P8dG^SV~){{(vSEVH~z)@8H z1P!CZ{I?`8;l~+bAY|T*h)ScCo2hdgaQPy?tRABW*SowX@v=sEc7C(>%b-k?GlvFi zgs7)u+Sxh3q9yZtMg1$as|<895GI7fyNY6N*?}5l$xbV7b^%rgsrZ9ZlNiH(${tWK zIx>@NY({7WuYBrPQ1@|oC{LeNE__%I)1FyJ$Cxz6sky=j^}p#!lW6fOj+bd7i##}4 z@0F@oHPl2j72o+2+cdPmD^0lnE>tpOg=DCzOM1`fWj)Tb6b+`O9eYMy8AKJQo5pd+ z>|>71(?&(DTL@NYh>nqF>XvcW>Y(i4_7S-%h)D%{C}p;;9lgZ0rY+opcyfYUMo(|- z@{{OzrHONJ_NMK0Q&C-lqv6H<;7r&slKx61F}gW38q-Ri4Jr{6&kw?B-Y|;w_BPfp zkMI^5T+sy!E9@LOnJ-&CVU~^+k_EfqCYZHL3nDM#a{*FR)mtMn(Xo%N5s2aWY_5UF zb_bN-LOt?Gx#>+)r^tqvDZKAs7Lp#ME^}nyM^$*9&Ic$UBuk+VT=K!Kf{@wp0*brQ z#{20U^W@s8lA_X1JqZ**MOEcgvB^q68!pw>o8fHOi^bs2H>Gtl`++UuZPRG85z`gI z5w~>9;f8l{7dtGG0b*@GWuV7;^Du*Hk2;~sgb72?bt^qPI_isTT~N97PiW6A!eau2i=k<2%vyi$w_Q1B&-0 zYFS^&43K>%ahSXqNJy^GIsFl!^i@$4dNizEnCo_zQ#ox3XULNy)Yxy;oFpjb0Dc3p z0!cEJd&bg3SyVMzqqhrn?}eOCcw6maQ;WR$s+5P5w_IF{*zlfDc5M|U>_Up}qE+2a zh$tNeU4sV$-OZ==vx%I<$o3m=;*6BS1`h3D0lrBXX0W%JkAMqX^c6(bmRd7ChGMQz zF#4$*m{-ljJi`oO81(cxGuQ1d?3wim_U&PolhD2BvMHL+ zg8;%9>2i}i>04Na3qSET5?pk`j1Ui}VZtFdAMzze7!2`}8F@GjxkGn^#gLiFwqyD- z1tU&DGvN$kO2s$9#8#UEdyX=C+7ch2rw_-+xR)1GL^KfY`Q>{xcqGj$xju05Hsz3| zaKJ5`kp@kQD!sXN(Lku$7AG!Gv#f^)$N-etxvYcLffOvO7KW(kcX{HKM>9n&kk1D> z`35kBGGWxMTv+kdri@wfl6BeZPE@Hurc>116hq#z>G%VZ1{Rx}ay)b!@C0O2fef@{ zU%*wSp+XsHz=Kh;*|UOdc8g$_#??eJRPxKZmkbU`uMTpt-R$<%^}at z`e>w~_3>f)3|gz&_9#m=%!cL$*F<*4MqPBWWzl>g2o2#+Hs(@retoFlOBT%T2yPL( zNYlkattF-+Npg*|?}fF^R(0I|TFNDmM1SV7-vmxk4Q_5- zw~kUQ3M-8IkYcpxia?Am*&v{&6b7K{Cxn_<1N4e0$K9fO%Mc1ls}@$Y+T?l@O)XC} zgjrf1VOAODCK@uwX5mj0X4b`%@lK-N#vN(a>(s-E;q)D|V66c8cz~A~?c;2SYhLBS zTK3N0!^Hzf$a%0!TG$-&Trag{hy1a22kid=3zgkzb7sKZ00000NkvXXu0mjf?G-<^ literal 0 HcmV?d00001 diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css new file mode 100644 index 0000000000..d479be3e08 --- /dev/null +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css @@ -0,0 +1,105 @@ +@import url(../../shared.module.css); + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.paper_title { + font-size: 20px; +} + +.subtitle { + font-family: 'FiraCode'; + font-size: 12px; + font-weight: 200; + margin-bottom: -8px; + text-transform: uppercase; + text-align: center; +} + +.paper_link { + font-family: 'FiraCode'; + font-size: 12px; + font-weight: 200; + text-transform: uppercase; + color: #d15168; +} + +.inline_link { + color: #d15168; + text-decoration: underline; + text-underline-offset: 3px; +} + +.list { + list-style-type: decimal; + margin: 10px 10px; + padding: inherit 10px; + display: flex; + gap: 10px; + flex-direction: column; + width: calc(100% - 40px); +} + +.code_block { + font-family: 'FiraCode'; + font-size: 12px; + font-weight: 200; + width: 100%; + padding: 16px 20px; + border: 1px solid rgba(41, 41, 41, 0.2); + box-sizing: border-box; + text-align: left; + overflow-x: auto; + white-space: nowrap; +} + +.metrics_table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.metrics_table th { + font-family: 'FiraCode'; + font-size: 11px; + font-weight: 400; + text-transform: uppercase; + text-align: left; + padding: 8px 12px; + border-bottom: 1px solid rgb(41, 41, 41); +} + +.metrics_table td { + padding: 8px 12px; + border-bottom: 1px solid rgba(41, 41, 41, 0.15); + text-align: left; +} + +.connect_links { + display: flex; + justify-content: center; + align-items: center; + gap: 24px; + margin-top: 24px; + flex-wrap: wrap; +} + +.connect_link { + display: inline-flex; + text-decoration: none; +} + +.connect_label { + font-family: 'FiraCode'; + font-weight: 400; + font-size: 12px; + line-height: 18px; + color: rgb(41, 41, 41); + text-transform: uppercase; +} diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx new file mode 100644 index 0000000000..c3df071e6e --- /dev/null +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -0,0 +1,307 @@ +import FadeInWrapper from '@/app/components/FadeInWrapper'; +import {MenuSchema} from '@/app/components/Header/MenuSchema'; +import {Link} from '@raofoundation/ui'; +import type {Metadata} from 'next'; +import {Suspense} from 'react'; +import styles from './page.module.css'; + +export const metadata: Metadata = { + title: 'The V430 Upgrade', + description: + 'One repository, one package, new documentation, and new network economics: ' + + 'conviction-based subnet ownership, price-driven emissions, and the bittensor v11 SDK.', + alternates: {canonical: '/releases/v430-upgrade'}, + openGraph: {images: '/images/og_thumbs/v430-upgrade.png'}, +}; + +const CONNECT_LINK_ORDER = ['DISCORD', 'X', 'GITHUB'] as const; +const connectLinks = CONNECT_LINK_ORDER.map((label) => + MenuSchema.connect.find((item) => item.label.toUpperCase() === label), +).filter((link): link is (typeof MenuSchema)['connect'][number] => Boolean(link)); + +const DocLink = ({href, children}: {href: string; children: React.ReactNode}) => ( + + {children} + +); + +const page = () => { + return ( + }> + +

+

The V430 Upgrade

+

+ One network, one repository, one package · July 2026 +

+
+ +
+

Introduction

+

+ The pending runtime upgrade to spec version 430{' '}is the largest + release in Bittensor's history — not because of any single feature, but because + of how much of the network's foundation moves at once. The chain gains new + economics: subnet ownership becomes contestable through long-term commitment, and + emissions between subnets are now driven purely by market price. The software gains a + new shape: the SDK, the command line, the documentation, and the chain itself now + live in one repository, ship as one release, and install as one package. +

+

+ This page summarizes what changes, why it matters, and what — if anything — you need + to do. Every section links into the{' '} + new documentation, where the full detail lives. +

+
+ +
+

Ownership by conviction

+

+ When you lock alpha on a subnet, the locked amount accrues conviction{' '} + — a time-weighted commitment score credited to the hotkey of your choice. Until now, + conviction was recorded but had no consequence. With this upgrade it gains one, and it + is significant: +

+

+ + If a subnet is more than a year old, and the total conviction across its lockers + exceeds ten percent of its outstanding alpha, the hotkey with the highest conviction + becomes the subnet's owner + + {' '}— including the owner's share of emissions. +

+

+ Subnet ownership is now contestable, on-chain, by whoever commits the most for the + longest. Perpetual locks mature toward their full mass in roughly six weeks; decaying + locks, the default, rise and then unwind over roughly four months. This is a new + game-theoretic layer over every subnet in the network, and it rewards exactly the + behavior the network wants: long-horizon alignment. +

+

+ The mechanics, the lock modes, and a worked example are in the{' '} + conviction guide. +

+
+ +
+

Emissions, simplified

+

+ Block emission is now divided between subnets purely in proportion to each + subnet's moving average price, weighted by a miner-burn penalty. + The root-proportion term — which structurally squeezed mature subnets as their alpha + issuance grew — has been removed from the cross-subnet split. Root proportion still + plays its role within{' '}each subnet, capping liquidity injection and reserving + the root stakers' share of dividends, but it no longer decides how emission is + divided between subnets. +

+

+ The consequence: a subnet's emission share is now a direct function of what the + market believes it is worth. The full formula and its parameters are documented in{' '} + emissions. +

+
+ +
+

One package: bittensor v11

+

+ pip install bittensor now delivers everything: the SDK and the{' '} + btcli command line in a single package, powered by a new Rust core + for keys, keyfiles, encoding, and timelock encryption. It replaces the separate + bittensor-cli and bittensor-wallet packages entirely. Wallet keyfiles are unchanged + and fully compatible. +

+

+ The Rust core is not a rewrite for its own sake. It was measured against the live + network, before and after: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Operationv10v11
Startup codec build (every btcli invocation)337 ms4 ms
Metagraph decode throughput3–6 MB/s60–77 MB/s
Storage map decode92k entries/s599k entries/s
1,000-operation batch construction~1.1 s~50 ms
+

+ Submission and inclusion remain bound by the chain itself — the gains are in startup, + decoding, and construction, which is what validators pulling metagraphs and anyone + scripting the CLI actually feel. v11 is a major API revision: the old Subtensor class + gives way to a client-and-intent model with planning, policy gates, and typed + results. The migration guide maps every + v9/v10 call to its v11 form, and the{' '} + quickstart covers a fresh start. +

+
+ +
+

Sign with anything

+

+ Every transaction can now be signed on a Ledger hardware wallet. + This is clear signing, not blind signing: through merkleized metadata, the device + decodes the actual transaction on its own screen, and the chain verifies the same + metadata digest that was signed. Nothing can display "transfer 1 TAO" while + signing something else — the device refuses anything it cannot prove. See{' '} + signing with a Ledger. +

+

+ Alternatively, sign with a browser extension — Talisman, Polkadot.js, + or SubWallet. The CLI relays the transaction to the extension through a local bridge; + only signatures flow back, so no keyfile, password, or mnemonic ever touches the + machine running btcli. See{' '} + + signing with a browser extension + + . +

+
+ +
+

Documentation, rebuilt

+

+ The documentation you are one click away from is new. Roughly two hundred pages, + including a generated reference for all 74 transactions and{' '} + all 82 queries — produced from the SDK itself, so the reference can + never drift from the software. It is written for humans and for machines: agent + catalogs and plain-text endpoints mean an AI coding assistant can drive Bittensor + natively. Start at the documentation home or point + your agent at the agents page. +

+
+ +
+

One repository, releases on rails

+

+ The chain, the SDK, the CLI, the documentation, and this website now live together in{' '} + + + github.com/RaoFoundation/subtensor + + + . One repository, one release train, one version. That train carries real safety + rails: every runtime change is tested against a live clone of mainnet state{' '} + before it merges; a single deterministic build is promoted through devnet and testnet + with automated checks at each stage before it is proposed to mainnet; and the upgrade + the keyholders sign is cryptographically verified against the exact bytes the + pipeline built. A new public devnet, documented in{' '} + the network overview, joins finney + and testnet as a first-class environment. +

+

+ The runtime itself is hardened in kind: proxy permissions are now deny-by-default, a + crowdloan reentrancy flaw is closed, and the randomness pipeline that secures + commit-reveal can no longer be wedged. +

+
+ +
+

Sign and verify — trust, but check

+

+ This upgrade also changes how upgrades themselves are approved. The release pipeline + publishes a proposal pre-release — a GitHub release tagged at the + exact commit being deployed, carrying the runtime, its deterministic build digest, + and the exact call data awaiting signatures. Keyholders approve it with a single + command, and the same tooling verifies everything before anything is signed: that + the call data is precisely a runtime upgrade and nothing else, that the embedded + runtime matches the published digest, and that the on-chain proposal carries the + same hash. +

+

+ The part that matters for everyone else: verification is not reserved for + keyholders. Runtime builds are deterministic — identical source produces a + byte-identical runtime — so any holder can check a pending upgrade against the code + it claims to be: +

+

+ btcli upgrade pending +
+ btcli upgrade check --url https://github.com/RaoFoundation/subtensor/releases/tag/v430 +

+

+ Or go further: build the runtime from source with the pinned toolchain and pass your + own bytes with --wasm — a passing check then proves the on-chain + proposal executes exactly the code you compiled yourself. A URL anyone can fetch, + call data anyone can re-derive, and an on-chain hash anyone can compare: this is the + template for verifiable governance. The full flow is documented in{' '} + the release process. +

+
+ +
+

What you need to do

+

+ Most participants need to do very little. In order of urgency: +

+
    +
  1. + Python users — uninstall bittensor-cli and bittensor-wallet, then + install the new bittensor package. Follow the{' '} + migration guide; keyfiles are unchanged. +
  2. +
  3. + Proxy users — permissions are now deny-by-default. Review every + proxy configuration before the upgrade executes. +
  4. +
  5. + Node operators — upgrade to the spec 430 binary before the + on-chain upgrade is authorized. +
  6. +
  7. + Indexers and SDK authors — chain metadata now carries typed + currency units; verify your decoders against the new{' '} + query reference. +
  8. +
  9. + Subnet owners and stakers — understand{' '} + conviction. Ownership of subnets + older than one year is now contestable. +
  10. +
+
+ + + Read the full documentation + + +
+
+ {connectLinks.map((link) => ( + + {link.label} + + ))} +
+
+ + + ); +}; + +export default page; diff --git a/website/apps/bittensor-website/src/app/sitemap.ts b/website/apps/bittensor-website/src/app/sitemap.ts index de3cee7d69..a06d642456 100644 --- a/website/apps/bittensor-website/src/app/sitemap.ts +++ b/website/apps/bittensor-website/src/app/sitemap.ts @@ -10,6 +10,7 @@ const staticRoutes = [ '/dtao-whitepaper', '/explore', '/intro', + '/releases/v430-upgrade', '/wallet', '/whitepaper', ]; From 8c27130945ee79f5cbee049edacf39d3b0274f03 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:08:05 -0600 Subject: [PATCH 47/68] fix: restrict production docs deploys to the main ref Co-authored-by: Cursor --- .github/workflows/deploy-docs.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 87a095e199..a7c247019c 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -25,8 +25,10 @@ jobs: deploy: name: Deploy staging docs to Vercel runs-on: [self-hosted, fireactions-turbo-8] - # Production deploys are gated behind the same environment approval as the - # post-upgrade promotion in watch-mainnet-release.yml. + # Production deploys only run from main (a dispatch on any other ref would + # otherwise ship unreviewed code) and are gated behind the same environment + # approval as the post-upgrade promotion in watch-mainnet-release.yml. + if: ${{ !inputs.production || github.ref == 'refs/heads/main' }} environment: ${{ inputs.production && 'mainnet' || null }} env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} From bba1ac3a791224a5577a3f0ec3e7d4e4f921f918 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:13:51 -0600 Subject: [PATCH 48/68] feat: add /releases index and a Releases link in the docs sidebar /releases lists all network releases newest-first (data-driven, one array entry per release) and links to each release page. The docs sidebar gains a Releases link via a meta.json external link entry. Co-authored-by: Cursor --- docs/meta.json | 1 + .../releases/page.module.css | 78 +++++++++++++++++++ .../(pages-without-footer)/releases/page.tsx | 66 ++++++++++++++++ .../apps/bittensor-website/src/app/sitemap.ts | 1 + 4 files changed, 146 insertions(+) create mode 100644 website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.module.css create mode 100644 website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx diff --git a/docs/meta.json b/docs/meta.json index 0839c055d9..15e19926b0 100644 --- a/docs/meta.json +++ b/docs/meta.json @@ -8,6 +8,7 @@ "concepts", "guides", "migration", + "[Releases](/releases)", "---Reference---", "tx", "query", diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.module.css b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.module.css new file mode 100644 index 0000000000..4064864cad --- /dev/null +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.module.css @@ -0,0 +1,78 @@ +@import url(../shared.module.css); + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.paper_title { + font-size: 20px; +} + +.subtitle { + font-family: 'FiraCode'; + font-size: 12px; + font-weight: 200; + margin-bottom: -8px; + text-transform: uppercase; + text-align: center; +} + +.release_list { + display: flex; + flex-direction: column; + width: 100%; + gap: 0; +} + +.release_item { + display: flex; + flex-direction: column; + gap: 8px; + padding: 28px 0; + border-bottom: 1px solid rgba(41, 41, 41, 0.15); + text-decoration: none; + color: inherit; +} + +.release_item:first-child { + border-top: 1px solid rgba(41, 41, 41, 0.15); +} + +.release_meta { + display: flex; + align-items: baseline; + gap: 16px; + flex-wrap: wrap; +} + +.release_tag { + font-family: 'FiraCode'; + font-size: 12px; + font-weight: 400; + text-transform: uppercase; + color: #d15168; +} + +.release_date { + font-family: 'FiraCode'; + font-size: 11px; + font-weight: 200; + text-transform: uppercase; + color: rgb(41, 41, 41); +} + +.release_title { + font-size: 16px; + font-weight: 600; + text-align: left; +} + +.release_summary { + text-align: left; + margin: 0; +} diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx new file mode 100644 index 0000000000..2afd024541 --- /dev/null +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx @@ -0,0 +1,66 @@ +import FadeInWrapper from '@/app/components/FadeInWrapper'; +import {Link} from '@raofoundation/ui'; +import type {Metadata} from 'next'; +import {Suspense} from 'react'; +import styles from './page.module.css'; + +export const metadata: Metadata = { + title: 'Releases', + description: + 'Bittensor network releases: every runtime upgrade with what changed, why it matters, ' + + 'and what to do about it.', + alternates: {canonical: '/releases'}, +}; + +type Release = { + tag: string; + date: string; + title: string; + summary: string; + href: string; +}; + +// Newest first. Add new releases to the top. +const releases: Release[] = [ + { + tag: 'v430', + date: 'July 2026', + title: 'The V430 Upgrade', + summary: + 'Conviction-based subnet ownership, price-driven emissions, the bittensor v11 SDK ' + + 'with a Rust core, Ledger and browser-extension signing, and a verifiable upgrade ' + + 'pipeline — the monorepo era.', + href: '/releases/v430-upgrade', + }, +]; + +const page = () => { + return ( + }> + +
+

Releases

+

+ Network upgrades, in order +

+
+
+
+ {releases.map((release) => ( + + + {release.tag} + {release.date} + + {release.title} +

{release.summary}

+ + ))} +
+
+
+
+ ); +}; + +export default page; diff --git a/website/apps/bittensor-website/src/app/sitemap.ts b/website/apps/bittensor-website/src/app/sitemap.ts index a06d642456..6ccd8e69be 100644 --- a/website/apps/bittensor-website/src/app/sitemap.ts +++ b/website/apps/bittensor-website/src/app/sitemap.ts @@ -10,6 +10,7 @@ const staticRoutes = [ '/dtao-whitepaper', '/explore', '/intro', + '/releases', '/releases/v430-upgrade', '/wallet', '/whitepaper', From cc3b855f319ca7cf8ef7464feea1b246d2e91d28 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:18:45 -0600 Subject: [PATCH 49/68] rewrite v430 release page in first person, released tense Byline and essay voice matching the About page; the upgrade is described as shipped rather than pending. Co-authored-by: Cursor --- .../releases/v430-upgrade/page.tsx | 207 ++++++++++-------- 1 file changed, 111 insertions(+), 96 deletions(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index c3df071e6e..861bb3b82e 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -32,24 +32,30 @@ const page = () => {

The V430 Upgrade

- One network, one repository, one package · July 2026 + Written by Const, core dev @ Rao Foundation +

+

+ July 2026

Introduction

- The pending runtime upgrade to spec version 430{' '}is the largest - release in Bittensor's history — not because of any single feature, but because - of how much of the network's foundation moves at once. The chain gains new - economics: subnet ownership becomes contestable through long-term commitment, and - emissions between subnets are now driven purely by market price. The software gains a - new shape: the SDK, the command line, the documentation, and the chain itself now - live in one repository, ship as one release, and install as one package. + Bittensor has just completed the largest upgrade in its history. I do not say that + lightly, and I do not mean it in the way every project means it when they ship a + version bump. I mean that more of the network's foundation moved in this single + release than in any release before it. The chain now runs + spec version 430. Subnet ownership is no longer a historical + accident — it is earned, on-chain, by whoever commits the most for the longest. + Emissions between subnets now follow one thing only: what the market believes + each subnet is worth. And the software that carries all of it — the chain, the + SDK, the command line, the documentation, this very website — now lives in one + repository, ships as one release, and installs as one package.

- This page summarizes what changes, why it matters, and what — if anything — you need - to do. Every section links into the{' '} + This page is my account of what changed and why it matters, and what — if anything — + you need to do about it. Every section links into the{' '} new documentation, where the full detail lives.

@@ -57,28 +63,31 @@ const page = () => {

Ownership by conviction

- When you lock alpha on a subnet, the locked amount accrues conviction{' '} - — a time-weighted commitment score credited to the hotkey of your choice. Until now, - conviction was recorded but had no consequence. With this upgrade it gains one, and it - is significant: + When you lock alpha on a subnet, the locked amount accrues + conviction — a time-weighted measure of commitment, credited to + the hotkey of your choosing. Until this upgrade, conviction was a number the chain + recorded and did nothing with. As of v430 it has teeth:

If a subnet is more than a year old, and the total conviction across its lockers - exceeds ten percent of its outstanding alpha, the hotkey with the highest conviction - becomes the subnet's owner + exceeds ten percent of its outstanding alpha, the hotkey with the highest + conviction becomes the subnet's owner - {' '}— including the owner's share of emissions. + {' '}— emissions cut and all.

- Subnet ownership is now contestable, on-chain, by whoever commits the most for the - longest. Perpetual locks mature toward their full mass in roughly six weeks; decaying - locks, the default, rise and then unwind over roughly four months. This is a new - game-theoretic layer over every subnet in the network, and it rewards exactly the - behavior the network wants: long-horizon alignment. + Think about what this means. Owning a subnet is no longer a fact about the past — + it is a position that must be defended. The person most committed to a + subnet's future can now take stewardship of it, openly, through rules everyone + can read. Perpetual locks mature toward their full mass in roughly six weeks; + decaying locks — the default — rise and then unwind over roughly four months. This + is the network rewarding exactly the thing it has always wanted from its + participants: long-horizon alignment. Skin in the game, verifiable + on-chain.

- The mechanics, the lock modes, and a worked example are in the{' '} + The mechanics, the lock modes, and a worked example live in the{' '} conviction guide.

@@ -86,33 +95,35 @@ const page = () => {

Emissions, simplified

- Block emission is now divided between subnets purely in proportion to each - subnet's moving average price, weighted by a miner-burn penalty. - The root-proportion term — which structurally squeezed mature subnets as their alpha - issuance grew — has been removed from the cross-subnet split. Root proportion still - plays its role within{' '}each subnet, capping liquidity injection and reserving - the root stakers' share of dividends, but it no longer decides how emission is - divided between subnets. + Every block, the chain divides its TAO emission between subnets. That split is now + driven purely by each subnet's moving average price, weighted + by a miner-burn penalty. The root-proportion term — which structurally squeezed + mature subnets as their alpha issuance grew, punishing them for the crime of + getting older — has been removed from the cross-subnet split. Root proportion still + does its job within each subnet, capping liquidity injection and reserving + the root stakers' share of dividends. But between subnets, the market decides. + Full stop.

- The consequence: a subnet's emission share is now a direct function of what the - market believes it is worth. The full formula and its parameters are documented in{' '} - emissions. + A subnet's emission is now a direct function of what people believe it is + worth. That is how it always should have been, and the formula is documented in{' '} + emissions for anyone who wants + to check my math.

One package: bittensor v11

- pip install bittensor now delivers everything: the SDK and the{' '} - btcli command line in a single package, powered by a new Rust core - for keys, keyfiles, encoding, and timelock encryption. It replaces the separate - bittensor-cli and bittensor-wallet packages entirely. Wallet keyfiles are unchanged - and fully compatible. + pip install bittensor. That is the entire instruction now. The SDK + and the btcli command line ship in a single package, powered by a + new Rust core for keys, keyfiles, encoding, and timelock encryption. The separate + bittensor-cli and bittensor-wallet packages are superseded entirely. Your wallet + keyfiles are unchanged and fully compatible — we do not break wallets.

- The Rust core is not a rewrite for its own sake. It was measured against the live - network, before and after: + The Rust core was not a rewrite for its own sake, and we did not guess at the + benefit. We measured it, against the live network, before and after:

@@ -146,10 +157,11 @@ const page = () => {

- Submission and inclusion remain bound by the chain itself — the gains are in startup, - decoding, and construction, which is what validators pulling metagraphs and anyone - scripting the CLI actually feel. v11 is a major API revision: the old Subtensor class - gives way to a client-and-intent model with planning, policy gates, and typed + I will be honest about what did not change: submission and inclusion are still bound + by the chain itself — twelve-second blocks do not care how fast your codec is. But + everything you actually feel — startup, metagraph pulls, batch construction — + got dramatically faster. v11 is also a major revision of the API: the old Subtensor + class gives way to a client-and-intent model with planning, policy gates, and typed results. The migration guide maps every v9/v10 call to its v11 form, and the{' '} quickstart covers a fresh start. @@ -160,17 +172,17 @@ const page = () => {

Sign with anything

Every transaction can now be signed on a Ledger hardware wallet. - This is clear signing, not blind signing: through merkleized metadata, the device - decodes the actual transaction on its own screen, and the chain verifies the same - metadata digest that was signed. Nothing can display "transfer 1 TAO" while - signing something else — the device refuses anything it cannot prove. See{' '} - signing with a Ledger. + And this is clear signing, not blind signing: through merkleized metadata, + the device decodes the actual transaction on its own screen, and the chain verifies + the same metadata digest that was signed. Nothing can show you "transfer 1 + TAO" while signing something else — the device refuses anything it cannot + prove. See signing with a Ledger.

- Alternatively, sign with a browser extension — Talisman, Polkadot.js, - or SubWallet. The CLI relays the transaction to the extension through a local bridge; - only signatures flow back, so no keyfile, password, or mnemonic ever touches the - machine running btcli. See{' '} + Prefer a browser extension? Talisman, Polkadot.js, and SubWallet + all work. The CLI relays the transaction to the extension through a local bridge; + only signatures flow back, so no keyfile, no password, and no mnemonic ever touches + the machine running btcli. See{' '} signing with a browser extension @@ -181,37 +193,39 @@ const page = () => {

Documentation, rebuilt

- The documentation you are one click away from is new. Roughly two hundred pages, - including a generated reference for all 74 transactions and{' '} - all 82 queries — produced from the SDK itself, so the reference can - never drift from the software. It is written for humans and for machines: agent - catalogs and plain-text endpoints mean an AI coding assistant can drive Bittensor - natively. Start at the documentation home or point - your agent at the agents page. + The documentation you are one click away from is new, and it holds itself to a + standard I have wanted for years: the reference for all 74 + transactions and all 82 queries is generated from the SDK + itself, so it cannot drift from the software. It is written for humans and + for machines — agent catalogs and plain-text endpoints mean an AI coding assistant + can drive Bittensor natively. That last part matters more than most people realize + yet. Start at the documentation home, or point your + agent at the agents page.

One repository, releases on rails

- The chain, the SDK, the CLI, the documentation, and this website now live together in{' '} + The chain, the SDK, the CLI, the documentation, and this website now live together + in{' '} github.com/RaoFoundation/subtensor - . One repository, one release train, one version. That train carries real safety - rails: every runtime change is tested against a live clone of mainnet state{' '} - before it merges; a single deterministic build is promoted through devnet and testnet - with automated checks at each stage before it is proposed to mainnet; and the upgrade - the keyholders sign is cryptographically verified against the exact bytes the + . One repository, one release train, one version. And this release rode those rails + all the way to mainnet: every runtime change was tested against a live clone of + mainnet state before it merged; a single deterministic build was promoted + through devnet and testnet with automated checks at each stage; and the upgrade the + keyholders signed was cryptographically verified against the exact bytes the pipeline built. A new public devnet, documented in{' '} the network overview, joins finney and testnet as a first-class environment.

- The runtime itself is hardened in kind: proxy permissions are now deny-by-default, a - crowdloan reentrancy flaw is closed, and the randomness pipeline that secures + The runtime itself was hardened in kind: proxy permissions are now deny-by-default, + a crowdloan reentrancy flaw is closed, and the randomness pipeline securing commit-reveal can no longer be wedged.

@@ -219,32 +233,32 @@ const page = () => {

Sign and verify — trust, but check

- This upgrade also changes how upgrades themselves are approved. The release pipeline - publishes a proposal pre-release — a GitHub release tagged at the - exact commit being deployed, carrying the runtime, its deterministic build digest, - and the exact call data awaiting signatures. Keyholders approve it with a single - command, and the same tooling verifies everything before anything is signed: that - the call data is precisely a runtime upgrade and nothing else, that the embedded - runtime matches the published digest, and that the on-chain proposal carries the - same hash. + This upgrade also changed how upgrades themselves are approved — and v430 was the + first to ship the new way. The release pipeline publishes a + proposal release — tagged at the exact commit deployed, carrying + the runtime, its deterministic build digest, and the exact call data that was + signed. The keyholders approved it with a single command, and that same tooling + verified everything before anything was signed: that the call data was precisely a + runtime upgrade and nothing else, that the embedded runtime matched the published + digest, and that the on-chain proposal carried the same hash.

- The part that matters for everyone else: verification is not reserved for + Here is the part I care about most: verification is not reserved for keyholders. Runtime builds are deterministic — identical source produces a - byte-identical runtime — so any holder can check a pending upgrade against the code - it claims to be: + byte-identical runtime — so anyone can check what was deployed against the + code it claims to be:

- btcli upgrade pending -
- btcli upgrade check --url https://github.com/RaoFoundation/subtensor/releases/tag/v430 + btcli upgrade check --url + https://github.com/RaoFoundation/subtensor/releases/tag/v430

- Or go further: build the runtime from source with the pinned toolchain and pass your - own bytes with --wasm — a passing check then proves the on-chain - proposal executes exactly the code you compiled yourself. A URL anyone can fetch, - call data anyone can re-derive, and an on-chain hash anyone can compare: this is the - template for verifiable governance. The full flow is documented in{' '} + Or go further: build the runtime from source with the pinned toolchain and pass + your own bytes with --wasm — a passing check proves the chain runs + exactly the code you compiled yourself. A URL anyone can fetch, call data anyone + can re-derive, an on-chain hash anyone can compare. Do not trust me;{' '} + check. This is the template for verifiable governance, and the full flow is + documented in{' '} the release process.

@@ -252,21 +266,22 @@ const page = () => {

What you need to do

- Most participants need to do very little. In order of urgency: + Most of you need to do very little. In order of urgency:

  1. - Python users — uninstall bittensor-cli and bittensor-wallet, then - install the new bittensor package. Follow the{' '} - migration guide; keyfiles are unchanged. + Python users — uninstall bittensor-cli and bittensor-wallet, + then install the new bittensor package. Follow the{' '} + migration guide; your keyfiles are + unchanged.
  2. Proxy users — permissions are now deny-by-default. Review every - proxy configuration before the upgrade executes. + proxy configuration you have.
  3. - Node operators — upgrade to the spec 430 binary before the - on-chain upgrade is authorized. + Node operators — if you are not yet running the spec 430 binary, + you have already noticed. Upgrade.
  4. Indexers and SDK authors — chain metadata now carries typed @@ -275,8 +290,8 @@ const page = () => {
  5. Subnet owners and stakers — understand{' '} - conviction. Ownership of subnets - older than one year is now contestable. + conviction. Ownership of + subnets older than one year is now contestable. That includes yours.
From 31b0f7e19c59eb99159cf692724a40c7ec4c2b5c Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:31:13 -0600 Subject: [PATCH 50/68] formalize v430 release page tone Remove first-person voice and hyperbole in favor of a formal, explanatory register with documentation links throughout. Co-authored-by: Cursor --- .../releases/v430-upgrade/page.tsx | 229 +++++++++--------- 1 file changed, 109 insertions(+), 120 deletions(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index 861bb3b82e..a6b4cd6aa6 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -32,7 +32,7 @@ const page = () => {

The V430 Upgrade

- Written by Const, core dev @ Rao Foundation + Rao Foundation

July 2026 @@ -42,52 +42,47 @@ const page = () => {

Introduction

- Bittensor has just completed the largest upgrade in its history. I do not say that - lightly, and I do not mean it in the way every project means it when they ship a - version bump. I mean that more of the network's foundation moved in this single - release than in any release before it. The chain now runs - spec version 430. Subnet ownership is no longer a historical - accident — it is earned, on-chain, by whoever commits the most for the longest. - Emissions between subnets now follow one thing only: what the market believes - each subnet is worth. And the software that carries all of it — the chain, the - SDK, the command line, the documentation, this very website — now lives in one - repository, ships as one release, and installs as one package. + Bittensor has completed a major network upgrade. The chain now runs + spec version 430, and the release changes both the network's + economics and the software used to interact with it. Subnet ownership is now + contestable through a time-weighted commitment mechanism called{' '} + conviction. Emission between + subnets is now allocated purely in proportion to each subnet's moving-average + price. The Python SDK and the btcli command line now ship together as{' '} + bittensor v11, built on a new Rust core. The documentation has + been rebuilt at bittensor.com/docs. And the chain, + SDK, CLI, documentation, and website are now developed and released together from a + single repository.

- This page is my account of what changed and why it matters, and what — if anything — - you need to do about it. Every section links into the{' '} - new documentation, where the full detail lives. + This page explains each change, the reasoning behind it, and the actions required + of network participants. Each section links to the relevant documentation.

Ownership by conviction

- When you lock alpha on a subnet, the locked amount accrues - conviction — a time-weighted measure of commitment, credited to - the hotkey of your choosing. Until this upgrade, conviction was a number the chain - recorded and did nothing with. As of v430 it has teeth: + Locking alpha on a subnet accrues conviction: a time-weighted + commitment score credited to a hotkey chosen by the locker. Prior to this upgrade, + conviction was recorded on-chain but had no effect. As of spec 430, it governs + subnet ownership:

- If a subnet is more than a year old, and the total conviction across its lockers - exceeds ten percent of its outstanding alpha, the hotkey with the highest - conviction becomes the subnet's owner + If a subnet is more than one year old, and the total conviction across its + lockers exceeds ten percent of its outstanding alpha, ownership of the subnet — + including the owner's share of emissions — transfers to the hotkey with the + highest conviction. - {' '}— emissions cut and all.

- Think about what this means. Owning a subnet is no longer a fact about the past — - it is a position that must be defended. The person most committed to a - subnet's future can now take stewardship of it, openly, through rules everyone - can read. Perpetual locks mature toward their full mass in roughly six weeks; - decaying locks — the default — rise and then unwind over roughly four months. This - is the network rewarding exactly the thing it has always wanted from its - participants: long-horizon alignment. Skin in the game, verifiable - on-chain. -

-

- The mechanics, the lock modes, and a worked example live in the{' '} + Subnet ownership is therefore no longer fixed at registration; it is contestable + through open, on-chain rules. Two lock modes are available. Perpetual locks mature + toward their full conviction mass in approximately six weeks. Decaying locks — the + default — accrue and then unwind over approximately four months. The mechanism is + designed to reward long-horizon commitment to a subnet's success. The lock + modes, the conviction formula, and a worked example are documented in the{' '} conviction guide.

@@ -95,35 +90,35 @@ const page = () => {

Emissions, simplified

- Every block, the chain divides its TAO emission between subnets. That split is now - driven purely by each subnet's moving average price, weighted - by a miner-burn penalty. The root-proportion term — which structurally squeezed - mature subnets as their alpha issuance grew, punishing them for the crime of - getting older — has been removed from the cross-subnet split. Root proportion still - does its job within each subnet, capping liquidity injection and reserving - the root stakers' share of dividends. But between subnets, the market decides. - Full stop. + Each block, the chain divides TAO emission between subnets. As of this upgrade, + that division is determined solely by each subnet's + moving-average price, weighted by a miner-burn penalty. The + root-proportion term has been removed from the cross-subnet calculation. + Previously, this term reduced a subnet's emission share as its alpha issuance + grew, which structurally disadvantaged older subnets. Root proportion continues to + operate within each subnet — capping liquidity injection and reserving the + root stakers' share of dividends — but it no longer affects how emission is + divided between subnets.

- A subnet's emission is now a direct function of what people believe it is - worth. That is how it always should have been, and the formula is documented in{' '} - emissions for anyone who wants - to check my math. + The result is that a subnet's emission share is a direct function of its + market price. The full formula and its parameters are documented in{' '} + emissions.

One package: bittensor v11

- pip install bittensor. That is the entire instruction now. The SDK - and the btcli command line ship in a single package, powered by a - new Rust core for keys, keyfiles, encoding, and timelock encryption. The separate - bittensor-cli and bittensor-wallet packages are superseded entirely. Your wallet - keyfiles are unchanged and fully compatible — we do not break wallets. + bittensor v11 consolidates the SDK and the btcli command line into + a single package, installed with pip install bittensor. It + replaces the separate bittensor-cli and bittensor-wallet packages. Existing wallet + keyfiles are unchanged and fully compatible.

- The Rust core was not a rewrite for its own sake, and we did not guess at the - benefit. We measured it, against the live network, before and after: + The package is built on a new Rust core covering keys, keyfiles, encoding, and + timelock encryption. The following measurements were taken against the live + network, before and after the change:

@@ -157,32 +152,31 @@ const page = () => {

- I will be honest about what did not change: submission and inclusion are still bound - by the chain itself — twelve-second blocks do not care how fast your codec is. But - everything you actually feel — startup, metagraph pulls, batch construction — - got dramatically faster. v11 is also a major revision of the API: the old Subtensor - class gives way to a client-and-intent model with planning, policy gates, and typed - results. The migration guide maps every - v9/v10 call to its v11 form, and the{' '} - quickstart covers a fresh start. + Transaction submission and inclusion remain bound by chain block time; the + improvements are concentrated in startup, decoding, and construction. v11 is also a + major revision of the API: the Subtensor class is replaced by a client-and-intent + model with planning, policy gates, and typed results. The{' '} + migration guide maps every v9/v10 call to + its v11 equivalent, and the{' '} + quickstart covers new installations.

-

Sign with anything

+

Hardware and extension signing

- Every transaction can now be signed on a Ledger hardware wallet. - And this is clear signing, not blind signing: through merkleized metadata, - the device decodes the actual transaction on its own screen, and the chain verifies - the same metadata digest that was signed. Nothing can show you "transfer 1 - TAO" while signing something else — the device refuses anything it cannot - prove. See signing with a Ledger. + Every transaction can now be signed on a Ledger hardware wallet{' '} + using clear signing. Through merkleized metadata, the device decodes the + transaction on its own screen, and the chain verifies the same metadata digest that + was signed; the device rejects any transaction it cannot decode and verify. Setup + and usage are documented in{' '} + signing with a Ledger.

- Prefer a browser extension? Talisman, Polkadot.js, and SubWallet - all work. The CLI relays the transaction to the extension through a local bridge; - only signatures flow back, so no keyfile, no password, and no mnemonic ever touches - the machine running btcli. See{' '} + Transactions can also be signed with a browser extension — + Talisman, Polkadot.js, or SubWallet. The CLI relays the transaction to the + extension through a local bridge, and only the signature is returned; no keyfile, + password, or mnemonic is present on the machine running btcli. See{' '} signing with a browser extension @@ -193,72 +187,67 @@ const page = () => {

Documentation, rebuilt

- The documentation you are one click away from is new, and it holds itself to a - standard I have wanted for years: the reference for all 74 - transactions and all 82 queries is generated from the SDK - itself, so it cannot drift from the software. It is written for humans and - for machines — agent catalogs and plain-text endpoints mean an AI coding assistant - can drive Bittensor natively. That last part matters more than most people realize - yet. Start at the documentation home, or point your - agent at the agents page. + The documentation has been rebuilt and now lives at{' '} + bittensor.com/docs. The reference pages for{' '} + all 74 transactions and all 82 queries are + generated directly from the SDK, so the reference cannot drift from the released + software. The site also publishes agent catalogs and plain-text endpoints, allowing + AI coding assistants to consume the documentation directly. Start at the{' '} + documentation home, or point an agent at the{' '} + agents page.

One repository, releases on rails

- The chain, the SDK, the CLI, the documentation, and this website now live together - in{' '} + The chain, SDK, CLI, documentation, and this website are now developed in a single + repository:{' '} github.com/RaoFoundation/subtensor - . One repository, one release train, one version. And this release rode those rails - all the way to mainnet: every runtime change was tested against a live clone of - mainnet state before it merged; a single deterministic build was promoted - through devnet and testnet with automated checks at each stage; and the upgrade the - keyholders signed was cryptographically verified against the exact bytes the - pipeline built. A new public devnet, documented in{' '} + . Releases are produced by an automated pipeline. Every runtime change is tested + against a live clone of mainnet state before it merges; a single + deterministic build is promoted through devnet and testnet with automated checks at + each stage; and the upgrade signed by the keyholders is cryptographically verified + against the exact bytes the pipeline produced. A new public devnet, documented in{' '} the network overview, joins finney - and testnet as a first-class environment. + and testnet as a supported environment.

- The runtime itself was hardened in kind: proxy permissions are now deny-by-default, - a crowdloan reentrancy flaw is closed, and the randomness pipeline securing - commit-reveal can no longer be wedged. + The runtime was also hardened in this release: proxy permissions are now + deny-by-default, a crowdloan reentrancy flaw was closed, and the randomness + pipeline that secures commit-reveal can no longer be stalled.

-

Sign and verify — trust, but check

+

Sign and verify

- This upgrade also changed how upgrades themselves are approved — and v430 was the - first to ship the new way. The release pipeline publishes a - proposal release — tagged at the exact commit deployed, carrying - the runtime, its deterministic build digest, and the exact call data that was - signed. The keyholders approved it with a single command, and that same tooling - verified everything before anything was signed: that the call data was precisely a - runtime upgrade and nothing else, that the embedded runtime matched the published - digest, and that the on-chain proposal carried the same hash. + This release also changes how runtime upgrades are approved, and v430 was the first + upgrade shipped this way. The release pipeline publishes a + proposal release: a GitHub release tagged at the exact commit + deployed, carrying the runtime, its deterministic build digest, and the exact call + data to be signed. Keyholders approve with a single command, and the tooling + verifies — before anything is signed — that the call data is precisely a runtime + upgrade and nothing else, that the embedded runtime matches the published digest, + and that the on-chain proposal carries the same hash.

- Here is the part I care about most: verification is not reserved for - keyholders. Runtime builds are deterministic — identical source produces a - byte-identical runtime — so anyone can check what was deployed against the - code it claims to be: + Verification is not limited to keyholders. Runtime builds are deterministic — + identical source produces a byte-identical runtime — so any participant can verify + a deployed upgrade against its published source:

btcli upgrade check --url https://github.com/RaoFoundation/subtensor/releases/tag/v430

- Or go further: build the runtime from source with the pinned toolchain and pass - your own bytes with --wasm — a passing check proves the chain runs - exactly the code you compiled yourself. A URL anyone can fetch, call data anyone - can re-derive, an on-chain hash anyone can compare. Do not trust me;{' '} - check. This is the template for verifiable governance, and the full flow is - documented in{' '} + Alternatively, build the runtime from source with the pinned toolchain and pass the + resulting bytes with --wasm; a passing check proves the chain is + running exactly the code that was compiled. The complete flow is documented in{' '} the release process.

@@ -266,32 +255,32 @@ const page = () => {

What you need to do

- Most of you need to do very little. In order of urgency: + Most participants require little or no action. In order of urgency:

  1. Python users — uninstall bittensor-cli and bittensor-wallet, then install the new bittensor package. Follow the{' '} - migration guide; your keyfiles are + migration guide; keyfiles are unchanged.
  2. - Proxy users — permissions are now deny-by-default. Review every - proxy configuration you have. + Proxy users — proxy permissions are now deny-by-default. Review + every existing proxy configuration.
  3. - Node operators — if you are not yet running the spec 430 binary, - you have already noticed. Upgrade. + Node operators — nodes not yet running the spec 430 binary must + upgrade to continue syncing.
  4. Indexers and SDK authors — chain metadata now carries typed - currency units; verify your decoders against the new{' '} + currency units; verify decoders against the new{' '} query reference.
  5. - Subnet owners and stakers — understand{' '} - conviction. Ownership of - subnets older than one year is now contestable. That includes yours. + Subnet owners and stakers — review the{' '} + conviction guide. Ownership of + subnets older than one year is now contestable.
From 6d2e1f39d6d3831b2a539db0bb0bc4f3fc01861e Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:33:16 -0600 Subject: [PATCH 51/68] remove sign-and-verify section from v430 release page Co-authored-by: Cursor --- .../releases/v430-upgrade/page.tsx | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index a6b4cd6aa6..d8f6c9433e 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -223,35 +223,6 @@ const page = () => {

-
-

Sign and verify

-

- This release also changes how runtime upgrades are approved, and v430 was the first - upgrade shipped this way. The release pipeline publishes a - proposal release: a GitHub release tagged at the exact commit - deployed, carrying the runtime, its deterministic build digest, and the exact call - data to be signed. Keyholders approve with a single command, and the tooling - verifies — before anything is signed — that the call data is precisely a runtime - upgrade and nothing else, that the embedded runtime matches the published digest, - and that the on-chain proposal carries the same hash. -

-

- Verification is not limited to keyholders. Runtime builds are deterministic — - identical source produces a byte-identical runtime — so any participant can verify - a deployed upgrade against its published source: -

-

- btcli upgrade check --url - https://github.com/RaoFoundation/subtensor/releases/tag/v430 -

-

- Alternatively, build the runtime from source with the pinned toolchain and pass the - resulting bytes with --wasm; a passing check proves the chain is - running exactly the code that was compiled. The complete flow is documented in{' '} - the release process. -

-
-

What you need to do

From 9fda4eb6819f033407066a78700acd455e4dda0e Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:35:33 -0600 Subject: [PATCH 52/68] add SDK and CLI code examples to v430 release page Upgrade commands, a v11 plan/execute example, and btcli usage taken from the migration guide and quickstart. Co-authored-by: Cursor --- .../releases/v430-upgrade/page.module.css | 4 +- .../releases/v430-upgrade/page.tsx | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css index d479be3e08..ba354ebea2 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css @@ -51,12 +51,14 @@ font-size: 12px; font-weight: 200; width: 100%; + margin: 0; padding: 16px 20px; border: 1px solid rgba(41, 41, 41, 0.2); box-sizing: border-box; text-align: left; overflow-x: auto; - white-space: nowrap; + white-space: pre; + line-height: 1.6; } .metrics_table { diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index d8f6c9433e..3d65b2415d 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -160,6 +160,47 @@ const page = () => { its v11 equivalent, and the{' '} quickstart covers new installations.

+

+ To upgrade an existing environment, uninstall the old packages first — both own the + btcli command, so order matters: +

+
+            {`pip uninstall -y bittensor-cli bittensor-wallet
+pip install -U bittensor`}
+          
+

+ In the new SDK, chain state is read through a typed client, and every transaction + is an intent that can be planned before it is executed: +

+
+            {`import asyncio
+import bittensor as sub
+from bittensor.wallet import Wallet
+
+async def main():
+    wallet = Wallet(name="my_coldkey", hotkey="my_hotkey")
+    async with sub.Client("finney") as client:
+        balance = await client.balances.get("5F...coldkey")
+
+        intent = sub.Transfer(dest_ss58="5F...dest", amount_tao=1.5)
+        plan = await client.plan(intent, wallet)      # fee and effects; nothing submitted
+        result = await client.execute(intent, wallet)
+        if not result.success:
+            print(result.error.code, result.error.remediation)
+
+asyncio.run(main())`}
+          
+

+ The CLI follows the same model — every mutation supports --dry-run, which shows the + fee, the predicted effects, and any policy verdict without submitting: +

+
+            {`btcli config set network finney
+btcli wallet balance my_coldkey
+btcli query metagraph --netuid 1
+btcli tx transfer --dest 5F...dest --amount-tao 1.5 --dry-run
+btcli tx transfer --dest 5F...dest --amount-tao 1.5 -w my_coldkey`}
+          
From 437badb04792b157ee7b70361edf6ddab717dea2 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:38:33 -0600 Subject: [PATCH 53/68] add conviction threshold graph to v430 release page Inline SVG showing total conviction crossing the 10% threshold after the one-year age gate, with the contestable region highlighted. Co-authored-by: Cursor --- .../releases/v430-upgrade/page.module.css | 12 +++ .../releases/v430-upgrade/page.tsx | 88 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css index ba354ebea2..49883df2b6 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css @@ -61,6 +61,18 @@ line-height: 1.6; } +.graph { + width: 100%; + height: auto; + margin-top: 8px; +} + +.graph_caption { + font-size: 12px; + opacity: 0.7; + text-align: center !important; +} + .metrics_table { width: 100%; border-collapse: collapse; diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index 3d65b2415d..3a6d38b2dd 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -25,6 +25,88 @@ const DocLink = ({href, children}: {href: string; children: React.ReactNode}) => ); +const GRAPH_TEXT = { + fontFamily: 'FiraCode', + fontSize: 10, + fill: 'rgb(41, 41, 41)', +} as const; + +const ConvictionGraph = () => ( + + {/* Region where both conditions hold */} + + + OWNERSHIP + + + CONTESTABLE + + + {/* Axes */} + + + + TIME + + + 0% + + + 10% + + + 20% + + + {/* 10% threshold */} + + + CONVICTION THRESHOLD: 10% OF OUTSTANDING ALPHA + + + {/* Subnet age = 1 year */} + + + SUBNET AGE = 1 YEAR + + + {/* Total conviction accrued by lockers */} + + + TOTAL CONVICTION + + + {/* Point where the threshold is crossed past one year of age */} + + +); + const page = () => { return ( }> @@ -76,6 +158,12 @@ const page = () => { highest conviction.

+ +

+ Ownership becomes contestable once both conditions hold: the subnet is older than + one year, and total conviction exceeds ten percent of its outstanding alpha. At + that point the hotkey with the highest conviction takes ownership. +

Subnet ownership is therefore no longer fixed at registration; it is contestable through open, on-chain rules. Two lock modes are available. Perpetual locks mature From 99132e23b92cbed10683032dbcf5afb8d97cbb22 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:39:01 -0600 Subject: [PATCH 54/68] credit v430 release page to Arbos Co-authored-by: Cursor --- .../app/(pages-without-footer)/releases/v430-upgrade/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index 3a6d38b2dd..58f8c4842d 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -114,7 +114,7 @@ const page = () => {

The V430 Upgrade

- Rao Foundation + Written by Arbos

July 2026 From bc17e1776972cdf36b637d815315b61abe95489b Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:43:07 -0600 Subject: [PATCH 55/68] add before/after emission split chart to v430 release page Bar comparison of three same-price subnets: shares shrink with age under the old root-proportion term, and are equal under the new price-only split. Co-authored-by: Cursor --- .../releases/v430-upgrade/page.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index 58f8c4842d..dc1de27f49 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -107,6 +107,74 @@ const ConvictionGraph = () => ( ); +const EMISSION_BARS = { + before: [150, 108, 70], + after: [109, 109, 109], + ages: ['3 MO', '1 YR', '2 YR'], +} as const; + +const EmissionGraph = () => ( + + {(['before', 'after'] as const).map((panel, p) => { + const x0 = p === 0 ? 80 : 425; + return ( + + + {panel === 'before' ? 'BEFORE V430' : 'AFTER V430'} + + + {panel === 'before' ? 'SAME PRICE, LESS WITH AGE' : 'EMISSION FOLLOWS PRICE'} + + {EMISSION_BARS[panel].map((h, i) => { + const x = x0 + i * 90; + return ( + + + + {EMISSION_BARS.ages[i]} + + + ); + })} + + + ); + })} + + {'\u2192'} + + + THREE SUBNETS, IDENTICAL MOVING-AVERAGE PRICE + + +); + const page = () => { return ( }> @@ -188,6 +256,12 @@ const page = () => { root stakers' share of dividends — but it no longer affects how emission is divided between subnets.

+ +

+ Three subnets with an identical moving-average price. Before v430, the + root-proportion term reduced each subnet's share as its alpha issuance grew; + after, the same price earns the same emission regardless of age. +

The result is that a subnet's emission share is a direct function of its market price. The full formula and its parameters are documented in{' '} From 7aba261f0e61748f41fb8776233ef51084b52d86 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:45:45 -0600 Subject: [PATCH 56/68] add Ledger and extension signing examples to v430 release page Co-authored-by: Cursor --- .../releases/v430-upgrade/page.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index dc1de27f49..8359c913cc 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -371,15 +371,29 @@ btcli tx transfer --dest 5F...dest --amount-tao 1.5 -w my_coldkey`} Every transaction can now be signed on a Ledger hardware wallet{' '} using clear signing. Through merkleized metadata, the device decodes the transaction on its own screen, and the chain verifies the same metadata digest that - was signed; the device rejects any transaction it cannot decode and verify. Setup - and usage are documented in{' '} + was signed; the device rejects any transaction it cannot decode and verify. Any + command that signs accepts the --ledger flag: +

+
+            {`btcli tx transfer --dest 5F...dest --amount-tao 1 --ledger
+btcli tx transfer --dest 5F...dest --amount-tao 1 --ledger --ledger-account 1`}
+          
+

+ Setup and usage are documented in{' '} signing with a Ledger.

Transactions can also be signed with a browser extension — Talisman, Polkadot.js, or SubWallet. The CLI relays the transaction to the extension through a local bridge, and only the signature is returned; no keyfile, - password, or mnemonic is present on the machine running btcli. See{' '} + password, or mnemonic is present on the machine running btcli: +

+
+            {`btcli extension accounts    # list accounts the extensions expose
+btcli tx transfer --dest 5F...dest --amount-tao 1 --signer extension`}
+          
+

+ The full flow is documented in{' '} signing with a browser extension From 0a071ffed25c678be5a8cad08108a380a665806f Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:51:13 -0600 Subject: [PATCH 57/68] list new v11 capabilities on v430 release page Unit-safe money, policy guardrails, typed errors, signed requests, and timelock encryption, each linking to its documentation. Co-authored-by: Cursor --- .../releases/v430-upgrade/page.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index 8359c913cc..dc7f31a798 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -363,6 +363,41 @@ btcli query metagraph --netuid 1 btcli tx transfer --dest 5F...dest --amount-tao 1.5 --dry-run btcli tx transfer --dest 5F...dest --amount-tao 1.5 -w my_coldkey`} +

+ Beyond the consolidation, v11 adds capabilities that did not exist in the old + stack: +

+
    +
  1. + Unit-safe money — every Balance is tagged with its currency, so + TAO and subnet alpha cannot be silently mixed; arithmetic across units raises + instead of producing a wrong number. See{' '} + money. +
  2. +
  3. + Policy guardrails — a client can be bound to hard limits + (maximum fee, maximum spend, allowed subnets), and any transaction that would + exceed them is refused before it is signed. See{' '} + the transaction model. +
  4. +
  5. + Typed errors — every failure returns a semantic error code and + a remediation hint rather than prose, with the full mapping published as a + machine-readable catalog. See errors. +
  6. +
  7. + Signed requests — hotkey-signed HTTP between validators and + miners, so a request provably came from a specific hotkey, covers exactly the + bytes received, and cannot be replayed. See{' '} + signed requests. +
  8. +
  9. + Timelock encryption — seal data that anyone can open at a known + future time and nobody, including the author, can open early; the same mechanism + that secures commit-reveal weights, exposed directly. See{' '} + timelock. +
  10. +
From 5d7de33026e4eb757d2b30a448f03fed2a0b02f7 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:54:15 -0600 Subject: [PATCH 58/68] expand v11 capability list on v430 release page Proxies, multisig, atomic batches, MEV-shielded submission, key rotation, and address-book resolution; note that the CLI is generated from the intent catalog. Co-authored-by: Cursor --- .../releases/v430-upgrade/page.tsx | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index dc7f31a798..4263b48035 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -353,8 +353,11 @@ async def main(): asyncio.run(main())`}

- The CLI follows the same model — every mutation supports --dry-run, which shows the - fee, the predicted effects, and any policy verdict without submitting: + The CLI is generated from the same catalog: every transaction is a btcli tx + command and every query a btcli query command, with hand-written groups wrapping + the familiar workflows and the v9 shorthands preserved as aliases. Every mutation + supports --dry-run, which shows the fee, the predicted effects, and any policy + verdict without submitting:

             {`btcli config set network finney
@@ -397,6 +400,35 @@ btcli tx transfer --dest 5F...dest --amount-tao 1.5 -w my_coldkey`}
               that secures commit-reveal weights, exposed directly. See{' '}
               timelock.
             
+            
  • + Proxies as a first-class signer — every transaction accepts + --proxy-for, so a delegate key can act for a coldkey that never comes online; + scoped proxy types, announced (delayed) proxies, and pure proxy accounts are all + supported. See advanced operations. +
  • +
  • + Multisig accounts — create and operate k-of-n multisig + accounts, with the full approve, execute, and cancel flow wrapped by the btcli + multisig command group. +
  • +
  • + Atomic batches and MEV-shielded submission — compose several + intents into one all-or-nothing transaction, or encrypt a coldkey transaction to + the next block's ephemeral key so it cannot be observed or front-run in the + mempool. +
  • +
  • + Safer key rotation — hotkey swaps move registrations and stake + to a new key, and a leaked coldkey can be evacuated through an announced, + five-day-delayed swap that the real owner can dispute. See{' '} + wallets and keys. +
  • +
  • + Address safety — CLI address arguments resolve from a saved + address book or local key names as well as raw ss58, a defense against address + poisoning documented in{' '} + address hygiene. +
  • From 216f3f12159515ff90f2707c597a5c56a300a870 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 13:56:53 -0600 Subject: [PATCH 59/68] add built-for-agents section to v430 release page Runtime-discoverable operation catalogs, execute-by-name, previews, policy bounds, and machine-readable docs endpoints. Co-authored-by: Cursor --- .../releases/v430-upgrade/page.tsx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx index 4263b48035..cb5821b9e8 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx @@ -475,10 +475,30 @@ btcli tx transfer --dest 5F...dest --amount-tao 1 --signer extension`} bittensor.com/docs. The reference pages for{' '} all 74 transactions and all 82 queries are generated directly from the SDK, so the reference cannot drift from the released - software. The site also publishes agent catalogs and plain-text endpoints, allowing - AI coding assistants to consume the documentation directly. Start at the{' '} - documentation home, or point an agent at the{' '} - agents page. + software. Start at the documentation home. +

    +
    + +
    +

    Built for agents

    +

    + The entire stack — SDK, CLI, documentation, and this website — is designed to be + driven by AI agents as well as humans. Every operation is discoverable at runtime + with a JSON schema (btcli tools on the CLI,{' '} + sub.intents.list_tools() in Python) and can be executed by name + from a plain dictionary, validated against that schema. Every mutation can be + previewed before it spends anything, every failure returns a machine-readable code + with a remediation hint, and a Policy can hard-bound what an agent's session + is allowed to do — spend caps, fee caps, allowed subnets. +

    +

    + The CLI never traps automation: --json produces machine-readable output on any + command, and a non-interactive session missing a confirmation is declined rather + than left hanging. The documentation publishes the same catalogs statically — + intents, reads, and errors as JSON — every page is fetchable as raw markdown, and + the full corpus is available at a single plain-text endpoint for loading into a + context window. The complete workflow is documented on{' '} + the agents page.

    From 1b810b5fff022baf065d4ee54e5856e005a775c4 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 14:12:23 -0600 Subject: [PATCH 60/68] fix v430 release page rendering on mobile Cap code blocks at the viewport width so their pre-formatted lines scroll instead of forcing the whole page to overflow horizontally, and bump SVG chart label sizes at phone widths where the scaled-down 10px labels became unreadable. Co-authored-by: Cursor --- .../releases/v430-upgrade/page.module.css | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css index 49883df2b6..f5ea37976f 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css @@ -50,7 +50,12 @@ font-family: 'FiraCode'; font-size: 12px; font-weight: 200; - width: 100%; + /* Viewport-based width: with white-space pre, width 100% would let the + longest code line dictate the section's min-content width and force the + whole page to overflow horizontally on narrow screens. The page + container is max-width 960px with 32px side padding. */ + width: calc(100vw - 64px); + max-width: 896px; margin: 0; padding: 16px 20px; border: 1px solid rgba(41, 41, 41, 0.2); @@ -67,6 +72,14 @@ margin-top: 8px; } +/* The SVG scales with the viewport, so at phone widths the 10px labels + become ~5px. CSS font-size beats the SVG presentation attribute. */ +@media (max-width: 640px) { + .graph text { + font-size: 16px; + } +} + .graph_caption { font-size: 12px; opacity: 0.7; From a5343baf1b17a759c9811b3118b787005892f789 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 14:15:07 -0600 Subject: [PATCH 61/68] add default slippage protection to SDK stake trades Stake intents gain a 5% rate tolerance by default with a slippage_protection escape hatch, SlippageTooHigh gets a targeted remediation message, and the fake substrate harness plus unit tests cover the new behavior. Co-authored-by: Cursor --- sdk/python/bittensor/intents/staking.py | 157 ++++++++++++++++-- sdk/python/bittensor/result.py | 20 ++- sdk/python/codegen/check.py | 1 - sdk/python/tests/harness/fake_substrate.py | 9 +- .../tests/unit/test_slippage_protection.py | 149 +++++++++++++++++ 5 files changed, 315 insertions(+), 21 deletions(-) create mode 100644 sdk/python/tests/unit/test_slippage_protection.py diff --git a/sdk/python/bittensor/intents/staking.py b/sdk/python/bittensor/intents/staking.py index 06cdb4782e..109433cd87 100644 --- a/sdk/python/bittensor/intents/staking.py +++ b/sdk/python/bittensor/intents/staking.py @@ -6,13 +6,18 @@ from typing import Any, ClassVar, Optional from .._generated import calls -from .._generated.runtime_apis import StakeInfoRuntimeApi +from .._generated.runtime_apis import StakeInfoRuntimeApi, SwapRuntimeApi from ..result import BittensorError +from ..settings import RAO_PER_TAO from ..signing import public_view from ._money import ALL, UNBOUNDED, Money, Spend, call_amount from .base import Intent from .registry import register +# Default slippage-protection tolerance: the pool price may move at most this +# fraction from the price observed at build time before the call stops filling. +DEFAULT_RATE_TOLERANCE = 0.05 + NETUID_HELP = "Subnet the stake lives on (netuid 0 is the root network)." STAKE_HOTKEY_HELP = "Hotkey the stake is held on (the validator backing the position)." @@ -34,6 +39,37 @@ "instead of failing the whole call when the limit would be breached." ) +SLIPPAGE_PROTECTION_HELP = ( + "Bound the price the swap may execute at (on by default): the call fails " + "(`SlippageTooHigh`) instead of filling once the pool price moves more than " + "`rate_tolerance` from the price at submission. Disable to execute at any price." +) + +RATE_TOLERANCE_HELP = ( + "Maximum price move slippage protection accepts, as a fraction (0.05 = 5%). " + "Ignored when slippage protection is disabled." +) + + +def _check_rate_tolerance(tolerance: float) -> None: + if not 0 <= tolerance < 1: + raise BittensorError( + f"rate_tolerance must be a fraction in [0, 1), got {tolerance!r} " + "(0.05 = 5%). To trade with no price bound, disable slippage protection " + "(slippage_protection=False / --no-slippage-protection) instead." + ) + + +async def _alpha_price_rao(substrate, netuid: int) -> int: + """Current spot alpha price (rao per alpha) used to derive a limit price.""" + price = await substrate.runtime_call(*SwapRuntimeApi.current_alpha_price, [netuid]) + if price is None: + raise BittensorError( + f"could not read the alpha price for netuid {netuid} to derive a slippage " + "limit; retry, or disable slippage protection to submit without a bound" + ) + return int(price) + async def _staked_rao(substrate, wallet: Any, hotkey_ss58: str, netuid: int) -> int: """Current stake (rao) the signing coldkey holds on ``hotkey_ss58`` at ``netuid``. @@ -60,18 +96,22 @@ class AddStake(Intent): Swaps TAO from the coldkey's free balance into the subnet's alpha at the current pool price and credits the result to your stake on the hotkey; on netuid 0 (root) the stake stays TAO-denominated. The swap moves the pool, - so large amounts incur slippage — use ``add_stake_limit`` to bound the - price. The position's value then follows the pool price and the validator's - performance, and can be exited later with ``remove_stake``. Fails if the - coldkey's free balance cannot cover the amount plus the transaction fee, - and with ``AmountTooLow`` when the amount is below the chain minimum of - 0.002 TAO plus the swap fee. Dynamic subnets also reject a single swap - larger than 1000x the pool's TAO reserve (``InsufficientLiquidity``). + so large amounts incur slippage. By default the call is slippage-protected: + it fails (``SlippageTooHigh``) instead of filling once the price rises more + than ``rate_tolerance`` (5%) above the price at submission — raise the + tolerance or set ``slippage_protection`` to False to execute at any price, + or use ``add_stake_limit`` to set an explicit limit price. The position's + value then follows the pool price and the validator's performance, and can + be exited later with ``remove_stake``. Fails if the coldkey's free balance + cannot cover the amount plus the transaction fee, and with ``AmountTooLow`` + when the amount is below the chain minimum of 0.002 TAO plus the swap fee. + Dynamic subnets also reject a single swap larger than 1000x the pool's TAO + reserve (``InsufficientLiquidity``). """ op = "add_stake" signer = "coldkey" - wraps = (("SubtensorModule", "add_stake"),) + wraps = (("SubtensorModule", "add_stake"), ("SubtensorModule", "add_stake_limit")) mev_shield_default = True hotkey_ss58: str = field( @@ -79,13 +119,29 @@ class AddStake(Intent): ) netuid: int = field(metadata={"help": NETUID_HELP}) amount_tao: Money = field(metadata={"help": "How much of the coldkey's free balance to stake."}) + slippage_protection: bool = field(default=True, metadata={"help": SLIPPAGE_PROTECTION_HELP}) + rate_tolerance: float = field( + default=DEFAULT_RATE_TOLERANCE, metadata={"help": RATE_TOLERANCE_HELP} + ) def __post_init__(self): self.amount_tao = call_amount( self.amount_tao, self.wraps[0], "amount_staked", netuid=self.netuid ) + _check_rate_tolerance(self.rate_tolerance) async def build(self, substrate, wallet: Any): + if self.slippage_protection: + price = await _alpha_price_rao(substrate, self.netuid) + return await substrate.compose( + calls.SubtensorModule.add_stake_limit( + hotkey=self.hotkey_ss58, + netuid=self.netuid, + amount_staked=self.amount_tao.rao, + limit_price=int(price * (1 + self.rate_tolerance)), + allow_partial=False, + ) + ) return await substrate.compose( calls.SubtensorModule.add_stake( hotkey=self.hotkey_ss58, @@ -95,7 +151,12 @@ async def build(self, substrate, wallet: Any): ) def summary(self) -> str: - return f"stake {self.amount_tao} to {self.hotkey_ss58} on netuid {self.netuid}" + note = ( + f" (fails if price moves >{self.rate_tolerance:.2%})" + if self.slippage_protection + else " (no slippage protection)" + ) + return f"stake {self.amount_tao} to {self.hotkey_ss58} on netuid {self.netuid}{note}" def spend(self) -> Spend: return self.amount_tao @@ -110,7 +171,11 @@ class RemoveStake(Intent): it to the signing coldkey's free balance. Pass ``all`` to exit the entire position on that hotkey and subnet (the build fails if nothing is staked there). Like staking, the swap moves the pool, so large amounts incur - slippage — use ``remove_stake_limit`` to bound the price. The hotkey and + slippage. By default the call is slippage-protected: it fails + (``SlippageTooHigh``) instead of filling once the price falls more than + ``rate_tolerance`` (5%) below the price at submission — raise the tolerance + or set ``slippage_protection`` to False to execute at any price, or use + ``remove_stake_limit`` to set an explicit limit price. The hotkey and netuid must match where the stake is actually held, and the subnet must have subtoken trading enabled. The requested amount is capped to the stake currently available. A partial unstake must leave a remainder @@ -120,7 +185,7 @@ class RemoveStake(Intent): op = "remove_stake" signer = "coldkey" - wraps = (("SubtensorModule", "remove_stake"),) + wraps = (("SubtensorModule", "remove_stake"), ("SubtensorModule", "remove_stake_limit")) mev_shield_default = True all_amount_fields: ClassVar[tuple[str, ...]] = ("amount_alpha",) @@ -129,17 +194,33 @@ class RemoveStake(Intent): amount_alpha: Money = field( metadata={"help": "How much to unstake from this position, or ``all``."} ) + slippage_protection: bool = field(default=True, metadata={"help": SLIPPAGE_PROTECTION_HELP}) + rate_tolerance: float = field( + default=DEFAULT_RATE_TOLERANCE, metadata={"help": RATE_TOLERANCE_HELP} + ) def __post_init__(self): self.amount_alpha = call_amount( self.amount_alpha, self.wraps[0], "amount_unstaked", netuid=self.netuid, allow_all=True ) + _check_rate_tolerance(self.rate_tolerance) async def build(self, substrate, wallet: Any): if self.amount_alpha == ALL: rao = await _staked_rao(substrate, wallet, self.hotkey_ss58, self.netuid) else: rao = self.amount_alpha.rao + if self.slippage_protection: + price = await _alpha_price_rao(substrate, self.netuid) + return await substrate.compose( + calls.SubtensorModule.remove_stake_limit( + hotkey=self.hotkey_ss58, + netuid=self.netuid, + amount_unstaked=rao, + limit_price=int(price * (1 - self.rate_tolerance)), + allow_partial=False, + ) + ) return await substrate.compose( calls.SubtensorModule.remove_stake( hotkey=self.hotkey_ss58, @@ -150,7 +231,12 @@ async def build(self, substrate, wallet: Any): def summary(self) -> str: amount = "ALL alpha" if self.amount_alpha == ALL else str(self.amount_alpha) - return f"unstake {amount} from {self.hotkey_ss58} on netuid {self.netuid}" + note = ( + f" (fails if price moves >{self.rate_tolerance:.2%})" + if self.slippage_protection + else " (no slippage protection)" + ) + return f"unstake {amount} from {self.hotkey_ss58} on netuid {self.netuid}{note}" async def warnings(self, substrate, signer_address: str) -> list[str]: if self.amount_alpha == ALL: @@ -412,14 +498,18 @@ class SwapStake(Intent): Moves part of a position from the origin subnet to the destination subnet while staying on the same hotkey: the alpha is swapped to TAO in the origin pool and then to alpha in the destination pool, so both legs can - incur slippage. The two netuids must differ (``SameNetuid``). Use - ``move_stake`` when the hotkey should change too, and ``remove_stake`` - plus ``add_stake`` only if you want to control each leg separately. + incur slippage. By default the call is slippage-protected: it fails + (``SlippageTooHigh``) instead of filling once the origin/destination price + ratio falls more than ``rate_tolerance`` (5%) below the ratio at submission + — raise the tolerance or set ``slippage_protection`` to False to execute at + any price. The two netuids must differ (``SameNetuid``). Use ``move_stake`` + when the hotkey should change too, and ``remove_stake`` plus ``add_stake`` + only if you want to control each leg separately. """ op = "swap_stake" signer = "coldkey" - wraps = (("SubtensorModule", "swap_stake"),) + wraps = (("SubtensorModule", "swap_stake"), ("SubtensorModule", "swap_stake_limit")) mev_shield_default = True hotkey_ss58: str = field(metadata={"help": STAKE_HOTKEY_HELP}) @@ -431,13 +521,39 @@ class SwapStake(Intent): "explicit amount; ``all`` is not accepted)." } ) + slippage_protection: bool = field(default=True, metadata={"help": SLIPPAGE_PROTECTION_HELP}) + rate_tolerance: float = field( + default=DEFAULT_RATE_TOLERANCE, metadata={"help": RATE_TOLERANCE_HELP} + ) def __post_init__(self): self.amount_alpha = call_amount( self.amount_alpha, self.wraps[0], "alpha_amount", netuid=self.origin_netuid ) + _check_rate_tolerance(self.rate_tolerance) async def build(self, substrate, wallet: Any): + if self.slippage_protection: + origin_price = await _alpha_price_rao(substrate, self.origin_netuid) + dest_price = await _alpha_price_rao(substrate, self.dest_netuid) + if dest_price <= 0: + raise BittensorError( + f"netuid {self.dest_netuid} has no alpha price; cannot derive a " + "slippage limit — disable slippage protection to submit anyway" + ) + # The chain compares the limit against the origin/destination price + # ratio (falling as the swap executes), scaled by 1e9. + ratio_rao = origin_price * RAO_PER_TAO // dest_price + return await substrate.compose( + calls.SubtensorModule.swap_stake_limit( + hotkey=self.hotkey_ss58, + origin_netuid=self.origin_netuid, + destination_netuid=self.dest_netuid, + alpha_amount=self.amount_alpha.rao, + limit_price=int(ratio_rao * (1 - self.rate_tolerance)), + allow_partial=False, + ) + ) return await substrate.compose( calls.SubtensorModule.swap_stake( hotkey=self.hotkey_ss58, @@ -448,9 +564,14 @@ async def build(self, substrate, wallet: Any): ) def summary(self) -> str: + note = ( + f" (fails if price ratio moves >{self.rate_tolerance:.2%})" + if self.slippage_protection + else " (no slippage protection)" + ) return ( f"swap {self.amount_alpha} on {self.hotkey_ss58} from netuid " - f"{self.origin_netuid} to netuid {self.dest_netuid}" + f"{self.origin_netuid} to netuid {self.dest_netuid}{note}" ) def touches_netuids(self) -> list[int]: diff --git a/sdk/python/bittensor/result.py b/sdk/python/bittensor/result.py index 38c30504a2..3b4aa1cec8 100644 --- a/sdk/python/bittensor/result.py +++ b/sdk/python/bittensor/result.py @@ -83,7 +83,12 @@ class ConnectionNotReady(BittensorError): "\n" "unlike insufficient_balance this is not about your account — it is about " "the subnet's liquidity. reduce the amount, split it into smaller steps " - "across blocks, or trade on a subnet with deeper reserves." + "across blocks, or trade on a subnet with deeper reserves.\n" + "\n" + "stake trades are slippage-protected by default with a 5% tolerance " + "(SlippageTooHigh): raise it with `--rate-tolerance`, or disable the " + "protection with `--no-slippage-protection` (slippage_protection=False " + "in the SDK) to trade through the price move." ), ErrorCode.RATE_LIMITED: ( "the chain enforces per-key rate limits on certain calls (registration, weight " @@ -242,6 +247,17 @@ class ConnectionNotReady(BittensorError): ("invalid transaction", ErrorCode.INVALID_ARGUMENT), ) +# Remediation overrides keyed by the exact chain error name; more specific +# than the per-code defaults above. +_NAME_HELP_OVERRIDES: dict[str, str] = { + "SlippageTooHigh": ( + "the price moved past the slippage-protection limit (stake trades are " + "protected by default with a 5% tolerance); retry, raise the tolerance " + "(`--rate-tolerance 0.1` / rate_tolerance=0.1), or disable protection " + "entirely (`--no-slippage-protection` / slippage_protection=False)" + ), +} + _HELP_OVERRIDES: tuple[tuple[str, str], ...] = ( ( "bad signature", @@ -287,6 +303,8 @@ def __init__( @property def remediation(self) -> str: + if self.name in _NAME_HELP_OVERRIDES: + return _NAME_HELP_OVERRIDES[self.name] haystack = self.message.lower() for needle, help_text in _HELP_OVERRIDES: if needle in haystack: diff --git a/sdk/python/codegen/check.py b/sdk/python/codegen/check.py index 1d22cd93f2..56959078c7 100644 --- a/sdk/python/codegen/check.py +++ b/sdk/python/codegen/check.py @@ -112,7 +112,6 @@ def check_drift(endpoint: str) -> int: "burn_alpha", "recycle_alpha", "remove_stake_full_limit", - "swap_stake_limit", "register_limit", # identity / metadata / misc (set_identity / set_subnet_identity / # update_symbol are wrapped) diff --git a/sdk/python/tests/harness/fake_substrate.py b/sdk/python/tests/harness/fake_substrate.py index 1c0124679b..fc7fbf964b 100644 --- a/sdk/python/tests/harness/fake_substrate.py +++ b/sdk/python/tests/harness/fake_substrate.py @@ -61,6 +61,13 @@ ("SubtensorModule", "InitialStartCallDelay"): 100, } +# Runtime-API results answered when the test seeds nothing. Chosen so intent +# builds that consult chain state (e.g. the alpha price backing the default +# slippage-protection limit) work offline. +DEFAULT_RUNTIME: dict[tuple[str, str], Any] = { + ("SwapRuntimeApi", "current_alpha_price"): 10**9, # 1 TAO per alpha +} + GENESIS_HASH = "0x" + "00" * 32 @@ -128,7 +135,7 @@ def __init__(self) -> None: self._maps: dict[tuple[str, str], list[tuple[Any, Any]]] = {} self._constants: dict[tuple[str, str], Any] = dict(DEFAULT_CONSTANTS) # (api, method) -> value, or callable(params) -> value - self._runtime: dict[tuple[str, str], Any] = {} + self._runtime: dict[tuple[str, str], Any] = dict(DEFAULT_RUNTIME) self.fee = Balance.from_rao(124_414) self.weight = {"ref_time": 1_000_000, "proof_size": 3_593} diff --git a/sdk/python/tests/unit/test_slippage_protection.py b/sdk/python/tests/unit/test_slippage_protection.py new file mode 100644 index 0000000000..2f219696de --- /dev/null +++ b/sdk/python/tests/unit/test_slippage_protection.py @@ -0,0 +1,149 @@ +"""Default slippage protection on the staking intents. + +``add_stake``, ``remove_stake``, and ``swap_stake`` are slippage-protected by +default: at build time they read the spot price and compose the ``*_limit`` +call variant with a fill-or-kill limit derived from ``rate_tolerance`` (5%). +These tests pin the call selection, the limit-price math against seeded +prices, the opt-out, the failure modes, and the ``SlippageTooHigh`` +remediation that tells the user how to loosen or disable the protection. +""" + +from __future__ import annotations + +import pytest + +from bittensor.intents import REGISTRY, build +from bittensor.result import BittensorError, ChainError, ErrorCode +from tests.harness.fake_substrate import FakeSubstrate +from tests.harness.samples import BOB_HOT, dev_wallet + +RAO = 10**9 + +ADD = {"hotkey_ss58": BOB_HOT, "netuid": 1, "amount_tao": 1.0} +REMOVE = {"hotkey_ss58": BOB_HOT, "netuid": 1, "amount_alpha": 1.0} +SWAP = {"hotkey_ss58": BOB_HOT, "origin_netuid": 1, "dest_netuid": 2, "amount_alpha": 1.0} + + +@pytest.fixture() +def substrate() -> FakeSubstrate: + sub = FakeSubstrate() + # Spot prices: netuid 1 at 2 TAO/alpha, netuid 2 at 1 TAO/alpha. + sub.seed_runtime("SwapRuntimeApi", "current_alpha_price", lambda p: {1: 2 * RAO, 2: RAO}[p[0]]) + return sub + + +@pytest.fixture(scope="module") +def wallet(): + return dev_wallet() + + +class TestDefaultProtection: + @pytest.mark.asyncio + async def test_add_stake_composes_limit_call(self, substrate, wallet): + call = await build("add_stake", ADD).build(substrate, wallet) + assert call.function == "add_stake_limit" + # Max price to pay: spot * (1 + 5%). + assert call.params["limit_price"] == int(2 * RAO * 1.05) + assert call.params["allow_partial"] is False + assert call.params["amount_staked"] == RAO + + @pytest.mark.asyncio + async def test_remove_stake_composes_limit_call(self, substrate, wallet): + call = await build("remove_stake", REMOVE).build(substrate, wallet) + assert call.function == "remove_stake_limit" + # Min price to accept: spot * (1 - 5%). + assert call.params["limit_price"] == int(2 * RAO * 0.95) + assert call.params["allow_partial"] is False + + @pytest.mark.asyncio + async def test_swap_stake_composes_limit_call(self, substrate, wallet): + call = await build("swap_stake", SWAP).build(substrate, wallet) + assert call.function == "swap_stake_limit" + # Min origin/destination price ratio (scaled by 1e9): 2.0 * (1 - 5%). + assert call.params["limit_price"] == int(2 * RAO * 0.95) + assert call.params["allow_partial"] is False + + @pytest.mark.asyncio + async def test_custom_tolerance_moves_the_limit(self, substrate, wallet): + call = await build("add_stake", {**ADD, "rate_tolerance": 0.1}).build(substrate, wallet) + assert call.params["limit_price"] == int(2 * RAO * 1.1) + call = await build("remove_stake", {**REMOVE, "rate_tolerance": 0.1}).build( + substrate, wallet + ) + assert call.params["limit_price"] == int(2 * RAO * 0.9) + + @pytest.mark.asyncio + async def test_remove_all_resolves_stake_and_keeps_protection(self, substrate, wallet): + substrate.seed_runtime( + "StakeInfoRuntimeApi", + "get_stake_info_for_hotkey_coldkey_netuid", + {"stake": 5 * RAO}, + ) + call = await build("remove_stake", {**REMOVE, "amount_alpha": "all"}).build( + substrate, wallet + ) + assert call.function == "remove_stake_limit" + assert call.params["amount_unstaked"] == 5 * RAO + + def test_summary_names_the_tolerance(self): + assert "5.00%" in build("add_stake", ADD).summary() + off = build("add_stake", {**ADD, "slippage_protection": False}).summary() + assert "no slippage protection" in off + + +class TestOptOut: + @pytest.mark.parametrize( + ("op", "args", "plain"), + [ + ("add_stake", ADD, "add_stake"), + ("remove_stake", REMOVE, "remove_stake"), + ("swap_stake", SWAP, "swap_stake"), + ], + ) + @pytest.mark.asyncio + async def test_disabled_composes_plain_call(self, substrate, wallet, op, args, plain): + call = await build(op, {**args, "slippage_protection": False}).build(substrate, wallet) + assert call.function == plain + assert "limit_price" not in call.params + + +class TestFailureModes: + @pytest.mark.parametrize("bad", [-0.1, 1.0, 5]) + @pytest.mark.parametrize( + ("op", "args"), [("add_stake", ADD), ("remove_stake", REMOVE), ("swap_stake", SWAP)] + ) + def test_out_of_range_tolerance_rejected_at_construction(self, op, args, bad): + with pytest.raises(BittensorError, match="rate_tolerance"): + build(op, {**args, "rate_tolerance": bad}) + + @pytest.mark.asyncio + async def test_missing_price_fails_with_disable_hint(self, wallet): + sub = FakeSubstrate() + sub.seed_runtime("SwapRuntimeApi", "current_alpha_price", None) + with pytest.raises(BittensorError, match="disable slippage protection"): + await build("add_stake", ADD).build(sub, wallet) + + @pytest.mark.asyncio + async def test_swap_with_unpriced_destination_fails_loudly(self, wallet): + sub = FakeSubstrate() + sub.seed_runtime("SwapRuntimeApi", "current_alpha_price", lambda p: {1: RAO, 2: 0}[p[0]]) + with pytest.raises(BittensorError, match="no alpha price"): + await build("swap_stake", SWAP).build(sub, wallet) + + +class TestErrorSurface: + def test_slippage_too_high_remediation_names_the_off_switch(self): + error = ChainError("Slippage is too high for the transaction.", "SlippageTooHigh") + assert error.code is ErrorCode.INSUFFICIENT_LIQUIDITY + assert "--rate-tolerance" in error.remediation + assert "--no-slippage-protection" in error.remediation + assert "slippage_protection=False" in error.remediation + + def test_schema_exposes_the_protection_fields(self): + for op in ("add_stake", "remove_stake", "swap_stake"): + schema = REGISTRY[op].json_schema() + assert schema["properties"]["slippage_protection"]["type"] == "boolean" + assert schema["properties"]["rate_tolerance"]["type"] == "number" + # Optional with defaults: agents that omit them get the protection. + assert "slippage_protection" not in schema["required"] + assert "rate_tolerance" not in schema["required"] From 19a2c9060377ad81a9af89781f9ca34041aba50c Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 14:20:42 -0600 Subject: [PATCH 62/68] rename release page to v431 and trim intro The pending SDK changes ship with this release, so the blog, route, sitemap, releases index, and OG image move from v430 to v431. Drop the redundant opening sentence. Co-authored-by: Cursor --- .../{v430-upgrade.png => v431-upgrade.png} | Bin .../(pages-without-footer)/releases/page.tsx | 6 ++--- .../page.module.css | 0 .../{v430-upgrade => v431-upgrade}/page.tsx | 22 +++++++++--------- .../apps/bittensor-website/src/app/sitemap.ts | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) rename website/apps/bittensor-website/public/images/og_thumbs/{v430-upgrade.png => v431-upgrade.png} (100%) rename website/apps/bittensor-website/src/app/(pages-without-footer)/releases/{v430-upgrade => v431-upgrade}/page.module.css (100%) rename website/apps/bittensor-website/src/app/(pages-without-footer)/releases/{v430-upgrade => v431-upgrade}/page.tsx (97%) diff --git a/website/apps/bittensor-website/public/images/og_thumbs/v430-upgrade.png b/website/apps/bittensor-website/public/images/og_thumbs/v431-upgrade.png similarity index 100% rename from website/apps/bittensor-website/public/images/og_thumbs/v430-upgrade.png rename to website/apps/bittensor-website/public/images/og_thumbs/v431-upgrade.png diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx index 2afd024541..5f4de79780 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/page.tsx @@ -23,14 +23,14 @@ type Release = { // Newest first. Add new releases to the top. const releases: Release[] = [ { - tag: 'v430', + tag: 'v431', date: 'July 2026', - title: 'The V430 Upgrade', + title: 'The V431 Upgrade', summary: 'Conviction-based subnet ownership, price-driven emissions, the bittensor v11 SDK ' + 'with a Rust core, Ledger and browser-extension signing, and a verifiable upgrade ' + 'pipeline — the monorepo era.', - href: '/releases/v430-upgrade', + href: '/releases/v431-upgrade', }, ]; diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.module.css similarity index 100% rename from website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.module.css rename to website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.module.css diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx similarity index 97% rename from website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx rename to website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx index cb5821b9e8..7e1bf578a5 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v430-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx @@ -6,12 +6,12 @@ import {Suspense} from 'react'; import styles from './page.module.css'; export const metadata: Metadata = { - title: 'The V430 Upgrade', + title: 'The V431 Upgrade', description: 'One repository, one package, new documentation, and new network economics: ' + 'conviction-based subnet ownership, price-driven emissions, and the bittensor v11 SDK.', - alternates: {canonical: '/releases/v430-upgrade'}, - openGraph: {images: '/images/og_thumbs/v430-upgrade.png'}, + alternates: {canonical: '/releases/v431-upgrade'}, + openGraph: {images: '/images/og_thumbs/v431-upgrade.png'}, }; const CONNECT_LINK_ORDER = ['DISCORD', 'X', 'GITHUB'] as const; @@ -118,14 +118,14 @@ const EmissionGraph = () => ( className={styles.graph} viewBox='0 0 760 340' role='img' - aria-label='Before v430, three subnets with the same price received progressively less emission with age; after v430, their emission shares are equal because price alone determines the split.' + aria-label='Before v431, three subnets with the same price received progressively less emission with age; after v431, their emission shares are equal because price alone determines the split.' > {(['before', 'after'] as const).map((panel, p) => { const x0 = p === 0 ? 80 : 425; return ( - {panel === 'before' ? 'BEFORE V430' : 'AFTER V430'} + {panel === 'before' ? 'BEFORE V431' : 'AFTER V431'} { }>
    -

    The V430 Upgrade

    +

    The V431 Upgrade

    Written by Arbos

    @@ -192,8 +192,8 @@ const page = () => {

    Introduction

    - Bittensor has completed a major network upgrade. The chain now runs - spec version 430, and the release changes both the network's + The chain now runs + spec version 431, and the release changes both the network's economics and the software used to interact with it. Subnet ownership is now contestable through a time-weighted commitment mechanism called{' '} conviction. Emission between @@ -215,7 +215,7 @@ const page = () => {

    Locking alpha on a subnet accrues conviction: a time-weighted commitment score credited to a hotkey chosen by the locker. Prior to this upgrade, - conviction was recorded on-chain but had no effect. As of spec 430, it governs + conviction was recorded on-chain but had no effect. As of spec 431, it governs subnet ownership:

    @@ -258,7 +258,7 @@ const page = () => {

    - Three subnets with an identical moving-average price. Before v430, the + Three subnets with an identical moving-average price. Before v431, the root-proportion term reduced each subnet's share as its alpha issuance grew; after, the same price earns the same emission regardless of age.

    @@ -544,7 +544,7 @@ btcli tx transfer --dest 5F...dest --amount-tao 1 --signer extension`} every existing proxy configuration.
  • - Node operators — nodes not yet running the spec 430 binary must + Node operators — nodes not yet running the spec 431 binary must upgrade to continue syncing.
  • diff --git a/website/apps/bittensor-website/src/app/sitemap.ts b/website/apps/bittensor-website/src/app/sitemap.ts index 6ccd8e69be..868b16975a 100644 --- a/website/apps/bittensor-website/src/app/sitemap.ts +++ b/website/apps/bittensor-website/src/app/sitemap.ts @@ -11,7 +11,7 @@ const staticRoutes = [ '/explore', '/intro', '/releases', - '/releases/v430-upgrade', + '/releases/v431-upgrade', '/wallet', '/whitepaper', ]; From 98092ea38cf18881264b7920413857cae9cbbcd0 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 14:28:04 -0600 Subject: [PATCH 63/68] describe conviction lock timing as half-lives on release page Co-authored-by: Cursor --- .../releases/v431-upgrade/page.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx index 7e1bf578a5..42a14b6d27 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx @@ -234,11 +234,14 @@ const page = () => {

    Subnet ownership is therefore no longer fixed at registration; it is contestable - through open, on-chain rules. Two lock modes are available. Perpetual locks mature - toward their full conviction mass in approximately six weeks. Decaying locks — the - default — accrue and then unwind over approximately four months. The mechanism is - designed to reward long-horizon commitment to a subnet's success. The lock - modes, the conviction formula, and a worked example are documented in the{' '} + through open, on-chain rules. Two lock modes are available, and both are + exponential processes rather than fixed terms. A perpetual lock's conviction + climbs toward its full locked mass, reaching half of it in roughly 43 days. A + decaying lock — the default — frees its locked mass with a half-life of roughly + 130 days, so its conviction peaks and then unwinds. Both rates are governance-set + parameters. The mechanism is designed to reward long-horizon commitment to a + subnet's success. The lock modes, the conviction formula, and a worked example + are documented in the{' '} conviction guide.

  • From 5d20ad3b6ad6fbf24e8b5c6ba15e768fec077827 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 14:29:09 -0600 Subject: [PATCH 64/68] describe lock timing via configured rates and owner exception Co-authored-by: Cursor --- .../releases/v431-upgrade/page.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx index 42a14b6d27..9d9db08653 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx @@ -236,10 +236,14 @@ const page = () => { Subnet ownership is therefore no longer fixed at registration; it is contestable through open, on-chain rules. Two lock modes are available, and both are exponential processes rather than fixed terms. A perpetual lock's conviction - climbs toward its full locked mass, reaching half of it in roughly 43 days. A - decaying lock — the default — frees its locked mass with a half-life of roughly - 130 days, so its conviction peaks and then unwinds. Both rates are governance-set - parameters. The mechanism is designed to reward long-horizon commitment to a + approaches its locked mass asymptotically — it never quite completes — at a pace + set by the chain's maturity rate, a time constant of roughly 43 days at + current values. A decaying lock — the default — frees its locked mass at the + chain's unlock rate, a time constant of roughly 130 days, so its + conviction peaks and then unwinds. Both rates are governance-set storage values + that can change, and there is one exception: locks credited to the subnet + owner's own hotkey mature instantly, so their conviction always equals their + locked mass. The mechanism is designed to reward long-horizon commitment to a subnet's success. The lock modes, the conviction formula, and a worked example are documented in the{' '} conviction guide. From 3c932b5fef37ed0a1c46c7b7aa187b38916a92a4 Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 14:30:38 -0600 Subject: [PATCH 65/68] state conviction time constants with explicit percentages Co-authored-by: Cursor --- .../releases/v431-upgrade/page.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx index 9d9db08653..ebf538826a 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx @@ -236,11 +236,13 @@ const page = () => { Subnet ownership is therefore no longer fixed at registration; it is contestable through open, on-chain rules. Two lock modes are available, and both are exponential processes rather than fixed terms. A perpetual lock's conviction - approaches its locked mass asymptotically — it never quite completes — at a pace - set by the chain's maturity rate, a time constant of roughly 43 days at - current values. A decaying lock — the default — frees its locked mass at the - chain's unlock rate, a time constant of roughly 130 days, so its - conviction peaks and then unwinds. Both rates are governance-set storage values + approaches its locked mass asymptotically — it never quite completes. The + chain's maturity rate sets the exponential time constant, roughly 43 + days at current values: after one time constant conviction stands at about 63% of + the locked mass, after two about 86%, and so on. A decaying lock — the default — + frees its locked mass on the chain's unlock rate, a time constant of + roughly 130 days, after which about 37% of the mass remains locked; its conviction + peaks and then unwinds. Both rates are governance-set storage values that can change, and there is one exception: locks credited to the subnet owner's own hotkey mature instantly, so their conviction always equals their locked mass. The mechanism is designed to reward long-horizon commitment to a From 7c7cd97888cca24c239e620cb6f3fe36316c570c Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 14:31:37 -0600 Subject: [PATCH 66/68] direct readers to live chain values for lock rates Co-authored-by: Cursor --- .../(pages-without-footer)/releases/v431-upgrade/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx index ebf538826a..da928da631 100644 --- a/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx +++ b/website/apps/bittensor-website/src/app/(pages-without-footer)/releases/v431-upgrade/page.tsx @@ -242,10 +242,10 @@ const page = () => { the locked mass, after two about 86%, and so on. A decaying lock — the default — frees its locked mass on the chain's unlock rate, a time constant of roughly 130 days, after which about 37% of the mass remains locked; its conviction - peaks and then unwinds. Both rates are governance-set storage values - that can change, and there is one exception: locks credited to the subnet - owner's own hotkey mature instantly, so their conviction always equals their - locked mass. The mechanism is designed to reward long-horizon commitment to a + peaks and then unwinds. Both rates are governance-set storage values — read them + from chain state before planning a lock rather than relying on the figures here — + and there is one exception: locks credited to the subnet owner's own hotkey + mature instantly, so their conviction always equals their locked mass. The mechanism is designed to reward long-horizon commitment to a subnet's success. The lock modes, the conviction formula, and a worked example are documented in the{' '} conviction guide. From 4c883f7f4780ab2d71a297664d21c745314ce10d Mon Sep 17 00:00:00 2001 From: unarbos Date: Mon, 13 Jul 2026 14:32:53 -0600 Subject: [PATCH 67/68] regenerate docs for slippage-protected stake intents The stake tx pages and intents catalog pick up the new slippage_protection and rate_tolerance parameters, fixing the docs drift gate. Co-authored-by: Cursor --- docs/tx/add-stake.mdx | 22 +++++++++----- docs/tx/remove-stake.mdx | 10 +++++-- docs/tx/swap-stake.mdx | 14 ++++++--- .../public/catalog/intents.json | 30 +++++++++++++++++-- 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/docs/tx/add-stake.mdx b/docs/tx/add-stake.mdx index c8eb47c2e3..a35d539c74 100644 --- a/docs/tx/add-stake.mdx +++ b/docs/tx/add-stake.mdx @@ -8,17 +8,21 @@ description: "Stake TAO from the coldkey onto a hotkey." Swaps TAO from the coldkey's free balance into the subnet's alpha at the current pool price and credits the result to your stake on the hotkey; on netuid 0 (root) the stake stays TAO-denominated. The swap moves the pool, -so large amounts incur slippage — use `add_stake_limit` to bound the -price. The position's value then follows the pool price and the validator's -performance, and can be exited later with `remove_stake`. Fails if the -coldkey's free balance cannot cover the amount plus the transaction fee, -and with `AmountTooLow` when the amount is below the chain minimum of -0.002 TAO plus the swap fee. Dynamic subnets also reject a single swap -larger than 1000x the pool's TAO reserve (`InsufficientLiquidity`). +so large amounts incur slippage. By default the call is slippage-protected: +it fails (`SlippageTooHigh`) instead of filling once the price rises more +than `rate_tolerance` (5%) above the price at submission — raise the +tolerance or set `slippage_protection` to False to execute at any price, +or use `add_stake_limit` to set an explicit limit price. The position's +value then follows the pool price and the validator's performance, and can +be exited later with `remove_stake`. Fails if the coldkey's free balance +cannot cover the amount plus the transaction fee, and with `AmountTooLow` +when the amount is below the chain minimum of 0.002 TAO plus the swap fee. +Dynamic subnets also reject a single swap larger than 1000x the pool's TAO +reserve (`InsufficientLiquidity`). | Signer | Pallet | Wraps | | --- | --- | --- | -| `coldkey` | SubtensorModule | `SubtensorModule.add_stake` | +| `coldkey` | SubtensorModule | `SubtensorModule.add_stake`, `SubtensorModule.add_stake_limit` | ## Parameters @@ -27,6 +31,8 @@ larger than 1000x the pool's TAO reserve (`InsufficientLiquidity`). | `hotkey_ss58` | string | yes | Hotkey the stake is added to (the validator you are backing). | | `netuid` | integer | yes | Subnet the stake lives on (netuid 0 is the root network). | | `amount_tao` | number \| `"all"` | yes | How much of the coldkey's free balance to stake. | +| `slippage_protection` | boolean | no | Bound the price the swap may execute at (on by default): the call fails (`SlippageTooHigh`) instead of filling once the pool price moves more than `rate_tolerance` from the price at submission. Disable to execute at any price. | +| `rate_tolerance` | number | no | Maximum price move slippage protection accepts, as a fraction (0.05 = 5%). Ignored when slippage protection is disabled. | Address parameters (`--hotkey`, `--coldkey`, `--dest`, ...) accept a raw ss58 address, an address-book or proxy-book name, or a local wallet/hotkey name. diff --git a/docs/tx/remove-stake.mdx b/docs/tx/remove-stake.mdx index 793db06552..e7f1812804 100644 --- a/docs/tx/remove-stake.mdx +++ b/docs/tx/remove-stake.mdx @@ -9,7 +9,11 @@ Swaps the alpha position back to TAO at the current pool price and credits it to the signing coldkey's free balance. Pass `all` to exit the entire position on that hotkey and subnet (the build fails if nothing is staked there). Like staking, the swap moves the pool, so large amounts incur -slippage — use `remove_stake_limit` to bound the price. The hotkey and +slippage. By default the call is slippage-protected: it fails +(`SlippageTooHigh`) instead of filling once the price falls more than +`rate_tolerance` (5%) below the price at submission — raise the tolerance +or set `slippage_protection` to False to execute at any price, or use +`remove_stake_limit` to set an explicit limit price. The hotkey and netuid must match where the stake is actually held, and the subnet must have subtoken trading enabled. The requested amount is capped to the stake currently available. A partial unstake must leave a remainder @@ -18,7 +22,7 @@ position instead of leaving dust (`AmountTooLow`). | Signer | Pallet | Wraps | | --- | --- | --- | -| `coldkey` | SubtensorModule | `SubtensorModule.remove_stake` | +| `coldkey` | SubtensorModule | `SubtensorModule.remove_stake`, `SubtensorModule.remove_stake_limit` | ## Parameters @@ -27,6 +31,8 @@ position instead of leaving dust (`AmountTooLow`). | `hotkey_ss58` | string | yes | Hotkey the stake is held on (the validator backing the position). | | `netuid` | integer | yes | Subnet the stake lives on (netuid 0 is the root network). | | `amount_alpha` | number \| `"all"` | yes | How much to unstake from this position, or ``all``. | +| `slippage_protection` | boolean | no | Bound the price the swap may execute at (on by default): the call fails (`SlippageTooHigh`) instead of filling once the pool price moves more than `rate_tolerance` from the price at submission. Disable to execute at any price. | +| `rate_tolerance` | number | no | Maximum price move slippage protection accepts, as a fraction (0.05 = 5%). Ignored when slippage protection is disabled. | Address parameters (`--hotkey`, `--coldkey`, `--dest`, ...) accept a raw ss58 address, an address-book or proxy-book name, or a local wallet/hotkey name. diff --git a/docs/tx/swap-stake.mdx b/docs/tx/swap-stake.mdx index fe4f27deb5..2f821a2e79 100644 --- a/docs/tx/swap-stake.mdx +++ b/docs/tx/swap-stake.mdx @@ -8,13 +8,17 @@ description: "Swap stake on one hotkey between two subnets." Moves part of a position from the origin subnet to the destination subnet while staying on the same hotkey: the alpha is swapped to TAO in the origin pool and then to alpha in the destination pool, so both legs can -incur slippage. The two netuids must differ (`SameNetuid`). Use -`move_stake` when the hotkey should change too, and `remove_stake` -plus `add_stake` only if you want to control each leg separately. +incur slippage. By default the call is slippage-protected: it fails +(`SlippageTooHigh`) instead of filling once the origin/destination price +ratio falls more than `rate_tolerance` (5%) below the ratio at submission +— raise the tolerance or set `slippage_protection` to False to execute at +any price. The two netuids must differ (`SameNetuid`). Use `move_stake` +when the hotkey should change too, and `remove_stake` plus `add_stake` +only if you want to control each leg separately. | Signer | Pallet | Wraps | | --- | --- | --- | -| `coldkey` | SubtensorModule | `SubtensorModule.swap_stake` | +| `coldkey` | SubtensorModule | `SubtensorModule.swap_stake`, `SubtensorModule.swap_stake_limit` | ## Parameters @@ -24,6 +28,8 @@ plus `add_stake` only if you want to control each leg separately. | `origin_netuid` | integer | yes | Subnet the stake currently sits on. | | `dest_netuid` | integer | yes | Subnet the stake ends up on. When it differs from the origin, the position is swapped through both subnet pools, which can incur slippage on each leg. | | `amount_alpha` | number \| `"all"` | yes | How much of the origin position to swap across (an explicit amount; ``all`` is not accepted). | +| `slippage_protection` | boolean | no | Bound the price the swap may execute at (on by default): the call fails (`SlippageTooHigh`) instead of filling once the pool price moves more than `rate_tolerance` from the price at submission. Disable to execute at any price. | +| `rate_tolerance` | number | no | Maximum price move slippage protection accepts, as a fraction (0.05 = 5%). Ignored when slippage protection is disabled. | Address parameters (`--hotkey`, `--coldkey`, `--dest`, ...) accept a raw ss58 address, an address-book or proxy-book name, or a local wallet/hotkey name. diff --git a/website/apps/bittensor-website/public/catalog/intents.json b/website/apps/bittensor-website/public/catalog/intents.json index 9eb8d7edf0..990cc26d4d 100644 --- a/website/apps/bittensor-website/public/catalog/intents.json +++ b/website/apps/bittensor-website/public/catalog/intents.json @@ -32,7 +32,7 @@ { "name": "add_stake", "summary": "Stake TAO from the coldkey onto a hotkey.", - "description": "Stake TAO from the coldkey onto a hotkey.\n\nSwaps TAO from the coldkey's free balance into the subnet's alpha at the\ncurrent pool price and credits the result to your stake on the hotkey; on\nnetuid 0 (root) the stake stays TAO-denominated. The swap moves the pool,\nso large amounts incur slippage \u2014 use `add_stake_limit` to bound the\nprice. The position's value then follows the pool price and the validator's\nperformance, and can be exited later with `remove_stake`. Fails if the\ncoldkey's free balance cannot cover the amount plus the transaction fee,\nand with `AmountTooLow` when the amount is below the chain minimum of\n0.002 TAO plus the swap fee. Dynamic subnets also reject a single swap\nlarger than 1000x the pool's TAO reserve (`InsufficientLiquidity`).", + "description": "Stake TAO from the coldkey onto a hotkey.\n\nSwaps TAO from the coldkey's free balance into the subnet's alpha at the\ncurrent pool price and credits the result to your stake on the hotkey; on\nnetuid 0 (root) the stake stays TAO-denominated. The swap moves the pool,\nso large amounts incur slippage. By default the call is slippage-protected:\nit fails (`SlippageTooHigh`) instead of filling once the price rises more\nthan `rate_tolerance` (5%) above the price at submission \u2014 raise the\ntolerance or set `slippage_protection` to False to execute at any price,\nor use `add_stake_limit` to set an explicit limit price. The position's\nvalue then follows the pool price and the validator's performance, and can\nbe exited later with `remove_stake`. Fails if the coldkey's free balance\ncannot cover the amount plus the transaction fee, and with `AmountTooLow`\nwhen the amount is below the chain minimum of 0.002 TAO plus the swap fee.\nDynamic subnets also reject a single swap larger than 1000x the pool's TAO\nreserve (`InsufficientLiquidity`).", "signer": "coldkey", "input_schema": { "type": "object", @@ -57,6 +57,14 @@ } ], "description": "How much of the coldkey's free balance to stake." + }, + "slippage_protection": { + "type": "boolean", + "description": "Bound the price the swap may execute at (on by default): the call fails (`SlippageTooHigh`) instead of filling once the pool price moves more than `rate_tolerance` from the price at submission. Disable to execute at any price." + }, + "rate_tolerance": { + "type": "number", + "description": "Maximum price move slippage protection accepts, as a fraction (0.05 = 5%). Ignored when slippage protection is disabled." } }, "required": [ @@ -1112,7 +1120,7 @@ { "name": "remove_stake", "summary": "Unstake alpha from a hotkey back to the coldkey.", - "description": "Unstake alpha from a hotkey back to the coldkey.\n\nSwaps the alpha position back to TAO at the current pool price and credits\nit to the signing coldkey's free balance. Pass `all` to exit the entire\nposition on that hotkey and subnet (the build fails if nothing is staked\nthere). Like staking, the swap moves the pool, so large amounts incur\nslippage \u2014 use `remove_stake_limit` to bound the price. The hotkey and\nnetuid must match where the stake is actually held, and the subnet must\nhave subtoken trading enabled. The requested amount is capped to the\nstake currently available. A partial unstake must leave a remainder\nworth at least 0.002 TAO at the simulated pool price \u2014 exit the full\nposition instead of leaving dust (`AmountTooLow`).", + "description": "Unstake alpha from a hotkey back to the coldkey.\n\nSwaps the alpha position back to TAO at the current pool price and credits\nit to the signing coldkey's free balance. Pass `all` to exit the entire\nposition on that hotkey and subnet (the build fails if nothing is staked\nthere). Like staking, the swap moves the pool, so large amounts incur\nslippage. By default the call is slippage-protected: it fails\n(`SlippageTooHigh`) instead of filling once the price falls more than\n`rate_tolerance` (5%) below the price at submission \u2014 raise the tolerance\nor set `slippage_protection` to False to execute at any price, or use\n`remove_stake_limit` to set an explicit limit price. The hotkey and\nnetuid must match where the stake is actually held, and the subnet must\nhave subtoken trading enabled. The requested amount is capped to the\nstake currently available. A partial unstake must leave a remainder\nworth at least 0.002 TAO at the simulated pool price \u2014 exit the full\nposition instead of leaving dust (`AmountTooLow`).", "signer": "coldkey", "input_schema": { "type": "object", @@ -1143,6 +1151,14 @@ } ], "description": "How much to unstake from this position, or ``all``." + }, + "slippage_protection": { + "type": "boolean", + "description": "Bound the price the swap may execute at (on by default): the call fails (`SlippageTooHigh`) instead of filling once the pool price moves more than `rate_tolerance` from the price at submission. Disable to execute at any price." + }, + "rate_tolerance": { + "type": "number", + "description": "Maximum price move slippage protection accepts, as a fraction (0.05 = 5%). Ignored when slippage protection is disabled." } }, "required": [ @@ -1990,7 +2006,7 @@ { "name": "swap_stake", "summary": "Swap stake on one hotkey between two subnets.", - "description": "Swap stake on one hotkey between two subnets.\n\nMoves part of a position from the origin subnet to the destination subnet\nwhile staying on the same hotkey: the alpha is swapped to TAO in the\norigin pool and then to alpha in the destination pool, so both legs can\nincur slippage. The two netuids must differ (`SameNetuid`). Use\n`move_stake` when the hotkey should change too, and `remove_stake`\nplus `add_stake` only if you want to control each leg separately.", + "description": "Swap stake on one hotkey between two subnets.\n\nMoves part of a position from the origin subnet to the destination subnet\nwhile staying on the same hotkey: the alpha is swapped to TAO in the\norigin pool and then to alpha in the destination pool, so both legs can\nincur slippage. By default the call is slippage-protected: it fails\n(`SlippageTooHigh`) instead of filling once the origin/destination price\nratio falls more than `rate_tolerance` (5%) below the ratio at submission\n\u2014 raise the tolerance or set `slippage_protection` to False to execute at\nany price. The two netuids must differ (`SameNetuid`). Use `move_stake`\nwhen the hotkey should change too, and `remove_stake` plus `add_stake`\nonly if you want to control each leg separately.", "signer": "coldkey", "input_schema": { "type": "object", @@ -2019,6 +2035,14 @@ } ], "description": "How much of the origin position to swap across (an explicit amount; ``all`` is not accepted)." + }, + "slippage_protection": { + "type": "boolean", + "description": "Bound the price the swap may execute at (on by default): the call fails (`SlippageTooHigh`) instead of filling once the pool price moves more than `rate_tolerance` from the price at submission. Disable to execute at any price." + }, + "rate_tolerance": { + "type": "number", + "description": "Maximum price move slippage protection accepts, as a fraction (0.05 = 5%). Ignored when slippage protection is disabled." } }, "required": [ From 4ba1519fa5445954a0db640fa795f35760e0c5f3 Mon Sep 17 00:00:00 2001 From: unarbos Date: Tue, 14 Jul 2026 10:42:13 -0600 Subject: [PATCH 68/68] build: record tikv-jemallocator in Cargo.lock for --locked builds Co-authored-by: Cursor --- Cargo.lock | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 14df26e2c0..bced401583 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8753,6 +8753,8 @@ dependencies = [ "subtensor-custom-rpc-runtime-api", "subtensor-macros", "subtensor-runtime-common", + "tikv-jemalloc-ctl", + "tikv-jemallocator", "tokio", ] @@ -19551,6 +19553,16 @@ dependencies = [ "libc", ] +[[package]] +name = "tikv-jemallocator" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965fe0c26be5c56c94e38ba547249074803efd52adfb66de62107d95aab3eaca" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + [[package]] name = "time" version = "0.3.53"