Skip to content

Commit 6043514

Browse files
Add time-capped txdb to Archive
- Add AddressTxidKey keyed as address || txid - Store filled tx, its Merkle proof, block hash/height, and Unix timestamp - Update txdb with new confirmed txs when blocks connect to the active chain - Query txdb by address set and minimum block height - Prune txdb by Unix timestamp every 144 * 7 blocks (~weekly) - Prune txdb records for disconnected blocks during reorgs - Make Merkle proof types serializable General rationale: lite wallet is UTXO-based so strictly speaking it does not need to know tx history, however, it is still nice to show users actual tx history so they can figure out what was going on. However again, storing full tx history is too demanding for a full node so it is capped.
1 parent 74323cd commit 6043514

5 files changed

Lines changed: 250 additions & 13 deletions

File tree

lib/archive.rs

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,24 @@ use std::{
77
use bitcoin::{self, hashes::Hash as _};
88
use fallible_iterator::{FallibleIterator, IteratorExt};
99
use heed::types::SerdeBincode;
10+
use serde::{Deserialize, Serialize};
1011
use sneed::{
1112
DatabaseUnique, EnvError, RoTxn, RwTxn, RwTxnError, UnitKey,
1213
db::{self, error::Error as DbError},
1314
env, rwtxn,
1415
};
1516

1617
use crate::types::{
17-
Block, BlockHash, BmmResult, Body, Header, Tip, Txid, VERSION, Version,
18+
Address, AddressTxidKey, Block, BlockHash, BmmResult, Body,
19+
FilledTransaction, Header, MerkleProof, Tip, Txid, VERSION, Version,
1820
proto::mainchain,
1921
};
2022

2123
#[allow(clippy::duplicated_attributes)]
2224
#[derive(thiserror::Error, transitive::Transitive, Debug)]
25+
#[transitive(from(db::error::Delete, DbError))]
26+
#[transitive(from(db::error::IterInit, DbError))]
27+
#[transitive(from(db::error::IterItem, DbError))]
2328
#[transitive(from(db::error::Put, DbError))]
2429
#[transitive(from(db::error::TryGet, DbError))]
2530
#[transitive(from(env::error::CreateDb, EnvError))]
@@ -73,6 +78,19 @@ pub enum Error {
7378
NoMainHeight(bitcoin::BlockHash),
7479
#[error("no tx with txid {0}")]
7580
NoTx(Txid),
81+
#[error(
82+
"txdb filled transaction length mismatch: expected {expected}, actual {actual}"
83+
)]
84+
TxDbFilledTxLenMismatch { expected: usize, actual: usize },
85+
}
86+
87+
#[derive(Clone, Debug, Deserialize, Serialize)]
88+
pub struct FilledTxEntry {
89+
pub tx: FilledTransaction,
90+
pub merkle_proof: MerkleProof,
91+
pub block_hash: BlockHash,
92+
pub block_height: u32,
93+
pub unix_stamp: u64,
7694
}
7795

7896
#[derive(Clone)]
@@ -148,11 +166,13 @@ pub struct Archive {
148166
SerdeBincode<Txid>,
149167
SerdeBincode<BTreeMap<BlockHash, u32>>,
150168
>,
169+
/// Capped tx cache, keyed by (address, txid).
170+
txdb: DatabaseUnique<AddressTxidKey, SerdeBincode<FilledTxEntry>>,
151171
_version: DatabaseUnique<UnitKey, SerdeBincode<Version>>,
152172
}
153173

154174
impl Archive {
155-
pub const NUM_DBS: u32 = 14;
175+
pub const NUM_DBS: u32 = 15;
156176

157177
pub fn new(env: &sneed::Env) -> Result<Self, Error> {
158178
let mut rwtxn = env.write_txn()?;
@@ -224,6 +244,7 @@ impl Archive {
224244
let total_work = DatabaseUnique::create(env, &mut rwtxn, "total_work")?;
225245
let txid_to_inclusions =
226246
DatabaseUnique::create(env, &mut rwtxn, "txid_to_inclusions")?;
247+
let txdb = DatabaseUnique::create(env, &mut rwtxn, "txdb")?;
227248
rwtxn.commit()?;
228249
Ok(Self {
229250
block_hash_to_height,
@@ -239,6 +260,7 @@ impl Archive {
239260
successors,
240261
total_work,
241262
txid_to_inclusions,
263+
txdb,
242264
_version: version,
243265
})
244266
}
@@ -672,6 +694,125 @@ impl Archive {
672694
Ok(inclusions)
673695
}
674696

697+
pub fn prune_txdb_block(
698+
&self,
699+
rwtxn: &mut RwTxn,
700+
block_hash: BlockHash,
701+
) -> Result<usize, Error> {
702+
let keys = {
703+
let mut keys = Vec::new();
704+
let mut iter = self.txdb.iter(rwtxn).map_err(DbError::from)?;
705+
while let Some((key, entry)) = iter.next().map_err(DbError::from)? {
706+
if entry.block_hash == block_hash {
707+
keys.push(key);
708+
}
709+
}
710+
keys
711+
};
712+
for key in &keys {
713+
let _ = self.txdb.delete(rwtxn, key)?;
714+
}
715+
Ok(keys.len())
716+
}
717+
718+
pub fn prune_txdb_older_than(
719+
&self,
720+
rwtxn: &mut RwTxn,
721+
cutoff_unix: u64,
722+
) -> Result<usize, Error> {
723+
let keys = {
724+
let mut keys = Vec::new();
725+
let mut iter = self.txdb.iter(rwtxn).map_err(DbError::from)?;
726+
while let Some((key, entry)) = iter.next().map_err(DbError::from)? {
727+
if entry.unix_stamp < cutoff_unix {
728+
keys.push(key);
729+
}
730+
}
731+
keys
732+
};
733+
for key in &keys {
734+
let _ = self.txdb.delete(rwtxn, key)?;
735+
}
736+
Ok(keys.len())
737+
}
738+
739+
pub fn put_txdb_for_connected_block(
740+
&self,
741+
rwtxn: &mut RwTxn,
742+
block_hash: BlockHash,
743+
block_height: u32,
744+
body: &Body,
745+
filled_txs: &[FilledTransaction],
746+
unix_stamp: u64,
747+
) -> Result<(), Error> {
748+
if body.transactions.len() != filled_txs.len() {
749+
return Err(Error::TxDbFilledTxLenMismatch {
750+
expected: body.transactions.len(),
751+
actual: filled_txs.len(),
752+
});
753+
}
754+
for (tx_index, filled_tx) in filled_txs.iter().enumerate() {
755+
let txid = filled_tx.txid();
756+
debug_assert_eq!(body.transactions[tx_index].txid(), txid);
757+
let merkle_proof = Body::compute_tx_merkle_proof(
758+
&body.coinbase,
759+
&body.transactions,
760+
tx_index,
761+
);
762+
debug_assert_eq!(merkle_proof.leaf_index, tx_index + 1);
763+
764+
let mut addresses = HashSet::new();
765+
addresses.extend(
766+
filled_tx.spent_utxos.iter().map(|output| output.address),
767+
);
768+
addresses.extend(
769+
filled_tx
770+
.transaction
771+
.outputs
772+
.iter()
773+
.map(|output| output.address),
774+
);
775+
776+
for address in addresses {
777+
let key = AddressTxidKey::new(address, txid);
778+
let entry = FilledTxEntry {
779+
tx: filled_tx.clone(),
780+
merkle_proof: merkle_proof.clone(),
781+
block_hash,
782+
block_height,
783+
unix_stamp,
784+
};
785+
self.txdb.put(rwtxn, &key, &entry)?;
786+
}
787+
}
788+
Ok(())
789+
}
790+
791+
pub fn get_txdb_entries_by_address(
792+
&self,
793+
rotxn: &RoTxn,
794+
addresses: &HashSet<Address>,
795+
height_threshold: u32,
796+
) -> Result<Vec<FilledTxEntry>, Error> {
797+
let mut entries = Vec::new();
798+
for address in addresses {
799+
let start = AddressTxidKey::start(*address);
800+
let end = AddressTxidKey::end(*address);
801+
let mut iter = self
802+
.txdb
803+
.range(rotxn, &(start..=end))
804+
.map_err(DbError::from)?;
805+
while let Some((_key, entry)) =
806+
iter.next().map_err(DbError::from)?
807+
{
808+
if entry.block_height >= height_threshold {
809+
entries.push(entry);
810+
}
811+
}
812+
}
813+
Ok(entries)
814+
}
815+
675816
/// Store a block body. The header must already exist.
676817
pub fn put_body(
677818
&self,

lib/node/net_task.rs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{
55
collections::{HashMap, HashSet},
66
net::SocketAddr,
77
sync::Arc,
8-
time::Duration,
8+
time::{Duration, SystemTime, UNIX_EPOCH},
99
};
1010

1111
use error_fatality::{Nested as _, Split};
@@ -129,6 +129,15 @@ impl From<net::Error> for Error {
129129
}
130130
}
131131

132+
const TXDB_PRUNE_INTERVAL_BLOCKS: u32 = 144 * 7;
133+
const TXDB_RETENTION_SECS: u64 = 28 * 24 * 60 * 60;
134+
135+
fn unix_now() -> u64 {
136+
SystemTime::now()
137+
.duration_since(UNIX_EPOCH)
138+
.map_or(0, |duration| duration.as_secs())
139+
}
140+
132141
fn connect_tip_(
133142
rwtxn: &mut RwTxn<'_>,
134143
archive: &Archive,
@@ -139,18 +148,30 @@ fn connect_tip_(
139148
two_way_peg_data: &mainchain::TwoWayPegData,
140149
) -> Result<(), Error> {
141150
let block_hash = header.hash();
151+
let prevalidated = state.prevalidate_block(rwtxn, header, body)?;
152+
let block_height = prevalidated.next_height;
153+
let merkle_root = prevalidated.computed_merkle_root;
154+
let filled_txs = prevalidated.filled_transactions.clone();
155+
state.connect_prevalidated_block(rwtxn, header, body, prevalidated)?;
142156
if tracing::enabled!(tracing::Level::DEBUG) {
143-
let merkle_root =
144-
Body::compute_merkle_root(&body.coinbase, &body.transactions);
145-
let height = state.try_get_height(rwtxn)?;
146-
state.apply_block(rwtxn, header, body)?;
147-
tracing::debug!(?height, %merkle_root, %block_hash, "connected body")
148-
} else {
149-
state.apply_block(rwtxn, header, body)?;
157+
tracing::debug!(height = block_height, %merkle_root, %block_hash, "connected body")
150158
}
151159
let () = state.connect_two_way_peg_data(rwtxn, two_way_peg_data)?;
152160
let () = archive.put_header(rwtxn, header)?;
153161
let () = archive.put_body(rwtxn, block_hash, body)?;
162+
let unix_stamp = unix_now();
163+
let () = archive.put_txdb_for_connected_block(
164+
rwtxn,
165+
block_hash,
166+
block_height,
167+
body,
168+
&filled_txs,
169+
unix_stamp,
170+
)?;
171+
if block_height > 0 && block_height % TXDB_PRUNE_INTERVAL_BLOCKS == 0 {
172+
let cutoff_unix = unix_stamp.saturating_sub(TXDB_RETENTION_SECS);
173+
let _ = archive.prune_txdb_older_than(rwtxn, cutoff_unix)?;
174+
}
154175
for transaction in &body.transactions {
155176
let () = mempool.delete(rwtxn, transaction.txid())?;
156177
}
@@ -250,6 +271,7 @@ fn disconnect_tip_(
250271
}
251272
};
252273
let () = state.disconnect_two_way_peg_data(rwtxn, &two_way_peg_data)?;
274+
let _ = archive.prune_txdb_block(rwtxn, tip_block_hash)?;
253275
let () = state.disconnect_tip(rwtxn, &tip_header, &tip_body)?;
254276
for transaction in tip_body.authorized_transactions().iter().rev() {
255277
mempool.put(rwtxn, transaction)?;

lib/types/hashes.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,13 @@ impl utoipa::ToSchema for MerkleRoot {
147147
}
148148
}
149149

150-
#[derive(Clone, Debug)]
150+
#[derive(Clone, Debug, Deserialize, Serialize)]
151151
pub struct MerkleProof {
152152
pub leaf_index: usize,
153153
pub siblings: Vec<MerkleProofNode>,
154154
}
155155

156-
#[derive(Clone, Copy, Debug)]
156+
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
157157
pub struct MerkleProofNode {
158158
pub hash: Hash,
159159
pub is_left: bool,

lib/types/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub use hashes::{
2929
};
3030
pub use keys::{EncryptionPubKey, VerifyingKey};
3131
pub use transaction::{
32-
AddressOutPointKey, AmmBurn, AmmMint, AmmSwap, AssetOutput,
32+
AddressOutPointKey, AddressTxidKey, AmmBurn, AmmMint, AmmSwap, AssetOutput,
3333
AssetOutputContent, Authorized, AuthorizedTransaction, BitcoinOutput,
3434
BitcoinOutputContent, DutchAuctionBid, DutchAuctionCollect,
3535
DutchAuctionParams, FilledOutput, FilledOutputContent, FilledTransaction,

lib/types/transaction/mod.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,80 @@ impl<'a> BytesDecode<'a> for AddressOutPointKey {
310310
}
311311
}
312312

313+
const TXID_SIZE: usize = blake3::OUT_LEN;
314+
const ADDR_TXID_DB_KEY_SIZE: usize = ADDRESS_SIZE + TXID_SIZE;
315+
316+
/// Fixed-width txdb key: address || txid.
317+
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
318+
pub struct AddressTxidKey([u8; ADDR_TXID_DB_KEY_SIZE]);
319+
320+
impl AddressTxidKey {
321+
#[inline]
322+
pub fn new(address: Address, txid: Txid) -> Self {
323+
let mut key = [0u8; ADDR_TXID_DB_KEY_SIZE];
324+
key[..ADDRESS_SIZE].copy_from_slice(&address.0);
325+
key[ADDRESS_SIZE..].copy_from_slice(txid.as_slice());
326+
Self(key)
327+
}
328+
329+
#[inline]
330+
pub fn start(address: Address) -> Self {
331+
let mut key = [0u8; ADDR_TXID_DB_KEY_SIZE];
332+
key[..ADDRESS_SIZE].copy_from_slice(&address.0);
333+
Self(key)
334+
}
335+
336+
#[inline]
337+
pub fn end(address: Address) -> Self {
338+
let mut key = [0xffu8; ADDR_TXID_DB_KEY_SIZE];
339+
key[..ADDRESS_SIZE].copy_from_slice(&address.0);
340+
Self(key)
341+
}
342+
343+
#[inline]
344+
pub fn address(&self) -> Address {
345+
let mut address = [0u8; ADDRESS_SIZE];
346+
address.copy_from_slice(&self.0[..ADDRESS_SIZE]);
347+
Address(address)
348+
}
349+
350+
#[inline]
351+
pub fn txid(&self) -> Txid {
352+
let mut txid = [0u8; TXID_SIZE];
353+
txid.copy_from_slice(&self.0[ADDRESS_SIZE..]);
354+
Txid(txid)
355+
}
356+
}
357+
358+
impl AsRef<[u8]> for AddressTxidKey {
359+
#[inline]
360+
fn as_ref(&self) -> &[u8] {
361+
&self.0
362+
}
363+
}
364+
365+
impl<'a> BytesEncode<'a> for AddressTxidKey {
366+
type EItem = AddressTxidKey;
367+
368+
#[inline]
369+
fn bytes_encode(
370+
item: &'a Self::EItem,
371+
) -> Result<Cow<'a, [u8]>, BoxedError> {
372+
Ok(Cow::Borrowed(item.as_ref()))
373+
}
374+
}
375+
376+
impl<'a> BytesDecode<'a> for AddressTxidKey {
377+
type DItem = AddressTxidKey;
378+
379+
#[inline]
380+
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, BoxedError> {
381+
let mut key = [0u8; ADDR_TXID_DB_KEY_SIZE];
382+
key.copy_from_slice(bytes);
383+
Ok(Self(key))
384+
}
385+
}
386+
313387
#[cfg(test)]
314388
mod tests {
315389
use super::{OUTPOINT_KEY_SIZE, OutPoint, OutPointKey};

0 commit comments

Comments
 (0)