use crate::storage::blockchain::{
compute_block_hash, verify_chain, Block, SignedFields, VerifyReport, GENESIS_PREV_HASH,
};
use crate::storage::schema::Value;
use crate::storage::signed_writes::{
reverify_row, RESERVED_SIGNATURE_COL, RESERVED_SIGNER_PUBKEY_COL, SIGNATURE_LEN,
SIGNER_PUBKEY_LEN,
};
use super::blockchain_kind::{
COL_BLOCK_HEIGHT, COL_HASH, COL_PREV_HASH, COL_TIMESTAMP,
};
pub const GENESIS_SIGNER_PUBKEY: [u8; SIGNER_PUBKEY_LEN] = [0u8; SIGNER_PUBKEY_LEN];
pub const GENESIS_SIGNATURE: [u8; SIGNATURE_LEN] = [0u8; SIGNATURE_LEN];
pub const RESERVED_COLUMNS_SIGNED_CHAIN: &[&str] = &[
COL_BLOCK_HEIGHT,
COL_PREV_HASH,
COL_TIMESTAMP,
COL_HASH,
RESERVED_SIGNER_PUBKEY_COL,
RESERVED_SIGNATURE_COL,
];
pub fn is_genesis_signed_marker(pubkey: &[u8; SIGNER_PUBKEY_LEN], signature: &[u8]) -> bool {
pubkey == &GENESIS_SIGNER_PUBKEY && signature.iter().all(|b| *b == 0)
}
pub fn make_signed_block_reserved_fields(
prev_hash: [u8; 32],
height: u64,
timestamp_ms: u64,
payload_canonical: &[u8],
signer_pubkey: [u8; SIGNER_PUBKEY_LEN],
signature: Vec<u8>,
) -> (Vec<(String, Value)>, [u8; 32]) {
let signed = SignedFields {
signer_pubkey,
signature: signature.clone(),
};
let hash = compute_block_hash(
&prev_hash,
height,
timestamp_ms,
payload_canonical,
Some(&signed),
);
let fields = vec![
(COL_BLOCK_HEIGHT.to_string(), Value::UnsignedInteger(height)),
(COL_PREV_HASH.to_string(), Value::Blob(prev_hash.to_vec())),
(
COL_TIMESTAMP.to_string(),
Value::UnsignedInteger(timestamp_ms),
),
(
RESERVED_SIGNER_PUBKEY_COL.to_string(),
Value::Blob(signer_pubkey.to_vec()),
),
(
RESERVED_SIGNATURE_COL.to_string(),
Value::Blob(signature),
),
(COL_HASH.to_string(), Value::Blob(hash.to_vec())),
];
(fields, hash)
}
pub fn genesis_signed_fields(timestamp_ms: u64) -> Vec<(String, Value)> {
make_signed_block_reserved_fields(
GENESIS_PREV_HASH,
0,
timestamp_ms,
&[],
GENESIS_SIGNER_PUBKEY,
GENESIS_SIGNATURE.to_vec(),
)
.0
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedChainVerifyOutcome {
pub checked: u64,
pub ok: bool,
pub first_bad_height: Option<u64>,
pub signature_failure: bool,
}
impl SignedChainVerifyOutcome {
pub fn ok(checked: u64) -> Self {
Self {
checked,
ok: true,
first_bad_height: None,
signature_failure: false,
}
}
}
pub fn verify_chain_with_signatures(blocks: &[Block]) -> SignedChainVerifyOutcome {
let checked = blocks.len() as u64;
match verify_chain(blocks) {
VerifyReport::Inconsistent { block_height, .. } => SignedChainVerifyOutcome {
checked,
ok: false,
first_bad_height: Some(block_height),
signature_failure: false,
},
VerifyReport::Ok => {
for block in blocks {
let Some(signed) = &block.signed else {
continue;
};
if block.block_height == 0
&& is_genesis_signed_marker(&signed.signer_pubkey, &signed.signature)
{
continue;
}
if signed.signature.len() != SIGNATURE_LEN {
return SignedChainVerifyOutcome {
checked,
ok: false,
first_bad_height: Some(block.block_height),
signature_failure: true,
};
}
let mut sig_arr = [0u8; SIGNATURE_LEN];
sig_arr.copy_from_slice(&signed.signature);
if reverify_row(&signed.signer_pubkey, &sig_arr, &block.payload).is_err() {
return SignedChainVerifyOutcome {
checked,
ok: false,
first_bad_height: Some(block.block_height),
signature_failure: true,
};
}
}
SignedChainVerifyOutcome::ok(checked)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::signed_writes::{
verify_insert, InsertSignatureFields, SignedWriteError, SignerRegistry,
};
use ed25519_dalek::{Signer, SigningKey};
fn signing_key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
fn pubkey_of(sk: &SigningKey) -> [u8; SIGNER_PUBKEY_LEN] {
sk.verifying_key().to_bytes()
}
fn build_signed_chain<const N: usize>(sk: &SigningKey, payloads: [&[u8]; N]) -> Vec<Block> {
let mut out: Vec<Block> = Vec::new();
let mut prev = GENESIS_PREV_HASH;
let g_hash = compute_block_hash(
&prev,
0,
1_000,
&[],
Some(&SignedFields {
signer_pubkey: GENESIS_SIGNER_PUBKEY,
signature: GENESIS_SIGNATURE.to_vec(),
}),
);
out.push(Block {
block_height: 0,
prev_hash: prev,
timestamp_ms: 1_000,
payload: Vec::new(),
signed: Some(SignedFields {
signer_pubkey: GENESIS_SIGNER_PUBKEY,
signature: GENESIS_SIGNATURE.to_vec(),
}),
hash: g_hash,
});
prev = g_hash;
let pk = pubkey_of(sk);
for (i, &payload) in payloads.iter().enumerate() {
let height = (i + 1) as u64;
let ts = 1_000 + height;
let sig = sk.sign(payload).to_bytes();
let signed = SignedFields {
signer_pubkey: pk,
signature: sig.to_vec(),
};
let hash = compute_block_hash(&prev, height, ts, payload, Some(&signed));
out.push(Block {
block_height: height,
prev_hash: prev,
timestamp_ms: ts,
payload: payload.to_vec(),
signed: Some(signed),
hash,
});
prev = hash;
}
out
}
#[test]
fn reserved_columns_signed_chain_is_union() {
assert_eq!(RESERVED_COLUMNS_SIGNED_CHAIN.len(), 6);
for col in [
COL_BLOCK_HEIGHT,
COL_PREV_HASH,
COL_TIMESTAMP,
COL_HASH,
RESERVED_SIGNER_PUBKEY_COL,
RESERVED_SIGNATURE_COL,
] {
assert!(
RESERVED_COLUMNS_SIGNED_CHAIN.contains(&col),
"missing reserved column {col}"
);
}
}
#[test]
fn genesis_uses_null_pubkey_and_signature() {
let fields = genesis_signed_fields(1_700_000_000_000);
let pk = fields
.iter()
.find(|(k, _)| k == RESERVED_SIGNER_PUBKEY_COL)
.unwrap();
match &pk.1 {
Value::Blob(b) => assert_eq!(&b[..], &GENESIS_SIGNER_PUBKEY[..]),
other => panic!("signer_pubkey must be Blob, got {other:?}"),
}
let sig = fields
.iter()
.find(|(k, _)| k == RESERVED_SIGNATURE_COL)
.unwrap();
match &sig.1 {
Value::Blob(b) => {
assert_eq!(b.len(), SIGNATURE_LEN);
assert!(b.iter().all(|x| *x == 0));
}
other => panic!("signature must be Blob, got {other:?}"),
}
let height = fields
.iter()
.find(|(k, _)| k == COL_BLOCK_HEIGHT)
.unwrap();
assert_eq!(height.1, Value::UnsignedInteger(0));
}
#[test]
fn hash_binds_signer_pubkey_and_signature() {
let sk = signing_key(7);
let pk = pubkey_of(&sk);
let payload = b"row=a;";
let sig = sk.sign(payload).to_bytes().to_vec();
let (_fields, hash_with_sig) =
make_signed_block_reserved_fields(GENESIS_PREV_HASH, 1, 42, payload, pk, sig.clone());
let mut sig_tampered = sig.clone();
sig_tampered[0] ^= 0x01;
let (_f2, hash_tampered) = make_signed_block_reserved_fields(
GENESIS_PREV_HASH,
1,
42,
payload,
pk,
sig_tampered,
);
assert_ne!(hash_with_sig, hash_tampered);
let mut pk_tampered = pk;
pk_tampered[0] ^= 0x01;
let (_f3, hash_pk_tampered) = make_signed_block_reserved_fields(
GENESIS_PREV_HASH,
1,
42,
payload,
pk_tampered,
sig,
);
assert_ne!(hash_with_sig, hash_pk_tampered);
}
#[test]
fn valid_signed_chain_verifies_ok() {
let sk = signing_key(3);
let chain = build_signed_chain(&sk, [b"a".as_slice(), b"b".as_slice(), b"c".as_slice()]);
let out = verify_chain_with_signatures(&chain);
assert!(out.ok, "{out:?}");
assert_eq!(out.checked, 4);
assert!(out.first_bad_height.is_none());
}
#[test]
fn tampering_signer_pubkey_fails_at_block_height() {
let sk = signing_key(4);
let mut chain = build_signed_chain(&sk, [b"a".as_slice(), b"b".as_slice(), b"c".as_slice()]);
if let Some(signed) = chain[2].signed.as_mut() {
signed.signer_pubkey[0] ^= 0x55;
}
let out = verify_chain_with_signatures(&chain);
assert!(!out.ok);
assert_eq!(out.first_bad_height, Some(2));
}
#[test]
fn tampering_signature_with_recomputed_hash_caught_by_sig_reverify() {
let sk = signing_key(5);
let attacker = signing_key(6);
let mut chain = build_signed_chain(&sk, [b"a".as_slice(), b"b".as_slice(), b"c".as_slice()]);
let target = &mut chain[2];
let bad_sig = attacker.sign(&target.payload).to_bytes().to_vec();
target.signed = Some(SignedFields {
signer_pubkey: pubkey_of(&sk),
signature: bad_sig,
});
let recomputed = compute_block_hash(
&target.prev_hash,
target.block_height,
target.timestamp_ms,
&target.payload,
target.signed.as_ref(),
);
target.hash = recomputed;
let mut prev = recomputed;
for i in 3..chain.len() {
chain[i].prev_hash = prev;
chain[i].hash = compute_block_hash(
&chain[i].prev_hash,
chain[i].block_height,
chain[i].timestamp_ms,
&chain[i].payload,
chain[i].signed.as_ref(),
);
prev = chain[i].hash;
}
let out = verify_chain_with_signatures(&chain);
assert!(!out.ok);
assert_eq!(out.first_bad_height, Some(2));
assert!(
out.signature_failure,
"expected signature_failure, got {out:?}"
);
}
#[test]
fn composition_chain_fail_then_sig_fail_atomic_reject() {
let sk = signing_key(8);
let pk = pubkey_of(&sk);
let payload = b"payload";
let sig = sk.sign(payload).to_bytes();
let registry = SignerRegistry::from_initial(&[pk], "@system", 0);
let attacker = signing_key(9);
let bad_sig = attacker.sign(payload).to_bytes();
let err = verify_insert(
®istry,
&InsertSignatureFields {
signer_pubkey: Some(&pk),
signature: Some(&bad_sig),
},
payload,
)
.unwrap_err();
assert_eq!(err, SignedWriteError::InvalidSignature);
verify_insert(
®istry,
&InsertSignatureFields {
signer_pubkey: Some(&pk),
signature: Some(&sig),
},
payload,
)
.unwrap();
}
#[test]
fn missing_signature_fields_typed_error() {
let registry = SignerRegistry::default();
let err = verify_insert(
®istry,
&InsertSignatureFields::default(),
b"payload",
)
.unwrap_err();
match err {
SignedWriteError::MissingSignatureFields { fields } => {
assert!(fields.contains(&RESERVED_SIGNER_PUBKEY_COL));
assert!(fields.contains(&RESERVED_SIGNATURE_COL));
}
other => panic!("expected MissingSignatureFields, got {other:?}"),
}
}
#[test]
fn genesis_marker_recognised() {
assert!(is_genesis_signed_marker(
&GENESIS_SIGNER_PUBKEY,
&GENESIS_SIGNATURE
));
assert!(!is_genesis_signed_marker(&[1u8; 32], &GENESIS_SIGNATURE));
let nonzero = [1u8; SIGNATURE_LEN];
assert!(!is_genesis_signed_marker(&GENESIS_SIGNER_PUBKEY, &nonzero));
}
}