diff --git a/Cargo.lock b/Cargo.lock index 9e13175..a0e9fef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3881,7 +3881,7 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plain_bitnames" -version = "0.9.1" +version = "0.9.2" dependencies = [ "addr", "anyhow", @@ -3935,7 +3935,7 @@ dependencies = [ [[package]] name = "plain_bitnames_app" -version = "0.9.1" +version = "0.9.2" dependencies = [ "anyhow", "base64", @@ -3980,7 +3980,7 @@ dependencies = [ [[package]] name = "plain_bitnames_app_cli" -version = "0.9.1" +version = "0.9.2" dependencies = [ "anyhow", "bitcoin", @@ -3995,7 +3995,7 @@ dependencies = [ [[package]] name = "plain_bitnames_app_rpc_api" -version = "0.9.1" +version = "0.9.2" dependencies = [ "anyhow", "bitcoin", diff --git a/Cargo.toml b/Cargo.toml index 19a098f..9ec50a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ authors = [ "Ash Manning " ] edition = "2021" license-file = "LICENSE.txt" publish = false -version = "0.9.1" +version = "0.9.2" [profile.release] # lto = "fat" \ No newline at end of file diff --git a/app/app.rs b/app/app.rs index 1715bca..0956c5e 100644 --- a/app/app.rs +++ b/app/app.rs @@ -411,7 +411,7 @@ impl App { if !ownership.contains(&height) { return None; }; - bitname_data.paymail_fee_sats + bitname_data.mutable_data.paymail_fee_sats }) .min(); let Some(min_fee) = min_fee else { diff --git a/app/gui/bitnames/all_bitnames.rs b/app/gui/bitnames/all_bitnames.rs index 15fbe92..23811db 100644 --- a/app/gui/bitnames/all_bitnames.rs +++ b/app/gui/bitnames/all_bitnames.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use eframe::egui; use hex::FromHex; -use plain_bitnames::types::{hashes::BitName, BitNameData}; +use plain_bitnames::types::{hashes::BitName, BitNameData, MutableBitNameData}; use crate::{ app::App, @@ -19,13 +19,17 @@ fn show_bitname_data( bitname_data: &BitNameData, ) -> egui::Response { let BitNameData { + seq_id, + mutable_data, + } = bitname_data; + let MutableBitNameData { commitment, ipv4_addr, ipv6_addr, encryption_pubkey, signing_pubkey, paymail_fee_sats, - } = bitname_data; + } = mutable_data; let commitment = commitment.map_or("Not set".to_owned(), hex::encode); let ipv4_addr = ipv4_addr .map_or("Not set".to_owned(), |ipv4_addr| ipv4_addr.to_string()); @@ -38,12 +42,16 @@ fn show_bitname_data( let paymail_fee_sats = paymail_fee_sats .map_or("Not set".to_owned(), |paymail_fee| paymail_fee.to_string()); ui.horizontal(|ui| { - ui.monospace_selectable_singleline( - true, - format!("Commitment: {commitment}"), - ) + ui.monospace_selectable_singleline(false, format!("Seq ID: {seq_id}")) }) .join() + | ui.horizontal(|ui| { + ui.monospace_selectable_singleline( + true, + format!("Commitment: {commitment}"), + ) + }) + .join() | ui.horizontal(|ui| { ui.monospace_selectable_singleline( false, diff --git a/app/gui/bitnames/reserve_register.rs b/app/gui/bitnames/reserve_register.rs index 583235e..3a9dafc 100644 --- a/app/gui/bitnames/reserve_register.rs +++ b/app/gui/bitnames/reserve_register.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use eframe::egui; -use plain_bitnames::types::BitNameData; +use plain_bitnames::types::MutableBitNameData; use crate::{ app::App, @@ -21,7 +21,7 @@ fn reserve_bitname( fn register_bitname( app: &App, plaintext_name: &str, - bitname_data: Cow, + bitname_data: Cow, fee: bitcoin::Amount, ) -> anyhow::Result<()> { let mut tx = app.wallet.create_regular_transaction(fee)?; @@ -117,7 +117,7 @@ impl Register { bitcoin::Denomination::Bitcoin, ); tx_creator::TxCreator::show_bitname_options(ui, &mut self.bitname_data); - let bitname_data: Result = + let bitname_data: Result = self.bitname_data.clone().try_into(); if let Err(err) = &bitname_data { ui.monospace_selectable_multiline(err.clone()); diff --git a/app/gui/coins/tx_creator.rs b/app/gui/coins/tx_creator.rs index 6ebfa91..9a597a7 100644 --- a/app/gui/coins/tx_creator.rs +++ b/app/gui/coins/tx_creator.rs @@ -9,7 +9,7 @@ use hex::FromHex; use plain_bitnames::{ authorization::VerifyingKey, - types::{BitNameData, EncryptionPubKey, Hash, Transaction, Txid}, + types::{EncryptionPubKey, Hash, MutableBitNameData, Transaction, Txid}, }; use crate::{app::App, gui::util::InnerResponseExt}; @@ -65,7 +65,7 @@ impl std::default::Default for TrySetOption { } } -impl TryFrom for BitNameData { +impl TryFrom for MutableBitNameData { type Error = String; fn try_from(try_set: TrySetBitNameData) -> Result { @@ -92,7 +92,7 @@ impl TryFrom for BitNameData { .paymail_fee_sats .0 .map_err(|err| format!("Cannot parse paymail fee: \"{err}\""))?; - Ok(BitNameData { + Ok(MutableBitNameData { commitment, ipv4_addr, ipv6_addr, @@ -126,7 +126,7 @@ impl TxCreator { plaintext_name, bitname_data, } => { - let bitname_data: BitNameData = (bitname_data.as_ref()) + let bitname_data: MutableBitNameData = (bitname_data.as_ref()) .clone() .try_into() .map_err(|err| anyhow::anyhow!("{err}"))?; diff --git a/app/gui/encrypt_message.rs b/app/gui/encrypt_message.rs index 0a585a0..1f36d80 100644 --- a/app/gui/encrypt_message.rs +++ b/app/gui/encrypt_message.rs @@ -54,7 +54,7 @@ impl EncryptMessage { .get_current_bitname_data(&bitname) .map_err(anyhow::Error::from) .and_then(|bitname_data| { - bitname_data.encryption_pubkey.ok_or( + bitname_data.mutable_data.encryption_pubkey.ok_or( anyhow::anyhow!( "No encryption pubkey exists for this BitName" ), diff --git a/lib/node/net_task.rs b/lib/node/net_task.rs index 1c3ea75..446540f 100644 --- a/lib/node/net_task.rs +++ b/lib/node/net_task.rs @@ -20,7 +20,6 @@ use thiserror::Error; use tokio::task::JoinHandle; use tokio_stream::StreamNotifyClose; use tokio_util::task::LocalPoolHandle; -use zeromq::Socket; use super::mainchain_task::{self, MainchainTaskHandle}; use crate::{ @@ -81,6 +80,7 @@ impl ZmqPubHandler { socket_addr: SocketAddr, ) -> Result { use futures::TryFutureExt as _; + use zeromq::Socket as _; let (tx, rx) = mpsc::unbounded::(); let zmq_pub_addr = format!("tcp://{socket_addr}"); let mut zmq_pub = zeromq::PubSocket::new(); diff --git a/lib/state/bitname_data.rs b/lib/state/bitname_data.rs new file mode 100644 index 0000000..74ce3c6 --- /dev/null +++ b/lib/state/bitname_data.rs @@ -0,0 +1,251 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use ed25519_dalek::VerifyingKey; +use serde::{Deserialize, Serialize}; + +use crate::{ + state::rollback::{RollBack, TxidStamped}, + types::{ + BitNameDataUpdates, BitNameSeqId, EncryptionPubKey, Hash, Txid, Update, + }, +}; + +/// Representation of BitName data that supports rollbacks. +/// The most recent datum is the element at the back of the vector. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BitNameData { + pub seq_id: BitNameSeqId, + /// commitment to arbitrary data + pub(in crate::state) commitment: RollBack>>, + /// set if the plain bitname is known to be an ICANN domain + pub(in crate::state) is_icann: bool, + /// optional ipv4 addr + pub(in crate::state) ipv4_addr: RollBack>>, + /// optional ipv6 addr + pub(in crate::state) ipv6_addr: RollBack>>, + /// optional pubkey used for encryption + pub(in crate::state) encryption_pubkey: + RollBack>>, + /// optional pubkey used for signing messages + pub(in crate::state) signing_pubkey: + RollBack>>, + /// optional minimum paymail fee, in sats + pub(in crate::state) paymail_fee_sats: RollBack>>, +} + +impl BitNameData { + // initialize from BitName data provided during a registration + pub(in crate::state) fn init( + bitname_data: crate::types::MutableBitNameData, + txid: Txid, + height: u32, + seq_id: BitNameSeqId, + ) -> Self { + Self { + seq_id, + commitment: RollBack::>::new( + bitname_data.commitment, + txid, + height, + ), + is_icann: false, + ipv4_addr: RollBack::>::new( + bitname_data.ipv4_addr, + txid, + height, + ), + ipv6_addr: RollBack::>::new( + bitname_data.ipv6_addr, + txid, + height, + ), + encryption_pubkey: RollBack::>::new( + bitname_data.encryption_pubkey, + txid, + height, + ), + signing_pubkey: RollBack::>::new( + bitname_data.signing_pubkey, + txid, + height, + ), + paymail_fee_sats: RollBack::>::new( + bitname_data.paymail_fee_sats, + txid, + height, + ), + } + } + + // apply bitname data updates + pub(in crate::state) fn apply_updates( + &mut self, + updates: BitNameDataUpdates, + txid: Txid, + height: u32, + ) { + let Self { + seq_id: _, + ref mut commitment, + is_icann: _, + ref mut ipv4_addr, + ref mut ipv6_addr, + ref mut encryption_pubkey, + ref mut signing_pubkey, + ref mut paymail_fee_sats, + } = self; + + // apply an update to a single data field + fn apply_field_update( + data_field: &mut RollBack>>, + update: Update, + txid: Txid, + height: u32, + ) { + match update { + Update::Delete => data_field.push(None, txid, height), + Update::Retain => (), + Update::Set(value) => { + data_field.push(Some(value), txid, height) + } + } + } + apply_field_update(commitment, updates.commitment, txid, height); + apply_field_update(ipv4_addr, updates.ipv4_addr, txid, height); + apply_field_update(ipv6_addr, updates.ipv6_addr, txid, height); + apply_field_update( + encryption_pubkey, + updates.encryption_pubkey, + txid, + height, + ); + apply_field_update( + signing_pubkey, + updates.signing_pubkey, + txid, + height, + ); + apply_field_update( + paymail_fee_sats, + updates.paymail_fee_sats, + txid, + height, + ); + } + + // revert bitname data updates + pub(in crate::state) fn revert_updates( + &mut self, + updates: BitNameDataUpdates, + txid: Txid, + height: u32, + ) { + // apply an update to a single data field + fn revert_field_update( + data_field: &mut RollBack>>, + update: Update, + txid: Txid, + height: u32, + ) where + T: std::fmt::Debug + Eq, + { + match update { + Update::Delete => { + let popped = data_field.pop(); + assert!(popped.is_some()); + let popped = popped.unwrap(); + assert!(popped.data.is_none()); + assert_eq!(popped.txid, txid); + assert_eq!(popped.height, height) + } + Update::Retain => (), + Update::Set(value) => { + let popped = data_field.pop(); + assert!(popped.is_some()); + let popped = popped.unwrap(); + assert!(popped.data.is_some()); + assert_eq!(popped.data.unwrap(), value); + assert_eq!(popped.txid, txid); + assert_eq!(popped.height, height) + } + } + } + + let Self { + seq_id: _, + ref mut commitment, + is_icann: _, + ref mut ipv4_addr, + ref mut ipv6_addr, + ref mut encryption_pubkey, + ref mut signing_pubkey, + ref mut paymail_fee_sats, + } = self; + revert_field_update( + paymail_fee_sats, + updates.paymail_fee_sats, + txid, + height, + ); + revert_field_update( + signing_pubkey, + updates.signing_pubkey, + txid, + height, + ); + revert_field_update( + encryption_pubkey, + updates.encryption_pubkey, + txid, + height, + ); + revert_field_update(ipv6_addr, updates.ipv6_addr, txid, height); + revert_field_update(ipv4_addr, updates.ipv4_addr, txid, height); + revert_field_update(commitment, updates.commitment, txid, height); + } + + /** Returns the Bitname data as it was, at the specified block height. + * If a value was updated several times in the block, returns the + * last value seen in the block. + * Returns `None` if the data did not exist at the specified block + * height. */ + pub fn at_block_height( + &self, + height: u32, + ) -> Option { + let mutable_data = crate::types::MutableBitNameData { + commitment: self.commitment.at_block_height(height)?.data, + ipv4_addr: self.ipv4_addr.at_block_height(height)?.data, + ipv6_addr: self.ipv6_addr.at_block_height(height)?.data, + encryption_pubkey: self + .encryption_pubkey + .at_block_height(height)? + .data, + signing_pubkey: self.signing_pubkey.at_block_height(height)?.data, + paymail_fee_sats: self + .paymail_fee_sats + .at_block_height(height)? + .data, + }; + Some(crate::types::BitNameData { + seq_id: self.seq_id, + mutable_data, + }) + } + + /// get the current bitname data + pub fn current(&self) -> crate::types::BitNameData { + let mutable_data = crate::types::MutableBitNameData { + commitment: self.commitment.latest().data, + ipv4_addr: self.ipv4_addr.latest().data, + ipv6_addr: self.ipv6_addr.latest().data, + encryption_pubkey: self.encryption_pubkey.latest().data, + signing_pubkey: self.signing_pubkey.latest().data, + paymail_fee_sats: self.paymail_fee_sats.latest().data, + }; + crate::types::BitNameData { + seq_id: self.seq_id, + mutable_data, + } + } +} diff --git a/lib/state/error.rs b/lib/state/error.rs new file mode 100644 index 0000000..87b6e63 --- /dev/null +++ b/lib/state/error.rs @@ -0,0 +1,104 @@ +use crate::types::{ + AmountOverflowError, AmountUnderflowError, BitName, BlockHash, M6id, + MerkleRoot, OutPoint, Txid, WithdrawalBundleError, +}; + +#[derive(Debug, thiserror::Error)] +pub enum InvalidHeader { + #[error("expected block hash {expected}, but computed {computed}")] + BlockHash { + expected: BlockHash, + computed: BlockHash, + }, + #[error("expected previous sidechain block hash {expected}, but received {received}")] + PrevSideHash { + expected: BlockHash, + received: BlockHash, + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + AmountOverflow(#[from] AmountOverflowError), + #[error(transparent)] + AmountUnderflow(#[from] AmountUnderflowError), + #[error("failed to verify authorization")] + AuthorizationError, + #[error("bad coinbase output content")] + BadCoinbaseOutputContent, + #[error("bitname {name_hash} already registered")] + BitNameAlreadyRegistered { name_hash: BitName }, + #[error("bitname {name_hash} already registered as an ICANN name")] + BitNameAlreadyIcann { name_hash: BitName }, + #[error("bundle too heavy {weight} > {max_weight}")] + BundleTooHeavy { weight: u64, max_weight: u64 }, + #[error(transparent)] + BorshSerialize(borsh::io::Error), + #[error("failed to fill tx output contents: invalid transaction")] + FillTxOutputContentsFailed, + #[error("heed error")] + Heed(#[from] heed::Error), + #[error("invalid ICANN name: {plain_name}")] + IcannNameInvalid { plain_name: String }, + #[error("invalid body: expected merkle root {expected}, but computed {computed}")] + InvalidBody { + expected: MerkleRoot, + computed: MerkleRoot, + }, + #[error("invalid header")] + InvalidHeader(#[from] InvalidHeader), + #[error("failed to compute merkle root")] + MerkleRoot, + #[error("missing BitName {name_hash}")] + MissingBitName { name_hash: BitName }, + #[error( + "Missing BitName data for {name_hash} at block height {block_height}" + )] + MissingBitNameData { + name_hash: BitName, + block_height: u32, + }, + #[error("missing BitName input {name_hash}")] + MissingBitNameInput { name_hash: BitName }, + #[error("missing BitName reservation {txid}")] + MissingReservation { txid: Txid }, + #[error("no BitNames to update")] + NoBitNamesToUpdate, + #[error("deposit block doesn't exist")] + NoDepositBlock, + #[error("total fees less than coinbase value")] + NotEnoughFees, + #[error("value in is less than value out")] + NotEnoughValueIn, + #[error("stxo {outpoint} doesn't exist")] + NoStxo { outpoint: OutPoint }, + #[error("no tip")] + NoTip, + #[error("utxo {outpoint} doesn't exist")] + NoUtxo { outpoint: OutPoint }, + #[error("Withdrawal bundle event block doesn't exist")] + NoWithdrawalBundleEventBlock, + #[error(transparent)] + SignatureError(#[from] ed25519_dalek::SignatureError), + #[error("Too few BitName outputs")] + TooFewBitNameOutputs, + #[error("unbalanced BitNames: {n_bitname_inputs} BitName inputs, {n_bitname_outputs} BitName outputs")] + UnbalancedBitNames { + n_bitname_inputs: usize, + n_bitname_outputs: usize, + }, + #[error("unbalanced reservations: {n_reservation_inputs} reservation inputs, {n_reservation_outputs} reservation outputs")] + UnbalancedReservations { + n_reservation_inputs: usize, + n_reservation_outputs: usize, + }, + #[error("Unknown withdrawal bundle: {m6id}")] + UnknownWithdrawalBundle { m6id: M6id }, + #[error("utxo double spent")] + UtxoDoubleSpent, + #[error(transparent)] + WithdrawalBundle(#[from] WithdrawalBundleError), + #[error("wrong public key for address")] + WrongPubKeyForAddress, +} diff --git a/lib/state.rs b/lib/state/mod.rs similarity index 79% rename from lib/state.rs rename to lib/state/mod.rs index 21d2ee8..50f90d9 100644 --- a/lib/state.rs +++ b/lib/state/mod.rs @@ -1,449 +1,32 @@ -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - net::{Ipv4Addr, Ipv6Addr}, -}; +use std::collections::{BTreeMap, HashMap, HashSet}; use futures::Stream; use heed::{types::SerdeBincode, Database, RoTxn, RwTxn}; -use nonempty::NonEmpty; -use serde::{Deserialize, Serialize}; use crate::{ - authorization::{Authorization, VerifyingKey}, + authorization::Authorization, types::{ self, constants, hashes::{self, BitName}, proto::mainchain::TwoWayPegData, - Address, AggregatedWithdrawal, AmountOverflowError, - AmountUnderflowError, Authorized, AuthorizedTransaction, - BatchIcannRegistrationData, BitNameDataUpdates, BlockHash, Body, - EncryptionPubKey, FilledOutput, FilledOutputContent, FilledTransaction, - GetAddress as _, GetValue as _, Hash, Header, InPoint, M6id, - MerkleRoot, OutPoint, OutputContent, SpentOutput, Transaction, TxData, - Txid, Update, Verify as _, WithdrawalBundle, WithdrawalBundleError, + Address, AggregatedWithdrawal, AmountOverflowError, Authorized, + AuthorizedTransaction, BatchIcannRegistrationData, BitNameDataUpdates, + BitNameSeqId, BlockHash, Body, FilledOutput, FilledOutputContent, + FilledTransaction, GetAddress as _, GetValue as _, Hash, Header, + InPoint, M6id, MerkleRoot, OutPoint, OutputContent, SpentOutput, + Transaction, TxData, Txid, Verify as _, WithdrawalBundle, WithdrawalBundleStatus, }, util::{EnvExt, UnitKey, Watchable, WatchableDb}, }; -/// Data of type `T` paired with block height at which it was last updated -#[derive(Clone, Debug, Deserialize, Serialize)] -struct HeightStamped { - value: T, - height: u32, -} - -/// Data of type `T` paired with -/// * the txid at which it was last updated -/// * block height at which it was last updated -#[derive(Clone, Debug, Deserialize, Serialize)] -struct TxidStamped { - data: T, - txid: Txid, - height: u32, -} - -/// Wrapper struct for fields that support rollbacks -#[derive(Clone, Debug, Deserialize, Serialize)] -#[repr(transparent)] -#[serde(transparent)] -struct RollBack(NonEmpty); - -impl RollBack> { - fn new(value: T, height: u32) -> Self { - let height_stamped = HeightStamped { value, height }; - Self(NonEmpty::new(height_stamped)) - } - - /// Pop the most recent value - fn pop(mut self) -> (Option, HeightStamped) { - if let Some(value) = self.0.pop() { - (Some(self), value) - } else { - (None, self.0.head) - } - } - - /// Attempt to push a value as the new most recent. - /// Returns the value if the operation fails. - fn push(&mut self, value: T, height: u32) -> Result<(), T> { - if self.0.last().height >= height { - return Err(value); - } - let height_stamped = HeightStamped { value, height }; - self.0.push(height_stamped); - Ok(()) - } - - /// Returns the earliest value - fn earliest(&self) -> &HeightStamped { - self.0.first() - } - - /// Returns the most recent value - fn latest(&self) -> &HeightStamped { - self.0.last() - } -} - -impl RollBack> { - fn new(value: T, txid: Txid, height: u32) -> Self { - let txid_stamped = TxidStamped { - data: value, - txid, - height, - }; - Self(NonEmpty::new(txid_stamped)) - } - - /// push a value as the new most recent - fn push(&mut self, value: T, txid: Txid, height: u32) { - let txid_stamped = TxidStamped { - data: value, - txid, - height, - }; - self.0.push(txid_stamped) - } - - /// pop the most recent value - fn pop(&mut self) -> Option> { - self.0.pop() - } - - /** Returns the value as it was, at the specified block height. - * If a value was updated several times in the block, returns the - * last value seen in the block. */ - fn at_block_height(&self, height: u32) -> Option<&TxidStamped> { - self.0 - .iter() - .rev() - .find(|txid_stamped| txid_stamped.height <= height) - } - - /// returns the most recent value, along with it's txid - fn latest(&self) -> &TxidStamped { - self.0.last() - } -} - -/// Representation of BitName data that supports rollbacks. -/// The most recent datum is the element at the back of the vector. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct BitNameData { - /// commitment to arbitrary data - commitment: RollBack>>, - /// set if the plain bitname is known to be an ICANN domain - is_icann: bool, - /// optional ipv4 addr - ipv4_addr: RollBack>>, - /// optional ipv6 addr - ipv6_addr: RollBack>>, - /// optional pubkey used for encryption - encryption_pubkey: RollBack>>, - /// optional pubkey used for signing messages - signing_pubkey: RollBack>>, - /// optional minimum paymail fee, in sats - paymail_fee_sats: RollBack>>, -} - -impl BitNameData { - // initialize from BitName data provided during a registration - fn init(bitname_data: types::BitNameData, txid: Txid, height: u32) -> Self { - Self { - commitment: RollBack::>::new( - bitname_data.commitment, - txid, - height, - ), - is_icann: false, - ipv4_addr: RollBack::>::new( - bitname_data.ipv4_addr, - txid, - height, - ), - ipv6_addr: RollBack::>::new( - bitname_data.ipv6_addr, - txid, - height, - ), - encryption_pubkey: RollBack::>::new( - bitname_data.encryption_pubkey, - txid, - height, - ), - signing_pubkey: RollBack::>::new( - bitname_data.signing_pubkey, - txid, - height, - ), - paymail_fee_sats: RollBack::>::new( - bitname_data.paymail_fee_sats, - txid, - height, - ), - } - } - - // apply bitname data updates - fn apply_updates( - &mut self, - updates: BitNameDataUpdates, - txid: Txid, - height: u32, - ) { - let Self { - ref mut commitment, - is_icann: _, - ref mut ipv4_addr, - ref mut ipv6_addr, - ref mut encryption_pubkey, - ref mut signing_pubkey, - ref mut paymail_fee_sats, - } = self; - - // apply an update to a single data field - fn apply_field_update( - data_field: &mut RollBack>>, - update: Update, - txid: Txid, - height: u32, - ) { - match update { - Update::Delete => data_field.push(None, txid, height), - Update::Retain => (), - Update::Set(value) => { - data_field.push(Some(value), txid, height) - } - } - } - apply_field_update(commitment, updates.commitment, txid, height); - apply_field_update(ipv4_addr, updates.ipv4_addr, txid, height); - apply_field_update(ipv6_addr, updates.ipv6_addr, txid, height); - apply_field_update( - encryption_pubkey, - updates.encryption_pubkey, - txid, - height, - ); - apply_field_update( - signing_pubkey, - updates.signing_pubkey, - txid, - height, - ); - apply_field_update( - paymail_fee_sats, - updates.paymail_fee_sats, - txid, - height, - ); - } - - // revert bitname data updates - fn revert_updates( - &mut self, - updates: BitNameDataUpdates, - txid: Txid, - height: u32, - ) { - // apply an update to a single data field - fn revert_field_update( - data_field: &mut RollBack>>, - update: Update, - txid: Txid, - height: u32, - ) where - T: std::fmt::Debug + Eq, - { - match update { - Update::Delete => { - let popped = data_field.pop(); - assert!(popped.is_some()); - let popped = popped.unwrap(); - assert!(popped.data.is_none()); - assert_eq!(popped.txid, txid); - assert_eq!(popped.height, height) - } - Update::Retain => (), - Update::Set(value) => { - let popped = data_field.pop(); - assert!(popped.is_some()); - let popped = popped.unwrap(); - assert!(popped.data.is_some()); - assert_eq!(popped.data.unwrap(), value); - assert_eq!(popped.txid, txid); - assert_eq!(popped.height, height) - } - } - } +mod bitname_data; +pub mod error; +mod rollback; - let Self { - ref mut commitment, - is_icann: _, - ref mut ipv4_addr, - ref mut ipv6_addr, - ref mut encryption_pubkey, - ref mut signing_pubkey, - ref mut paymail_fee_sats, - } = self; - revert_field_update( - paymail_fee_sats, - updates.paymail_fee_sats, - txid, - height, - ); - revert_field_update( - signing_pubkey, - updates.signing_pubkey, - txid, - height, - ); - revert_field_update( - encryption_pubkey, - updates.encryption_pubkey, - txid, - height, - ); - revert_field_update(ipv6_addr, updates.ipv6_addr, txid, height); - revert_field_update(ipv4_addr, updates.ipv4_addr, txid, height); - revert_field_update(commitment, updates.commitment, txid, height); - } - - /** Returns the Bitname data as it was, at the specified block height. - * If a value was updated several times in the block, returns the - * last value seen in the block. - * Returns `None` if the data did not exist at the specified block - * height. */ - pub fn at_block_height(&self, height: u32) -> Option { - Some(types::BitNameData { - commitment: self.commitment.at_block_height(height)?.data, - ipv4_addr: self.ipv4_addr.at_block_height(height)?.data, - ipv6_addr: self.ipv6_addr.at_block_height(height)?.data, - encryption_pubkey: self - .encryption_pubkey - .at_block_height(height)? - .data, - signing_pubkey: self.signing_pubkey.at_block_height(height)?.data, - paymail_fee_sats: self - .paymail_fee_sats - .at_block_height(height)? - .data, - }) - } - - /// get the current bitname data - pub fn current(&self) -> types::BitNameData { - types::BitNameData { - commitment: self.commitment.latest().data, - ipv4_addr: self.ipv4_addr.latest().data, - ipv6_addr: self.ipv6_addr.latest().data, - encryption_pubkey: self.encryption_pubkey.latest().data, - signing_pubkey: self.signing_pubkey.latest().data, - paymail_fee_sats: self.paymail_fee_sats.latest().data, - } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum InvalidHeaderError { - #[error("expected block hash {expected}, but computed {computed}")] - BlockHash { - expected: BlockHash, - computed: BlockHash, - }, - #[error("expected previous sidechain block hash {expected}, but received {received}")] - PrevSideHash { - expected: BlockHash, - received: BlockHash, - }, -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error(transparent)] - AmountOverflow(#[from] AmountOverflowError), - #[error(transparent)] - AmountUnderflow(#[from] AmountUnderflowError), - #[error("failed to verify authorization")] - AuthorizationError, - #[error("bad coinbase output content")] - BadCoinbaseOutputContent, - #[error("bitname {name_hash} already registered")] - BitNameAlreadyRegistered { name_hash: BitName }, - #[error("bitname {name_hash} already registered as an ICANN name")] - BitNameAlreadyIcann { name_hash: BitName }, - #[error("bundle too heavy {weight} > {max_weight}")] - BundleTooHeavy { weight: u64, max_weight: u64 }, - #[error(transparent)] - BorshSerialize(borsh::io::Error), - #[error("failed to fill tx output contents: invalid transaction")] - FillTxOutputContentsFailed, - #[error("heed error")] - Heed(#[from] heed::Error), - #[error("invalid ICANN name: {plain_name}")] - IcannNameInvalid { plain_name: String }, - #[error("invalid body: expected merkle root {expected}, but computed {computed}")] - InvalidBody { - expected: MerkleRoot, - computed: MerkleRoot, - }, - #[error("invalid header: {0}")] - InvalidHeader(InvalidHeaderError), - #[error("failed to compute merkle root")] - MerkleRoot, - #[error("missing BitName {name_hash}")] - MissingBitName { name_hash: BitName }, - #[error( - "Missing BitName data for {name_hash} at block height {block_height}" - )] - MissingBitNameData { - name_hash: BitName, - block_height: u32, - }, - #[error("missing BitName input {name_hash}")] - MissingBitNameInput { name_hash: BitName }, - #[error("missing BitName reservation {txid}")] - MissingReservation { txid: Txid }, - #[error("no BitNames to update")] - NoBitNamesToUpdate, - #[error("deposit block doesn't exist")] - NoDepositBlock, - #[error("total fees less than coinbase value")] - NotEnoughFees, - #[error("value in is less than value out")] - NotEnoughValueIn, - #[error("stxo {outpoint} doesn't exist")] - NoStxo { outpoint: OutPoint }, - #[error("no tip")] - NoTip, - #[error("utxo {outpoint} doesn't exist")] - NoUtxo { outpoint: OutPoint }, - #[error("Withdrawal bundle event block doesn't exist")] - NoWithdrawalBundleEventBlock, - #[error(transparent)] - SignatureError(#[from] ed25519_dalek::SignatureError), - #[error("Too few BitName outputs")] - TooFewBitNameOutputs, - #[error("unbalanced BitNames: {n_bitname_inputs} BitName inputs, {n_bitname_outputs} BitName outputs")] - UnbalancedBitNames { - n_bitname_inputs: usize, - n_bitname_outputs: usize, - }, - #[error("unbalanced reservations: {n_reservation_inputs} reservation inputs, {n_reservation_outputs} reservation outputs")] - UnbalancedReservations { - n_reservation_inputs: usize, - n_reservation_outputs: usize, - }, - #[error("Unknown withdrawal bundle: {m6id}")] - UnknownWithdrawalBundle { m6id: M6id }, - #[error("utxo double spent")] - UtxoDoubleSpent, - #[error(transparent)] - WithdrawalBundle(#[from] WithdrawalBundleError), - #[error("wrong public key for address")] - WrongPubKeyForAddress, -} +use bitname_data::BitNameData; +pub use error::Error; +use rollback::{HeightStamped, RollBack}; type WithdrawalBundlesDb = Database< SerdeBincode, @@ -459,9 +42,11 @@ pub struct State { tip: WatchableDb, SerdeBincode>, /// Current height height: Database, SerdeBincode>, - /// associates tx hashes with bitname reservation commitments + /// Associates tx hashes with bitname reservation commitments pub bitname_reservations: Database, SerdeBincode>, - /// associates bitname IDs (name hashes) with bitname data + /// Associates BitName sequence numbers with BitName IDs (name hashes) + pub bitname_seq_to_bitname: Database>, + /// Associates bitname IDs (name hashes) with bitname data pub bitnames: Database, SerdeBincode>, pub utxos: Database, SerdeBincode>, pub stxos: Database, SerdeBincode>, @@ -482,7 +67,7 @@ pub struct State { } impl State { - pub const NUM_DBS: u32 = 11; + pub const NUM_DBS: u32 = 12; pub const WITHDRAWAL_BUNDLE_FAILURE_GAP: u32 = 5; pub fn new(env: &heed::Env) -> Result { @@ -491,6 +76,8 @@ impl State { let height = env.create_database(&mut rwtxn, Some("height"))?; let bitname_reservations = env.create_database(&mut rwtxn, Some("bitname_reservations"))?; + let bitname_seq_to_bitname = + env.create_database(&mut rwtxn, Some("bitname_seq_to_bitname"))?; let bitnames = env.create_database(&mut rwtxn, Some("bitnames"))?; let utxos = env.create_database(&mut rwtxn, Some("utxos"))?; let stxos = env.create_database(&mut rwtxn, Some("stxos"))?; @@ -513,6 +100,7 @@ impl State { tip, height, bitname_reservations, + bitname_seq_to_bitname, bitnames, utxos, stxos, @@ -968,7 +556,7 @@ impl State { ) -> Result<(bitcoin::Amount, MerkleRoot), Error> { let tip_hash = self.get_tip(rotxn)?; if header.prev_side_hash != tip_hash { - let err = InvalidHeaderError::PrevSideHash { + let err = error::InvalidHeader::PrevSideHash { expected: tip_hash, received: header.prev_side_hash, }; @@ -1490,7 +1078,7 @@ impl State { rwtxn: &mut RwTxn, filled_tx: &FilledTransaction, name_hash: BitName, - bitname_data: &types::BitNameData, + bitname_data: &types::MutableBitNameData, height: u32, ) -> Result<(), Error> { // Find the reservation to burn @@ -1516,8 +1104,19 @@ impl State { txid: *burned_reservation_txid, }); } - let bitname_data = - BitNameData::init(bitname_data.clone(), filled_tx.txid(), height); + let next_seq_id = self + .bitname_seq_to_bitname + .last(rwtxn)? + .map(|(seq, _)| seq.next()) + .unwrap_or(BitNameSeqId::new(0)); + self.bitname_seq_to_bitname + .put(rwtxn, &next_seq_id, &name_hash)?; + let bitname_data = BitNameData::init( + bitname_data.clone(), + filled_tx.txid(), + height, + next_seq_id, + ); self.bitnames.put(rwtxn, &name_hash, &bitname_data)?; Ok(()) } @@ -1531,6 +1130,13 @@ impl State { if !self.bitnames.delete(rwtxn, &name_hash)? { return Err(Error::MissingBitName { name_hash }); } + let (last_seq_id, latest_registered_bitname) = self + .bitname_seq_to_bitname + .last(rwtxn)? + .expect("A registered bitname should have a seq id"); + assert_eq!(latest_registered_bitname, name_hash); + self.bitname_seq_to_bitname.delete(rwtxn, &last_seq_id)?; + // Find the reservation to restore let implied_commitment = filled_tx.implied_reservation_commitment().expect( @@ -1692,7 +1298,7 @@ impl State { let height = self.get_height(rwtxn)?; let tip_hash = self.get_tip(rwtxn)?; if tip_hash != header.prev_side_hash { - let err = InvalidHeaderError::PrevSideHash { + let err = error::InvalidHeader::PrevSideHash { expected: tip_hash, received: header.prev_side_hash, }; @@ -1818,7 +1424,7 @@ impl State { ) -> Result<(), Error> { let tip_hash = self.get_tip(rwtxn)?; if tip_hash != header.hash() { - let err = InvalidHeaderError::BlockHash { + let err = error::InvalidHeader::BlockHash { expected: tip_hash, computed: header.hash(), }; diff --git a/lib/state/rollback.rs b/lib/state/rollback.rs new file mode 100644 index 0000000..c1ebf31 --- /dev/null +++ b/lib/state/rollback.rs @@ -0,0 +1,105 @@ +use nonempty::NonEmpty; +use serde::{Deserialize, Serialize}; + +use crate::types::Txid; + +/// Data of type `T` paired with block height at which it was last updated +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(in crate::state) struct HeightStamped { + pub value: T, + pub height: u32, +} + +/// Data of type `T` paired with +/// * the txid at which it was last updated +/// * block height at which it was last updated +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(in crate::state) struct TxidStamped { + pub data: T, + pub txid: Txid, + pub height: u32, +} + +/// Wrapper struct for fields that support rollbacks +#[derive(Clone, Debug, Deserialize, Serialize)] +#[repr(transparent)] +#[serde(transparent)] +pub(in crate::state) struct RollBack(pub NonEmpty); + +impl RollBack> { + pub fn new(value: T, height: u32) -> Self { + let height_stamped = HeightStamped { value, height }; + Self(NonEmpty::new(height_stamped)) + } + + /// Pop the most recent value + pub fn pop(mut self) -> (Option, HeightStamped) { + if let Some(value) = self.0.pop() { + (Some(self), value) + } else { + (None, self.0.head) + } + } + + /// Attempt to push a value as the new most recent. + /// Returns the value if the operation fails. + pub fn push(&mut self, value: T, height: u32) -> Result<(), T> { + if self.0.last().height >= height { + return Err(value); + } + let height_stamped = HeightStamped { value, height }; + self.0.push(height_stamped); + Ok(()) + } + + /// Returns the earliest value + pub fn earliest(&self) -> &HeightStamped { + self.0.first() + } + + /// Returns the most recent value + pub fn latest(&self) -> &HeightStamped { + self.0.last() + } +} + +impl RollBack> { + pub fn new(value: T, txid: Txid, height: u32) -> Self { + let txid_stamped = TxidStamped { + data: value, + txid, + height, + }; + Self(NonEmpty::new(txid_stamped)) + } + + /// push a value as the new most recent + pub fn push(&mut self, value: T, txid: Txid, height: u32) { + let txid_stamped = TxidStamped { + data: value, + txid, + height, + }; + self.0.push(txid_stamped) + } + + /// pop the most recent value + pub fn pop(&mut self) -> Option> { + self.0.pop() + } + + /** Returns the value as it was, at the specified block height. + * If a value was updated several times in the block, returns the + * last value seen in the block. */ + pub fn at_block_height(&self, height: u32) -> Option<&TxidStamped> { + self.0 + .iter() + .rev() + .find(|txid_stamped| txid_stamped.height <= height) + } + + /// returns the most recent value, along with it's txid + pub fn latest(&self) -> &TxidStamped { + self.0.last() + } +} diff --git a/lib/types/bitname_data.rs b/lib/types/bitname_data.rs new file mode 100644 index 0000000..123232b --- /dev/null +++ b/lib/types/bitname_data.rs @@ -0,0 +1,228 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use borsh::BorshSerialize; +use educe::Educe; +use serde::{Deserialize, Serialize}; +use utoipa::{ + openapi::{RefOr, Schema}, + PartialSchema, ToSchema, +}; + +use crate::{ + authorization::VerifyingKey, + types::{BitNameSeqId, EncryptionPubKey, Hash}, +}; + +fn hash_option_verifying_key(pk: &Option, state: &mut H) +where + H: std::hash::Hasher, +{ + use std::hash::Hash; + pk.map(|pk| pk.to_bytes()).hash(state) +} + +fn borsh_serialize_option_verifying_key( + pk: &Option, + writer: &mut W, +) -> borsh::io::Result<()> +where + W: borsh::io::Write, +{ + borsh::BorshSerialize::serialize(&pk.map(|pk| pk.to_bytes()), writer) +} + +/// Bitname data that can be updated later +#[derive( + BorshSerialize, + Clone, + Debug, + Default, + Deserialize, + Educe, + Eq, + PartialEq, + Serialize, + ToSchema, +)] +#[educe(Hash)] +pub struct MutableBitNameData { + /// commitment to arbitrary data + #[schema(value_type = Option)] + pub commitment: Option, + /// optional ipv4 addr + #[schema(value_type = Option)] + pub ipv4_addr: Option, + /// optional ipv6 addr + #[schema(value_type = Option)] + pub ipv6_addr: Option, + /// optional pubkey used for encryption + #[schema(value_type = Option)] + pub encryption_pubkey: Option, + /// optional pubkey used for signing messages + #[borsh(serialize_with = "borsh_serialize_option_verifying_key")] + #[educe(Hash(method = "hash_option_verifying_key"))] + #[schema(value_type = Option)] + pub signing_pubkey: Option, + /// optional minimum paymail fee, in sats + pub paymail_fee_sats: Option, +} + +/// Bitname data that can be updated later +#[derive( + BorshSerialize, + Clone, + Debug, + Deserialize, + Educe, + Eq, + PartialEq, + Serialize, + ToSchema, +)] +#[educe(Hash)] +pub struct BitNameData { + pub seq_id: BitNameSeqId, + #[serde(flatten)] + pub mutable_data: MutableBitNameData, +} + +/// delete, retain, or set a value +#[derive(BorshSerialize, Clone, Debug, Deserialize, Serialize)] +pub enum Update { + Delete, + Retain, + Set(T), +} + +impl Update { + /// Create a schema from a schema for `T`. + fn schema(schema_t: RefOr) -> RefOr { + let schema_delete = utoipa::openapi::ObjectBuilder::new() + .schema_type(utoipa::openapi::Type::String) + .enum_values(Some(["Delete"])); + let schema_retain = utoipa::openapi::ObjectBuilder::new() + .schema_type(utoipa::openapi::Type::String) + .enum_values(Some(["Retain"])); + let schema_set = utoipa::openapi::ObjectBuilder::new() + .property("Set", schema_t) + .required("Set"); + let schema = utoipa::openapi::OneOfBuilder::new() + .item(schema_delete) + .item(schema_retain) + .item(schema_set) + .build() + .into(); + RefOr::T(schema) + } +} + +impl PartialSchema for Update { + fn schema() -> utoipa::openapi::RefOr { + Self::schema(::schema()) + } +} + +impl ToSchema for Update { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("UpdateHash") + } +} + +impl PartialSchema for Update { + fn schema() -> utoipa::openapi::RefOr { + Self::schema(::schema()) + } +} + +impl ToSchema for Update { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("UpdateIpv4Addr") + } +} + +impl PartialSchema for Update { + fn schema() -> utoipa::openapi::RefOr { + Self::schema(::schema()) + } +} + +impl ToSchema for Update { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("UpdateIpv6Addr") + } +} + +impl PartialSchema for Update { + fn schema() -> utoipa::openapi::RefOr { + Self::schema(::schema()) + } +} + +impl ToSchema for Update { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("UpdateEncryptionPubKey") + } +} + +impl PartialSchema for Update { + fn schema() -> utoipa::openapi::RefOr { + Self::schema(::schema()) + } +} + +impl ToSchema for Update { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("UpdateVerifyingKey") + } +} + +impl PartialSchema for Update { + fn schema() -> utoipa::openapi::RefOr { + Self::schema(::schema()) + } +} + +impl ToSchema for Update { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("UpdateU64") + } +} + +fn borsh_serialize_update_pubkey( + update: &Update, + writer: &mut W, +) -> borsh::io::Result<()> +where + W: borsh::io::Write, +{ + let update = match update { + Update::Delete => Update::Delete, + Update::Retain => Update::Retain, + Update::Set(value) => Update::Set(value.as_bytes()), + }; + borsh::BorshSerialize::serialize(&update, writer) +} + +/// updates to the data associated with a BitName +#[derive(BorshSerialize, Clone, Debug, Deserialize, Serialize, ToSchema)] +pub struct BitNameDataUpdates { + /// commitment to arbitrary data + #[schema(schema_with = as PartialSchema>::schema)] + pub commitment: Update, + /// optional ipv4 addr + #[schema(schema_with = as PartialSchema>::schema)] + pub ipv4_addr: Update, + /// optional ipv6 addr + #[schema(schema_with = as PartialSchema>::schema)] + pub ipv6_addr: Update, + /// optional pubkey used for encryption + #[schema(schema_with = as PartialSchema>::schema)] + pub encryption_pubkey: Update, + /// optional pubkey used for signing messages + #[borsh(serialize_with = "borsh_serialize_update_pubkey")] + #[schema(schema_with = as PartialSchema>::schema)] + pub signing_pubkey: Update, + /// optional minimum paymail fee, in sats + #[schema(schema_with = as PartialSchema>::schema)] + pub paymail_fee_sats: Update, +} diff --git a/lib/types/bitname_seq_id.rs b/lib/types/bitname_seq_id.rs new file mode 100644 index 0000000..d0d9cc1 --- /dev/null +++ b/lib/types/bitname_seq_id.rs @@ -0,0 +1,259 @@ +use borsh::BorshSerialize; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use utoipa::{openapi, PartialSchema, ToSchema}; + +#[derive(Debug, Error)] +pub enum ParseBitNameSeqIdError { + #[error("Empty segment; cannot start with `-` char")] + EmptySegmentStart, + #[error("Empty segment; cannot end with `-` char")] + EmptySegmentEnd, + #[error("Empty segment; cannot contain sequential `-` chars")] + EmptySegment, + #[error("Invalid char; must contain only ASCII digits and `-`: `{char}`")] + InvalidChar { char: char }, + #[error("Invalid segment; must contain exactly 4 ASCII digits: `{invalid_segment}`")] + InvalidSegment { invalid_segment: String }, + #[error( + "Value overflow: BitName seq ID encodes a number greater than u32::MAX" + )] + Overflow, + #[error("Too few segments; 2 or 3 segments required")] + TooFewSegments, + #[error("Too many segments; 2 or 3 segments required")] + TooManySegments, +} + +/// Sequential IDs for BitNames. +/// Has a special 'human-readable' representation, used in Display, and +/// human-readable serialization. +#[derive( + BorshSerialize, Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, +)] +#[repr(transparent)] +pub struct BitNameSeqId(u32); + +impl BitNameSeqId { + /// Used for the 'human-readable' representation + const DISPLAY_OFFSET: u32 = 23071990; + + pub fn new(seq_id: u32) -> Self { + Self(seq_id) + } + + pub fn next(&self) -> Self { + Self(self.0 + 1) + } +} + +impl std::fmt::Display for BitNameSeqId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let first_4_digit_component: u32 = self.0 / 1_0000_0000; + // last 8 digits with offset applied + let last_8_digit_component: u32 = + ((self.0 % 1_0000_0000) + Self::DISPLAY_OFFSET) % 1_0000_0000; + let reordered_ascii: [[u8; 4]; 2] = { + let last_8_digits: String = format!("{last_8_digit_component:08}"); + let last_8_digits: &[u8] = last_8_digits.as_bytes(); + assert_eq!(last_8_digits.len(), 8); + let mut reordered: [[u8; 4]; 2] = [[b'0'; 4]; 2]; + reordered[0][0] = last_8_digits[4]; + reordered[0][1] = last_8_digits[3]; + reordered[0][2] = last_8_digits[1]; + reordered[0][3] = last_8_digits[6]; + reordered[1][0] = last_8_digits[2]; + reordered[1][1] = last_8_digits[7]; + reordered[1][2] = last_8_digits[0]; + reordered[1][3] = last_8_digits[5]; + reordered + }; + if first_4_digit_component > 0 { + write!( + f, + "{first_4_digit_component:04}-{}-{}", + String::from_utf8_lossy(&reordered_ascii[0]), + String::from_utf8_lossy(&reordered_ascii[1]), + ) + } else { + write!( + f, + "{}-{}", + String::from_utf8_lossy(&reordered_ascii[0]), + String::from_utf8_lossy(&reordered_ascii[1]), + ) + } + } +} + +impl std::str::FromStr for BitNameSeqId { + type Err = ParseBitNameSeqIdError; + + fn from_str(mut s: &str) -> Result { + // parse an ASCII segment + fn parse_segment(s: &str) -> Result<[u8; 4], ParseBitNameSeqIdError> { + if s.starts_with('-') { + return Err(ParseBitNameSeqIdError::EmptySegment); + }; + if !s.chars().all(|c| c.is_ascii_digit()) { + return Err(ParseBitNameSeqIdError::InvalidSegment { + invalid_segment: s.to_owned(), + }); + }; + s.as_bytes().try_into().map_err(|_err| { + ParseBitNameSeqIdError::InvalidSegment { + invalid_segment: s.to_owned(), + } + }) + } + fn last_8_digit_component(s0: &[u8; 4], s1: &[u8; 4]) -> u32 { + let mut last_8_digits_ascii: [u8; 8] = [b'0'; 8]; + last_8_digits_ascii[0] = s1[2]; + last_8_digits_ascii[1] = s0[2]; + last_8_digits_ascii[2] = s1[0]; + last_8_digits_ascii[3] = s0[1]; + last_8_digits_ascii[4] = s0[0]; + last_8_digits_ascii[5] = s1[3]; + last_8_digits_ascii[6] = s0[3]; + last_8_digits_ascii[7] = s1[1]; + let last_8_digits_offset: u32 = + String::from_utf8_lossy(&last_8_digits_ascii) + .parse() + .unwrap(); + // subtract offset + if last_8_digits_offset >= BitNameSeqId::DISPLAY_OFFSET { + last_8_digits_offset - BitNameSeqId::DISPLAY_OFFSET + } else { + 1_0000_0000 + - (BitNameSeqId::DISPLAY_OFFSET - last_8_digits_offset) + } + } + if let Some(invalid_char) = + s.chars().find(|c| !(c.is_ascii_digit() || *c == '-')) + { + return Err(Self::Err::InvalidChar { char: invalid_char }); + }; + if s.starts_with('-') { + return Err(Self::Err::EmptySegmentStart); + }; + if s.ends_with('-') { + return Err(Self::Err::EmptySegmentEnd); + }; + let mut segments = Vec::new(); + while let Some((segment, rest)) = s.split_once('-') { + segments.push(parse_segment(segment)?); + if segments.len() > 2 { + return Err(Self::Err::TooManySegments); + } + s = rest; + } + // push final segment + segments.push(parse_segment(s)?); + match segments.as_slice() { + [] | [_] => Err(Self::Err::TooFewSegments), + [s0, s1] => Ok(Self(last_8_digit_component(s0, s1))), + [s0, s1, s2] => { + let first_4_digits: u32 = + String::from_utf8_lossy(s0).parse().unwrap(); + Ok(Self( + first_4_digits + .checked_mul(1_0000_0000) + .ok_or(Self::Err::Overflow)? + + last_8_digit_component(s1, s2), + )) + } + _ => Err(Self::Err::TooManySegments), + } + } +} + +impl<'de> Deserialize<'de> for BitNameSeqId { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + if deserializer.is_human_readable() { + let s = <&'de str as Deserialize<'de>>::deserialize(deserializer)?; + s.parse().map_err(D::Error::custom) + } else { + >::deserialize(deserializer).map(Self) + } + } +} + +impl Serialize for BitNameSeqId { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + Serialize::serialize(&self.to_string(), serializer) + } else { + Serialize::serialize(&self.0, serializer) + } + } +} + +/// Decode from big-endian +impl<'a> heed::BytesDecode<'a> for BitNameSeqId { + type DItem = Self; + + fn bytes_decode(bytes: &'a [u8]) -> Result { + as heed::BytesDecode>::bytes_decode(bytes) + .map(Self) + } +} + +/// Encode as big-endian +impl<'a> heed::BytesEncode<'a> for BitNameSeqId { + type EItem = Self; + + fn bytes_encode( + item: &'a Self::EItem, + ) -> Result, heed::BoxedError> { + as heed::BytesEncode>::bytes_encode(&item.0) + } +} + +impl PartialSchema for BitNameSeqId { + fn schema() -> openapi::RefOr { + let obj = utoipa::openapi::Object::with_type(openapi::Type::String); + openapi::RefOr::T(openapi::Schema::Object(obj)) + } +} + +impl ToSchema for BitNameSeqId { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("BitNameSeqId") + } +} + +#[cfg(test)] +mod test { + use super::BitNameSeqId; + + // Test roundtrip display/parse of BitName IDs + #[test] + fn parse_display_bitname_ids() { + [ + (0, "1739-0029"), + (1, "1739-0129"), + (24, "2731-0420"), + (76928008, "9999-9899"), + (76928009, "9999-9999"), + (76928013, "0000-0300"), + (1_0000_0000, "0001-1739-0029"), + ] + .into_iter() + .for_each(|(seq, expected_str)| { + let seq = BitNameSeqId(seq); + let human_readable_repr = seq.to_string(); + assert_eq!(human_readable_repr, expected_str); + assert_eq!( + human_readable_repr.parse::().unwrap(), + seq + ) + }) + } +} diff --git a/lib/types/mod.rs b/lib/types/mod.rs index 14f0927..712d633 100644 --- a/lib/types/mod.rs +++ b/lib/types/mod.rs @@ -13,6 +13,8 @@ use utoipa::ToSchema; pub use crate::authorization::Authorization; mod address; +pub mod bitname_data; +pub mod bitname_seq_id; pub mod constants; pub mod hashes; pub mod proto; @@ -20,13 +22,17 @@ pub mod schema; mod transaction; pub use address::Address; +pub use bitname_data::{ + BitNameData, BitNameDataUpdates, MutableBitNameData, Update, +}; +pub use bitname_seq_id::BitNameSeqId; pub use hashes::{BitName, BlockHash, Hash, M6id, MerkleRoot, Txid}; pub use transaction::{ - Authorized, AuthorizedTransaction, BatchIcannRegistrationData, BitNameData, - BitNameDataUpdates, Content as OutputContent, - FilledContent as FilledOutputContent, FilledOutput, FilledTransaction, - InPoint, OutPoint, Output, Pointed as PointedOutput, SpentOutput, - Transaction, TransactionData, TxData, Update, + Authorized, AuthorizedTransaction, BatchIcannRegistrationData, + Content as OutputContent, FilledContent as FilledOutputContent, + FilledOutput, FilledTransaction, InPoint, OutPoint, Output, + Pointed as PointedOutput, SpentOutput, Transaction, TransactionData, + TxData, }; pub const THIS_SIDECHAIN: u8 = 2; diff --git a/lib/types/transaction/mod.rs b/lib/types/transaction/mod.rs index c11b315..da06a60 100644 --- a/lib/types/transaction/mod.rs +++ b/lib/types/transaction/mod.rs @@ -1,24 +1,18 @@ -use std::{ - hash::Hasher, - net::{Ipv4Addr, Ipv6Addr}, -}; - use bitcoin::amount::CheckedSum; use borsh::BorshSerialize; -use educe::Educe; use serde::{Deserialize, Serialize}; -use utoipa::{ - openapi::{RefOr, Schema}, - PartialSchema, ToSchema, -}; +use utoipa::{PartialSchema, ToSchema}; use super::{ address::Address, hashes::{self, BitName, Hash, M6id, MerkleRoot, Txid}, serde_display_fromstr_human_readable, serde_hexstr_human_readable, - AmountOverflowError, EncryptionPubKey, GetValue, + AmountOverflowError, GetValue, +}; +use crate::{ + authorization::{Authorization, Signature}, + types::{BitNameDataUpdates, MutableBitNameData}, }; -use crate::authorization::{Authorization, Signature, VerifyingKey}; mod output_content; pub use output_content::{Content, Filled as FilledContent}; @@ -154,200 +148,6 @@ pub type TxInputs = Vec; pub type TxOutputs = Vec; -fn hash_option_verifying_key(pk: &Option, state: &mut H) -where - H: Hasher, -{ - use std::hash::Hash; - pk.map(|pk| pk.to_bytes()).hash(state) -} - -fn borsh_serialize_option_verifying_key( - pk: &Option, - writer: &mut W, -) -> borsh::io::Result<()> -where - W: borsh::io::Write, -{ - borsh::BorshSerialize::serialize(&pk.map(|pk| pk.to_bytes()), writer) -} - -#[derive( - BorshSerialize, - Clone, - Debug, - Default, - Deserialize, - Educe, - Eq, - PartialEq, - Serialize, - ToSchema, -)] -#[educe(Hash)] -pub struct BitNameData { - /// commitment to arbitrary data - #[schema(value_type = Option)] - pub commitment: Option, - /// optional ipv4 addr - #[schema(value_type = Option)] - pub ipv4_addr: Option, - /// optional ipv6 addr - #[schema(value_type = Option)] - pub ipv6_addr: Option, - /// optional pubkey used for encryption - #[schema(value_type = Option)] - pub encryption_pubkey: Option, - /// optional pubkey used for signing messages - #[borsh(serialize_with = "borsh_serialize_option_verifying_key")] - #[educe(Hash(method = "hash_option_verifying_key"))] - #[schema(value_type = Option)] - pub signing_pubkey: Option, - /// optional minimum paymail fee, in sats - pub paymail_fee_sats: Option, -} - -/// delete, retain, or set a value -#[derive(BorshSerialize, Clone, Debug, Deserialize, Serialize)] -pub enum Update { - Delete, - Retain, - Set(T), -} - -impl Update { - /// Create a schema from a schema for `T`. - fn schema(schema_t: RefOr) -> RefOr { - let schema_delete = utoipa::openapi::ObjectBuilder::new() - .schema_type(utoipa::openapi::Type::String) - .enum_values(Some(["Delete"])); - let schema_retain = utoipa::openapi::ObjectBuilder::new() - .schema_type(utoipa::openapi::Type::String) - .enum_values(Some(["Retain"])); - let schema_set = utoipa::openapi::ObjectBuilder::new() - .property("Set", schema_t) - .required("Set"); - let schema = utoipa::openapi::OneOfBuilder::new() - .item(schema_delete) - .item(schema_retain) - .item(schema_set) - .build() - .into(); - RefOr::T(schema) - } -} - -impl PartialSchema for Update { - fn schema() -> utoipa::openapi::RefOr { - Self::schema(::schema()) - } -} - -impl ToSchema for Update { - fn name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("UpdateHash") - } -} - -impl PartialSchema for Update { - fn schema() -> utoipa::openapi::RefOr { - Self::schema(::schema()) - } -} - -impl ToSchema for Update { - fn name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("UpdateIpv4Addr") - } -} - -impl PartialSchema for Update { - fn schema() -> utoipa::openapi::RefOr { - Self::schema(::schema()) - } -} - -impl ToSchema for Update { - fn name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("UpdateIpv6Addr") - } -} - -impl PartialSchema for Update { - fn schema() -> utoipa::openapi::RefOr { - Self::schema(::schema()) - } -} - -impl ToSchema for Update { - fn name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("UpdateEncryptionPubKey") - } -} - -impl PartialSchema for Update { - fn schema() -> utoipa::openapi::RefOr { - Self::schema(::schema()) - } -} - -impl ToSchema for Update { - fn name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("UpdateVerifyingKey") - } -} - -impl PartialSchema for Update { - fn schema() -> utoipa::openapi::RefOr { - Self::schema(::schema()) - } -} - -impl ToSchema for Update { - fn name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("UpdateU64") - } -} - -fn borsh_serialize_update_pubkey( - update: &Update, - writer: &mut W, -) -> borsh::io::Result<()> -where - W: borsh::io::Write, -{ - let update = match update { - Update::Delete => Update::Delete, - Update::Retain => Update::Retain, - Update::Set(value) => Update::Set(value.as_bytes()), - }; - borsh::BorshSerialize::serialize(&update, writer) -} - -/// updates to the data associated with a BitName -#[derive(BorshSerialize, Clone, Debug, Deserialize, Serialize, ToSchema)] -pub struct BitNameDataUpdates { - /// commitment to arbitrary data - #[schema(schema_with = as PartialSchema>::schema)] - pub commitment: Update, - /// optional ipv4 addr - #[schema(schema_with = as PartialSchema>::schema)] - pub ipv4_addr: Update, - /// optional ipv6 addr - #[schema(schema_with = as PartialSchema>::schema)] - pub ipv6_addr: Update, - /// optional pubkey used for encryption - #[schema(schema_with = as PartialSchema>::schema)] - pub encryption_pubkey: Update, - /// optional pubkey used for signing messages - #[borsh(serialize_with = "borsh_serialize_update_pubkey")] - #[schema(schema_with = as PartialSchema>::schema)] - pub signing_pubkey: Update, - /// optional minimum paymail fee, in sats - #[schema(schema_with = as PartialSchema>::schema)] - pub paymail_fee_sats: Update, -} - fn borsh_serialize_signature( sig: &Signature, writer: &mut W, @@ -386,7 +186,7 @@ pub enum TransactionData { #[schema(value_type = String)] revealed_nonce: Hash, /// initial BitName data - bitname_data: Box, + bitname_data: Box, }, BitNameUpdate(Box), BatchIcann(BatchIcannRegistrationData), diff --git a/lib/wallet.rs b/lib/wallet.rs index 37e6b28..a74fe48 100644 --- a/lib/wallet.rs +++ b/lib/wallet.rs @@ -20,9 +20,9 @@ use crate::{ authorization::{get_address, Authorization}, types::{ hashes::BitName, Address, AmountOverflowError, AmountUnderflowError, - AuthorizedTransaction, BitNameData, FilledOutput, GetValue, Hash, - InPoint, OutPoint, Output, OutputContent, SpentOutput, Transaction, - TxData, + AuthorizedTransaction, FilledOutput, GetValue, Hash, InPoint, + MutableBitNameData, OutPoint, Output, OutputContent, SpentOutput, + Transaction, TxData, }, util::{EnvExt, Watchable, WatchableDb}, }; @@ -360,7 +360,7 @@ impl Wallet { &self, tx: &mut Transaction, plain_name: &str, - bitname_data: Cow, + bitname_data: Cow, ) -> Result<(), Error> { assert!(tx.is_regular(), "this function only accepts a regular tx"); // address for the registration output