commit 75fddf425102369828f7e8366ebdad4ea086fd07 from: murilo ijanc date: Fri Mar 27 14:58:41 2026 UTC Bump version to 0.1.3, update tesseras-dht to 0.1.4 - Block marker on delete prevents DHT re-import - Remote delete propagation via delete_callback - New index page with project info and man page links - Fix lookups status order (started/completed) commit - 18fa0f13f64e69bf70addc1e28a8ab0a39207eb2 commit + 75fddf425102369828f7e8366ebdad4ea086fd07 blob - 528900662ba4a21427138802e96965247a96a989 blob + 2bf98e4c9e3f157fdc4ae3e7b9f1463c381ab7f8 --- CHANGELOG.md +++ CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project will be documented The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.3] - 2026-03-27 + +### Fixed + +- Delete now creates a block marker, preventing `sync_dht_to_store` + from re-importing deleted pastes from the DHT every 5 seconds. +- `sync_dht_to_store` skips blocked keys before attempting import. +- `lookups` status now shows started/completed (was inverted and + started was never incremented). + +### Added + +- Remote delete propagation: `set_delete_callback` blocks and removes + pastes on disk when a remote store TTL=0 arrives from the DHT. +- New index page at `/` with project info, quick start, philosophy, + bootstrap nodes, source repos, donation address, and man page links. + +### Changed + +- Update tesseras-dht to 0.1.4. + ## [0.1.2] - 2026-03-27 ### Changed blob - 8f54a02fe1e1b71196f26568ea58b3285e77aa11 blob + 775aeded8e31d7c183c724829f79cdfec8474e41 --- Cargo.lock +++ Cargo.lock @@ -546,9 +546,9 @@ dependencies = [ [[package]] name = "tesseras-dht" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b285288b07dd1e2b4e7b1b1868090a8fb6c3d544e78ca089ba7ad3c01f5ae5f3" +checksum = "bbf078ea60af6f03507fbc92e29ce579e961e467f74f3885cca0b97c0d45f44c" dependencies = [ "ed25519-dalek", "log", @@ -558,7 +558,7 @@ dependencies = [ [[package]] name = "tesseras-paste" -version = "0.1.2" +version = "0.1.3" dependencies = [ "chacha20poly1305", "env_logger", blob - ed1af59c24c92af0b62be9bad729108d1a86673d blob + ef41e774e8f2ca24b6528157c785b19225be5821 --- Cargo.toml +++ Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tesseras-paste" -version = "0.1.2" +version = "0.1.3" edition = "2024" license = "ISC" readme = "README.md" @@ -24,4 +24,4 @@ env_logger = "=0.11.10" log = "=0.4.29" # tesseras -tesseras-dht = "=0.1.3" +tesseras-dht = "=0.1.4" blob - 4895a487d322f683df8f0146b20f1f655eb86421 blob + 8952d78e05f3c4600b06bb7cc372610e31eccc06 --- src/daemon.rs +++ src/daemon.rs @@ -36,6 +36,143 @@ const REJOIN_INTERVAL: Duration = Duration::from_secs( /// How often to sync DHT-replicated values to local store (5 s). const SYNC_INTERVAL: Duration = Duration::from_secs(5); +const INDEX_PAGE: &str = "\ +tesseras-paste + + Decentralized pastebin built on the tesseras-dht + Kademlia DHT. Pastes are encrypted with XChaCha20- + Poly1305, content-addressed via SHA-256, and + replicated across the network. No accounts, no + databases, no JavaScript. + + \"In the beginning there was the server, + and the server was centralized, + and the centralized was fragile, + and the fragile was doomed.\" + + -- Apocrypha of the Lost Nodes + + +Why + + Pastebin.com sold out and filled with ads. + Ghostbin shut down. Hastebin went offline. + ZeroBin stopped being maintained. + PrivateBin requires a server you must trust. + + Every centralized pastebin is one decision away + from disappearing, censoring, or monetizing your + content. + + We got tired of renting someone else's clipboard. + + tesseras-paste has no single point of failure. + No company can shut it down. No server can be + seized. Your paste lives as long as the network + has nodes. + + +Philosophy + + We believe in: + - Code you can read in an afternoon + - Protocols you can implement in a weekend + - Systems that work without permission + - Networks that survive their creators + + We do not believe in: + - Move fast and break things + - Microservices for a text file + - 400MB Docker images to serve hello world + - npm install the-entire-internet + + +How it works + + 1. You write text (or pipe it from stdin) + 2. tp encrypts it with a random key + 3. SHA-256 hashes the ciphertext into a content + address + 4. The encrypted blob is stored on the K-closest + DHT nodes + 5. The URL contains the only key that decrypts it + 6. The network never sees your plaintext + + +Quick start + + Install from crates.io: + + $ cargo install tesseras-paste + + Start the daemon (connects to the public bootstrap + nodes automatically via DNS SRV): + + $ tpd -d /var/tesseras-paste -w 9999 + + Create a paste: + + $ echo 'hello world' | tp put + 4zxDwJQEte37CQE4xzVCB1GaNodBprLUHjHcWzhTqP7Y#... + + Retrieve it: + + $ tp get 4zxDwJQEte37CQE4xzVCB1GaNodBprLUHjHcWzhTqP7Y#... + hello world + + Public (unencrypted) paste: + + $ echo 'visible to all' | tp put -p + EbpnKntDRBkuDKJuFKY7Ke7jM9ygtLCSYpmykXvzWb8U + + Pin a paste so it never expires: + + $ tp pin + + +Bootstrap nodes + + bootstrap1.tesseras.net port 4433 + bootstrap2.tesseras.net port 4433 + + +Source code + + official https://got.tesseras.net + mirror https://git.tesseras.net + sourcehut https://git.sr.ht/~ijanc/tesseras + github https://github.com/tesseras-net + + +Donate + + Bitcoin bc1qm3srpwnpe3y58mhn7lp37lyw0s45tfdganvat5 + + +Website + + https://tesseras.net + + +Greets + + To my beloved Aninha, for the patience and love. + + To the cypherpunks, for writing code instead + of laws. + + To OpenBSD, for building an OS where security + is not a feature but a principle. + + To everyone running a node: you ARE the network. + + +See also + + tp(1) https://p.tesseras.net/64RQsdrPsmtdYzLQX9SQN3NBkuU1fD1pQMGdYkUXERV5 + tpd(1) https://p.tesseras.net/Ho4jVs1tj4endcZ3ymus3Tm33XGze2tz2K8Qey4EMTRD +"; + /// A request from the socket thread to the main thread. pub struct DaemonRequest { pub cmd: Request, @@ -56,6 +193,15 @@ pub fn run_daemon( let mut last_sync = Instant::now(); let mut last_rejoin = Instant::now(); + // Block + remove paste on disk when a remote + // delete (store TTL=0) arrives from the DHT. + let del_store = store.clone(); + node.set_delete_callback(move |key: &[u8]| { + del_store.block(key); + del_store.remove_paste(key); + log::info!("remote delete: blocked key {}", crate::base58::encode(key)); + }); + log::info!("daemon main loop started"); while !shutdown.load(Ordering::Relaxed) { @@ -189,8 +335,8 @@ fn handle_request( store.paste_count(), m.messages_sent, m.messages_received, - m.lookups_completed, m.lookups_started, + m.lookups_completed, ); Response::Ok(status) } @@ -205,6 +351,9 @@ fn sync_dht_to_store(node: &Node, store: &PasteStore) if key.len() != 32 { continue; } + if store.is_blocked(&key) { + continue; + } if store.get_paste(&key).is_none() { let _ = store.put_paste(&key, &value); } @@ -473,8 +622,8 @@ fn handle_http( http_response( &mut stream, 200, - "text/plain", - b"Hello Tesseras World\n", + "text/plain; charset=utf-8", + INDEX_PAGE.as_bytes(), ); return; } blob - 45fb919d90f70615db9b181e4c17eebe0fcc696d blob + f45b9abe1f1b41422c8e313a23564421a69a2d35 --- src/ops.rs +++ src/ops.rs @@ -151,13 +151,17 @@ pub fn get_paste( } /// Delete a paste from local store and the DHT. +/// Creates a block marker so the paste is not +/// re-imported from the DHT by sync. pub fn delete_paste( node: &mut Node, store: &PasteStore, key_str: &str, ) -> Result<(), PasteError> { let hash = parse_hash(key_str)?; + store.block(&hash); store.remove_paste(&hash); + store.unpin(&hash).ok(); node.delete(&hash); let hash_b58 = key_str.split_once('#').map(|(h, _)| h).unwrap_or(key_str); log::info!("del: removed paste {hash_b58}"); blob - 04d74146365b24dc8c0554df5b11592bf282e117 blob + 2e4f53acaff4f89f816163f0e0bed1234bd06823 --- src/store.rs +++ src/store.rs @@ -134,6 +134,11 @@ impl PasteStore { self.pin_path(key).exists() } + /// Mark a paste as blocked (prevents re-import from DHT). + pub fn block(&self, key: &[u8]) { + let _ = fs::File::create(self.block_path(key)); + } + pub fn is_blocked(&self, key: &[u8]) -> bool { self.block_path(key).exists() }