diff --git a/Cargo.lock b/Cargo.lock index 2525bb4..96f2356 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1823,6 +1823,7 @@ dependencies = [ "log", "rand", "serde", + "serde_json", ] [[package]] @@ -2219,11 +2220,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] diff --git a/node/src/bin/space-cli.rs b/node/src/bin/space-cli.rs index 8eaf52d..033ce04 100644 --- a/node/src/bin/space-cli.rs +++ b/node/src/bin/space-cli.rs @@ -11,8 +11,7 @@ use jsonrpsee::{ use protocol::{ bitcoin::{Amount, FeeRate, OutPoint, Txid}, hasher::{KeyHasher, SpaceKey}, - opcodes::OP_SETALL, - sname::{NameLike, SName}, + slabel::SLabel, Covenant, FullSpaceOut, }; use serde::{Deserialize, Serialize}; @@ -232,6 +231,13 @@ enum Commands { /// The space name space: String, }, + /// Force spend an output owned by wallet (for testing only) + #[command(name = "forcespend")] + ForceSpend { + outpoint: OutPoint, + #[arg(long, short)] + fee_rate: u64, + }, } struct SpaceCli { @@ -372,8 +378,8 @@ async fn main() -> anyhow::Result<()> { fn space_hash(spaceish: &str) -> anyhow::Result { let space = normalize_space(&spaceish); - let sname = SName::from_str(&space)?; - let spacehash = SpaceKey::from(Sha256::hash(sname.to_bytes())); + let sname = SLabel::from_str(&space)?; + let spacehash = SpaceKey::from(Sha256::hash(sname.as_ref())); Ok(hex::encode(spacehash.as_slice())) } @@ -553,13 +559,13 @@ async fn handle_commands( ))) } }; - let builder = protocol::script::ScriptBuilder::new() - .push_slice(data.as_slice()) - .push_opcode(OP_SETALL.into()); + + let space_script = protocol::script::SpaceScript::create_set_fallback(data.as_slice()); + cli.send_request( Some(RpcWalletRequest::Execute(ExecuteParams { context: vec![space], - space_script: builder, + space_script, })), None, fee_rate, @@ -610,6 +616,17 @@ async fn handle_commands( space_hash(&space).map_err(|e| ClientError::Custom(e.to_string()))? ); } + Commands::ForceSpend { outpoint, fee_rate } => { + let result = cli + .client + .wallet_force_spend( + &cli.wallet, + outpoint, + FeeRate::from_sat_per_vb(fee_rate).unwrap(), + ) + .await?; + println!("{}", serde_json::to_string_pretty(&result).expect("result")); + } } Ok(()) diff --git a/node/src/bin/spaced.rs b/node/src/bin/spaced.rs index d3e6be7..2cd2251 100644 --- a/node/src/bin/spaced.rs +++ b/node/src/bin/spaced.rs @@ -128,7 +128,7 @@ impl Composer { } async fn run(&mut self) -> anyhow::Result<()> { - let spaced = Args::configure()?; + let spaced = Args::configure().await?; self.setup_rpc_services(&spaced).await; self.setup_sync_service(spaced).await; diff --git a/node/src/config.rs b/node/src/config.rs index 73514a8..7047a0a 100644 --- a/node/src/config.rs +++ b/node/src/config.rs @@ -73,12 +73,16 @@ pub struct Args { /// Listen for JSON-RPC connections on #[arg(long, help_heading = Some(RPC_OPTIONS), env = "SPACED_RPC_PORT")] rpc_port: Option, + /// Index blocks including the full transaction data + #[arg(long, env = "SPACED_BLOCK_INDEX_FULL", default_value = "false")] + block_index_full: bool, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, ValueEnum, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ExtendedNetwork { Mainnet, + MainnetAlpha, Testnet, Testnet4, Signet, @@ -88,7 +92,7 @@ pub enum ExtendedNetwork { impl ExtendedNetwork { pub fn fallback_network(&self) -> Network { match self { - ExtendedNetwork::Mainnet => Network::Bitcoin, + ExtendedNetwork::Mainnet | ExtendedNetwork::MainnetAlpha => Network::Bitcoin, ExtendedNetwork::Testnet => Network::Testnet, ExtendedNetwork::Signet => Network::Signet, ExtendedNetwork::Regtest => Network::Regtest, @@ -100,7 +104,7 @@ impl ExtendedNetwork { impl Args { /// Configures spaced node by processing command line arguments /// and configuration files - pub fn configure() -> anyhow::Result { + pub async fn configure() -> anyhow::Result { let mut args = Args::merge_args_config(None); let default_dirs = get_default_node_dirs(); @@ -126,9 +130,10 @@ impl Args { } let data_dir = match args.data_dir { - None => default_dirs.data_dir().join(args.chain.to_string()), + None => default_dirs.data_dir().to_path_buf(), Some(data_dir) => data_dir, - }; + } + .join(args.chain.to_string()); let default_port = args.rpc_port.unwrap(); let rpc_bind_addresses: Vec = args @@ -144,7 +149,6 @@ impl Args { }) .collect(); - let genesis = Spaced::genesis(args.chain); let bitcoin_rpc_auth = if let Some(cookie) = args.bitcoin_rpc_cookie { let cookie = std::fs::read_to_string(cookie)?; BitcoinRpcAuth::Cookie(cookie) @@ -159,6 +163,8 @@ impl Args { bitcoin_rpc_auth, ); + let genesis = Spaced::genesis(&rpc, args.chain).await?; + fs::create_dir_all(data_dir.clone())?; let proto_db_path = data_dir.join("protocol.sdb"); @@ -170,7 +176,8 @@ impl Args { store: chain_store, }; - let block_index = if args.block_index { + let block_index_enabled = args.block_index || args.block_index_full; + let block_index = if block_index_enabled { let block_db_path = data_dir.join("block_index.sdb"); if !initial_sync && !block_db_path.exists() { return Err(anyhow::anyhow!( @@ -203,6 +210,7 @@ impl Args { bind: rpc_bind_addresses, chain, block_index, + block_index_full: args.block_index_full, num_workers: args.jobs as usize, }) } @@ -259,9 +267,9 @@ pub fn safe_exit(code: i32) -> ! { std::process::exit(code) } -fn default_bitcoin_rpc_url(network: &ExtendedNetwork) -> &'static str { +pub fn default_bitcoin_rpc_url(network: &ExtendedNetwork) -> &'static str { match network { - ExtendedNetwork::Mainnet => "http://127.0.0.1:8332", + ExtendedNetwork::Mainnet | ExtendedNetwork::MainnetAlpha => "http://127.0.0.1:8332", ExtendedNetwork::Testnet4 => "http://127.0.0.1:48332", ExtendedNetwork::Signet => "http://127.0.0.1:38332", ExtendedNetwork::Testnet => "http://127.0.0.1:18332", @@ -359,6 +367,7 @@ impl Display for ExtendedNetwork { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let str = match self { ExtendedNetwork::Mainnet => "mainnet".to_string(), + ExtendedNetwork::MainnetAlpha => "mainnet-alpha".to_string(), ExtendedNetwork::Testnet => "testnet".to_string(), ExtendedNetwork::Testnet4 => "testnet4".to_string(), ExtendedNetwork::Signet => "signet".to_string(), @@ -371,6 +380,7 @@ impl Display for ExtendedNetwork { pub fn default_spaces_rpc_port(chain: &ExtendedNetwork) -> u16 { match chain { ExtendedNetwork::Mainnet => 7225, + ExtendedNetwork::MainnetAlpha => 7225, ExtendedNetwork::Testnet4 => 7224, ExtendedNetwork::Testnet => 7223, ExtendedNetwork::Signet => 7221, diff --git a/node/src/node.rs b/node/src/node.rs index d24e059..9347e4f 100644 --- a/node/src/node.rs +++ b/node/src/node.rs @@ -10,9 +10,8 @@ use protocol::{ constants::{ChainAnchor, ROLLOUT_BATCH_SIZE, ROLLOUT_BLOCK_INTERVAL}, hasher::{BidKey, KeyHasher, OutpointKey, SpaceKey}, prepare::TxContext, - sname::NameLike, validate::{TxChangeSet, UpdateKind, Validator}, - Covenant, FullSpaceOut, RevokeReason, SpaceOut, + Bytes, Covenant, FullSpaceOut, RevokeReason, SpaceOut, }; use serde::{Deserialize, Serialize}; use wallet::bitcoin::Transaction; @@ -33,13 +32,29 @@ pub trait BlockSource { #[derive(Debug, Clone)] pub struct Node { validator: Validator, + tx_data: bool, } /// A block structure containing validated transaction metadata /// relevant to the Spaces protocol #[derive(Clone, Serialize, Deserialize, Encode, Decode)] pub struct BlockMeta { - pub tx_meta: Vec, + pub height: u32, + pub tx_meta: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Encode, Decode)] +pub struct TxEntry { + #[serde(flatten)] + pub changeset: TxChangeSet, + #[serde(skip_serializing_if = "Option::is_none", flatten)] + pub tx: Option, +} + +#[derive(Clone, Serialize, Deserialize, Encode, Decode)] +pub struct TxData { + pub position: u32, + pub raw: Bytes, } #[derive(Debug)] @@ -61,9 +76,10 @@ impl fmt::Display for SyncError { impl Error for SyncError {} impl Node { - pub fn new() -> Self { + pub fn new(tx_data: bool) -> Self { Self { validator: Validator::new(), + tx_data, } } @@ -86,7 +102,10 @@ impl Node { } } - let mut block_data = BlockMeta { tx_meta: vec![] }; + let mut block_data = BlockMeta { + height, + tx_meta: vec![], + }; if (height - 1) % ROLLOUT_BLOCK_INTERVAL == 0 { let batch = Self::get_rollout_batch(ROLLOUT_BATCH_SIZE, chain)?; @@ -97,13 +116,24 @@ impl Node { let validated = self.validator.rollout(height, &coinbase, batch); if get_block_data { - block_data.tx_meta.push(validated.clone()); + block_data.tx_meta.push(TxEntry { + changeset: validated.clone(), + tx: if self.tx_data { + Some(TxData { + position: 0, + raw: Bytes::new(protocol::bitcoin::consensus::encode::serialize( + &coinbase, + )), + }) + } else { + None + }, + }); } - self.apply_tx(&mut chain.state, &coinbase, validated); } - for tx in block.txdata { + for (position, tx) in block.txdata.into_iter().enumerate() { let prepared_tx = { TxContext::from_tx::(&mut chain.state, &tx)? }; @@ -111,7 +141,19 @@ impl Node { let validated_tx = self.validator.process(height, &tx, prepared_tx); if get_block_data { - block_data.tx_meta.push(validated_tx.clone()); + block_data.tx_meta.push(TxEntry { + changeset: validated_tx.clone(), + tx: if self.tx_data { + Some(TxData { + position: position as u32, + raw: Bytes::new(protocol::bitcoin::consensus::encode::serialize( + &tx, + )), + }) + } else { + None + }, + }); } self.apply_tx(&mut chain.state, &tx, validated_tx); } @@ -149,7 +191,7 @@ impl Node { // Space => Outpoint if let Some(space) = create.space.as_ref() { - let space_key = SpaceKey::from(Sha256::hash(space.name.to_bytes())); + let space_key = SpaceKey::from(Sha256::hash(space.name.as_ref())); state.insert_space(space_key, outpoint.into()); } // Outpoint => SpaceOut @@ -168,7 +210,7 @@ impl Node { // Since these are caused by spends // Outpoint -> Spaceout mapping is already removed, let space = update.output.spaceout.space.unwrap(); - let base_hash = Sha256::hash(space.name.to_bytes()); + let base_hash = Sha256::hash(space.name.as_ref()); // Remove Space -> Outpoint let space_key = SpaceKey::from(base_hash); @@ -209,7 +251,7 @@ impl Node { .as_ref() .expect("a space in rollout") .name - .to_bytes(), + .as_ref(), ); let bid_key = BidKey::from_bid(rollout.priority, base_hash); @@ -229,7 +271,7 @@ impl Node { .as_ref() .expect("space") .name - .to_bytes(), + .as_ref(), ); let (bid_value, previous_bid) = unwrap_bid_value(&update.output.spaceout); diff --git a/node/src/rpc.rs b/node/src/rpc.rs index 1a22e92..7ef78ee 100644 --- a/node/src/rpc.rs +++ b/node/src/rpc.rs @@ -24,7 +24,6 @@ use protocol::{ constants::ChainAnchor, hasher::{BaseHash, SpaceKey}, prepare::DataSource, - validate::TxChangeSet, FullSpaceOut, SpaceOut, }; use serde::{Deserialize, Serialize}; @@ -40,7 +39,7 @@ use wallet::{ use crate::{ config::ExtendedNetwork, - node::BlockMeta, + node::{BlockMeta, TxEntry}, source::BitcoinRpc, store::{ChainState, LiveSnapshot}, wallets::{ @@ -75,7 +74,7 @@ pub enum ChainStateCommand { }, GetTxMeta { txid: Txid, - resp: Responder>>, + resp: Responder>>, }, GetBlockMeta { block_hash: BlockHash, @@ -124,7 +123,7 @@ pub trait Rpc { ) -> Result, ErrorObjectOwned>; #[method(name = "gettxmeta")] - async fn get_tx_meta(&self, txid: Txid) -> Result, ErrorObjectOwned>; + async fn get_tx_meta(&self, txid: Txid) -> Result, ErrorObjectOwned>; #[method(name = "walletload")] async fn wallet_load(&self, name: &str) -> Result<(), ErrorObjectOwned>; @@ -163,6 +162,14 @@ pub trait Rpc { fee_rate: FeeRate, ) -> Result, ErrorObjectOwned>; + #[method(name = "walletforcespend")] + async fn wallet_force_spend( + &self, + wallet: &str, + outpoint: OutPoint, + fee_rate: FeeRate, + ) -> Result; + #[method(name = "walletlistspaces")] async fn wallet_list_spaces(&self, wallet: &str) -> Result, ErrorObjectOwned>; @@ -225,7 +232,7 @@ pub struct SendCoinsParams { #[derive(Clone, Serialize, Deserialize)] pub struct ExecuteParams { pub context: Vec, - pub space_script: protocol::script::ScriptBuilder, + pub space_script: Vec, } #[derive(Clone, Serialize, Deserialize)] @@ -624,7 +631,7 @@ impl RpcServer for RpcServerImpl { Ok(data) } - async fn get_tx_meta(&self, txid: Txid) -> Result, ErrorObjectOwned> { + async fn get_tx_meta(&self, txid: Txid) -> Result, ErrorObjectOwned> { let data = self .store .get_tx_meta(txid) @@ -715,6 +722,19 @@ impl RpcServer for RpcServerImpl { .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } + async fn wallet_force_spend( + &self, + wallet: &str, + outpoint: OutPoint, + fee_rate: FeeRate, + ) -> Result { + self.wallet(&wallet) + .await? + .send_force_spend(outpoint, fee_rate) + .await + .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) + } + async fn wallet_list_spaces( &self, wallet: &str, @@ -768,7 +788,7 @@ impl AsyncChainState { client: &reqwest::Client, rpc: &BitcoinRpc, chain_state: &mut LiveSnapshot, - ) -> Result, anyhow::Error> { + ) -> Result, anyhow::Error> { let info: serde_json::Value = rpc .send_json(client, &rpc.get_raw_transaction(&txid, true)) .await @@ -781,7 +801,10 @@ impl AsyncChainState { let block = Self::get_indexed_block(index, &block_hash, client, rpc, chain_state).await?; if let Some(block) = block { - return Ok(block.tx_meta.into_iter().find(|tx| &tx.txid == txid)); + return Ok(block + .tx_meta + .into_iter() + .find(|tx| &tx.changeset.txid == txid)); } Ok(None) } @@ -951,7 +974,7 @@ impl AsyncChainState { resp_rx.await? } - pub async fn get_tx_meta(&self, txid: Txid) -> anyhow::Result> { + pub async fn get_tx_meta(&self, txid: Txid) -> anyhow::Result> { let (resp, resp_rx) = oneshot::channel(); self.sender .send(ChainStateCommand::GetTxMeta { txid, resp }) diff --git a/node/src/source.rs b/node/src/source.rs index 090d9e2..836cc9a 100644 --- a/node/src/source.rs +++ b/node/src/source.rs @@ -694,7 +694,8 @@ impl std::error::Error for BitcoinRpcError { impl ErrorForRpc for reqwest::Response { async fn error_for_rpc(self) -> Result { - let rpc_res: JsonRpcResponse = self.json().await?; + let res = self.error_for_status()?; + let rpc_res: JsonRpcResponse = res.json().await?; if let Some(e) = rpc_res.error { return Err(BitcoinRpcError::Rpc(e)); } @@ -705,7 +706,8 @@ impl ErrorForRpc for reqwest::Response { impl ErrorForRpcBlocking for reqwest::blocking::Response { fn error_for_rpc(self) -> Result { - let rpc_res: JsonRpcResponse = self.json()?; + let res = self.error_for_status()?; + let rpc_res: JsonRpcResponse = res.json()?; if let Some(e) = rpc_res.error { return Err(BitcoinRpcError::Rpc(e)); } diff --git a/node/src/sync.rs b/node/src/sync.rs index d9f9589..6f47500 100644 --- a/node/src/sync.rs +++ b/node/src/sync.rs @@ -3,7 +3,7 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration}; use anyhow::{anyhow, Context}; use log::info; use protocol::{ - bitcoin::{Block, BlockHash}, + bitcoin::{hashes::Hash, Block, BlockHash}, constants::ChainAnchor, hasher::BaseHash, }; @@ -33,6 +33,7 @@ pub struct Spaced { pub network: ExtendedNetwork, pub chain: LiveStore, pub block_index: Option, + pub block_index_full: bool, pub rpc: BitcoinRpc, pub data_dir: PathBuf, pub bind: Vec, @@ -147,7 +148,7 @@ impl Spaced { shutdown: broadcast::Sender<()>, ) -> anyhow::Result<()> { let start_block: ChainAnchor = { self.chain.state.tip.read().expect("read").clone() }; - let mut node = Node::new(); + let mut node = Node::new(self.block_index_full); info!( "Start block={} height={}", @@ -190,12 +191,37 @@ impl Spaced { Ok(()) } - pub fn genesis(network: ExtendedNetwork) -> ChainAnchor { - match network { + pub async fn genesis( + rpc: &BitcoinRpc, + network: ExtendedNetwork, + ) -> anyhow::Result { + let mut anchor = match network { ExtendedNetwork::Testnet => ChainAnchor::TESTNET(), ExtendedNetwork::Testnet4 => ChainAnchor::TESTNET4(), ExtendedNetwork::Regtest => ChainAnchor::REGTEST(), + ExtendedNetwork::Mainnet => ChainAnchor::MAINNET(), + ExtendedNetwork::MainnetAlpha => ChainAnchor::MAINNET_ALPHA(), _ => panic!("unsupported network"), + }; + + if anchor.hash == BlockHash::all_zeros() { + let client = reqwest::Client::new(); + + anchor.hash = match rpc + .send_json(&client, &rpc.get_block_hash(anchor.height)) + .await + { + Ok(hash) => hash, + Err(e) => { + return Err(anyhow!( + "Could not retrieve activation block at height {}: {}", + anchor.height, + e + )); + } + } } + + Ok(anchor) } } diff --git a/node/src/wallets.rs b/node/src/wallets.rs index 0dbf5a2..337d499 100644 --- a/node/src/wallets.rs +++ b/node/src/wallets.rs @@ -9,7 +9,8 @@ use protocol::{ constants::ChainAnchor, hasher::{KeyHasher, SpaceKey}, prepare::DataSource, - sname::{NameLike, SName}, + script::SpaceScript, + slabel::SLabel, Space, }; use serde::{Deserialize, Serialize}; @@ -26,13 +27,13 @@ use wallet::{ KeychainKind, LocalOutput, }, bitcoin, - bitcoin::{Address, Amount, FeeRate}, + bitcoin::{Address, Amount, FeeRate, OutPoint}, builder::{ CoinTransfer, SpaceTransfer, SpacesAwareCoinSelection, TransactionTag, TransferRequest, }, DoubleUtxo, SpacesWallet, WalletInfo, }; - +use wallet::bdk_wallet::wallet::tx_builder::TxOrdering; use crate::{ config::ExtendedNetwork, node::BlockSource, @@ -92,6 +93,11 @@ pub enum WalletCommand { ListUnspent { resp: crate::rpc::Responder>>, }, + ForceSpendOutput { + outpoint: OutPoint, + fee_rate: FeeRate, + resp: crate::rpc::Responder>, + }, GetBalance { resp: crate::rpc::Responder>, }, @@ -154,8 +160,11 @@ impl RpcWallet { balance, dust: unspent .into_iter() - .filter(|output| output.is_spaceout || - output.output.txout.value <= SpacesAwareCoinSelection::DUST_THRESHOLD) + .filter(|output| + (output.is_spaceout || output.output.txout.value <= SpacesAwareCoinSelection::DUST_THRESHOLD) && + // trusted pending only + (output.output.confirmation_time.is_confirmed() || output.output.keychain == KeychainKind::Internal) + ) .map(|output| output.output.txout.value) .sum(), }; @@ -168,27 +177,71 @@ impl RpcWallet { fn handle_fee_bump( source: &BitcoinBlockSource, + state: &mut LiveSnapshot, wallet: &mut SpacesWallet, txid: Txid, fee_rate: FeeRate, ) -> anyhow::Result> { - let mut builder = wallet.spaces.build_fee_bump(txid)?; - builder.fee_rate(fee_rate); + let coin_selection = Self::get_spaces_coin_selection(wallet, state)?; + let mut builder = wallet.spaces + .build_fee_bump(txid)? + .coin_selection(coin_selection); + + builder + .enable_rbf() + .ordering(TxOrdering::Untouched) + .fee_rate(fee_rate); let psbt = builder.finish()?; let tx = wallet.sign(psbt, None)?; + let new_txid = tx.compute_txid(); let confirmation = source.rpc.broadcast_tx(&source.client, &tx)?; wallet.insert_tx(tx, confirmation)?; wallet.commit()?; Ok(vec![TxResponse { - txid, + txid: new_txid, tags: vec![TransactionTag::FeeBump], error: None, }]) } + fn handle_force_spend_output( + source: &BitcoinBlockSource, + state: &mut LiveSnapshot, + wallet: &mut SpacesWallet, + output: OutPoint, + fee_rate: FeeRate, + ) -> anyhow::Result { + let coin_selection = Self::get_spaces_coin_selection(wallet, state)?; + let addre = wallet.spaces.next_unused_address(KeychainKind::External); + let mut builder = wallet + .spaces + .build_tx() + .coin_selection(coin_selection); + + builder.ordering(TxOrdering::Untouched); + builder.fee_rate(fee_rate); + builder.enable_rbf(); + builder.add_utxo(output)?; + builder.add_recipient(addre.script_pubkey(), Amount::from_sat(5000)); + + let psbt = builder.finish()?; + let tx = wallet.sign(psbt, None)?; + + let txid = tx.compute_txid(); + let confirmation = source.rpc.broadcast_tx(&source.client, &tx)?; + wallet.insert_tx(tx, confirmation)?; + wallet.commit()?; + + Ok(TxResponse { + txid, + tags: vec![TransactionTag::ForceSpendTestOnly], + error: None, + }) + } + fn wallet_handle_commands( network: ExtendedNetwork, source: &BitcoinBlockSource, @@ -207,7 +260,15 @@ impl RpcWallet { fee_rate, resp, } => { - let result = Self::handle_fee_bump(source, wallet, txid, fee_rate); + let result = Self::handle_fee_bump(source, &mut state, wallet, txid, fee_rate); + _ = resp.send(result); + } + WalletCommand::ForceSpendOutput { + outpoint, + fee_rate, + resp, + } => { + let result = Self::handle_force_spend_output(source, &mut state, wallet, outpoint, fee_rate); _ = resp.send(result); } WalletCommand::GetNewAddress { kind, resp } => { @@ -414,7 +475,7 @@ impl RpcWallet { return Ok(Some(space_address.0)); } - let sname = match SName::from_str(to) { + let sname = match SLabel::from_str(to) { Ok(sname) => sname, Err(_) => { return Err(anyhow!( @@ -423,7 +484,7 @@ impl RpcWallet { } }; - let spacehash = SpaceKey::from(Sha256::hash(sname.to_bytes())); + let spacehash = SpaceKey::from(Sha256::hash(sname.as_ref())); let script_pubkey = match store.get_space_info(&spacehash)? { None => return Ok(None), Some(fullspaceout) => fullspaceout.spaceout.script_pubkey, @@ -446,8 +507,10 @@ impl RpcWallet { if dust > SpacesAwareCoinSelection::DUST_THRESHOLD { // Allowing higher dust may space outs to be accidentally // spent during coin selection - return Err(anyhow!("dust cannot be higher than {}", - SpacesAwareCoinSelection::DUST_THRESHOLD)); + return Err(anyhow!( + "dust cannot be higher than {}", + SpacesAwareCoinSelection::DUST_THRESHOLD + )); } } @@ -486,7 +549,7 @@ impl RpcWallet { let spaces: Vec<_> = params .spaces .iter() - .filter_map(|space| SName::from_str(space).ok()) + .filter_map(|space| SLabel::from_str(space).ok()) .collect(); if spaces.len() != params.spaces.len() { return Err(anyhow!("sendspaces: some names were malformed")); @@ -498,7 +561,7 @@ impl RpcWallet { Some(r) => r, }; for space in spaces { - let spacehash = SpaceKey::from(Sha256::hash(space.to_bytes())); + let spacehash = SpaceKey::from(Sha256::hash(space.as_ref())); match store.get_space_info(&spacehash)? { None => return Err(anyhow!("sendspaces: you don't own `{}`", space)), Some(full) @@ -521,10 +584,10 @@ impl RpcWallet { } } RpcWalletRequest::Open(params) => { - let name = SName::from_str(¶ms.name)?; + let name = SLabel::from_str(¶ms.name)?; if !tx.force { // Warn if already exists - let spacehash = SpaceKey::from(Sha256::hash(name.to_bytes())); + let spacehash = SpaceKey::from(Sha256::hash(name.as_ref())); let spaceout = store.get_space_info(&spacehash)?; if spaceout.is_some() { return Err(anyhow!("open '{}': space already exists", params.name)); @@ -534,8 +597,8 @@ impl RpcWallet { builder = builder.add_open(¶ms.name, Amount::from_sat(params.amount)); } RpcWalletRequest::Bid(params) => { - let name = SName::from_str(¶ms.name)?; - let spacehash = SpaceKey::from(Sha256::hash(name.to_bytes())); + let name = SLabel::from_str(¶ms.name)?; + let spacehash = SpaceKey::from(Sha256::hash(name.as_ref())); let spaceout = store.get_space_info(&spacehash)?; if spaceout.is_none() { return Err(anyhow!("bid '{}': space does not exist", params.name)); @@ -543,8 +606,8 @@ impl RpcWallet { builder = builder.add_bid(spaceout.unwrap(), Amount::from_sat(params.amount)); } RpcWalletRequest::Register(params) => { - let name = SName::from_str(¶ms.name)?; - let spacehash = SpaceKey::from(Sha256::hash(name.to_bytes())); + let name = SLabel::from_str(¶ms.name)?; + let spacehash = SpaceKey::from(Sha256::hash(name.as_ref())); let spaceout = store.get_space_info(&spacehash)?; if spaceout.is_none() { return Err(anyhow!("register '{}': space does not exist", params.name)); @@ -596,8 +659,8 @@ impl RpcWallet { RpcWalletRequest::Execute(params) => { let mut spaces = Vec::new(); for space in params.context.iter() { - let name = SName::from_str(&space)?; - let spacehash = SpaceKey::from(Sha256::hash(name.to_bytes())); + let name = SLabel::from_str(&space)?; + let spacehash = SpaceKey::from(Sha256::hash(name.as_ref())); let spaceout = store.get_space_info(&spacehash)?; if spaceout.is_none() { return Err(anyhow!("execute on '{}': space does not exist", space)); @@ -615,7 +678,9 @@ impl RpcWallet { recipient: address.0, }); } - builder = builder.add_execute(spaces, params.space_script); + + let script = SpaceScript::nop_script(params.space_script); + builder = builder.add_execute(spaces, script); } } } @@ -789,6 +854,22 @@ impl RpcWallet { resp_rx.await? } + pub async fn send_force_spend( + &self, + outpoint: OutPoint, + fee_rate: FeeRate, + ) -> anyhow::Result { + let (resp, resp_rx) = oneshot::channel(); + self.sender + .send(WalletCommand::ForceSpendOutput { + outpoint, + fee_rate, + resp, + }) + .await?; + resp_rx.await? + } + pub async fn send_list_spaces(&self) -> anyhow::Result> { let (resp, resp_rx) = oneshot::channel(); self.sender.send(WalletCommand::ListSpaces { resp }).await?; diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index c07b13a..d09e304 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -8,11 +8,12 @@ bitcoin = { version = "0.32.2", features = ["base64", "serde"], default-features log = "0.4.14" ## optional features -bincode = { version = "2.0.0-rc.3", features = [ "derive", "serde" ], default-features = false, optional = true } +bincode = { version = "2.0.0-rc.3", features = [ "derive", "serde", "alloc" ], default-features = false, optional = true } serde = { version = "^1.0", features = ["derive"], default-features = false, optional = true } [dev-dependencies] rand = "0.8.5" +serde_json = "1.0.132" [features] default = [] diff --git a/protocol/src/constants.rs b/protocol/src/constants.rs index 637cf76..2ca677e 100644 --- a/protocol/src/constants.rs +++ b/protocol/src/constants.rs @@ -16,6 +16,8 @@ pub struct ChainAnchor { pub height: u32, } +pub const RESERVED_SPACES: [&'static [u8]; 3] = [b"\x07example", b"\x04test", b"\x04local"]; + /// The number of blocks between each rollout of new spaces for auction. pub const ROLLOUT_BLOCK_INTERVAL: u32 = 144; @@ -55,6 +57,16 @@ impl ChainAnchor { } } + pub const MAINNET: fn() -> Self = || ChainAnchor { + hash: BlockHash::all_zeros(), + height: 871_222, + }; + + pub const MAINNET_ALPHA: fn() -> Self = || ChainAnchor { + hash: BlockHash::all_zeros(), + height: 870_000, + }; + // Testnet4 activation block pub const TESTNET4: fn() -> Self = || { Self::new( @@ -63,7 +75,7 @@ impl ChainAnchor { 0x3d, 0x68, 0x8f, 0x7a, 0x90, 0x0d, 0x56, 0x79, 0xe0, 0x63, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ], - 38_580, + 50_000, ) }; @@ -94,6 +106,8 @@ impl ChainAnchor { #[cfg(feature = "bincode")] pub mod bincode_impl { + use alloc::vec::Vec; + use bincode::{ config, de::Decoder, diff --git a/protocol/src/errors.rs b/protocol/src/errors.rs index 90bd3d3..3d61828 100644 --- a/protocol/src/errors.rs +++ b/protocol/src/errors.rs @@ -1,6 +1,8 @@ use alloc::string::String; use core::fmt::{self, Display, Formatter}; +use crate::slabel::NameErrorKind; + pub type Result = core::result::Result; #[derive(Debug)] @@ -26,11 +28,6 @@ pub enum StateErrorKind { MissingOpenTxOut, } -#[derive(Debug)] -pub enum NameErrorKind { - MalformedName, -} - impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index bf04de1..49d4427 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -22,16 +22,15 @@ use serde::{Deserialize, Serialize}; use crate::{ constants::{BID_PSBT_INPUT_SEQUENCE, BID_PSBT_TX_LOCK_TIME, BID_PSBT_TX_VERSION}, - sname::SName, + slabel::SLabel, }; pub mod constants; pub mod errors; pub mod hasher; -pub mod opcodes; pub mod prepare; pub mod script; -pub mod sname; +pub mod slabel; pub mod validate; #[derive(Clone, PartialEq, Debug)] @@ -70,8 +69,7 @@ pub struct Space { /// The target is the Space name if a spend does not follow /// protocol rules the target space will be disassociated from future /// spends - #[cfg_attr(feature = "bincode", bincode(with_serde))] - pub name: SName, + pub name: SLabel, // Space specific spending conditions pub covenant: Covenant, } @@ -104,7 +102,7 @@ pub enum Covenant { /// Block height at which this covenant expires expire_height: u32, // Any data associated with this Space - data: Option>, + data: Option, }, /// Using a reserved op code during a spend /// Space will be locked until a future upgrade @@ -115,10 +113,7 @@ pub enum Covenant { #[derive(Copy, Clone, PartialEq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[cfg_attr( - feature = "serde", - serde(rename_all = "snake_case", tag = "reason") -)] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case", tag = "reason"))] pub enum RevokeReason { /// Space was prematurely spent during the auctions phase PrematureClaim, @@ -127,14 +122,14 @@ pub enum RevokeReason { BadSpend, Expired, #[cfg_attr(feature = "serde", serde(untagged))] - BidPsbt(BidPsbtReason) + BidPsbt(BidPsbtReason), } #[derive(Copy, Clone, PartialEq, Debug, Eq)] #[cfg_attr( feature = "serde", derive(Serialize, Deserialize), - serde(rename_all = "snake_case", tag="reason") + serde(rename_all = "snake_case", tag = "reason") )] #[cfg_attr(feature = "bincode", derive(Encode, Decode))] pub enum RejectReason { @@ -161,6 +156,89 @@ pub enum BidPsbtReason { OutputSpent, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Bytes(Vec); + +impl Bytes { + pub fn new(bytes: Vec) -> Self { + Bytes(bytes) + } + pub fn to_vec(self) -> Vec { + self.0 + } + pub fn as_slice(&self) -> &[u8] { + self.0.as_slice() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[cfg(feature = "serde")] +pub mod serde_bytes_impl { + use bitcoin::hex::prelude::*; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use crate::Bytes; + + impl Serialize for Bytes { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&self.0.to_lower_hex_string()) + } else { + serializer.serialize_bytes(&self.0) + } + } + } + + impl<'de> Deserialize<'de> for Bytes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + let hex_str = String::deserialize(deserializer)?; + let c = Vec::from_hex(&hex_str).map_err(serde::de::Error::custom)?; + Ok(Bytes::new(c)) + } else { + let bytes: Vec = Deserialize::deserialize(deserializer)?; + Ok(Bytes(bytes)) + } + } + } +} + +#[cfg(feature = "bincode")] +pub mod bincode_bytes_impl { + use bincode::{ + de::Decoder, + enc::Encoder, + error::{DecodeError, EncodeError}, + impl_borrow_decode, Decode, Encode, + }; + + use super::Bytes; + + impl Encode for Bytes { + fn encode(&self, encoder: &mut E) -> Result<(), EncodeError> { + Encode::encode(&self.as_slice(), encoder) + } + } + + impl Decode for Bytes { + fn decode(decoder: &mut D) -> Result { + let raw: Vec = Decode::decode(decoder)?; + Ok(Bytes::new(raw)) + } + } + + impl_borrow_decode!(Bytes); +} + impl Space { pub fn is_expired(&self, height: u32) -> bool { match self.covenant { @@ -206,7 +284,7 @@ impl Space { } } - pub fn data_owned(&self) -> Option> { + pub fn data_owned(&self) -> Option { match &self.covenant { Covenant::Transfer { data, .. } => match &data { None => None, diff --git a/protocol/src/opcodes.rs b/protocol/src/opcodes.rs deleted file mode 100644 index 22ee743..0000000 --- a/protocol/src/opcodes.rs +++ /dev/null @@ -1,295 +0,0 @@ -#[derive(Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Debug)] -pub struct SpaceOpcode { - pub code: u8, -} - -impl From for SpaceOpcode { - fn from(value: u8) -> Self { - return SpaceOpcode { code: value }; - } -} - -impl From for u8 { - fn from(value: SpaceOpcode) -> Self { - value.code - } -} - -macro_rules! define_space_opcodes { - ($($op:ident => $val:expr, $doc:expr);*) => { - $( - #[doc = $doc] - pub const $op: u8 = $val; - )* - - impl core::fmt::Display for SpaceOpcode { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - match *self { - $( - SpaceOpcode { code: $op } => core::fmt::Display::fmt(stringify!($op), f), - )+ - } - } - } - } -} - -define_space_opcodes! { - OP_PUSH => 0x00, "Reads varint bytes as N; push the next N bytes as an array onto the stack."; - OP_OPEN => 0x01, "Pops a space encoded name off the stack; initiates the auction."; - OP_SET => 0x02, "Pops encoded space element from the stack; associates this data with the space being transferred."; - OP_SETALL => 0x03, "Pops encoded space records as a single element off the stack; \ - associates it with all spaces being transferred."; - OP_NOP_4 => 0x04, "Does nothing."; - OP_NOP_5 => 0x05, "Does nothing."; - OP_NOP_6 => 0x06, "Does nothing."; - OP_NOP_7 => 0x07, "Does nothing."; - OP_NOP_8 => 0x08, "Does nothing."; - OP_NOP_9 => 0x09, "Does nothing."; - OP_NOP_A => 0x0A, "Does nothing."; - OP_NOP_B => 0x0B, "Does nothing."; - OP_NOP_C => 0x0C, "Does nothing."; - OP_NOP_D => 0x0D, "Does nothing."; - OP_NOP_E => 0x0E, "Does nothing."; - OP_NOP_F => 0x0F, "Does nothing."; - OP_NOP_10 => 0x10, "Does nothing."; - OP_NOP_11 => 0x11, "Does nothing."; - OP_NOP_12 => 0x12, "Does nothing."; - OP_NOP_13 => 0x13, "Does nothing."; - OP_NOP_14 => 0x14, "Does nothing."; - OP_NOP_15 => 0x15, "Does nothing."; - OP_NOP_16 => 0x16, "Does nothing."; - OP_NOP_17 => 0x17, "Does nothing."; - OP_NOP_18 => 0x18, "Does nothing."; - OP_NOP_19 => 0x19, "Does nothing."; - OP_NOP_1A => 0x1A, "Does nothing."; - OP_NOP_1B => 0x1B, "Does nothing."; - OP_NOP_1C => 0x1C, "Does nothing."; - OP_NOP_1D => 0x1D, "Does nothing."; - OP_RESERVED_1E => 0x1E, "Reserved op code."; - OP_RESERVED_1F => 0x1F, "Reserved op code."; - OP_RESERVED_20 => 0x20, "Reserved op code."; - OP_RESERVED_21 => 0x21, "Reserved op code."; - OP_RESERVED_22 => 0x22, "Reserved op code."; - OP_RESERVED_23 => 0x23, "Reserved op code."; - OP_RESERVED_24 => 0x24, "Reserved op code."; - OP_RESERVED_25 => 0x25, "Reserved op code."; - OP_RESERVED_26 => 0x26, "Reserved op code."; - OP_RESERVED_27 => 0x27, "Reserved op code."; - OP_RESERVED_28 => 0x28, "Reserved op code."; - OP_RESERVED_29 => 0x29, "Reserved op code."; - OP_RESERVED_2A => 0x2A, "Reserved op code."; - OP_RESERVED_2B => 0x2B, "Reserved op code."; - OP_RESERVED_2C => 0x2C, "Reserved op code."; - OP_RESERVED_2D => 0x2D, "Reserved op code."; - OP_RESERVED_2E => 0x2E, "Reserved op code."; - OP_RESERVED_2F => 0x2F, "Reserved op code."; - OP_RESERVED_30 => 0x30, "Reserved op code."; - OP_RESERVED_31 => 0x31, "Reserved op code."; - OP_RESERVED_32 => 0x32, "Reserved op code."; - OP_RESERVED_33 => 0x33, "Reserved op code."; - OP_RESERVED_34 => 0x34, "Reserved op code."; - OP_RESERVED_35 => 0x35, "Reserved op code."; - OP_RESERVED_36 => 0x36, "Reserved op code."; - OP_RESERVED_37 => 0x37, "Reserved op code."; - OP_RESERVED_38 => 0x38, "Reserved op code."; - OP_RESERVED_39 => 0x39, "Reserved op code."; - OP_RESERVED_3A => 0x3A, "Reserved op code."; - OP_RESERVED_3B => 0x3B, "Reserved op code."; - OP_RESERVED_3C => 0x3C, "Reserved op code."; - OP_RESERVED_3D => 0x3D, "Reserved op code."; - OP_RESERVED_3E => 0x3E, "Reserved op code."; - OP_RESERVED_3F => 0x3F, "Reserved op code."; - OP_RESERVED_40 => 0x40, "Reserved op code."; - OP_RESERVED_41 => 0x41, "Reserved op code."; - OP_RESERVED_42 => 0x42, "Reserved op code."; - OP_RESERVED_43 => 0x43, "Reserved op code."; - OP_RESERVED_44 => 0x44, "Reserved op code."; - OP_RESERVED_45 => 0x45, "Reserved op code."; - OP_RESERVED_46 => 0x46, "Reserved op code."; - OP_RESERVED_47 => 0x47, "Reserved op code."; - OP_RESERVED_48 => 0x48, "Reserved op code."; - OP_RESERVED_49 => 0x49, "Reserved op code."; - OP_RESERVED_4A => 0x4A, "Reserved op code."; - OP_RESERVED_4B => 0x4B, "Reserved op code."; - OP_RESERVED_4C => 0x4C, "Reserved op code."; - OP_RESERVED_4D => 0x4D, "Reserved op code."; - OP_RESERVED_4E => 0x4E, "Reserved op code."; - OP_RESERVED_4F => 0x4F, "Reserved op code."; - OP_RESERVED_50 => 0x50, "Reserved op code."; - OP_RESERVED_51 => 0x51, "Reserved op code."; - OP_RESERVED_52 => 0x52, "Reserved op code."; - OP_RESERVED_53 => 0x53, "Reserved op code."; - OP_RESERVED_54 => 0x54, "Reserved op code."; - OP_RESERVED_55 => 0x55, "Reserved op code."; - OP_RESERVED_56 => 0x56, "Reserved op code."; - OP_RESERVED_57 => 0x57, "Reserved op code."; - OP_RESERVED_58 => 0x58, "Reserved op code."; - OP_RESERVED_59 => 0x59, "Reserved op code."; - OP_RESERVED_5A => 0x5A, "Reserved op code."; - OP_RESERVED_5B => 0x5B, "Reserved op code."; - OP_RESERVED_5C => 0x5C, "Reserved op code."; - OP_RESERVED_5D => 0x5D, "Reserved op code."; - OP_RESERVED_5E => 0x5E, "Reserved op code."; - OP_RESERVED_5F => 0x5F, "Reserved op code."; - OP_RESERVED_60 => 0x60, "Reserved op code."; - OP_RESERVED_61 => 0x61, "Reserved op code."; - OP_RESERVED_62 => 0x62, "Reserved op code."; - OP_RESERVED_63 => 0x63, "Reserved op code."; - OP_RESERVED_64 => 0x64, "Reserved op code."; - OP_RESERVED_65 => 0x65, "Reserved op code."; - OP_RESERVED_66 => 0x66, "Reserved op code."; - OP_RESERVED_67 => 0x67, "Reserved op code."; - OP_RESERVED_68 => 0x68, "Reserved op code."; - OP_RESERVED_69 => 0x69, "Reserved op code."; - OP_RESERVED_6A => 0x6A, "Reserved op code."; - OP_RESERVED_6B => 0x6B, "Reserved op code."; - OP_RESERVED_6C => 0x6C, "Reserved op code."; - OP_RESERVED_6D => 0x6D, "Reserved op code."; - OP_RESERVED_6E => 0x6E, "Reserved op code."; - OP_RESERVED_6F => 0x6F, "Reserved op code."; - OP_RESERVED_70 => 0x70, "Reserved op code."; - OP_RESERVED_71 => 0x71, "Reserved op code."; - OP_RESERVED_72 => 0x72, "Reserved op code."; - OP_RESERVED_73 => 0x73, "Reserved op code."; - OP_RESERVED_74 => 0x74, "Reserved op code."; - OP_RESERVED_75 => 0x75, "Reserved op code."; - OP_RESERVED_76 => 0x76, "Reserved op code."; - OP_RESERVED_77 => 0x77, "Reserved op code."; - OP_RESERVED_78 => 0x78, "Reserved op code."; - OP_RESERVED_79 => 0x79, "Reserved op code."; - OP_RESERVED_7A => 0x7A, "Reserved op code."; - OP_RESERVED_7B => 0x7B, "Reserved op code."; - OP_RESERVED_7C => 0x7C, "Reserved op code."; - OP_RESERVED_7D => 0x7D, "Reserved op code."; - OP_RESERVED_7E => 0x7E, "Reserved op code."; - OP_RESERVED_7F => 0x7F, "Reserved op code."; - OP_RESERVED_80 => 0x80, "Reserved op code."; - OP_RESERVED_81 => 0x81, "Reserved op code."; - OP_RESERVED_82 => 0x82, "Reserved op code."; - OP_RESERVED_83 => 0x83, "Reserved op code."; - OP_RESERVED_84 => 0x84, "Reserved op code."; - OP_RESERVED_85 => 0x85, "Reserved op code."; - OP_RESERVED_86 => 0x86, "Reserved op code."; - OP_RESERVED_87 => 0x87, "Reserved op code."; - OP_RESERVED_88 => 0x88, "Reserved op code."; - OP_RESERVED_89 => 0x89, "Reserved op code."; - OP_RESERVED_8A => 0x8A, "Reserved op code."; - OP_RESERVED_8B => 0x8B, "Reserved op code."; - OP_RESERVED_8C => 0x8C, "Reserved op code."; - OP_RESERVED_8D => 0x8D, "Reserved op code."; - OP_RESERVED_8E => 0x8E, "Reserved op code."; - OP_RESERVED_8F => 0x8F, "Reserved op code."; - OP_RESERVED_90 => 0x90, "Reserved op code."; - OP_RESERVED_91 => 0x91, "Reserved op code."; - OP_RESERVED_92 => 0x92, "Reserved op code."; - OP_RESERVED_93 => 0x93, "Reserved op code."; - OP_RESERVED_94 => 0x94, "Reserved op code."; - OP_RESERVED_95 => 0x95, "Reserved op code."; - OP_RESERVED_96 => 0x96, "Reserved op code."; - OP_RESERVED_97 => 0x97, "Reserved op code."; - OP_RESERVED_98 => 0x98, "Reserved op code."; - OP_RESERVED_99 => 0x99, "Reserved op code."; - OP_RESERVED_9A => 0x9A, "Reserved op code."; - OP_RESERVED_9B => 0x9B, "Reserved op code."; - OP_RESERVED_9C => 0x9C, "Reserved op code."; - OP_RESERVED_9D => 0x9D, "Reserved op code."; - OP_RESERVED_9E => 0x9E, "Reserved op code."; - OP_RESERVED_9F => 0x9F, "Reserved op code."; - OP_RESERVED_A0 => 0xA0, "Reserved op code."; - OP_RESERVED_A1 => 0xA1, "Reserved op code."; - OP_RESERVED_A2 => 0xA2, "Reserved op code."; - OP_RESERVED_A3 => 0xA3, "Reserved op code."; - OP_RESERVED_A4 => 0xA4, "Reserved op code."; - OP_RESERVED_A5 => 0xA5, "Reserved op code."; - OP_RESERVED_A6 => 0xA6, "Reserved op code."; - OP_RESERVED_A7 => 0xA7, "Reserved op code."; - OP_RESERVED_A8 => 0xA8, "Reserved op code."; - OP_RESERVED_A9 => 0xA9, "Reserved op code."; - OP_RESERVED_AA => 0xAA, "Reserved op code."; - OP_RESERVED_AB => 0xAB, "Reserved op code."; - OP_RESERVED_AC => 0xAC, "Reserved op code."; - OP_RESERVED_AD => 0xAD, "Reserved op code."; - OP_RESERVED_AE => 0xAE, "Reserved op code."; - OP_RESERVED_AF => 0xAF, "Reserved op code."; - OP_RESERVED_B0 => 0xB0, "Reserved op code."; - OP_RESERVED_B1 => 0xB1, "Reserved op code."; - OP_RESERVED_B2 => 0xB2, "Reserved op code."; - OP_RESERVED_B3 => 0xB3, "Reserved op code."; - OP_RESERVED_B4 => 0xB4, "Reserved op code."; - OP_RESERVED_B5 => 0xB5, "Reserved op code."; - OP_RESERVED_B6 => 0xB6, "Reserved op code."; - OP_RESERVED_B7 => 0xB7, "Reserved op code."; - OP_RESERVED_B8 => 0xB8, "Reserved op code."; - OP_RESERVED_B9 => 0xB9, "Reserved op code."; - OP_RESERVED_BA => 0xBA, "Reserved op code."; - OP_RESERVED_BB => 0xBB, "Reserved op code."; - OP_RESERVED_BC => 0xBC, "Reserved op code."; - OP_RESERVED_BD => 0xBD, "Reserved op code."; - OP_RESERVED_BE => 0xBE, "Reserved op code."; - OP_RESERVED_BF => 0xBF, "Reserved op code."; - OP_RESERVED_C0 => 0xC0, "Reserved op code."; - OP_RESERVED_C1 => 0xC1, "Reserved op code."; - OP_RESERVED_C2 => 0xC2, "Reserved op code."; - OP_RESERVED_C3 => 0xC3, "Reserved op code."; - OP_RESERVED_C4 => 0xC4, "Reserved op code."; - OP_RESERVED_C5 => 0xC5, "Reserved op code."; - OP_RESERVED_C6 => 0xC6, "Reserved op code."; - OP_RESERVED_C7 => 0xC7, "Reserved op code."; - OP_RESERVED_C8 => 0xC8, "Reserved op code."; - OP_RESERVED_C9 => 0xC9, "Reserved op code."; - OP_RESERVED_CA => 0xCA, "Reserved op code."; - OP_RESERVED_CB => 0xCB, "Reserved op code."; - OP_RESERVED_CC => 0xCC, "Reserved op code."; - OP_RESERVED_CD => 0xCD, "Reserved op code."; - OP_RESERVED_CE => 0xCE, "Reserved op code."; - OP_RESERVED_CF => 0xCF, "Reserved op code."; - OP_RESERVED_D0 => 0xD0, "Reserved op code."; - OP_RESERVED_D1 => 0xD1, "Reserved op code."; - OP_RESERVED_D2 => 0xD2, "Reserved op code."; - OP_RESERVED_D3 => 0xD3, "Reserved op code."; - OP_RESERVED_D4 => 0xD4, "Reserved op code."; - OP_RESERVED_D5 => 0xD5, "Reserved op code."; - OP_RESERVED_D6 => 0xD6, "Reserved op code."; - OP_RESERVED_D7 => 0xD7, "Reserved op code."; - OP_RESERVED_D8 => 0xD8, "Reserved op code."; - OP_RESERVED_D9 => 0xD9, "Reserved op code."; - OP_RESERVED_DA => 0xDA, "Reserved op code."; - OP_RESERVED_DB => 0xDB, "Reserved op code."; - OP_RESERVED_DC => 0xDC, "Reserved op code."; - OP_RESERVED_DD => 0xDD, "Reserved op code."; - OP_RESERVED_DE => 0xDE, "Reserved op code."; - OP_RESERVED_DF => 0xDF, "Reserved op code."; - OP_RESERVED_E0 => 0xE0, "Reserved op code."; - OP_RESERVED_E1 => 0xE1, "Reserved op code."; - OP_RESERVED_E2 => 0xE2, "Reserved op code."; - OP_RESERVED_E3 => 0xE3, "Reserved op code."; - OP_RESERVED_E4 => 0xE4, "Reserved op code."; - OP_RESERVED_E5 => 0xE5, "Reserved op code."; - OP_RESERVED_E6 => 0xE6, "Reserved op code."; - OP_RESERVED_E7 => 0xE7, "Reserved op code."; - OP_RESERVED_E8 => 0xE8, "Reserved op code."; - OP_RESERVED_E9 => 0xE9, "Reserved op code."; - OP_RESERVED_EA => 0xEA, "Reserved op code."; - OP_RESERVED_EB => 0xEB, "Reserved op code."; - OP_RESERVED_EC => 0xEC, "Reserved op code."; - OP_RESERVED_ED => 0xED, "Reserved op code."; - OP_RESERVED_EE => 0xEE, "Reserved op code."; - OP_RESERVED_EF => 0xEF, "Reserved op code."; - OP_RESERVED_F0 => 0xF0, "Reserved op code."; - OP_RESERVED_F1 => 0xF1, "Reserved op code."; - OP_RESERVED_F2 => 0xF2, "Reserved op code."; - OP_RESERVED_F3 => 0xF3, "Reserved op code."; - OP_RESERVED_F4 => 0xF4, "Reserved op code."; - OP_RESERVED_F5 => 0xF5, "Reserved op code."; - OP_RESERVED_F6 => 0xF6, "Reserved op code."; - OP_RESERVED_F7 => 0xF7, "Reserved op code."; - OP_RESERVED_F8 => 0xF8, "Reserved op code."; - OP_RESERVED_F9 => 0xF9, "Reserved op code."; - OP_RESERVED_FA => 0xFA, "Reserved op code."; - OP_RESERVED_FB => 0xFB, "Reserved op code."; - OP_RESERVED_FC => 0xFC, "Reserved op code."; - OP_RESERVED_FD => 0xFD, "Reserved op code."; - OP_RESERVED_FE => 0xFE, "Reserved op code."; - OP_RESERVED_FF => 0xFF, "Reserved op code." -} diff --git a/protocol/src/prepare.rs b/protocol/src/prepare.rs index c3baa3b..d55e449 100644 --- a/protocol/src/prepare.rs +++ b/protocol/src/prepare.rs @@ -10,7 +10,7 @@ use bitcoin::{ use crate::{ errors::Result, hasher::{KeyHasher, SpaceKey}, - script::{ScriptMachine, ScriptResult}, + script::{ScriptResult, SpaceScript}, SpaceOut, }; @@ -30,7 +30,7 @@ pub struct TxContext { pub struct InputContext { pub n: usize, pub sstxo: SSTXO, - pub script: Option>, + pub script: Option>, } /// Spent Spaces Transaction Output @@ -112,7 +112,7 @@ impl TxContext { // Run any space scripts if let Some(script) = input.witness.tapscript() { - spacein.script = Some(ScriptMachine::execute::(n, src, script)?); + spacein.script = SpaceScript::eval::(src, script)?; } inputs.push(spacein) } @@ -188,6 +188,10 @@ pub fn is_magic_lock_time(lock_time: &LockTime) -> bool { impl TrackableOutput for TxOut { fn is_magic_output(&self) -> bool { - self.value.to_sat() % 10 == 2 + is_magic_amount(self.value) } } + +pub fn is_magic_amount(amount: Amount) -> bool { + amount.to_sat() % 10 == 2 +} diff --git a/protocol/src/script.rs b/protocol/src/script.rs index 7ee26d5..5dbb1b3 100644 --- a/protocol/src/script.rs +++ b/protocol/src/script.rs @@ -1,506 +1,413 @@ -use alloc::{collections::btree_map::BTreeMap, vec::Vec}; +use alloc::vec::Vec; #[cfg(feature = "bincode")] use bincode::{Decode, Encode}; use bitcoin::{ - consensus::{Decodable, Encodable}, - opcodes::{ - all::{OP_DROP, OP_ENDIF, OP_IF}, - OP_FALSE, - }, - script::{Instruction, Instructions, PushBytesBuf}, - Script, VarInt, + opcodes::all::OP_DROP, + script, + script::{Instruction, PushBytesBuf}, + Script, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ + constants::RESERVED_SPACES, hasher::{KeyHasher, SpaceKey}, - opcodes::{SpaceOpcode, *}, prepare::DataSource, - sname::{NameLike, SName, SNameRef}, + slabel::{SLabel, SLabelRef}, validate::RejectParams, FullSpaceOut, }; -pub const MAGIC: &[u8] = &[0xde, 0xde, 0xde, 0xde]; -pub const MAGIC_LEN: usize = MAGIC.len(); +/// Ways that a script might fail. Not everything is split up as +/// much as it could be; patches welcome if more detailed errors +/// would help you. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(tag = "type", rename_all = "snake_case") +)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[non_exhaustive] +pub enum ScriptError { + MalformedName, + ReservedName, + Reject(RejectParams), +} pub type ScriptResult = Result; -#[derive(Clone, Debug)] -pub struct ScriptMachine { - pub open: Option, - pub default_sdata: Option>, - pub sdata: BTreeMap>, - pub reserve: bool, -} +pub const OP_OPEN: u8 = 1; +pub const OP_SETFALLBACK: u8 = 2; +pub const OP_RESERVE_1: u8 = 252; +pub const OP_RESERVE_2: u8 = 253; +pub const OP_RESERVE_3: u8 = 254; +pub const OP_RESERVE_4: u8 = 255; -#[derive(Clone, Debug)] -pub struct OpOpenContext { - // Whether its attempting to open a new space or an existing one - pub spaceout: SpaceKind, - pub input_index: usize, -} +pub const MAGIC: &[u8] = &[0xde, 0xde, 0xde, 0xde]; +pub const MAGIC_LEN: usize = MAGIC.len(); #[derive(Clone, Debug)] -pub enum SpaceKind { +pub enum OpenHistory { /// If OP_OPEN is attempting to initiate an auction for an existing Space, /// a reference for the previous space is included ExistingSpace(FullSpaceOut), /// A new Space we haven't seen before - NewSpace(SName), + NewSpace(SLabel), } -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -pub struct ScriptBuilder(Vec); - -pub trait SpaceScript { - fn space_instructions(&self) -> SpaceInstructions; +#[derive(Clone, Debug)] +pub enum SpaceScript { + Open(OpenHistory), + Set(Vec), + Reserve, } -pub struct SpaceInstructions<'a> { - inner: Instructions<'a>, - seen_magic: bool, - push_len: u64, - remaining: u64, - next: Option<&'a [u8]>, -} +impl SpaceScript { + pub fn create_open(name: SLabel) -> Vec { + let name = name.as_ref(); + let mut space_script = Vec::with_capacity(MAGIC_LEN + 1 + name.len()); + space_script.extend(MAGIC); + space_script.push(OP_OPEN); + space_script.extend(name); + space_script + } -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum SpaceInstruction<'a> { - /// A bunch of pushed data. - PushBytes(Vec<&'a [u8]>), - /// Some non-push opcode. - Op(SpaceOpcode), -} + pub fn create_set_fallback(data: &[u8]) -> Vec { + let mut space_script = Vec::with_capacity(MAGIC_LEN + 1 + data.len()); + space_script.extend(MAGIC); + space_script.push(OP_SETFALLBACK); + space_script.extend(data); + space_script + } -impl ScriptMachine { - fn op_open( - input_index: usize, + pub fn create_reserve() -> Vec { + let mut space_script = Vec::with_capacity(MAGIC_LEN + 1); + space_script.extend(MAGIC); + space_script.push(OP_RESERVE_1); + space_script + } + + pub fn nop_script(space_script: Vec) -> script::Builder { + script::Builder::new() + .push_slice( + PushBytesBuf::try_from(space_script) + .expect("push bytes") + .as_push_bytes(), + ) + .push_opcode(OP_DROP) + } + + pub fn eval( src: &mut T, - stack: &mut Vec>, - ) -> crate::errors::Result> { - let name = match stack.pop() { - None => return Ok(Err(ScriptError::EarlyEndOfScript)), - Some(slices) => { - if slices.len() != 1 { - return Ok(Err(ScriptError::EarlyEndOfScript)); - } - let name = SNameRef::try_from(slices[0]); - if name.is_err() { - return Ok(Err(ScriptError::UnexpectedLabelCount)); - } - let name = name.unwrap(); - if name.label_count() != 1 { - return Ok(Err(ScriptError::UnexpectedLabelCount)); + script: &Script, + ) -> crate::errors::Result>> { + let space_script = Self::find_space_script(script); + if space_script.is_none() { + return Ok(None); + } + let space_script = space_script.unwrap(); + let op = space_script[0]; + let op_data = &space_script[1..]; + + match op { + OP_OPEN => { + let open_result = Self::op_open::(src, op_data)?; + if open_result.is_err() { + return Ok(Some(Err(open_result.unwrap_err()))); } - name + Ok(Some(Ok(SpaceScript::Open(open_result.unwrap())))) } - }; + OP_SETFALLBACK => Ok(Some(Ok(SpaceScript::Set(op_data.to_vec())))), + OP_RESERVE_1..=u8::MAX => Ok(Some(Ok(SpaceScript::Reserve))), + _ => { + // NOOP + Ok(None) + } + } + } + + fn op_open( + src: &mut T, + op_data: &[u8], + ) -> crate::errors::Result> { + let name = SLabelRef::try_from(op_data); + if name.is_err() { + return Ok(Err(ScriptError::MalformedName)); + } + let name = name.unwrap(); - let spaceout = { - let spacehash = SpaceKey::from(H::hash(name.to_bytes())); + if RESERVED_SPACES + .iter() + .any(|reserved| *reserved == name.as_ref()) + { + return Ok(Err(ScriptError::ReservedName)); + } + + let kind = { + let spacehash = SpaceKey::from(H::hash(name.as_ref())); let existing = src.get_space_outpoint(&spacehash)?; match existing { - None => SpaceKind::NewSpace(name.to_owned()), - Some(outpoint) => SpaceKind::ExistingSpace(FullSpaceOut { + None => OpenHistory::NewSpace(name.to_owned()), + Some(outpoint) => OpenHistory::ExistingSpace(FullSpaceOut { txid: outpoint.txid, spaceout: src.get_spaceout(&outpoint)?.expect("spaceout exists"), }), } }; - - let open = Ok(OpOpenContext { - input_index, - spaceout, - }); + let open = Ok(kind); Ok(open) } - pub fn execute( - input_index: usize, - src: &mut T, - script: &Script, - ) -> crate::errors::Result> { - let mut machine = Self { - open: None, - default_sdata: None, - sdata: Default::default(), - reserve: false, - }; - - let mut stack = Vec::new(); - for instruction in script.space_instructions() { - if instruction.is_err() { - return Ok(Err(instruction.unwrap_err())); + // Find the first OP_PUSH bytes in a bitcoin script prefixed with our magic + #[inline(always)] + fn find_space_script(script: &Script) -> Option<&[u8]> { + // Find the first OP_PUSH bytes in a bitcoin script prefixed with our magic + let mut space_script = None; + for op in script.instructions() { + if op.is_err() { + return None; } - match instruction.unwrap() { - SpaceInstruction::PushBytes(data) => { - stack.push(data); - } - SpaceInstruction::Op(op) => { - match op.code { - OP_OPEN => { - let open_result = Self::op_open::(input_index, src, &mut stack)?; - if open_result.is_err() { - return Ok(Err(open_result.unwrap_err())); - } - machine.open = Some(open_result.unwrap()); - } - OP_SET => { - let slices = stack.pop(); - match slices { - None => return Ok(Err(ScriptError::EarlyEndOfScript)), - Some(slices) => { - if slices.len() != 1 { - // Only one stack item worth of data is allowed - return Ok(Err(ScriptError::TooManyItems)); - } - let slice = slices[0]; - if slice.len() < 1 { - return Ok(Err(ScriptError::EarlyEndOfScript)); - } - let vout = slice[0]; - let data = if slice.len() > 1 { - (&slice[1..]).to_vec() - } else { - Vec::with_capacity(0) - }; - machine.sdata.insert(vout, data); - } - } - } - OP_SETALL => { - let slices = stack.pop(); - match slices { - None => return Ok(Err(ScriptError::EarlyEndOfScript)), - Some(slices) => { - if slices.len() != 1 { - return Ok(Err(ScriptError::TooManyItems)); - } - machine.default_sdata = Some(slices[0].to_vec()); - } - } - } - // all reserved op codes - OP_RESERVED_1E..=OP_RESERVED_FF => { - machine.reserve = true; - return Ok(Ok(machine)); - } - OP_PUSH => panic!("must be handled by push bytes"), - _ => { - // nop - } + match op.unwrap() { + Instruction::Op(_) => continue, + Instruction::PushBytes(push_bytes) => { + let mut bytes = push_bytes.as_bytes(); + // Starts with our prefix + at least 1 additional op code byte + if bytes.len() < MAGIC_LEN + 1 || !bytes.starts_with(MAGIC) { + continue; } + bytes = &bytes[MAGIC_LEN..]; + space_script = Some(bytes); + break; } } } - - Ok(Ok(machine)) + space_script } } -impl SpaceScript for Script { - fn space_instructions(&self) -> SpaceInstructions { - SpaceInstructions { - inner: self.instructions(), - seen_magic: false, - push_len: 0, - remaining: 0, - next: None, +impl core::fmt::Display for ScriptError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + use ScriptError::*; + + match *self { + MalformedName => f.write_str("malformed name"), + ReservedName => f.write_str("reserved name"), + Reject(_) => f.write_str("rejected"), } } } -impl ScriptBuilder { - pub fn new() -> Self { - Self(Vec::new()) - } - - pub fn push_opcode(mut self, op: SpaceOpcode) -> Self { - self.0.push(op.into()); - self - } - - pub fn push_slice(mut self, data: &[u8]) -> Self { - self.0.push(OP_PUSH.into()); +#[cfg(test)] +mod tests { + use alloc::{collections::BTreeMap, format, string::ToString, vec::Vec}; + use core::str::FromStr; - let varint_len = VarInt(data.len() as u64); - self.0.reserve(varint_len.size()); + use bitcoin::{ + hashes::Hash as OtherHash, opcodes, script::PushBytesBuf, OutPoint, ScriptBuf, Txid, + }; - varint_len - .consensus_encode(&mut self.0) - .expect("should encode"); + use crate::{ + hasher::{Hash, KeyHasher, SpaceKey}, + prepare::DataSource, + script::{OpenHistory, ScriptError, SpaceScript, MAGIC, MAGIC_LEN, OP_OPEN}, + slabel::SLabel, + Covenant, FullSpaceOut, Space, SpaceOut, + }; - self.0.extend_from_slice(data); - self + pub struct DummySource { + spaces: BTreeMap, + spaceouts: BTreeMap, } - - pub fn to_nop_script(self) -> bitcoin::script::Builder { - let script_len = self.0.len(); - let script_varint = VarInt(script_len as u64); - let mut data = Vec::with_capacity(MAGIC_LEN + script_varint.size() + script_len); - - data.extend_from_slice(MAGIC); - script_varint - .consensus_encode(&mut data) - .expect("should encode"); - - data.extend_from_slice(self.0.as_slice()); - - let mut builder = bitcoin::script::Builder::new(); - - if data.len() <= bitcoin::blockdata::constants::MAX_SCRIPT_ELEMENT_SIZE { - builder = builder - .push_slice( - PushBytesBuf::try_from(data) - .expect("push bytes") - .as_push_bytes(), - ) - .push_opcode(OP_DROP); - } else { - let chunks = data.chunks(bitcoin::blockdata::constants::MAX_SCRIPT_ELEMENT_SIZE); - builder = builder.push_opcode(OP_FALSE).push_opcode(OP_IF); - for chunk in chunks { - builder = builder.push_slice( - PushBytesBuf::try_from(chunk.to_vec()) - .expect("push bytes") - .as_push_bytes(), - ); + impl DummySource { + fn new() -> Self { + let mut ds = Self { + spaces: Default::default(), + spaceouts: Default::default(), + }; + + for i in 0..20 { + let name = format!("@test{}", i); + ds.insert(FullSpaceOut { + txid: Txid::all_zeros(), + spaceout: SpaceOut { + n: i, + space: Some(Space { + name: SLabel::from_str(&name).unwrap(), + covenant: Covenant::Reserved, + }), + value: Default::default(), + script_pubkey: Default::default(), + }, + }); } - builder = builder.push_opcode(OP_ENDIF); - } - builder - } -} - -impl<'a> SpaceInstructions<'a> { - #[inline(always)] - fn next_bytes(&mut self) -> Option> { - if let Some(next) = self.next.take() { - return Some(Ok(next)); + ds } - while let Some(op) = self.inner.next() { - if op.is_err() { - return Some(Err(ScriptError::Serialization)); - } - match op.unwrap() { - Instruction::PushBytes(data) => { - let mut data = data.as_bytes(); - if !self.seen_magic { - if !data.starts_with(MAGIC) { - return None; - } - - self.seen_magic = true; - - if data.len() < MAGIC_LEN { - continue; - } - data = &data[MAGIC_LEN..]; - - if let Ok(script_len) = VarInt::consensus_decode(&mut data) { - self.remaining = script_len.0; - - if data.is_empty() { - continue; - } - } else { - return Some(Err(ScriptError::ExpectedValidVarInt)); - } - } - - if self.remaining == 0 { - return None; - } - if data.len() as u64 > self.remaining { - data = &data[..self.remaining as usize]; - self.remaining = 0; - } else { - self.remaining -= data.len() as u64; - } - return Some(Ok(data)); - } - Instruction::Op(_) => continue, - } + fn insert(&mut self, space: FullSpaceOut) { + let key = DummyHasher::hash(space.spaceout.space.as_ref().unwrap().name.as_ref()); + assert!( + self.spaces + .insert(SpaceKey::from(key), space.outpoint()) + .is_none(), + "space already exists" + ); + assert!( + self.spaceouts + .insert(space.outpoint(), space.spaceout) + .is_none(), + "outpoint already exists" + ); } - - if self.remaining > 0 { - return Some(Err(ScriptError::EarlyEndOfScript)); + } + impl DataSource for DummySource { + fn get_space_outpoint( + &mut self, + space_hash: &SpaceKey, + ) -> crate::errors::Result> { + Ok(self.spaces.get(space_hash).cloned()) + } + fn get_spaceout(&mut self, outpoint: &OutPoint) -> crate::errors::Result> { + Ok(self.spaceouts.get(outpoint).cloned()) } - None } -} - -impl<'a> Iterator for SpaceInstructions<'a> { - type Item = Result, ScriptError>; - - fn next(&mut self) -> Option { - while let Some(bytes) = self.next_bytes() { - if bytes.is_err() { - return Some(Err(bytes.unwrap_err())); - } - - let mut data = bytes.unwrap(); - - if self.push_len > 0 { - let mut slices = Vec::new(); - - if data.len() as u64 > self.push_len { - slices.push(&data[..self.push_len as usize]); - self.next = Some(&data[self.push_len as usize..]); - self.push_len = 0; - } else { - slices.push(data); - self.push_len -= data.len() as u64; - } - - while self.push_len > 0 { - if let Some(more_bytes) = self.next_bytes() { - if more_bytes.is_err() { - return Some(Err(more_bytes.unwrap_err())); - } - - let more_data = more_bytes.unwrap(); - if more_data.len() as u64 > self.push_len { - slices.push(&more_data[..self.push_len as usize]); - self.next = Some(&more_data[self.push_len as usize..]); - self.push_len = 0; - } else { - slices.push(more_data); - self.push_len -= more_data.len() as u64; - } - - continue; - } - - return Some(Err(ScriptError::EarlyEndOfScript)); - } - - return Some(Ok(SpaceInstruction::PushBytes(slices))); - } - - if data.is_empty() { - return Some(Err(ScriptError::EarlyEndOfScript)); - } - - let op: SpaceOpcode = data[0].into(); - - if op.code == OP_PUSH { - if data.len() < 2 { - return Some(Err(ScriptError::EarlyEndOfScript)); - } - data = &data[1..]; - - let push_bytes_len = match VarInt::consensus_decode(&mut data) - .map_err(|_| ScriptError::ExpectedValidVarInt) - { - Ok(b) => b, - Err(err) => return Some(Err(err)), - }; - - self.push_len = push_bytes_len.0; - self.next = Some(data); - continue; - } - if data.len() > 1 { - self.next = Some(&data[1..]); - } + pub struct DummyHasher; - return Some(Ok(SpaceInstruction::Op(op))); + impl KeyHasher for DummyHasher { + fn hash(data: &[u8]) -> Hash { + let mut hash = [*data.last().unwrap(); 32]; + let len = data.len().min(32); + hash[..len].copy_from_slice(&data[..len]); + hash } - - None } -} - -#[cfg(test)] -mod tests { - use crate::{ - opcodes::{OP_OPEN, OP_RESERVED_1E}, - script::{ScriptBuilder, SpaceInstruction, SpaceInstructions}, - }; #[test] - fn test_builder() { - let b = ScriptBuilder::new(); - let script = b - .push_opcode(OP_OPEN.into()) - .push_slice("data 1".as_bytes()) - .push_slice("data 2".as_bytes()) - .push_opcode(OP_OPEN.into()) - .push_slice("data 4".repeat(4096).as_bytes()) - .push_opcode(OP_OPEN.into()) - .push_opcode(OP_RESERVED_1E.into()) - .to_nop_script(); - - let iter = SpaceInstructions { - inner: script.as_script().instructions(), - seen_magic: false, - push_len: 0, - remaining: 0, - next: None, - }; + pub fn test_open_scripts() { + let mut src = DummySource::new(); + + let mut builder = ScriptBuf::new(); + + // Doesn't matter just throwing some dummy script + builder.push_slice(&[0u8; 32]); + builder.push_opcode(opcodes::all::OP_CHECKSIG); + + // Should ignore magic without an opcode + builder.push_slice( + PushBytesBuf::try_from(MAGIC.to_vec()) + .expect("push bytes") + .as_push_bytes(), + ); + + // Valid script with correct magic + let pancake_space = SpaceScript::create_open(SLabel::from_str("@pancakes").unwrap()); + builder.push_slice( + PushBytesBuf::try_from(pancake_space) + .expect("push bytes") + .as_push_bytes(), + ); + builder.push_opcode(opcodes::all::OP_DROP); + + // Another script, ignored since it picks the first one it sees + let example_space = SpaceScript::create_open(SLabel::from_str("@example").unwrap()); + builder.push_slice( + PushBytesBuf::try_from(example_space) + .expect("push bytes") + .as_push_bytes(), + ); + builder.push_opcode(opcodes::all::OP_DROP); + + let res = SpaceScript::eval::<_, DummyHasher>(&mut src, &builder) + .expect("execute") + .expect("result") + .expect("script"); + + match res { + SpaceScript::Open(ctx) => match ctx { + OpenHistory::NewSpace(space) => assert_eq!(space.to_string(), "@pancakes"), + _ => panic!("unexpected space type"), + }, + _ => panic!("unexpected op type"), + } - for instruction in iter { - let instruction = instruction.unwrap(); - match instruction { - SpaceInstruction::PushBytes(bytes) => { - for b in bytes { - println!("got {}", core::str::from_utf8(b).unwrap()) - } - } - SpaceInstruction::Op(op) => { - println!("{op}"); + // Test with existing space + let mut builder2 = ScriptBuf::new(); + let test_space = SpaceScript::create_open(SLabel::from_str("@test12").unwrap()); + builder2.push_slice( + PushBytesBuf::try_from(test_space) + .expect("push bytes") + .as_push_bytes(), + ); + builder2.push_opcode(opcodes::all::OP_DROP); + + let res = SpaceScript::eval::<_, DummyHasher>(&mut src, &builder2) + .expect("execute") + .expect("result") + .expect("script"); + + match res { + SpaceScript::Open(ctx) => match ctx { + OpenHistory::ExistingSpace(e) => { + assert_eq!( + e.spaceout.space.as_ref().unwrap().name.to_string(), + "@test12" + ) } - } + _ => panic!("unexpected space type"), + }, + _ => panic!("unexpected op type"), } } -} - -/// Ways that a script might fail. Not everything is split up as -/// much as it could be; patches welcome if more detailed errors -/// would help you. -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr( - feature = "serde", - derive(Serialize, Deserialize), - serde(tag = "type", rename_all = "snake_case") -)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[non_exhaustive] -pub enum ScriptError { - /// Some opcode expected a parameter but it was missing or truncated. - EarlyEndOfScript, - Serialization, - /// Tried to to parse a varint - ExpectedValidVarInt, - /// invalid/malformed during OP_OPEN - UnexpectedLabelCount, - TooManyItems, - TooManyOpens, - Reject(RejectParams), -} -impl core::fmt::Display for ScriptError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use ScriptError::*; + #[test] + fn test_open_malformed_name() { + let mut src = DummySource::new(); + + // Now try an OP_OPEN with malformed name + let bad_name = [200u8; 60]; + let mut space_script = Vec::with_capacity(MAGIC_LEN + 1 + bad_name.len()); + space_script.extend(MAGIC); + space_script.push(OP_OPEN); + space_script.extend(bad_name); + + let mut builder3 = ScriptBuf::new(); + builder3.push_slice( + PushBytesBuf::try_from(space_script) + .expect("push bytes") + .as_push_bytes(), + ); + + let res = SpaceScript::eval::<_, DummyHasher>(&mut src, &builder3).expect("execute"); + + assert_eq!(res.unwrap().err(), Some(ScriptError::MalformedName)); + } - match *self { - EarlyEndOfScript => f.write_str("unexpected end of script"), - Serialization => f.write_str("script serialization"), - ExpectedValidVarInt => f.write_str("expected a valid varint"), - UnexpectedLabelCount => f.write_str("unexpected label count in space name"), - TooManyItems => f.write_str("too many items"), - TooManyOpens => f.write_str("multiple opens"), - Reject(_) => f.write_str("rejected"), + #[test] + fn test_reserve() { + let mut src = DummySource::new(); + + let mut builder = ScriptBuf::new(); + let reserve_script = SpaceScript::create_reserve(); + builder.push_slice( + PushBytesBuf::try_from(reserve_script) + .expect("push bytes") + .as_push_bytes(), + ); + builder.push_opcode(opcodes::all::OP_DROP); + + let res = SpaceScript::eval::<_, DummyHasher>(&mut src, &builder) + .expect("execute") + .expect("result") + .expect("script"); + + match res { + SpaceScript::Reserve => {} + _ => panic!("unexpected op type"), } } } diff --git a/protocol/src/slabel.rs b/protocol/src/slabel.rs new file mode 100644 index 0000000..3fffe21 --- /dev/null +++ b/protocol/src/slabel.rs @@ -0,0 +1,377 @@ +#[cfg(feature = "serde")] +use alloc::string::ToString; +use alloc::{string::String, vec::Vec}; +use core::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +#[cfg(feature = "serde")] +use serde::{de::Error as ErrorUtil, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::errors::Error; + +pub const MAX_LABEL_LEN: usize = 62; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct SLabel([u8; MAX_LABEL_LEN + 1]); + +#[cfg(feature = "bincode")] +pub mod bincode_impl { + use bincode::{ + de::{read::Reader, Decoder}, + enc::Encoder, + error::{DecodeError, EncodeError}, + impl_borrow_decode, Decode, Encode, + }; + + use super::*; + + impl Encode for SLabel { + fn encode(&self, encoder: &mut E) -> Result<(), EncodeError> { + // We skip encoding the length byte since bincode adds a length prefix + // which we reuse as our length byte when decoding + Encode::encode(&self.as_ref()[1..], encoder) + } + } + + impl Decode for SLabel { + fn decode(decoder: &mut D) -> Result { + let reader = decoder.reader(); + let mut buf = [0u8; MAX_LABEL_LEN + 1]; + + // read bincode's length byte + reader.read(&mut buf[..1])?; + let len = buf[0] as usize; + if len > MAX_LABEL_LEN { + return Err(DecodeError::Other("length exceeds maximum for the label")); + } + reader.read(&mut buf[1..=len])?; + Ok(SLabel(buf)) + } + } + + impl_borrow_decode!(SLabel); +} + +#[cfg(feature = "serde")] +impl Serialize for SLabel { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(self.to_string().as_str()) + } else { + serializer.serialize_bytes(&self.0) + } + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for SLabel { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + SLabel::from_str(&s).map_err(|_| D::Error::custom("malformed name")) + } else { + let bytes: Vec = Deserialize::deserialize(deserializer)?; + let mut buf = [0u8; MAX_LABEL_LEN + 1]; + buf.copy_from_slice(&bytes); + Ok(SLabel(buf)) + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct SLabelRef<'a>(pub &'a [u8]); + +impl AsRef<[u8]> for SLabel { + fn as_ref(&self) -> &[u8] { + let len = self.0[0] as usize; + &self.0[..=len] + } +} + +impl<'a> AsRef<[u8]> for SLabelRef<'a> { + fn as_ref(&self) -> &[u8] { + let len = self.0[0] as usize; + &self.0[..=len] + } +} + +impl FromStr for SLabel { + type Err = Error; + + fn from_str(s: &str) -> Result { + s.try_into() + } +} + +impl TryFrom<&[u8; N]> for SLabel { + type Error = Error; + + fn try_from(value: &[u8; N]) -> Result { + value.as_slice().try_into() + } +} + +impl TryFrom<&Vec> for SLabel { + type Error = Error; + + fn try_from(value: &Vec) -> Result { + value.as_slice().try_into() + } +} + +impl TryFrom<&[u8]> for SLabel { + type Error = Error; + + fn try_from(value: &[u8]) -> Result { + let name_ref: SLabelRef = value.try_into()?; + Ok(name_ref.to_owned()) + } +} + +#[derive(Debug)] +pub enum NameErrorKind { + Empty, + ZeroLength, + TooLong, + EOF, + InvalidCharacter, + NotCanonical, +} + +impl<'a> TryFrom<&'a [u8]> for SLabelRef<'a> { + type Error = Error; + + fn try_from(value: &'a [u8]) -> Result { + if value.is_empty() { + return Err(Error::Name(NameErrorKind::Empty)); + } + let label_len = value[0] as usize; + if label_len == 0 { + return Err(Error::Name(NameErrorKind::ZeroLength)); + } + if label_len > MAX_LABEL_LEN { + return Err(Error::Name(NameErrorKind::TooLong)); + } + if label_len + 1 > value.len() { + return Err(Error::Name(NameErrorKind::EOF)); + } + let label = &value[..=label_len]; + if !label[1..] + .iter() + .all(|&b| b.is_ascii_lowercase() || b.is_ascii_digit()) + { + return Err(Error::Name(NameErrorKind::InvalidCharacter)); + } + Ok(SLabelRef(label)) + } +} + +impl TryFrom for SLabel { + type Error = Error; + + fn try_from(value: String) -> Result { + value.as_str().try_into() + } +} + +impl TryFrom<&str> for SLabel { + type Error = Error; + + fn try_from(value: &str) -> Result { + if !value.starts_with('@') { + return Err(Error::Name(NameErrorKind::NotCanonical)); + } + let label = &value[1..]; + if label.is_empty() { + return Err(Error::Name(NameErrorKind::ZeroLength)); + } + if label.len() > MAX_LABEL_LEN { + return Err(Error::Name(NameErrorKind::TooLong)); + } + if !label + .bytes() + .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit()) + { + return Err(Error::Name(NameErrorKind::InvalidCharacter)); + } + let mut label_bytes = [0; MAX_LABEL_LEN + 1]; + label_bytes[0] = label.len() as u8; + label_bytes[1..=label.len()].copy_from_slice(label.as_bytes()); + Ok(SLabel(label_bytes)) + } +} + +impl Display for SLabel { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let label_len = self.0[0] as usize; + let label = &self.0[1..=label_len]; + + let label_str = core::str::from_utf8(label).map_err(|_| core::fmt::Error)?; + write!(f, "@{}", label_str) + } +} + +impl Display for SLabelRef<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self) + } +} + +impl SLabel { + pub fn as_name_ref(&self) -> SLabelRef { + SLabelRef(&self.0) + } +} + +impl SLabelRef<'_> { + pub fn to_owned(&self) -> SLabel { + let mut owned = SLabel([0; MAX_LABEL_LEN + 1]); + owned.0[..self.0.len()].copy_from_slice(self.0); + owned + } +} + +#[cfg(test)] +mod tests { + use alloc::{borrow::ToOwned, format, string::ToString}; + + use super::*; + + #[test] + fn test_valid_label() { + let label_str = "@example"; + let label = SLabel::try_from(label_str).unwrap(); + assert_eq!( + label.to_string(), + "@example", + "Valid label should serialize correctly" + ); + + let dns_encoded = b"\x07example"; + let label = SLabel::try_from(dns_encoded).expect("valid label"); + assert_eq!( + label.as_ref(), + &dns_encoded[..], + "Valid label should serialize correctly" + ); + assert_eq!( + label.to_string(), + "@example", + "Valid label should serialize correctly" + ); + } + + #[test] + fn test_invalid_label() { + assert!( + SLabel::try_from("example").is_err(), + "Should fail if label does not start with '@'" + ); + assert!( + SLabel::try_from("@").is_err(), + "Should fail if label is empty after '@'" + ); + assert!( + SLabel::try_from("@EXAMPLE").is_err(), + "Should fail if label contains uppercase characters" + ); + assert!( + SLabel::try_from("@exampl3$").is_err(), + "Should fail if label contains invalid characters" + ); + assert!( + SLabel::try_from("@example-ok").is_err(), + "Should fail if label contains hyphens" + ); + assert!( + SLabel::try_from(b"\x07exam").is_err(), + "Should fail if buffer is too short" + ); + assert_eq!( + SLabel::try_from(b"\x02exam").unwrap().to_string(), + "@ex", + "Should work" + ); + assert_eq!( + SLabel::try_from(b"\x02exam").unwrap().as_ref(), + b"\x02ex", + "Should work" + ); + } + + #[test] + fn test_label_length() { + let long_label = "@".to_owned() + &"a".repeat(62); + assert!( + SLabel::try_from(long_label.as_str()).is_ok(), + "Should allow label with 62 characters" + ); + + let too_long_label = "@".to_owned() + &"a".repeat(63); + assert!( + SLabel::try_from(too_long_label.as_str()).is_err(), + "Should fail if label exceeds 62 characters" + ); + } + + #[test] + fn test_display() { + let label_str = "@example"; + let label = SLabel::try_from(label_str).unwrap(); + assert_eq!( + format!("{}", label), + label_str, + "Display should match input label" + ); + } + + #[test] + fn test_serialization() { + #[cfg(feature = "serde")] + { + use serde_json; + + let label = SLabel::try_from("@example").unwrap(); + let serialized = serde_json::to_string(&label).unwrap(); + assert_eq!( + serialized, "\"@example\"", + "Serialization should produce correct JSON" + ); + + let deserialized: SLabel = serde_json::from_str(&serialized).unwrap(); + assert_eq!( + deserialized, label, + "Deserialization should produce the original label" + ); + } + + #[cfg(feature = "bincode")] + { + use bincode::config; + let label = SLabel::try_from("@example").unwrap(); + let serialized = + bincode::encode_to_vec(label.clone(), config::standard()).expect("encoded"); + + assert_eq!( + serialized.len(), + label.as_ref().len(), + "Serialization should produce correct length" + ); + let (deserialized, _): (SLabel, _) = + bincode::decode_from_slice(serialized.as_slice(), config::standard()) + .expect("deserialize"); + assert_eq!( + deserialized, label, + "Deserialization should produce the original label" + ); + } + } +} diff --git a/protocol/src/sname.rs b/protocol/src/sname.rs deleted file mode 100644 index 9615feb..0000000 --- a/protocol/src/sname.rs +++ /dev/null @@ -1,434 +0,0 @@ -use alloc::{string::String, vec::Vec}; -use core::{ - fmt::{Display, Formatter}, - result, - str::FromStr, -}; - -#[cfg(feature = "serde")] -use serde::{ - de::{Error as ErrorUtil, SeqAccess, Visitor}, - Deserialize, Deserializer, Serialize, Serializer, -}; - -use crate::errors::{Error, NameErrorKind}; - -pub const MAX_SPACE_LEN: usize = 255; -pub const MAX_LABEL_LEN: usize = 63; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct SName([u8; MAX_SPACE_LEN]); - -#[cfg(feature = "serde")] -impl Serialize for SName { - fn serialize(&self, serializer: S) -> core::result::Result - where - S: Serializer, - { - if serializer.is_human_readable() { - serializer.serialize_str(self.to_string().as_str()) - } else { - serializer.serialize_bytes(&self.0) - } - } -} - -#[cfg(feature = "serde")] -struct SNameVisitorBytes; - -#[cfg(feature = "serde")] -impl<'de> Visitor<'de> for SNameVisitorBytes { - type Value = SName; - - fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { - formatter.write_str("a byte array representing SNAME") - } - - fn visit_seq(self, mut seq: A) -> core::result::Result - where - A: SeqAccess<'de>, - { - let mut bytes = [0; MAX_SPACE_LEN]; - let mut index = 0; - - while let Some(byte) = seq.next_element()? { - if index >= MAX_SPACE_LEN { - return Err(serde::de::Error::invalid_length(index, &self)); - } - bytes[index] = byte; - index += 1; - } - - Ok(SName(bytes)) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for SName { - fn deserialize(deserializer: D) -> core::result::Result - where - D: Deserializer<'de>, - { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - SName::from_str(&s).map_err(|_| D::Error::custom("malformed name")) - } else { - deserializer.deserialize_seq(SNameVisitorBytes) - } - } -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct SNameRef<'a>(pub &'a [u8]); - -pub struct LabelIterator<'a>(&'a [u8]); - -impl NameLike for SName { - fn inner_bytes(&self) -> &[u8] { - &self.0 - } -} - -impl NameLike for SNameRef<'_> { - fn inner_bytes(&self) -> &[u8] { - self.0 - } -} - -pub trait NameLike { - fn inner_bytes(&self) -> &[u8]; - - fn to_bytes(&self) -> &[u8] { - let mut len = 0; - for label in self.iter() { - len += label.len() + 1; - } - len += 1; // null byte - &self.inner_bytes()[..len] - } - - #[inline(always)] - fn is_single_label(&self) -> bool { - self.label_count() == 1 - } - - fn label_count(&self) -> usize { - let mut count = 0; - let mut slice = &self.inner_bytes()[..]; - while !slice.is_empty() && slice[0] != 0 { - slice = &slice[slice[0] as usize + 1..]; - count += 1; - } - count - } - - #[inline(always)] - fn iter(&self) -> LabelIterator { - LabelIterator(&self.inner_bytes()[..]) - } -} - -impl FromStr for SName { - type Err = crate::errors::Error; - - fn from_str(s: &str) -> result::Result { - s.try_into() - } -} - -impl TryFrom<&[u8; N]> for SName { - type Error = crate::errors::Error; - - fn try_from(value: &[u8; N]) -> result::Result { - value.as_slice().try_into() - } -} - -impl TryFrom<&Vec> for SName { - type Error = crate::errors::Error; - - fn try_from(value: &Vec) -> result::Result { - value.as_slice().try_into() - } -} - -impl core::convert::TryFrom<&[u8]> for SName { - type Error = crate::errors::Error; - - fn try_from(value: &[u8]) -> result::Result { - let name_ref: SNameRef = value.try_into()?; - Ok(name_ref.to_owned()) - } -} - -impl<'a> core::convert::TryFrom<&'a [u8]> for SNameRef<'a> { - type Error = crate::errors::Error; - - fn try_from(value: &'a [u8]) -> core::result::Result { - let mut remaining = value; - if remaining.len() == 0 || remaining.len() > MAX_SPACE_LEN { - return Err(Error::Name(NameErrorKind::MalformedName)); - } - - let mut parsed_len = 0; - loop { - if remaining.is_empty() { - return Err(Error::Name(NameErrorKind::MalformedName)); - } - let label_len = remaining[0] as usize; - if label_len == 0 { - parsed_len += 1; - break; - } - if label_len > MAX_LABEL_LEN || label_len + 1 > remaining.len() { - return Err(Error::Name(NameErrorKind::MalformedName)); - } - remaining = &remaining[label_len + 1..]; - parsed_len += label_len + 1; - } - - Ok(SNameRef(&value[..parsed_len])) - } -} - -impl TryFrom for SName { - type Error = crate::errors::Error; - - fn try_from(value: String) -> result::Result { - value.as_str().try_into() - } -} - -impl TryFrom<&str> for SName { - type Error = crate::errors::Error; - - fn try_from(value: &str) -> result::Result { - let (subspace, space) = value - .split_once('@') - .ok_or(Error::Name(NameErrorKind::MalformedName))?; - - if space.is_empty() || space.contains('.') { - return Err(Error::Name(NameErrorKind::MalformedName)); - } - - let mut space_bytes = [0; MAX_SPACE_LEN]; - let mut space_len = 0; - - for label in subspace.split('.').chain(core::iter::once(space)) { - if space_len == 0 && label.is_empty() { - continue; // Skip initial subspace label if empty - } - - let label_bytes = label.as_bytes(); - let label_len = label_bytes.len(); - - if label_len == 0 - || label_len > MAX_LABEL_LEN - || space_len + label_len + 2 > MAX_SPACE_LEN - { - return Err(Error::Name(NameErrorKind::MalformedName)); - } - - if label - .bytes() - .any(|b| !b.is_ascii_alphanumeric() || b.is_ascii_uppercase()) - { - return Err(Error::Name(NameErrorKind::MalformedName)); - } - - // Insert the length of the label before the label itself - space_bytes[space_len] = label_len as u8; - space_len += 1; - - // Copy the label into the space_bytes array - space_bytes[space_len..space_len + label_len].copy_from_slice(label_bytes); - space_len += label_len; - } - - // Mark end with null byte - space_bytes[space_len] = 0; - - Ok(SName(space_bytes)) - } -} - -impl core::fmt::Display for SName { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - let labels: Vec<&str> = self - .iter() - .map(|label| core::str::from_utf8(label).unwrap()) - .collect(); - - let last_label = labels.last().unwrap(); - let all_but_last = &labels[..labels.len() - 1]; - write!(f, "{}@{}", all_but_last.join("."), last_label) - } -} - -impl Display for SNameRef<'_> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", self) - } -} - -impl<'a> Iterator for LabelIterator<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - if self.0.is_empty() || self.0[0] == 0 { - return None; - } - - let label_len = self.0[0] as usize; - let (label, rest) = self.0.split_at(label_len + 1); - self.0 = rest; - Some(&label[1..]) - } -} - -impl SName { - pub fn as_name_ref(&self) -> SNameRef { - SNameRef(&self.0) - } -} - -impl SNameRef<'_> { - pub fn to_owned(&self) -> SName { - let mut owned = SName([0; MAX_SPACE_LEN]); - owned.0[..self.0.len()].copy_from_slice(self.0); - owned - } -} - -#[cfg(test)] -mod tests { - use alloc::vec; - - use super::*; - - #[test] - fn test_from_slice() { - assert!(SName::try_from(b"").is_err(), "Should fail on empty slice"); - - assert!( - SName::try_from(b"\x00").is_ok(), - "Should succeed on root domain (empty space)" - ); - assert_eq!( - SName::try_from(b"\x00").unwrap().label_count(), - 0, - "Root domain should have 0 labels" - ); - - assert!( - SName::try_from(b"\x03bob").is_err(), - "Should fail on missing null byte" - ); - - assert!( - SName::try_from(b"\x03bob\x00").is_ok(), - "Should succeed on single label" - ); - assert_eq!( - SName::try_from(b"\x03bob\x00").unwrap().label_count(), - 1, - "Should count single label" - ); - - assert!( - SName::try_from(b"\x03bob\x07bitcoin\x00").is_ok(), - "Should succeed on two labels" - ); - assert_eq!( - SName::try_from(b"\x03bob\x07bitcoin\x00") - .unwrap() - .label_count(), - 2, - "Should count two labels" - ); - - let mut max_label = vec![0x3f]; // Length byte for 63 characters - max_label.extend_from_slice(&vec![b'a'; 63]); // 63 'a's - max_label.push(0x00); // Null byte - assert!( - SName::try_from(&max_label).is_ok(), - "Should succeed on max length label" - ); - - assert!( - SName::try_from(b"\x03bob\x00\x03foo").is_ok(), - "Should stop parsing at null byte" - ); - assert_eq!( - SName::try_from(b"\x03bob\x00\x03foo") - .unwrap() - .label_count(), - 1, - "Should parse up to first null byte" - ); - - let mut long_label = vec![0x40]; // Length byte for 64 characters - long_label.extend_from_slice(&vec![b'b'; 64]); // 64 'b's - long_label.push(0x00); // Null byte - assert!( - SName::try_from(&long_label).is_err(), - "Should fail on label too long" - ); - - assert!( - SName::try_from(b"\x03bob\x04foo\x00").is_err(), - "Should fail on incorrect label length byte" - ); - } - - #[test] - fn test_iter() { - let space = SName::try_from(b"\x03bob\x07bitcoin\x00").unwrap(); - let mut iter = space.iter(); - assert_eq!(iter.next(), Some(b"bob" as &[u8])); - assert_eq!(iter.next(), Some(b"bitcoin" as &[u8])); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_from_string() { - assert!(SName::from_str("").is_err(), "Should fail on empty string"); - assert!( - SName::from_str("bitcoin").is_err(), - "Should fail on missing @" - ); - assert!( - SName::from_str("@").is_err(), - "Should fail on missing subspace" - ); - assert!( - SName::from_str("hey..bob@bitcoin").is_err(), - "Should fail on empty label" - ); - - assert!( - SName::from_str("@bitcoin").is_ok(), - "Should succeed on single label" - ); - assert!( - SName::from_str("bob@bitcoin").is_ok(), - "Should succeed on two label" - ); - assert!( - SName::from_str("hello.bob@bitcoin").is_ok(), - "Should succeed on multi labels" - ); - - let example = SName::from_str("hello.bob@bitcoin").unwrap(); - assert_eq!(example.label_count(), 3, "Should count three labels"); - let mut iter = example.iter(); - assert_eq!(iter.next(), Some(b"hello" as &[u8])); - assert_eq!(iter.next(), Some(b"bob" as &[u8])); - assert_eq!(iter.next(), Some(b"bitcoin" as &[u8])); - assert_eq!(iter.next(), None); - assert_eq!( - example.to_bytes(), - b"\x05hello\x03bob\x07bitcoin\x00" as &[u8] - ); - } -} diff --git a/protocol/src/validate.rs b/protocol/src/validate.rs index 9d57298..c8bc60b 100644 --- a/protocol/src/validate.rs +++ b/protocol/src/validate.rs @@ -1,4 +1,4 @@ -use alloc::{collections::btree_map::BTreeMap, vec, vec::Vec}; +use alloc::{vec, vec::Vec}; #[cfg(feature = "bincode")] use bincode::{Decode, Encode}; @@ -8,10 +8,10 @@ use serde::{Deserialize, Serialize}; use crate::{ constants::{AUCTION_DURATION, AUCTION_EXTENSION_ON_BID, RENEWAL_INTERVAL, ROLLOUT_BATCH_SIZE}, - prepare::{is_magic_lock_time, AuctionedOutput, TrackableOutput, TxContext, SSTXO}, - script::{OpOpenContext, ScriptError, SpaceKind}, - sname::SName, - BidPsbtReason, Covenant, FullSpaceOut, RejectReason, RevokeReason, Space, SpaceOut, + prepare::{AuctionedOutput, TrackableOutput, TxContext, SSTXO}, + script::{OpenHistory, ScriptError, SpaceScript}, + slabel::SLabel, + BidPsbtReason, Bytes, Covenant, FullSpaceOut, RejectReason, RevokeReason, Space, SpaceOut, }; #[derive(Debug, Clone)] @@ -80,8 +80,7 @@ pub struct RevokeParams { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "bincode", derive(Encode, Decode))] pub struct RejectParams { - #[cfg_attr(feature = "bincode", bincode(with_serde))] - pub name: SName, + pub name: SLabel, #[cfg_attr(feature = "serde", serde(flatten))] pub reason: RejectReason, @@ -114,13 +113,12 @@ impl Validator { updates: vec![], }; - let mut default_space_data = Vec::new(); - let mut space_data = BTreeMap::new(); + let mut space_data = Bytes::new(Vec::new()); let mut reserve = false; - for fullspacein in ctx.inputs.into_iter() { + for input_ctx in ctx.inputs.into_iter() { changeset.spends.push(SpaceIn { - n: fullspacein.n, + n: input_ctx.n, script_error: None, }); @@ -129,34 +127,34 @@ impl Validator { height, tx, &mut ctx.auctioned_output, - fullspacein.n, - fullspacein.sstxo, + input_ctx.n, + input_ctx.sstxo, &mut changeset, ); // Process any space scripts - if let Some(script) = fullspacein.script { - if script.is_err() { - let last = changeset.spends.last_mut().unwrap(); - last.script_error = Some(script.unwrap_err()); - } else { - let mut script = script.unwrap(); - if !script.reserve { - if let Some(open) = script.open { + if let Some(script) = input_ctx.script { + match script { + Ok(op) => match op { + SpaceScript::Open(open) => { self.process_open( + input_ctx.n, height, open, &mut ctx.auctioned_output, &mut changeset, ); } - if let Some(data) = script.default_sdata { - default_space_data = data; + SpaceScript::Set(data) => { + space_data = Bytes::new(data); + } + SpaceScript::Reserve => { + reserve = true; } - space_data.append(&mut script.sdata); - } else { - // Script uses reserved op codes - reserve = true; + }, + Err(script_error) => { + let last = changeset.spends.last_mut().unwrap(); + last.script_error = Some(script_error); } } } @@ -164,7 +162,7 @@ impl Validator { // If one of the input scripts is using reserved op codes // then all space outputs with the transfer covenant must be marked as reserved - // This does not have an effect on meta outputs + // This does not have an effect on meta outputs/updates if reserve { for out in changeset.creates.iter_mut() { if let Some(space) = out.space.as_mut() { @@ -175,13 +173,13 @@ impl Validator { } } - // Set default space data if any - if !default_space_data.is_empty() { + // Set space data if any + if !space_data.is_empty() { changeset.creates.iter_mut().for_each(|output| { if let Some(space) = output.space.as_mut() { match &mut space.covenant { Covenant::Transfer { data, .. } => { - *data = Some(default_space_data.clone()); + *data = Some(space_data.clone()); } _ => {} } @@ -189,40 +187,17 @@ impl Validator { }); } - // Set space specific data - if !space_data.is_empty() { - for (key, value) in space_data.into_iter() { - if let Some(spaceout) = changeset.creates.get_mut(key as usize) { - if let Some(space) = spaceout.space.as_mut() { - match &mut space.covenant { - Covenant::Transfer { data, .. } => { - *data = Some(value); - } - _ => {} - } - } - } - } - } - // Check if any outputs should be tracked - if is_magic_lock_time(&tx.lock_time) { - for (n, output) in tx.output.iter().enumerate() { - match changeset.creates.iter().find(|x| x.n == n) { - None => { - if output.is_magic_output() { - changeset.creates.push(SpaceOut { - n, - value: output.value, - script_pubkey: output.script_pubkey.clone(), - space: None, - }) - } - } - Some(_) => { - // already tracked - } - } + // we don't check for any special lock times here + // as this is a spaces transaction + for (n, output) in tx.output.iter().enumerate() { + if changeset.creates.iter().all(|x| x.n != n) && output.is_magic_output() { + changeset.creates.push(SpaceOut { + n, + value: output.value, + script_pubkey: output.script_pubkey.clone(), + space: None, + }); } } @@ -300,19 +275,20 @@ impl Validator { fn process_open( &self, + input_index: usize, height: u32, - open: OpOpenContext, + open: OpenHistory, auctiond: &mut Option, changeset: &mut TxChangeSet, ) { let spend_index = changeset .spends .iter() - .position(|s| s.n == open.input_index) + .position(|s| s.n == input_index) .expect("open must have an input index revealing the space in witness"); - let name = match open.spaceout { - SpaceKind::ExistingSpace(mut prev) => { + let name = match open { + OpenHistory::ExistingSpace(mut prev) => { let prev_space = prev.spaceout.space.as_mut().unwrap(); if !prev_space.is_expired(height) { let reject = ScriptError::Reject(RejectParams { @@ -333,7 +309,7 @@ impl Validator { }); prev.spaceout.space.unwrap().name } - SpaceKind::NewSpace(name) => name, + OpenHistory::NewSpace(name) => name, }; let mut auctiond = match auctiond.take() { @@ -402,13 +378,23 @@ impl Validator { if spaceout.space.is_none() { return; } - changeset.updates.push(UpdateOut { + + let revoke = UpdateOut { output: FullSpaceOut { txid: auctioned.bid_psbt.outpoint.txid, spaceout: spaceout.clone(), }, kind: UpdateKind::Revoke(RevokeReason::BadSpend), - }); + }; + + // check if it's already revoked for some other reason + if changeset.updates.iter().any(|update| match update.kind { + UpdateKind::Revoke(_) => update.output.outpoint() == revoke.output.outpoint(), + _ => false, + }) { + return; + } + changeset.updates.push(revoke); } } @@ -596,7 +582,7 @@ impl Validator { tx: &Transaction, input_index: usize, mut spaceout: SpaceOut, - existing_data: Option>, + existing_data: Option, changeset: &mut TxChangeSet, ) { let input = tx.input.get(input_index).expect("input"); diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index b655ad3..56899e1 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] bitcoin = { version = "0.32.2", features = ["base64", "serde"] } -bdk_wallet = { version = "1.0.0-alpha.13", features = ["keys-bip39"] } +bdk_wallet = { version = "=1.0.0-alpha.13", features = ["keys-bip39"] } bdk_file_store = "0.13.0" secp256k1 = "0.29.0" anyhow = "1.0.80" diff --git a/wallet/src/builder.rs b/wallet/src/builder.rs index 483439e..20ef5b8 100644 --- a/wallet/src/builder.rs +++ b/wallet/src/builder.rs @@ -17,15 +17,13 @@ use bdk_wallet::{ KeychainKind, TxBuilder, WeightedUtxo, }; use bitcoin::{ - absolute::LockTime, psbt, psbt::Input, script::PushBytesBuf, Address, Amount, FeeRate, Network, - OutPoint, Psbt, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid, Witness, + absolute::LockTime, psbt, psbt::Input, script, script::PushBytesBuf, Address, Amount, FeeRate, + Network, OutPoint, Psbt, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid, Witness, }; use protocol::{ bitcoin::absolute::Height, constants::{BID_PSBT_INPUT_SEQUENCE, BID_PSBT_TX_VERSION}, - opcodes::OP_OPEN, - script::ScriptBuilder, - sname::NameLike, + script::SpaceScript, Covenant, FullSpaceOut, Space, }; use serde::{Deserialize, Serialize}; @@ -131,7 +129,7 @@ pub struct CoinTransfer { #[derive(Debug, Clone)] pub struct ExecuteRequest { pub context: Vec, - pub script: ScriptBuilder, + pub script: script::Builder, } pub struct CreateParams { @@ -234,7 +232,7 @@ impl<'a, Cs: CoinSelectionAlgorithm> TxBuilderSpacesUtils<'a, Cs> for TxBuilder< placeholder.auction.outpoint.vout as u8, &offer, )?) - .expect("compressed psbt script bytes"); + .expect("compressed psbt script bytes"); let carrier = ScriptBuf::new_op_return(&compressed_psbt); @@ -281,6 +279,15 @@ impl<'a, Cs: CoinSelectionAlgorithm> TxBuilderSpacesUtils<'a, Cs> for TxBuilder< fn add_transfer(&mut self, request: TransferRequest) -> anyhow::Result<&mut Self> { match request { TransferRequest::Space(request) => { + let output_value = space_dust( + request + .space + .spaceout + .script_pubkey + .minimal_non_dust() + .mul(2), + ); + let mut spend_input = psbt::Input { witness_utxo: Some(TxOut { value: request.space.spaceout.value, @@ -302,9 +309,13 @@ impl<'a, Cs: CoinSelectionAlgorithm> TxBuilderSpacesUtils<'a, Cs> for TxBuilder< spend_input, 66, )?; + self.add_recipient( request.recipient.script_pubkey(), - request.space.spaceout.value, + // TODO: another reason we need to keep more metadata + // we use a special dust value here so that list auction + // outputs won't accidentally auction off this output + output_value, ); } TransferRequest::Coin(request) => { @@ -349,16 +360,19 @@ impl Builder { None => addr1.minimal_non_dust().mul(2), Some(dust) => dust, }; + let connector_dust = connector_dust(dust); let magic_dust = magic_dust(dust); - placeholder_outputs.push((addr1, dust)); - // auctioned output + + placeholder_outputs.push((addr1, connector_dust)); let addr2 = w.spaces.next_unused_address(KeychainKind::External); placeholder_outputs.push((addr2.script_pubkey(), magic_dust)); } } let commit_psbt = { - let mut builder = w.spaces.build_tx().coin_selection(coin_selection); + let mut builder = w.spaces + .build_tx() + .coin_selection(coin_selection); builder.nlocktime(magic_lock_time(median_time)); builder.ordering(TxOrdering::Untouched); @@ -439,6 +453,7 @@ pub enum TransactionTag { Open, Bid, Script, + ForceSpendTestOnly, } impl Iterator for BuilderIterator<'_> { @@ -478,10 +493,8 @@ impl Iterator for BuilderIterator<'_> { let mut contexts = Vec::with_capacity(params.executes.len()); for execute in params.executes { - let signing_info = SpaceScriptSigningInfo::new( - self.wallet.config.network, - execute.script.to_nop_script(), - ); + let signing_info = + SpaceScriptSigningInfo::new(self.wallet.config.network, execute.script); if signing_info.is_err() { return Some(Err(signing_info.unwrap_err())); } @@ -654,7 +667,11 @@ impl Builder { self } - pub fn add_execute(mut self, spaces: Vec, space_script: ScriptBuilder) -> Self { + pub fn add_execute( + mut self, + spaces: Vec, + space_script: script::Builder, + ) -> Self { self.requests.push(StackRequest::Execute(ExecuteRequest { context: spaces, script: space_script, @@ -784,7 +801,9 @@ impl Builder { ) -> anyhow::Result { let (offer, placeholder) = w.new_bid_psbt(bid)?; let bid_psbt = { - let mut builder = w.spaces.build_tx().coin_selection(coin_selection); + let mut builder = w.spaces + .build_tx() + .coin_selection(coin_selection); builder .ordering(TxOrdering::Untouched) .nlocktime(LockTime::Blocks(Height::ZERO)) @@ -815,7 +834,9 @@ impl Builder { let (offer, placeholder) = w.new_bid_psbt(params.amount)?; let mut extra_prevouts = BTreeMap::new(); let open_psbt = { - let mut builder = w.spaces.build_tx().coin_selection(coin_selection); + let mut builder = w.spaces + .build_tx() + .coin_selection(coin_selection); builder.ordering(TxOrdering::Untouched).add_bid( None, offer, @@ -850,7 +871,9 @@ impl Builder { .spaces .next_unused_address(KeychainKind::Internal) .script_pubkey(); - let mut builder = w.spaces.build_tx().coin_selection(coin_selection); + let mut builder = w.spaces + .build_tx() + .coin_selection(coin_selection); builder .ordering(TxOrdering::Untouched) @@ -882,15 +905,9 @@ impl Builder { network: Network, name: &str, ) -> anyhow::Result { - let space_builder = ScriptBuilder::new(); - let sname = protocol::sname::SName::from_str(name).expect("valid space name"); - - let builder = space_builder - .push_slice(sname.to_bytes()) - .push_opcode(OP_OPEN.into()) - .to_nop_script(); - - SpaceScriptSigningInfo::new(network, builder) + let sname = protocol::slabel::SLabel::from_str(name).expect("valid space name"); + let nop = SpaceScript::nop_script(SpaceScript::create_open(sname)); + SpaceScriptSigningInfo::new(network, nop) } } @@ -936,8 +953,8 @@ impl CoinSelectionAlgorithm for SpacesAwareCoinSelection { optional_utxos.retain(|weighted_utxo| { weighted_utxo.utxo.txout().value > SpacesAwareCoinSelection::DUST_THRESHOLD && !self - .exclude_outputs - .contains(&weighted_utxo.utxo.outpoint()) + .exclude_outputs + .contains(&weighted_utxo.utxo.outpoint()) }); let mut result = self.default_algorithm.coin_select( @@ -977,3 +994,23 @@ pub fn magic_dust(amount: Amount) -> Amount { let amount = amount.to_sat(); Amount::from_sat(amount - (amount % 10) + 2) } + +pub fn connector_dust(amount: Amount) -> Amount { + let amount = amount.to_sat(); + Amount::from_sat(amount - (amount % 10) + 4) +} + +pub fn is_connector_dust(amount: Amount) -> bool { + amount.to_sat() % 10 == 4 +} + +// Special dust value to indicate this output is a space +// could be removed once we track output metadata in some db +pub fn space_dust(amount: Amount) -> Amount { + let amount = amount.to_sat(); + Amount::from_sat(amount - (amount % 10) + 6) +} + +pub fn is_space_dust(amount: Amount) -> bool { + amount.to_sat() % 10 == 6 +} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 36f3880..e949853 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -27,16 +27,22 @@ use bitcoin::{ Amount, Block, BlockHash, FeeRate, Network, OutPoint, Psbt, Sequence, TapLeafHash, TapSighashType, Transaction, TxOut, Witness, }; -use protocol::bitcoin::{ - constants::genesis_block, - key::{rand, UntweakedKeypair}, - opcodes, - taproot::{ControlBlock, TaprootBuilder}, - Address, ScriptBuf, XOnlyPublicKey, +use protocol::{ + bitcoin::{ + constants::genesis_block, + key::{rand, UntweakedKeypair}, + opcodes, + taproot::{ControlBlock, TaprootBuilder}, + Address, ScriptBuf, XOnlyPublicKey, + }, + prepare::TrackableOutput, }; use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer}; -use crate::address::SpaceAddress; +use crate::{ + address::SpaceAddress, + builder::{is_connector_dust, is_space_dust, SpacesAwareCoinSelection}, +}; pub extern crate bdk_wallet; pub extern crate bitcoin; @@ -243,6 +249,20 @@ impl SpacesWallet { && utxo1.keychain == KeychainKind::Internal && utxo2.outpoint.vout == utxo1.outpoint.vout + 1 && utxo2.keychain == KeychainKind::External + + // Adding these as additional safety checks since: + // 1. outputs less than dust threshold + // are protected from being spent to fund txs. + // 2. outputs representing spaces use "space dust" values. + // + // All these checks are needed because we don't actaully know + // if an unconfirmed output is a spaceout representing a space ... + // TODO: store metadata to simplify things and make it safer to use + && utxo1.txout.value < SpacesAwareCoinSelection::DUST_THRESHOLD + && utxo2.txout.value < SpacesAwareCoinSelection::DUST_THRESHOLD + && is_connector_dust(utxo1.txout.value) + && !is_space_dust(utxo2.txout.value) + && utxo2.txout.is_magic_output() { not_auctioned.push(DoubleUtxo { spend: FullTxOut {