Skip to main content

nectar_primitives/
signing.rs

1//! BzzAddress sign-data byte layout (Swarm handshake / hive shared spec).
2//!
3//! The Swarm peer record (overlay + underlay + nonce + timestamp + chequebook)
4//! is authenticated by an EIP-191 personal-sign signature over the byte
5//! sequence built by [`sign_data`] below. The layout matches bee
6//! `pkg/bzz/address.go:138-160` exactly so any Swarm impl can interop.
7//!
8//! ```text
9//! sign_data = "bee-handshake-"        (14 bytes)
10//!           || underlay_bytes         (caller-supplied wire encoding)
11//!           || overlay                (32 bytes)
12//!           || network_id big-endian  (8 bytes)
13//!           || nonce                  (32 bytes)
14//!           || timestamp big-endian   (8 bytes, i64 two's-complement)
15//!           || chequebook             (20 bytes, all-zero if None)
16//! ```
17//!
18//! Signing and recovery are **not wrapped** - callers use alloy directly:
19//!
20//! ```ignore
21//! use alloy_signer::SignerSync;
22//! let sig = signer.sign_message_sync(&sign_data(/* … */))?;
23//! let eth = sig.recover_address_from_msg(&sign_data(/* … */))?;
24//! ```
25//!
26//! Overlay verification is also one line - compare against
27//! [`compute_overlay`](crate::compute_overlay):
28//!
29//! ```ignore
30//! if compute_overlay(&recovered_eth, network_id, &nonce) == claimed_overlay { /* ok */ }
31//! ```
32//!
33//! Nectar intentionally does **not** depend on libp2p - the `underlay_bytes`
34//! argument is whatever wire encoding the calling node uses for its multiaddr
35//! list.
36
37use alloy_primitives::Address;
38
39use crate::{NetworkId, Nonce, SwarmAddress, Timestamp};
40
41/// Magic prefix matching bee `pkg/bzz/address.go:138` (`signDataPrefix`).
42pub const SIGN_DATA_PREFIX: &[u8] = b"bee-handshake-";
43
44/// Build the canonical sign-data buffer for a BzzAddress.
45///
46/// `underlay_bytes` is the wire-encoded multiaddr list (caller-defined format).
47/// `chequebook` is `None` for nodes without a chequebook; the byte layout
48/// pads with 20 zero bytes either way, matching bee's `common.Address{}.Bytes()`
49/// behaviour (so `None` and `Some(Address::ZERO)` produce byte-identical
50/// sign-data - verified by the test suite).
51#[must_use]
52pub fn sign_data(
53    underlay_bytes: &[u8],
54    overlay: &SwarmAddress,
55    network_id: NetworkId,
56    nonce: &Nonce,
57    timestamp: Timestamp,
58    chequebook: Option<&Address>,
59) -> Vec<u8> {
60    let mut buf = Vec::with_capacity(
61        SIGN_DATA_PREFIX.len()
62            + underlay_bytes.len()
63            + 32  // overlay
64            + 8   // network_id
65            + 32  // nonce
66            + 8   // timestamp
67            + 20, // chequebook
68    );
69    buf.extend_from_slice(SIGN_DATA_PREFIX);
70    buf.extend_from_slice(underlay_bytes);
71    buf.extend_from_slice(overlay.as_bytes());
72    buf.extend_from_slice(&network_id.to_be_bytes());
73    buf.extend_from_slice(nonce.as_slice());
74    buf.extend_from_slice(&timestamp.to_be_bytes());
75    match chequebook {
76        Some(addr) => buf.extend_from_slice(addr.as_slice()),
77        None => buf.extend_from_slice(&[0u8; 20]),
78    }
79    buf
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::compute_overlay;
86    use alloy_primitives::{address, b256};
87    use alloy_signer::SignerSync;
88    use alloy_signer_local::LocalSigner;
89
90    #[test]
91    fn layout_matches_bee_spec() {
92        let overlay = SwarmAddress::new([0xaa; 32]);
93        let net = NetworkId::MAINNET;
94        let nonce = Nonce::new([0xbb; 32]);
95        let ts = Timestamp::from(0x0102_0304_0506_0708_i64);
96        let cb = address!("00112233445566778899aabbccddeeff00112233");
97        let underlay = b"\x01\x02\x03";
98
99        let buf = sign_data(underlay, &overlay, net, &nonce, ts, Some(&cb));
100
101        assert_eq!(&buf[0..14], b"bee-handshake-");
102        assert_eq!(&buf[14..17], b"\x01\x02\x03");
103        assert_eq!(&buf[17..49], &[0xaa; 32]);
104        assert_eq!(&buf[49..57], &1u64.to_be_bytes());
105        assert_eq!(&buf[57..89], &[0xbb; 32]);
106        assert_eq!(
107            &buf[89..97],
108            &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
109        );
110        assert_eq!(&buf[97..117], cb.as_slice());
111        assert_eq!(buf.len(), 117);
112    }
113
114    #[test]
115    fn no_chequebook_is_byte_identical_to_zero_chequebook() {
116        let a = sign_data(
117            &[],
118            &SwarmAddress::zero(),
119            NetworkId::MAINNET,
120            &Nonce::ZERO,
121            Timestamp::ZERO,
122            None,
123        );
124        let b = sign_data(
125            &[],
126            &SwarmAddress::zero(),
127            NetworkId::MAINNET,
128            &Nonce::ZERO,
129            Timestamp::ZERO,
130            Some(&Address::ZERO),
131        );
132        assert_eq!(a, b);
133        assert_eq!(&a[a.len() - 20..], &[0u8; 20]);
134    }
135
136    /// End-to-end: build sign-data, sign with alloy, recover via alloy,
137    /// verify overlay by comparing `compute_overlay` against the claimed one.
138    /// Demonstrates the canonical caller flow - no wrapper layer needed.
139    #[test]
140    fn caller_flow_sign_recover_verify_overlay() {
141        let signer = LocalSigner::random();
142        let eth = signer.address();
143        let net = NetworkId::TESTNET;
144        let nonce = Nonce::new([0x55; 32]);
145        let overlay = compute_overlay(&eth, net, &nonce);
146        let ts = Timestamp::from(1_700_000_000_i64);
147
148        let data = sign_data(b"/ip4/127.0.0.1/tcp/1634", &overlay, net, &nonce, ts, None);
149
150        // Sign - alloy directly.
151        let sig = signer.sign_message_sync(&data).expect("sign");
152        // Recover - alloy directly.
153        let recovered = sig.recover_address_from_msg(&data).expect("recover");
154        assert_eq!(recovered, eth);
155        // Verify overlay - direct compute + compare.
156        assert_eq!(compute_overlay(&recovered, net, &nonce), overlay);
157    }
158
159    /// Pinned vector: tampering with the nonce changes the derived overlay so
160    /// the equality check fails. Catches regressions in either `sign_data`
161    /// layout or `compute_overlay` hashing.
162    #[test]
163    fn tampered_nonce_breaks_overlay_check() {
164        let signer = LocalSigner::random();
165        let eth = signer.address();
166        let net = NetworkId::MAINNET;
167        let nonce = Nonce::from(b256!(
168            "0202020202020202020202020202020202020202020202020202020202020202"
169        ));
170        let overlay = compute_overlay(&eth, net, &nonce);
171        let wrong_nonce = Nonce::new([0x88; 32]);
172        assert_ne!(compute_overlay(&eth, net, &wrong_nonce), overlay);
173    }
174}