diff --git a/chainspecs/plain_spec_finney.json b/chainspecs/plain_spec_finney.json index da16b1e21f..9866edf202 100644 --- a/chainspecs/plain_spec_finney.json +++ b/chainspecs/plain_spec_finney.json @@ -105142,5 +105142,9 @@ } } } + }, + "grandpaWarpSyncCheckpoint": { + "finalizedBlockHeader": "0xc03558186a0676467b1b2e4880c4f1dbde6acbd1e8399e48b408242b732f59eee2391d028b8094d0b3533319529c1b9f924aaf4810dec9be8001b12644b4771f54d63224cf4c23d6547a671f3391c592173e03d6403a949db43a8ac9b80196e994742f1914066175726120ba4be008000000000461757261090a0150dabc064c6e5cc3a0cedcf261c01fafbab51e2bc2cb4406ba2c6c642e086ff52a8c2993efeca89c5de543a908f1e71b5f1c2be45e1701021cc57f840d82a2f702bafed45bea146e871cfa990fba657a349eb816e77f76e211c3d7bfb1926baa2234be319e058fc1dd864c3e9e2722e0e6748eb9b0bc5885b60142cfbabcbb2c748caf0abca3bcc146d5223cf123759ba35a71ce0d020eedee53724c0e1572203aeea1cdcf2cfe4947c8b39af0cf97ae2d548e693ffee2b09e3d246075ef6672749840e6b1f55dedad1c2b385b0978902bbed1007e5d4490767ffb022eb83411255effe234def96ddbea1c4d4d67850b6c57808967d5b066358ed263a5471ff232689eaa5d627b821b760b23269f509921ad5da8c32ed5722bbccccf2e66e36f55ecf82af34921ec7e61e72ffac388fc16dd39470fbfe6fa2a43d379e42a837d5944716244c29353b6bca0edeb8dd114f27e0e14471bedc9e8060c7b801f57f3448cedbe6e90299446c8d03babe22b7e0e2b7689431170dd64da52eb96a0d06f7ff4150a46300dd97e3a11c084c54f7ca0fddc20acfa0c45dd70396a0eed2c3c09a839631d0fabea178ee72ce38c1324bfcb5c1bfad509eb4e25356477cee7db4606e241dc7f7694ab47b7c824e3c9e83bd6516a4a5c083f952b4db3d4d4e17c5d668ffb5da465590dc5684ee630be3e01b808e106b3547e212578348443f19b145286b1b6fe737cbdd2d5a380d78c8b62170c88ffd8ebb73d6351b4466ca70c1d02436f394a241292fe933de1322dc6d5d036ff0a20fd6f27d0bbfe190b11235c426ffb42e3c41abfd1f77fbc29083b3c80d49d32a70ea5c46ad1618554444e3740112f3d548b6316e3ec4b5da6cd1f9704708a4f31c1da267d4b9a040190db230446524e4b990c01503c0694d473b0203bd9842aabbac7ab7d73205a3ccf97096bc5361ea75dcdd0a4010000000000000045ddfeed53c360c0ad3a10fcf8b09b63f032a81152c73f8971fc71660c22b5cf01000000000000000c06118ac33cdf11a313778fd600e45b12af35c4e96fdfd1f24b622a47f5a6040100000000000000f55a9261d2d5cc475537c27d18158d3a0cae213fa87e305a345df2771a31e12501000000000000004077cc7f31e19faa539e089f8282e03f33318290a32fbad369dac950986dcd430100000000000000c40dd57ef4b2c4408548665620cbfd420f09cdaa2cb6056f77b5b769eddeaed00100000000000000ba5fb07ded8410d39649f8e30a3fb4ffa623c9232b30c14f1f42afb36f4bfb3b0100000000000000e96069b13bef0322f8e3e9bac94e303dafe1a6372736cc57a9f32d0b5231809501000000000000005201d27eb236bf32fa024f5c1c094513f084192e756a7511a3649db3b76ba2c30100000000000000600d1e6db2964a84b13569c2a437d8eab4888768b57b73bebca36cc214e836b80100000000000000477b9292e050855f808d743cbaccb52e9c00a5a96d8f0b091f87d1660d96609d0100000000000000370e2fadd6e17124db38edaf10045a039d136b52c125151f8aaa090a1554cd3801000000000000001d351649339513b4061077a469fee1f49f79a130bdc2cf54cdea3b3894c59cbc0100000000000000b8bc8e248f8a1e7c4f3c0954206f57a5a60d0a48418cc54f162b27289f2b3987010000000000000033f0e8e844911223d3c859f3283ca737bf33437d965ed34e350ddb3e5f49ae220100000000000000c92f57a27012a9c9fac61d3185c845c50622f0ac136fce99e62e2920c89dfbdd01000000000000001ac50d47914fdb8a1827b57f623b8aac857ad84a08d77c1ae606d7d24ec46945010000000000000046d41f69d5df314822236500c3d57687ce9b9db3f44f45a4ff91f02e27b3bc2d01000000000000001ab5cd4e35349c94a0516e3d4850cdfb174d23290fdf35e86dfca84d57a3a5c4010000000000000055b0f4f90805b1518d2de726d8767465440f54a5602c92fb0481a9c5eb0b17f80100000000000000000000000466726f6e0902011921a2d414f0a049fd2ec0a5a1fadd6037c94fa953a68b4288a7cd69f29ebcbc0caad2ce0f923890fdc0a3a6cf91026362615de9544092d615fb74acfb1d8dfa8f7b9d61e0baa3e37cf79e5f2e0348c19060423d677b8e7b17c3565173f1adfe3c0966626c2cd982cc30922d17c32bf3e1ddd0b8b785903dd703c6d99ba7f958da056175726101016262a0512462424da899a7a8eeb54deee118b4fa85b0bd931b4f847238baf61c9fe4ca23c2e2b4cfd23e8bcaed3d9fec8cd401d0dac8a3d517fb449946c28084", + "grandpaAuthoritySet": "0x0500000000000000503c0694d473b0203bd9842aabbac7ab7d73205a3ccf97096bc5361ea75dcdd0a4010000000000000045ddfeed53c360c0ad3a10fcf8b09b63f032a81152c73f8971fc71660c22b5cf01000000000000000c06118ac33cdf11a313778fd600e45b12af35c4e96fdfd1f24b622a47f5a6040100000000000000f55a9261d2d5cc475537c27d18158d3a0cae213fa87e305a345df2771a31e12501000000000000004077cc7f31e19faa539e089f8282e03f33318290a32fbad369dac950986dcd430100000000000000c40dd57ef4b2c4408548665620cbfd420f09cdaa2cb6056f77b5b769eddeaed00100000000000000ba5fb07ded8410d39649f8e30a3fb4ffa623c9232b30c14f1f42afb36f4bfb3b0100000000000000e96069b13bef0322f8e3e9bac94e303dafe1a6372736cc57a9f32d0b5231809501000000000000005201d27eb236bf32fa024f5c1c094513f084192e756a7511a3649db3b76ba2c30100000000000000600d1e6db2964a84b13569c2a437d8eab4888768b57b73bebca36cc214e836b801000000000000002c9f738dc1d390c4d9fdb8cdf030441f403ec3ce2d55049d96d6650604f9a07a010000000000000020f5d60bf0f880aa1624fb77895d6ba9c55e6e712c5072b7892595481acb054301000000000000003d362b97f2dbd334ee880489b8bf098478e2b4f25c7fe1db30ae0770d55748750100000000000000f3e7b8a7cb4e4916c6af512c74954c7f71708bfb6172e3ac898cbd87e94d8669010000000000000042c228a2ae6b5f9a587d5bd9fae23371c61bfc5030f2f75bd353828ee774ae9d0100000000000000aef7e3f9920dcfdcf8d84b4df5142cbcf82540a6720bbda8d08b6845b7f81ac301000000000000001bbdc4187afeaf58b851fd56b06ca49b522ccdeed5e727ee10415c03eeb12fd101000000000000005e1ac1aefc93081ca4973e8ea6a90eed26e6bab14e7f1bf4cbc400283533eda70100000000000000a26547efb9369cda4ac126d036e0ecdb08cc01feb095367cd120e3fbda85b2750100000000000000480a24a810ae53434a1db22b3ccb1644bb9c17295c742f96b53534de697926930100000000000000" } } diff --git a/chainspecs/raw_spec_finney.json b/chainspecs/raw_spec_finney.json index 51fbc255d1..d087fbc29f 100644 --- a/chainspecs/raw_spec_finney.json +++ b/chainspecs/raw_spec_finney.json @@ -47742,5 +47742,9 @@ }, "childrenDefault": {} } + }, + "grandpaWarpSyncCheckpoint": { + "finalizedBlockHeader": "0xc03558186a0676467b1b2e4880c4f1dbde6acbd1e8399e48b408242b732f59eee2391d028b8094d0b3533319529c1b9f924aaf4810dec9be8001b12644b4771f54d63224cf4c23d6547a671f3391c592173e03d6403a949db43a8ac9b80196e994742f1914066175726120ba4be008000000000461757261090a0150dabc064c6e5cc3a0cedcf261c01fafbab51e2bc2cb4406ba2c6c642e086ff52a8c2993efeca89c5de543a908f1e71b5f1c2be45e1701021cc57f840d82a2f702bafed45bea146e871cfa990fba657a349eb816e77f76e211c3d7bfb1926baa2234be319e058fc1dd864c3e9e2722e0e6748eb9b0bc5885b60142cfbabcbb2c748caf0abca3bcc146d5223cf123759ba35a71ce0d020eedee53724c0e1572203aeea1cdcf2cfe4947c8b39af0cf97ae2d548e693ffee2b09e3d246075ef6672749840e6b1f55dedad1c2b385b0978902bbed1007e5d4490767ffb022eb83411255effe234def96ddbea1c4d4d67850b6c57808967d5b066358ed263a5471ff232689eaa5d627b821b760b23269f509921ad5da8c32ed5722bbccccf2e66e36f55ecf82af34921ec7e61e72ffac388fc16dd39470fbfe6fa2a43d379e42a837d5944716244c29353b6bca0edeb8dd114f27e0e14471bedc9e8060c7b801f57f3448cedbe6e90299446c8d03babe22b7e0e2b7689431170dd64da52eb96a0d06f7ff4150a46300dd97e3a11c084c54f7ca0fddc20acfa0c45dd70396a0eed2c3c09a839631d0fabea178ee72ce38c1324bfcb5c1bfad509eb4e25356477cee7db4606e241dc7f7694ab47b7c824e3c9e83bd6516a4a5c083f952b4db3d4d4e17c5d668ffb5da465590dc5684ee630be3e01b808e106b3547e212578348443f19b145286b1b6fe737cbdd2d5a380d78c8b62170c88ffd8ebb73d6351b4466ca70c1d02436f394a241292fe933de1322dc6d5d036ff0a20fd6f27d0bbfe190b11235c426ffb42e3c41abfd1f77fbc29083b3c80d49d32a70ea5c46ad1618554444e3740112f3d548b6316e3ec4b5da6cd1f9704708a4f31c1da267d4b9a040190db230446524e4b990c01503c0694d473b0203bd9842aabbac7ab7d73205a3ccf97096bc5361ea75dcdd0a4010000000000000045ddfeed53c360c0ad3a10fcf8b09b63f032a81152c73f8971fc71660c22b5cf01000000000000000c06118ac33cdf11a313778fd600e45b12af35c4e96fdfd1f24b622a47f5a6040100000000000000f55a9261d2d5cc475537c27d18158d3a0cae213fa87e305a345df2771a31e12501000000000000004077cc7f31e19faa539e089f8282e03f33318290a32fbad369dac950986dcd430100000000000000c40dd57ef4b2c4408548665620cbfd420f09cdaa2cb6056f77b5b769eddeaed00100000000000000ba5fb07ded8410d39649f8e30a3fb4ffa623c9232b30c14f1f42afb36f4bfb3b0100000000000000e96069b13bef0322f8e3e9bac94e303dafe1a6372736cc57a9f32d0b5231809501000000000000005201d27eb236bf32fa024f5c1c094513f084192e756a7511a3649db3b76ba2c30100000000000000600d1e6db2964a84b13569c2a437d8eab4888768b57b73bebca36cc214e836b80100000000000000477b9292e050855f808d743cbaccb52e9c00a5a96d8f0b091f87d1660d96609d0100000000000000370e2fadd6e17124db38edaf10045a039d136b52c125151f8aaa090a1554cd3801000000000000001d351649339513b4061077a469fee1f49f79a130bdc2cf54cdea3b3894c59cbc0100000000000000b8bc8e248f8a1e7c4f3c0954206f57a5a60d0a48418cc54f162b27289f2b3987010000000000000033f0e8e844911223d3c859f3283ca737bf33437d965ed34e350ddb3e5f49ae220100000000000000c92f57a27012a9c9fac61d3185c845c50622f0ac136fce99e62e2920c89dfbdd01000000000000001ac50d47914fdb8a1827b57f623b8aac857ad84a08d77c1ae606d7d24ec46945010000000000000046d41f69d5df314822236500c3d57687ce9b9db3f44f45a4ff91f02e27b3bc2d01000000000000001ab5cd4e35349c94a0516e3d4850cdfb174d23290fdf35e86dfca84d57a3a5c4010000000000000055b0f4f90805b1518d2de726d8767465440f54a5602c92fb0481a9c5eb0b17f80100000000000000000000000466726f6e0902011921a2d414f0a049fd2ec0a5a1fadd6037c94fa953a68b4288a7cd69f29ebcbc0caad2ce0f923890fdc0a3a6cf91026362615de9544092d615fb74acfb1d8dfa8f7b9d61e0baa3e37cf79e5f2e0348c19060423d677b8e7b17c3565173f1adfe3c0966626c2cd982cc30922d17c32bf3e1ddd0b8b785903dd703c6d99ba7f958da056175726101016262a0512462424da899a7a8eeb54deee118b4fa85b0bd931b4f847238baf61c9fe4ca23c2e2b4cfd23e8bcaed3d9fec8cd401d0dac8a3d517fb449946c28084", + "grandpaAuthoritySet": "0x0500000000000000503c0694d473b0203bd9842aabbac7ab7d73205a3ccf97096bc5361ea75dcdd0a4010000000000000045ddfeed53c360c0ad3a10fcf8b09b63f032a81152c73f8971fc71660c22b5cf01000000000000000c06118ac33cdf11a313778fd600e45b12af35c4e96fdfd1f24b622a47f5a6040100000000000000f55a9261d2d5cc475537c27d18158d3a0cae213fa87e305a345df2771a31e12501000000000000004077cc7f31e19faa539e089f8282e03f33318290a32fbad369dac950986dcd430100000000000000c40dd57ef4b2c4408548665620cbfd420f09cdaa2cb6056f77b5b769eddeaed00100000000000000ba5fb07ded8410d39649f8e30a3fb4ffa623c9232b30c14f1f42afb36f4bfb3b0100000000000000e96069b13bef0322f8e3e9bac94e303dafe1a6372736cc57a9f32d0b5231809501000000000000005201d27eb236bf32fa024f5c1c094513f084192e756a7511a3649db3b76ba2c30100000000000000600d1e6db2964a84b13569c2a437d8eab4888768b57b73bebca36cc214e836b801000000000000002c9f738dc1d390c4d9fdb8cdf030441f403ec3ce2d55049d96d6650604f9a07a010000000000000020f5d60bf0f880aa1624fb77895d6ba9c55e6e712c5072b7892595481acb054301000000000000003d362b97f2dbd334ee880489b8bf098478e2b4f25c7fe1db30ae0770d55748750100000000000000f3e7b8a7cb4e4916c6af512c74954c7f71708bfb6172e3ac898cbd87e94d8669010000000000000042c228a2ae6b5f9a587d5bd9fae23371c61bfc5030f2f75bd353828ee774ae9d0100000000000000aef7e3f9920dcfdcf8d84b4df5142cbcf82540a6720bbda8d08b6845b7f81ac301000000000000001bbdc4187afeaf58b851fd56b06ca49b522ccdeed5e727ee10415c03eeb12fd101000000000000005e1ac1aefc93081ca4973e8ea6a90eed26e6bab14e7f1bf4cbc400283533eda70100000000000000a26547efb9369cda4ac126d036e0ecdb08cc01feb095367cd120e3fbda85b2750100000000000000480a24a810ae53434a1db22b3ccb1644bb9c17295c742f96b53534de697926930100000000000000" } } diff --git a/node/src/chain_spec/mod.rs b/node/src/chain_spec/mod.rs index 85ca78d353..b6398f900d 100644 --- a/node/src/chain_spec/mod.rs +++ b/node/src/chain_spec/mod.rs @@ -31,8 +31,20 @@ pub struct Extensions { pub fork_blocks: sc_client_api::ForkBlocks, /// Known bad block hashes. pub bad_blocks: sc_client_api::BadBlocks, + /// A trusted finalized checkpoint for bootstrapping warp sync. + pub grandpa_warp_sync_checkpoint: GrandpaWarpSyncCheckpointExtension, } +/// A trusted GRANDPA authority checkpoint used to verify warp proofs. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GrandpaWarpSyncCheckpoint { + pub finalized_block_header: String, + pub grandpa_authority_set: String, +} + +pub type GrandpaWarpSyncCheckpointExtension = Option; + /// Specialized `ChainSpec`. This is a specialization of the general Substrate ChainSpec type. pub type ChainSpec = sc_service::GenericChainSpec; diff --git a/node/src/cli.rs b/node/src/cli.rs index a35ea86029..834d03bd9a 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -43,6 +43,13 @@ pub struct Cli { /// For `build-patched-spec`, the implicit default is `skip` unless this flag is explicitly set. #[arg(long, value_enum, default_value_t = HistoryBackfill::Keep)] pub history_backfill: HistoryBackfill, + + /// Expose `grandpa_genWarpSyncCheckpoint` for generating a trusted warp-sync checkpoint. + /// + /// The response is compact but requires retained GRANDPA transition history. Public endpoints + /// should apply their normal RPC rate limits. + #[arg(long, default_value_t = false)] + pub enable_warp_sync_checkpoint_rpc: bool, } #[allow(clippy::large_enum_variant)] diff --git a/node/src/command.rs b/node/src/command.rs index fd3a122ec5..0ce741cd90 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -291,6 +291,7 @@ fn start_babe_service( cli.sealing, None, skip_history_backfill, + cli.enable_warp_sync_checkpoint_rpc, ) .await }) { @@ -346,6 +347,7 @@ fn start_aura_service( cli.sealing, Some(custom_service_signal_clone), skip_history_backfill, + cli.enable_warp_sync_checkpoint_rpc, ) .await }) { diff --git a/node/src/service.rs b/node/src/service.rs index 09c07f4dbb..00456bdd09 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -1,6 +1,7 @@ //! Service and ServiceFactory implementation. Specialized wrapper over substrate service. mod grandpa_warp_sync; +mod warp_sync_checkpoint; use crate::consensus::ConsensusMechanism; use futures::{FutureExt, StreamExt as _, channel::mpsc}; @@ -50,6 +51,32 @@ pub type FullSelectChain = sc_consensus::LongestChain; pub type GrandpaBlockImport = sc_consensus_grandpa::GrandpaBlockImport; type GrandpaLinkHalf = sc_consensus_grandpa::LinkHalf; + +fn warp_sync_provider( + genesis_hash: H256, + chain_type: sc_chain_spec::ChainType, + backend: Arc, + authority_set: sc_consensus_grandpa::SharedAuthoritySet>, + trusted_checkpoint: Option>, +) -> WarpSyncConfig { + let warp_sync_config = grandpa_warp_sync::config(genesis_hash, chain_type); + log::warn!("{}", warp_sync_config.log_message()); + let initial_set_id = warp_sync_config.one_time_initial_set_id(trusted_checkpoint.is_some()); + let inner = sc_consensus_grandpa::warp_proof::NetworkProvider::new( + backend, + authority_set, + warp_sync_config.into_hard_forks(trusted_checkpoint), + ); + let provider: Arc> = match initial_set_id { + Some(initial_set_id) => Arc::new(grandpa_warp_sync::InitialSetIdProvider::new( + inner, + initial_set_id, + )), + None => Arc::new(inner), + }; + + WarpSyncConfig::WithProvider(provider) +} #[allow(clippy::upper_case_acronyms)] pub type BIQ<'a> = Box< dyn FnOnce( @@ -264,6 +291,7 @@ pub async fn new_full( sealing: Option, custom_service_signal: Option>, skip_history_backfill: bool, + enable_warp_sync_checkpoint_rpc: bool, ) -> Result where NumberFor: BlockNumberOps, @@ -322,24 +350,24 @@ where None } else { net_config.add_notification_protocol(grandpa_protocol_config); - let warp_sync_config = - grandpa_warp_sync::config(genesis_hash, config.chain_spec.chain_type()); - log::warn!("{}", warp_sync_config.log_message()); - let initial_set_id = warp_sync_config.one_time_initial_set_id(); - let inner = sc_consensus_grandpa::warp_proof::NetworkProvider::new( + let trusted_checkpoint = + warp_sync_checkpoint::trusted_checkpoint(config.chain_spec.as_ref())?; + if let Some(checkpoint) = trusted_checkpoint.as_ref() { + log::info!( + target: LOG_TARGET, + "Using trusted GRANDPA authority checkpoint at #{} ({:?}), set ID {}", + checkpoint.block.1, + checkpoint.block.0, + checkpoint.set_id, + ); + } + Some(warp_sync_provider( + genesis_hash, + config.chain_spec.chain_type(), backend.clone(), grandpa_link.shared_authority_set().clone(), - warp_sync_config.into_hard_forks(), - ); - let warp_sync: Arc> = match initial_set_id { - Some(initial_set_id) => Arc::new(grandpa_warp_sync::InitialSetIdProvider::new( - inner, - initial_set_id, - )), - None => Arc::new(inner), - }; - - Some(WarpSyncConfig::WithProvider(warp_sync)) + trusted_checkpoint, + )) }; let (network, system_rpc_tx, tx_handler_controller, sync_service) = @@ -462,11 +490,18 @@ where async move { CM::pending_create_inherent_data_providers(slot_duration, keystore) } }; - let rpc_methods = consensus_mechanism.rpc_methods( + let mut rpc_methods = consensus_mechanism.rpc_methods( client.clone(), keystore_container.keystore(), select_chain.clone(), )?; + if enable_warp_sync_checkpoint_rpc { + rpc_methods.push(warp_sync_checkpoint::rpc_methods( + config.chain_spec.cloned_box(), + client.clone(), + grandpa_link.shared_authority_set().clone(), + )?); + } Box::new(move |subscription_task_executor| { let eth_deps = crate::rpc::EthDeps { client: client.clone(), @@ -669,6 +704,7 @@ pub async fn build_full( sealing: Option, custom_service_signal: Option>, skip_history_backfill: bool, + enable_warp_sync_checkpoint_rpc: bool, ) -> Result { match config.network.network_backend { sc_network::config::NetworkBackendType::Libp2p => { @@ -678,6 +714,7 @@ pub async fn build_full( sealing, custom_service_signal, skip_history_backfill, + enable_warp_sync_checkpoint_rpc, ) .await } @@ -688,6 +725,7 @@ pub async fn build_full( sealing, custom_service_signal, skip_history_backfill, + enable_warp_sync_checkpoint_rpc, ) .await } diff --git a/node/src/service/grandpa_warp_sync.rs b/node/src/service/grandpa_warp_sync.rs index dc32e7488a..059ec322f4 100644 --- a/node/src/service/grandpa_warp_sync.rs +++ b/node/src/service/grandpa_warp_sync.rs @@ -49,22 +49,35 @@ impl Config { } } - pub(super) fn one_time_initial_set_id(&self) -> Option { + pub(super) fn one_time_initial_set_id(&self, has_trusted_checkpoint: bool) -> Option { match self { Self::OneTimeInitialSetId(set_id) => Some(*set_id), + // A checkpoint requires the hard-fork mode below, so preserve the initial offset in + // the outer verifier instead of losing it when `ReinitializeSetId` is replaced. + Self::InitialSetId(set_id) if has_trusted_checkpoint => Some(*set_id), Self::TestnetCheckpoints(_) | Self::InitialSetId(_) => None, } } - pub(super) fn into_hard_forks(self) -> HardForks { + pub(super) fn into_hard_forks( + self, + trusted_checkpoint: Option>, + ) -> HardForks { match self { - Self::TestnetCheckpoints(checkpoints) => { + Self::TestnetCheckpoints(mut checkpoints) => { + checkpoints.extend(trusted_checkpoint); HardForks::new_hard_forked_authorities(checkpoints) } // Keep the provider in reinitialized-set mode so a completed proof does not replace // its shared authority set. The outer provider supplies the actual one-time offset. - Self::OneTimeInitialSetId(_) => HardForks::new_initial_set_id(0), - Self::InitialSetId(set_id) => HardForks::new_initial_set_id(set_id), + Self::OneTimeInitialSetId(_) => trusted_checkpoint.map_or_else( + || HardForks::new_initial_set_id(0), + |checkpoint| HardForks::new_hard_forked_authorities(vec![checkpoint]), + ), + Self::InitialSetId(set_id) => trusted_checkpoint.map_or_else( + || HardForks::new_initial_set_id(set_id), + |checkpoint| HardForks::new_hard_forked_authorities(vec![checkpoint]), + ), } } } @@ -310,4 +323,11 @@ mod tests { assert!(matches!(second, VerificationResult::Partial(5, _, _))); assert_eq!(provider.inner.set_id.load(Ordering::Relaxed), 4); } + + #[test] + fn a_trusted_checkpoint_preserves_generic_initial_set_offset() { + let config = Config::InitialSetId(3); + assert_eq!(config.one_time_initial_set_id(false), None); + assert_eq!(config.one_time_initial_set_id(true), Some(3)); + } } diff --git a/node/src/service/warp_sync_checkpoint.rs b/node/src/service/warp_sync_checkpoint.rs new file mode 100644 index 0000000000..476ecc23d2 --- /dev/null +++ b/node/src/service/warp_sync_checkpoint.rs @@ -0,0 +1,410 @@ +use crate::chain_spec::{GrandpaWarpSyncCheckpoint, GrandpaWarpSyncCheckpointExtension}; +use crate::client::FullClient; +use jsonrpsee::RpcModule; +use jsonrpsee::types::ErrorObjectOwned; +use node_subtensor_runtime::opaque::{Block, Header}; +use sc_client_api::{BlockBackend, HeaderBackend}; +use sc_consensus_grandpa::{AuthoritySetChanges, AuthoritySetHardFork, SharedAuthoritySet}; +use sc_service::error::Error as ServiceError; +use serde::Serialize; +use sp_api::ProvideRuntimeApi; +use sp_consensus_grandpa::{AuthorityList, GRANDPA_ENGINE_ID, GrandpaApi, SetId}; +use sp_runtime::codec::{DecodeAll, Encode}; +use sp_runtime::traits::Header as _; +use std::sync::Arc; + +const FINALIZED_BLOCK_HEADER: &str = "finalizedBlockHeader"; +const GRANDPA_AUTHORITY_SET: &str = "grandpaAuthoritySet"; + +/// Read and validate a historical GRANDPA signing checkpoint from the chain-spec extension. +pub(super) fn trusted_checkpoint( + chain_spec: &dyn sc_chain_spec::ChainSpec, +) -> Result>, ServiceError> { + let Some(checkpoint) = + sc_chain_spec::get_extension::(chain_spec.extensions()) + else { + return Ok(None); + }; + let Some(checkpoint) = checkpoint else { + return Ok(None); + }; + + let header = decode_finalized_header(checkpoint)?; + validate_transition_header(&header)?; + let (set_id, authorities) = decode_authority_set(checkpoint)?; + validate_authorities(&authorities)?; + + Ok(Some(AuthoritySetHardFork { + set_id, + block: (header.hash(), *header.number()), + authorities, + last_finalized: None, + })) +} + +struct WarpSyncCheckpointRpcContext { + chain_spec: Box, + client: Arc, + authority_set: SharedAuthoritySet, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct GeneratedGrandpaWarpSyncCheckpoint { + chain_spec_id: String, + genesis_hash: sp_core::H256, + grandpa_warp_sync_checkpoint: GrandpaWarpSyncCheckpoint, +} + +/// Expose a GRANDPA-only generator for a historical signing checkpoint. +pub(super) fn rpc_methods( + chain_spec: Box, + client: Arc, + authority_set: SharedAuthoritySet, +) -> Result { + let mut module = RpcModule::new(WarpSyncCheckpointRpcContext { + chain_spec, + client, + authority_set, + }); + module + .register_method("grandpa_genWarpSyncCheckpoint", |_, context, _| { + generate_warp_sync_checkpoint(context).map_err(internal_rpc_error) + }) + .map_err(|error| ServiceError::Other(error.to_string()))?; + Ok(module.into()) +} + +fn generate_warp_sync_checkpoint( + context: &WarpSyncCheckpointRpcContext, +) -> Result { + let changes = authority_change_records(&context.authority_set.authority_set_changes())?; + let (set_id, block_number) = changes.iter().last().copied().ok_or_else(|| { + ServiceError::Other("GRANDPA has no finalized authority transition".into()) + })?; + + let transition_hash = context + .client + .block_hash(block_number) + .map_err(|error| ServiceError::Other(error.to_string()))? + .ok_or_else(|| { + ServiceError::Other(format!( + "authority transition block #{block_number} is missing from the local database" + )) + })?; + let header = context + .client + .header(transition_hash) + .map_err(|error| ServiceError::Other(error.to_string()))? + .ok_or_else(|| { + ServiceError::Other(format!( + "authority transition header {transition_hash:?} is missing from the local database" + )) + })?; + if sc_consensus_grandpa::find_scheduled_change::(&header).is_none() { + return Err(ServiceError::Other(format!( + "authority transition at #{block_number} is not a scheduled GRANDPA change" + ))); + } + let has_grandpa_justification = context + .client + .justifications(transition_hash) + .map_err(|error| ServiceError::Other(error.to_string()))? + .and_then(|justifications| justifications.into_justification(GRANDPA_ENGINE_ID)) + .is_some(); + if !has_grandpa_justification { + return Err(ServiceError::Other(format!( + "authority transition at #{block_number} has no retained GRANDPA justification" + ))); + } + + let authorities = context + .client + .runtime_api() + .grandpa_authorities(*header.parent_hash()) + .map_err(|error| ServiceError::Other(error.to_string()))?; + validate_authorities(&authorities)?; + + let current_set_id = context.authority_set.set_id(); + if set_id.checked_add(1) != Some(current_set_id) { + return Err(ServiceError::Other(format!( + "latest recorded GRANDPA transition ends set {set_id}, but the current set is {current_set_id}" + ))); + } + + let checkpoint = GrandpaWarpSyncCheckpoint { + finalized_block_header: format!("0x{}", hex::encode(header.encode())), + grandpa_authority_set: format!("0x{}", hex::encode((set_id, authorities).encode())), + }; + + Ok(GeneratedGrandpaWarpSyncCheckpoint { + chain_spec_id: context.chain_spec.id().into(), + genesis_hash: context.client.info().genesis_hash, + grandpa_warp_sync_checkpoint: checkpoint, + }) +} + +fn authority_change_records( + changes: &AuthoritySetChanges, +) -> Result, ServiceError> { + // `AuthoritySetChanges` is a SCALE newtype around this vector, but its public iterator + // deliberately rejects histories whose first retained set is non-zero. Finney legitimately + // begins at a corrected non-zero set ID, so decode the complete retained record here. + Vec::<(SetId, u32)>::decode_all(&mut changes.encode().as_slice()) + .map_err(|error| ServiceError::Other(error.to_string())) +} + +fn decode_authority_set( + checkpoint: &GrandpaWarpSyncCheckpoint, +) -> Result<(SetId, AuthorityList), ServiceError> { + let encoded = &checkpoint.grandpa_authority_set; + let encoded = encoded.strip_prefix("0x").ok_or_else(|| { + ServiceError::Other(format!( + "grandpaWarpSyncCheckpoint.{GRANDPA_AUTHORITY_SET} must start with 0x" + )) + })?; + let bytes = hex::decode(encoded).map_err(|error| { + ServiceError::Other(format!( + "invalid grandpaWarpSyncCheckpoint.{GRANDPA_AUTHORITY_SET} hex: {error}" + )) + })?; + + <(SetId, AuthorityList)>::decode_all(&mut bytes.as_slice()).map_err(|error| { + ServiceError::Other(format!( + "invalid grandpaWarpSyncCheckpoint.{GRANDPA_AUTHORITY_SET}: {error}" + )) + }) +} + +fn validate_authorities(authorities: &AuthorityList) -> Result<(), ServiceError> { + if authorities.is_empty() || authorities.iter().any(|(_, weight)| *weight == 0) { + return Err(ServiceError::Other( + "trusted GRANDPA authority set must be non-empty with non-zero weights".into(), + )); + } + Ok(()) +} + +fn validate_transition_header(header: &Header) -> Result<(), ServiceError> { + if sc_consensus_grandpa::find_scheduled_change::(header).is_none() { + return Err(ServiceError::Other( + "trusted GRANDPA checkpoint header must contain a scheduled authority change".into(), + )); + } + Ok(()) +} + +fn internal_rpc_error(error: ServiceError) -> ErrorObjectOwned { + ErrorObjectOwned::owned(-32603, error.to_string(), None::<()>) +} + +fn decode_finalized_header(checkpoint: &GrandpaWarpSyncCheckpoint) -> Result { + let encoded_header = &checkpoint.finalized_block_header; + let encoded_header = encoded_header.strip_prefix("0x").ok_or_else(|| { + ServiceError::Other(format!( + "grandpaWarpSyncCheckpoint.{FINALIZED_BLOCK_HEADER} must start with 0x" + )) + })?; + let bytes = hex::decode(encoded_header).map_err(|error| { + ServiceError::Other(format!( + "invalid grandpaWarpSyncCheckpoint.{FINALIZED_BLOCK_HEADER} hex: {error}" + )) + })?; + + Header::decode_all(&mut bytes.as_slice()).map_err(|error| { + ServiceError::Other(format!( + "invalid grandpaWarpSyncCheckpoint.{FINALIZED_BLOCK_HEADER} header: {error}" + )) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use sp_core::H256; + use sp_runtime::codec::Encode; + + #[test] + fn decodes_trusted_checkpoint_header() { + let header = Header::new( + 42, + H256::repeat_byte(1), + H256::repeat_byte(2), + H256::repeat_byte(3), + Default::default(), + ); + let state = GrandpaWarpSyncCheckpoint { + finalized_block_header: format!("0x{}", hex::encode(header.encode())), + grandpa_authority_set: "0x".into(), + }; + + let Ok(decoded_header) = decode_finalized_header(&state) else { + panic!("valid checkpoint header should decode"); + }; + assert_eq!(decoded_header, header); + } + + #[test] + fn rejects_checkpoint_header_without_scheduled_change() { + let header = Header::new( + 42, + H256::repeat_byte(1), + H256::repeat_byte(2), + H256::repeat_byte(3), + Default::default(), + ); + + assert!(validate_transition_header(&header).is_err()); + } + + #[test] + fn generated_checkpoint_response_is_compact() { + let response = GeneratedGrandpaWarpSyncCheckpoint { + chain_spec_id: "bittensor".into(), + genesis_hash: H256::repeat_byte(4), + grandpa_warp_sync_checkpoint: GrandpaWarpSyncCheckpoint { + finalized_block_header: "0x01".into(), + grandpa_authority_set: "0x02".into(), + }, + }; + let Ok(value) = serde_json::to_value(response) else { + panic!("generated checkpoint response should serialize"); + }; + + assert_eq!(value.as_object().map(|object| object.len()), Some(3)); + assert_eq!( + value.get("chainSpecId"), + Some(&serde_json::json!("bittensor")), + ); + assert_eq!( + value.get("genesisHash"), + Some(&serde_json::json!(H256::repeat_byte(4))), + ); + assert!( + value + .get("grandpaWarpSyncCheckpoint") + .is_some_and(serde_json::Value::is_object) + ); + } + + #[test] + fn decodes_trusted_checkpoint_authorities() { + let authorities = vec![( + sp_consensus_grandpa::AuthorityId::from(sp_core::ed25519::Public::from_raw([7; 32])), + 1, + )]; + let state = GrandpaWarpSyncCheckpoint { + finalized_block_header: "0x".into(), + grandpa_authority_set: format!( + "0x{}", + hex::encode((6_u64, authorities.clone()).encode()), + ), + }; + + let Ok(decoded_authorities) = decode_authority_set(&state) else { + panic!("valid checkpoint authority set should decode"); + }; + assert_eq!(decoded_authorities, (6, authorities)); + } + + #[test] + fn reads_authority_history_with_a_non_zero_initial_set() { + let changes = AuthoritySetChanges::from(vec![(3, 10), (4, 20), (5, 30)]); + let Ok(records) = authority_change_records(&changes) else { + panic!("valid authority history should decode"); + }; + assert_eq!(records, vec![(3, 10), (4, 20), (5, 30)]); + } + + #[test] + fn finney_chain_spec_contains_the_verified_transition_checkpoint() { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../chainspecs/raw_spec_finney.json"); + let Ok(spec) = crate::chain_spec::ChainSpec::from_json_file(path) else { + panic!("Finney chain spec should load"); + }; + let Ok(Some(checkpoint)) = trusted_checkpoint(&spec) else { + panic!("Finney chain spec should contain a valid GRANDPA checkpoint"); + }; + + assert_eq!(checkpoint.set_id, 5); + assert_eq!(checkpoint.block.1, 8_867_448); + assert_eq!( + checkpoint.block.0, + H256::from(hex_literal::hex!( + "511948e96e1d479d0a92d89bb976638780f2c65a93a5d5be710f22ee15c60200" + )), + ); + assert_eq!(checkpoint.authorities.len(), 20); + } + + #[test] + fn rejects_missing_checkpoint_fields() { + let Err(error) = serde_json::from_value::(serde_json::json!({ + GRANDPA_AUTHORITY_SET: "0x" + })) else { + panic!("checkpoint without a finalized header should be rejected"); + }; + assert!(error.to_string().contains(FINALIZED_BLOCK_HEADER)); + + let Err(error) = serde_json::from_value::(serde_json::json!({ + FINALIZED_BLOCK_HEADER: "0x" + })) else { + panic!("checkpoint without an authority set should be rejected"); + }; + assert!(error.to_string().contains(GRANDPA_AUTHORITY_SET)); + } + + #[test] + fn rejects_non_hex_header() { + let state = GrandpaWarpSyncCheckpoint { + finalized_block_header: "not-hex".into(), + grandpa_authority_set: "0x".into(), + }; + assert!(decode_finalized_header(&state).is_err()); + + let state = GrandpaWarpSyncCheckpoint { + finalized_block_header: "0xzz".into(), + grandpa_authority_set: "0x".into(), + }; + assert!(decode_finalized_header(&state).is_err()); + } + + #[test] + fn rejects_trailing_scale_data() { + let header = Header::new( + 42, + H256::repeat_byte(1), + H256::repeat_byte(2), + H256::repeat_byte(3), + Default::default(), + ); + let mut encoded = header.encode(); + encoded.push(0); + let state = GrandpaWarpSyncCheckpoint { + finalized_block_header: format!("0x{}", hex::encode(encoded)), + grandpa_authority_set: "0x".into(), + }; + + assert!(decode_finalized_header(&state).is_err()); + + let mut encoded_authorities = (6_u64, AuthorityList::new()).encode(); + encoded_authorities.push(0); + let state = GrandpaWarpSyncCheckpoint { + finalized_block_header: "0x".into(), + grandpa_authority_set: format!("0x{}", hex::encode(encoded_authorities)), + }; + assert!(decode_authority_set(&state).is_err()); + } + + #[test] + fn rejects_invalid_authority_lists() { + assert!(validate_authorities(&AuthorityList::new()).is_err()); + + let authorities = vec![( + sp_consensus_grandpa::AuthorityId::from(sp_core::ed25519::Public::from_raw([7; 32])), + 0, + )]; + assert!(validate_authorities(&authorities).is_err()); + } +} diff --git a/scripts/build_all_chainspecs.sh b/scripts/build_all_chainspecs.sh index 1e291b47f1..1c1be5b211 100755 --- a/scripts/build_all_chainspecs.sh +++ b/scripts/build_all_chainspecs.sh @@ -5,7 +5,8 @@ # the old chain specs and insert them into the new chain specs to ensure there # are no genesis mismatch issues. -# This script updates the chain spec files keeping the genesis unchanged. +# This script updates the chain spec files while keeping genesis and trusted GRANDPA warp-sync +# checkpoints unchanged. set -e @@ -20,16 +21,21 @@ save_genesis() { jq -r ".genesis" "$1" >"$2" } +save_grandpa_warp_sync_checkpoint() { + jq ".grandpaWarpSyncCheckpoint" "$1" >"$2" +} + buildspec() { local chain="$1" shift ./target/debug/node-subtensor build-spec --chain "$chain" --disable-default-bootnode "$@" } -# Update genesis in new chainspecs using the extracted genesis data from the -# temporary files -update_genesis() { - jq --slurpfile genesis "$1" '.genesis = $genesis[0]' "$2" >"$3" +# Restore fields that are sourced from the live chain rather than the static config builders. +restore_preserved_fields() { + jq --slurpfile genesis "$1" --slurpfile checkpoint "$2" \ + '.genesis = $genesis[0] | .grandpaWarpSyncCheckpoint = $checkpoint[0]' \ + "$3" >"$4" } update_spec() { @@ -39,6 +45,8 @@ update_spec() { raw_genesis_temp=$(mktemp) plain_genesis_temp=$(mktemp) + raw_warp_sync_checkpoint_temp=$(mktemp) + plain_warp_sync_checkpoint_temp=$(mktemp) raw_spec_temp=$(mktemp) plain_spec_temp=$(mktemp) @@ -46,6 +54,8 @@ update_spec() { save_genesis "$raw_path" "$raw_genesis_temp" save_genesis "$plain_path" "$plain_genesis_temp" + save_grandpa_warp_sync_checkpoint "$raw_path" "$raw_warp_sync_checkpoint_temp" + save_grandpa_warp_sync_checkpoint "$plain_path" "$plain_warp_sync_checkpoint_temp" echo "*** Building new chainspec for '$chain'..." @@ -55,12 +65,15 @@ update_spec() { echo "*** Restoring genesis in '$chain'..." - update_genesis "$raw_genesis_temp" "$raw_spec_temp" "$raw_path" - update_genesis "$plain_genesis_temp" "$plain_spec_temp" "$plain_path" + restore_preserved_fields "$raw_genesis_temp" "$raw_warp_sync_checkpoint_temp" \ + "$raw_spec_temp" "$raw_path" + restore_preserved_fields "$plain_genesis_temp" "$plain_warp_sync_checkpoint_temp" \ + "$plain_spec_temp" "$plain_path" # cleanup - rm -f "$raw_genesis_temp" "$plain_genesis_temp" "$raw_spec_temp" \ - "$plain_spec_temp" + rm -f "$raw_genesis_temp" "$plain_genesis_temp" \ + "$raw_warp_sync_checkpoint_temp" "$plain_warp_sync_checkpoint_temp" \ + "$raw_spec_temp" "$plain_spec_temp" } # SCRIPT