From cfb4272e397afef5cccd2011c5e9ec6149553557 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Wed, 4 Nov 2026 14:27:25 +0000 Subject: [PATCH 1/2] wallet: Date a new wallet by this computer's clock Bitcoin Core resolves an import timestamp of "now" to its tip's median time. On an offline signer that never synced, that is the genesis block, so every new wallet was born in 2009, and the watch-only copy Bails exports from it asks the online node to rescan from genesis, which a pruned node cannot do. A new wallet now takes this computer's clock, a day early like a recorded creation date, so a clock running up to a day fast cannot skip its first payments. Restores keep the recorded date. On a Core 32.1 regtest signer at genesis, the descriptors were dated 1296688602 (genesis) before and the clock less a day after; the signer still found a payment from blocks it synced later. Claude-Session: https://claude.ai/code/session_01N8jLK61uohaW6W7Y8AHnMb --- src/codex32/_bitcoin_core.py | 6 +++++- tests/test_bitcoin_core.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/codex32/_bitcoin_core.py b/src/codex32/_bitcoin_core.py index b2689ea5..99525610 100644 --- a/src/codex32/_bitcoin_core.py +++ b/src/codex32/_bitcoin_core.py @@ -449,12 +449,16 @@ def initialize( continue # Checked last, after any wait above: a pruning node keeps pruning while it waits. self.check_history(timestamp) + # Core's "now" is its tip's time: the genesis block's on an offline signer that never + # synced, and watch-only exports carry it. A new wallet takes this computer's clock, + # a day early like a record's date, so a fast clock cannot skip its first payments. + start = max(0, int(time.time()) - 86400) if timestamp == "now" else timestamp records = core_descriptors( secret, account=account, testnet=self.chain != "main", private=True, - timestamp=timestamp, + timestamp=start, ) imported = self._rpc( "importdescriptors", diff --git a/tests/test_bitcoin_core.py b/tests/test_bitcoin_core.py index f0ecee7e..557f7962 100644 --- a/tests/test_bitcoin_core.py +++ b/tests/test_bitcoin_core.py @@ -391,6 +391,7 @@ def test_encrypted_import_retries_without_a_passphrase_verifies_and_relocks( "_rpc", lambda client, *args, wallet=None, stdin=None: rpc(client, *args, wallet=wallet, stdin=stdin), ) + monkeypatch.setattr("codex32._bitcoin_core.time.time", lambda: 1_700_000_000.5) client = BitcoinCore("bitcoin-cli", "main", 300000) messages: list[str] = [] delays: list[int] = [] @@ -411,7 +412,8 @@ def unlock(seconds: int) -> None: assert arguments == ("importdescriptors",) and wallet == "signer" assert private_stdin is not None and private_stdin.endswith("\n") records = json.loads(private_stdin) - assert len(records) == 4 and all(record["timestamp"] == "now" for record in records) + # A new wallet's "now" is this computer's clock less a day, not Core's tip (genesis offline). + assert len(records) == 4 and all(record["timestamp"] == 1_700_000_000 - 86400 for record in records) assert all("xprv" in record["desc"] for record in records) assert all("xprv" not in " ".join((*args, selected or "")) for args, selected, _data in rpc.calls) assert sum(args[:2] == ("-named", "derivehdkey") for args, _wallet, _stdin in rpc.calls) == 4 From ae9d360a74020b00e5d0dcf0703fa7e9fce6de50 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Wed, 4 Nov 2026 18:05:16 +0000 Subject: [PATCH 2/2] wallet: Keep the new-wallet timestamp within the size budget Claude-Session: https://claude.ai/code/session_01N8jLK61uohaW6W7Y8AHnMb --- src/codex32/_bitcoin_core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/codex32/_bitcoin_core.py b/src/codex32/_bitcoin_core.py index 99525610..b1eae17c 100644 --- a/src/codex32/_bitcoin_core.py +++ b/src/codex32/_bitcoin_core.py @@ -452,13 +452,12 @@ def initialize( # Core's "now" is its tip's time: the genesis block's on an offline signer that never # synced, and watch-only exports carry it. A new wallet takes this computer's clock, # a day early like a record's date, so a fast clock cannot skip its first payments. - start = max(0, int(time.time()) - 86400) if timestamp == "now" else timestamp records = core_descriptors( secret, account=account, testnet=self.chain != "main", private=True, - timestamp=start, + timestamp=max(0, int(time.time()) - 86400) if timestamp == "now" else timestamp, ) imported = self._rpc( "importdescriptors",