1use crate::primitives::hash::sha256;
28use crate::primitives::public_key::PublicKey;
29use crate::primitives::transaction_signature::{SIGHASH_ALL, SIGHASH_FORKID};
30use crate::script::error::ScriptError;
31use crate::script::locking_script::LockingScript;
32use crate::script::op::Op;
33use crate::script::script::Script;
34use crate::script::script_chunk::ScriptChunk;
35use crate::script::unlocking_script::UnlockingScript;
36use crate::wallet::interfaces::{CreateSignatureArgs, GetPublicKeyArgs, WalletInterface};
37use crate::wallet::types::{Counterparty, Protocol};
38
39#[derive(Clone, Copy, Debug, Default, PartialEq)]
41pub enum LockPosition {
42 #[default]
44 Before,
45 After,
47}
48
49#[derive(Clone, Debug)]
55pub struct PushDropData {
56 pub locking_public_key: PublicKey,
58 pub fields: Vec<Vec<u8>>,
61}
62
63pub struct PushDrop<'a, W: WalletInterface + ?Sized> {
72 pub wallet: &'a W,
74 pub originator: Option<String>,
76}
77
78impl<'a, W: WalletInterface + ?Sized> PushDrop<'a, W> {
79 pub fn new(wallet: &'a W, originator: Option<String>) -> Self {
81 Self { wallet, originator }
82 }
83
84 #[allow(clippy::too_many_arguments)]
94 pub async fn lock(
95 &self,
96 mut fields: Vec<Vec<u8>>,
97 protocol_id: Protocol,
98 key_id: &str,
99 counterparty: Counterparty,
100 for_self: bool,
101 include_signature: bool,
102 lock_position: LockPosition,
103 ) -> Result<LockingScript, ScriptError> {
104 if fields.is_empty() && !include_signature {
105 return Err(ScriptError::InvalidScript(
106 "PushDrop: at least one data field required".into(),
107 ));
108 }
109
110 let pk = self
111 .wallet
112 .get_public_key(
113 GetPublicKeyArgs {
114 identity_key: false,
115 protocol_id: Some(protocol_id.clone()),
116 key_id: Some(key_id.to_string()),
117 counterparty: Some(counterparty.clone()),
118 privileged: false,
119 privileged_reason: None,
120 for_self: Some(for_self),
121 seek_permission: None,
122 },
123 self.originator.as_deref(),
124 )
125 .await
126 .map_err(|e| ScriptError::InvalidScript(format!("PushDrop lock: getPublicKey: {e}")))?;
127
128 let pubkey_bytes = pk.public_key.to_der();
129 let mut lock_chunks = vec![
130 ScriptChunk::new_raw(pubkey_bytes.len() as u8, Some(pubkey_bytes)),
131 ScriptChunk::new_opcode(Op::OpCheckSig),
132 ];
133
134 if include_signature {
135 let data_to_sign: Vec<u8> = fields.concat();
138 let sig = self
139 .wallet
140 .create_signature(
141 CreateSignatureArgs {
142 protocol_id: protocol_id.clone(),
143 key_id: key_id.to_string(),
144 counterparty: counterparty.clone(),
145 data: Some(data_to_sign),
146 hash_to_directly_sign: None,
147 privileged: false,
148 privileged_reason: None,
149 seek_permission: None,
150 },
151 self.originator.as_deref(),
152 )
153 .await
154 .map_err(|e| {
155 ScriptError::InvalidScript(format!("PushDrop lock: createSignature: {e}"))
156 })?;
157 fields.push(sig.signature);
158 }
159
160 let mut push_drop_chunks: Vec<ScriptChunk> =
161 fields.iter().map(|f| make_data_push(f)).collect();
162
163 let mut not_yet_dropped = fields.len();
165 while not_yet_dropped > 1 {
166 push_drop_chunks.push(ScriptChunk::new_opcode(Op::Op2Drop));
167 not_yet_dropped -= 2;
168 }
169 if not_yet_dropped != 0 {
170 push_drop_chunks.push(ScriptChunk::new_opcode(Op::OpDrop));
171 }
172
173 let chunks = match lock_position {
174 LockPosition::Before => {
175 lock_chunks.extend(push_drop_chunks);
176 lock_chunks
177 }
178 LockPosition::After => {
179 push_drop_chunks.extend(lock_chunks);
180 push_drop_chunks
181 }
182 };
183
184 Ok(LockingScript::from_script(Script::from_chunks(chunks)))
185 }
186
187 pub async fn unlock(
195 &self,
196 preimage: &[u8],
197 protocol_id: Protocol,
198 key_id: &str,
199 counterparty: Counterparty,
200 sighash_type: u8,
201 ) -> Result<UnlockingScript, ScriptError> {
202 let preimage_hash = sha256(preimage);
203
204 let sig = self
205 .wallet
206 .create_signature(
207 CreateSignatureArgs {
208 protocol_id,
209 key_id: key_id.to_string(),
210 counterparty,
211 data: Some(preimage_hash.to_vec()),
212 hash_to_directly_sign: None,
213 privileged: false,
214 privileged_reason: None,
215 seek_permission: None,
216 },
217 self.originator.as_deref(),
218 )
219 .await
220 .map_err(|e| {
221 ScriptError::InvalidScript(format!("PushDrop unlock: createSignature: {e}"))
222 })?;
223
224 let mut sig_bytes = sig.signature;
225 sig_bytes.push(sighash_type);
226
227 let chunks = vec![ScriptChunk::new_raw(sig_bytes.len() as u8, Some(sig_bytes))];
228 Ok(UnlockingScript::from_script(Script::from_chunks(chunks)))
229 }
230
231 pub fn default_sighash_type() -> u8 {
233 (SIGHASH_ALL | SIGHASH_FORKID) as u8
234 }
235
236 pub fn estimate_unlock_length() -> usize {
238 73
239 }
240}
241
242pub fn decode(script: &LockingScript) -> Result<PushDropData, ScriptError> {
248 decode_with_position(script, LockPosition::Before)
249}
250
251pub fn decode_with_position(
253 script: &LockingScript,
254 position: LockPosition,
255) -> Result<PushDropData, ScriptError> {
256 let chunks = script.chunks();
257 if chunks.len() < 3 {
258 return Err(ScriptError::InvalidScript(
259 "PushDrop::decode: script too short".into(),
260 ));
261 }
262
263 match position {
264 LockPosition::Before => decode_before(chunks),
265 LockPosition::After => decode_after(chunks),
266 }
267}
268
269impl PushDropData {
270 pub fn decode(script: &LockingScript) -> Result<PushDropData, ScriptError> {
272 decode(script)
273 }
274}
275
276fn decode_before(chunks: &[ScriptChunk]) -> Result<PushDropData, ScriptError> {
278 if chunks[0].data.is_none() || chunks[1].op != Op::OpCheckSig {
279 return Err(ScriptError::InvalidScript(
280 "PushDrop::decode(before): expected <pubkey> OP_CHECKSIG at start".into(),
281 ));
282 }
283 let locking_public_key = PublicKey::from_der_bytes(chunks[0].data.as_ref().unwrap())
284 .map_err(|e| ScriptError::InvalidScript(format!("PushDrop::decode: pubkey: {e}")))?;
285
286 let mut fields = Vec::new();
287 for i in 2..chunks.len() {
288 let next_is_drop = chunks
289 .get(i + 1)
290 .is_some_and(|next| next.op == Op::OpDrop || next.op == Op::Op2Drop);
291
292 if chunks[i].op == Op::OpDrop || chunks[i].op == Op::Op2Drop {
293 break;
294 }
295
296 fields.push(decode_field(&chunks[i]));
299
300 if next_is_drop {
301 break;
302 }
303 }
304
305 Ok(PushDropData {
306 locking_public_key,
307 fields,
308 })
309}
310
311fn decode_after(chunks: &[ScriptChunk]) -> Result<PushDropData, ScriptError> {
313 let last = &chunks[chunks.len() - 1];
314 if last.op != Op::OpCheckSig {
315 return Err(ScriptError::InvalidScript(
316 "PushDrop::decode(after): last opcode must be OP_CHECKSIG".into(),
317 ));
318 }
319 let pubkey_chunk = &chunks[chunks.len() - 2];
320 let pubkey_bytes = pubkey_chunk.data.as_ref().ok_or_else(|| {
321 ScriptError::InvalidScript(
322 "PushDrop::decode(after): expected pubkey before OP_CHECKSIG".into(),
323 )
324 })?;
325 let locking_public_key = PublicKey::from_der_bytes(pubkey_bytes)
326 .map_err(|e| ScriptError::InvalidScript(format!("PushDrop::decode: pubkey: {e}")))?;
327
328 let mut drop_field_count = 0usize;
330 let mut pos = chunks.len() - 3;
331 loop {
332 let chunk = &chunks[pos];
333 if chunk.op == Op::Op2Drop {
334 drop_field_count += 2;
335 } else if chunk.op == Op::OpDrop {
336 drop_field_count += 1;
337 } else {
338 break;
339 }
340 if pos == 0 {
341 break;
342 }
343 pos -= 1;
344 }
345
346 if drop_field_count == 0 {
347 return Err(ScriptError::InvalidScript(
348 "PushDrop::decode(after): no OP_DROP/OP_2DROP found".into(),
349 ));
350 }
351 if drop_field_count > chunks.len() {
352 return Err(ScriptError::InvalidScript(
353 "PushDrop::decode(after): drop count exceeds script length".into(),
354 ));
355 }
356
357 let fields = chunks[0..drop_field_count]
361 .iter()
362 .map(decode_field)
363 .collect();
364
365 Ok(PushDropData {
366 locking_public_key,
367 fields,
368 })
369}
370
371fn make_data_push(data: &[u8]) -> ScriptChunk {
384 if data.is_empty() {
385 return ScriptChunk::new_opcode(Op::Op0);
386 }
387 if data.len() == 1 {
388 let b = data[0];
389 if b == 0 {
390 return ScriptChunk::new_opcode(Op::Op0);
391 }
392 if (1..=16).contains(&b) {
393 return ScriptChunk::new_raw(0x50 + b, None);
395 }
396 if b == 0x81 {
397 return ScriptChunk::new_opcode(Op::Op1Negate);
398 }
399 }
400
401 let len = data.len();
402 if len < 0x4c {
403 ScriptChunk::new_raw(len as u8, Some(data.to_vec()))
404 } else if len < 256 {
405 ScriptChunk::new_raw(Op::OpPushData1.to_byte(), Some(data.to_vec()))
406 } else if len < 65536 {
407 ScriptChunk::new_raw(Op::OpPushData2.to_byte(), Some(data.to_vec()))
408 } else {
409 ScriptChunk::new_raw(Op::OpPushData4.to_byte(), Some(data.to_vec()))
410 }
411}
412
413fn decode_field(chunk: &ScriptChunk) -> Vec<u8> {
429 if let Some(data) = &chunk.data {
430 if !data.is_empty() {
431 return data.clone();
432 }
433 }
434 match chunk.op_byte {
435 0x50..=0x60 => vec![chunk.op_byte - 0x50],
437 0x00 => vec![0], 0x4f => vec![0x81], _ => Vec::new(),
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use crate::primitives::private_key::PrivateKey;
447 use crate::wallet::proto_wallet::ProtoWallet;
448
449 fn wallet() -> ProtoWallet {
450 ProtoWallet::new(PrivateKey::from_bytes(&[0x55u8; 32]).unwrap())
451 }
452
453 fn protocol() -> Protocol {
454 Protocol {
455 security_level: 2,
456 protocol: "did revocation".to_string(),
457 }
458 }
459
460 fn cpty() -> Counterparty {
461 Counterparty {
462 counterparty_type: crate::wallet::types::CounterpartyType::Self_,
463 public_key: None,
464 }
465 }
466
467 #[tokio::test]
468 async fn lock_derives_the_pubkey_from_the_wallet_not_the_raw_key() {
469 let script = PushDrop::new(&wallet(), None)
470 .lock(
471 vec![b"hello".to_vec()],
472 protocol(),
473 "k",
474 cpty(),
475 false,
476 false,
477 LockPosition::Before,
478 )
479 .await
480 .unwrap();
481
482 let decoded = decode(&script).unwrap();
483 let raw = PrivateKey::from_bytes(&[0x55u8; 32])
484 .unwrap()
485 .to_public_key();
486
487 assert_ne!(
488 decoded.locking_public_key.to_der_hex(),
489 raw.to_der_hex(),
490 "the locking key must be a BRC-42 DERIVED child, never the raw key"
491 );
492 assert_eq!(decoded.fields, vec![b"hello".to_vec()]);
493 }
494
495 #[tokio::test]
498 async fn include_signature_appends_the_signature_as_an_extra_field() {
499 let fields = vec![b"a".to_vec(), b"b".to_vec()];
500
501 let no_sig = PushDrop::new(&wallet(), None)
502 .lock(
503 fields.clone(),
504 protocol(),
505 "k",
506 cpty(),
507 false,
508 false,
509 LockPosition::Before,
510 )
511 .await
512 .unwrap();
513 let with_sig = PushDrop::new(&wallet(), None)
514 .lock(
515 fields.clone(),
516 protocol(),
517 "k",
518 cpty(),
519 false,
520 true,
521 LockPosition::Before,
522 )
523 .await
524 .unwrap();
525
526 let d_no = decode(&no_sig).unwrap();
527 let d_with = decode(&with_sig).unwrap();
528
529 assert_eq!(d_no.fields.len(), 2);
530 assert_eq!(d_with.fields.len(), 3, "the signature is an extra field");
531 assert_eq!(&d_with.fields[..2], &fields[..]);
532 assert!(
533 d_with.fields[2].starts_with(&[0x30]),
534 "the appended field is a DER signature"
535 );
536 }
537
538 #[tokio::test]
540 async fn minimally_encoded_fields_round_trip() {
541 let fields = vec![
542 vec![0x01],
543 vec![0x10],
544 vec![0x81],
545 vec![0x00],
546 b"abc".to_vec(),
547 ];
548 let script = PushDrop::new(&wallet(), None)
549 .lock(
550 fields.clone(),
551 protocol(),
552 "k",
553 cpty(),
554 false,
555 false,
556 LockPosition::Before,
557 )
558 .await
559 .unwrap();
560
561 let hex = script.to_hex();
562 assert!(hex.contains("51"), "[1] must encode as OP_1");
563 assert!(hex.contains("60"), "[16] must encode as OP_16");
564 assert!(hex.contains("4f"), "[0x81] must encode as OP_1NEGATE");
565
566 let decoded = decode(&script).unwrap();
567 assert_eq!(decoded.fields, fields);
568 }
569
570 #[tokio::test]
571 async fn after_position_round_trips() {
572 let fields = vec![b"x".to_vec(), b"y".to_vec()];
573 let script = PushDrop::new(&wallet(), None)
574 .lock(
575 fields.clone(),
576 protocol(),
577 "k",
578 cpty(),
579 false,
580 false,
581 LockPosition::After,
582 )
583 .await
584 .unwrap();
585
586 let decoded = decode_with_position(&script, LockPosition::After).unwrap();
587 assert_eq!(decoded.fields, fields);
588 }
589
590 #[tokio::test]
591 async fn unlock_produces_a_der_signature_with_the_sighash_byte() {
592 let sig = PushDrop::new(&wallet(), None)
593 .unlock(
594 b"preimage",
595 protocol(),
596 "k",
597 cpty(),
598 PushDrop::<ProtoWallet>::default_sighash_type(),
599 )
600 .await
601 .unwrap();
602
603 let chunks = sig.chunks();
604 assert_eq!(chunks.len(), 1);
605 let data = chunks[0].data.as_ref().unwrap();
606 assert_eq!(data[0], 0x30, "DER sequence");
607 assert_eq!(
608 *data.last().unwrap(),
609 PushDrop::<ProtoWallet>::default_sighash_type()
610 );
611 }
612
613 #[test]
614 fn decode_non_pushdrop_errors() {
615 let script = LockingScript::from_hex("76a914").expect("parses as a script");
616 assert!(decode(&script).is_err(), "a P2PKH prefix is not a PushDrop");
617 }
618}