Remove Tendermint for GRANDPA

Updates to polkadot-v0.9.40, with a variety of dependency updates accordingly.
Substrate thankfully now uses k256 0.13, pathing the way for #256. We couldn't
upgrade to polkadot-v0.9.40 without this due to polkadot-v0.9.40 having
fundamental changes to syncing. While we could've updated tendermint, it's not
worth the continued development effort given its inability to work with
multiple validator sets.

Purges sc-tendermint. Keeps tendermint-machine for #163.

Closes #137, #148, #157, #171. #96 and #99 should be re-scoped/clarified. #134
and #159 also should be clarified. #169 is also no longer a priority since
we're only considering temporal deployments of tendermint. #170 also isn't
since we're looking at effectively sharded validator sets, so there should
be no singular large set needing high performance.
This commit is contained in:
Luke Parker 2023-03-26 08:43:01 -04:00
parent 534e1bb11d
commit aea6ac104f
No known key found for this signature in database
42 changed files with 1089 additions and 2143 deletions

837
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,8 @@ members = [
"processor/messages",
"processor",
"tendermint",
"substrate/serai/primitives",
"substrate/serai/client",
@ -35,11 +37,6 @@ members = [
"substrate/validator-sets/primitives",
"substrate/validator-sets/pallet",
"substrate/tendermint/machine",
"substrate/tendermint/primitives",
"substrate/tendermint/client",
"substrate/tendermint/pallet",
"substrate/runtime",
"substrate/node",
]

View file

@ -28,6 +28,11 @@ wallet.
- `processor`: A generic chain processor to process data for Serai and process
events from Serai, executing transactions as expected and needed.
- `tendermint`: An abstracted implementation of the Tendermint BFT algorithm,
planned to be used in our own micro-blockchain for temporal data needed to
coordinate the Serai validators, yet irrelevant to the Serai network as a
whole.
- `substrate`: Substrate crates used to instantiate the Serai network.
- `deploy`: Scripts to deploy a Serai node/test environment.

View file

@ -56,10 +56,6 @@ exceptions = [
{ allow = ["AGPL-3.0"], name = "validator-sets-pallet" },
{ allow = ["AGPL-3.0"], name = "sp-tendermint" },
{ allow = ["AGPL-3.0"], name = "pallet-tendermint" },
{ allow = ["AGPL-3.0"], name = "sc-tendermint" },
{ allow = ["AGPL-3.0"], name = "serai-runtime" },
{ allow = ["AGPL-3.0"], name = "serai-node" },

View file

@ -6,7 +6,7 @@ ADD common /serai/common
ADD crypto /serai/crypto
ADD coins /serai/coins
ADD processor /serai/processor
ADD contracts /serai/contracts
ADD tendermint /serai/tendermint
ADD substrate /serai/substrate
ADD Cargo.toml /serai
ADD Cargo.lock /serai

View file

@ -2,7 +2,7 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
use scale::{Encode, Decode};
use scale::Encode;
use sp_runtime::RuntimeDebug;
@ -12,7 +12,7 @@ pub use in_instructions_primitives as primitives;
use primitives::{InInstruction, InInstructionWithBalance, SignedBatch};
#[derive(Clone, Copy, Encode, RuntimeDebug)]
#[cfg_attr(feature = "std", derive(Decode, thiserror::Error))]
#[cfg_attr(feature = "std", derive(scale::Decode, thiserror::Error))]
pub enum PalletError {
#[cfg_attr(feature = "std", error("batch for unrecognized network"))]
UnrecognizedNetwork,
@ -42,7 +42,6 @@ pub mod pallet {
}
#[pallet::pallet]
#[pallet::generate_store(pub(crate) trait Store)]
pub struct Pallet<T>(PhantomData<T>);
// Latest block number agreed upon for a coin

View file

@ -16,16 +16,19 @@ async-trait = "0.1"
clap = { version = "4", features = ["derive"] }
futures = "0.3"
jsonrpsee = { version = "0.16", features = ["server"] }
sp-core = { git = "https://github.com/serai-dex/substrate" }
sp-keyring = { git = "https://github.com/serai-dex/substrate" }
sp-inherents = { git = "https://github.com/serai-dex/substrate" }
sp-timestamp = { git = "https://github.com/serai-dex/substrate" }
sp-runtime = { git = "https://github.com/serai-dex/substrate" }
sp-blockchain = { git = "https://github.com/serai-dex/substrate" }
sp-api = { git = "https://github.com/serai-dex/substrate" }
sp-block-builder = { git = "https://github.com/serai-dex/substrate" }
sp-consensus = { git = "https://github.com/serai-dex/substrate" }
sp-consensus-babe = { git = "https://github.com/serai-dex/substrate" }
frame-benchmarking = { git = "https://github.com/serai-dex/substrate" }
frame-benchmarking-cli = { git = "https://github.com/serai-dex/substrate" }
@ -39,9 +42,18 @@ sc-executor = { git = "https://github.com/serai-dex/substrate" }
sc-service = { git = "https://github.com/serai-dex/substrate" }
sc-client-db = { git = "https://github.com/serai-dex/substrate" }
sc-client-api = { git = "https://github.com/serai-dex/substrate" }
sc-network-common = { git = "https://github.com/serai-dex/substrate" }
sc-network = { git = "https://github.com/serai-dex/substrate" }
sc-consensus = { git = "https://github.com/serai-dex/substrate" }
sc-consensus-babe = { git = "https://github.com/serai-dex/substrate" }
sc-consensus-babe-rpc = { git = "https://github.com/serai-dex/substrate" }
sc-consensus-grandpa = { git = "https://github.com/serai-dex/substrate" }
sc-consensus-grandpa-rpc = { git = "https://github.com/serai-dex/substrate" }
sc-authority-discovery = { git = "https://github.com/serai-dex/substrate" }
sc-telemetry = { git = "https://github.com/serai-dex/substrate" }
sc-cli = { git = "https://github.com/serai-dex/substrate" }
@ -50,8 +62,6 @@ sc-rpc-api = { git = "https://github.com/serai-dex/substrate" }
substrate-frame-rpc-system = { git = "https://github.com/serai-dex/substrate" }
pallet-transaction-payment-rpc = { git = "https://github.com/serai-dex/substrate" }
sc-tendermint = { path = "../tendermint/client" }
[build-dependencies]
substrate-build-script-utils = { git = "https://github.com/serai-dex/substrate.git" }

View file

@ -3,9 +3,9 @@ use sp_core::Pair as PairTrait;
use sc_service::ChainType;
use serai_runtime::{
primitives::*, tokens::primitives::ADDRESS as TOKENS_ADDRESS, tendermint::crypto::Public,
WASM_BINARY, opaque::SessionKeys, GenesisConfig, SystemConfig, BalancesConfig, AssetsConfig,
ValidatorSetsConfig, SessionConfig,
primitives::*, tokens::primitives::ADDRESS as TOKENS_ADDRESS, WASM_BINARY, opaque::SessionKeys,
BABE_GENESIS_EPOCH_CONFIG, GenesisConfig, SystemConfig, BalancesConfig, AssetsConfig,
ValidatorSetsConfig, SessionConfig, BabeConfig, GrandpaConfig, AuthorityDiscoveryConfig,
};
pub type ChainSpec = sc_service::GenericChainSpec<GenesisConfig>;
@ -21,7 +21,11 @@ fn testnet_genesis(
) -> GenesisConfig {
let session_key = |name| {
let key = account_from_name(name);
(key, key, SessionKeys { tendermint: Public::from(key) })
(
key,
key,
SessionKeys { babe: key.into(), grandpa: key.into(), authority_discovery: key.into() },
)
};
GenesisConfig {
@ -47,7 +51,6 @@ fn testnet_genesis(
accounts: vec![],
},
session: SessionConfig { keys: validators.iter().map(|name| session_key(*name)).collect() },
validator_sets: ValidatorSetsConfig {
bond: Amount(1_000_000 * 10_u64.pow(8)),
networks: vec![
@ -57,6 +60,11 @@ fn testnet_genesis(
],
participants: validators.iter().map(|name| account_from_name(name)).collect(),
},
session: SessionConfig { keys: validators.iter().map(|name| session_key(*name)).collect() },
babe: BabeConfig { authorities: vec![], epoch_config: Some(BABE_GENESIS_EPOCH_CONFIG) },
grandpa: GrandpaConfig { authorities: vec![] },
authority_discovery: AuthorityDiscoveryConfig { keys: vec![] },
}
}

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
use serai_runtime::Block;
use sc_service::{PruningMode, PartialComponents};
@ -9,7 +11,7 @@ use crate::{
chain_spec,
cli::{Cli, Subcommand},
command_helper::{RemarkBuilder, inherent_benchmark_data},
service,
service::{self, FullClient},
};
impl SubstrateCli for Cli {
@ -62,23 +64,23 @@ pub fn run() -> sc_cli::Result<()> {
Some(Subcommand::CheckBlock(cmd)) => cli.create_runner(cmd)?.async_run(|config| {
let PartialComponents { client, task_manager, import_queue, .. } =
service::new_partial(&config)?.1;
service::new_partial(&config)?;
Ok((cmd.run(client, import_queue), task_manager))
}),
Some(Subcommand::ExportBlocks(cmd)) => cli.create_runner(cmd)?.async_run(|config| {
let PartialComponents { client, task_manager, .. } = service::new_partial(&config)?.1;
let PartialComponents { client, task_manager, .. } = service::new_partial(&config)?;
Ok((cmd.run(client, config.database), task_manager))
}),
Some(Subcommand::ExportState(cmd)) => cli.create_runner(cmd)?.async_run(|config| {
let PartialComponents { client, task_manager, .. } = service::new_partial(&config)?.1;
let PartialComponents { client, task_manager, .. } = service::new_partial(&config)?;
Ok((cmd.run(client, config.chain_spec), task_manager))
}),
Some(Subcommand::ImportBlocks(cmd)) => cli.create_runner(cmd)?.async_run(|config| {
let PartialComponents { client, task_manager, import_queue, .. } =
service::new_partial(&config)?.1;
service::new_partial(&config)?;
Ok((cmd.run(client, import_queue), task_manager))
}),
@ -87,15 +89,19 @@ pub fn run() -> sc_cli::Result<()> {
}
Some(Subcommand::Revert(cmd)) => cli.create_runner(cmd)?.async_run(|config| {
let PartialComponents { client, task_manager, backend, .. } =
service::new_partial(&config)?.1;
Ok((cmd.run(client, backend, None), task_manager))
let PartialComponents { client, task_manager, backend, .. } = service::new_partial(&config)?;
let aux_revert = Box::new(|client: Arc<FullClient>, backend, blocks| {
sc_consensus_babe::revert(client.clone(), backend, blocks)?;
sc_consensus_grandpa::revert(client, blocks)?;
Ok(())
});
Ok((cmd.run(client, backend, Some(aux_revert)), task_manager))
}),
Some(Subcommand::Benchmark(cmd)) => cli.create_runner(cmd)?.sync_run(|config| match cmd {
BenchmarkCmd::Pallet(cmd) => cmd.run::<Block, service::ExecutorDispatch>(config),
BenchmarkCmd::Block(cmd) => cmd.run(service::new_partial(&config)?.1.client),
BenchmarkCmd::Block(cmd) => cmd.run(service::new_partial(&config)?.client),
#[cfg(not(feature = "runtime-benchmarks"))]
BenchmarkCmd::Storage(_) => {
@ -104,12 +110,12 @@ pub fn run() -> sc_cli::Result<()> {
#[cfg(feature = "runtime-benchmarks")]
BenchmarkCmd::Storage(cmd) => {
let PartialComponents { client, backend, .. } = service::new_partial(&config)?.1;
let PartialComponents { client, backend, .. } = service::new_partial(&config)?;
cmd.run(config, client, backend.expose_db(), backend.expose_storage())
}
BenchmarkCmd::Overhead(cmd) => {
let client = service::new_partial(&config)?.1.client;
let client = service::new_partial(&config)?.client;
cmd.run(
config,
client.clone(),
@ -120,7 +126,7 @@ pub fn run() -> sc_cli::Result<()> {
}
BenchmarkCmd::Extrinsic(cmd) => {
let client = service::new_partial(&config)?.1.client;
let client = service::new_partial(&config)?.client;
cmd.run(
client.clone(),
inherent_benchmark_data()?,

View file

@ -1,42 +1,24 @@
use std::{
error::Error,
boxed::Box,
sync::Arc,
time::{UNIX_EPOCH, SystemTime, Duration},
str::FromStr,
};
use std::{boxed::Box, sync::Arc};
use sp_runtime::traits::{Block as BlockTrait};
use sp_inherents::CreateInherentDataProviders;
use sp_consensus::DisableProofRecording;
use sp_api::ProvideRuntimeApi;
use futures::stream::StreamExt;
use sp_timestamp::InherentDataProvider as TimestampInherent;
use sp_consensus_babe::{SlotDuration, inherents::InherentDataProvider as BabeInherent};
use sc_executor::{NativeVersion, NativeExecutionDispatch, NativeElseWasmExecutor};
use sc_transaction_pool::FullPool;
use sc_network::NetworkService;
use sc_network_common::sync::warp::WarpSyncParams;
use sc_network::{Event, NetworkEventStream};
use sc_service::{error::Error as ServiceError, Configuration, TaskManager, TFullClient};
use sc_client_api::BlockBackend;
use sc_telemetry::{Telemetry, TelemetryWorker};
pub(crate) use sc_tendermint::{
TendermintClientMinimal, TendermintValidator, TendermintImport, TendermintAuthority,
TendermintSelectChain, import_queue,
};
use serai_runtime::{self as runtime, BLOCK_SIZE, TARGET_BLOCK_TIME, opaque::Block, RuntimeApi};
use serai_runtime::{self as runtime, opaque::Block, RuntimeApi};
type FullBackend = sc_service::TFullBackend<Block>;
pub type FullClient = TFullClient<Block, RuntimeApi, NativeElseWasmExecutor<ExecutorDispatch>>;
type PartialComponents = sc_service::PartialComponents<
FullClient,
FullBackend,
TendermintSelectChain<Block, FullBackend>,
sc_consensus::DefaultImportQueue<Block, FullClient>,
sc_transaction_pool::FullPool<Block, FullClient>,
Option<Telemetry>,
>;
use sc_consensus_babe::{self, SlotProportion};
use sc_consensus_grandpa as grandpa;
pub struct ExecutorDispatch;
impl NativeExecutionDispatch for ExecutorDispatch {
@ -54,57 +36,36 @@ impl NativeExecutionDispatch for ExecutorDispatch {
}
}
pub struct Cidp;
#[async_trait::async_trait]
impl CreateInherentDataProviders<Block, ()> for Cidp {
type InherentDataProviders = ();
async fn create_inherent_data_providers(
&self,
_: <Block as BlockTrait>::Hash,
_: (),
) -> Result<Self::InherentDataProviders, Box<dyn Send + Sync + Error>> {
Ok(())
}
type FullBackend = sc_service::TFullBackend<Block>;
pub type FullClient = TFullClient<Block, RuntimeApi, NativeElseWasmExecutor<ExecutorDispatch>>;
type SelectChain = sc_consensus::LongestChain<FullBackend, Block>;
type GrandpaBlockImport = grandpa::GrandpaBlockImport<FullBackend, Block, FullClient, SelectChain>;
type BabeBlockImport = sc_consensus_babe::BabeBlockImport<Block, FullClient, GrandpaBlockImport>;
type PartialComponents = sc_service::PartialComponents<
FullClient,
FullBackend,
SelectChain,
sc_consensus::DefaultImportQueue<Block, FullClient>,
sc_transaction_pool::FullPool<Block, FullClient>,
(
BabeBlockImport,
sc_consensus_babe::BabeLink<Block>,
grandpa::LinkHalf<Block, FullClient, SelectChain>,
grandpa::SharedVoterState,
Option<Telemetry>,
),
>;
fn create_inherent_data_providers(
slot_duration: SlotDuration,
) -> (BabeInherent, TimestampInherent) {
let timestamp = TimestampInherent::from_system_time();
(BabeInherent::from_timestamp_and_slot_duration(*timestamp, slot_duration), timestamp)
}
pub struct TendermintValidatorFirm;
impl TendermintClientMinimal for TendermintValidatorFirm {
// TODO: This is passed directly to propose, which warns not to use the hard limit as finalize
// may grow the block. We don't use storage proofs and use the Executive finalize_block. Is that
// guaranteed not to grow the block?
const PROPOSED_BLOCK_SIZE_LIMIT: usize = { BLOCK_SIZE as usize };
// 3 seconds
const BLOCK_PROCESSING_TIME_IN_SECONDS: u32 = { (TARGET_BLOCK_TIME / 2) as u32 };
// 1 second
const LATENCY_TIME_IN_SECONDS: u32 = { (TARGET_BLOCK_TIME / 2 / 3) as u32 };
type Block = Block;
type Backend = sc_client_db::Backend<Block>;
type Api = <FullClient as ProvideRuntimeApi<Block>>::Api;
type Client = FullClient;
}
impl TendermintValidator for TendermintValidatorFirm {
type CIDP = Cidp;
type Environment = sc_basic_authorship::ProposerFactory<
FullPool<Block, FullClient>,
Self::Backend,
Self::Client,
DisableProofRecording,
>;
type Network = Arc<NetworkService<Block, <Block as BlockTrait>::Hash>>;
}
pub fn new_partial(
config: &Configuration,
) -> Result<(TendermintImport<TendermintValidatorFirm>, PartialComponents), ServiceError> {
debug_assert_eq!(TARGET_BLOCK_TIME, 6);
if config.keystore_remote.is_some() {
return Err(ServiceError::Other("Remote Keystores are not supported".to_string()));
}
pub fn new_partial(config: &Configuration) -> Result<PartialComponents, ServiceError> {
let telemetry = config
.telemetry_endpoints
.clone()
@ -136,6 +97,8 @@ pub fn new_partial(
telemetry
});
let select_chain = sc_consensus::LongestChain::new(backend.clone());
let transaction_pool = sc_transaction_pool::BasicPool::new_full(
config.transaction_pool.clone(),
config.role.is_authority().into(),
@ -144,55 +107,69 @@ pub fn new_partial(
client.clone(),
);
let (authority, import_queue) = import_queue(
&task_manager.spawn_essential_handle(),
let (grandpa_block_import, grandpa_link) = grandpa::block_import(
client.clone(),
&client,
select_chain.clone(),
telemetry.as_ref().map(Telemetry::handle),
)?;
let justification_import = grandpa_block_import.clone();
let (block_import, babe_link) = sc_consensus_babe::block_import(
sc_consensus_babe::configuration(&*client)?,
grandpa_block_import,
client.clone(),
)?;
let slot_duration = babe_link.config().slot_duration();
let import_queue = sc_consensus_babe::import_queue(
babe_link.clone(),
block_import.clone(),
Some(Box::new(justification_import)),
client.clone(),
select_chain.clone(),
move |_, _| async move { Ok(create_inherent_data_providers(slot_duration)) },
&task_manager.spawn_essential_handle(),
config.prometheus_registry(),
);
telemetry.as_ref().map(Telemetry::handle),
)?;
let select_chain = TendermintSelectChain::new(backend.clone());
Ok((
authority,
sc_service::PartialComponents {
client,
backend,
task_manager,
import_queue,
keystore_container,
select_chain,
transaction_pool,
other: telemetry,
},
))
Ok(sc_service::PartialComponents {
client,
backend,
task_manager,
keystore_container,
select_chain,
import_queue,
transaction_pool,
other: (block_import, babe_link, grandpa_link, grandpa::SharedVoterState::empty(), telemetry),
})
}
pub async fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceError> {
let (
authority,
sc_service::PartialComponents {
client,
backend,
mut task_manager,
import_queue,
keystore_container,
select_chain: _,
other: mut telemetry,
transaction_pool,
},
) = new_partial(&config)?;
let sc_service::PartialComponents {
client,
backend,
mut task_manager,
import_queue,
keystore_container,
select_chain,
transaction_pool,
other: (block_import, babe_link, grandpa_link, shared_voter_state, mut telemetry),
} = new_partial(&config)?;
let is_authority = config.role.is_authority();
let genesis = client.block_hash(0).unwrap().unwrap();
let tendermint_protocol = sc_tendermint::protocol_name(genesis, config.chain_spec.fork_id());
if is_authority {
config
.network
.extra_sets
.push(sc_tendermint::set_config(tendermint_protocol.clone(), BLOCK_SIZE.into()));
}
let publish_non_global_ips = config.network.allow_non_globals_in_dht;
let grandpa_protocol_name =
grandpa::protocol_standard_name(&client.block_hash(0).unwrap().unwrap(), &config.chain_spec);
let (network, system_rpc_tx, tx_handler_controller, network_starter) =
config.network.extra_sets.push(grandpa::grandpa_peers_set_config(grandpa_protocol_name.clone()));
let warp_sync = Arc::new(grandpa::warp_proof::NetworkProvider::new(
backend.clone(),
grandpa_link.shared_authority_set().clone(),
vec![],
));
let (network, system_rpc_tx, tx_handler_controller, network_starter, sync_service) =
sc_service::build_network(sc_service::BuildNetworkParams {
config: &config,
client: client.clone(),
@ -200,7 +177,7 @@ pub async fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceE
spawn_handle: task_manager.spawn_handle(),
import_queue,
block_announce_validator_builder: None,
warp_sync_params: None,
warp_sync_params: Some(WarpSyncParams::WithProvider(warp_sync)),
})?;
if config.offchain_worker.enabled {
@ -212,7 +189,7 @@ pub async fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceE
);
}
let rpc_extensions_builder = {
let rpc_builder = {
let client = client.clone();
let pool = transaction_pool.clone();
@ -226,48 +203,113 @@ pub async fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceE
})
};
let genesis_time = if config.chain_spec.id() != "devnet" {
UNIX_EPOCH + Duration::from_secs(u64::from_str(&std::env::var("GENESIS").unwrap()).unwrap())
} else {
SystemTime::now()
};
let enable_grandpa = !config.disable_grandpa;
let role = config.role.clone();
let force_authoring = config.force_authoring;
let name = config.network.node_name.clone();
let prometheus_registry = config.prometheus_registry().cloned();
let keystore = keystore_container.keystore();
let registry = config.prometheus_registry().cloned();
sc_service::spawn_tasks(sc_service::SpawnTasksParams {
network: network.clone(),
client: client.clone(),
keystore: keystore_container.sync_keystore(),
task_manager: &mut task_manager,
transaction_pool: transaction_pool.clone(),
rpc_builder: rpc_extensions_builder,
config,
backend,
client: client.clone(),
keystore: keystore.clone(),
network: network.clone(),
rpc_builder,
transaction_pool: transaction_pool.clone(),
task_manager: &mut task_manager,
system_rpc_tx,
tx_handler_controller,
config,
sync_service: sync_service.clone(),
telemetry: telemetry.as_mut(),
})?;
if is_authority {
task_manager.spawn_essential_handle().spawn(
"tendermint",
None,
TendermintAuthority::new(
genesis_time,
tendermint_protocol,
authority,
keystore_container.keystore(),
Cidp,
task_manager.spawn_essential_handle(),
sc_basic_authorship::ProposerFactory::new(
task_manager.spawn_handle(),
client,
transaction_pool,
registry.as_ref(),
telemetry.map(|telemtry| telemtry.handle()),
),
network,
None,
if let sc_service::config::Role::Authority { .. } = &role {
let slot_duration = babe_link.config().slot_duration();
let babe_config = sc_consensus_babe::BabeParams {
keystore: keystore.clone(),
client: client.clone(),
select_chain,
env: sc_basic_authorship::ProposerFactory::new(
task_manager.spawn_handle(),
client.clone(),
transaction_pool,
prometheus_registry.as_ref(),
telemetry.as_ref().map(Telemetry::handle),
),
block_import,
sync_oracle: sync_service.clone(),
justification_sync_link: sync_service.clone(),
create_inherent_data_providers: move |_, _| async move {
Ok(create_inherent_data_providers(slot_duration))
},
force_authoring,
backoff_authoring_blocks: None::<()>,
babe_link,
block_proposal_slot_portion: SlotProportion::new(0.5),
max_block_proposal_slot_portion: None,
telemetry: telemetry.as_ref().map(Telemetry::handle),
};
task_manager.spawn_essential_handle().spawn_blocking(
"babe-proposer",
Some("block-authoring"),
sc_consensus_babe::start_babe(babe_config)?,
);
}
if role.is_authority() {
task_manager.spawn_handle().spawn(
"authority-discovery-worker",
Some("networking"),
sc_authority_discovery::new_worker_and_service_with_config(
#[allow(clippy::field_reassign_with_default)]
{
let mut worker = sc_authority_discovery::WorkerConfig::default();
worker.publish_non_global_ips = publish_non_global_ips;
worker
},
client,
network.clone(),
Box::pin(network.event_stream("authority-discovery").filter_map(|e| async move {
match e {
Event::Dht(e) => Some(e),
_ => None,
}
})),
sc_authority_discovery::Role::PublishAndDiscover(keystore.clone()),
prometheus_registry.clone(),
)
.0
.run(),
);
}
if enable_grandpa {
task_manager.spawn_essential_handle().spawn_blocking(
"grandpa-voter",
None,
grandpa::run_grandpa_voter(grandpa::GrandpaParams {
config: grandpa::Config {
gossip_duration: std::time::Duration::from_millis(333),
justification_period: 512,
name: Some(name),
observer_enabled: false,
keystore: if role.is_authority() { Some(keystore) } else { None },
local_role: role,
telemetry: telemetry.as_ref().map(Telemetry::handle),
protocol_name: grandpa_protocol_name,
},
link: grandpa_link,
network,
sync: Arc::new(sync_service),
telemetry: telemetry.as_ref().map(Telemetry::handle),
voting_rule: grandpa::VotingRulesBuilder::default().build(),
prometheus_registry,
shared_voter_state,
})?,
);
}

View file

@ -19,17 +19,23 @@ scale-info = { version = "2", default-features = false, features = ["derive"] }
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-offchain = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-version = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-inherents = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-offchain = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-session = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-consensus-babe = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-consensus-grandpa = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-authority-discovery = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-transaction-pool = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-block-builder = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-api = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-tendermint = { path = "../tendermint/primitives", default-features = false }
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-executive = { git = "https://github.com/serai-dex/substrate", default-features = false }
@ -37,6 +43,8 @@ frame-benchmarking = { git = "https://github.com/serai-dex/substrate", default-f
serai-primitives = { path = "../serai/primitives", default-features = false }
pallet-timestamp = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-balances = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-assets = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", default-features = false }
@ -46,7 +54,10 @@ in-instructions-pallet = { path = "../in-instructions/pallet", default-features
validator-sets-pallet = { path = "../validator-sets/pallet", default-features = false }
pallet-session = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-tendermint = { path = "../tendermint/pallet", default-features = false }
pallet-babe = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-grandpa = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-authority-discovery = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-system-rpc-runtime-api = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-transaction-payment-rpc-runtime-api = { git = "https://github.com/serai-dex/substrate", default-features = false }
@ -61,23 +72,31 @@ std = [
"sp-core/std",
"sp-std/std",
"sp-offchain/std",
"sp-version/std",
"sp-inherents/std",
"sp-offchain/std",
"sp-session/std",
"sp-consensus-babe/std",
"sp-consensus-grandpa/std",
"sp-authority-discovery/std",
"sp-transaction-pool/std",
"sp-block-builder/std",
"sp-runtime/std",
"sp-api/std",
"sp-tendermint/std",
"frame-system/std",
"frame-support/std",
"frame-executive/std",
"serai-primitives/std",
"pallet-timestamp/std",
"pallet-balances/std",
"pallet-transaction-payment/std",
@ -87,7 +106,10 @@ std = [
"validator-sets-pallet/std",
"pallet-session/std",
"pallet-tendermint/std",
"pallet-babe/std",
"pallet-grandpa/std",
"pallet-authority-discovery/std",
"frame-system-rpc-runtime-api/std",
"pallet-transaction-payment-rpc-runtime-api/std",
@ -102,10 +124,13 @@ runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"frame-benchmarking/runtime-benchmarks",
"pallet-timestamp/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"pallet-assets/runtime-benchmarks",
"pallet-tendermint/runtime-benchmarks",
"pallet-babe/runtime-benchmarks",
"pallet-grandpa/runtime-benchmarks",
]
default = ["std"]

View file

@ -12,6 +12,8 @@ pub use serai_primitives as primitives;
pub use frame_system as system;
pub use frame_support as support;
pub use pallet_timestamp as timestamp;
pub use pallet_balances as balances;
pub use pallet_transaction_payment as transaction_payment;
@ -22,7 +24,10 @@ pub use in_instructions_pallet as in_instructions;
pub use validator_sets_pallet as validator_sets;
pub use pallet_session as session;
pub use pallet_tendermint as tendermint;
pub use pallet_babe as babe;
pub use pallet_grandpa as grandpa;
pub use pallet_authority_discovery as authority_discovery;
// Actually used by the runtime
use sp_core::OpaqueMetadata;
@ -52,7 +57,9 @@ use support::{
use transaction_payment::CurrencyAdapter;
use session::PeriodicSessions;
use babe::AuthorityId as BabeId;
use grandpa::AuthorityId as GrandpaId;
use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId;
/// An index to a block.
pub type BlockNumber = u64;
@ -74,7 +81,9 @@ pub mod opaque {
impl_opaque_keys! {
pub struct SessionKeys {
pub tendermint: Tendermint,
pub babe: Babe,
pub grandpa: Grandpa,
pub authority_discovery: AuthorityDiscovery,
}
}
}
@ -94,6 +103,11 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
state_version: 1,
};
#[cfg(feature = "std")]
pub fn native_version() -> NativeVersion {
NativeVersion { runtime_version: VERSION, can_author_with: Default::default() }
}
// 1 MB
pub const BLOCK_SIZE: u32 = 1024 * 1024;
// 6 seconds
@ -104,10 +118,13 @@ pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME;
pub const HOURS: BlockNumber = MINUTES * 60;
pub const DAYS: BlockNumber = HOURS * 24;
#[cfg(feature = "std")]
pub fn native_version() -> NativeVersion {
NativeVersion { runtime_version: VERSION, can_author_with: Default::default() }
}
pub const PRIMARY_PROBABILITY: (u64, u64) = (1, 4);
pub const BABE_GENESIS_EPOCH_CONFIG: sp_consensus_babe::BabeEpochConfiguration =
sp_consensus_babe::BabeEpochConfiguration {
c: PRIMARY_PROBABILITY,
allowed_slots: sp_consensus_babe::AllowedSlots::PrimaryAndSecondaryPlainSlots,
};
const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75);
@ -122,14 +139,20 @@ parameter_types! {
system::limits::BlockLength::max_with_normal_ratio(BLOCK_SIZE, NORMAL_DISPATCH_RATIO);
pub BlockWeights: system::limits::BlockWeights =
system::limits::BlockWeights::with_sensible_defaults(
Weight::from_ref_time(2u64 * WEIGHT_REF_TIME_PER_SECOND).set_proof_size(u64::MAX),
Weight::from_parts(2u64 * WEIGHT_REF_TIME_PER_SECOND, u64::MAX),
NORMAL_DISPATCH_RATIO,
);
pub const MaxAuthorities: u32 = 100;
}
pub struct CallFilter;
impl Contains<RuntimeCall> for CallFilter {
fn contains(call: &RuntimeCall) -> bool {
if let RuntimeCall::Timestamp(call) = call {
return matches!(call, timestamp::Call::set { .. });
}
if let RuntimeCall::Balances(call) = call {
return matches!(call, balances::Call::transfer { .. } | balances::Call::transfer_all { .. });
}
@ -188,14 +211,29 @@ impl system::Config for Runtime {
type MaxConsumers = support::traits::ConstU32<16>;
}
impl timestamp::Config for Runtime {
type Moment = u64;
type OnTimestampSet = Babe;
type MinimumPeriod = ConstU64<{ (TARGET_BLOCK_TIME * 1000) / 2 }>;
type WeightInfo = ();
}
impl balances::Config for Runtime {
type MaxLocks = ConstU32<50>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = SubstrateAmount;
type RuntimeEvent = RuntimeEvent;
type Balance = SubstrateAmount;
type ReserveIdentifier = ();
type HoldIdentifier = ();
type FreezeIdentifier = ();
type MaxLocks = ();
type MaxReserves = ();
type MaxHolds = ();
type MaxFreezes = ();
type DustRemoval = ();
type ExistentialDeposit = ConstU64<500>;
type ExistentialDeposit = ConstU64<1>;
type AccountStore = System;
type WeightInfo = balances::weights::SubstrateWeight<Runtime>;
}
@ -248,8 +286,9 @@ impl in_instructions::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
const SESSION_LENGTH: BlockNumber = 5 * DAYS;
type Sessions = PeriodicSessions<ConstU64<{ SESSION_LENGTH }>, ConstU64<{ SESSION_LENGTH }>>;
impl validator_sets::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
pub struct IdentityValidatorIdOf;
impl Convert<PublicKey, Option<PublicKey>> for IdentityValidatorIdOf {
@ -258,23 +297,49 @@ impl Convert<PublicKey, Option<PublicKey>> for IdentityValidatorIdOf {
}
}
impl validator_sets::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
impl session::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type ValidatorId = PublicKey;
type ValidatorIdOf = IdentityValidatorIdOf;
type ShouldEndSession = Sessions;
type NextSessionRotation = Sessions;
type SessionManager = ();
type ShouldEndSession = Babe;
type NextSessionRotation = Babe;
type SessionManager = (); // TODO?
type SessionHandler = <SessionKeys as OpaqueKeys>::KeyTypeIdProviders;
type Keys = SessionKeys;
type WeightInfo = session::weights::SubstrateWeight<Runtime>;
}
impl tendermint::Config for Runtime {}
impl babe::Config for Runtime {
#[allow(clippy::identity_op)]
type EpochDuration = ConstU64<{ 1 * DAYS }>;
type ExpectedBlockTime = ConstU64<{ TARGET_BLOCK_TIME * 1000 }>;
type EpochChangeTrigger = pallet_babe::ExternalTrigger;
type DisabledValidators = Session;
type WeightInfo = ();
type MaxAuthorities = MaxAuthorities;
// TODO: Handle equivocation reports
type KeyOwnerProof = sp_core::Void;
type EquivocationReportSystem = ();
}
impl grandpa::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type MaxAuthorities = MaxAuthorities;
// TODO: Handle equivocation reports
type MaxSetIdSessionEntries = ConstU64<0>;
type KeyOwnerProof = sp_core::Void;
type EquivocationReportSystem = ();
}
impl authority_discovery::Config for Runtime {
type MaxAuthorities = MaxAuthorities;
}
pub type Header = generic::Header<BlockNumber, BlakeTwo256>;
pub type Block = generic::Block<Header, UncheckedExtrinsic>;
@ -307,6 +372,8 @@ construct_runtime!(
{
System: system,
Timestamp: timestamp,
Balances: balances,
TransactionPayment: transaction_payment,
@ -317,7 +384,10 @@ construct_runtime!(
ValidatorSets: validator_sets,
Session: session,
Tendermint: tendermint,
Babe: babe,
Grandpa: grandpa,
AuthorityDiscovery: authority_discovery,
}
);
@ -329,8 +399,15 @@ extern crate frame_benchmarking;
mod benches {
define_benchmarks!(
[frame_benchmarking, BaselineBench::<Runtime>]
[system, SystemBench::<Runtime>]
[pallet_timestamp, Timestamp]
[balances, Balances]
[babe, Babe]
[grandpa, Grandpa]
);
}
@ -353,6 +430,14 @@ sp_api::impl_runtime_apis! {
fn metadata() -> OpaqueMetadata {
OpaqueMetadata::new(Runtime::metadata().into())
}
fn metadata_at_version(version: u32) -> Option<OpaqueMetadata> {
Runtime::metadata_at_version(version)
}
fn metadata_versions() -> sp_std::vec::Vec<u32> {
Runtime::metadata_versions()
}
}
impl sp_block_builder::BlockBuilder<Block> for Runtime {
@ -404,13 +489,69 @@ sp_api::impl_runtime_apis! {
}
}
impl sp_tendermint::TendermintApi<Block> for Runtime {
fn current_session() -> u32 {
Tendermint::session()
impl sp_consensus_babe::BabeApi<Block> for Runtime {
fn configuration() -> sp_consensus_babe::BabeConfiguration {
use support::traits::Get;
let epoch_config = Babe::epoch_config().unwrap_or(BABE_GENESIS_EPOCH_CONFIG);
sp_consensus_babe::BabeConfiguration {
slot_duration: Babe::slot_duration(),
epoch_length: <Runtime as babe::Config>::EpochDuration::get(),
c: epoch_config.c,
authorities: Babe::authorities().to_vec(),
randomness: Babe::randomness(),
allowed_slots: epoch_config.allowed_slots,
}
}
fn validators() -> Vec<PublicKey> {
Session::validators().drain(..).map(Into::into).collect()
fn current_epoch_start() -> sp_consensus_babe::Slot {
Babe::current_epoch_start()
}
fn current_epoch() -> sp_consensus_babe::Epoch {
Babe::current_epoch()
}
fn next_epoch() -> sp_consensus_babe::Epoch {
Babe::next_epoch()
}
fn generate_key_ownership_proof(
_: sp_consensus_babe::Slot,
_: BabeId,
) -> Option<sp_consensus_babe::OpaqueKeyOwnershipProof> {
None
}
fn submit_report_equivocation_unsigned_extrinsic(
_: sp_consensus_babe::EquivocationProof<<Block as BlockT>::Header>,
_: sp_consensus_babe::OpaqueKeyOwnershipProof,
) -> Option<()> {
None
}
}
impl sp_consensus_grandpa::GrandpaApi<Block> for Runtime {
fn grandpa_authorities() -> sp_consensus_grandpa::AuthorityList {
Grandpa::grandpa_authorities()
}
fn current_set_id() -> sp_consensus_grandpa::SetId {
Grandpa::current_set_id()
}
fn submit_report_equivocation_unsigned_extrinsic(
_: sp_consensus_grandpa::EquivocationProof<<Block as BlockT>::Hash, u64>,
_: sp_consensus_grandpa::OpaqueKeyOwnershipProof,
) -> Option<()> {
None
}
fn generate_key_ownership_proof(
_set_id: sp_consensus_grandpa::SetId,
_authority_id: GrandpaId,
) -> Option<sp_consensus_grandpa::OpaqueKeyOwnershipProof> {
None
}
}
@ -446,4 +587,10 @@ sp_api::impl_runtime_apis! {
TransactionPayment::length_to_fee(length)
}
}
impl sp_authority_discovery::AuthorityDiscoveryApi<Block> for Runtime {
fn authorities() -> Vec<AuthorityDiscoveryId> {
AuthorityDiscovery::authorities()
}
}
}

View file

@ -12,6 +12,7 @@ use subxt::{
error::Error as SubxtError,
utils::Encoded,
config::{
Header as HeaderTrait,
substrate::{BlakeTwo256, SubstrateHeader},
extrinsic_params::{BaseExtrinsicParams, BaseExtrinsicParamsBuilder},
},
@ -66,6 +67,8 @@ pub enum SeraiError {
RpcError(SubxtError),
#[error("serai-client library was intended for a different runtime version")]
InvalidRuntime,
#[error("node is faulty")]
InvalidNode,
}
#[derive(Clone)]
@ -123,6 +126,44 @@ impl Serai {
Ok(self.0.rpc().finalized_head().await.map_err(SeraiError::RpcError)?.into())
}
// There is no provided method for this
// TODO: Add one to Serai
pub async fn is_finalized(&self, header: &Header) -> Result<Option<bool>, SeraiError> {
// Get the latest finalized block
let finalized = self.get_latest_block_hash().await?.into();
// If the latest finalized block is this block, return true
if finalized == header.hash() {
return Ok(Some(true));
}
let Some(finalized) =
self.0.rpc().header(Some(finalized)).await.map_err(SeraiError::RpcError)? else {
return Ok(None);
};
// If the finalized block has a lower number, this block can't be finalized
if finalized.number() < header.number() {
return Ok(Some(false));
}
// This block, if finalized, comes before the finalized block
// If we request the hash of this block's number, Substrate will return the hash on the main
// chain
// If that hash is this hash, this block is finalized
let Some(hash) =
self
.0
.rpc()
.block_hash(Some(header.number().into()))
.await
.map_err(SeraiError::RpcError)? else {
// This is an error since there is a block at this index
return Err(SeraiError::InvalidNode);
};
Ok(Some(header.hash() == hash))
}
pub async fn get_block(&self, hash: [u8; 32]) -> Result<Option<Block>, SeraiError> {
let Some(res) =
self.0.rpc().block(Some(hash.into())).await.map_err(SeraiError::RpcError)? else {
@ -130,8 +171,7 @@ impl Serai {
};
// Only return finalized blocks
let Some(justifications) = res.justifications.as_ref() else { return Ok(None); };
if justifications.is_empty() {
if self.is_finalized(&res.block.header).await? != Some(true) {
return Ok(None);
}
@ -140,9 +180,9 @@ impl Serai {
// Ideally, this would be get_block_hash, not get_block_by_number
// Unfortunately, in order to only operate over only finalized data, we have to check the
// returned hash is for a finalized block. We can only do that by calling subxt's `block`, which
// will return the block and any justifications
// If we're already putting in all the work to get the block, we may as well just return it here
// returned hash is for a finalized block. We can only do that by calling the extensive
// is_finalized method, which at least requires the header
// In practice, the block is likely more useful than the header
pub async fn get_block_by_number(&self, number: u64) -> Result<Option<Block>, SeraiError> {
let Some(hash) =
self.0.rpc().block_hash(Some(number.into())).await.map_err(SeraiError::RpcError)? else {

View file

@ -25,7 +25,7 @@ pub async fn provide_batch(batch: SignedBatch) -> [u8; 32] {
.await
.unwrap()
.unwrap()
.header()
.header
.number();
let execution = serai.execute_batch(batch.clone()).unwrap();

View file

@ -10,7 +10,6 @@ use serde::{Serialize, Deserialize};
use sp_core::H256;
/// The type used to identify block numbers.
// Doesn't re-export tendermint-machine's due to traits.
#[derive(
Clone, Copy, Default, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo,
)]

View file

@ -1,46 +0,0 @@
[package]
name = "sc-tendermint"
version = "0.1.0"
description = "Tendermint client for Substrate"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/tendermint/client"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
publish = false
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
async-trait = "0.1"
hex = "0.4"
log = "0.4"
futures = "0.3"
tokio = { version = "1", features = ["sync", "rt"] }
sp-core = { git = "https://github.com/serai-dex/substrate" }
sp-application-crypto = { git = "https://github.com/serai-dex/substrate" }
sp-keystore = { git = "https://github.com/serai-dex/substrate" }
sp-inherents = { git = "https://github.com/serai-dex/substrate" }
sp-staking = { git = "https://github.com/serai-dex/substrate" }
sp-blockchain = { git = "https://github.com/serai-dex/substrate" }
sp-runtime = { git = "https://github.com/serai-dex/substrate" }
sp-api = { git = "https://github.com/serai-dex/substrate" }
sp-consensus = { git = "https://github.com/serai-dex/substrate" }
sp-tendermint = { path = "../primitives" }
sc-network-common = { git = "https://github.com/serai-dex/substrate" }
sc-network = { git = "https://github.com/serai-dex/substrate" }
sc-network-gossip = { git = "https://github.com/serai-dex/substrate" }
sc-service = { git = "https://github.com/serai-dex/substrate" }
sc-client-api = { git = "https://github.com/serai-dex/substrate" }
sc-block-builder = { git = "https://github.com/serai-dex/substrate" }
sc-consensus = { git = "https://github.com/serai-dex/substrate" }
substrate-prometheus-endpoint = { git = "https://github.com/serai-dex/substrate" }
tendermint-machine = { path = "../machine", features = ["substrate"] }

View file

@ -1,15 +0,0 @@
AGPL-3.0-only license
Copyright (c) 2022-2023 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View file

@ -1,67 +0,0 @@
use std::sync::{Arc, RwLock};
use sp_core::Decode;
use sp_runtime::traits::{Hash, Header, Block};
use sc_network::PeerId;
use sc_network_gossip::{Validator, ValidatorContext, ValidationResult};
use tendermint_machine::{ext::SignatureScheme, SignedMessage};
use crate::{TendermintValidator, validators::TendermintValidators};
#[derive(Clone)]
pub(crate) struct TendermintGossip<T: TendermintValidator> {
number: Arc<RwLock<u64>>,
signature_scheme: TendermintValidators<T>,
}
impl<T: TendermintValidator> TendermintGossip<T> {
pub(crate) fn new(number: Arc<RwLock<u64>>, signature_scheme: TendermintValidators<T>) -> Self {
TendermintGossip { number, signature_scheme }
}
pub(crate) fn topic(number: u64) -> <T::Block as Block>::Hash {
<<<T::Block as Block>::Header as Header>::Hashing as Hash>::hash(
&[b"Tendermint Block Topic".as_ref(), &number.to_le_bytes()].concat(),
)
}
}
impl<T: TendermintValidator> Validator<T::Block> for TendermintGossip<T> {
fn validate(
&self,
_: &mut dyn ValidatorContext<T::Block>,
_: &PeerId,
data: &[u8],
) -> ValidationResult<<T::Block as Block>::Hash> {
let msg = match SignedMessage::<
u16,
T::Block,
<TendermintValidators<T> as SignatureScheme>::Signature,
>::decode(&mut &*data)
{
Ok(msg) => msg,
Err(_) => return ValidationResult::Discard,
};
if msg.block().0 < *self.number.read().unwrap() {
return ValidationResult::Discard;
}
// Verify the signature here so we don't carry invalid messages in our gossip layer
// This will cause double verification of the signature, yet that's a minimal cost
if !msg.verify_signature(&self.signature_scheme) {
return ValidationResult::Discard;
}
ValidationResult::ProcessAndKeep(Self::topic(msg.block().0))
}
fn message_expired<'a>(
&'a self,
) -> Box<dyn FnMut(<T::Block as Block>::Hash, &[u8]) -> bool + 'a> {
let number = self.number.clone();
Box::new(move |topic, _| topic != Self::topic(*number.read().unwrap()))
}
}

View file

@ -1,72 +0,0 @@
use std::{
pin::Pin,
sync::RwLock,
task::{Poll, Context},
future::Future,
};
use sp_runtime::traits::{Header, Block};
use sp_consensus::Error;
use sc_consensus::{BlockImportStatus, BlockImportError, Link};
use sc_service::ImportQueue;
use tendermint_machine::ext::BlockError;
use crate::TendermintImportQueue;
// Custom helpers for ImportQueue in order to obtain the result of a block's importing
struct ValidateLink<B: Block>(Option<(B::Hash, Result<(), BlockError>)>);
impl<B: Block> Link<B> for ValidateLink<B> {
fn blocks_processed(
&mut self,
imported: usize,
count: usize,
mut results: Vec<(
Result<BlockImportStatus<<B::Header as Header>::Number>, BlockImportError>,
B::Hash,
)>,
) {
assert!(imported <= 1);
assert_eq!(count, 1);
self.0 = Some((
results[0].1,
match results.swap_remove(0).0 {
Ok(_) => Ok(()),
Err(BlockImportError::Other(Error::Other(err))) => Err(
err.downcast::<BlockError>().map(|boxed| *boxed.as_ref()).unwrap_or(BlockError::Fatal),
),
_ => Err(BlockError::Fatal),
},
));
}
}
pub(crate) struct ImportFuture<'a, B: Block, T: Send>(
B::Hash,
RwLock<&'a mut TendermintImportQueue<B, T>>,
);
impl<'a, B: Block, T: Send> ImportFuture<'a, B, T> {
pub(crate) fn new(
hash: B::Hash,
queue: &'a mut TendermintImportQueue<B, T>,
) -> ImportFuture<B, T> {
ImportFuture(hash, RwLock::new(queue))
}
}
impl<'a, B: Block, T: Send> Future for ImportFuture<'a, B, T> {
type Output = Result<(), BlockError>;
fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Self::Output> {
let mut link = ValidateLink(None);
self.1.write().unwrap().poll_actions(ctx, &mut link);
if let Some(res) = link.0 {
assert_eq!(res.0, self.0);
Poll::Ready(res.1)
} else {
Poll::Pending
}
}
}

View file

@ -1,494 +0,0 @@
use std::{
sync::{Arc, RwLock},
time::{UNIX_EPOCH, SystemTime, Duration},
collections::HashSet,
};
use async_trait::async_trait;
use log::{debug, warn, error};
use futures::{
SinkExt, StreamExt,
lock::Mutex,
channel::mpsc::{self, UnboundedSender},
};
use sp_core::{Encode, Decode, traits::SpawnEssentialNamed};
use sp_keystore::CryptoStore;
use sp_runtime::{
traits::{Header, Block},
Digest,
};
use sp_blockchain::HeaderBackend;
use sp_consensus::{Error, BlockOrigin, BlockStatus, Proposer, Environment};
use sc_consensus::import_queue::IncomingBlock;
use sc_service::ImportQueue;
use sc_client_api::{BlockBackend, Finalizer, BlockchainEvents};
use sc_network::{ProtocolName, NetworkBlock};
use sc_network_gossip::GossipEngine;
use substrate_prometheus_endpoint::Registry;
use tendermint_machine::{
ext::{BlockError, BlockNumber, Commit, SignatureScheme, Network},
SignedMessage, TendermintMachine, TendermintHandle,
};
use crate::{
CONSENSUS_ID, TendermintValidator,
validators::{TendermintSigner, TendermintValidators},
tendermint::TendermintImport,
};
mod gossip;
use gossip::TendermintGossip;
mod import_future;
use import_future::ImportFuture;
// Data for an active validator
// This is distinct as even when we aren't an authority, we still create stubbed Authority objects
// as it's only Authority which implements tendermint_machine::ext::Network. Network has
// verify_commit provided, and even non-authorities have to verify commits
struct ActiveAuthority<T: TendermintValidator> {
signer: TendermintSigner<T>,
// The number of the Block we're working on producing
block_in_progress: Arc<RwLock<u64>>,
// Notification channel for when we start on a new block
new_block_event: UnboundedSender<()>,
// Outgoing message queue, placed here as the GossipEngine itself can't be
gossip: UnboundedSender<
SignedMessage<u16, T::Block, <TendermintValidators<T> as SignatureScheme>::Signature>,
>,
// Block producer
env: Arc<Mutex<T::Environment>>,
announce: T::Network,
}
/// Tendermint Authority. Participates in the block proposal and voting process.
pub struct TendermintAuthority<T: TendermintValidator> {
import: TendermintImport<T>,
active: Option<ActiveAuthority<T>>,
}
// Get a block to propose after the specified header
// If stub is true, no time will be spent adding transactions to it (beyond what's required),
// making it as minimal as possible (a stub)
// This is so we can create proposals when syncing, respecting tendermint-machine's API boundaries,
// without spending the entire block processing time trying to include transactions (since we know
// our proposal is meaningless and we'll just be syncing a new block anyways)
async fn get_proposal<T: TendermintValidator>(
env: &Arc<Mutex<T::Environment>>,
import: &TendermintImport<T>,
header: &<T::Block as Block>::Header,
) -> T::Block {
let proposer =
env.lock().await.init(header).await.expect("Failed to create a proposer for the new block");
proposer
.propose(
import.inherent_data(*header.parent_hash()).await,
Digest::default(),
// The first processing time is to build the block
// The second is for it to be downloaded (assumes a block won't take longer to download
// than it'll take to process)
// The third is for it to actually be processed
Duration::from_secs((T::BLOCK_PROCESSING_TIME_IN_SECONDS / 3).into()),
Some(T::PROPOSED_BLOCK_SIZE_LIMIT),
)
.await
.expect("Failed to crate a new block proposal")
.block
}
impl<T: TendermintValidator> TendermintAuthority<T> {
// Authority which is capable of verifying commits
pub(crate) fn stub(import: TendermintImport<T>) -> Self {
Self { import, active: None }
}
async fn get_proposal(&self, header: &<T::Block as Block>::Header) -> T::Block {
get_proposal(&self.active.as_ref().unwrap().env, &self.import, header).await
}
/// Create and run a new Tendermint Authority, proposing and voting on blocks.
/// This should be spawned on a task as it will not return until the P2P stack shuts down.
#[allow(clippy::too_many_arguments, clippy::new_ret_no_self)]
pub async fn new(
genesis: SystemTime,
protocol: ProtocolName,
import: TendermintImport<T>,
keys: Arc<dyn CryptoStore>,
providers: T::CIDP,
spawner: impl SpawnEssentialNamed,
env: T::Environment,
network: T::Network,
registry: Option<&Registry>,
) {
// This should only have a single value, yet a bounded channel with a capacity of 1 would cause
// a firm bound. It's not worth having a backlog crash the node since we aren't constrained
let (new_block_event_send, mut new_block_event_recv) = mpsc::unbounded();
let (msg_send, mut msg_recv) = mpsc::unbounded();
// Move the env into an Arc
let env = Arc::new(Mutex::new(env));
// Scoped so the temporary variables used here don't leak
let (block_in_progress, mut gossip, TendermintHandle { mut step, mut messages, machine }) = {
// Get the info necessary to spawn the machine
let info = import.client.info();
// Header::Number: TryInto<u64> doesn't implement Debug and can't be unwrapped
let last_block: u64 = match info.finalized_number.try_into() {
Ok(best) => best,
Err(_) => panic!("BlockNumber exceeded u64"),
};
let last_hash = info.finalized_hash;
let last_time = {
// Convert into a Unix timestamp
let genesis = genesis.duration_since(UNIX_EPOCH).unwrap().as_secs();
// Get the last block's time by grabbing its commit and reading the time from that
Commit::<TendermintValidators<T>>::decode(
&mut import
.client
.justifications(last_hash)
.unwrap()
.map(|justifications| justifications.get(CONSENSUS_ID).cloned().unwrap())
.unwrap_or_default()
.as_ref(),
)
.map(|commit| commit.end_time)
// The commit provides the time its block ended at
// The genesis time is when the network starts
// Accordingly, the end of the genesis block is a block time after the genesis time
.unwrap_or_else(|_| genesis + u64::from(Self::block_time()))
};
let next_block = last_block + 1;
// Shared references between us and the Tendermint machine (and its actions via its Network
// trait)
let block_in_progress = Arc::new(RwLock::new(next_block));
// Write the providers into the import so it can verify inherents
*import.providers.write().await = Some(providers);
let authority = Self {
import: import.clone(),
active: Some(ActiveAuthority {
signer: TendermintSigner(keys, import.validators.clone()),
block_in_progress: block_in_progress.clone(),
new_block_event: new_block_event_send,
gossip: msg_send,
env: env.clone(),
announce: network.clone(),
}),
};
// Get our first proposal
let proposal =
authority.get_proposal(&import.client.header(last_hash).unwrap().unwrap()).await;
// Create the gossip network
// This has to be spawning the machine, else gossip fails for some reason
let gossip = GossipEngine::new(
network,
protocol,
Arc::new(TendermintGossip::new(block_in_progress.clone(), import.validators.clone())),
registry,
);
(
block_in_progress,
gossip,
TendermintMachine::new(authority, BlockNumber(last_block), last_time, proposal).await,
)
};
spawner.spawn_essential("machine", Some("tendermint"), Box::pin(machine.run()));
// Start receiving messages about the Tendermint process for this block
let mut gossip_recv =
gossip.messages_for(TendermintGossip::<T>::topic(*block_in_progress.read().unwrap()));
// Get finality events from Substrate
let mut finality = import.client.finality_notification_stream();
loop {
futures::select_biased! {
// GossipEngine closed down
_ = gossip => {
debug!(
target: "tendermint",
"GossipEngine shut down. {}",
"Is the node shutting down?"
);
break;
},
// Synced a block from the network
notif = finality.next() => {
if let Some(notif) = notif {
let number = match (*notif.header.number()).try_into() {
Ok(number) => number,
Err(_) => panic!("BlockNumber exceeded u64"),
};
// There's a race condition between the machine add_block and this
// Both wait for a write lock on this ref and don't release it until after updating it
// accordingly
{
let mut block_in_progress = block_in_progress.write().unwrap();
if number < *block_in_progress {
continue;
}
let next_block = number + 1;
*block_in_progress = next_block;
gossip_recv = gossip.messages_for(TendermintGossip::<T>::topic(next_block));
}
let justifications = import.client.justifications(notif.hash).unwrap().unwrap();
step.send((
BlockNumber(number),
Commit::decode(&mut justifications.get(CONSENSUS_ID).unwrap().as_ref()).unwrap(),
// Creating a proposal will fail if syncing occurs radically faster than machine
// stepping takes
// Don't create proposals when stepping accordingly
None
)).await.unwrap();
} else {
debug!(
target: "tendermint",
"Finality notification stream closed down. {}",
"Is the node shutting down?"
);
break;
}
},
// Machine accomplished a new block
new_block = new_block_event_recv.next() => {
if new_block.is_some() {
gossip_recv = gossip.messages_for(
TendermintGossip::<T>::topic(*block_in_progress.read().unwrap())
);
} else {
debug!(
target: "tendermint",
"Block notification stream shut down. {}",
"Is the node shutting down?"
);
break;
}
},
// Message to broadcast
msg = msg_recv.next() => {
if let Some(msg) = msg {
let topic = TendermintGossip::<T>::topic(msg.block().0);
gossip.gossip_message(topic, msg.encode(), false);
} else {
debug!(
target: "tendermint",
"Machine's message channel shut down. {}",
"Is the node shutting down?"
);
break;
}
},
// Received a message
msg = gossip_recv.next() => {
if let Some(msg) = msg {
messages.send(
match SignedMessage::decode(&mut msg.message.as_ref()) {
Ok(msg) => msg,
Err(e) => {
// This is guaranteed to be valid thanks to to the gossip validator, assuming
// that pipeline is correct. This doesn't panic as a hedge
error!(target: "tendermint", "Couldn't decode valid message: {}", e);
continue;
}
}
).await.unwrap();
} else {
debug!(
target: "tendermint",
"Gossip channel shut down. {}",
"Is the node shutting down?"
);
break;
}
}
}
}
}
}
#[async_trait]
impl<T: TendermintValidator> Network for TendermintAuthority<T> {
type ValidatorId = u16;
type SignatureScheme = TendermintValidators<T>;
type Weights = TendermintValidators<T>;
type Block = T::Block;
const BLOCK_PROCESSING_TIME: u32 = T::BLOCK_PROCESSING_TIME_IN_SECONDS;
const LATENCY_TIME: u32 = T::LATENCY_TIME_IN_SECONDS;
fn signer(&self) -> TendermintSigner<T> {
self.active.as_ref().unwrap().signer.clone()
}
fn signature_scheme(&self) -> TendermintValidators<T> {
self.import.validators.clone()
}
fn weights(&self) -> TendermintValidators<T> {
self.import.validators.clone()
}
async fn broadcast(
&mut self,
msg: SignedMessage<u16, Self::Block, <TendermintValidators<T> as SignatureScheme>::Signature>,
) {
if self.active.as_mut().unwrap().gossip.unbounded_send(msg).is_err() {
warn!(
target: "tendermint",
"Attempted to broadcast a message except the gossip channel is closed. {}",
"Is the node shutting down?"
);
}
}
async fn slash(&mut self, validator: u16) {
// TODO
error!("slashing {}, if this is a local network, this shouldn't happen", validator);
}
// The Tendermint machine will call add_block for any block which is committed to, regardless of
// validity. To determine validity, it expects a validate function, which Substrate doesn't
// directly offer, and an add function. In order to comply with Serai's modified view of inherent
// transactions, validate MUST check inherents, yet add_block must not.
//
// In order to acquire a validate function, any block proposed by a legitimate proposer is
// imported. This performs full validation and makes the block available as a tip. While this
// would be incredibly unsafe thanks to the unchecked inherents, it's defined as a tip with less
// work, despite being a child of some parent. This means it won't be moved to nor operated on by
// the node.
//
// When Tendermint completes, the block is finalized, setting it as the tip regardless of work.
async fn validate(&mut self, block: &T::Block) -> Result<(), BlockError> {
let hash = block.hash();
let (header, body) = block.clone().deconstruct();
let parent = *header.parent_hash();
let number = *header.number();
// Can happen when we sync a block while also acting as a validator
if number <= self.import.client.info().best_number {
debug!(target: "tendermint", "Machine proposed a block for a slot we've already synced");
Err(BlockError::Temporal)?;
}
let mut queue_write = self.import.queue.write().await;
*self.import.importing_block.write().unwrap() = Some(hash);
queue_write.as_mut().unwrap().service_ref().import_blocks(
BlockOrigin::ConsensusBroadcast, // TODO: Use BlockOrigin::Own when it's our block
vec![IncomingBlock {
hash,
header: Some(header),
body: Some(body),
indexed_body: None,
justifications: None,
origin: None, // TODO
allow_missing_state: false,
skip_execution: false,
import_existing: self.import.recheck.read().unwrap().contains(&hash),
state: None,
}],
);
ImportFuture::new(hash, queue_write.as_mut().unwrap()).await?;
// Sanity checks that a child block can have less work than its parent
{
let info = self.import.client.info();
assert_eq!(info.best_hash, parent);
assert_eq!(info.finalized_hash, parent);
assert_eq!(info.best_number, number - 1u8.into());
assert_eq!(info.finalized_number, number - 1u8.into());
}
Ok(())
}
async fn add_block(
&mut self,
block: T::Block,
commit: Commit<TendermintValidators<T>>,
) -> Option<T::Block> {
// Prevent import_block from being called while we run
let _lock = self.import.sync_lock.lock().await;
// If we didn't import this block already, return
// If it's a legitimate block, we'll pick it up in the standard sync loop
if self.import.client.block_status(block.hash()).unwrap() != BlockStatus::InChainWithState {
return None;
}
// Check if we already imported this externally
if self.import.client.justifications(block.hash()).unwrap().is_some() {
debug!(target: "tendermint", "Machine produced a commit after we already synced it");
} else {
let hash = block.hash();
let justification = (CONSENSUS_ID, commit.encode());
debug_assert!(self.import.verify_justification(hash, &justification).is_ok());
let raw_number = *block.header().number();
let number: u64 = match raw_number.try_into() {
Ok(number) => number,
Err(_) => panic!("BlockNumber exceeded u64"),
};
let active = self.active.as_mut().unwrap();
let mut block_in_progress = active.block_in_progress.write().unwrap();
// This will hold true unless we received, and handled, a notification for the block before
// its justification was made available
debug_assert_eq!(number, *block_in_progress);
// Finalize the block
self
.import
.client
.finalize_block(hash, Some(justification), true)
.map_err(|_| Error::InvalidJustification)
.unwrap();
// Tell the loop we received a block and to move to the next
*block_in_progress = number + 1;
if active.new_block_event.unbounded_send(()).is_err() {
warn!(
target: "tendermint",
"Attempted to send a new number to the gossip handler except it's closed. {}",
"Is the node shutting down?"
);
}
// Announce the block to the network so new clients can sync properly
active.announce.announce_block(hash, None);
active.announce.new_best_block_imported(hash, raw_number);
}
// Clear any blocks for the previous slot which we were willing to recheck
*self.import.recheck.write().unwrap() = HashSet::new();
Some(self.get_proposal(block.header()).await)
}
}

View file

@ -1,180 +0,0 @@
use std::{marker::PhantomData, sync::Arc, collections::HashMap};
use async_trait::async_trait;
use sp_runtime::traits::{Header, Block};
use sp_blockchain::{BlockStatus, HeaderBackend, Backend as BlockchainBackend};
use sp_consensus::{Error, CacheKeyId, BlockOrigin, SelectChain};
use sc_consensus::{BlockCheckParams, BlockImportParams, ImportResult, BlockImport, Verifier};
use sc_client_api::{Backend, BlockBackend};
use crate::{TendermintValidator, tendermint::TendermintImport};
impl<T: TendermintValidator> TendermintImport<T> {
fn check_already_in_chain(&self, hash: <T::Block as Block>::Hash) -> bool {
// If it's in chain, with justifications, return it's already on chain
// If it's in chain, without justifications, continue the block import process to import its
// justifications
// This can be triggered if the validators add a block, without justifications, yet the p2p
// process then broadcasts it with its justifications
(self.client.status(hash).unwrap() == BlockStatus::InChain) &&
self.client.justifications(hash).unwrap().is_some()
}
}
#[async_trait]
impl<T: TendermintValidator> BlockImport<T::Block> for TendermintImport<T>
where
Arc<T::Client>: BlockImport<T::Block, Transaction = T::BackendTransaction>,
<Arc<T::Client> as BlockImport<T::Block>>::Error: Into<Error>,
{
type Error = Error;
type Transaction = T::BackendTransaction;
// TODO: Is there a DoS where you send a block without justifications, causing it to error,
// yet adding it to the blacklist in the process preventing further syncing?
async fn check_block(
&mut self,
mut block: BlockCheckParams<T::Block>,
) -> Result<ImportResult, Self::Error> {
if self.check_already_in_chain(block.hash) {
return Ok(ImportResult::AlreadyInChain);
}
self.verify_order(block.parent_hash, block.number)?;
// Does not verify origin here as origin only applies to unfinalized blocks
// We don't have context on if this block has justifications or not
block.allow_missing_state = false;
block.allow_missing_parent = false;
self.client.check_block(block).await.map_err(Into::into)
}
async fn import_block(
&mut self,
mut block: BlockImportParams<T::Block, Self::Transaction>,
new_cache: HashMap<CacheKeyId, Vec<u8>>,
) -> Result<ImportResult, Self::Error> {
// Don't allow multiple blocks to be imported at once
let _lock = self.sync_lock.lock().await;
if self.check_already_in_chain(block.header.hash()) {
return Ok(ImportResult::AlreadyInChain);
}
self.check(&mut block).await?;
self.client.import_block(block, new_cache).await.map_err(Into::into)
}
}
#[async_trait]
impl<T: TendermintValidator> Verifier<T::Block> for TendermintImport<T>
where
Arc<T::Client>: BlockImport<T::Block, Transaction = T::BackendTransaction>,
<Arc<T::Client> as BlockImport<T::Block>>::Error: Into<Error>,
{
async fn verify(
&mut self,
mut block: BlockImportParams<T::Block, ()>,
) -> Result<(BlockImportParams<T::Block, ()>, Option<Vec<(CacheKeyId, Vec<u8>)>>), String> {
block.origin = match block.origin {
BlockOrigin::Genesis => BlockOrigin::Genesis,
BlockOrigin::NetworkBroadcast => BlockOrigin::NetworkBroadcast,
// Re-map NetworkInitialSync to NetworkBroadcast so it still triggers notifications
// Tendermint will listen to the finality stream. If we sync a block we're running a machine
// for, it'll force the machine to move ahead. We can only do that if there actually are
// notifications
//
// Then Serai also runs data indexing code based on block addition, so ensuring it always
// emits events ensures we always perform our necessary indexing (albeit with a race
// condition since Substrate will eventually prune the block's state, potentially before
// indexing finishes when syncing)
//
// The alternative to this would be editing Substrate directly, which would be a lot less
// fragile, manually triggering the notifications (which may be possible with code intended
// for testing), writing our own notification system, or implementing lock_import_and_run
// on our end, letting us directly set the notifications, so we're not beholden to when
// Substrate decides to call notify_finalized
//
// lock_import_and_run unfortunately doesn't allow async code and generally isn't feasible to
// work with though. We also couldn't use it to prevent Substrate from creating
// notifications, so it only solves half the problem. We'd *still* have to keep this patch,
// with all its fragility, unless we edit Substrate or move the entire block import flow here
BlockOrigin::NetworkInitialSync => BlockOrigin::NetworkBroadcast,
// Also re-map File so bootstraps also trigger notifications, enabling using bootstraps
BlockOrigin::File => BlockOrigin::NetworkBroadcast,
// We do not want this block, which hasn't been confirmed, to be broadcast over the net
// Substrate will generate notifications unless it's Genesis, which this isn't, InitialSync,
// which changes telemetry behavior, or File, which is... close enough
BlockOrigin::ConsensusBroadcast => BlockOrigin::File,
BlockOrigin::Own => BlockOrigin::File,
};
if self.check_already_in_chain(block.header.hash()) {
return Ok((block, None));
}
self.check(&mut block).await.map_err(|e| format!("{e}"))?;
Ok((block, None))
}
}
/// Tendermint's Select Chain, where the best chain is defined as the most recently finalized
/// block.
///
/// leaves panics on call due to not being applicable under Tendermint. Any provided answer would
/// have conflicts best left unraised.
//
// SelectChain, while provided by Substrate and part of PartialComponents, isn't used by Substrate
// It's common between various block-production/finality crates, yet Substrate as a system doesn't
// rely on it, which is good, because its definition is explicitly incompatible with Tendermint
//
// leaves is supposed to return all leaves of the blockchain. While Tendermint maintains that view,
// an honest node will only build on the most recently finalized block, so it is a 'leaf' despite
// having descendants
//
// best_chain will always be this finalized block, yet Substrate explicitly defines it as one of
// the above leaves, which this finalized block is explicitly not included in. Accordingly, we
// can never provide a compatible decision
//
// Since PartialComponents expects it, an implementation which does its best is provided. It panics
// if leaves is called, yet returns the finalized chain tip for best_chain, as that's intended to
// be the header to build upon
pub struct TendermintSelectChain<B: Block, Be: Backend<B>>(Arc<Be>, PhantomData<B>);
impl<B: Block, Be: Backend<B>> Clone for TendermintSelectChain<B, Be> {
fn clone(&self) -> Self {
TendermintSelectChain(self.0.clone(), PhantomData)
}
}
impl<B: Block, Be: Backend<B>> TendermintSelectChain<B, Be> {
pub fn new(backend: Arc<Be>) -> TendermintSelectChain<B, Be> {
TendermintSelectChain(backend, PhantomData)
}
}
#[async_trait]
impl<B: Block, Be: Backend<B>> SelectChain<B> for TendermintSelectChain<B, Be> {
async fn leaves(&self) -> Result<Vec<B::Hash>, Error> {
panic!("Substrate definition of leaves is incompatible with Tendermint")
}
async fn best_chain(&self) -> Result<B::Header, Error> {
Ok(
self
.0
.blockchain()
// There should always be a finalized block
.header(self.0.blockchain().last_finalized().unwrap())
// There should not be an error in retrieving it and since it's finalized, it should exist
.unwrap()
.unwrap(),
)
}
}

View file

@ -1,163 +0,0 @@
use std::sync::Arc;
use sp_core::crypto::KeyTypeId;
use sp_inherents::CreateInherentDataProviders;
use sp_runtime::traits::{Header, Block};
use sp_blockchain::HeaderBackend;
use sp_api::{StateBackend, StateBackendFor, TransactionFor, ApiExt, ProvideRuntimeApi};
use sp_consensus::{Error, Environment};
use sc_client_api::{BlockBackend, Backend, Finalizer, BlockchainEvents};
use sc_block_builder::BlockBuilderApi;
use sc_consensus::{BlockImport, BasicQueue};
use sc_network_common::config::NonDefaultSetConfig;
use sc_network::{ProtocolName, NetworkBlock};
use sc_network_gossip::Network;
use sp_tendermint::TendermintApi;
use substrate_prometheus_endpoint::Registry;
mod validators;
pub(crate) mod tendermint;
pub use tendermint::TendermintImport;
mod block_import;
pub use block_import::TendermintSelectChain;
pub(crate) mod authority;
pub use authority::TendermintAuthority;
pub const CONSENSUS_ID: [u8; 4] = *b"tend";
pub(crate) const KEY_TYPE_ID: KeyTypeId = KeyTypeId(CONSENSUS_ID);
const PROTOCOL_NAME: &str = "/tendermint/1";
pub fn protocol_name<Hash: AsRef<[u8]>>(genesis: Hash, fork: Option<&str>) -> ProtocolName {
let mut name = format!("/{}", hex::encode(genesis.as_ref()));
if let Some(fork) = fork {
name += &format!("/{fork}");
}
name += PROTOCOL_NAME;
name.into()
}
pub fn set_config(protocol: ProtocolName, block_size: u64) -> NonDefaultSetConfig {
// The extra 512 bytes is for the additional data part of Tendermint
// Even with BLS, that should just be 161 bytes in the worst case, for a perfect messaging scheme
// While 256 bytes would suffice there, it's unknown if any LibP2P overhead exists nor if
// anything here will be perfect. Considering this is miniscule compared to the block size, it's
// better safe than sorry.
let mut cfg = NonDefaultSetConfig::new(protocol, block_size + 512);
cfg.allow_non_reserved(25, 25);
cfg
}
/// Trait consolidating all generics required by sc_tendermint for processing.
pub trait TendermintClient: Send + Sync + 'static {
const PROPOSED_BLOCK_SIZE_LIMIT: usize;
const BLOCK_PROCESSING_TIME_IN_SECONDS: u32;
const LATENCY_TIME_IN_SECONDS: u32;
type Block: Block;
type Backend: Backend<Self::Block> + 'static;
/// TransactionFor<Client, Block>
type BackendTransaction: Send + Sync + 'static;
/// StateBackendFor<Client, Block>
type StateBackend: StateBackend<
<<Self::Block as Block>::Header as Header>::Hashing,
Transaction = Self::BackendTransaction,
>;
// Client::Api
type Api: ApiExt<Self::Block, StateBackend = Self::StateBackend>
+ BlockBuilderApi<Self::Block>
+ TendermintApi<Self::Block>;
type Client: Send
+ Sync
+ HeaderBackend<Self::Block>
+ BlockBackend<Self::Block>
+ BlockImport<Self::Block, Transaction = Self::BackendTransaction>
+ Finalizer<Self::Block, Self::Backend>
+ BlockchainEvents<Self::Block>
+ ProvideRuntimeApi<Self::Block, Api = Self::Api>
+ 'static;
}
/// Trait implementable on firm types to automatically provide a full TendermintClient impl.
pub trait TendermintClientMinimal: Send + Sync + 'static {
const PROPOSED_BLOCK_SIZE_LIMIT: usize;
const BLOCK_PROCESSING_TIME_IN_SECONDS: u32;
const LATENCY_TIME_IN_SECONDS: u32;
type Block: Block;
type Backend: Backend<Self::Block> + 'static;
type Api: ApiExt<Self::Block> + BlockBuilderApi<Self::Block> + TendermintApi<Self::Block>;
type Client: Send
+ Sync
+ HeaderBackend<Self::Block>
+ BlockBackend<Self::Block>
+ BlockImport<Self::Block, Transaction = TransactionFor<Self::Client, Self::Block>>
+ Finalizer<Self::Block, Self::Backend>
+ BlockchainEvents<Self::Block>
+ ProvideRuntimeApi<Self::Block, Api = Self::Api>
+ 'static;
}
impl<T: TendermintClientMinimal> TendermintClient for T
where
<T::Client as ProvideRuntimeApi<T::Block>>::Api:
BlockBuilderApi<T::Block> + TendermintApi<T::Block>,
TransactionFor<T::Client, T::Block>: Send + Sync + 'static,
{
const PROPOSED_BLOCK_SIZE_LIMIT: usize = T::PROPOSED_BLOCK_SIZE_LIMIT;
const BLOCK_PROCESSING_TIME_IN_SECONDS: u32 = T::BLOCK_PROCESSING_TIME_IN_SECONDS;
const LATENCY_TIME_IN_SECONDS: u32 = T::LATENCY_TIME_IN_SECONDS;
type Block = T::Block;
type Backend = T::Backend;
type BackendTransaction = TransactionFor<T::Client, T::Block>;
type StateBackend = StateBackendFor<T::Client, T::Block>;
type Api = <T::Client as ProvideRuntimeApi<T::Block>>::Api;
type Client = T::Client;
}
/// Trait consolidating additional generics required by sc_tendermint for authoring.
pub trait TendermintValidator: TendermintClient {
type CIDP: CreateInherentDataProviders<Self::Block, ()> + 'static;
type Environment: Send + Sync + Environment<Self::Block> + 'static;
type Network: Clone
+ Send
+ Sync
+ Network<Self::Block>
+ NetworkBlock<<Self::Block as Block>::Hash, <<Self::Block as Block>::Header as Header>::Number>
+ 'static;
}
pub type TendermintImportQueue<Block, Transaction> = BasicQueue<Block, Transaction>;
/// Create an import queue, additionally returning the Tendermint Import object iself, enabling
/// creating an author later as well.
pub fn import_queue<T: TendermintValidator>(
spawner: &impl sp_core::traits::SpawnEssentialNamed,
client: Arc<T::Client>,
registry: Option<&Registry>,
) -> (TendermintImport<T>, TendermintImportQueue<T::Block, T::BackendTransaction>)
where
Arc<T::Client>: BlockImport<T::Block, Transaction = T::BackendTransaction>,
<Arc<T::Client> as BlockImport<T::Block>>::Error: Into<Error>,
{
let import = TendermintImport::<T>::new(client);
let boxed = Box::new(import.clone());
// Use None for the justification importer since justifications always come with blocks
// Therefore, they're never imported after the fact, which is what mandates an importer
let queue = || BasicQueue::new(import.clone(), boxed.clone(), None, spawner, registry);
*futures::executor::block_on(import.queue.write()) = Some(queue());
(import.clone(), queue())
}

View file

@ -1,247 +0,0 @@
use std::{
sync::{Arc, RwLock},
collections::HashSet,
};
use log::{debug, warn};
use tokio::sync::{Mutex, RwLock as AsyncRwLock};
use sp_core::Decode;
use sp_runtime::{
traits::{Header, Block},
Justification,
};
use sp_inherents::{InherentData, InherentDataProvider, CreateInherentDataProviders};
use sp_blockchain::HeaderBackend;
use sp_api::ProvideRuntimeApi;
use sp_consensus::Error;
use sc_consensus::{ForkChoiceStrategy, BlockImportParams};
use sc_block_builder::BlockBuilderApi;
use tendermint_machine::ext::{BlockError, Commit, Network};
use crate::{
CONSENSUS_ID, TendermintClient, TendermintValidator, validators::TendermintValidators,
TendermintImportQueue, authority::TendermintAuthority,
};
type InstantiatedTendermintImportQueue<T> = TendermintImportQueue<
<T as TendermintClient>::Block,
<T as TendermintClient>::BackendTransaction,
>;
/// Tendermint import handler.
pub struct TendermintImport<T: TendermintValidator> {
// Lock ensuring only one block is imported at a time
pub(crate) sync_lock: Arc<Mutex<()>>,
pub(crate) validators: TendermintValidators<T>,
pub(crate) providers: Arc<AsyncRwLock<Option<T::CIDP>>>,
pub(crate) importing_block: Arc<RwLock<Option<<T::Block as Block>::Hash>>>,
// A set of blocks which we're willing to recheck
// We reject blocks with invalid inherents, yet inherents can be fatally flawed or solely
// perceived as flawed
// If we solely perceive them as flawed, we mark them as eligible for being checked again. Then,
// if they're proposed again, we see if our perception has changed
pub(crate) recheck: Arc<RwLock<HashSet<<T::Block as Block>::Hash>>>,
pub(crate) client: Arc<T::Client>,
pub(crate) queue: Arc<AsyncRwLock<Option<InstantiatedTendermintImportQueue<T>>>>,
}
impl<T: TendermintValidator> Clone for TendermintImport<T> {
fn clone(&self) -> Self {
TendermintImport {
sync_lock: self.sync_lock.clone(),
validators: self.validators.clone(),
providers: self.providers.clone(),
importing_block: self.importing_block.clone(),
recheck: self.recheck.clone(),
client: self.client.clone(),
queue: self.queue.clone(),
}
}
}
impl<T: TendermintValidator> TendermintImport<T> {
pub(crate) fn new(client: Arc<T::Client>) -> TendermintImport<T> {
TendermintImport {
sync_lock: Arc::new(Mutex::new(())),
validators: TendermintValidators::new(client.clone()),
providers: Arc::new(AsyncRwLock::new(None)),
importing_block: Arc::new(RwLock::new(None)),
recheck: Arc::new(RwLock::new(HashSet::new())),
client,
queue: Arc::new(AsyncRwLock::new(None)),
}
}
pub(crate) async fn inherent_data(&self, parent: <T::Block as Block>::Hash) -> InherentData {
match self
.providers
.read()
.await
.as_ref()
.unwrap()
.create_inherent_data_providers(parent, ())
.await
{
Ok(providers) => match providers.create_inherent_data().await {
Ok(data) => Some(data),
Err(err) => {
warn!(target: "tendermint", "Failed to create inherent data: {}", err);
None
}
},
Err(err) => {
warn!(target: "tendermint", "Failed to create inherent data providers: {}", err);
None
}
}
.unwrap_or_else(InherentData::new)
}
async fn check_inherents(
&self,
hash: <T::Block as Block>::Hash,
block: T::Block,
) -> Result<(), Error> {
let inherent_data = self.inherent_data(*block.header().parent_hash()).await;
let err = self
.client
.runtime_api()
.check_inherents(self.client.info().finalized_hash, block, inherent_data)
.map_err(|_| Error::Other(BlockError::Fatal.into()))?;
if err.ok() {
self.recheck.write().unwrap().remove(&hash);
Ok(())
} else if err.fatal_error() {
Err(Error::Other(BlockError::Fatal.into()))
} else {
debug!(target: "tendermint", "Proposed block has temporally wrong inherents");
self.recheck.write().unwrap().insert(hash);
Err(Error::Other(BlockError::Temporal.into()))
}
}
// Ensure this is part of a sequential import
pub(crate) fn verify_order(
&self,
parent: <T::Block as Block>::Hash,
number: <<T::Block as Block>::Header as Header>::Number,
) -> Result<(), Error> {
let info = self.client.info();
if (info.finalized_hash != parent) || ((info.finalized_number + 1u16.into()) != number) {
Err(Error::Other("non-sequential import".into()))?;
}
Ok(())
}
// Do not allow blocks from the traditional network to be broadcast
// Only allow blocks from Tendermint
// Tendermint's propose message could be rewritten as a seal OR Tendermint could produce blocks
// which this checks the proposer slot for, and then tells the Tendermint machine
// While those would be more seamless with Substrate, there's no actual benefit to doing so
fn verify_origin(&self, hash: <T::Block as Block>::Hash) -> Result<(), Error> {
if let Some(tm_hash) = *self.importing_block.read().unwrap() {
if hash == tm_hash {
return Ok(());
}
}
Err(Error::Other("block created outside of tendermint".into()))
}
// Errors if the justification isn't valid
pub(crate) fn verify_justification(
&self,
hash: <T::Block as Block>::Hash,
justification: &Justification,
) -> Result<(), Error> {
if justification.0 != CONSENSUS_ID {
Err(Error::InvalidJustification)?;
}
let commit: Commit<TendermintValidators<T>> =
Commit::decode(&mut justification.1.as_ref()).map_err(|_| Error::InvalidJustification)?;
// Create a stubbed TendermintAuthority so we can verify the commit
if !TendermintAuthority::stub(self.clone()).verify_commit(hash, &commit) {
Err(Error::InvalidJustification)?;
}
Ok(())
}
// Verifies the justifications aren't malformed, not that the block is justified
// Errors if justifications is neither empty nor a single Tendermint justification
// If the block does have a justification, finalized will be set to true
fn verify_justifications<BT>(
&self,
block: &mut BlockImportParams<T::Block, BT>,
) -> Result<(), Error> {
if !block.finalized {
if let Some(justifications) = &block.justifications {
let mut iter = justifications.iter();
let next = iter.next();
if next.is_none() || iter.next().is_some() {
Err(Error::InvalidJustification)?;
}
self.verify_justification(block.header.hash(), next.unwrap())?;
block.finalized = true;
}
}
Ok(())
}
pub(crate) async fn check<BT>(
&self,
block: &mut BlockImportParams<T::Block, BT>,
) -> Result<(), Error> {
if block.finalized {
if block.fork_choice != Some(ForkChoiceStrategy::Custom(false)) {
// Since we alw1ays set the fork choice, this means something else marked the block as
// finalized, which shouldn't be possible. Ensuring nothing else is setting blocks as
// finalized helps ensure our security
panic!("block was finalized despite not setting the fork choice");
}
return Ok(());
}
// Set the block as a worse choice
block.fork_choice = Some(ForkChoiceStrategy::Custom(false));
self.verify_order(*block.header.parent_hash(), *block.header.number())?;
self.verify_justifications(block)?;
// If the block wasn't finalized, verify the origin and validity of its inherents
if !block.finalized {
let hash = block.header.hash();
self.verify_origin(hash)?;
self
.check_inherents(hash, T::Block::new(block.header.clone(), block.body.clone().unwrap()))
.await?;
}
// Additionally check these fields are empty
// They *should* be unused, so requiring their emptiness prevents malleability and ensures
// nothing slips through
if !block.post_digests.is_empty() {
Err(Error::Other("post-digests included".into()))?;
}
if !block.auxiliary.is_empty() {
Err(Error::Other("auxiliary included".into()))?;
}
Ok(())
}
}

View file

@ -1,191 +0,0 @@
use core::ops::Deref;
use std::sync::{Arc, RwLock};
use async_trait::async_trait;
use sp_core::Decode;
use sp_application_crypto::{
RuntimePublic as PublicTrait,
sr25519::{Public, Signature},
};
use sp_keystore::CryptoStore;
use sp_staking::SessionIndex;
use sp_api::ProvideRuntimeApi;
use sc_client_api::HeaderBackend;
use tendermint_machine::ext::{BlockNumber, RoundNumber, Weights, Signer, SignatureScheme};
use sp_tendermint::TendermintApi;
use crate::{KEY_TYPE_ID, TendermintClient};
struct TendermintValidatorsStruct {
session: SessionIndex,
total_weight: u64,
weights: Vec<u64>,
lookup: Vec<Public>,
}
impl TendermintValidatorsStruct {
fn from_module<T: TendermintClient>(client: &Arc<T::Client>) -> Self {
let last = client.info().finalized_hash;
let api = client.runtime_api();
let session = api.current_session(last).unwrap();
let validators = api.validators(last).unwrap();
Self {
session,
// TODO
total_weight: validators.len().try_into().unwrap(),
weights: vec![1; validators.len()],
lookup: validators,
}
}
}
// Wrap every access of the validators struct in something which forces calling refresh
struct Refresh<T: TendermintClient> {
client: Arc<T::Client>,
_refresh: Arc<RwLock<TendermintValidatorsStruct>>,
}
impl<T: TendermintClient> Refresh<T> {
// If the session has changed, re-create the struct with the data on it
fn refresh(&self) {
let session = self._refresh.read().unwrap().session;
if session !=
self.client.runtime_api().current_session(self.client.info().finalized_hash).unwrap()
{
*self._refresh.write().unwrap() = TendermintValidatorsStruct::from_module::<T>(&self.client);
}
}
}
impl<T: TendermintClient> Deref for Refresh<T> {
type Target = RwLock<TendermintValidatorsStruct>;
fn deref(&self) -> &RwLock<TendermintValidatorsStruct> {
self.refresh();
&self._refresh
}
}
/// Tendermint validators observer, providing data on the active validators.
pub struct TendermintValidators<T: TendermintClient>(Refresh<T>);
impl<T: TendermintClient> Clone for TendermintValidators<T> {
fn clone(&self) -> Self {
Self(Refresh { _refresh: self.0._refresh.clone(), client: self.0.client.clone() })
}
}
impl<T: TendermintClient> TendermintValidators<T> {
pub(crate) fn new(client: Arc<T::Client>) -> TendermintValidators<T> {
TendermintValidators(Refresh {
_refresh: Arc::new(RwLock::new(TendermintValidatorsStruct::from_module::<T>(&client))),
client,
})
}
}
pub struct TendermintSigner<T: TendermintClient>(
pub(crate) Arc<dyn CryptoStore>,
pub(crate) TendermintValidators<T>,
);
impl<T: TendermintClient> Clone for TendermintSigner<T> {
fn clone(&self) -> Self {
Self(self.0.clone(), self.1.clone())
}
}
impl<T: TendermintClient> TendermintSigner<T> {
async fn get_public_key(&self) -> Public {
let pubs = self.0.sr25519_public_keys(KEY_TYPE_ID).await;
if pubs.is_empty() {
self.0.sr25519_generate_new(KEY_TYPE_ID, None).await.unwrap()
} else {
pubs[0]
}
}
}
#[async_trait]
impl<T: TendermintClient> Signer for TendermintSigner<T> {
type ValidatorId = u16;
type Signature = Signature;
async fn validator_id(&self) -> Option<u16> {
let key = self.get_public_key().await;
for (i, k) in (*self.1 .0).read().unwrap().lookup.iter().enumerate() {
if k == &key {
return Some(u16::try_from(i).unwrap());
}
}
None
}
async fn sign(&self, msg: &[u8]) -> Signature {
Signature::decode(
&mut self
.0
.sign_with(KEY_TYPE_ID, &self.get_public_key().await.into(), msg)
.await
.unwrap()
.unwrap()
.as_ref(),
)
.unwrap()
}
}
impl<T: TendermintClient> SignatureScheme for TendermintValidators<T> {
type ValidatorId = u16;
type Signature = Signature;
type AggregateSignature = Vec<Signature>;
type Signer = TendermintSigner<T>;
fn verify(&self, validator: u16, msg: &[u8], sig: &Signature) -> bool {
self.0.read().unwrap().lookup[usize::try_from(validator).unwrap()].verify(&msg, sig)
}
fn aggregate(sigs: &[Signature]) -> Vec<Signature> {
sigs.to_vec()
}
fn verify_aggregate(&self, validators: &[u16], msg: &[u8], sigs: &Vec<Signature>) -> bool {
if validators.len() != sigs.len() {
return false;
}
for (v, sig) in validators.iter().zip(sigs.iter()) {
if !self.verify(*v, msg, sig) {
return false;
}
}
true
}
}
impl<T: TendermintClient> Weights for TendermintValidators<T> {
type ValidatorId = u16;
fn total_weight(&self) -> u64 {
self.0.read().unwrap().total_weight
}
fn weight(&self, id: u16) -> u64 {
self.0.read().unwrap().weights[usize::try_from(id).unwrap()]
}
// TODO: https://github.com/serai-dex/serai/issues/159
fn proposer(&self, number: BlockNumber, round: RoundNumber) -> u16 {
u16::try_from(
(number.0 + u64::from(round.0)) % u64::try_from(self.0.read().unwrap().lookup.len()).unwrap(),
)
.unwrap()
}
}

View file

@ -1,38 +0,0 @@
[package]
name = "pallet-tendermint"
version = "0.1.0"
description = "Tendermint pallet for Substrate"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/tendermint/pallet"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
parity-scale-codec = { version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
[features]
std = [
"sp-application-crypto/std",
"frame-system/std",
"frame-support/std",
]
runtime-benchmarks = [
"frame-system/runtime-benchmarks",
"frame-support/runtime-benchmarks",
]
default = ["std"]

View file

@ -1,15 +0,0 @@
AGPL-3.0-only license
Copyright (c) 2022-2023 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View file

@ -1,75 +0,0 @@
#![cfg_attr(not(feature = "std"), no_std)]
#[frame_support::pallet]
pub mod pallet {
use sp_std::vec::Vec;
use sp_core::sr25519::Public;
use frame_support::pallet_prelude::*;
use frame_support::traits::{ConstU32, OneSessionHandler};
type MaxValidators = ConstU32<{ u16::MAX as u32 }>;
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(PhantomData<T>);
#[pallet::storage]
#[pallet::getter(fn session)]
pub type Session<T: Config> = StorageValue<_, u32, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn validators)]
pub type Validators<T: Config> = StorageValue<_, BoundedVec<Public, MaxValidators>, ValueQuery>;
pub mod crypto {
use sp_application_crypto::{KeyTypeId, app_crypto, sr25519};
app_crypto!(sr25519, KeyTypeId(*b"tend"));
impl<T> sp_application_crypto::BoundToRuntimeAppPublic for crate::Pallet<T> {
type Public = Public;
}
sp_application_crypto::with_pair! {
pub type AuthorityPair = Pair;
}
pub type AuthoritySignature = Signature;
pub type AuthorityId = Public;
}
impl<T: Config, V> OneSessionHandler<V> for Pallet<T> {
type Key = crypto::Public;
// TODO
fn on_genesis_session<'a, I: 'a>(_validators: I)
where
I: Iterator<Item = (&'a V, Self::Key)>,
V: 'a,
{
}
fn on_new_session<'a, I: 'a>(changed: bool, validators: I, _queued: I)
where
I: Iterator<Item = (&'a V, Self::Key)>,
V: 'a,
{
if !changed {
return;
}
Session::<T>::put(Self::session() + 1);
Validators::<T>::put(
BoundedVec::try_from(validators.map(|(_, key)| key.into()).collect::<Vec<Public>>())
.unwrap(),
);
}
// TODO
fn on_disabled(_validator_index: u32) {}
}
}
pub use pallet::*;

View file

@ -1,21 +0,0 @@
[package]
name = "sp-tendermint"
version = "0.1.0"
description = "Tendermint primitives for Substrate"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/tendermint/primitives"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-api = { git = "https://github.com/serai-dex/substrate", default-features = false }
[features]
std = ["sp-core/std", "sp-std/std", "sp-api/std"]
default = ["std"]

View file

@ -1,15 +0,0 @@
AGPL-3.0-only license
Copyright (c) 2022-2023 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View file

@ -1,16 +0,0 @@
#![cfg_attr(not(feature = "std"), no_std)]
use sp_core::sr25519::Public;
use sp_std::vec::Vec;
sp_api::decl_runtime_apis! {
/// TendermintApi trait for runtimes to implement.
pub trait TendermintApi {
/// Current session number. A session is NOT a fixed length of blocks, yet rather a continuous
/// set of validators.
fn current_session() -> u32;
/// Current validators.
fn validators() -> Vec<Public>;
}
}

View file

@ -34,7 +34,6 @@ pub mod pallet {
}
#[pallet::pallet]
#[pallet::generate_store(pub(crate) trait Store)]
pub struct Pallet<T>(PhantomData<T>);
impl<T: Config> Pallet<T> {

View file

@ -52,7 +52,6 @@ pub mod pallet {
}
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(PhantomData<T>);
/// The details of a validator set instance.

View file

@ -3,7 +3,7 @@ name = "tendermint-machine"
version = "0.2.0"
description = "An implementation of the Tendermint state machine in Rust"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/tendermint/machine"
repository = "https://github.com/serai-dex/serai/tree/develop/tendermint"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"