Skip to content

Commit

Permalink
blockchain: introduce Fingerprint and change singing schema
Browse files Browse the repository at this point in the history
So far signing schema was such that validators would sign
Borsh-selialised block.  This had two undesirable properties:

1. If the same validator signed blocks for two different blockchains,
   the could be accused of double signing since there was no way to
   distinguish that signatures belong to two different chains.

2. To prove misbehaviour, full block had to be transmitted.  This
   becomes a bit of an issue for blocks which are final in an epoch
   since those include new epoch which may be couple KB in size.

This commit introduces a Fingerprint which is what validators sign.
The fingerprint is concatenation of a) genesis block hash, b) block
height as little endian¹ and c) block hash.  This addresses the above
concerns:

1. Since fingerprint includes chain’s genesis hash (which uniquely
   identifies a chain) it’s now possible to distinguish signatures
   made for two different chains.

2. The fingerprint is always 72 bytes long so there’s never an issue
   of an unbound growth.

The only concern is that rather than signing the serialised
representation of the block, the validators sign just the hash.
However, the assumption of any blockchain is that hash of a block
uniquely identifies the block and it’s impossible to create two blocks
with the same hash.  As such, signing just the hash is equivalent to
signing the whole block (regardless of the signing algorithm).

____
¹ Little endian was chosen for consistency with borsh.  Normally my
  preference is to use big endian representation but since we’re using
  Borsh for calculating hash and Borsh uses little endian I’ve figured
  it might be confusing if Fingerprint didn’t follow that format as
  well.
  • Loading branch information
mina86 committed Nov 14, 2023
1 parent 9774bd0 commit f41bccf
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 50 deletions.
3 changes: 2 additions & 1 deletion common/blockchain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ edition = "2021"

[dependencies]
borsh.workspace = true
bytemuck.workspace = true
derive_more.workspace = true
strum.workspace = true

lib = { workspace = true, features = ["borsh"] }
stdx.workspace = true

[dev-dependencies]
lib = { workspace = true, features = ["test_utils"] }
rand.workspace = true
stdx.workspace = true

[features]
std = []
155 changes: 123 additions & 32 deletions common/blockchain/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ pub struct Block<PK> {
pub next_epoch: Option<crate::Epoch<PK>>,
}

/// Block’s fingerprint which is used when signing.
///
/// The fingerprint is what validators sign when attesting the validity of the
/// block. It consists of a) chain’s genesis block hash, b) block height and c)
/// block hash.
///
/// Inclusion of the genesis hash means that signatures for blocks with the
/// same height but on different chains won’t be confused as malicious.
///
/// Inclusion of block height and hash mean that
#[derive(
Clone,
PartialEq,
Eq,
borsh::BorshSerialize,
borsh::BorshDeserialize,
bytemuck::TransparentWrapper,
)]
#[repr(transparent)]
pub struct Fingerprint([u8; 72]);

/// Error while generating new block.
#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::IntoStaticStr)]
pub enum GenerateError {
Expand All @@ -67,23 +88,6 @@ impl<PK: crate::PubKey> Block<PK> {
builder.build()
}


/// Signs the block.
pub fn sign(&self, signer: &impl crate::Signer<PK>) -> PK::Signature {
signer.sign(self.calc_hash().as_slice())
}

/// Verifies signature for the block.
#[inline]
pub fn verify(
&self,
pubkey: &PK,
signature: &PK::Signature,
verifier: &impl crate::Verifier<PK>,
) -> bool {
verifier.verify(self.calc_hash().as_slice(), pubkey, signature)
}

/// Constructs next block.
///
/// Returns a new block with `self` as the previous block. Verifies that
Expand Down Expand Up @@ -147,10 +151,77 @@ impl<PK: crate::PubKey> Block<PK> {
}
}

impl Default for Fingerprint {
fn default() -> Self { Self([0; 72]) }
}

impl Fingerprint {
/// Calculates the fingerprint of the given block.
pub fn new<PK: crate::PubKey>(
genesis_hash: &CryptoHash,
block: &Block<PK>,
) -> Self {
Self::from_hash(genesis_hash, block.block_height, &block.calc_hash())
}

/// Constructs the fingerprint of a block at given height and with given
/// hash.
pub fn from_hash(
genesis_hash: &CryptoHash,
block_height: crate::BlockHeight,
block_hash: &CryptoHash,
) -> Self {
let mut fp = Self::default();
let (genesis, rest) = stdx::split_array_mut::<32, 40, 72>(&mut fp.0);
let (height, hash) = stdx::split_array_mut::<8, 32, 40>(rest);
*genesis = genesis_hash.into();
*height = u64::from(block_height).to_le_bytes();
*hash = block_hash.into();
fp
}

/// Parses the fingerprint extracting genesis hash, block height and block
/// hash from it.
pub fn parse(&self) -> (&CryptoHash, crate::BlockHeight, &CryptoHash) {
let (genesis, rest) = stdx::split_array_ref::<32, 40, 72>(&self.0);
let (height, hash) = stdx::split_array_ref::<8, 32, 40>(rest);
let height = u64::from_le_bytes(*height);
(genesis.into(), height.into(), hash.into())
}

/// Returns the fingerprint as bytes slice.
fn as_slice(&self) -> &[u8] { &self.0[..] }

/// Signs the fingerprint
#[inline]
pub fn sign<PK: crate::PubKey>(
&self,
signer: &impl crate::Signer<PK>,
) -> PK::Signature {
signer.sign(self.as_slice())
}

/// Verifies the signature.
#[inline]
pub fn verify<PK: crate::PubKey>(
&self,
pubkey: &PK,
signature: &PK::Signature,
verifier: &impl crate::Verifier<PK>,
) -> bool {
verifier.verify(self.as_slice(), pubkey, signature)
}
}

impl core::fmt::Debug for Fingerprint {
fn fmt(&self, fmtr: &mut core::fmt::Formatter) -> core::fmt::Result {
let (genesis, height, hash) = self.parse();
write!(fmtr, "FP(genesis={genesis}, height={height}, block={hash})")
}
}

#[test]
fn test_block_generation() {
use crate::validators::{MockPubKey, MockSignature, MockSigner};

// Generate a genesis block and test it’s behaviour.
let genesis_hash = "Zq3s+b7x6R8tKV1iQtByAWqlDMXVVD9tSDOlmuLH7wI=";
let genesis_hash = CryptoHash::from_base64(genesis_hash).unwrap();
Expand All @@ -177,19 +248,6 @@ fn test_block_generation() {
assert_eq!(genesis_hash, genesis.calc_hash());
assert_ne!(genesis_hash, block.calc_hash());

let pk = MockPubKey(77);
let signer = MockSigner(pk);
let signature = genesis.sign(&signer);
assert_eq!(MockSignature(1722674425, pk), signature);
assert!(genesis.verify(&pk, &signature, &()));
assert!(!genesis.verify(&MockPubKey(88), &signature, &()));
assert!(!genesis.verify(&pk, &MockSignature(0, pk), &()));

let mut block = genesis.clone();
block.host_timestamp += 1;
assert_ne!(genesis_hash, block.calc_hash());
assert!(!block.verify(&pk, &signature, &()));

// Try creating invalid next block.
assert_eq!(
Err(GenerateError::BadHostHeight),
Expand Down Expand Up @@ -255,3 +313,36 @@ fn test_block_generation() {
assert_eq!(hash, block.prev_block_hash);
assert_eq!(hash, block.epoch_id);
}

#[test]
fn test_signatures() {
use crate::validators::{MockPubKey, MockSignature, MockSigner};

let genesis = CryptoHash::test(1);
let height = 2.into();
let hash = CryptoHash::test(3);

let fingerprint = Fingerprint::from_hash(&genesis, height, &hash);

assert_eq!((&genesis, height, &hash), fingerprint.parse());

let pk = MockPubKey(42);
let signer = MockSigner(pk);

let signature = fingerprint.sign(&signer);
assert_eq!(MockSignature((1, 2, 3), pk), signature);
assert!(fingerprint.verify(&pk, &signature, &()));
assert!(!fingerprint.verify(&MockPubKey(88), &signature, &()));
assert!(!fingerprint.verify(&pk, &MockSignature((0, 0, 0), pk), &()));

let fingerprint =
Fingerprint::from_hash(&CryptoHash::test(66), height, &hash);
assert!(!fingerprint.verify(&pk, &signature, &()));

let fingerprint = Fingerprint::from_hash(&genesis, 66.into(), &hash);
assert!(!fingerprint.verify(&pk, &signature, &()));

let fingerprint =
Fingerprint::from_hash(&genesis, height, &CryptoHash::test(66));
assert!(!fingerprint.verify(&pk, &signature, &()));
}
30 changes: 21 additions & 9 deletions common/blockchain/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ pub struct ChainManager<PK> {
/// Configuration specifying limits for block generation.
config: crate::Config,

/// Hash of the chain’s genesis block.
genesis: CryptoHash,

/// Current latest block which has been signed by quorum of validators.
block: crate::Block<PK>,

Expand Down Expand Up @@ -40,14 +43,17 @@ pub struct ChainManager<PK> {
struct PendingBlock<PK> {
/// The block that waits for signatures.
next_block: crate::Block<PK>,
/// Hash of the block.

/// Fingerprint of the block.
///
/// This is what validators are signing. It equals `next_block.calc_hash()`
/// and we’re keeping it as a field to avoid having to hash the block each
/// time.
hash: CryptoHash,
/// This is what validators are signing. It equals `Fingerprint(&genesis,
/// &next_block)` and we’re keeping it as a field to avoid having to hash
/// the block each time.
fingerprint: crate::block::Fingerprint,

/// Validators who so far submitted valid signatures for the block.
signers: Set<PK>,

/// Sum of stake of validators who have signed the block.
signing_stake: u128,
}
Expand Down Expand Up @@ -117,6 +123,7 @@ impl<PK: crate::PubKey> ChainManager<PK> {
let epoch_height = genesis.host_height;
Ok(Self {
config,
genesis: genesis.calc_hash(),
block: genesis,
next_epoch,
pending_block: None,
Expand Down Expand Up @@ -169,8 +176,10 @@ impl<PK: crate::PubKey> ChainManager<PK> {
state_root,
next_epoch,
)?;
let fingerprint =
crate::block::Fingerprint::new(&self.genesis, &next_block);
self.pending_block = Some(PendingBlock {
hash: next_block.calc_hash(),
fingerprint,
next_block,
signers: Set::new(),
signing_stake: 0,
Expand Down Expand Up @@ -227,7 +236,7 @@ impl<PK: crate::PubKey> ChainManager<PK> {
.ok_or(AddSignatureError::BadValidator)?
.stake()
.get();
if !verifier.verify(pending.hash.as_slice(), &pubkey, signature) {
if !pending.fingerprint.verify(&pubkey, signature, verifier) {
return Err(AddSignatureError::BadSignature);
}

Expand Down Expand Up @@ -320,7 +329,9 @@ fn test_generate() {
mgr: &mut ChainManager<MockPubKey>,
validator: &crate::validators::Validator<MockPubKey>,
) -> Result<AddSignatureEffect, AddSignatureError> {
let signature = mgr.head().1.sign(&validator.pubkey().make_signer());
let signature =
crate::block::Fingerprint::new(&mgr.genesis, mgr.head().1)
.sign(&validator.pubkey().make_signer());
mgr.add_signature(validator.pubkey().clone(), &signature, &())
}

Expand All @@ -343,7 +354,8 @@ fn test_generate() {

// Signatures are verified
let pubkey = MockPubKey(42);
let signature = mgr.head().1.sign(&pubkey.make_signer());
let signature = crate::block::Fingerprint::new(&mgr.genesis, mgr.head().1)
.sign(&pubkey.make_signer());
assert_eq!(
Err(AddSignatureError::BadValidator),
mgr.add_signature(pubkey, &signature, &())
Expand Down
29 changes: 21 additions & 8 deletions common/blockchain/src/validators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ impl<PK> Validator<PK> {

#[cfg(test)]
pub(crate) mod test_utils {
use bytemuck::TransparentWrapper;

/// A mock implementation of a PubKey. Offers no security; intended for
/// tests only.
#[derive(
Expand Down Expand Up @@ -96,7 +98,7 @@ pub(crate) mod test_utils {
borsh::BorshSerialize,
borsh::BorshDeserialize,
)]
pub struct MockSignature(pub u32, pub MockPubKey);
pub struct MockSignature(pub (u32, u64, u32), pub MockPubKey);

impl core::fmt::Debug for MockPubKey {
#[inline]
Expand All @@ -115,7 +117,11 @@ pub(crate) mod test_utils {
impl core::fmt::Debug for MockSignature {
#[inline]
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(fmt, "Sig({:x} by {:?})", self.0, self.1)
write!(
fmt,
"Sig((genesis={}, height={}, block={}) signed by {:?})",
self.0 .0, self.0 .1, self.0 .2, self.1
)
}
}

Expand All @@ -130,7 +136,7 @@ pub(crate) mod test_utils {
pubkey: &MockPubKey,
signature: &<MockPubKey as super::PubKey>::Signature,
) -> bool {
signature.0 == short_hash(message) && &signature.1 == pubkey
signature.0 == short_fp(message) && &signature.1 == pubkey
}
}

Expand All @@ -139,14 +145,21 @@ pub(crate) mod test_utils {
&self,
message: &[u8],
) -> <MockPubKey as super::PubKey>::Signature {
MockSignature(short_hash(message), self.0)
MockSignature(short_fp(message), self.0)
}
}

fn short_hash(message: &[u8]) -> u32 {
let hash = <&[u8; 32]>::try_from(message).unwrap();
let (hash, _) = stdx::split_array_ref::<4, 28, 32>(&hash);
u32::from_be_bytes(*hash)
fn short_fp(message: &[u8]) -> (u32, u64, u32) {
fn h32(hash: &lib::hash::CryptoHash) -> u32 {
let (bytes, _) =
stdx::split_array_ref::<4, 28, 32>(hash.as_array());
u32::from_be_bytes(*bytes)
}

let fp = <&[u8; 72]>::try_from(message).unwrap();
let fp = crate::block::Fingerprint::wrap_ref(fp);
let (genesis, height, hash) = fp.parse();
(h32(genesis), u64::from(height), h32(hash))
}
}

Expand Down

0 comments on commit f41bccf

Please sign in to comment.