use core::fmt;
use crate::{TokenID, consensus::{Encodable, Decodable, deserialize_partial}, VarInt, ScriptBuf};
use super::opcodes;
pub const PREFIX_BYTE: u8 = opcodes::all::OP_SPECIAL_TOKEN_PREFIX.to_u8();
pub const MAX_CONSENSUS_COMMITMENT_LENGTH: usize = 128;
#[repr(u8)]
pub enum Structure {
HasAmount = 0x10,
HasNFT = 0x20,
HasCommitmentLength = 0x40,
Reserved = 0x80,
}
#[repr(u8)]
pub enum Capability {
None = 0x00,
Mutable = 0x01,
Minting = 0x02,
}
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(crate = "actual_serde", try_from = "i64", into = "i64"))]
pub struct TokenAmount(i64);
impl TokenAmount {
pub const ZERO: TokenAmount = TokenAmount(0);
pub const MAX: TokenAmount = TokenAmount(i64::MAX);
pub const fn from_int(n: i64) -> Option<TokenAmount> {
if n < 0 {
None
} else {
Some(TokenAmount(n))
}
}
pub const fn to_int(self) -> i64 { self.0 }
pub fn checked_add(self, other: TokenAmount) -> Option<TokenAmount> {
self.0.checked_add(other.0).map(TokenAmount)
}
pub fn checked_sub(self, other: TokenAmount) -> Option<TokenAmount> {
self.0.checked_sub(other.0).filter(|n| *n >= 0).map(TokenAmount)
}
}
impl fmt::Display for TokenAmount {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
}
impl From<TokenAmount> for i64 {
fn from(amount: TokenAmount) -> i64 { amount.0 }
}
impl TryFrom<i64> for TokenAmount {
type Error = AmountOutOfRangeError;
fn try_from(n: i64) -> Result<TokenAmount, Self::Error> {
TokenAmount::from_int(n).ok_or(AmountOutOfRangeError { value: n })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AmountOutOfRangeError {
pub value: i64,
}
impl fmt::Display for AmountOutOfRangeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "token amount {} out of range 0..=2^63-1", self.value)
}
}
#[cfg(feature = "std")]
impl std::error::Error for AmountOutOfRangeError {}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
pub struct OutputData {
pub id: TokenID,
pub bitfield: u8,
pub amount: TokenAmount,
pub commitment: Vec<u8>
}
fn is_valid_bitfield(bitfield: u8) -> bool {
let structure = bitfield & 0xf0;
if structure >= Structure::Reserved as u8 || structure == 0x00 {
return false;
}
let has_nft = (bitfield & Structure::HasNFT as u8) != 0;
let has_amount = (bitfield & Structure::HasAmount as u8) != 0;
let has_commitment_length = (bitfield & Structure::HasCommitmentLength as u8) != 0;
if (bitfield & 0x0f) > Capability::Minting as u8 {
return false;
}
if !has_nft && !has_amount {
return false;
}
if !has_nft && (bitfield & 0x0f) != 0 {
return false;
}
if !has_nft && has_commitment_length {
return false;
}
true
}
impl OutputData {
pub fn has_commitment_length(&self) -> bool {
(self.bitfield & Structure::HasCommitmentLength as u8) != 0
}
pub fn has_amount(&self) -> bool {
(self.bitfield & Structure::HasAmount as u8) != 0
}
pub fn capability(&self) -> u8 {
self.bitfield & 0x0f
}
pub fn has_nft(&self) -> bool {
(self.bitfield & Structure::HasNFT as u8) != 0
}
pub fn is_minting_nft(&self) -> bool {
self.has_nft() && ((self.capability() & Capability::Minting as u8) != 0)
}
pub fn is_valid_bitfield(&self) -> bool {
is_valid_bitfield(self.bitfield)
}
}
impl Encodable for OutputData {
fn consensus_encode<W: crate::io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, crate::io::Error> {
use crate::io::{Error, ErrorKind};
if !self.is_valid_bitfield() {
return Err(Error::new(ErrorKind::InvalidData, "invalid token bitfield"));
}
let mut len = 0;
len += self.id.consensus_encode(writer)?;
len += self.bitfield.consensus_encode(writer)?;
if self.has_commitment_length() {
if self.commitment.is_empty() {
return Err(Error::new(ErrorKind::InvalidData, "token commitment may not be empty"));
}
len += self.commitment.consensus_encode(writer)?;
}
if self.has_amount() {
if self.amount == TokenAmount::ZERO {
return Err(Error::new(ErrorKind::InvalidData, "serialized token amount may not be 0"));
}
len += VarInt(self.amount.to_int() as u64).consensus_encode(writer)?;
}
Ok(len)
}
}
impl Decodable for OutputData {
fn consensus_decode<R: crate::io::Read + ?Sized>(reader: &mut R) -> Result<Self, crate::consensus::encode::Error> {
use crate::consensus::encode::Error;
let id = TokenID::consensus_decode(reader)?;
let bitfield = u8::consensus_decode(reader)?;
if !is_valid_bitfield(bitfield) {
return Err(Error::ParseFailed("invalid token bitfield"));
}
let commitment = if (bitfield & Structure::HasCommitmentLength as u8) != 0 {
let commitment = Vec::<u8>::consensus_decode(reader)?;
if commitment.is_empty() {
return Err(Error::ParseFailed("token commitment may not be empty"));
}
commitment
} else {
vec![]
};
let amount = if (bitfield & Structure::HasAmount as u8) != 0 {
let amount = VarInt::consensus_decode(reader)?.0;
if amount == 0 {
return Err(Error::ParseFailed("serialized token amount may not be 0"));
}
if amount > i64::MAX as u64 {
return Err(Error::ParseFailed("token amount out of range"));
}
TokenAmount::from_int(amount as i64).expect("range checked above")
} else {
TokenAmount::ZERO
};
Ok(OutputData {
id,
bitfield,
amount,
commitment,
})
}
}
pub fn wrap_scriptpubkey(scriptpubkey: ScriptBuf, token_data: &Option<OutputData>) -> Result<ScriptBuf, crate::io::Error> {
match token_data {
Some(data) => {
let mut bytes = Vec::with_capacity(1 + 34 + data.commitment.len() + 9 + scriptpubkey.len());
bytes.push(PREFIX_BYTE);
data.consensus_encode(&mut bytes)?;
bytes.extend_from_slice(scriptpubkey.as_bytes());
Ok(ScriptBuf::from(bytes))
}
None => Ok(scriptpubkey)
}
}
pub fn unwrap_scriptpubkey(scriptpubkey: ScriptBuf) -> (ScriptBuf, Option<OutputData>) {
if scriptpubkey.as_bytes().first() != Some(&PREFIX_BYTE) {
return (scriptpubkey, None);
}
match deserialize_partial::<OutputData>(&scriptpubkey.as_bytes()[1..]) {
Ok((output_data, consumed)) => {
let remaining: Vec<u8> = scriptpubkey.as_bytes()[1 + consumed..].to_vec();
(ScriptBuf::from(remaining), Some(output_data))
}
Err(_) => (scriptpubkey, None),
}
}
#[cfg(test)]
mod test {
use crate::hex::{DisplayHex, Case};
use super::*;
const ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
fn unwrap_hex(payload_after_prefix_byte: &str) -> (ScriptBuf, Option<OutputData>) {
let script = ScriptBuf::from_hex(&format!("ef{}", payload_after_prefix_byte)).unwrap();
unwrap_scriptpubkey(script)
}
fn decode_payload(payload: &str) -> Result<OutputData, crate::consensus::encode::Error> {
use crate::hex::FromHex;
let bytes = Vec::<u8>::from_hex(payload).unwrap();
deserialize_partial::<OutputData>(&bytes).map(|(data, _)| data)
}
fn assert_invalid(payload: &str) {
assert!(decode_payload(payload).is_err(), "payload {} should not parse", payload);
let script = ScriptBuf::from_hex(&format!("ef{}", payload)).unwrap();
let (unwrapped, token_data) = unwrap_scriptpubkey(script.clone());
assert_eq!(script, unwrapped);
assert_eq!(None, token_data);
}
#[test]
fn test_vectors() {
let prefix = "efaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1001".to_string();
let other_payload = "f00d".to_string();
let script = ScriptBuf::from_hex(&(prefix + &other_payload)).unwrap();
let (unwrapped_script, token_data) = unwrap_scriptpubkey(script.clone());
let token_data = token_data.unwrap();
assert_eq!(other_payload, unwrapped_script.to_hex_string());
assert_eq!(TokenAmount::from_int(1).unwrap(), token_data.amount);
assert_eq!(ID, token_data.id.to_string());
assert_eq!(script, wrap_scriptpubkey(unwrapped_script, &Some(token_data)).unwrap());
let prefix = "efbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb7229ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccffffffffffffffff7f".to_string();
let script = ScriptBuf::from_hex(&(prefix + &other_payload)).unwrap();
let (unwrapped_script, token_data) = unwrap_scriptpubkey(script.clone());
let token_data = token_data.unwrap();
assert_eq!(other_payload, unwrapped_script.to_hex_string());
assert_eq!(TokenAmount::MAX, token_data.amount);
assert_eq!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", token_data.id.to_string());
assert_eq!("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", token_data.commitment.to_hex_string(Case::Lower));
assert!(token_data.is_minting_nft());
assert_eq!(script, wrap_scriptpubkey(unwrapped_script, &Some(token_data)).unwrap());
let (_, token_data) = unwrap_hex(&format!("{}20", ID));
let token_data = token_data.unwrap();
assert!(token_data.has_nft());
assert!(!token_data.has_amount());
assert_eq!(TokenAmount::ZERO, token_data.amount);
for (bitfield, minting) in [("30", false), ("31", false), ("32", true)] {
let (_, token_data) = unwrap_hex(&format!("{}{}01", ID, bitfield));
let token_data = token_data.unwrap();
assert!(token_data.has_nft() && token_data.has_amount());
assert_eq!(minting, token_data.is_minting_nft());
}
let commitment = "cc".repeat(253);
let (_, token_data) = unwrap_hex(&format!("{}60fdfd00{}", ID, commitment));
assert_eq!(253, token_data.unwrap().commitment.len());
}
#[test]
fn invalid_bitfield_rejected() {
assert_invalid(&format!("{}00", ID));
assert_invalid(&format!("{}90{}", ID, "01"));
assert_invalid(&format!("{}23", ID));
assert_invalid(&format!("{}11{}", ID, "01"));
assert_invalid(&format!("{}50{}", ID, "01cc01"));
}
#[test]
fn invalid_amount_rejected() {
assert_invalid(&format!("{}10{}", ID, "00"));
assert_invalid(&format!("{}10{}", ID, "ff0000000000000080"));
assert_invalid(&format!("{}10{}", ID, "ffffffffffffffffff"));
assert_invalid(&format!("{}10{}", ID, "fd0100"));
let (_, token_data) = unwrap_hex(&format!("{}10{}", ID, "ffffffffffffffff7f"));
assert_eq!(TokenAmount::MAX, token_data.unwrap().amount);
}
#[test]
fn invalid_commitment_rejected() {
assert_invalid(&format!("{}60{}", ID, "00"));
assert_invalid(&format!("{}60{}", ID, "fd0100cc"));
}
#[test]
fn truncated_payload_rejected() {
assert_invalid("");
assert_invalid("aaaa");
assert_invalid(ID);
assert_invalid(&format!("{}60", ID));
assert_invalid(&format!("{}6002cc", ID));
assert_invalid(&format!("{}10", ID));
assert_invalid(&format!("{}10fd00", ID));
}
#[test]
fn unparseable_prefix_txout_roundtrip() {
use crate::consensus::{deserialize, serialize};
use crate::{Amount, TxOut};
let script = ScriptBuf::from_hex(&format!("ef{}00", ID)).unwrap();
let txout = TxOut { value: Amount::from_sat(1000), script_pubkey: script.clone(), token: None };
let bytes = serialize(&txout);
let decoded: TxOut = deserialize(&bytes).unwrap();
assert_eq!(txout, decoded);
assert!(decoded.has_unparseable_token_data());
assert_eq!(bytes, serialize(&decoded));
}
#[test]
fn encode_validates() {
let valid = OutputData {
id: ID.parse().unwrap(),
bitfield: Structure::HasNFT as u8 | Structure::HasCommitmentLength as u8,
amount: TokenAmount::ZERO,
commitment: vec![0xcc],
};
let mut sink = Vec::new();
assert!(valid.consensus_encode(&mut sink).is_ok());
let mut invalid = valid.clone();
invalid.bitfield = 0x00;
assert!(invalid.consensus_encode(&mut Vec::new()).is_err());
let mut invalid = valid.clone();
invalid.commitment = Vec::new();
assert!(invalid.consensus_encode(&mut Vec::new()).is_err());
let mut invalid = valid;
invalid.bitfield |= Structure::HasAmount as u8;
assert!(invalid.consensus_encode(&mut Vec::new()).is_err());
assert!(wrap_scriptpubkey(ScriptBuf::new(), &Some(invalid.clone())).is_err());
let txout = crate::TxOut {
value: crate::Amount::from_sat(1000),
script_pubkey: ScriptBuf::new(),
token: Some(invalid),
};
assert!(txout.consensus_encode(&mut Vec::new()).is_err());
}
#[test]
fn token_amount() {
assert_eq!(None, TokenAmount::from_int(-1));
assert_eq!(Err(AmountOutOfRangeError { value: -1 }), TokenAmount::try_from(-1));
assert_eq!(Ok(TokenAmount::MAX), TokenAmount::try_from(i64::MAX));
let one = TokenAmount::from_int(1).unwrap();
assert_eq!(None, TokenAmount::MAX.checked_add(one));
assert_eq!(Some(TokenAmount::MAX), TokenAmount::MAX.checked_sub(TokenAmount::ZERO));
assert_eq!(None, TokenAmount::ZERO.checked_sub(one));
assert_eq!(Some(TokenAmount::ZERO), one.checked_sub(one));
}
}