1use bitcoin::secp256k1::{Scalar, Secp256k1, SecretKey};
15use bitcoin::{Amount, OutPoint, ScriptBuf, XOnlyPublicKey};
16
17use crate::error::ReceiveError;
18use silent_payments_core::keys::SpendSecretKey;
19
20#[derive(Debug, Clone)]
31pub struct DetectedOutput {
32 pub outpoint: OutPoint,
34 pub amount: Amount,
36 pub script_pubkey: ScriptBuf,
38 tweak: SecretKey,
41 pub label: Option<u32>,
43}
44
45impl DetectedOutput {
46 pub fn new(
48 outpoint: OutPoint,
49 amount: Amount,
50 script_pubkey: ScriptBuf,
51 tweak: SecretKey,
52 label: Option<u32>,
53 ) -> Self {
54 Self {
55 outpoint,
56 amount,
57 script_pubkey,
58 tweak,
59 label,
60 }
61 }
62
63 pub fn derive_spend_key(
80 &self,
81 spend_secret: &SpendSecretKey,
82 secp: &Secp256k1<bitcoin::secp256k1::All>,
83 ) -> Result<SecretKey, ReceiveError> {
84 let tweak_scalar = Scalar::from_be_bytes(self.tweak.secret_bytes())
86 .map_err(|_| ReceiveError::SpendKeyDerivation("tweak scalar is zero".into()))?;
87
88 let spend_key = spend_secret
90 .as_inner()
91 .add_tweak(&tweak_scalar)
92 .map_err(|e| ReceiveError::SpendKeyDerivation(format!("tweak addition failed: {e}")))?;
93
94 let (derived_xonly, _parity) = spend_key.public_key(secp).x_only_public_key();
96
97 let script_bytes = self.script_pubkey.as_bytes();
99 if script_bytes.len() >= 34 {
100 let expected_xonly = XOnlyPublicKey::from_slice(&script_bytes[2..34]).map_err(|e| {
101 ReceiveError::SpendKeyDerivation(format!("invalid x-only key in script: {e}"))
102 })?;
103
104 if derived_xonly != expected_xonly {
105 return Err(ReceiveError::SpendKeyDerivation(format!(
106 "derived key {derived_xonly} does not match output key {expected_xonly}"
107 )));
108 }
109 }
110
111 Ok(spend_key)
112 }
113
114 pub fn tweak_secret_key(&self) -> &SecretKey {
116 &self.tweak
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use bitcoin::key::TweakedPublicKey;
124 use bitcoin::secp256k1::{Secp256k1, SecretKey};
125 use silent_payments_core::keys::SpendSecretKey;
126
127 const SPEND_SK_BYTES: [u8; 32] = [
129 0x93, 0xf5, 0xed, 0x90, 0x7a, 0xd5, 0xb2, 0xbd, 0xbb, 0xdc, 0xb5, 0xd9, 0x11, 0x6e, 0xbc,
130 0x0a, 0x4e, 0x1f, 0x92, 0xf9, 0x10, 0xd5, 0x26, 0x02, 0x37, 0xfa, 0x45, 0xa9, 0x40, 0x8a,
131 0xad, 0x16,
132 ];
133
134 const TWEAK_BYTES: [u8; 32] = [
135 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
136 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e,
137 0x1f, 0x20,
138 ];
139
140 #[test]
141 fn derive_spend_key_produces_valid_key() {
142 let secp = Secp256k1::new();
143 let spend_sk = SpendSecretKey::from_slice(&SPEND_SK_BYTES).unwrap();
144 let tweak = SecretKey::from_slice(&TWEAK_BYTES).unwrap();
145
146 let tweak_scalar = Scalar::from_be_bytes(TWEAK_BYTES).unwrap();
148 let expected_spend_key = spend_sk.as_inner().add_tweak(&tweak_scalar).unwrap();
149 let (expected_xonly, _) = expected_spend_key.public_key(&secp).x_only_public_key();
150
151 let tweaked_pk = TweakedPublicKey::dangerous_assume_tweaked(expected_xonly);
153 let script = ScriptBuf::new_p2tr_tweaked(tweaked_pk);
154
155 let detected = DetectedOutput::new(
156 OutPoint::null(),
157 Amount::from_sat(100_000),
158 script,
159 tweak,
160 None,
161 );
162
163 let derived = detected.derive_spend_key(&spend_sk, &secp).unwrap();
164
165 let (derived_xonly, _) = derived.public_key(&secp).x_only_public_key();
167 assert_eq!(derived_xonly, expected_xonly);
168 }
169
170 #[test]
171 fn derive_spend_key_fails_on_script_mismatch() {
172 let secp = Secp256k1::new();
173 let spend_sk = SpendSecretKey::from_slice(&SPEND_SK_BYTES).unwrap();
174 let tweak = SecretKey::from_slice(&TWEAK_BYTES).unwrap();
175
176 let wrong_sk = SecretKey::from_slice(&[0x42; 32]).unwrap();
178 let (wrong_xonly, _) = wrong_sk.public_key(&secp).x_only_public_key();
179 let tweaked_pk = TweakedPublicKey::dangerous_assume_tweaked(wrong_xonly);
180 let script = ScriptBuf::new_p2tr_tweaked(tweaked_pk);
181
182 let detected = DetectedOutput::new(
183 OutPoint::null(),
184 Amount::from_sat(100_000),
185 script,
186 tweak,
187 None,
188 );
189
190 let result = detected.derive_spend_key(&spend_sk, &secp);
191 assert!(
192 matches!(result, Err(ReceiveError::SpendKeyDerivation(_))),
193 "should fail with script mismatch, got: {:?}",
194 result
195 );
196 }
197
198 #[test]
199 fn tweak_secret_key_accessor() {
200 let tweak = SecretKey::from_slice(&TWEAK_BYTES).unwrap();
201 let detected = DetectedOutput::new(
202 OutPoint::null(),
203 Amount::from_sat(50_000),
204 ScriptBuf::new(),
205 tweak,
206 Some(1),
207 );
208
209 assert_eq!(detected.tweak_secret_key().secret_bytes(), TWEAK_BYTES);
210 assert_eq!(detected.label, Some(1));
211 }
212
213 #[test]
214 fn new_sets_all_fields_correctly() {
215 let tweak = SecretKey::from_slice(&TWEAK_BYTES).unwrap();
216 let outpoint = OutPoint::null();
217 let amount = Amount::from_sat(42_000);
218 let script = ScriptBuf::new();
219
220 let detected = DetectedOutput::new(outpoint, amount, script.clone(), tweak, Some(5));
221
222 assert_eq!(detected.outpoint, outpoint);
223 assert_eq!(detected.amount, amount);
224 assert_eq!(detected.script_pubkey, script);
225 assert_eq!(detected.label, Some(5));
226 }
227}