Commit Diff


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 <key>
+
+
+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()
     }