use crate::primitives::hash::sha256;
use crate::primitives::public_key::PublicKey;
use crate::primitives::transaction_signature::{SIGHASH_ALL, SIGHASH_FORKID};
use crate::script::error::ScriptError;
use crate::script::locking_script::LockingScript;
use crate::script::op::Op;
use crate::script::script::Script;
use crate::script::script_chunk::ScriptChunk;
use crate::script::unlocking_script::UnlockingScript;
use crate::wallet::interfaces::{CreateSignatureArgs, GetPublicKeyArgs, WalletInterface};
use crate::wallet::types::{Counterparty, Protocol};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum LockPosition {
#[default]
Before,
After,
}
#[derive(Clone, Debug)]
pub struct PushDropData {
pub locking_public_key: PublicKey,
pub fields: Vec<Vec<u8>>,
}
pub struct PushDrop<'a, W: WalletInterface + ?Sized> {
pub wallet: &'a W,
pub originator: Option<String>,
}
impl<'a, W: WalletInterface + ?Sized> PushDrop<'a, W> {
pub fn new(wallet: &'a W, originator: Option<String>) -> Self {
Self { wallet, originator }
}
#[allow(clippy::too_many_arguments)]
pub async fn lock(
&self,
mut fields: Vec<Vec<u8>>,
protocol_id: Protocol,
key_id: &str,
counterparty: Counterparty,
for_self: bool,
include_signature: bool,
lock_position: LockPosition,
) -> Result<LockingScript, ScriptError> {
if fields.is_empty() && !include_signature {
return Err(ScriptError::InvalidScript(
"PushDrop: at least one data field required".into(),
));
}
let pk = self
.wallet
.get_public_key(
GetPublicKeyArgs {
identity_key: false,
protocol_id: Some(protocol_id.clone()),
key_id: Some(key_id.to_string()),
counterparty: Some(counterparty.clone()),
privileged: false,
privileged_reason: None,
for_self: Some(for_self),
seek_permission: None,
},
self.originator.as_deref(),
)
.await
.map_err(|e| ScriptError::InvalidScript(format!("PushDrop lock: getPublicKey: {e}")))?;
let pubkey_bytes = pk.public_key.to_der();
let mut lock_chunks = vec![
ScriptChunk::new_raw(pubkey_bytes.len() as u8, Some(pubkey_bytes)),
ScriptChunk::new_opcode(Op::OpCheckSig),
];
if include_signature {
let data_to_sign: Vec<u8> = fields.concat();
let sig = self
.wallet
.create_signature(
CreateSignatureArgs {
protocol_id: protocol_id.clone(),
key_id: key_id.to_string(),
counterparty: counterparty.clone(),
data: Some(data_to_sign),
hash_to_directly_sign: None,
privileged: false,
privileged_reason: None,
seek_permission: None,
},
self.originator.as_deref(),
)
.await
.map_err(|e| {
ScriptError::InvalidScript(format!("PushDrop lock: createSignature: {e}"))
})?;
fields.push(sig.signature);
}
let mut push_drop_chunks: Vec<ScriptChunk> =
fields.iter().map(|f| make_data_push(f)).collect();
let mut not_yet_dropped = fields.len();
while not_yet_dropped > 1 {
push_drop_chunks.push(ScriptChunk::new_opcode(Op::Op2Drop));
not_yet_dropped -= 2;
}
if not_yet_dropped != 0 {
push_drop_chunks.push(ScriptChunk::new_opcode(Op::OpDrop));
}
let chunks = match lock_position {
LockPosition::Before => {
lock_chunks.extend(push_drop_chunks);
lock_chunks
}
LockPosition::After => {
push_drop_chunks.extend(lock_chunks);
push_drop_chunks
}
};
Ok(LockingScript::from_script(Script::from_chunks(chunks)))
}
pub async fn unlock(
&self,
preimage: &[u8],
protocol_id: Protocol,
key_id: &str,
counterparty: Counterparty,
sighash_type: u8,
) -> Result<UnlockingScript, ScriptError> {
let preimage_hash = sha256(preimage);
let sig = self
.wallet
.create_signature(
CreateSignatureArgs {
protocol_id,
key_id: key_id.to_string(),
counterparty,
data: Some(preimage_hash.to_vec()),
hash_to_directly_sign: None,
privileged: false,
privileged_reason: None,
seek_permission: None,
},
self.originator.as_deref(),
)
.await
.map_err(|e| {
ScriptError::InvalidScript(format!("PushDrop unlock: createSignature: {e}"))
})?;
let mut sig_bytes = sig.signature;
sig_bytes.push(sighash_type);
let chunks = vec![ScriptChunk::new_raw(sig_bytes.len() as u8, Some(sig_bytes))];
Ok(UnlockingScript::from_script(Script::from_chunks(chunks)))
}
pub fn default_sighash_type() -> u8 {
(SIGHASH_ALL | SIGHASH_FORKID) as u8
}
pub fn estimate_unlock_length() -> usize {
73
}
}
pub fn decode(script: &LockingScript) -> Result<PushDropData, ScriptError> {
decode_with_position(script, LockPosition::Before)
}
pub fn decode_with_position(
script: &LockingScript,
position: LockPosition,
) -> Result<PushDropData, ScriptError> {
let chunks = script.chunks();
if chunks.len() < 3 {
return Err(ScriptError::InvalidScript(
"PushDrop::decode: script too short".into(),
));
}
match position {
LockPosition::Before => decode_before(chunks),
LockPosition::After => decode_after(chunks),
}
}
impl PushDropData {
pub fn decode(script: &LockingScript) -> Result<PushDropData, ScriptError> {
decode(script)
}
}
fn decode_before(chunks: &[ScriptChunk]) -> Result<PushDropData, ScriptError> {
if chunks[0].data.is_none() || chunks[1].op != Op::OpCheckSig {
return Err(ScriptError::InvalidScript(
"PushDrop::decode(before): expected <pubkey> OP_CHECKSIG at start".into(),
));
}
let locking_public_key = PublicKey::from_der_bytes(chunks[0].data.as_ref().unwrap())
.map_err(|e| ScriptError::InvalidScript(format!("PushDrop::decode: pubkey: {e}")))?;
let mut fields = Vec::new();
for i in 2..chunks.len() {
let next_is_drop = chunks
.get(i + 1)
.is_some_and(|next| next.op == Op::OpDrop || next.op == Op::Op2Drop);
if chunks[i].op == Op::OpDrop || chunks[i].op == Op::Op2Drop {
break;
}
fields.push(decode_field(&chunks[i]));
if next_is_drop {
break;
}
}
Ok(PushDropData {
locking_public_key,
fields,
})
}
fn decode_after(chunks: &[ScriptChunk]) -> Result<PushDropData, ScriptError> {
let last = &chunks[chunks.len() - 1];
if last.op != Op::OpCheckSig {
return Err(ScriptError::InvalidScript(
"PushDrop::decode(after): last opcode must be OP_CHECKSIG".into(),
));
}
let pubkey_chunk = &chunks[chunks.len() - 2];
let pubkey_bytes = pubkey_chunk.data.as_ref().ok_or_else(|| {
ScriptError::InvalidScript(
"PushDrop::decode(after): expected pubkey before OP_CHECKSIG".into(),
)
})?;
let locking_public_key = PublicKey::from_der_bytes(pubkey_bytes)
.map_err(|e| ScriptError::InvalidScript(format!("PushDrop::decode: pubkey: {e}")))?;
let mut drop_field_count = 0usize;
let mut pos = chunks.len() - 3;
loop {
let chunk = &chunks[pos];
if chunk.op == Op::Op2Drop {
drop_field_count += 2;
} else if chunk.op == Op::OpDrop {
drop_field_count += 1;
} else {
break;
}
if pos == 0 {
break;
}
pos -= 1;
}
if drop_field_count == 0 {
return Err(ScriptError::InvalidScript(
"PushDrop::decode(after): no OP_DROP/OP_2DROP found".into(),
));
}
if drop_field_count > chunks.len() {
return Err(ScriptError::InvalidScript(
"PushDrop::decode(after): drop count exceeds script length".into(),
));
}
let fields = chunks[0..drop_field_count]
.iter()
.map(decode_field)
.collect();
Ok(PushDropData {
locking_public_key,
fields,
})
}
fn make_data_push(data: &[u8]) -> ScriptChunk {
if data.is_empty() {
return ScriptChunk::new_opcode(Op::Op0);
}
if data.len() == 1 {
let b = data[0];
if b == 0 {
return ScriptChunk::new_opcode(Op::Op0);
}
if (1..=16).contains(&b) {
return ScriptChunk::new_raw(0x50 + b, None);
}
if b == 0x81 {
return ScriptChunk::new_opcode(Op::Op1Negate);
}
}
let len = data.len();
if len < 0x4c {
ScriptChunk::new_raw(len as u8, Some(data.to_vec()))
} else if len < 256 {
ScriptChunk::new_raw(Op::OpPushData1.to_byte(), Some(data.to_vec()))
} else if len < 65536 {
ScriptChunk::new_raw(Op::OpPushData2.to_byte(), Some(data.to_vec()))
} else {
ScriptChunk::new_raw(Op::OpPushData4.to_byte(), Some(data.to_vec()))
}
}
fn decode_field(chunk: &ScriptChunk) -> Vec<u8> {
if let Some(data) = &chunk.data {
if !data.is_empty() {
return data.clone();
}
}
match chunk.op_byte {
0x50..=0x60 => vec![chunk.op_byte - 0x50],
0x00 => vec![0], 0x4f => vec![0x81], _ => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::private_key::PrivateKey;
use crate::wallet::proto_wallet::ProtoWallet;
fn wallet() -> ProtoWallet {
ProtoWallet::new(PrivateKey::from_bytes(&[0x55u8; 32]).unwrap())
}
fn protocol() -> Protocol {
Protocol {
security_level: 2,
protocol: "did revocation".to_string(),
}
}
fn cpty() -> Counterparty {
Counterparty {
counterparty_type: crate::wallet::types::CounterpartyType::Self_,
public_key: None,
}
}
#[tokio::test]
async fn lock_derives_the_pubkey_from_the_wallet_not_the_raw_key() {
let script = PushDrop::new(&wallet(), None)
.lock(
vec![b"hello".to_vec()],
protocol(),
"k",
cpty(),
false,
false,
LockPosition::Before,
)
.await
.unwrap();
let decoded = decode(&script).unwrap();
let raw = PrivateKey::from_bytes(&[0x55u8; 32])
.unwrap()
.to_public_key();
assert_ne!(
decoded.locking_public_key.to_der_hex(),
raw.to_der_hex(),
"the locking key must be a BRC-42 DERIVED child, never the raw key"
);
assert_eq!(decoded.fields, vec![b"hello".to_vec()]);
}
#[tokio::test]
async fn include_signature_appends_the_signature_as_an_extra_field() {
let fields = vec![b"a".to_vec(), b"b".to_vec()];
let no_sig = PushDrop::new(&wallet(), None)
.lock(
fields.clone(),
protocol(),
"k",
cpty(),
false,
false,
LockPosition::Before,
)
.await
.unwrap();
let with_sig = PushDrop::new(&wallet(), None)
.lock(
fields.clone(),
protocol(),
"k",
cpty(),
false,
true,
LockPosition::Before,
)
.await
.unwrap();
let d_no = decode(&no_sig).unwrap();
let d_with = decode(&with_sig).unwrap();
assert_eq!(d_no.fields.len(), 2);
assert_eq!(d_with.fields.len(), 3, "the signature is an extra field");
assert_eq!(&d_with.fields[..2], &fields[..]);
assert!(
d_with.fields[2].starts_with(&[0x30]),
"the appended field is a DER signature"
);
}
#[tokio::test]
async fn minimally_encoded_fields_round_trip() {
let fields = vec![
vec![0x01],
vec![0x10],
vec![0x81],
vec![0x00],
b"abc".to_vec(),
];
let script = PushDrop::new(&wallet(), None)
.lock(
fields.clone(),
protocol(),
"k",
cpty(),
false,
false,
LockPosition::Before,
)
.await
.unwrap();
let hex = script.to_hex();
assert!(hex.contains("51"), "[1] must encode as OP_1");
assert!(hex.contains("60"), "[16] must encode as OP_16");
assert!(hex.contains("4f"), "[0x81] must encode as OP_1NEGATE");
let decoded = decode(&script).unwrap();
assert_eq!(decoded.fields, fields);
}
#[tokio::test]
async fn after_position_round_trips() {
let fields = vec![b"x".to_vec(), b"y".to_vec()];
let script = PushDrop::new(&wallet(), None)
.lock(
fields.clone(),
protocol(),
"k",
cpty(),
false,
false,
LockPosition::After,
)
.await
.unwrap();
let decoded = decode_with_position(&script, LockPosition::After).unwrap();
assert_eq!(decoded.fields, fields);
}
#[tokio::test]
async fn unlock_produces_a_der_signature_with_the_sighash_byte() {
let sig = PushDrop::new(&wallet(), None)
.unlock(
b"preimage",
protocol(),
"k",
cpty(),
PushDrop::<ProtoWallet>::default_sighash_type(),
)
.await
.unwrap();
let chunks = sig.chunks();
assert_eq!(chunks.len(), 1);
let data = chunks[0].data.as_ref().unwrap();
assert_eq!(data[0], 0x30, "DER sequence");
assert_eq!(
*data.last().unwrap(),
PushDrop::<ProtoWallet>::default_sighash_type()
);
}
#[test]
fn decode_non_pushdrop_errors() {
let script = LockingScript::from_hex("76a914").expect("parses as a script");
assert!(decode(&script).is_err(), "a P2PKH prefix is not a PushDrop");
}
}