use crate::overlay::types::Protocol;
use crate::primitives::hash::sha256;
use crate::primitives::{PrivateKey, PublicKey};
use crate::script::templates::PushDrop;
use crate::script::LockingScript;
use crate::wallet::{Counterparty, KeyDeriver, Protocol as WalletProtocol, SecurityLevel};
use crate::{Error, Result};
const SHIP_BRC43_PROTOCOL_NAME: &str = "service host interconnect";
const SLAP_BRC43_PROTOCOL_NAME: &str = "service lookup availability";
const OVERLAY_ADMIN_KEY_ID: &str = "1";
#[derive(Debug, Clone)]
pub struct OverlayAdminTokenData {
pub protocol: Protocol,
pub identity_key: PublicKey,
pub domain: String,
pub topic_or_service: String,
}
impl OverlayAdminTokenData {
pub fn identity_key_hex(&self) -> String {
crate::primitives::to_hex(&self.identity_key.to_compressed())
}
}
pub fn decode_overlay_admin_token(script: &LockingScript) -> Result<OverlayAdminTokenData> {
let pushdrop = PushDrop::decode(script)
.map_err(|_| Error::OverlayError("Script is not a valid PushDrop format".into()))?;
let fields = &pushdrop.fields;
if fields.len() < 4 {
return Err(Error::OverlayError(format!(
"Invalid SHIP/SLAP advertisement: expected at least 4 fields, got {}",
fields.len()
)));
}
let protocol_str = std::str::from_utf8(&fields[0])
.map_err(|_| Error::OverlayError("Protocol field is not valid UTF-8".into()))?;
let protocol = Protocol::parse(protocol_str)
.ok_or_else(|| Error::OverlayError(format!("Invalid protocol type: {}", protocol_str)))?;
let identity_key = PublicKey::from_bytes(&fields[1])
.map_err(|e| Error::OverlayError(format!("Invalid identity key: {}", e)))?;
let domain = std::str::from_utf8(&fields[2])
.map_err(|_| Error::OverlayError("Domain field is not valid UTF-8".into()))?
.to_string();
let topic_or_service = std::str::from_utf8(&fields[3])
.map_err(|_| Error::OverlayError("Topic/service field is not valid UTF-8".into()))?
.to_string();
Ok(OverlayAdminTokenData {
protocol,
identity_key,
domain,
topic_or_service,
})
}
#[deprecated(
since = "0.3.7",
note = "Produces a 4-field unsigned PushDrop with identity-key locking, which \
is rejected by current SHIP/SLAP validators (`@bsv/overlay-discovery-services` \
requires 5 fields with a BRC-42-derived locking key and trailing \
signature — see tm_ship.ts). Use `create_signed_overlay_admin_token` \
instead; it requires a `PrivateKey` (not just a `PublicKey`) because \
it has to SIGN the advert. Byte-equivalent to `@bsv/sdk 1.10.1`'s \
`pushdrop.lock(fields, [2, protocol_name], '1', 'anyone', true, true, \
'before')` path — verified via byte-exact fixtures in \
`overlay_admin_token_template_parity.rs`."
)]
pub fn create_overlay_admin_token(
protocol: Protocol,
identity_key: &PublicKey,
domain: &str,
topic_or_service: &str,
) -> LockingScript {
let fields = vec![
protocol.as_str().as_bytes().to_vec(),
identity_key.to_compressed().to_vec(),
domain.as_bytes().to_vec(),
topic_or_service.as_bytes().to_vec(),
];
let pushdrop = PushDrop::new(identity_key.clone(), fields);
pushdrop.lock()
}
pub fn create_signed_overlay_admin_token(
root_key: &PrivateKey,
protocol: Protocol,
domain: &str,
topic_or_service: &str,
) -> LockingScript {
let brc43_name = match protocol {
Protocol::Ship => SHIP_BRC43_PROTOCOL_NAME,
Protocol::Slap => SLAP_BRC43_PROTOCOL_NAME,
};
let wallet_protocol = WalletProtocol::new(SecurityLevel::Counterparty, brc43_name);
let deriver = KeyDeriver::new(Some(root_key.clone()));
let locking_pubkey = deriver
.derive_public_key(
&wallet_protocol,
OVERLAY_ADMIN_KEY_ID,
&Counterparty::Anyone,
true,
)
.expect("BRC-42 derive_public_key is infallible for well-formed inputs");
let signing_priv = deriver
.derive_private_key(
&wallet_protocol,
OVERLAY_ADMIN_KEY_ID,
&Counterparty::Anyone,
)
.expect("BRC-42 derive_private_key is infallible for well-formed inputs");
let identity_pubkey = root_key.public_key();
let data_fields: Vec<Vec<u8>> = vec![
protocol.as_str().as_bytes().to_vec(),
identity_pubkey.to_compressed().to_vec(),
domain.as_bytes().to_vec(),
topic_or_service.as_bytes().to_vec(),
];
let sign_data: Vec<u8> = data_fields.iter().flat_map(|f| f.iter().copied()).collect();
let sig_der = signing_priv
.sign(&sha256(&sign_data))
.expect("ECDSA sign is infallible with a valid private key + 32-byte digest")
.to_der();
let mut all_fields = data_fields;
all_fields.push(sig_der);
let pushdrop = PushDrop::new(locking_pubkey, all_fields);
pushdrop.lock()
}
pub fn is_overlay_admin_token(script: &LockingScript) -> bool {
decode_overlay_admin_token(script).is_ok()
}
pub fn is_ship_token(script: &LockingScript) -> bool {
decode_overlay_admin_token(script)
.map(|t| t.protocol == Protocol::Ship)
.unwrap_or(false)
}
pub fn is_slap_token(script: &LockingScript) -> bool {
decode_overlay_admin_token(script)
.map(|t| t.protocol == Protocol::Slap)
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::PrivateKey;
#[test]
#[allow(deprecated)]
fn test_create_and_decode_ship_token() {
let private_key = PrivateKey::random();
let public_key = private_key.public_key();
let script = create_overlay_admin_token(
Protocol::Ship,
&public_key,
"https://example.com",
"tm_test_topic",
);
let decoded = decode_overlay_admin_token(&script).unwrap();
assert_eq!(decoded.protocol, Protocol::Ship);
assert_eq!(
decoded.identity_key.to_compressed(),
public_key.to_compressed()
);
assert_eq!(decoded.domain, "https://example.com");
assert_eq!(decoded.topic_or_service, "tm_test_topic");
}
#[test]
fn test_create_signed_and_decode_ship_token() {
let root = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap();
let identity_key = root.public_key();
let script = create_signed_overlay_admin_token(
&root,
Protocol::Ship,
"https://example.com",
"tm_test_topic",
);
let decoded = decode_overlay_admin_token(&script).unwrap();
assert_eq!(decoded.protocol, Protocol::Ship);
assert_eq!(
decoded.identity_key.to_compressed(),
identity_key.to_compressed()
);
assert_eq!(decoded.domain, "https://example.com");
assert_eq!(decoded.topic_or_service, "tm_test_topic");
assert!(is_ship_token(&script));
assert!(!is_slap_token(&script));
}
#[test]
fn test_create_signed_and_decode_slap_token() {
let root = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap();
let script = create_signed_overlay_admin_token(
&root,
Protocol::Slap,
"https://overlay.example.com",
"ls_myservice",
);
let decoded = decode_overlay_admin_token(&script).unwrap();
assert_eq!(decoded.protocol, Protocol::Slap);
assert_eq!(decoded.domain, "https://overlay.example.com");
assert_eq!(decoded.topic_or_service, "ls_myservice");
assert!(is_slap_token(&script));
}
#[test]
fn test_create_signed_overlay_admin_token_is_deterministic() {
let root = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap();
let a = create_signed_overlay_admin_token(
&root,
Protocol::Ship,
"https://overlay.example.com",
"tm_uhrp",
);
let b = create_signed_overlay_admin_token(
&root,
Protocol::Ship,
"https://overlay.example.com",
"tm_uhrp",
);
assert_eq!(a.to_hex(), b.to_hex());
}
#[test]
#[allow(deprecated)]
fn test_signed_and_unsigned_tokens_produce_different_bytes() {
let root = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap();
let signed = create_signed_overlay_admin_token(
&root,
Protocol::Ship,
"https://overlay.example.com",
"tm_uhrp",
);
let unsigned = create_overlay_admin_token(
Protocol::Ship,
&root.public_key(),
"https://overlay.example.com",
"tm_uhrp",
);
assert_ne!(
signed.to_hex(),
unsigned.to_hex(),
"signed (5-field) and unsigned (4-field) variants must produce different bytes"
);
}
#[test]
#[allow(deprecated)]
fn test_create_and_decode_slap_token() {
let private_key = PrivateKey::random();
let public_key = private_key.public_key();
let script = create_overlay_admin_token(
Protocol::Slap,
&public_key,
"https://service.example.com",
"ls_myservice",
);
let decoded = decode_overlay_admin_token(&script).unwrap();
assert_eq!(decoded.protocol, Protocol::Slap);
assert_eq!(decoded.domain, "https://service.example.com");
assert_eq!(decoded.topic_or_service, "ls_myservice");
}
#[test]
#[allow(deprecated)]
fn test_is_ship_token() {
let private_key = PrivateKey::random();
let public_key = private_key.public_key();
let ship_script = create_overlay_admin_token(
Protocol::Ship,
&public_key,
"https://example.com",
"tm_test",
);
let slap_script = create_overlay_admin_token(
Protocol::Slap,
&public_key,
"https://example.com",
"ls_test",
);
assert!(is_ship_token(&ship_script));
assert!(!is_ship_token(&slap_script));
}
#[test]
#[allow(deprecated)]
fn test_is_slap_token() {
let private_key = PrivateKey::random();
let public_key = private_key.public_key();
let slap_script = create_overlay_admin_token(
Protocol::Slap,
&public_key,
"https://example.com",
"ls_test",
);
let ship_script = create_overlay_admin_token(
Protocol::Ship,
&public_key,
"https://example.com",
"tm_test",
);
assert!(is_slap_token(&slap_script));
assert!(!is_slap_token(&ship_script));
}
#[test]
#[allow(deprecated)]
fn test_is_overlay_admin_token() {
let private_key = PrivateKey::random();
let public_key = private_key.public_key();
let admin_script = create_overlay_admin_token(
Protocol::Ship,
&public_key,
"https://example.com",
"tm_test",
);
let regular_script =
LockingScript::from_asm("OP_DUP OP_HASH160 0x14 OP_EQUALVERIFY OP_CHECKSIG");
assert!(is_overlay_admin_token(&admin_script));
assert!(regular_script.is_err() || !is_overlay_admin_token(®ular_script.unwrap()));
}
#[test]
#[allow(deprecated)]
fn test_identity_key_hex() {
let private_key = PrivateKey::random();
let public_key = private_key.public_key();
let script = create_overlay_admin_token(
Protocol::Ship,
&public_key,
"https://example.com",
"tm_test",
);
let decoded = decode_overlay_admin_token(&script).unwrap();
let hex = decoded.identity_key_hex();
assert_eq!(hex.len(), 66);
}
#[test]
fn test_decode_invalid_script() {
let script = LockingScript::new();
assert!(decode_overlay_admin_token(&script).is_err());
}
#[test]
fn test_decode_insufficient_fields() {
let private_key = PrivateKey::random();
let public_key = private_key.public_key();
let fields = vec![b"SHIP".to_vec(), b"domain".to_vec()];
let pushdrop = PushDrop::new(public_key, fields);
let script = pushdrop.lock();
let result = decode_overlay_admin_token(&script);
assert!(result.is_err());
}
#[test]
fn test_decode_invalid_protocol() {
let private_key = PrivateKey::random();
let public_key = private_key.public_key();
let fields = vec![
b"INVALID".to_vec(),
public_key.to_compressed().to_vec(),
b"domain".to_vec(),
b"topic".to_vec(),
];
let pushdrop = PushDrop::new(public_key, fields);
let script = pushdrop.lock();
let result = decode_overlay_admin_token(&script);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid protocol"));
}
}