Skip to main content

hopper_sdk/
fingerprint.rs

1//! # Layout fingerprint verification
2//!
3//! Before a client decodes an account, it should verify that the account
4//! header's `layout_id` matches the fingerprint the client was compiled
5//! against. This prevents the classic "program was redeployed with a new
6//! layout and my client silently misparsed the bytes" bug class.
7//!
8//! The on-chain header layout is documented in `hopper-core::account::header`;
9//! the layout_id fingerprint is stored at offset **4..12** of the account
10//! header. This module knows exactly that and nothing more.
11
12use hopper_schema::{LayoutManifest, ProgramIdl};
13
14/// Byte offset of the layout_id within the Hopper account header.
15///
16/// Header layout: `[0..4] = disc|version|flags|reserved`, `[4..12] = layout_id`.
17pub const LAYOUT_ID_OFFSET: usize = 4;
18
19/// Result of running a fingerprint check against some account bytes.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum FingerprintCheck {
22    /// Bytes matched the expected layout_id.
23    Match,
24    /// Bytes decoded but the layout_id differs.
25    Mismatch {
26        /// Expected fingerprint (from manifest).
27        expected: [u8; 8],
28        /// Fingerprint actually on-chain.
29        actual: [u8; 8],
30    },
31}
32
33/// Error surface for fingerprint operations.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum FingerprintError {
36    /// Account bytes too short to contain a layout_id (< 12 bytes).
37    TooShort,
38    /// No matching layout manifest for the supplied name.
39    UnknownLayout,
40}
41
42/// Read the layout_id from a Hopper account's raw bytes.
43///
44/// Returns `Err(TooShort)` if there aren't at least 12 bytes.
45pub fn read_layout_id(bytes: &[u8]) -> Result<[u8; 8], FingerprintError> {
46    if bytes.len() < LAYOUT_ID_OFFSET + 8 {
47        return Err(FingerprintError::TooShort);
48    }
49    let mut id = [0u8; 8];
50    id.copy_from_slice(&bytes[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + 8]);
51    Ok(id)
52}
53
54/// Check a raw account blob against a specific `LayoutManifest`.
55pub fn check_against_layout(
56    bytes: &[u8],
57    layout: &LayoutManifest,
58) -> Result<FingerprintCheck, FingerprintError> {
59    let actual = read_layout_id(bytes)?;
60    if actual == layout.layout_id {
61        Ok(FingerprintCheck::Match)
62    } else {
63        Ok(FingerprintCheck::Mismatch {
64            expected: layout.layout_id,
65            actual,
66        })
67    }
68}
69
70/// Check a raw account blob against the layout with the given name in the
71/// supplied `ProgramIdl`.
72pub fn check_in_idl(
73    bytes: &[u8],
74    idl: &ProgramIdl,
75    layout_name: &str,
76) -> Result<FingerprintCheck, FingerprintError> {
77    let layout = idl
78        .find_account(layout_name)
79        .ok_or(FingerprintError::UnknownLayout)?;
80    check_against_layout(bytes, layout)
81}
82
83/// Identify which layout in a `ProgramIdl` a blob belongs to by fingerprint.
84/// Returns `None` if no layout matches.
85pub fn identify_in_idl<'a>(bytes: &[u8], idl: &'a ProgramIdl) -> Option<&'a LayoutManifest> {
86    let actual = read_layout_id(bytes).ok()?;
87    let mut i = 0;
88    while i < idl.accounts.len() {
89        if idl.accounts[i].layout_id == actual {
90            return Some(&idl.accounts[i]);
91        }
92        i += 1;
93    }
94    None
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use hopper_schema::FieldDescriptor;
101
102    const SAMPLE_ID: [u8; 8] = [9, 8, 7, 6, 5, 4, 3, 2];
103
104    fn mk_manifest() -> LayoutManifest {
105        LayoutManifest {
106            name: "Vault",
107            disc: 7,
108            version: 1,
109            layout_id: SAMPLE_ID,
110            total_size: 80,
111            field_count: 0,
112            fields: &[] as &[FieldDescriptor],
113        }
114    }
115
116    fn mk_bytes(id: [u8; 8]) -> [u8; 32] {
117        let mut b = [0u8; 32];
118        b[0] = 7; // disc
119        b[1] = 1; // version
120        b[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + 8].copy_from_slice(&id);
121        b
122    }
123
124    #[test]
125    fn matches_when_equal() {
126        let bytes = mk_bytes(SAMPLE_ID);
127        let layout = mk_manifest();
128        assert_eq!(
129            check_against_layout(&bytes, &layout).unwrap(),
130            FingerprintCheck::Match
131        );
132    }
133
134    #[test]
135    fn reports_mismatch_with_actual() {
136        let other = [1, 1, 1, 1, 1, 1, 1, 1];
137        let bytes = mk_bytes(other);
138        let layout = mk_manifest();
139        let check = check_against_layout(&bytes, &layout).unwrap();
140        assert_eq!(
141            check,
142            FingerprintCheck::Mismatch {
143                expected: SAMPLE_ID,
144                actual: other,
145            }
146        );
147    }
148
149    #[test]
150    fn too_short_returns_error() {
151        let bytes = [0u8; 6];
152        let layout = mk_manifest();
153        assert_eq!(
154            check_against_layout(&bytes, &layout),
155            Err(FingerprintError::TooShort)
156        );
157    }
158}