use std::ffi::CString;
use std::path::PathBuf;
use crate::error::{NodeParityError, ScriptError};
use crate::ffi::{
tidecoin_node_block_subsidy, tidecoin_node_check_block, tidecoin_node_check_contextual_block,
tidecoin_node_check_transaction, tidecoin_node_has_valid_header_pow,
tidecoin_node_mlkem512_decaps, tidecoin_node_mlkem512_encaps_deterministic,
tidecoin_node_mlkem512_keypair_from_coins, tidecoin_node_parse_script_asm,
tidecoin_node_permitted_difficulty_transition, tidecoin_node_pq_compute_public_key,
tidecoin_node_pq_keygen_from_seed, tidecoin_node_pq_p2sh_p2wpkh_input_vsize,
tidecoin_node_pq_p2wpkh_input_vsize, tidecoin_node_pq_sign_message,
tidecoin_node_pq_verify_message, tidecoin_node_pqhd_keygen_from_stream_key,
tidecoin_node_scrypt_pow_hash, tidecoin_node_verify_script_case, tidecoin_node_verify_tx_input,
tidecoin_node_yespower_pow_hash,
};
use crate::fixtures::TIDECOIN_NODE_REPO_ENV;
#[derive(Debug, Clone)]
pub struct TidecoinNodeHarness {
node_repo: PathBuf,
}
impl TidecoinNodeHarness {
pub fn from_env() -> Result<Self, NodeParityError> {
let node_repo = std::env::var_os(TIDECOIN_NODE_REPO_ENV)
.map(PathBuf::from)
.ok_or(NodeParityError::Environment("TIDECOIN_NODE_REPO is not configured"))?;
if !node_repo.exists() {
return Err(NodeParityError::Environment(
"configured TIDECOIN_NODE_REPO does not exist",
));
}
if !node_repo.join("src").exists() {
return Err(NodeParityError::Environment(
"configured TIDECOIN_NODE_REPO is not a Tidecoin node checkout",
));
}
Ok(Self { node_repo })
}
pub fn backend_label(&self) -> String {
format!("tidecoin-node:{}", self.node_repo.display())
}
pub fn node_repo(&self) -> &std::path::Path {
&self.node_repo
}
pub fn check_transaction_hex(&self, tx_hex: &str) -> Result<(), NodeParityError> {
let tx_hex = CString::new(tx_hex).map_err(|_| NodeParityError::InvalidCString("tx hex"))?;
let rc = unsafe { tidecoin_node_check_transaction(tx_hex.as_ptr()) };
match rc {
1 => Ok(()),
0 => Err(NodeParityError::BridgeFailure("invalid transaction")),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_check_transaction")),
}
}
pub fn scrypt_pow_hash(&self, header: &[u8; 80]) -> Result<[u8; 32], NodeParityError> {
let mut out = [0u8; 32];
let rc = unsafe {
tidecoin_node_scrypt_pow_hash(
header.as_ptr(),
header.len(),
out.as_mut_ptr(),
out.len(),
)
};
match rc {
1 => Ok(out),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_scrypt_pow_hash")),
}
}
pub fn yespower_pow_hash(&self, header: &[u8; 80]) -> Result<[u8; 32], NodeParityError> {
let mut out = [0u8; 32];
let rc = unsafe {
tidecoin_node_yespower_pow_hash(
header.as_ptr(),
header.len(),
out.as_mut_ptr(),
out.len(),
)
};
match rc {
1 => Ok(out),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_yespower_pow_hash")),
}
}
pub fn has_valid_header_pow_hex(
&self,
header_hex: &str,
network: i32,
height: Option<i32>,
) -> Result<(), NodeParityError> {
let header_hex =
CString::new(header_hex).map_err(|_| NodeParityError::InvalidCString("header hex"))?;
let rc = unsafe {
tidecoin_node_has_valid_header_pow(
header_hex.as_ptr(),
network,
i32::from(height.is_some()),
height.unwrap_or_default(),
)
};
match rc {
1 => Ok(()),
0 => Err(NodeParityError::BridgeFailure("invalid header proof of work")),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_has_valid_header_pow")),
}
}
pub fn check_block_hex(
&self,
block_hex: &str,
network: i32,
check_pow: bool,
check_merkle_root: bool,
) -> Result<(), NodeParityError> {
let block_hex =
CString::new(block_hex).map_err(|_| NodeParityError::InvalidCString("block hex"))?;
let rc = unsafe {
tidecoin_node_check_block(
block_hex.as_ptr(),
network,
i32::from(check_pow),
i32::from(check_merkle_root),
)
};
match rc {
1 => Ok(()),
0 => Err(NodeParityError::BridgeFailure("invalid block")),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_check_block")),
}
}
pub fn check_contextual_block_hex(
&self,
block_hex: &str,
network: i32,
height: i32,
lock_time_cutoff: i64,
enforce_bip34_height: bool,
expect_witness_commitment: bool,
) -> Result<(), NodeParityError> {
let block_hex =
CString::new(block_hex).map_err(|_| NodeParityError::InvalidCString("block hex"))?;
let rc = unsafe {
tidecoin_node_check_contextual_block(
block_hex.as_ptr(),
network,
height,
lock_time_cutoff,
i32::from(enforce_bip34_height),
i32::from(expect_witness_commitment),
)
};
match rc {
1 => Ok(()),
0 => Err(NodeParityError::BridgeFailure("invalid contextual block")),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_check_contextual_block")),
}
}
pub fn permitted_difficulty_transition(
&self,
network: i32,
height: i64,
old_nbits: u32,
new_nbits: u32,
) -> Result<bool, NodeParityError> {
let rc = unsafe {
tidecoin_node_permitted_difficulty_transition(network, height, old_nbits, new_nbits)
};
match rc {
1 => Ok(true),
0 => Ok(false),
_ => {
Err(NodeParityError::BridgeFailure("tidecoin_node_permitted_difficulty_transition"))
}
}
}
pub fn block_subsidy(&self, network: i32, height: i32) -> Result<u64, NodeParityError> {
match unsafe { tidecoin_node_block_subsidy(network, height) } {
subsidy if subsidy >= 0 => Ok(subsidy as u64),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_block_subsidy")),
}
}
pub fn pq_p2wpkh_input_vsize(
&self,
sig_len: usize,
pubkey_len: usize,
) -> Result<i64, NodeParityError> {
match unsafe { tidecoin_node_pq_p2wpkh_input_vsize(sig_len, pubkey_len) } {
v if v >= 0 => Ok(v),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_pq_p2wpkh_input_vsize")),
}
}
pub fn pq_p2sh_p2wpkh_input_vsize(
&self,
sig_len: usize,
pubkey_len: usize,
) -> Result<i64, NodeParityError> {
match unsafe { tidecoin_node_pq_p2sh_p2wpkh_input_vsize(sig_len, pubkey_len) } {
v if v >= 0 => Ok(v),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_pq_p2sh_p2wpkh_input_vsize")),
}
}
pub fn verify_tx_input_hex(
&self,
tx_hex: &str,
input_index: usize,
prev_script: &[u8],
amount_sat: i64,
flags: u32,
) -> Result<(), NodeParityError> {
let tx_hex = CString::new(tx_hex).map_err(|_| NodeParityError::InvalidCString("tx hex"))?;
let mut script_error = 0;
let rc = unsafe {
tidecoin_node_verify_tx_input(
tx_hex.as_ptr(),
input_index,
prev_script.as_ptr(),
prev_script.len(),
amount_sat,
flags,
&mut script_error,
)
};
match rc {
1 => Ok(()),
0 => Err(NodeParityError::Script(Self::script_error_or_unknown(script_error))),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_verify_tx_input")),
}
}
pub fn verify_script_case(
&self,
script_sig: &[u8],
script_pubkey: &[u8],
witness: &[&[u8]],
amount_sat: i64,
flags: u32,
) -> Result<(), NodeParityError> {
let witness_items: Vec<*const u8> = witness.iter().map(|item| item.as_ptr()).collect();
let witness_lens: Vec<usize> = witness.iter().map(|item| item.len()).collect();
let mut script_error = 0;
let rc = unsafe {
tidecoin_node_verify_script_case(
script_sig.as_ptr(),
script_sig.len(),
script_pubkey.as_ptr(),
script_pubkey.len(),
witness_items.as_ptr(),
witness_lens.as_ptr(),
witness.len(),
amount_sat,
flags,
&mut script_error,
)
};
match rc {
1 => Ok(()),
0 => Err(NodeParityError::Script(Self::script_error_or_unknown(script_error))),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_verify_script_case")),
}
}
pub fn parse_script_asm(&self, script_asm: &str) -> Result<Vec<u8>, NodeParityError> {
let script_asm =
CString::new(script_asm).map_err(|_| NodeParityError::InvalidCString("script asm"))?;
let mut out_len = 0usize;
let probe = unsafe {
tidecoin_node_parse_script_asm(script_asm.as_ptr(), core::ptr::null_mut(), &mut out_len)
};
if probe != 0 {
return Err(NodeParityError::BridgeFailure("tidecoin_node_parse_script_asm"));
}
let mut out = vec![0u8; out_len];
let rc = unsafe {
tidecoin_node_parse_script_asm(script_asm.as_ptr(), out.as_mut_ptr(), &mut out_len)
};
if rc != 1 {
return Err(NodeParityError::BridgeFailure("tidecoin_node_parse_script_asm"));
}
out.truncate(out_len);
Ok(out)
}
pub fn pq_keypair_from_seed(
&self,
scheme_prefix: u8,
seed: &[u8],
pubkey_len: usize,
seckey_len: usize,
) -> Result<(Vec<u8>, Vec<u8>), NodeParityError> {
let mut pk = vec![0u8; pubkey_len];
let mut sk = vec![0u8; seckey_len];
let rc = unsafe {
tidecoin_node_pq_keygen_from_seed(
scheme_prefix,
seed.as_ptr(),
seed.len(),
pk.as_mut_ptr(),
pk.len(),
sk.as_mut_ptr(),
sk.len(),
)
};
match rc {
1 => Ok((pk, sk)),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_pq_keygen_from_seed")),
}
}
pub fn pq_keypair_from_stream_key(
&self,
scheme_prefix: u8,
stream_key: &[u8],
pubkey_len: usize,
seckey_len: usize,
) -> Result<(Vec<u8>, Vec<u8>), NodeParityError> {
let mut pk = vec![0u8; pubkey_len];
let mut sk = vec![0u8; seckey_len];
let rc = unsafe {
tidecoin_node_pqhd_keygen_from_stream_key(
scheme_prefix,
stream_key.as_ptr(),
stream_key.len(),
pk.as_mut_ptr(),
pk.len(),
sk.as_mut_ptr(),
sk.len(),
)
};
match rc {
1 => Ok((pk, sk)),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_pqhd_keygen_from_stream_key")),
}
}
pub fn pq_public_key_from_secret(
&self,
scheme_prefix: u8,
secret_key: &[u8],
pubkey_len: usize,
) -> Result<Vec<u8>, NodeParityError> {
let mut pk = vec![0u8; pubkey_len];
let rc = unsafe {
tidecoin_node_pq_compute_public_key(
scheme_prefix,
secret_key.as_ptr(),
secret_key.len(),
pk.as_mut_ptr(),
pk.len(),
)
};
match rc {
1 => Ok(pk),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_pq_compute_public_key")),
}
}
pub fn pq_sign_message(
&self,
scheme_prefix: u8,
message: &[u8],
secret_key: &[u8],
max_sig_len: usize,
legacy: bool,
) -> Result<Vec<u8>, NodeParityError> {
let mut sig_len = max_sig_len;
let probe = unsafe {
tidecoin_node_pq_sign_message(
scheme_prefix,
message.as_ptr(),
message.len(),
secret_key.as_ptr(),
secret_key.len(),
if legacy { 1 } else { 0 },
core::ptr::null_mut(),
&mut sig_len,
)
};
if probe != 0 {
return Err(NodeParityError::BridgeFailure("tidecoin_node_pq_sign_message"));
}
let mut sig = vec![0u8; sig_len];
let rc = unsafe {
tidecoin_node_pq_sign_message(
scheme_prefix,
message.as_ptr(),
message.len(),
secret_key.as_ptr(),
secret_key.len(),
if legacy { 1 } else { 0 },
sig.as_mut_ptr(),
&mut sig_len,
)
};
if rc != 1 {
return Err(NodeParityError::BridgeFailure("tidecoin_node_pq_sign_message"));
}
sig.truncate(sig_len);
Ok(sig)
}
pub fn pq_verify_message(
&self,
scheme_prefix: u8,
message: &[u8],
signature: &[u8],
public_key: &[u8],
legacy: bool,
) -> Result<bool, NodeParityError> {
let rc = unsafe {
tidecoin_node_pq_verify_message(
scheme_prefix,
message.as_ptr(),
message.len(),
signature.as_ptr(),
signature.len(),
public_key.as_ptr(),
public_key.len(),
if legacy { 1 } else { 0 },
)
};
match rc {
1 => Ok(true),
0 => Ok(false),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_pq_verify_message")),
}
}
pub fn mlkem512_keypair_from_coins(
&self,
coins: &[u8],
pubkey_len: usize,
seckey_len: usize,
) -> Result<(Vec<u8>, Vec<u8>), NodeParityError> {
let mut pk = vec![0u8; pubkey_len];
let mut sk = vec![0u8; seckey_len];
let rc = unsafe {
tidecoin_node_mlkem512_keypair_from_coins(
coins.as_ptr(),
coins.len(),
pk.as_mut_ptr(),
pk.len(),
sk.as_mut_ptr(),
sk.len(),
)
};
match rc {
1 => Ok((pk, sk)),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_mlkem512_keypair_from_coins")),
}
}
pub fn mlkem512_encaps_deterministic(
&self,
public_key: &[u8],
coins: &[u8],
ciphertext_len: usize,
shared_secret_len: usize,
) -> Result<(Vec<u8>, Vec<u8>), NodeParityError> {
let mut ct = vec![0u8; ciphertext_len];
let mut ss = vec![0u8; shared_secret_len];
let rc = unsafe {
tidecoin_node_mlkem512_encaps_deterministic(
public_key.as_ptr(),
public_key.len(),
coins.as_ptr(),
coins.len(),
ct.as_mut_ptr(),
ct.len(),
ss.as_mut_ptr(),
ss.len(),
)
};
match rc {
1 => Ok((ct, ss)),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_mlkem512_encaps_deterministic")),
}
}
pub fn mlkem512_decaps(
&self,
ciphertext: &[u8],
secret_key: &[u8],
shared_secret_len: usize,
) -> Result<Vec<u8>, NodeParityError> {
let mut ss = vec![0u8; shared_secret_len];
let rc = unsafe {
tidecoin_node_mlkem512_decaps(
ciphertext.as_ptr(),
ciphertext.len(),
secret_key.as_ptr(),
secret_key.len(),
ss.as_mut_ptr(),
ss.len(),
)
};
match rc {
1 => Ok(ss),
_ => Err(NodeParityError::BridgeFailure("tidecoin_node_mlkem512_decaps")),
}
}
fn script_error_or_unknown(code: i32) -> ScriptError {
match ScriptError::from_ffi(code) {
ScriptError::Ok => ScriptError::UnknownError,
other => other,
}
}
}