Skip to main content

confium_tc_keys/
stealth_address.rs

1//! Stealth address derivation.
2
3use getrandom::SysRng;
4use p256::elliptic_curve::rand_core::UnwrapErr;
5use p256::elliptic_curve::sec1::ToSec1Point;
6use p256::elliptic_curve::{Field, PrimeField};
7use p256::{AffinePoint, FieldBytes, ProjectivePoint, Scalar};
8use sha2::{Digest, Sha256};
9
10/// A scanning key (public) used by the scanner to detect stealth addresses.
11#[derive(Debug, Clone)]
12pub struct ScanPubkey {
13    pub point: AffinePoint,
14}
15
16/// The spending key (secret) used to spend from stealth addresses.
17#[derive(Debug, Clone)]
18pub struct SpendKeypair {
19    pub secret: Scalar,
20    pub public: AffinePoint,
21}
22
23/// A stealth address output.
24#[derive(Debug, Clone)]
25pub struct StealthAddress {
26    /// The ephemeral point published by the sender.
27    pub ephemeral: AffinePoint,
28    /// The one-time public key for this address.
29    pub one_time_pubkey: AffinePoint,
30}
31
32/// Generate a spending keypair.
33pub fn generate_spend_keypair() -> SpendKeypair {
34    let secret = Scalar::random(&mut UnwrapErr(SysRng));
35    let public = (ProjectivePoint::GENERATOR * secret).to_affine();
36    SpendKeypair { secret, public }
37}
38
39/// Generate a scanning keypair.
40pub fn generate_scan_keypair() -> (Scalar, ScanPubkey) {
41    let secret = Scalar::random(&mut UnwrapErr(SysRng));
42    let public = (ProjectivePoint::GENERATOR * secret).to_affine();
43    (secret, ScanPubkey { point: public })
44}
45
46/// Sender creates a stealth address for the recipient.
47/// Uses the recipient's scan pubkey and spend pubkey.
48pub fn create_stealth_address(
49    scan_pubkey: &ScanPubkey,
50    spend_pubkey: &AffinePoint,
51) -> (StealthAddress, Scalar) {
52    // Sender picks ephemeral key r
53    let r = Scalar::random(&mut UnwrapErr(SysRng));
54    let ephemeral = (ProjectivePoint::GENERATOR * r).to_affine();
55
56    // Shared secret: r * scan_pubkey = scan_secret * ephemeral
57    let shared_point = (ProjectivePoint::from(scan_pubkey.point) * r).to_affine();
58
59    // Derive one-time key adjustment: hash(shared)
60    let adjustment = hash_to_scalar(&shared_point);
61
62    // One-time public key: spend_pubkey + adjustment * G
63    let one_time_pubkey = (ProjectivePoint::from(*spend_pubkey)
64        + ProjectivePoint::GENERATOR * adjustment)
65        .to_affine();
66
67    (
68        StealthAddress {
69            ephemeral,
70            one_time_pubkey,
71        },
72        r,
73    )
74}
75
76/// Recipient scans for stealth addresses using their scan secret.
77/// Returns the one-time secret key if this address belongs to them.
78pub fn scan_stealth_address(
79    scan_secret: &Scalar,
80    spend_secret: &Scalar,
81    address: &StealthAddress,
82) -> Option<Scalar> {
83    // Shared secret: scan_secret * ephemeral
84    let shared_point = (ProjectivePoint::from(address.ephemeral) * scan_secret).to_affine();
85    let adjustment = hash_to_scalar(&shared_point);
86
87    // One-time secret: spend_secret + adjustment
88    let one_time_secret = spend_secret + &adjustment;
89
90    // Verify: one_time_secret * G == one_time_pubkey
91    let expected_pubkey = (ProjectivePoint::GENERATOR * one_time_secret).to_affine();
92    if expected_pubkey == address.one_time_pubkey {
93        Some(one_time_secret)
94    } else {
95        None
96    }
97}
98
99/// Reduce 32 bytes to a scalar by rejection sampling with re-hash.
100/// Never falls back to a constant: a zero result here would void the
101/// derivation or proof guarantees.
102fn reduce_to_scalar(mut bytes: [u8; 32]) -> Scalar {
103    loop {
104        if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(bytes))) {
105            return s;
106        }
107        let mut h = Sha256::new();
108        h.update(b"confium-scalar-reduce-v1");
109        h.update(bytes);
110        bytes = h.finalize().into();
111    }
112}
113
114fn hash_to_scalar(point: &AffinePoint) -> Scalar {
115    let mut hasher = Sha256::new();
116    hasher.update(b"stealth-hash");
117    hasher.update(point.to_sec1_point(true).as_bytes());
118    let bytes: [u8; 32] = hasher.finalize().into();
119    reduce_to_scalar(bytes)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn create_and_scan() {
128        let (scan_secret, scan_pubkey) = generate_scan_keypair();
129        let spend_kp = generate_spend_keypair();
130
131        let (address, _r) = create_stealth_address(&scan_pubkey, &spend_kp.public);
132
133        let one_time_sk = scan_stealth_address(&scan_secret, &spend_kp.secret, &address);
134
135        assert!(one_time_sk.is_some());
136    }
137
138    #[test]
139    fn wrong_scan_secret_fails() {
140        let (_, scan_pubkey) = generate_scan_keypair();
141        let spend_kp = generate_spend_keypair();
142        let (address, _) = create_stealth_address(&scan_pubkey, &spend_kp.public);
143
144        let wrong_scan = Scalar::random(&mut UnwrapErr(SysRng));
145        let result = scan_stealth_address(&wrong_scan, &spend_kp.secret, &address);
146        assert!(result.is_none());
147    }
148
149    #[test]
150    fn wrong_spend_secret_fails() {
151        let (scan_secret, scan_pubkey) = generate_scan_keypair();
152        let spend_kp = generate_spend_keypair();
153        let (address, _) = create_stealth_address(&scan_pubkey, &spend_kp.public);
154
155        let wrong_spend = Scalar::random(&mut UnwrapErr(SysRng));
156        let result = scan_stealth_address(&scan_secret, &wrong_spend, &address);
157        assert!(result.is_none());
158    }
159
160    #[test]
161    fn different_senders_different_addresses() {
162        let (_, scan_pubkey) = generate_scan_keypair();
163        let spend_kp = generate_spend_keypair();
164
165        let (addr1, _) = create_stealth_address(&scan_pubkey, &spend_kp.public);
166        let (addr2, _) = create_stealth_address(&scan_pubkey, &spend_kp.public);
167
168        assert_ne!(addr1.ephemeral, addr2.ephemeral);
169        assert_ne!(addr1.one_time_pubkey, addr2.one_time_pubkey);
170    }
171
172    #[test]
173    fn one_time_secret_matches_pubkey() {
174        let (scan_secret, scan_pubkey) = generate_scan_keypair();
175        let spend_kp = generate_spend_keypair();
176        let (address, _) = create_stealth_address(&scan_pubkey, &spend_kp.public);
177
178        let one_time_sk = scan_stealth_address(&scan_secret, &spend_kp.secret, &address).unwrap();
179        let derived_pk = (ProjectivePoint::GENERATOR * one_time_sk).to_affine();
180        assert_eq!(derived_pk, address.one_time_pubkey);
181    }
182
183    #[test]
184    fn ephemeral_published() {
185        let (_, scan_pubkey) = generate_scan_keypair();
186        let spend_kp = generate_spend_keypair();
187        let (address, _) = create_stealth_address(&scan_pubkey, &spend_kp.public);
188        // Ephemeral must not be identity
189        assert!(address.ephemeral != AffinePoint::IDENTITY);
190    }
191}