From 74c10cefb8bb8bef203fb7ea9902f026690b1fe1 Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Sat, 28 May 2022 04:53:08 +0300 Subject: [PATCH 1/2] Replace non-canonical signatures with canonical for local challenge and PoR --- Cargo.lock | 1 + crates/pallet-subspace/src/lib.rs | 32 ++--- crates/pallet-subspace/src/mock.rs | 33 +++--- crates/pallet-subspace/src/tests.rs | 2 +- crates/sc-consensus-subspace-rpc/src/lib.rs | 35 ++---- .../sc-consensus-subspace/src/aux_schema.rs | 7 +- crates/sc-consensus-subspace/src/lib.rs | 31 ++++- .../sc-consensus-subspace/src/slot_worker.rs | 20 ++-- crates/sc-consensus-subspace/src/tests.rs | 47 ++++---- crates/sp-consensus-subspace/src/digests.rs | 15 +-- crates/sp-consensus-subspace/src/lib.rs | 32 +++-- .../sp-consensus-subspace/src/verification.rs | 60 +++++----- crates/subspace-core-primitives/src/lib.rs | 104 +++++++--------- .../bin/subspace-farmer/bench_rpc_client.rs | 4 +- crates/subspace-farmer/src/farming.rs | 33 ++---- crates/subspace-farmer/src/identity.rs | 21 ++-- crates/subspace-farmer/src/mock_rpc_client.rs | 11 +- crates/subspace-farmer/src/node_rpc_client.rs | 4 +- crates/subspace-farmer/src/rpc_client.rs | 8 +- crates/subspace-rpc-primitives/src/lib.rs | 8 +- crates/subspace-runtime/src/lib.rs | 16 ++- crates/subspace-solving/Cargo.toml | 2 + crates/subspace-solving/src/lib.rs | 111 ++++++++++++++++-- test/subspace-test-client/src/lib.rs | 23 ++-- test/subspace-test-runtime/src/lib.rs | 16 ++- 25 files changed, 390 insertions(+), 286 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27561e3cde601..ff0dd8a6deb47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8361,6 +8361,7 @@ dependencies = [ name = "subspace-solving" version = "0.1.0" dependencies = [ + "merlin", "num-traits", "num_cpus", "rand 0.8.5", diff --git a/crates/pallet-subspace/src/lib.rs b/crates/pallet-subspace/src/lib.rs index 997b84aa8f729..4525ec4ea0709 100644 --- a/crates/pallet-subspace/src/lib.rs +++ b/crates/pallet-subspace/src/lib.rs @@ -45,8 +45,9 @@ use sp_consensus_subspace::offence::{OffenceDetails, OffenceError, OnOffenceHand use sp_consensus_subspace::verification::{ PieceCheckParams, VerificationError, VerifySolutionParams, }; -use sp_consensus_subspace::{verification, EquivocationProof, FarmerPublicKey, SignedVote, Vote}; -use sp_io::hashing; +use sp_consensus_subspace::{ + derive_randomness, verification, EquivocationProof, FarmerPublicKey, SignedVote, Vote, +}; use sp_runtime::generic::DigestItem; use sp_runtime::traits::{ BlockNumberProvider, Hash, Header as HeaderT, One, SaturatedConversion, Saturating, Zero, @@ -59,12 +60,10 @@ use sp_runtime::DispatchError; use sp_std::collections::btree_map::BTreeMap; use sp_std::prelude::*; use subspace_core_primitives::{ - crypto, Randomness, RootBlock, Salt, Signature, PIECE_SIZE, RANDOMNESS_LENGTH, SALT_SIZE, + crypto, Randomness, RootBlock, Salt, PIECE_SIZE, RANDOMNESS_LENGTH, SALT_SIZE, }; -use subspace_solving::{REWARD_SIGNING_CONTEXT, SOLUTION_SIGNING_CONTEXT}; +use subspace_solving::REWARD_SIGNING_CONTEXT; -const GLOBAL_CHALLENGE_HASHING_PREFIX: &[u8] = b"global_challenge"; -const GLOBAL_CHALLENGE_HASHING_PREFIX_LEN: usize = GLOBAL_CHALLENGE_HASHING_PREFIX.len(); const SALT_HASHING_PREFIX: &[u8] = b"salt"; const SALT_HASHING_PREFIX_LEN: usize = SALT_HASHING_PREFIX.len(); @@ -789,7 +788,7 @@ impl Pallet { CurrentSlot::::put(pre_digest.slot); { - let key = (pre_digest.solution.public_key, pre_digest.slot); + let key = (pre_digest.solution.public_key.clone(), pre_digest.slot); if ParentBlockVoters::::get().contains_key(&key) { let (public_key, slot) = key; @@ -857,16 +856,13 @@ impl Pallet { } // Extract PoR randomness from pre-digest. - let por_randomness: Randomness = hashing::blake2_256(&{ - let mut input = - [0u8; GLOBAL_CHALLENGE_HASHING_PREFIX_LEN + mem::size_of::()]; - input[..GLOBAL_CHALLENGE_HASHING_PREFIX_LEN] - .copy_from_slice(GLOBAL_CHALLENGE_HASHING_PREFIX); - input[GLOBAL_CHALLENGE_HASHING_PREFIX_LEN..] - .copy_from_slice(&pre_digest.solution.signature); - - input - }); + // Tag signature is validated by the client and is always valid here. + let por_randomness: Randomness = derive_randomness( + &pre_digest.solution.public_key, + pre_digest.solution.tag, + &pre_digest.solution.tag_signature, + ) + .expect("Tag signature is verified by the client and is always valid; qed"); // Store PoR randomness for block duration as it might be useful. PorRandomness::::put(por_randomness); @@ -1414,7 +1410,6 @@ fn check_vote( max_plot_size: vote_verification_data.max_plot_size, total_pieces: vote_verification_data.total_pieces, }), - solution_signing_context: &schnorrkel::signing_context(SOLUTION_SIGNING_CONTEXT), }, ) { debug!( @@ -1571,7 +1566,6 @@ impl subspace_runtime_primitives::FindVotingRewardAddresses frame_support::traits::Randomness for Pallet { fn random(subject: &[u8]) -> (T::Hash, T::BlockNumber) { let mut subject = subject.to_vec(); - subject.reserve(RANDOMNESS_LENGTH); subject.extend_from_slice( PorRandomness::::get() .expect("PoR randomness is always set in block initialization; qed") diff --git a/crates/pallet-subspace/src/mock.rs b/crates/pallet-subspace/src/mock.rs index 98f88f84f11bd..be51d892b0a38 100644 --- a/crates/pallet-subspace/src/mock.rs +++ b/crates/pallet-subspace/src/mock.rs @@ -40,7 +40,9 @@ use subspace_core_primitives::{ ArchivedBlockProgress, LastArchivedBlock, LocalChallenge, Piece, Randomness, RootBlock, Salt, Sha256Hash, Solution, Tag, PIECE_SIZE, }; -use subspace_solving::{SubspaceCodec, REWARD_SIGNING_CONTEXT, SOLUTION_SIGNING_CONTEXT}; +use subspace_solving::{ + create_tag_signature, derive_local_challenge, SubspaceCodec, REWARD_SIGNING_CONTEXT, +}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -190,7 +192,6 @@ pub fn go_to_block( }; let subspace_codec = SubspaceCodec::new(keypair.public.as_ref()); - let ctx = schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT); let piece_index = 0; let mut encoding = Piece::default(); subspace_codec.encode(&mut encoding, piece_index).unwrap(); @@ -210,8 +211,11 @@ pub fn go_to_block( reward_address, piece_index: 0, encoding, - signature: keypair.sign(ctx.bytes(&tag)).to_bytes().into(), - local_challenge: LocalChallenge::default(), + tag_signature: create_tag_signature(keypair, tag), + local_challenge: LocalChallenge { + output: [0; 32], + proof: [0; 64], + }, tag, }, ); @@ -267,12 +271,10 @@ pub fn generate_equivocation_proof( let current_block = System::block_number(); let current_slot = CurrentSlot::::get(); - let ctx = schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT); let encoding = Piece::default(); let tag: Tag = [(current_block % 8) as u8; 8]; let public_key = FarmerPublicKey::unchecked_from(keypair.public.to_bytes()); - let signature = keypair.sign(ctx.bytes(&tag)).to_bytes(); let make_header = |piece_index, reward_address: ::AccountId| { let parent_hash = System::parent_hash(); @@ -283,8 +285,11 @@ pub fn generate_equivocation_proof( reward_address, piece_index, encoding: encoding.clone(), - signature: signature.into(), - local_challenge: LocalChallenge::default(), + tag_signature: create_tag_signature(keypair, tag), + local_challenge: LocalChallenge { + output: [0; 32], + proof: [0; 64], + }, tag, }, ); @@ -381,15 +386,10 @@ pub fn create_signed_vote( encoding: Piece, reward_address: ::AccountId, ) -> SignedVote::Hash, ::AccountId> { - let solution_signing_context = schnorrkel::signing_context(SOLUTION_SIGNING_CONTEXT); let reward_signing_context = schnorrkel::signing_context(REWARD_SIGNING_CONTEXT); let global_challenge = subspace_solving::derive_global_challenge(global_randomnesses, slot.into()); - let local_challenge = keypair - .sign(solution_signing_context.bytes(&global_challenge)) - .to_bytes() - .into(); let tag = subspace_solving::create_tag(&encoding, salt); @@ -402,11 +402,8 @@ pub fn create_signed_vote( reward_address, piece_index: 0, encoding, - signature: keypair - .sign(solution_signing_context.bytes(&tag)) - .to_bytes() - .into(), - local_challenge, + tag_signature: create_tag_signature(keypair, tag), + local_challenge: derive_local_challenge(keypair, global_challenge), tag, }, }; diff --git a/crates/pallet-subspace/src/tests.rs b/crates/pallet-subspace/src/tests.rs index fb7ff49567ab6..0f0f73ef8c74d 100644 --- a/crates/pallet-subspace/src/tests.rs +++ b/crates/pallet-subspace/src/tests.rs @@ -1108,7 +1108,7 @@ fn vote_invalid_solution_signature() { ); let Vote::V0 { solution, .. } = &mut signed_vote.vote; - solution.signature = rand::random::<[u8; 64]>().into(); + solution.tag_signature.output = rand::random(); // Fix signed vote signature after changed contents signed_vote.signature = FarmerSignature::unchecked_from( diff --git a/crates/sc-consensus-subspace-rpc/src/lib.rs b/crates/sc-consensus-subspace-rpc/src/lib.rs index a608a11541e86..e2eb209401716 100644 --- a/crates/sc-consensus-subspace-rpc/src/lib.rs +++ b/crates/sc-consensus-subspace-rpc/src/lib.rs @@ -47,7 +47,7 @@ use std::time::Duration; use subspace_archiving::archiver::ArchivedSegment; use subspace_core_primitives::Solution; use subspace_rpc_primitives::{ - FarmerMetadata, RewardSignature, RewardSigningInfo, SlotInfo, SolutionResponse, + FarmerMetadata, RewardSignatureResponse, RewardSigningInfo, SlotInfo, SolutionResponse, }; const SOLUTION_TIMEOUT: Duration = Duration::from_secs(2); @@ -80,7 +80,7 @@ pub trait SubspaceRpcApi { fn subscribe_reward_signing(&self); #[method(name = "subspace_submitRewardSignature")] - fn submit_reward_signature(&self, reward_signature: RewardSignature) -> RpcResult<()>; + fn submit_reward_signature(&self, reward_signature: RewardSignatureResponse) -> RpcResult<()>; /// Archived segment subscription #[subscription( @@ -103,7 +103,7 @@ struct SolutionResponseSenders { #[derive(Default)] struct BlockSignatureSenders { current_hash: H256, - senders: Vec>, + senders: Vec>, } #[derive(Default)] @@ -248,35 +248,18 @@ where let forward_solution_fut = async move { if let Ok(solution_response) = response_receiver.await { if let Some(solution) = solution_response.maybe_solution { - let public_key = - match FarmerPublicKey::from_slice(&solution.public_key) { - Ok(public_key) => public_key, - Err(()) => { - warn!( - "Failed to convert public key: {:?}", - solution.public_key - ); - return; - } - }; + let public_key = FarmerPublicKey::from_slice(&solution.public_key) + .expect("Always correct length; qed"); let reward_address = - match FarmerPublicKey::from_slice(&solution.reward_address) { - Ok(public_key) => public_key, - Err(()) => { - warn!( - "Failed to convert reward address: {:?}", - solution.reward_address, - ); - return; - } - }; + FarmerPublicKey::from_slice(&solution.reward_address) + .expect("Always correct length; qed"); let solution = Solution { public_key, reward_address, piece_index: solution.piece_index, encoding: solution.encoding, - signature: solution.signature, + tag_signature: solution.tag_signature, local_challenge: solution.local_challenge, tag: solution.tag, }; @@ -403,7 +386,7 @@ where ); } - fn submit_reward_signature(&self, reward_signature: RewardSignature) -> RpcResult<()> { + fn submit_reward_signature(&self, reward_signature: RewardSignatureResponse) -> RpcResult<()> { let reward_signature_senders = self.reward_signature_senders.clone(); // TODO: This doesn't track what client sent a solution, allowing some clients to send diff --git a/crates/sc-consensus-subspace/src/aux_schema.rs b/crates/sc-consensus-subspace/src/aux_schema.rs index c8f6704d76986..1d6c096337413 100644 --- a/crates/sc-consensus-subspace/src/aux_schema.rs +++ b/crates/sc-consensus-subspace/src/aux_schema.rs @@ -21,7 +21,12 @@ use codec::{Decode, Encode}; use sc_client_api::backend::AuxStore; use sp_blockchain::{Error as ClientError, Result as ClientResult}; -use sp_consensus_subspace::SubspaceBlockWeight; + +/// The cumulative weight of a Subspace block, i.e. sum of block weights starting +/// at this block until the genesis block. +/// +/// The closer solution's tag is to the target, the heavier it is. +type SubspaceBlockWeight = u128; /// The aux storage key used to store the block weight of the given block hash. fn block_weight_key(block_hash: H) -> Vec { diff --git a/crates/sc-consensus-subspace/src/lib.rs b/crates/sc-consensus-subspace/src/lib.rs index 55fbcfbb0e0d8..50da2e22d1f17 100644 --- a/crates/sc-consensus-subspace/src/lib.rs +++ b/crates/sc-consensus-subspace/src/lib.rs @@ -53,6 +53,7 @@ use sc_consensus_slots::{ use sc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_TRACE}; use sc_utils::mpsc::TracingUnboundedSender; use schnorrkel::context::SigningContext; +use schnorrkel::PublicKey; use sp_api::{ApiError, ApiExt, BlockT, HeaderT, NumberFor, ProvideRuntimeApi, TransactionFor}; use sp_block_builder::BlockBuilder as BlockBuilderApi; use sp_blockchain::{Error as ClientError, HeaderBackend, HeaderMetadata, Result as ClientResult}; @@ -80,7 +81,7 @@ use std::pin::Pin; use std::sync::Arc; use subspace_archiving::archiver::ArchivedSegment; use subspace_core_primitives::{BlockNumber, RootBlock, Salt, Sha256Hash, Solution}; -use subspace_solving::{REWARD_SIGNING_CONTEXT, SOLUTION_SIGNING_CONTEXT}; +use subspace_solving::{derive_global_challenge, derive_target, REWARD_SIGNING_CONTEXT}; /// Information about new slot that just arrived #[derive(Debug, Copy, Clone)] @@ -415,7 +416,6 @@ where force_authoring, backoff_authoring_blocks, subspace_link: subspace_link.clone(), - solution_signing_context: schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT), reward_signing_context: schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT), block_proposal_slot_portion, max_block_proposal_slot_portion, @@ -618,7 +618,6 @@ pub struct SubspaceVerifier { select_chain: SelectChain, slot_now: SN, telemetry: Option, - solution_signing_context: SigningContext, reward_signing_context: SigningContext, block: PhantomData, } @@ -776,7 +775,6 @@ where solution_range, salt, piece_check_params: None, - solution_signing_context: &self.solution_signing_context, }, reward_signing_context: &self.reward_signing_context, }, @@ -1135,7 +1133,29 @@ where })? }; - let total_weight = parent_weight + pre_digest.added_weight(); + let added_weight = { + let global_randomness = find_global_randomness_descriptor(&block.header) + .expect("Verification of the header was done before this; qed") + .expect("Verification of the header was done before this; qed") + .global_randomness; + let global_challenge = + derive_global_challenge(&global_randomness, pre_digest.slot.into()); + + // Verification of the local challenge was done before this + let target = u64::from_be_bytes( + derive_target( + &PublicKey::from_bytes(pre_digest.solution.public_key.as_ref()) + .expect("Always correct length; qed"), + global_challenge, + &pre_digest.solution.local_challenge, + ) + .expect("Verification of the local challenge was done before this; qed"), + ); + let tag = u64::from_be_bytes(pre_digest.solution.tag); + + u128::from(u64::MAX - subspace_core_primitives::bidirectional_distance(&target, &tag)) + }; + let total_weight = parent_weight + added_weight; let info = self.client.info(); @@ -1311,7 +1331,6 @@ where slot_now, telemetry, client, - solution_signing_context: schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT), reward_signing_context: schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT), block: PhantomData::default(), }; diff --git a/crates/sc-consensus-subspace/src/slot_worker.rs b/crates/sc-consensus-subspace/src/slot_worker.rs index 2a1250d1515d5..7206e7a37110d 100644 --- a/crates/sc-consensus-subspace/src/slot_worker.rs +++ b/crates/sc-consensus-subspace/src/slot_worker.rs @@ -30,6 +30,7 @@ use sc_consensus_slots::{ use sc_telemetry::TelemetryHandle; use sc_utils::mpsc::tracing_unbounded; use schnorrkel::context::SigningContext; +use schnorrkel::PublicKey; use sp_api::{ApiError, NumberFor, ProvideRuntimeApi, TransactionFor}; use sp_blockchain::{Error as ClientError, HeaderBackend, HeaderMetadata}; use sp_consensus::{BlockOrigin, Environment, Error as ConsensusError, Proposer, SyncOracle}; @@ -45,6 +46,7 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; use subspace_core_primitives::{Randomness, Salt, Solution}; +use subspace_solving::{derive_global_challenge, derive_target}; pub(super) struct SubspaceSlotWorker { pub(super) client: Arc, @@ -55,7 +57,6 @@ pub(super) struct SubspaceSlotWorker { pub(super) force_authoring: bool, pub(super) backoff_authoring_blocks: Option, pub(super) subspace_link: SubspaceLink, - pub(super) solution_signing_context: SigningContext, pub(super) reward_signing_context: SigningContext, pub(super) block_proposal_slot_portion: SlotProportion, pub(super) max_block_proposal_slot_portion: Option, @@ -129,13 +130,11 @@ where extract_solution_ranges_for_block(self.client.as_ref(), &parent_block_id).ok()?; let (salt, next_salt) = extract_salt_for_block(self.client.as_ref(), &parent_block_id).ok()?; + let global_challenge = derive_global_challenge(&global_randomness, slot.into()); let new_slot_info = NewSlotInfo { slot, - global_challenge: subspace_solving::derive_global_challenge( - &global_randomness, - slot.into(), - ), + global_challenge, salt, next_salt, solution_range, @@ -227,17 +226,24 @@ where max_plot_size, total_pieces, }), - solution_signing_context: &self.solution_signing_context, }, ); if let Err(error) = solution_verification_result { warn!(target: "subspace", "Invalid solution received for slot {slot}: {error:?}"); } else { + // Verification of the local challenge was done before this + let target = derive_target( + &PublicKey::from_bytes(solution.public_key.as_ref()) + .expect("Always correct length; qed"), + global_challenge, + &solution.local_challenge, + ) + .expect("Verification of the local challenge was done before this; qed"); // If solution is of high enough quality and block pre-digest wasn't produced yet, // block reward is claimed if maybe_pre_digest.is_none() - && verification::is_within_solution_range(&solution, solution_range) + && verification::is_within_solution_range(target, solution.tag, solution_range) { info!(target: "subspace", "🚜 Claimed block at slot {slot}"); diff --git a/crates/sc-consensus-subspace/src/tests.rs b/crates/sc-consensus-subspace/src/tests.rs index 0e39a61003404..9f21db07d9b9e 100644 --- a/crates/sc-consensus-subspace/src/tests.rs +++ b/crates/sc-consensus-subspace/src/tests.rs @@ -70,8 +70,10 @@ use std::task::Poll; use std::time::Duration; use subspace_archiving::archiver::Archiver; use subspace_core_primitives::objects::BlockObjectMapping; -use subspace_core_primitives::{FlatPieces, LocalChallenge, Piece, Signature, Solution, Tag}; -use subspace_solving::{SubspaceCodec, REWARD_SIGNING_CONTEXT, SOLUTION_SIGNING_CONTEXT}; +use subspace_core_primitives::{FlatPieces, LocalChallenge, Piece, Solution, Tag, TagSignature}; +use subspace_solving::{ + create_tag_signature, derive_local_challenge, SubspaceCodec, REWARD_SIGNING_CONTEXT, +}; use substrate_test_runtime::{Block as TestBlock, Hash}; type TestClient = substrate_test_runtime_client::client::Client< @@ -406,9 +408,6 @@ impl TestNetFactory for SubspaceTestNet { Slot::from_timestamp(*timestamp, SlotDuration::from_millis(6000)) }), telemetry: None, - solution_signing_context: schnorrkel::context::signing_context( - SOLUTION_SIGNING_CONTEXT, - ), reward_signing_context: schnorrkel::context::signing_context( REWARD_SIGNING_CONTEXT, ), @@ -573,7 +572,6 @@ fn run_one_test(mutator: impl Fn(&mut TestHeader, Stage) + Send + Sync + 'static let subspace_farmer = async move { let keypair = Keypair::generate(); let subspace_codec = SubspaceCodec::new(keypair.public.as_ref()); - let ctx = schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT); let (piece_index, mut encoding) = archived_pieces_receiver .await .unwrap() @@ -601,11 +599,11 @@ fn run_one_test(mutator: impl Fn(&mut TestHeader, Stage) + Send + Sync + 'static ), piece_index, encoding: encoding.clone(), - signature: keypair.sign(ctx.bytes(&tag)).to_bytes().into(), - local_challenge: keypair - .sign(ctx.bytes(&new_slot_info.global_challenge)) - .to_bytes() - .into(), + tag_signature: create_tag_signature(&keypair, tag), + local_challenge: derive_local_challenge( + &keypair, + new_slot_info.global_challenge, + ), tag, }) .await; @@ -674,7 +672,7 @@ fn rejects_missing_seals() { fn wrong_consensus_engine_id_rejected() { sp_tracing::try_init_simple(); let keypair = Keypair::generate(); - let ctx = schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT); + let ctx = schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT); let bad_seal = DigestItem::Seal([0; 4], keypair.sign(ctx.bytes(b"")).to_bytes().to_vec()); assert!(CompatibleDigestItem::as_subspace_pre_digest::(&bad_seal).is_none()); assert!(CompatibleDigestItem::as_subspace_seal(&bad_seal).is_none()) @@ -691,7 +689,7 @@ fn malformed_pre_digest_rejected() { fn sig_is_not_pre_digest() { sp_tracing::try_init_simple(); let keypair = Keypair::generate(); - let ctx = schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT); + let ctx = schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT); let bad_seal = DigestItem::subspace_seal(FarmerSignature::unchecked_from( keypair.sign(ctx.bytes(b"")).to_bytes(), )); @@ -710,8 +708,14 @@ pub fn dummy_claim_slot( reward_address: FarmerPublicKey::unchecked_from([0u8; 32]), piece_index: 0, encoding: Piece::default(), - signature: Signature::default(), - local_challenge: LocalChallenge::default(), + tag_signature: TagSignature { + output: [0; 32], + proof: [0; 64], + }, + local_challenge: LocalChallenge { + output: [0; 32], + proof: [0; 64], + }, tag: Tag::default(), }, slot, @@ -753,14 +757,12 @@ fn propose_and_import_block( }); let keypair = Keypair::generate(); - let ctx = schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT); + let ctx = schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT); let (pre_digest, signature) = { let encoding = Piece::default(); let tag: Tag = [0u8; 8]; - let signature = keypair.sign(ctx.bytes(&tag)).to_bytes(); - ( sp_runtime::generic::Digest { logs: vec![DigestItem::subspace_pre_digest(&PreDigest { @@ -770,13 +772,16 @@ fn propose_and_import_block( reward_address: FarmerPublicKey::unchecked_from(keypair.public.to_bytes()), piece_index: 0, encoding, - signature: signature.into(), - local_challenge: LocalChallenge::default(), + tag_signature: create_tag_signature(&keypair, tag), + local_challenge: LocalChallenge { + output: [0; 32], + proof: [0; 64], + }, tag, }, })], }, - signature, + keypair.sign(ctx.bytes(&[])).to_bytes(), ) }; diff --git a/crates/sp-consensus-subspace/src/digests.rs b/crates/sp-consensus-subspace/src/digests.rs index 5153dccb5f66c..92ad6449b368b 100644 --- a/crates/sp-consensus-subspace/src/digests.rs +++ b/crates/sp-consensus-subspace/src/digests.rs @@ -16,9 +16,7 @@ //! Private implementation details of Subspace consensus digests. -use crate::{ - ConsensusLog, FarmerPublicKey, FarmerSignature, SubspaceBlockWeight, SUBSPACE_ENGINE_ID, -}; +use crate::{ConsensusLog, FarmerPublicKey, FarmerSignature, SUBSPACE_ENGINE_ID}; use codec::{Decode, Encode}; use sp_consensus_slots::Slot; use sp_runtime::DigestItem; @@ -34,17 +32,6 @@ pub struct PreDigest { pub solution: Solution, } -impl PreDigest { - /// Returns the weight _added_ by this digest, not the cumulative weight - /// of the chain. - pub fn added_weight(&self) -> SubspaceBlockWeight { - let target = u64::from_be_bytes(self.solution.local_challenge.derive_target()); - let tag = u64::from_be_bytes(self.solution.tag); - - u128::from(u64::MAX - subspace_core_primitives::bidirectional_distance(&target, &tag)) - } -} - /// Information about the global randomness for the block. #[derive(Debug, Decode, Encode, PartialEq, Eq, Clone)] pub struct GlobalRandomnessDescriptor { diff --git a/crates/sp-consensus-subspace/src/lib.rs b/crates/sp-consensus-subspace/src/lib.rs index 65bda5a465b9b..94067a87e0478 100644 --- a/crates/sp-consensus-subspace/src/lib.rs +++ b/crates/sp-consensus-subspace/src/lib.rs @@ -31,6 +31,8 @@ use crate::digests::{ use codec::{Decode, Encode, MaxEncodedLen}; use core::time::Duration; use scale_info::TypeInfo; +use schnorrkel::vrf::VRFOutput; +use schnorrkel::{PublicKey, SignatureResult}; use sp_api::{BlockT, HeaderT}; use sp_consensus_slots::Slot; use sp_core::crypto::KeyTypeId; @@ -38,7 +40,10 @@ use sp_core::H256; use sp_io::hashing; use sp_runtime::{ConsensusEngineId, RuntimeAppPublic}; use sp_std::vec::Vec; -use subspace_core_primitives::{Randomness, RootBlock, Salt, Sha256Hash, Solution}; +use subspace_core_primitives::{ + Randomness, RootBlock, Salt, Sha256Hash, Solution, Tag, TagSignature, +}; +use subspace_solving::create_tag_signature_transcript; /// Key type for Subspace pallet. const KEY_TYPE: KeyTypeId = KeyTypeId(*b"sub_"); @@ -61,15 +66,11 @@ pub type FarmerPublicKey = app::Public; /// The `ConsensusEngineId` of Subspace. const SUBSPACE_ENGINE_ID: ConsensusEngineId = *b"SUB_"; +const RANDOMNESS_CONTEXT: &[u8] = b"subspace_randomness"; + /// An equivocation proof for multiple block authorships on the same slot (i.e. double vote). pub type EquivocationProof
= sp_consensus_slots::EquivocationProof; -/// The cumulative weight of a Subspace block, i.e. sum of block weights starting -/// at this block until the genesis block. -/// -/// The closer solution's tag is to the target, the heavier it is. -pub type SubspaceBlockWeight = u128; - /// An consensus log item for Subspace. #[derive(Debug, Decode, Encode, Clone, PartialEq, Eq)] enum ConsensusLog { @@ -241,6 +242,23 @@ impl Default for SolutionRanges { } } +/// Derive on-chain randomness from tag signature. +/// +/// NOTE: If you are not the signer then you must verify the local challenge before calling this +/// function. +pub fn derive_randomness( + public_key: &FarmerPublicKey, + tag: Tag, + tag_signature: &TagSignature, +) -> SignatureResult { + let in_out = VRFOutput(tag_signature.output).attach_input_hash( + &PublicKey::from_bytes(public_key.as_ref())?, + create_tag_signature_transcript(tag), + )?; + + Ok(in_out.make_bytes(RANDOMNESS_CONTEXT)) +} + /// Subspace salts used for challenges. #[derive(Default, Decode, Encode, MaxEncodedLen, PartialEq, Eq, Clone, Copy, Debug, TypeInfo)] pub struct Salts { diff --git a/crates/sp-consensus-subspace/src/verification.rs b/crates/sp-consensus-subspace/src/verification.rs index 3bff7b2385fcb..e1d43b0290ad8 100644 --- a/crates/sp-consensus-subspace/src/verification.rs +++ b/crates/sp-consensus-subspace/src/verification.rs @@ -23,12 +23,12 @@ use schnorrkel::context::SigningContext; use schnorrkel::{PublicKey, Signature}; use sp_api::HeaderT; use sp_consensus_slots::Slot; -use sp_core::crypto::ByteArray; use sp_runtime::DigestItem; use subspace_archiving::archiver; -use subspace_core_primitives::{PieceIndex, Randomness, Salt, Sha256Hash, Solution}; +use subspace_core_primitives::{PieceIndex, Randomness, Salt, Sha256Hash, Solution, Tag}; use subspace_solving::{ - derive_global_challenge, is_local_challenge_valid, PieceDistance, SubspaceCodec, + derive_global_challenge, derive_target, verify_local_challenge, verify_tag_signature, + PieceDistance, SubspaceCodec, }; /// Errors encountered by the Subspace authorship task. @@ -192,21 +192,11 @@ pub fn check_reward_signature( public_key: &FarmerPublicKey, reward_signing_context: &SigningContext, ) -> Result<(), schnorrkel::SignatureError> { - let public_key = PublicKey::from_bytes(public_key.as_slice())?; + let public_key = PublicKey::from_bytes(public_key.as_ref())?; let signature = Signature::from_bytes(signature)?; public_key.verify(reward_signing_context.bytes(hash), &signature) } -/// Check the solution signature validity. -fn check_solution_signature( - solution: &Solution, - solution_signing_context: &SigningContext, -) -> Result<(), schnorrkel::SignatureError> { - let public_key = PublicKey::from_bytes(solution.public_key.as_slice())?; - let signature = Signature::from_bytes(&solution.signature)?; - public_key.verify(solution_signing_context.bytes(&solution.tag), &signature) -} - /// Check if the tag of a solution's piece is valid. fn check_piece_tag( slot: Slot, @@ -257,16 +247,11 @@ where } /// Returns true if `solution.tag` is within the solution range. -pub fn is_within_solution_range( - solution: &Solution, - solution_range: u64, -) -> bool { - let solution_tag = u64::from_be_bytes(solution.tag); - let target = u64::from_be_bytes(solution.local_challenge.derive_target()); +pub fn is_within_solution_range(target: Tag, tag: Tag, solution_range: u64) -> bool { + let target = u64::from_be_bytes(target); + let tag = u64::from_be_bytes(tag); - let distance = subspace_core_primitives::bidirectional_distance(&target, &solution_tag); - - distance <= solution_range / 2 + subspace_core_primitives::bidirectional_distance(&target, &tag) <= solution_range / 2 } /// Returns true if piece index is within farmer sector @@ -309,8 +294,6 @@ pub struct VerifySolutionParams<'a> { /// /// If `None`, piece validity check will be skipped. pub piece_check_params: Option, - /// Signing context for solution signature - pub solution_signing_context: &'a SigningContext, } /// Solution verification @@ -327,23 +310,38 @@ where solution_range, salt, piece_check_params, - solution_signing_context, } = params; - if let Err(error) = is_local_challenge_valid( + let public_key = + PublicKey::from_bytes(solution.public_key.as_ref()).expect("Always correct length; qed"); + + if let Err(error) = verify_local_challenge( + &public_key, derive_global_challenge(global_randomness, slot.into()), &solution.local_challenge, - solution.public_key.as_ref(), ) { return Err(VerificationError::BadLocalChallenge(slot, error)); } - if !is_within_solution_range(solution, solution_range) { + // Verification of the local challenge was done above + let target = match derive_target( + &public_key, + derive_global_challenge(global_randomness, slot.into()), + &solution.local_challenge, + ) { + Ok(target) => target, + Err(error) => { + return Err(VerificationError::BadLocalChallenge(slot, error)); + } + }; + + if !is_within_solution_range(solution.tag, target, solution_range) { return Err(VerificationError::OutsideOfSolutionRange(slot)); } - check_solution_signature(solution, solution_signing_context) - .map_err(|e| VerificationError::BadSolutionSignature(slot, e))?; + if let Err(error) = verify_tag_signature(solution.tag, &solution.tag_signature, &public_key) { + return Err(VerificationError::BadSolutionSignature(slot, error)); + } check_piece_tag(slot, salt, solution)?; diff --git a/crates/subspace-core-primitives/src/lib.rs b/crates/subspace-core-primitives/src/lib.rs index 40a0ff77dad61..1d59dfb9d8570 100644 --- a/crates/subspace-core-primitives/src/lib.rs +++ b/crates/subspace-core-primitives/src/lib.rs @@ -72,6 +72,10 @@ pub type SlotNumber = u64; /// Length of public key in bytes. pub const PUBLIC_KEY_LENGTH: usize = 32; +const REWARD_SIGNATURE_LENGTH: usize = 64; +const VRF_OUTPUT_LENGTH: usize = 32; +const VRF_PROOF_LENGTH: usize = 64; + /// A Ristretto Schnorr public key as bytes produced by `schnorrkel` crate. #[derive( Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Encode, Decode, TypeInfo, @@ -105,34 +109,26 @@ impl AsRef<[u8]> for PublicKey { } } -const SIGNATURE_LENGTH: usize = 64; - /// A Ristretto Schnorr signature as bytes produced by `schnorrkel` crate. #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Encode, Decode, TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct Signature( - #[cfg_attr(feature = "std", serde(with = "serde_arrays"))] [u8; SIGNATURE_LENGTH], +pub struct RewardSignature( + #[cfg_attr(feature = "std", serde(with = "serde_arrays"))] [u8; REWARD_SIGNATURE_LENGTH], ); -impl Default for Signature { - fn default() -> Self { - Self([0u8; SIGNATURE_LENGTH]) - } -} - -impl From<[u8; SIGNATURE_LENGTH]> for Signature { - fn from(bytes: [u8; SIGNATURE_LENGTH]) -> Self { +impl From<[u8; REWARD_SIGNATURE_LENGTH]> for RewardSignature { + fn from(bytes: [u8; REWARD_SIGNATURE_LENGTH]) -> Self { Self(bytes) } } -impl From for [u8; SIGNATURE_LENGTH] { - fn from(signature: Signature) -> Self { +impl From for [u8; REWARD_SIGNATURE_LENGTH] { + fn from(signature: RewardSignature) -> Self { signature.0 } } -impl Deref for Signature { +impl Deref for RewardSignature { type Target = [u8]; fn deref(&self) -> &Self::Target { @@ -140,58 +136,32 @@ impl Deref for Signature { } } -impl AsRef<[u8]> for Signature { +impl AsRef<[u8]> for RewardSignature { fn as_ref(&self) -> &[u8] { &self.0 } } -/// A Ristretto Schnorr signature as bytes produced by `schnorrkel` crate. +/// VRF signature output and proof as produced by `schnorrkel` crate. #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Encode, Decode, TypeInfo)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct LocalChallenge( - #[cfg_attr(feature = "std", serde(with = "serde_arrays"))] [u8; SIGNATURE_LENGTH], -); - -impl Default for LocalChallenge { - fn default() -> Self { - Self([0u8; SIGNATURE_LENGTH]) - } -} - -impl From<[u8; SIGNATURE_LENGTH]> for LocalChallenge { - fn from(bytes: [u8; SIGNATURE_LENGTH]) -> Self { - Self(bytes) - } -} - -impl From for [u8; SIGNATURE_LENGTH] { - fn from(signature: LocalChallenge) -> Self { - signature.0 - } -} - -impl Deref for LocalChallenge { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.0 - } +pub struct TagSignature { + /// VRF output bytes. + pub output: [u8; VRF_OUTPUT_LENGTH], + /// VRF proof bytes. + #[cfg_attr(feature = "std", serde(with = "serde_arrays"))] + pub proof: [u8; VRF_PROOF_LENGTH], } -impl AsRef<[u8]> for LocalChallenge { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl LocalChallenge { - /// Derive tags search target from local challenge. - pub fn derive_target(&self) -> Tag { - crypto::sha256_hash(&self.0)[..TAG_SIZE] - .try_into() - .expect("Signature is always bigger than tag; qed") - } +/// VRF signature output and proof as produced by `schnorrkel` crate. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Encode, Decode, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct LocalChallenge { + /// VRF output bytes. + pub output: [u8; VRF_OUTPUT_LENGTH], + /// VRF proof bytes. + #[cfg_attr(feature = "std", serde(with = "serde_arrays"))] + pub proof: [u8; VRF_PROOF_LENGTH], } /// A piece of archival history in Subspace Network. @@ -490,8 +460,8 @@ pub struct Solution { pub piece_index: PieceIndex, /// Encoding pub encoding: Piece, - /// Signature of the tag - pub signature: Signature, + /// VRF signature of the tag + pub tag_signature: TagSignature, /// Local challenge derived from global challenge using farmer's identity. pub local_challenge: LocalChallenge, /// Tag (hmac of encoding and salt) @@ -513,7 +483,7 @@ impl Solution { reward_address, piece_index, encoding, - signature, + tag_signature, local_challenge, tag, } = self; @@ -522,7 +492,7 @@ impl Solution { reward_address: Into::::into(reward_address).into(), piece_index, encoding, - signature, + tag_signature, local_challenge, tag, } @@ -541,8 +511,14 @@ where reward_address, piece_index: 0, encoding: Piece::default(), - signature: Signature::default(), - local_challenge: LocalChallenge::default(), + tag_signature: TagSignature { + output: [0; 32], + proof: [0; 64], + }, + local_challenge: LocalChallenge { + output: [0; 32], + proof: [0; 64], + }, tag: Tag::default(), } } diff --git a/crates/subspace-farmer/src/bin/subspace-farmer/bench_rpc_client.rs b/crates/subspace-farmer/src/bin/subspace-farmer/bench_rpc_client.rs index 36189b005ac8e..d48cd80ecd2c0 100644 --- a/crates/subspace-farmer/src/bin/subspace-farmer/bench_rpc_client.rs +++ b/crates/subspace-farmer/src/bin/subspace-farmer/bench_rpc_client.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use subspace_archiving::archiver::ArchivedSegment; use subspace_farmer::{RpcClient, RpcClientError as MockError}; use subspace_rpc_primitives::{ - FarmerMetadata, RewardSignature, RewardSigningInfo, SlotInfo, SolutionResponse, + FarmerMetadata, RewardSignatureResponse, RewardSigningInfo, SlotInfo, SolutionResponse, }; use tokio::sync::Mutex; use tokio::task::JoinHandle; @@ -91,7 +91,7 @@ impl RpcClient for BenchRpcClient { async fn submit_reward_signature( &self, - _reward_signature: RewardSignature, + _reward_signature: RewardSignatureResponse, ) -> Result<(), MockError> { unreachable!("Unreachable, as we don't start farming for benchmarking") } diff --git a/crates/subspace-farmer/src/farming.rs b/crates/subspace-farmer/src/farming.rs index d3e5a7c91eb1f..cd195500cabe0 100644 --- a/crates/subspace-farmer/src/farming.rs +++ b/crates/subspace-farmer/src/farming.rs @@ -11,8 +11,10 @@ use futures::future::Either; use futures::{future, StreamExt}; use std::sync::mpsc; use std::time::Instant; -use subspace_core_primitives::{LocalChallenge, PublicKey, Salt, Solution}; -use subspace_rpc_primitives::{RewardSignature, RewardSigningInfo, SlotInfo, SolutionResponse}; +use subspace_core_primitives::{PublicKey, Salt, Solution}; +use subspace_rpc_primitives::{ + RewardSignatureResponse, RewardSigningInfo, SlotInfo, SolutionResponse, +}; use thiserror::Error; use tokio::task::JoinHandle; use tracing::{debug, error, info, trace, warn}; @@ -151,7 +153,7 @@ async fn subscribe_to_slot_info( let signature = identity.sign_reward_hash(&hash); match client - .submit_reward_signature(RewardSignature { + .submit_reward_signature(RewardSignatureResponse { hash, signature: Some(signature.to_bytes().into()), }) @@ -185,21 +187,19 @@ async fn subscribe_to_slot_info( let plot = plot.clone(); move || { - let local_challenge = derive_local_challenge(slot_info.global_challenge, &identity); + let (local_challenge, target) = + identity.derive_local_challenge_and_target(slot_info.global_challenge); + // Try to first find a block authoring solution, then if not found try to find a vote let maybe_tag = commitments - .find_by_range( - local_challenge.derive_target(), - slot_info.solution_range, - slot_info.salt, - ) + .find_by_range(target, slot_info.solution_range, slot_info.salt) .or_else(|| { if slot_info.solution_range == slot_info.voting_solution_range { return None; } commitments.find_by_range( - local_challenge.derive_target(), + target, slot_info.voting_solution_range, slot_info.salt, ) @@ -214,7 +214,7 @@ async fn subscribe_to_slot_info( reward_address, piece_index, encoding, - signature: identity.sign_farmer_solution(&tag).to_bytes().into(), + tag_signature: identity.create_tag_signature(tag), local_challenge, tag, }; @@ -331,14 +331,3 @@ fn update_commitments( } } } - -/// Derive local challenge for farmer's identity from the global challenge. -fn derive_local_challenge>( - global_challenge: C, - identity: &Identity, -) -> LocalChallenge { - identity - .sign_farmer_solution(global_challenge.as_ref()) - .to_bytes() - .into() -} diff --git a/crates/subspace-farmer/src/identity.rs b/crates/subspace-farmer/src/identity.rs index 08248e92bcd98..7b9fc62fb0de4 100644 --- a/crates/subspace-farmer/src/identity.rs +++ b/crates/subspace-farmer/src/identity.rs @@ -4,7 +4,10 @@ use schnorrkel::context::SigningContext; use schnorrkel::{ExpansionMode, Keypair, PublicKey, SecretKey, Signature}; use std::fs; use std::path::Path; -use subspace_solving::{REWARD_SIGNING_CONTEXT, SOLUTION_SIGNING_CONTEXT}; +use subspace_core_primitives::{LocalChallenge, Sha256Hash, Tag, TagSignature}; +use subspace_solving::{ + create_tag_signature, derive_local_challenge_and_target, REWARD_SIGNING_CONTEXT, +}; use substrate_bip39::mini_secret_from_entropy; use tracing::debug; use zeroize::Zeroizing; @@ -31,7 +34,6 @@ fn keypair_from_entropy(entropy: &[u8]) -> Keypair { pub struct Identity { keypair: Zeroizing, entropy: Zeroizing>, - farmer_solution_ctx: SigningContext, substrate_ctx: SigningContext, } @@ -57,7 +59,6 @@ impl Identity { Ok(Some(Self { keypair: Zeroizing::new(keypair_from_entropy(&entropy)), entropy: Zeroizing::new(entropy), - farmer_solution_ctx: schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT), substrate_ctx: schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT), })) } else { @@ -80,7 +81,6 @@ impl Identity { Ok(Self { keypair: Zeroizing::new(keypair_from_entropy(&entropy)), entropy: Zeroizing::new(entropy), - farmer_solution_ctx: schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT), substrate_ctx: schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT), }) } @@ -104,7 +104,6 @@ impl Identity { Ok(Self { keypair: Zeroizing::new(keypair_from_entropy(&entropy)), entropy: Zeroizing::new(entropy), - farmer_solution_ctx: schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT), substrate_ctx: schnorrkel::context::signing_context(REWARD_SIGNING_CONTEXT), }) } @@ -124,9 +123,15 @@ impl Identity { &self.entropy } - /// Sign farmer solution. - pub fn sign_farmer_solution(&self, data: &[u8]) -> Signature { - self.keypair.sign(self.farmer_solution_ctx.bytes(data)) + pub fn create_tag_signature(&self, tag: Tag) -> TagSignature { + create_tag_signature(&self.keypair, tag) + } + + pub fn derive_local_challenge_and_target( + &self, + global_challenge: Sha256Hash, + ) -> (LocalChallenge, Tag) { + derive_local_challenge_and_target(&self.keypair, global_challenge) } /// Sign reward hash. diff --git a/crates/subspace-farmer/src/mock_rpc_client.rs b/crates/subspace-farmer/src/mock_rpc_client.rs index 49ec8b9b34df1..8badaa894d721 100644 --- a/crates/subspace-farmer/src/mock_rpc_client.rs +++ b/crates/subspace-farmer/src/mock_rpc_client.rs @@ -6,7 +6,7 @@ use std::pin::Pin; use std::sync::Arc; use subspace_archiving::archiver::ArchivedSegment; use subspace_rpc_primitives::{ - FarmerMetadata, RewardSignature, RewardSigningInfo, SlotInfo, SolutionResponse, + FarmerMetadata, RewardSignatureResponse, RewardSigningInfo, SlotInfo, SolutionResponse, }; use tokio::sync::Mutex; @@ -28,10 +28,10 @@ pub struct Inner { #[allow(dead_code)] reward_signing_info_sender: Mutex>>, reward_signing_info_receiver: Arc>>, - reward_signature_sender: mpsc::Sender, + reward_signature_sender: mpsc::Sender, // TODO: Use this #[allow(dead_code)] - reward_signature_receiver: Arc>>, + reward_signature_receiver: Arc>>, archived_segments_sender: Mutex>>, archived_segments_receiver: Arc>>, acknowledge_archived_segment_sender: mpsc::Sender, @@ -193,7 +193,10 @@ impl RpcClient for MockRpcClient { Ok(Box::pin(receiver)) } - async fn submit_reward_signature(&self, signature: RewardSignature) -> Result<(), MockError> { + async fn submit_reward_signature( + &self, + signature: RewardSignatureResponse, + ) -> Result<(), MockError> { self.inner .reward_signature_sender .clone() diff --git a/crates/subspace-farmer/src/node_rpc_client.rs b/crates/subspace-farmer/src/node_rpc_client.rs index 7c41f2af0a3d7..3e33d68d69311 100644 --- a/crates/subspace-farmer/src/node_rpc_client.rs +++ b/crates/subspace-farmer/src/node_rpc_client.rs @@ -9,7 +9,7 @@ use std::pin::Pin; use std::sync::Arc; use subspace_archiving::archiver::ArchivedSegment; use subspace_rpc_primitives::{ - FarmerMetadata, RewardSignature, RewardSigningInfo, SlotInfo, SolutionResponse, + FarmerMetadata, RewardSignatureResponse, RewardSigningInfo, SlotInfo, SolutionResponse, }; /// `WsClient` wrapper. @@ -85,7 +85,7 @@ impl RpcClient for NodeRpcClient { /// Submit a block signature async fn submit_reward_signature( &self, - reward_signature: RewardSignature, + reward_signature: RewardSignatureResponse, ) -> Result<(), RpcError> { Ok(self .client diff --git a/crates/subspace-farmer/src/rpc_client.rs b/crates/subspace-farmer/src/rpc_client.rs index c370bbd8f311c..87bb5c4c98b09 100644 --- a/crates/subspace-farmer/src/rpc_client.rs +++ b/crates/subspace-farmer/src/rpc_client.rs @@ -3,7 +3,7 @@ use futures::Stream; use std::pin::Pin; use subspace_archiving::archiver::ArchivedSegment; use subspace_rpc_primitives::{ - FarmerMetadata, RewardSignature, RewardSigningInfo, SlotInfo, SolutionResponse, + FarmerMetadata, RewardSignatureResponse, RewardSigningInfo, SlotInfo, SolutionResponse, }; /// To become error type agnostic @@ -32,8 +32,10 @@ pub trait RpcClient: Clone + Send + Sync + 'static { ) -> Result + Send + 'static>>, Error>; /// Submit a block signature - async fn submit_reward_signature(&self, reward_signature: RewardSignature) - -> Result<(), Error>; + async fn submit_reward_signature( + &self, + reward_signature: RewardSignatureResponse, + ) -> Result<(), Error>; /// Subscribe to archived segments async fn subscribe_archived_segments( diff --git a/crates/subspace-rpc-primitives/src/lib.rs b/crates/subspace-rpc-primitives/src/lib.rs index 97ff4fe67c176..8482c279fbe30 100644 --- a/crates/subspace-rpc-primitives/src/lib.rs +++ b/crates/subspace-rpc-primitives/src/lib.rs @@ -17,7 +17,9 @@ use hex_buffer_serde::{Hex, HexForm}; use serde::{Deserialize, Serialize}; -use subspace_core_primitives::{PublicKey, Salt, Sha256Hash, Signature, SlotNumber, Solution}; +use subspace_core_primitives::{ + PublicKey, RewardSignature, Salt, Sha256Hash, SlotNumber, Solution, +}; /// Metadata necessary for farmer operation #[derive(Clone, Debug, Serialize, Deserialize)] @@ -77,10 +79,10 @@ pub struct RewardSigningInfo { /// Signature in response to reward hash signing request. #[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RewardSignature { +pub struct RewardSignatureResponse { /// Hash that was signed. #[serde(with = "HexForm")] pub hash: [u8; 32], /// Pre-header or vote hash signature. - pub signature: Option, + pub signature: Option, } diff --git a/crates/subspace-runtime/src/lib.rs b/crates/subspace-runtime/src/lib.rs index d98c8d2a99948..12ef86920c20e 100644 --- a/crates/subspace-runtime/src/lib.rs +++ b/crates/subspace-runtime/src/lib.rs @@ -44,7 +44,8 @@ use scale_info::TypeInfo; use sp_api::{impl_runtime_apis, BlockT, HashT, HeaderT}; use sp_consensus_subspace::digests::CompatibleDigestItem; use sp_consensus_subspace::{ - EquivocationProof, FarmerPublicKey, GlobalRandomnesses, Salts, SignedVote, SolutionRanges, Vote, + derive_randomness, EquivocationProof, FarmerPublicKey, GlobalRandomnesses, Salts, SignedVote, + SolutionRanges, Vote, }; use sp_core::crypto::{ByteArray, KeyTypeId}; use sp_core::{Hasher, OpaqueMetadata}; @@ -1004,7 +1005,18 @@ fn extrinsics_shuffling_seed(header: Block::Header) -> Randomness let pre_digest = pre_digest.expect("Header must contain one pre-runtime digest; qed"); - BlakeTwo256::hash_of(&pre_digest.solution.signature).into() + let seed: &[u8] = b"extrinsics-shuffling-seed"; + let randomness = derive_randomness( + &pre_digest.solution.public_key, + pre_digest.solution.tag, + &pre_digest.solution.tag_signature, + ) + .expect("Tag signature is verified by the client and must always be valid; qed"); + let mut data = Vec::with_capacity(seed.len() + randomness.len()); + data.extend_from_slice(seed); + data.extend_from_slice(&randomness); + + BlakeTwo256::hash_of(&data).into() } } diff --git a/crates/subspace-solving/Cargo.toml b/crates/subspace-solving/Cargo.toml index 4292facccb3d6..2976f2d08bd59 100644 --- a/crates/subspace-solving/Cargo.toml +++ b/crates/subspace-solving/Cargo.toml @@ -12,6 +12,7 @@ include = [ ] [dependencies] +merlin = { version = "2.0.1", default-features = false } num_cpus = { version = "1.13.0", optional = true } num-traits = { version = "0.2.15", default-features = false } rayon = { version = "1.5.3", optional = true } @@ -30,6 +31,7 @@ default = [ "std", ] std = [ + "merlin/std", "num_cpus", "num-traits/std", "rayon", diff --git a/crates/subspace-solving/src/lib.rs b/crates/subspace-solving/src/lib.rs index a3a1c6e2dcdb5..80223a04203a2 100644 --- a/crates/subspace-solving/src/lib.rs +++ b/crates/subspace-solving/src/lib.rs @@ -24,16 +24,19 @@ mod codec; pub use codec::{BatchEncodeError, SubspaceCodec}; pub use construct_uint::PieceDistance; -use schnorrkel::SignatureResult; +use merlin::Transcript; +use schnorrkel::vrf::{VRFInOut, VRFOutput, VRFProof}; +use schnorrkel::{Keypair, PublicKey, SignatureResult}; use subspace_core_primitives::{ - crypto, LocalChallenge, Piece, Randomness, Salt, Sha256Hash, Tag, TAG_SIZE, + crypto, LocalChallenge, Piece, Randomness, Salt, Sha256Hash, Tag, TagSignature, TAG_SIZE, }; -/// Signing context used for creating solution signatures by farmers. -pub const SOLUTION_SIGNING_CONTEXT: &[u8] = b"farmer_solution"; +const LOCAL_CHALLENGE_LABEL: &[u8] = b"subspace_local_challenge"; +const PLOT_TARGET_CONTEXT: &[u8] = b"subspace_plot_target"; +const TAG_SIGNATURE_LABEL: &[u8] = b"subspace_tag_signature"; /// Signing context used for creating reward signatures by farmers. -pub const REWARD_SIGNING_CONTEXT: &[u8] = b"farmer_reward"; +pub const REWARD_SIGNING_CONTEXT: &[u8] = b"subspace_reward"; #[allow(clippy::assign_op_pattern, clippy::ptr_offset_with_cast)] mod construct_uint { @@ -95,20 +98,104 @@ pub fn create_tag(piece: &[u8], salt: Salt) -> Tag { .expect("Slice is always of correct size; qed") } +// TODO: Separate type for global challenge /// Derive global slot challenge from global randomness. pub fn derive_global_challenge(global_randomness: &Randomness, slot: u64) -> Sha256Hash { crypto::sha256_hash_pair(global_randomness, &slot.to_le_bytes()) } +fn create_local_challenge_transcript(global_challenge: &Sha256Hash) -> Transcript { + let mut transcript = Transcript::new(LOCAL_CHALLENGE_LABEL); + transcript.append_message(b"global challenge", global_challenge); + transcript +} + +/// Derive local challenge for farmer from keypair and global challenge. +pub fn derive_local_challenge(keypair: &Keypair, global_challenge: Sha256Hash) -> LocalChallenge { + let (in_out, proof, _) = keypair.vrf_sign(create_local_challenge_transcript(&global_challenge)); + + LocalChallenge { + output: in_out.output.to_bytes(), + proof: proof.to_bytes(), + } +} + +/// Derive local challenge and target for farmer from keypair and global challenge. +pub fn derive_local_challenge_and_target( + keypair: &Keypair, + global_challenge: Sha256Hash, +) -> (LocalChallenge, Tag) { + let (in_out, proof, _) = keypair.vrf_sign(create_local_challenge_transcript(&global_challenge)); + + let local_challenge = LocalChallenge { + output: in_out.output.to_bytes(), + proof: proof.to_bytes(), + }; + let target = in_out.make_bytes(PLOT_TARGET_CONTEXT); + + (local_challenge, target) +} + /// Verify local challenge for farmer's public key that was derived from the global challenge. -pub fn is_local_challenge_valid( +pub fn verify_local_challenge( + public_key: &PublicKey, global_challenge: Sha256Hash, local_challenge: &LocalChallenge, - public_key: &[u8], -) -> SignatureResult<()> { - let signature = schnorrkel::Signature::from_bytes(local_challenge)?; - let public_key = schnorrkel::PublicKey::from_bytes(public_key)?; +) -> SignatureResult { + public_key + .vrf_verify( + create_local_challenge_transcript(&global_challenge), + &VRFOutput(local_challenge.output), + &VRFProof::from_bytes(&local_challenge.proof)?, + ) + .map(|(in_out, _)| in_out) +} + +/// Derive challenge target from public key and local challenge. +/// +/// NOTE: If you are not the signer then you must verify the local challenge before calling this +/// function. +pub fn derive_target( + public_key: &PublicKey, + global_challenge: Sha256Hash, + local_challenge: &LocalChallenge, +) -> SignatureResult { + let in_out = VRFOutput(local_challenge.output).attach_input_hash( + public_key, + create_local_challenge_transcript(&global_challenge), + )?; + + Ok(in_out.make_bytes(PLOT_TARGET_CONTEXT)) +} + +/// Transcript used for creation and verification of VRF signatures for tags. +pub fn create_tag_signature_transcript(tag: Tag) -> Transcript { + let mut transcript = Transcript::new(TAG_SIGNATURE_LABEL); + transcript.append_message(b"tag", &tag); + transcript +} + +/// Create tag signature using farmer's keypair. +pub fn create_tag_signature(keypair: &Keypair, tag: Tag) -> TagSignature { + let (in_out, proof, _) = keypair.vrf_sign(create_tag_signature_transcript(tag)); + + TagSignature { + output: in_out.output.to_bytes(), + proof: proof.to_bytes(), + } +} - let ctx = schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT); - public_key.verify(ctx.bytes(&global_challenge), &signature) +/// Verify that tag signature was created correctly. +pub fn verify_tag_signature( + tag: Tag, + tag_signature: &TagSignature, + public_key: &PublicKey, +) -> SignatureResult { + public_key + .vrf_verify( + create_tag_signature_transcript(tag), + &VRFOutput(tag_signature.output), + &VRFProof::from_bytes(&tag_signature.proof)?, + ) + .map(|(in_out, _)| in_out) } diff --git a/test/subspace-test-client/src/lib.rs b/test/subspace-test-client/src/lib.rs index 3eb508f8b56b9..ad9500317d834 100644 --- a/test/subspace-test-client/src/lib.rs +++ b/test/subspace-test-client/src/lib.rs @@ -34,7 +34,9 @@ use subspace_core_primitives::objects::BlockObjectMapping; use subspace_core_primitives::{FlatPieces, Piece, Solution, Tag}; use subspace_runtime_primitives::opaque::Block; use subspace_service::{FullClient, NewFull}; -use subspace_solving::{SubspaceCodec, REWARD_SIGNING_CONTEXT, SOLUTION_SIGNING_CONTEXT}; +use subspace_solving::{ + create_tag_signature, derive_local_challenge, SubspaceCodec, REWARD_SIGNING_CONTEXT, +}; use zeroize::Zeroizing; /// Subspace native executor instance. @@ -107,10 +109,10 @@ pub fn start_farmer(new_full: &NewFull) { }) = reward_signing_notification_stream.next().await { let header_hash: [u8; 32] = header_hash.into(); - let reward_signature: schnorrkel::Signature = - signing_pair.sign(substrate_ctx.bytes(&header_hash)); - let signature: subspace_core_primitives::Signature = - reward_signature.to_bytes().into(); + let signature: subspace_core_primitives::RewardSignature = signing_pair + .sign(substrate_ctx.bytes(&header_hash)) + .to_bytes() + .into(); signature_sender .send( FarmerSignature::decode(&mut signature.encode().as_ref()) @@ -140,7 +142,6 @@ async fn start_farming( }); let subspace_codec = SubspaceCodec::new(keypair.public.as_ref()); - let ctx = schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT); let (piece_index, mut encoding) = archived_pieces_receiver .await .unwrap() @@ -168,11 +169,11 @@ async fn start_farming( reward_address: FarmerPublicKey::unchecked_from(keypair.public.to_bytes()), piece_index, encoding: encoding.clone(), - signature: keypair.sign(ctx.bytes(&tag)).to_bytes().into(), - local_challenge: keypair - .sign(ctx.bytes(&new_slot_info.global_challenge)) - .to_bytes() - .into(), + tag_signature: create_tag_signature(&keypair, tag), + local_challenge: derive_local_challenge( + &keypair, + new_slot_info.global_challenge, + ), tag, }) .await; diff --git a/test/subspace-test-runtime/src/lib.rs b/test/subspace-test-runtime/src/lib.rs index a0448758bc936..4e620a7196d49 100644 --- a/test/subspace-test-runtime/src/lib.rs +++ b/test/subspace-test-runtime/src/lib.rs @@ -43,7 +43,8 @@ use pallet_grandpa_finality_verifier::chain::Chain; use sp_api::{impl_runtime_apis, BlockT, HashT, HeaderT}; use sp_consensus_subspace::digests::CompatibleDigestItem; use sp_consensus_subspace::{ - EquivocationProof, FarmerPublicKey, GlobalRandomnesses, Salts, SignedVote, SolutionRanges, Vote, + derive_randomness, EquivocationProof, FarmerPublicKey, GlobalRandomnesses, Salts, SignedVote, + SolutionRanges, Vote, }; use sp_core::crypto::{ByteArray, KeyTypeId}; use sp_core::{Hasher, OpaqueMetadata}; @@ -856,7 +857,18 @@ fn extrinsics_shuffling_seed(header: Block::Header) -> Randomness let pre_digest = pre_digest.expect("Header must contain one pre-runtime digest; qed"); - BlakeTwo256::hash_of(&pre_digest.solution.signature).into() + let seed: &[u8] = b"extrinsics-shuffling-seed"; + let randomness = derive_randomness( + &pre_digest.solution.public_key, + pre_digest.solution.tag, + &pre_digest.solution.tag_signature, + ) + .expect("Tag signature is verified by the client and must always be valid; qed"); + let mut data = Vec::with_capacity(seed.len() + randomness.len()); + data.extend_from_slice(seed); + data.extend_from_slice(&randomness); + + BlakeTwo256::hash_of(&data).into() } } From 03977f4bdbf0b504b5f52833eda5ddd7e7b9eb5c Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Sat, 28 May 2022 04:57:19 +0300 Subject: [PATCH 2/2] Unify function calls --- crates/pallet-subspace/src/mock.rs | 10 +++++----- crates/sc-consensus-subspace/src/tests.rs | 4 ++-- crates/sp-consensus-subspace/src/verification.rs | 6 +++--- crates/subspace-farmer/src/commitments.rs | 9 ++++----- crates/subspace-farmer/src/plotting/tests.rs | 4 ++-- test/subspace-test-client/src/lib.rs | 4 ++-- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/crates/pallet-subspace/src/mock.rs b/crates/pallet-subspace/src/mock.rs index be51d892b0a38..fd45a7a4836c5 100644 --- a/crates/pallet-subspace/src/mock.rs +++ b/crates/pallet-subspace/src/mock.rs @@ -41,7 +41,8 @@ use subspace_core_primitives::{ Sha256Hash, Solution, Tag, PIECE_SIZE, }; use subspace_solving::{ - create_tag_signature, derive_local_challenge, SubspaceCodec, REWARD_SIGNING_CONTEXT, + create_tag, create_tag_signature, derive_global_challenge, derive_local_challenge, + SubspaceCodec, REWARD_SIGNING_CONTEXT, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -195,7 +196,7 @@ pub fn go_to_block( let piece_index = 0; let mut encoding = Piece::default(); subspace_codec.encode(&mut encoding, piece_index).unwrap(); - let tag: Tag = subspace_solving::create_tag(&encoding, { + let tag: Tag = create_tag(&encoding, { let salts = Subspace::salts(); if salts.switch_next_block { salts.next.unwrap() @@ -388,10 +389,9 @@ pub fn create_signed_vote( ) -> SignedVote::Hash, ::AccountId> { let reward_signing_context = schnorrkel::signing_context(REWARD_SIGNING_CONTEXT); - let global_challenge = - subspace_solving::derive_global_challenge(global_randomnesses, slot.into()); + let global_challenge = derive_global_challenge(global_randomnesses, slot.into()); - let tag = subspace_solving::create_tag(&encoding, salt); + let tag = create_tag(&encoding, salt); let vote = Vote::::Hash, _>::V0 { height, diff --git a/crates/sc-consensus-subspace/src/tests.rs b/crates/sc-consensus-subspace/src/tests.rs index 9f21db07d9b9e..ac6b8dbbc78f6 100644 --- a/crates/sc-consensus-subspace/src/tests.rs +++ b/crates/sc-consensus-subspace/src/tests.rs @@ -72,7 +72,7 @@ use subspace_archiving::archiver::Archiver; use subspace_core_primitives::objects::BlockObjectMapping; use subspace_core_primitives::{FlatPieces, LocalChallenge, Piece, Solution, Tag, TagSignature}; use subspace_solving::{ - create_tag_signature, derive_local_challenge, SubspaceCodec, REWARD_SIGNING_CONTEXT, + create_tag, create_tag_signature, derive_local_challenge, SubspaceCodec, REWARD_SIGNING_CONTEXT, }; use substrate_test_runtime::{Block as TestBlock, Hash}; @@ -589,7 +589,7 @@ fn run_one_test(mutator: impl Fn(&mut TestHeader, Stage) + Send + Sync + 'static }) = new_slot_notification_stream.next().await { if Into::::into(new_slot_info.slot) % 3 == (*peer_id) as u64 { - let tag: Tag = subspace_solving::create_tag(&encoding, new_slot_info.salt); + let tag: Tag = create_tag(&encoding, new_slot_info.salt); let _ = solution_sender .send(Solution { diff --git a/crates/sp-consensus-subspace/src/verification.rs b/crates/sp-consensus-subspace/src/verification.rs index e1d43b0290ad8..66766ee1db3cf 100644 --- a/crates/sp-consensus-subspace/src/verification.rs +++ b/crates/sp-consensus-subspace/src/verification.rs @@ -27,8 +27,8 @@ use sp_runtime::DigestItem; use subspace_archiving::archiver; use subspace_core_primitives::{PieceIndex, Randomness, Salt, Sha256Hash, Solution, Tag}; use subspace_solving::{ - derive_global_challenge, derive_target, verify_local_challenge, verify_tag_signature, - PieceDistance, SubspaceCodec, + derive_global_challenge, derive_target, is_tag_valid, verify_local_challenge, + verify_tag_signature, PieceDistance, SubspaceCodec, }; /// Errors encountered by the Subspace authorship task. @@ -206,7 +206,7 @@ fn check_piece_tag( where Header: HeaderT, { - if !subspace_solving::is_tag_valid(&solution.encoding, salt, solution.tag) { + if !is_tag_valid(&solution.encoding, salt, solution.tag) { return Err(VerificationError::InvalidTag(slot)); } diff --git a/crates/subspace-farmer/src/commitments.rs b/crates/subspace-farmer/src/commitments.rs index 0dad8059ba0d9..be1a05d6a173f 100644 --- a/crates/subspace-farmer/src/commitments.rs +++ b/crates/subspace-farmer/src/commitments.rs @@ -13,6 +13,7 @@ use std::io; use std::path::PathBuf; use std::sync::Arc; use subspace_core_primitives::{Piece, Salt, Tag, PIECE_SIZE}; +use subspace_solving::create_tag; use thiserror::Error; use tracing::trace; @@ -136,7 +137,7 @@ impl Commitments { let tags: Vec = pieces .par_chunks_exact(PIECE_SIZE) - .map(|piece| subspace_solving::create_tag(piece, salt)) + .map(|piece| create_tag(piece, salt)) .collect(); for (tag, offset) in tags.iter().zip(batch_start..) { @@ -198,7 +199,7 @@ impl Commitments { if let Some(db) = db_guard.as_ref() { for piece in pieces { - let tag = subspace_solving::create_tag(piece, salt); + let tag = create_tag(piece, salt); db.delete(tag).map_err(CommitmentError::CommitmentDb)?; } } @@ -236,9 +237,7 @@ impl Commitments { if let Some(db) = db_guard.as_ref() { let tags_with_offset: Vec<(PieceOffset, Tag)> = pieces_with_offsets() - .map(|(piece_offset, piece)| { - (piece_offset, subspace_solving::create_tag(piece, salt)) - }) + .map(|(piece_offset, piece)| (piece_offset, create_tag(piece, salt))) .collect(); for (piece_offset, tag) in tags_with_offset { diff --git a/crates/subspace-farmer/src/plotting/tests.rs b/crates/subspace-farmer/src/plotting/tests.rs index 8bfb86c0f8576..b2c9769580fc2 100644 --- a/crates/subspace-farmer/src/plotting/tests.rs +++ b/crates/subspace-farmer/src/plotting/tests.rs @@ -11,7 +11,7 @@ use subspace_archiving::archiver::Archiver; use subspace_core_primitives::objects::BlockObjectMapping; use subspace_core_primitives::{PieceIndexHash, Salt, PIECE_SIZE, SHA256_HASH_SIZE}; use subspace_rpc_primitives::FarmerMetadata; -use subspace_solving::SubspaceCodec; +use subspace_solving::{create_tag, SubspaceCodec}; use tempfile::TempDir; const MERKLE_NUM_LEAVES: usize = 8_usize; @@ -181,7 +181,7 @@ async fn plotting_piece_eviction() { // allow `None` and not errors once that is the case if let Ok(mut read_piece) = plot.read_piece(PieceIndexHash::from_index(piece_index)) { - let correct_tag = subspace_solving::create_tag(&read_piece, salt); + let correct_tag = create_tag(&read_piece, salt); subspace_codec.decode(&mut read_piece, piece_index).unwrap(); diff --git a/test/subspace-test-client/src/lib.rs b/test/subspace-test-client/src/lib.rs index ad9500317d834..0fd98938a20f8 100644 --- a/test/subspace-test-client/src/lib.rs +++ b/test/subspace-test-client/src/lib.rs @@ -35,7 +35,7 @@ use subspace_core_primitives::{FlatPieces, Piece, Solution, Tag}; use subspace_runtime_primitives::opaque::Block; use subspace_service::{FullClient, NewFull}; use subspace_solving::{ - create_tag_signature, derive_local_challenge, SubspaceCodec, REWARD_SIGNING_CONTEXT, + create_tag, create_tag_signature, derive_local_challenge, SubspaceCodec, REWARD_SIGNING_CONTEXT, }; use zeroize::Zeroizing; @@ -161,7 +161,7 @@ async fn start_farming( }) = new_slot_notification_stream.next().await { if Into::::into(new_slot_info.slot) % 2 == 0 { - let tag: Tag = subspace_solving::create_tag(&encoding, new_slot_info.salt); + let tag: Tag = create_tag(&encoding, new_slot_info.salt); let _ = solution_sender .send(Solution {