use log::{trace, warn};
use std::sync::Arc;
use codec::{Decode, Encode};
use sc_client_api::backend::Backend;
use sp_blockchain::{Backend as BlockchainBackend, HeaderBackend};
use sp_consensus_grandpa::GRANDPA_ENGINE_ID;
use sp_runtime::{
generic::BlockId,
traits::{Block as BlockT, Header as HeaderT, NumberFor, One},
};
use crate::{
authorities::{AuthoritySetChangeId, AuthoritySetChanges},
best_justification,
justification::GrandpaJustification,
SharedAuthoritySet, LOG_TARGET,
};
const MAX_UNKNOWN_HEADERS: usize = 100_000;
#[derive(Clone)]
pub struct FinalityProofProvider<BE, Block: BlockT> {
backend: Arc<BE>,
shared_authority_set: Option<SharedAuthoritySet<Block::Hash, NumberFor<Block>>>,
}
impl<B, Block> FinalityProofProvider<B, Block>
where
Block: BlockT,
B: Backend<Block>,
{
pub fn new(
backend: Arc<B>,
shared_authority_set: Option<SharedAuthoritySet<Block::Hash, NumberFor<Block>>>,
) -> Self {
FinalityProofProvider { backend, shared_authority_set }
}
pub fn new_for_service(
backend: Arc<B>,
shared_authority_set: Option<SharedAuthoritySet<Block::Hash, NumberFor<Block>>>,
) -> Arc<Self> {
Arc::new(Self::new(backend, shared_authority_set))
}
}
impl<B, Block> FinalityProofProvider<B, Block>
where
Block: BlockT,
B: Backend<Block>,
{
pub fn prove_finality(
&self,
block: NumberFor<Block>,
) -> Result<Option<Vec<u8>>, FinalityProofError> {
Ok(self.prove_finality_proof(block, true)?.map(|proof| proof.encode()))
}
pub fn prove_finality_proof(
&self,
block: NumberFor<Block>,
collect_unknown_headers: bool,
) -> Result<Option<FinalityProof<Block::Header>>, FinalityProofError> {
let authority_set_changes = if let Some(changes) = self
.shared_authority_set
.as_ref()
.map(SharedAuthoritySet::authority_set_changes)
{
changes
} else {
return Ok(None)
};
prove_finality(&*self.backend, authority_set_changes, block, collect_unknown_headers)
}
}
#[derive(Debug, PartialEq, Encode, Decode, Clone)]
pub struct FinalityProof<Header: HeaderT> {
pub block: Header::Hash,
pub justification: Vec<u8>,
pub unknown_headers: Vec<Header>,
}
#[derive(Debug, thiserror::Error)]
pub enum FinalityProofError {
#[error("Block not yet finalized")]
BlockNotYetFinalized,
#[error("Block not covered by authority set changes")]
BlockNotInAuthoritySetChanges,
#[error(transparent)]
Client(#[from] sp_blockchain::Error),
}
fn prove_finality<Block, B>(
backend: &B,
authority_set_changes: AuthoritySetChanges<NumberFor<Block>>,
block: NumberFor<Block>,
collect_unknown_headers: bool,
) -> Result<Option<FinalityProof<Block::Header>>, FinalityProofError>
where
Block: BlockT,
B: Backend<Block>,
{
let finalized_number = backend.blockchain().info().finalized_number;
if finalized_number < block {
let err = format!(
"Requested finality proof for descendant of #{} while we only have finalized #{}.",
block, finalized_number,
);
trace!(target: LOG_TARGET, "{}", &err);
return Err(FinalityProofError::BlockNotYetFinalized)
}
let (justification, just_block) = match authority_set_changes.get_set_id(block) {
AuthoritySetChangeId::Latest => {
if let Some(justification) = best_justification(backend)?
.map(|j: GrandpaJustification<Block>| (j.encode(), j.target().0))
{
justification
} else {
trace!(
target: LOG_TARGET,
"No justification found for the latest finalized block. \
Returning empty proof.",
);
return Ok(None)
}
},
AuthoritySetChangeId::Set(_, last_block_for_set) => {
let last_block_for_set_id = backend
.blockchain()
.expect_block_hash_from_id(&BlockId::Number(last_block_for_set))?;
let justification = if let Some(grandpa_justification) = backend
.blockchain()
.justifications(last_block_for_set_id)?
.and_then(|justifications| justifications.into_justification(GRANDPA_ENGINE_ID))
{
grandpa_justification
} else {
trace!(
target: LOG_TARGET,
"No justification found when making finality proof for {}. \
Returning empty proof.",
block,
);
return Ok(None)
};
(justification, last_block_for_set)
},
AuthoritySetChangeId::Unknown => {
warn!(
target: LOG_TARGET,
"AuthoritySetChanges does not cover the requested block #{} due to missing data. \
You need to resync to populate AuthoritySetChanges properly.",
block,
);
return Err(FinalityProofError::BlockNotInAuthoritySetChanges)
},
};
let mut headers = Vec::new();
if collect_unknown_headers {
let mut current = block + One::one();
loop {
if current > just_block || headers.len() >= MAX_UNKNOWN_HEADERS {
break
}
let hash = backend.blockchain().expect_block_hash_from_id(&BlockId::Number(current))?;
headers.push(backend.blockchain().expect_header(hash)?);
current += One::one();
}
};
Ok(Some(FinalityProof {
block: backend.blockchain().expect_block_hash_from_id(&BlockId::Number(just_block))?,
justification,
unknown_headers: headers,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{authorities::AuthoritySetChanges, BlockNumberOps, ClientError, SetId};
use futures::executor::block_on;
use sc_block_builder::BlockBuilderBuilder;
use sc_client_api::{apply_aux, LockImportRun};
use sp_consensus::BlockOrigin;
use sp_consensus_grandpa::GRANDPA_ENGINE_ID as ID;
use sp_core::crypto::UncheckedFrom;
use sp_keyring::Ed25519Keyring;
use substrate_test_runtime_client::{
runtime::{Block, Header, H256},
Backend as TestBackend, ClientBlockImportExt, ClientExt, DefaultTestClientBuilderExt,
TestClient, TestClientBuilder, TestClientBuilderExt,
};
fn check_finality_proof<Block: BlockT>(
current_set_id: SetId,
current_authorities: sp_consensus_grandpa::AuthorityList,
remote_proof: Vec<u8>,
) -> sp_blockchain::Result<super::FinalityProof<Block::Header>>
where
NumberFor<Block>: BlockNumberOps,
{
let proof = super::FinalityProof::<Block::Header>::decode(&mut &remote_proof[..])
.map_err(|_| ClientError::BadJustification("failed to decode finality proof".into()))?;
let justification: GrandpaJustification<Block> =
Decode::decode(&mut &proof.justification[..])
.map_err(|_| ClientError::JustificationDecode)?;
justification.verify(current_set_id, ¤t_authorities)?;
Ok(proof)
}
pub(crate) type FinalityProof = super::FinalityProof<Header>;
fn header(number: u64) -> Header {
let parent_hash = match number {
0 => Default::default(),
_ => header(number - 1).hash(),
};
Header::new(
number,
H256::from_low_u64_be(0),
H256::from_low_u64_be(0),
parent_hash,
Default::default(),
)
}
fn test_blockchain(
number_of_blocks: u64,
to_finalize: &[u64],
) -> (Arc<TestClient>, Arc<TestBackend>, Vec<Block>) {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let mut blocks = Vec::new();
for _ in 0..number_of_blocks {
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().best_hash)
.with_parent_block_number(client.chain_info().best_number)
.build()
.unwrap()
.build()
.unwrap()
.block;
block_on(client.import(BlockOrigin::Own, block.clone())).unwrap();
blocks.push(block);
}
for block in to_finalize {
let hash = blocks[*block as usize - 1].hash();
client.finalize_block(hash, None).unwrap();
}
(client, backend, blocks)
}
fn store_best_justification(client: &TestClient, just: &GrandpaJustification<Block>) {
client
.lock_import_and_run(|import_op| {
crate::aux_schema::update_best_justification(just, |insert| {
apply_aux(import_op, insert, &[])
})
})
.unwrap();
}
#[test]
fn finality_proof_fails_if_no_more_last_finalized_blocks() {
let (_, backend, _) = test_blockchain(6, &[4]);
let authority_set_changes = AuthoritySetChanges::empty();
let proof_of_5 = prove_finality(&*backend, authority_set_changes, 5, true);
assert!(matches!(proof_of_5, Err(FinalityProofError::BlockNotYetFinalized)));
}
#[test]
fn finality_proof_is_none_if_no_justification_known() {
let (_, backend, _) = test_blockchain(6, &[4]);
let mut authority_set_changes = AuthoritySetChanges::empty();
authority_set_changes.append(0, 4);
let proof_of_3 = prove_finality(&*backend, authority_set_changes, 3, true).unwrap();
assert_eq!(proof_of_3, None);
}
#[test]
fn finality_proof_check_fails_when_proof_decode_fails() {
check_finality_proof::<Block>(
1,
vec![(UncheckedFrom::unchecked_from([3u8; 32]), 1u64)],
vec![42],
)
.unwrap_err();
}
#[test]
fn finality_proof_check_fails_when_proof_is_empty() {
check_finality_proof::<Block>(
1,
vec![(UncheckedFrom::unchecked_from([3u8; 32]), 1u64)],
Vec::<GrandpaJustification<Block>>::new().encode(),
)
.unwrap_err();
}
#[test]
fn finality_proof_check_fails_with_incomplete_justification() {
let (_, _, blocks) = test_blockchain(8, &[4, 5, 8]);
let commit = finality_grandpa::Commit {
target_hash: blocks[7].hash(),
target_number: *blocks[7].header().number(),
precommits: Vec::new(),
};
let grandpa_just: GrandpaJustification<Block> =
sp_consensus_grandpa::GrandpaJustification::<Header> {
round: 8,
votes_ancestries: Vec::new(),
commit,
}
.into();
let finality_proof = FinalityProof {
block: header(2).hash(),
justification: grandpa_just.encode(),
unknown_headers: Vec::new(),
};
check_finality_proof::<Block>(
1,
vec![(UncheckedFrom::unchecked_from([3u8; 32]), 1u64)],
finality_proof.encode(),
)
.unwrap_err();
}
fn create_commit<S, Id>(
block: Block,
round: u64,
set_id: SetId,
auth: &[Ed25519Keyring],
) -> finality_grandpa::Commit<H256, u64, S, Id>
where
Id: From<sp_core::ed25519::Public>,
S: From<sp_core::ed25519::Signature>,
{
let mut precommits = Vec::new();
for voter in auth {
let precommit = finality_grandpa::Precommit {
target_hash: block.hash(),
target_number: *block.header().number(),
};
let msg = finality_grandpa::Message::Precommit(precommit.clone());
let encoded = sp_consensus_grandpa::localized_payload(round, set_id, &msg);
let signature = voter.sign(&encoded[..]).into();
let signed_precommit = finality_grandpa::SignedPrecommit {
precommit,
signature,
id: voter.public().into(),
};
precommits.push(signed_precommit);
}
finality_grandpa::Commit {
target_hash: block.hash(),
target_number: *block.header().number(),
precommits,
}
}
#[test]
fn finality_proof_check_works_with_correct_justification() {
let (client, _, blocks) = test_blockchain(8, &[4, 5, 8]);
let alice = Ed25519Keyring::Alice;
let set_id = 1;
let round = 8;
let commit = create_commit(blocks[7].clone(), round, set_id, &[alice]);
let grandpa_just = GrandpaJustification::from_commit(&client, round, commit).unwrap();
let finality_proof = FinalityProof {
block: header(2).hash(),
justification: grandpa_just.encode(),
unknown_headers: Vec::new(),
};
assert_eq!(
finality_proof,
check_finality_proof::<Block>(
set_id,
vec![(alice.public().into(), 1u64)],
finality_proof.encode(),
)
.unwrap(),
);
}
#[test]
fn finality_proof_using_authority_set_changes_fails_with_undefined_start() {
let (_, backend, _) = test_blockchain(8, &[4, 5, 8]);
let mut authority_set_changes = AuthoritySetChanges::empty();
authority_set_changes.append(1, 8);
let proof_of_6 = prove_finality(&*backend, authority_set_changes, 6, true);
assert!(matches!(proof_of_6, Err(FinalityProofError::BlockNotInAuthoritySetChanges)));
}
#[test]
fn finality_proof_using_authority_set_changes_works() {
let (client, backend, blocks) = test_blockchain(8, &[4, 5]);
let block7 = &blocks[6];
let block8 = &blocks[7];
let round = 8;
let commit = create_commit(block8.clone(), round, 1, &[Ed25519Keyring::Alice]);
let grandpa_just8 = GrandpaJustification::from_commit(&client, round, commit).unwrap();
client
.finalize_block(block8.hash(), Some((ID, grandpa_just8.encode().clone())))
.unwrap();
let mut authority_set_changes = AuthoritySetChanges::empty();
authority_set_changes.append(0, 5);
authority_set_changes.append(1, 8);
let proof_of_6: FinalityProof =
prove_finality(&*backend, authority_set_changes.clone(), 6, true)
.unwrap()
.unwrap();
assert_eq!(
proof_of_6,
FinalityProof {
block: block8.hash(),
justification: grandpa_just8.encode(),
unknown_headers: vec![block7.header().clone(), block8.header().clone()],
},
);
let proof_of_6_without_unknown: FinalityProof =
prove_finality(&*backend, authority_set_changes.clone(), 6, false)
.unwrap()
.unwrap();
assert_eq!(
proof_of_6_without_unknown,
FinalityProof {
block: block8.hash(),
justification: grandpa_just8.encode(),
unknown_headers: vec![],
},
);
}
#[test]
fn finality_proof_in_last_set_fails_without_latest() {
let (_, backend, _) = test_blockchain(8, &[4, 5, 8]);
let mut authority_set_changes = AuthoritySetChanges::empty();
authority_set_changes.append(0, 5);
assert!(matches!(prove_finality(&*backend, authority_set_changes, 6, true), Ok(None)));
}
#[test]
fn finality_proof_in_last_set_using_latest_justification_works() {
let (client, backend, blocks) = test_blockchain(8, &[4, 5, 8]);
let block7 = &blocks[6];
let block8 = &blocks[7];
let round = 8;
let commit = create_commit(block8.clone(), round, 1, &[Ed25519Keyring::Alice]);
let grandpa_just8 = GrandpaJustification::from_commit(&client, round, commit).unwrap();
store_best_justification(&client, &grandpa_just8);
let mut authority_set_changes = AuthoritySetChanges::empty();
authority_set_changes.append(0, 5);
let proof_of_6: FinalityProof =
prove_finality(&*backend, authority_set_changes, 6, true).unwrap().unwrap();
assert_eq!(
proof_of_6,
FinalityProof {
block: block8.hash(),
justification: grandpa_just8.encode(),
unknown_headers: vec![block7.header().clone(), block8.header().clone()],
}
);
}
}