1use crate::storage::blockchain::{
28 compute_block_hash, verify_chain, Block, SignedFields, VerifyReport, GENESIS_PREV_HASH,
29};
30use crate::storage::schema::Value;
31use crate::storage::signed_writes::{
32 reverify_row, RESERVED_SIGNATURE_COL, RESERVED_SIGNER_PUBKEY_COL, SIGNATURE_LEN,
33 SIGNER_PUBKEY_LEN,
34};
35
36use super::blockchain_kind::{COL_BLOCK_HEIGHT, COL_HASH, COL_PREV_HASH, COL_TIMESTAMP};
37
38pub const GENESIS_SIGNER_PUBKEY: [u8; SIGNER_PUBKEY_LEN] = [0u8; SIGNER_PUBKEY_LEN];
42
43pub const GENESIS_SIGNATURE: [u8; SIGNATURE_LEN] = [0u8; SIGNATURE_LEN];
46
47pub const RESERVED_COLUMNS_SIGNED_CHAIN: &[&str] = &[
51 COL_BLOCK_HEIGHT,
52 COL_PREV_HASH,
53 COL_TIMESTAMP,
54 COL_HASH,
55 RESERVED_SIGNER_PUBKEY_COL,
56 RESERVED_SIGNATURE_COL,
57];
58
59pub fn is_genesis_signed_marker(pubkey: &[u8; SIGNER_PUBKEY_LEN], signature: &[u8]) -> bool {
63 pubkey == &GENESIS_SIGNER_PUBKEY && signature.iter().all(|b| *b == 0)
64}
65
66pub fn make_signed_block_reserved_fields(
79 prev_hash: [u8; 32],
80 height: u64,
81 timestamp_ms: u64,
82 payload_canonical: &[u8],
83 signer_pubkey: [u8; SIGNER_PUBKEY_LEN],
84 signature: Vec<u8>,
85) -> (Vec<(String, Value)>, [u8; 32]) {
86 let signed = SignedFields {
87 signer_pubkey,
88 signature: signature.clone(),
89 };
90 let hash = compute_block_hash(
91 &prev_hash,
92 height,
93 timestamp_ms,
94 payload_canonical,
95 Some(&signed),
96 );
97 let fields = vec![
98 (COL_BLOCK_HEIGHT.to_string(), Value::UnsignedInteger(height)),
99 (COL_PREV_HASH.to_string(), Value::Blob(prev_hash.to_vec())),
100 (
101 COL_TIMESTAMP.to_string(),
102 Value::UnsignedInteger(timestamp_ms),
103 ),
104 (
105 RESERVED_SIGNER_PUBKEY_COL.to_string(),
106 Value::Blob(signer_pubkey.to_vec()),
107 ),
108 (RESERVED_SIGNATURE_COL.to_string(), Value::Blob(signature)),
109 (COL_HASH.to_string(), Value::Blob(hash.to_vec())),
110 ];
111 (fields, hash)
112}
113
114pub fn genesis_signed_fields(timestamp_ms: u64) -> Vec<(String, Value)> {
118 make_signed_block_reserved_fields(
119 GENESIS_PREV_HASH,
120 0,
121 timestamp_ms,
122 &[],
123 GENESIS_SIGNER_PUBKEY,
124 GENESIS_SIGNATURE.to_vec(),
125 )
126 .0
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct SignedChainVerifyOutcome {
135 pub checked: u64,
136 pub ok: bool,
137 pub first_bad_height: Option<u64>,
138 pub signature_failure: bool,
141}
142
143impl SignedChainVerifyOutcome {
144 pub fn ok(checked: u64) -> Self {
145 Self {
146 checked,
147 ok: true,
148 first_bad_height: None,
149 signature_failure: false,
150 }
151 }
152}
153
154pub fn verify_chain_with_signatures(blocks: &[Block]) -> SignedChainVerifyOutcome {
171 let checked = blocks.len() as u64;
172 match verify_chain(blocks) {
173 VerifyReport::Inconsistent { block_height, .. } => SignedChainVerifyOutcome {
174 checked,
175 ok: false,
176 first_bad_height: Some(block_height),
177 signature_failure: false,
178 },
179 VerifyReport::Ok => {
180 for block in blocks {
181 let Some(signed) = &block.signed else {
182 continue;
186 };
187 if block.block_height == 0
188 && is_genesis_signed_marker(&signed.signer_pubkey, &signed.signature)
189 {
190 continue;
191 }
192 if signed.signature.len() != SIGNATURE_LEN {
193 return SignedChainVerifyOutcome {
194 checked,
195 ok: false,
196 first_bad_height: Some(block.block_height),
197 signature_failure: true,
198 };
199 }
200 let mut sig_arr = [0u8; SIGNATURE_LEN];
201 sig_arr.copy_from_slice(&signed.signature);
202 if reverify_row(&signed.signer_pubkey, &sig_arr, &block.payload).is_err() {
203 return SignedChainVerifyOutcome {
204 checked,
205 ok: false,
206 first_bad_height: Some(block.block_height),
207 signature_failure: true,
208 };
209 }
210 }
211 SignedChainVerifyOutcome::ok(checked)
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::storage::signed_writes::{
220 verify_insert, InsertSignatureFields, SignedWriteError, SignerRegistry,
221 };
222 use ed25519_dalek::{Signer, SigningKey};
223
224 fn signing_key(seed: u8) -> SigningKey {
225 SigningKey::from_bytes(&[seed; 32])
226 }
227
228 fn pubkey_of(sk: &SigningKey) -> [u8; SIGNER_PUBKEY_LEN] {
229 sk.verifying_key().to_bytes()
230 }
231
232 fn build_signed_chain<const N: usize>(sk: &SigningKey, payloads: [&[u8]; N]) -> Vec<Block> {
234 let mut out: Vec<Block> = Vec::new();
235 let mut prev = GENESIS_PREV_HASH;
236 let g_hash = compute_block_hash(
238 &prev,
239 0,
240 1_000,
241 &[],
242 Some(&SignedFields {
243 signer_pubkey: GENESIS_SIGNER_PUBKEY,
244 signature: GENESIS_SIGNATURE.to_vec(),
245 }),
246 );
247 out.push(Block {
248 block_height: 0,
249 prev_hash: prev,
250 timestamp_ms: 1_000,
251 payload: Vec::new(),
252 signed: Some(SignedFields {
253 signer_pubkey: GENESIS_SIGNER_PUBKEY,
254 signature: GENESIS_SIGNATURE.to_vec(),
255 }),
256 hash: g_hash,
257 });
258 prev = g_hash;
259 let pk = pubkey_of(sk);
260 for (i, &payload) in payloads.iter().enumerate() {
261 let height = (i + 1) as u64;
262 let ts = 1_000 + height;
263 let sig = sk.sign(payload).to_bytes();
264 let signed = SignedFields {
265 signer_pubkey: pk,
266 signature: sig.to_vec(),
267 };
268 let hash = compute_block_hash(&prev, height, ts, payload, Some(&signed));
269 out.push(Block {
270 block_height: height,
271 prev_hash: prev,
272 timestamp_ms: ts,
273 payload: payload.to_vec(),
274 signed: Some(signed),
275 hash,
276 });
277 prev = hash;
278 }
279 out
280 }
281
282 #[test]
283 fn reserved_columns_signed_chain_is_union() {
284 assert_eq!(RESERVED_COLUMNS_SIGNED_CHAIN.len(), 6);
286 for col in [
287 COL_BLOCK_HEIGHT,
288 COL_PREV_HASH,
289 COL_TIMESTAMP,
290 COL_HASH,
291 RESERVED_SIGNER_PUBKEY_COL,
292 RESERVED_SIGNATURE_COL,
293 ] {
294 assert!(
295 RESERVED_COLUMNS_SIGNED_CHAIN.contains(&col),
296 "missing reserved column {col}"
297 );
298 }
299 }
300
301 #[test]
302 fn genesis_uses_null_pubkey_and_signature() {
303 let fields = genesis_signed_fields(1_700_000_000_000);
306 let pk = fields
307 .iter()
308 .find(|(k, _)| k == RESERVED_SIGNER_PUBKEY_COL)
309 .unwrap();
310 match &pk.1 {
311 Value::Blob(b) => assert_eq!(&b[..], &GENESIS_SIGNER_PUBKEY[..]),
312 other => panic!("signer_pubkey must be Blob, got {other:?}"),
313 }
314 let sig = fields
315 .iter()
316 .find(|(k, _)| k == RESERVED_SIGNATURE_COL)
317 .unwrap();
318 match &sig.1 {
319 Value::Blob(b) => {
320 assert_eq!(b.len(), SIGNATURE_LEN);
321 assert!(b.iter().all(|x| *x == 0));
322 }
323 other => panic!("signature must be Blob, got {other:?}"),
324 }
325 let height = fields.iter().find(|(k, _)| k == COL_BLOCK_HEIGHT).unwrap();
326 assert_eq!(height.1, Value::UnsignedInteger(0));
327 }
328
329 #[test]
330 fn hash_binds_signer_pubkey_and_signature() {
331 let sk = signing_key(7);
333 let pk = pubkey_of(&sk);
334 let payload = b"row=a;";
335 let sig = sk.sign(payload).to_bytes().to_vec();
336 let (_fields, hash_with_sig) =
337 make_signed_block_reserved_fields(GENESIS_PREV_HASH, 1, 42, payload, pk, sig.clone());
338 let mut sig_tampered = sig.clone();
340 sig_tampered[0] ^= 0x01;
341 let (_f2, hash_tampered) =
342 make_signed_block_reserved_fields(GENESIS_PREV_HASH, 1, 42, payload, pk, sig_tampered);
343 assert_ne!(hash_with_sig, hash_tampered);
344 let mut pk_tampered = pk;
346 pk_tampered[0] ^= 0x01;
347 let (_f3, hash_pk_tampered) =
348 make_signed_block_reserved_fields(GENESIS_PREV_HASH, 1, 42, payload, pk_tampered, sig);
349 assert_ne!(hash_with_sig, hash_pk_tampered);
350 }
351
352 #[test]
353 fn valid_signed_chain_verifies_ok() {
354 let sk = signing_key(3);
355 let chain = build_signed_chain(&sk, [b"a".as_slice(), b"b".as_slice(), b"c".as_slice()]);
356 let out = verify_chain_with_signatures(&chain);
357 assert!(out.ok, "{out:?}");
358 assert_eq!(out.checked, 4);
359 assert!(out.first_bad_height.is_none());
360 }
361
362 #[test]
363 fn tampering_signer_pubkey_fails_at_block_height() {
364 let sk = signing_key(4);
367 let mut chain =
368 build_signed_chain(&sk, [b"a".as_slice(), b"b".as_slice(), b"c".as_slice()]);
369 if let Some(signed) = chain[2].signed.as_mut() {
372 signed.signer_pubkey[0] ^= 0x55;
373 }
374 let out = verify_chain_with_signatures(&chain);
375 assert!(!out.ok);
376 assert_eq!(out.first_bad_height, Some(2));
377 }
378
379 #[test]
380 fn tampering_signature_with_recomputed_hash_caught_by_sig_reverify() {
381 let sk = signing_key(5);
385 let attacker = signing_key(6);
386 let mut chain =
387 build_signed_chain(&sk, [b"a".as_slice(), b"b".as_slice(), b"c".as_slice()]);
388 let target = &mut chain[2];
393 let bad_sig = attacker.sign(&target.payload).to_bytes().to_vec();
394 target.signed = Some(SignedFields {
395 signer_pubkey: pubkey_of(&sk),
396 signature: bad_sig,
397 });
398 let recomputed = compute_block_hash(
401 &target.prev_hash,
402 target.block_height,
403 target.timestamp_ms,
404 &target.payload,
405 target.signed.as_ref(),
406 );
407 target.hash = recomputed;
408 let mut prev = recomputed;
411 for i in 3..chain.len() {
412 chain[i].prev_hash = prev;
413 chain[i].hash = compute_block_hash(
414 &chain[i].prev_hash,
415 chain[i].block_height,
416 chain[i].timestamp_ms,
417 &chain[i].payload,
418 chain[i].signed.as_ref(),
419 );
420 prev = chain[i].hash;
421 }
422 let out = verify_chain_with_signatures(&chain);
423 assert!(!out.ok);
424 assert_eq!(out.first_bad_height, Some(2));
425 assert!(
426 out.signature_failure,
427 "expected signature_failure, got {out:?}"
428 );
429 }
430
431 #[test]
432 fn composition_chain_fail_then_sig_fail_atomic_reject() {
433 let sk = signing_key(8);
444 let pk = pubkey_of(&sk);
445 let payload = b"payload";
446 let sig = sk.sign(payload).to_bytes();
447 let registry = SignerRegistry::from_initial(&[pk], "@system", 0);
448
449 let attacker = signing_key(9);
451 let bad_sig = attacker.sign(payload).to_bytes();
452 let err = verify_insert(
453 ®istry,
454 &InsertSignatureFields {
455 signer_pubkey: Some(&pk),
456 signature: Some(&bad_sig),
457 },
458 payload,
459 )
460 .unwrap_err();
461 assert_eq!(err, SignedWriteError::InvalidSignature);
462
463 verify_insert(
465 ®istry,
466 &InsertSignatureFields {
467 signer_pubkey: Some(&pk),
468 signature: Some(&sig),
469 },
470 payload,
471 )
472 .unwrap();
473 }
474
475 #[test]
476 fn missing_signature_fields_typed_error() {
477 let registry = SignerRegistry::default();
480 let err =
481 verify_insert(®istry, &InsertSignatureFields::default(), b"payload").unwrap_err();
482 match err {
483 SignedWriteError::MissingSignatureFields { fields } => {
484 assert!(fields.contains(&RESERVED_SIGNER_PUBKEY_COL));
485 assert!(fields.contains(&RESERVED_SIGNATURE_COL));
486 }
487 other => panic!("expected MissingSignatureFields, got {other:?}"),
488 }
489 }
490
491 #[test]
492 fn genesis_marker_recognised() {
493 assert!(is_genesis_signed_marker(
494 &GENESIS_SIGNER_PUBKEY,
495 &GENESIS_SIGNATURE
496 ));
497 assert!(!is_genesis_signed_marker(&[1u8; 32], &GENESIS_SIGNATURE));
498 let nonzero = [1u8; SIGNATURE_LEN];
499 assert!(!is_genesis_signed_marker(&GENESIS_SIGNER_PUBKEY, &nonzero));
500 }
501}