use crate::error::Error;
use crate::primitives::bsv::TransactionSignature;
use crate::primitives::ec::{PrivateKey, PublicKey};
use crate::script::op::*;
use crate::script::template::{
compute_sighash_scope, ScriptTemplateUnlock, SignOutputs, SigningContext,
};
use crate::script::{LockingScript, Script, ScriptChunk, UnlockingScript};
use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LockPosition {
#[default]
Before,
After,
}
#[derive(Debug, Clone)]
pub struct PushDrop {
pub locking_public_key: PublicKey,
pub fields: Vec<Vec<u8>>,
pub lock_position: LockPosition,
}
impl PushDrop {
pub fn new(locking_public_key: PublicKey, fields: Vec<Vec<u8>>) -> Self {
Self {
locking_public_key,
fields,
lock_position: LockPosition::Before,
}
}
pub fn with_position(mut self, position: LockPosition) -> Self {
self.lock_position = position;
self
}
pub fn lock(&self) -> LockingScript {
let mut chunks = Vec::new();
match self.lock_position {
LockPosition::Before => {
chunks.push(ScriptChunk::new_push(
self.locking_public_key.to_compressed().to_vec(),
));
chunks.push(ScriptChunk::new_opcode(OP_CHECKSIG));
for field in &self.fields {
chunks.push(Self::create_minimally_encoded_chunk(field));
}
Self::add_drop_operations(&mut chunks, self.fields.len());
}
LockPosition::After => {
for field in &self.fields {
chunks.push(Self::create_minimally_encoded_chunk(field));
}
Self::add_drop_operations(&mut chunks, self.fields.len());
chunks.push(ScriptChunk::new_push(
self.locking_public_key.to_compressed().to_vec(),
));
chunks.push(ScriptChunk::new_opcode(OP_CHECKSIG));
}
}
LockingScript::from_chunks(chunks)
}
fn create_minimally_encoded_chunk(data: &[u8]) -> ScriptChunk {
if data.is_empty() || (data.len() == 1 && data[0] == 0) {
return ScriptChunk::new_opcode(OP_0);
}
if data.len() == 1 {
let byte = data[0];
if (1..=16).contains(&byte) {
return ScriptChunk::new_opcode(0x50 + byte);
}
if byte == 0x81 {
return ScriptChunk::new_opcode(OP_1NEGATE);
}
}
ScriptChunk::new_push(data.to_vec())
}
fn add_drop_operations(chunks: &mut Vec<ScriptChunk>, count: usize) {
let mut remaining = count;
while remaining >= 2 {
chunks.push(ScriptChunk::new_opcode(OP_2DROP));
remaining -= 2;
}
if remaining == 1 {
chunks.push(ScriptChunk::new_opcode(OP_DROP));
}
}
pub fn decode(script: &LockingScript) -> Result<Self> {
let chunks = script.chunks();
if chunks.len() < 2 {
return Err(Error::ScriptParseError(
"Script too short for PushDrop".into(),
));
}
let first_is_pubkey = chunks[0]
.data
.as_ref()
.map(|d| d.len() == 33 || d.len() == 65)
.unwrap_or(false);
if first_is_pubkey {
Self::decode_lock_before(&chunks)
} else {
Self::decode_lock_after(&chunks)
}
}
fn decode_lock_before(chunks: &[ScriptChunk]) -> Result<Self> {
let pubkey_data = chunks[0]
.data
.as_ref()
.ok_or_else(|| Error::ScriptParseError("Expected public key".into()))?;
let locking_public_key = PublicKey::from_bytes(pubkey_data)?;
if chunks[1].op != OP_CHECKSIG {
return Err(Error::ScriptParseError(
"Expected OP_CHECKSIG after pubkey".into(),
));
}
let mut fields = Vec::new();
for chunk in chunks.iter().skip(2) {
if chunk.op == OP_DROP || chunk.op == OP_2DROP {
break;
}
fields.push(Self::chunk_to_bytes(chunk));
}
Ok(Self {
locking_public_key,
fields,
lock_position: LockPosition::Before,
})
}
fn decode_lock_after(chunks: &[ScriptChunk]) -> Result<Self> {
if chunks.len() < 2 {
return Err(Error::ScriptParseError("Script too short".into()));
}
let last_idx = chunks.len() - 1;
if chunks[last_idx].op != OP_CHECKSIG {
return Err(Error::ScriptParseError(
"Expected OP_CHECKSIG at end".into(),
));
}
let pubkey_data = chunks[last_idx - 1].data.as_ref().ok_or_else(|| {
Error::ScriptParseError("Expected public key before OP_CHECKSIG".into())
})?;
let locking_public_key = PublicKey::from_bytes(pubkey_data)?;
let mut fields = Vec::new();
for chunk in chunks.iter().take(last_idx - 1) {
if chunk.op == OP_DROP || chunk.op == OP_2DROP {
break;
}
fields.push(Self::chunk_to_bytes(chunk));
}
Ok(Self {
locking_public_key,
fields,
lock_position: LockPosition::After,
})
}
fn chunk_to_bytes(chunk: &ScriptChunk) -> Vec<u8> {
if let Some(ref data) = chunk.data {
return data.clone();
}
let op = chunk.op;
if op == OP_0 {
return vec![0];
}
if (OP_1..=OP_16).contains(&op) {
return vec![op - 0x50];
}
if op == OP_1NEGATE {
return vec![0x81];
}
Vec::new()
}
pub fn unlock(
private_key: &PrivateKey,
sign_outputs: SignOutputs,
anyone_can_pay: bool,
) -> ScriptTemplateUnlock {
let key = private_key.clone();
let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
ScriptTemplateUnlock::new(
move |context: &SigningContext| {
let sighash = context.compute_sighash(scope)?;
let signature = key.sign(&sighash)?;
let tx_sig = TransactionSignature::new(signature, scope);
let sig_bytes = tx_sig.to_checksig_format();
let mut script = Script::new();
script.write_bin(&sig_bytes);
Ok(UnlockingScript::from_script(script))
},
|| {
73
},
)
}
pub fn sign_with_sighash(
private_key: &PrivateKey,
sighash: &[u8; 32],
sign_outputs: SignOutputs,
anyone_can_pay: bool,
) -> Result<UnlockingScript> {
let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
let signature = private_key.sign(sighash)?;
let tx_sig = TransactionSignature::new(signature, scope);
let sig_bytes = tx_sig.to_checksig_format();
let mut script = Script::new();
script.write_bin(&sig_bytes);
Ok(UnlockingScript::from_script(script))
}
pub fn estimate_unlocking_length(&self) -> usize {
73
}
}
impl PartialEq for PushDrop {
fn eq(&self, other: &Self) -> bool {
self.locking_public_key.to_compressed() == other.locking_public_key.to_compressed()
&& self.fields == other.fields
&& self.lock_position == other.lock_position
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::ec::PrivateKey;
#[test]
fn test_pushdrop_lock_before() {
let privkey = PrivateKey::random();
let pubkey = privkey.public_key();
let fields = vec![b"field1".to_vec(), b"field2".to_vec()];
let pushdrop = PushDrop::new(pubkey.clone(), fields.clone());
let script = pushdrop.lock();
let decoded = PushDrop::decode(&script).unwrap();
assert_eq!(
decoded.locking_public_key.to_compressed(),
pubkey.to_compressed()
);
assert_eq!(decoded.fields, fields);
assert_eq!(decoded.lock_position, LockPosition::Before);
}
#[test]
fn test_pushdrop_lock_after() {
let privkey = PrivateKey::random();
let pubkey = privkey.public_key();
let fields = vec![b"data".to_vec()];
let pushdrop =
PushDrop::new(pubkey.clone(), fields.clone()).with_position(LockPosition::After);
let script = pushdrop.lock();
let decoded = PushDrop::decode(&script).unwrap();
assert_eq!(decoded.lock_position, LockPosition::After);
assert_eq!(
decoded.locking_public_key.to_compressed(),
pubkey.to_compressed()
);
}
#[test]
fn test_pushdrop_minimal_encoding() {
let privkey = PrivateKey::random();
let pubkey = privkey.public_key();
let fields = vec![
vec![0], vec![1], vec![16], vec![0x81], vec![17], ];
let pushdrop = PushDrop::new(pubkey, fields.clone());
let script = pushdrop.lock();
let decoded = PushDrop::decode(&script).unwrap();
assert_eq!(decoded.fields.len(), 5);
assert_eq!(decoded.fields[0], vec![0]);
assert_eq!(decoded.fields[1], vec![1]);
assert_eq!(decoded.fields[2], vec![16]);
assert_eq!(decoded.fields[3], vec![0x81]);
assert_eq!(decoded.fields[4], vec![17]);
}
#[test]
fn test_pushdrop_empty_fields() {
let privkey = PrivateKey::random();
let pubkey = privkey.public_key();
let pushdrop = PushDrop::new(pubkey.clone(), vec![]);
let script = pushdrop.lock();
let chunks = script.chunks();
assert_eq!(chunks.len(), 2);
let decoded = PushDrop::decode(&script).unwrap();
assert!(decoded.fields.is_empty());
}
#[test]
fn test_pushdrop_large_field() {
let privkey = PrivateKey::random();
let pubkey = privkey.public_key();
let large_field = vec![0xABu8; 10_000];
let pushdrop = PushDrop::new(pubkey, vec![large_field.clone()]);
let script = pushdrop.lock();
let decoded = PushDrop::decode(&script).unwrap();
assert_eq!(decoded.fields[0], large_field);
}
#[test]
fn test_pushdrop_drop_count() {
let privkey = PrivateKey::random();
let pubkey = privkey.public_key();
let fields: Vec<Vec<u8>> = (0..5).map(|i| vec![i as u8 + 17]).collect(); let pushdrop = PushDrop::new(pubkey, fields);
let script = pushdrop.lock();
let chunks = script.chunks();
let drop_count = chunks.iter().filter(|c| c.op == OP_DROP).count();
let drop2_count = chunks.iter().filter(|c| c.op == OP_2DROP).count();
assert_eq!(drop2_count, 2);
assert_eq!(drop_count, 1);
}
#[test]
fn test_pushdrop_estimate_unlocking_length() {
let privkey = PrivateKey::random();
let pubkey = privkey.public_key();
let pushdrop = PushDrop::new(pubkey, vec![b"test".to_vec()]);
assert_eq!(pushdrop.estimate_unlocking_length(), 73);
}
#[test]
fn test_pushdrop_unlock_estimate_length() {
use crate::script::template::SignOutputs;
let privkey = PrivateKey::random();
let unlock = PushDrop::unlock(&privkey, SignOutputs::All, false);
assert_eq!(unlock.estimate_length(), 73);
}
#[test]
fn test_pushdrop_sign_with_sighash() {
use crate::script::template::SignOutputs;
let private_key = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap();
let sighash = [1u8; 32];
let unlocking =
PushDrop::sign_with_sighash(&private_key, &sighash, SignOutputs::All, false).unwrap();
let chunks = unlocking.chunks();
assert_eq!(chunks.len(), 1);
assert!(chunks[0].data.is_some());
let sig_data = chunks[0].data.as_ref().unwrap();
assert!(sig_data.len() >= 70 && sig_data.len() <= 73);
assert_eq!(
sig_data.last().unwrap(),
&0x41_u8 );
}
#[test]
fn test_pushdrop_sign_outputs_variants() {
use crate::script::template::SignOutputs;
let private_key = PrivateKey::random();
let sighash = [1u8; 32];
let unlocking =
PushDrop::sign_with_sighash(&private_key, &sighash, SignOutputs::All, false).unwrap();
let chunks = unlocking.chunks();
let sig_data = chunks[0].data.as_ref().unwrap();
assert_eq!(sig_data.last().unwrap(), &0x41u8);
let unlocking =
PushDrop::sign_with_sighash(&private_key, &sighash, SignOutputs::None, false).unwrap();
let chunks = unlocking.chunks();
let sig_data = chunks[0].data.as_ref().unwrap();
assert_eq!(sig_data.last().unwrap(), &0x42u8);
let unlocking =
PushDrop::sign_with_sighash(&private_key, &sighash, SignOutputs::Single, false)
.unwrap();
let chunks = unlocking.chunks();
let sig_data = chunks[0].data.as_ref().unwrap();
assert_eq!(sig_data.last().unwrap(), &0x43u8);
let unlocking =
PushDrop::sign_with_sighash(&private_key, &sighash, SignOutputs::All, true).unwrap();
let chunks = unlocking.chunks();
let sig_data = chunks[0].data.as_ref().unwrap();
assert_eq!(sig_data.last().unwrap(), &0xC1u8); }
#[test]
fn test_pushdrop_empty_data_becomes_op_0() {
let privkey = PrivateKey::random();
let pubkey = privkey.public_key();
let fields = vec![vec![]];
let pushdrop = PushDrop::new(pubkey, fields);
let script = pushdrop.lock();
let chunks = script.chunks();
assert_eq!(chunks[2].op, OP_0);
assert!(chunks[2].data.is_none());
let decoded = PushDrop::decode(&script).unwrap();
assert_eq!(decoded.fields[0], vec![0]);
}
#[test]
fn test_pushdrop_roundtrip_all_special_values() {
let privkey = PrivateKey::random();
let pubkey = privkey.public_key();
let fields = vec![
vec![], vec![0], vec![1], vec![2], vec![15], vec![16], vec![0x81], vec![17], vec![255], ];
let pushdrop = PushDrop::new(pubkey, fields);
let script = pushdrop.lock();
let decoded = PushDrop::decode(&script).unwrap();
assert_eq!(decoded.fields[0], vec![0]);
assert_eq!(decoded.fields[1], vec![0]);
assert_eq!(decoded.fields[2], vec![1]);
assert_eq!(decoded.fields[3], vec![2]);
assert_eq!(decoded.fields[4], vec![15]);
assert_eq!(decoded.fields[5], vec![16]);
assert_eq!(decoded.fields[6], vec![0x81]);
assert_eq!(decoded.fields[7], vec![17]);
assert_eq!(decoded.fields[8], vec![255]);
}
#[test]
fn test_pushdrop_lock_after_multiple_fields() {
let privkey = PrivateKey::random();
let pubkey = privkey.public_key();
let fields = vec![b"token".to_vec(), b"transfer".to_vec(), b"100".to_vec()];
let pushdrop =
PushDrop::new(pubkey.clone(), fields.clone()).with_position(LockPosition::After);
let script = pushdrop.lock();
let decoded = PushDrop::decode(&script).unwrap();
assert_eq!(decoded.fields, fields);
assert_eq!(decoded.lock_position, LockPosition::After);
assert_eq!(
decoded.locking_public_key.to_compressed(),
pubkey.to_compressed()
);
}
}