use bitcoin::{
opcodes::all::OP_RETURN,
script::{Builder, PushBytesBuf},
ScriptBuf,
};
use csv_adapter_core::hash::Hash;
pub const TAPRET_SCRIPT_SIZE: usize = 67;
pub const OPRET_SCRIPT_SIZE: usize = 66;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TapretCommitment {
pub protocol_id: [u8; 32],
pub commitment: Hash,
}
impl TapretCommitment {
pub fn new(protocol_id: [u8; 32], commitment: Hash) -> Self {
Self {
protocol_id,
commitment,
}
}
pub fn payload(&self) -> [u8; 64] {
let mut payload = [0u8; 64];
payload[..32].copy_from_slice(&self.protocol_id);
payload[32..].copy_from_slice(self.commitment.as_bytes());
payload
}
pub fn leaf_script(&self) -> ScriptBuf {
let payload = self.payload();
let push_bytes = PushBytesBuf::try_from(payload.to_vec()).unwrap();
Builder::new()
.push_opcode(OP_RETURN)
.push_slice(push_bytes)
.into_script()
}
pub fn leaf_script_with_nonce(&self, nonce: u8) -> ScriptBuf {
let mut payload = [0u8; 65];
payload[..32].copy_from_slice(&self.protocol_id);
payload[32] = nonce;
payload[33..65].copy_from_slice(self.commitment.as_bytes());
let push_bytes = PushBytesBuf::try_from(payload.to_vec()).unwrap();
Builder::new()
.push_opcode(OP_RETURN)
.push_slice(push_bytes)
.into_script()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OpretCommitment {
pub protocol_id: [u8; 32],
pub commitment: Hash,
}
impl OpretCommitment {
pub fn new(protocol_id: [u8; 32], commitment: Hash) -> Self {
Self {
protocol_id,
commitment,
}
}
pub fn script(&self) -> ScriptBuf {
let mut data = Vec::with_capacity(64);
data.extend_from_slice(&self.protocol_id);
data.extend_from_slice(self.commitment.as_bytes());
let push_bytes = PushBytesBuf::try_from(data).unwrap();
Builder::new()
.push_opcode(OP_RETURN)
.push_slice(push_bytes)
.into_script()
}
}
pub fn mine_tapret_nonce(
tapret: &TapretCommitment,
max_attempts: u32,
) -> Result<(u8, ScriptBuf), TapretError> {
use rand::RngCore;
let mut rng = rand::thread_rng();
for _attempt in 0..max_attempts {
let nonce = rng.next_u32() as u8;
let script = tapret.leaf_script_with_nonce(nonce);
if script.is_op_return() && script.len() == TAPRET_SCRIPT_SIZE {
return Ok((nonce, script));
}
}
Err(TapretError::NonceMiningFailed(max_attempts))
}
#[derive(Debug, thiserror::Error)]
pub enum TapretError {
#[error("Nonce mining failed after {0} attempts")]
NonceMiningFailed(u32),
}
#[cfg(test)]
mod tests {
use super::*;
fn test_commitment() -> TapretCommitment {
TapretCommitment::new([1u8; 32], Hash::new([2u8; 32]))
}
#[test]
fn test_tapret_payload() {
let tc = test_commitment();
let payload = tc.payload();
assert_eq!(payload.len(), 64);
assert_eq!(&payload[..32], &[1u8; 32]);
assert_eq!(&payload[32..], &[2u8; 32]);
}
#[test]
fn test_tapret_leaf_script() {
let tc = test_commitment();
let script = tc.leaf_script();
assert_eq!(script.len(), OPRET_SCRIPT_SIZE);
}
#[test]
fn test_tapret_leaf_with_nonce() {
let tc = test_commitment();
let script_no_nonce = tc.leaf_script();
let script_with_nonce = tc.leaf_script_with_nonce(42);
assert_eq!(script_with_nonce.len(), TAPRET_SCRIPT_SIZE);
assert_eq!(script_with_nonce.len(), script_no_nonce.len() + 1);
}
#[test]
fn test_nonce_mining() {
let tc = test_commitment();
let (nonce, script) = mine_tapret_nonce(&tc, 256).unwrap();
assert_eq!(script.len(), TAPRET_SCRIPT_SIZE);
assert!(script.as_bytes().contains(&nonce));
}
#[test]
fn test_opret_script() {
let oc = OpretCommitment::new([1u8; 32], Hash::new([2u8; 32]));
let script = oc.script();
assert!(script.is_op_return());
assert_eq!(script.len(), OPRET_SCRIPT_SIZE);
}
#[test]
fn test_opret_script_content() {
let oc = OpretCommitment::new([0xAB; 32], Hash::new([0xCD; 32]));
let script = oc.script();
let bytes = script.as_bytes();
assert_eq!(bytes[0], 0x6a); assert_eq!(bytes[1], 0x40); }
}