diff --git a/orchestration/docker-compose.yml b/orchestration/docker-compose.yml index ae8637bb..6ed7f8e7 100644 --- a/orchestration/docker-compose.yml +++ b/orchestration/docker-compose.yml @@ -143,6 +143,24 @@ services: volumes: - "./serai/scripts:/scripts" + serai-fast-epoch: + restart: unless-stopped + # image: serai:dev + profiles: + - serai + build: + context: ../ + dockerfile: ./orchestration/serai/Dockerfile.fast-epoch + args: + TAG: serai + entrypoint: /scripts/entry-dev.sh + volumes: + - "./serai/scripts:/scripts" + hostname: serai + environment: + CHAIN: local + NAME: node + serai: <<: *serai_defaults hostname: serai diff --git a/orchestration/serai/Dockerfile.fast-epoch b/orchestration/serai/Dockerfile.fast-epoch new file mode 100644 index 00000000..783a4918 --- /dev/null +++ b/orchestration/serai/Dockerfile.fast-epoch @@ -0,0 +1,71 @@ +FROM debian:bookworm-slim as mimalloc + +RUN apt update && apt upgrade -y && apt install -y gcc g++ make cmake git +RUN git clone https://github.com/microsoft/mimalloc && \ + cd mimalloc && \ + git checkout 43ce4bd7fd34bcc730c1c7471c99995597415488 && \ + mkdir -p out/secure && \ + cd out/secure && \ + cmake -DMI_SECURE=ON ../.. && \ + make && \ + cp ./libmimalloc-secure.so ../../../libmimalloc.so +FROM rust:1.75-slim-bookworm as builder + +COPY --from=mimalloc libmimalloc.so /usr/lib +RUN echo "/usr/lib/libmimalloc.so" >> /etc/ld.so.preload + +RUN apt update && apt upgrade -y && apt autoremove -y && apt clean + +# Add dev dependencies +RUN apt install -y pkg-config clang + +# Dependencies for the Serai node +RUN apt install -y make protobuf-compiler + +# Add the wasm toolchain +RUN rustup target add wasm32-unknown-unknown + +# Add files for build +ADD common /serai/common +ADD crypto /serai/crypto +ADD coins /serai/coins +ADD message-queue /serai/message-queue +ADD processor /serai/processor +ADD coordinator /serai/coordinator +ADD substrate /serai/substrate +ADD mini /serai/mini +ADD tests /serai/tests +ADD patches /serai/patches +ADD Cargo.toml /serai +ADD Cargo.lock /serai +ADD AGPL-3.0 /serai + +WORKDIR /serai + +# Mount the caches and build +RUN --mount=type=cache,target=/root/.cargo \ + --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=/serai/target \ + mkdir /serai/bin && \ + cargo build --release --features fast-epoch -p serai-node && \ + mv /serai/target/release/serai-node /serai/bin +FROM debian:bookworm-slim as image + +COPY --from=mimalloc libmimalloc.so /usr/lib +RUN echo "/usr/lib/libmimalloc.so" >> /etc/ld.so.preload + +RUN apt update && apt upgrade -y && apt autoremove -y && apt clean +# Switch to a non-root user +RUN useradd --system --home /home/serai --shell /sbin/nologin serai +USER serai + +WORKDIR /home/serai + +# Copy the Serai binary and relevant license +COPY --from=builder --chown=serai /serai/bin/serai-node /bin/ +COPY --from=builder --chown=serai /serai/AGPL-3.0 . + +# Run node +EXPOSE 30333 9615 9933 9944 +CMD ["serai-node"] diff --git a/substrate/client/src/serai/validator_sets.rs b/substrate/client/src/serai/validator_sets.rs index 0f13d2a8..7459fe9e 100644 --- a/substrate/client/src/serai/validator_sets.rs +++ b/substrate/client/src/serai/validator_sets.rs @@ -35,23 +35,6 @@ impl<'a> SeraiValidatorSets<'a> { .await } - pub async fn extrinsic_failed(&self) -> Result, SeraiError> { - self - .0 - .events(|event| { - if let serai_abi::Event::System(event) = event { - if matches!(event, serai_abi::system::Event::ExtrinsicFailed { .. }) { - Some(event.clone()) - } else { - None - } - } else { - None - } - }) - .await - } - pub async fn participant_removed_events(&self) -> Result, SeraiError> { self .0 @@ -160,6 +143,22 @@ impl<'a> SeraiValidatorSets<'a> { .await } + pub async fn pending_deallocations( + &self, + network: NetworkId, + account: Public, + session: Session, + ) -> Result, SeraiError> { + self + .0 + .storage( + PALLET, + "PendingDeallocations", + (sp_core::hashing::blake2_128(&(network, account).encode()), (network, account, session)), + ) + .await + } + // TODO: Store these separately since we almost never need both at once? pub async fn keys(&self, set: ValidatorSet) -> Result, SeraiError> { self.0.storage(PALLET, "Keys", (sp_core::hashing::twox_64(&set.encode()), set)).await diff --git a/substrate/client/tests/common/mod.rs b/substrate/client/tests/common/mod.rs index caeed120..73fe52cb 100644 --- a/substrate/client/tests/common/mod.rs +++ b/substrate/client/tests/common/mod.rs @@ -13,7 +13,6 @@ macro_rules! serai_test { PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, TestBodySpecification, DockerTest, }; - use std::collections::HashMap; serai_docker_tests::build("serai".to_string()); @@ -29,7 +28,6 @@ macro_rules! serai_test { "--rpc-cors".to_string(), "all".to_string(), ]) - .replace_env(HashMap::from([("RUST_LOG=runtime".to_string(), "debug".to_string())])) .set_publish_all_ports(true) .set_handle(handle) .set_start_policy(StartPolicy::Strict) diff --git a/substrate/client/tests/validator_sets.rs b/substrate/client/tests/validator_sets.rs index c570a992..b366046e 100644 --- a/substrate/client/tests/validator_sets.rs +++ b/substrate/client/tests/validator_sets.rs @@ -82,12 +82,14 @@ async fn validator_set_rotation() { PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, TestBodySpecification, DockerTest, }; + use std::collections::HashMap; + + serai_docker_tests::build("serai-fast-epoch".to_string()); - serai_docker_tests::build("serai".to_string()); let handle = |name| format!("serai_client-serai_node-{name}"); let composition = |name| { TestBodySpecification::with_image( - Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never), + Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never), ) .replace_cmd(vec![ "serai-node".to_string(), @@ -98,6 +100,7 @@ async fn validator_set_rotation() { "local".to_string(), format!("--{name}"), ]) + .replace_env(HashMap::from([("RUST_LOG=runtime".to_string(), "debug".to_string())])) .set_publish_all_ports(true) .set_handle(handle(name)) .set_start_policy(StartPolicy::Strict) @@ -113,6 +116,7 @@ async fn validator_set_rotation() { test.provide_container(composition("bob")); test.provide_container(composition("charlie")); test.provide_container(composition("dave")); + test.provide_container(composition("eve")); test .run_async(|ops| async move { // Sleep until the Substrate RPC starts @@ -121,18 +125,10 @@ async fn validator_set_rotation() { let alice_rpc = format!("http://{}:{}", alice_rpc.0, alice_rpc.1); // Sleep for a minute - tokio::time::sleep(core::time::Duration::from_secs(60)).await; + tokio::time::sleep(core::time::Duration::from_secs(20)).await; let serai = Serai::new(alice_rpc.clone()).await.unwrap(); - // taken from testnet config - let pair1 = insecure_pair_from_name("Alice"); - let pair2 = insecure_pair_from_name("Bob"); - let pair3 = insecure_pair_from_name("Charlie"); - let pair4 = insecure_pair_from_name("Dave"); - let single_key_share = Amount(50_000 * 10_u64.pow(8)); - // Make sure the genesis is as expected - let network = NetworkId::Serai; assert_eq!( serai .as_of(serai.finalized_block_by_number(0).await.unwrap().unwrap().hash()) @@ -149,25 +145,77 @@ async fn validator_set_rotation() { .collect::>(), ); - // we start the chain with 4 default participants that has a single key share each - let mut participants = vec![pair1.public(), pair2.public(), pair3.public(), pair4.public()]; - participants.sort(); - verfiy_session_and_active_validators(&serai, network, 0, &participants).await; + // genesis accounts + let pair1 = insecure_pair_from_name("Alice"); + let pair2 = insecure_pair_from_name("Bob"); + let pair3 = insecure_pair_from_name("Charlie"); + let pair4 = insecure_pair_from_name("Dave"); + let pair5 = insecure_pair_from_name("Eve"); - // remove 1 participant - let hash = deallocate_stake(&serai, network, single_key_share, &pair2, 0).await; - participants.remove(1); + // amounts for single key share per network + let key_shares = HashMap::from([ + (NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))), + (NetworkId::Bitcoin, Amount(1_000_000 * 10_u64.pow(8))), + (NetworkId::Monero, Amount(100_000 * 10_u64.pow(8))), + (NetworkId::Ethereum, Amount(1_000_000 * 10_u64.pow(8))), + ]); - // TODO: check pending deallocations + // genesis participants per network + let default_participants = + vec![pair1.public(), pair2.public(), pair3.public(), pair4.public()]; + let mut participants = HashMap::from([ + (NetworkId::Serai, default_participants.clone()), + (NetworkId::Bitcoin, default_participants.clone()), + (NetworkId::Monero, default_participants.clone()), + (NetworkId::Ethereum, default_participants), + ]); - // verify for 2 epoch later(it takes 1 extra session for serai net to make the changes active) - // and since we removed a participant, we also need 1 extra session for cool down period. - let block_number = serai.block(hash).await.unwrap().unwrap().header.number; - let epoch_number = block_number / EPOCH_INTERVAL; - participants.sort(); - verfiy_session_and_active_validators(&serai, network, epoch_number + 3, &participants).await; + // test the set rotation + for (i, network) in NETWORKS.into_iter().enumerate() { + let participants = participants.get_mut(&network).unwrap(); - // TODO: test add valiators + // we start the chain with 4 default participants that has a single key share each + participants.sort(); + verfiy_session_and_active_validators(&serai, network, 0, &participants).await; + + // add 1 participant & verify + let hash = + allocate_stake(&serai, network, key_shares[&network], &pair5, i.try_into().unwrap()) + .await; + participants.push(pair5.public()); + participants.sort(); + verfiy_session_and_active_validators( + &serai, + network, + get_active_session(&serai, network, hash).await, + &participants, + ) + .await; + + // remove 1 participant & verify + let hash = + deallocate_stake(&serai, network, key_shares[&network], &pair2, i.try_into().unwrap()) + .await; + participants.swap_remove(participants.iter().position(|k| *k == pair2.public()).unwrap()); + let active_session = get_active_session(&serai, network, hash).await; + participants.sort(); + verfiy_session_and_active_validators(&serai, network, active_session, &participants).await; + + // check pending deallocations + let pending = serai + .as_of_latest_finalized_block() + .await + .unwrap() + .validator_sets() + .pending_deallocations( + network, + pair2.public(), + Session(u32::try_from(active_session + 1).unwrap()), + ) + .await + .unwrap(); + assert_eq!(pending, Some(key_shares[&network])); + } }) .await; } @@ -196,5 +244,29 @@ async fn verfiy_session_and_active_validators( validators.sort(); assert_eq!(validators, participants); + // make sure finalization continues as usual after the changes + tokio::time::timeout(tokio::time::Duration::from_secs(60), async move { + let mut finalized_block = serai.latest_finalized_block().await.unwrap().header.number; + while finalized_block <= epoch_block + 2 { + tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; + finalized_block = serai.latest_finalized_block().await.unwrap().header.number; + } + }) + .await + .unwrap(); + // TODO: verfiy key shares as well? } + +async fn get_active_session(serai: &Serai, network: NetworkId, hash: [u8; 32]) -> u64 { + let block_number = serai.block(hash).await.unwrap().unwrap().header.number; + let epoch = block_number / EPOCH_INTERVAL; + + // changes should be active in the next session + if network == NetworkId::Serai { + // it takes 1 extra session for serai net to make the changes active. + epoch + 2 + } else { + epoch + 1 + } +} diff --git a/substrate/node/Cargo.toml b/substrate/node/Cargo.toml index c34f7044..25faf3b7 100644 --- a/substrate/node/Cargo.toml +++ b/substrate/node/Cargo.toml @@ -66,6 +66,7 @@ substrate-build-script-utils = { git = "https://github.com/serai-dex/substrate" [features] default = [] +fast-epoch = ["serai-runtime/fast-epoch"] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index 9d31feb8..e4b7d639 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -124,6 +124,8 @@ std = [ "pallet-transaction-payment-rpc-runtime-api/std", ] +fast-epoch = [] + runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index 7f97ac29..0386fe60 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -301,8 +301,14 @@ pub type MaxAuthorities = ConstU32<{ validator_sets::primitives::MAX_KEY_SHARES_ pub type ReportLongevity = ::EpochDuration; impl babe::Config for Runtime { + #[cfg(feature = "fast-epoch")] #[allow(clippy::identity_op)] type EpochDuration = ConstU64<{ DAYS / (24 * 60 * 2) }>; // 30 seconds + + #[cfg(not(feature = "fast-epoch"))] + #[allow(clippy::identity_op)] + type EpochDuration = ConstU64<{ DAYS }>; + type ExpectedBlockTime = ConstU64<{ TARGET_BLOCK_TIME * 1000 }>; type EpochChangeTrigger = babe::ExternalTrigger; type DisabledValidators = ValidatorSets; diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 1e89064e..b72bc0a4 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -2,8 +2,6 @@ use core::marker::PhantomData; -use sp_runtime::print; - use scale::{Encode, Decode}; use scale_info::TypeInfo; @@ -570,7 +568,6 @@ pub mod pallet { account: T::AccountId, amount: Amount, ) -> Result { - print("in daellocate"); // Check it's safe to decrease this set's stake by this amount let new_total_staked = Self::total_allocated_stake(network) .unwrap() @@ -582,8 +579,6 @@ pub mod pallet { Err(Error::::DeallocationWouldRemoveEconomicSecurity)?; } - print("passed stake req"); - let old_allocation = Self::allocation((network, account)).ok_or(Error::::NonExistentValidator)?.0; let new_allocation = @@ -596,8 +591,6 @@ pub mod pallet { Err(Error::::DeallocationWouldRemoveParticipant)?; } - print("passed DeallocationWouldRemoveParticipant"); - let decreased_key_shares = (old_allocation / allocation_per_key_share) > (new_allocation / allocation_per_key_share); @@ -620,8 +613,6 @@ pub mod pallet { } } - print("passed bft"); - // If we're not in-set, allow immediate deallocation if !Self::in_set(network, account) { Self::deposit_event(Event::AllocationDecreased { @@ -630,12 +621,9 @@ pub mod pallet { amount, delayed_until: None, }); - print("returning ok true"); return Ok(true); } - print("passed in set"); - // Set it to PendingDeallocations, letting it be released upon a future session // This unwrap should be fine as this account is active, meaning a session has occurred let to_unlock_on = Self::session_to_unlock_on_for_current_set(network).unwrap(); @@ -647,8 +635,6 @@ pub mod pallet { Some(Amount(existing.0 + amount.0)), ); - print("passed PendingDeallocations"); - Self::deposit_event(Event::AllocationDecreased { validator: account, network, @@ -656,7 +642,6 @@ pub mod pallet { delayed_until: Some(to_unlock_on), }); - print("return ok false at the end"); Ok(false) } @@ -745,11 +730,6 @@ pub mod pallet { .expect("no Serai participants upon rotate_session"); let prior_serai_session = Self::session(NetworkId::Serai).unwrap(); - print("now session:"); - print(prior_serai_session.0); - print("now validators: "); - print(now_validators.len()); - // TODO: T::SessionHandler::on_before_session_ending() was here. // end the current serai session. Self::retire_set(ValidatorSet { network: NetworkId::Serai, session: prior_serai_session }); @@ -760,10 +740,6 @@ pub mod pallet { // Update Babe and Grandpa let session = prior_serai_session.0 + 1; let next_validators = Participants::::get(NetworkId::Serai).unwrap(); - print("next session:"); - print(session); - print("next validators: "); - print(next_validators.len()); Babe::::enact_epoch_change( WeakBoundedVec::force_from( now_validators.iter().copied().map(|(id, w)| (BabeAuthorityId::from(id), w)).collect(), diff --git a/tests/docker/src/lib.rs b/tests/docker/src/lib.rs index 2ae847e3..acfa43fb 100644 --- a/tests/docker/src/lib.rs +++ b/tests/docker/src/lib.rs @@ -74,6 +74,8 @@ pub fn build(name: String) { .join("processor") .join(name.split('-').next().unwrap()) .join("Dockerfile"); + } else if name == "serai-fast-epoch" { + dockerfile_path = dockerfile_path.join("serai").join("Dockerfile.fast-epoch"); } else { dockerfile_path = dockerfile_path.join(&name).join("Dockerfile"); } @@ -108,7 +110,7 @@ pub fn build(name: String) { meta(repo_path.join("message-queue")), meta(repo_path.join("coordinator")), ], - "runtime" | "serai" => vec![ + "runtime" | "serai" | "serai-fast-epoch" => vec![ meta(repo_path.join("common")), meta(repo_path.join("crypto")), meta(repo_path.join("substrate")),