With proof of work skipped, chaining headers from a checkpoint up to a
transaction's block proves nothing: a server can fabricate a header and
fake headers after a real checkpoint. The header must now hash-chain up to
the nearest checkpoint at or above its height, whose hash is hardcoded.
This also anchors heights below the first checkpoint. The cached anchor
state is renamed AnchoredDownTo and caches without it are re-verified
once. Blocks above the newest checkpoint remain a documented limit.
See ADR 0040.
Empty header range during checkpoint anchoring, send-all limits, SyncCache
roundtrip, corrupted config, derivation edge cases, spendability
boundaries, unit formatting, loader errors, in-flight cap, disposal under
load and wallet-lock contention. Also raises the synchronizer partial-result
timeouts so they hold under load.
Adds a WalletDocument.WatchAddresses field and WalletLoader.NewFromAddresses
to build an ImportedKeyAccount with no private key at all (unlike xpub/WIF
imports, which can still derive/hold key material). ScriptKind is inferred
from the address itself via a new DerivationPaths.KindFor helper. Signing
already refuses to run for any IsWatchOnly account (TransactionFactory), so
this only had to wire up construction/reload of the new account shape.
A tampered or corrupted wallet file could reach Decrypt with broken
JSON, missing fields, invalid base64, or a wrong nonce/tag size, all of
which previously escaped as raw JsonException/FormatException/
ArgumentNullException instead of the documented WrongPasswordException/
InvalidDataException contract. Worse, the iteration count is read
straight from the (attacker-controlled) container with no upper bound:
a tampered file demanding e.g. 2^31 PBKDF2 iterations would hang the
wallet at open - a DoS via a file the user merely tries to open. Now
every malformed shape maps to InvalidDataException and the iteration
count is clamped to a sane maximum (10,000,000, well above the current
600,000 default).
DefaultDataRoot, the pointer-file location and the portable-mode root
all read machine-global locations (APPDATA, AppContext.BaseDirectory),
so the override→portable→pointer→default precedence in DataRoot/
IsDataLocationConfigured was previously untestable beyond the override
case. Added three internal overrides (BootstrapDirOverride,
PortableBaseOverride, DefaultRootOverride) as sandboxing seams, and a
new AppPathsResolutionTests class covering every precedence branch:
default-only, portable-beats-pointer, pointer-beats-default, a blank
pointer falling back to default, "already has data" counting as
configured, and override beating everything. Both AppPaths test
classes share the "AppPaths" xUnit collection since this state is
static.
- AppPaths: data-root resolution, per-network paths, wallet file listing.
- TransactionFactory: legacy/P2SH/segwit destinations, multi-UTXO selection,
dust change absorbed into fee, and a golden txid for the PSBT signing path
(RFC 6979 makes it deterministic; the builder's own output shuffling for
privacy makes anchoring the vector at the top-level Build() unreliable).
- PropertyTests: Slip132 roundtrip for every script kind and network,
WalletDocument JSON roundtrip with arbitrary labels/contacts, Scripthash
cross-checked against an independent SHA-256 computation.
- StorageTests: regression cases for the IsEncrypted fix.
- Removed CS8602/CS8604 nullable warnings from existing address/loader tests.
Translate every Italian /// XML doc comment, <!-- --> XAML comment, and
// inline comment to English across all source files (Core, App, tests).
Add the language policy to CLAUDE.md (conversation Italian; all code
and docs English). Update one test assertion that checked an Italian
exception message that was also translated.