From 899d5c3a4157e1d60ec2a9f19ea53dde85fbcd68 Mon Sep 17 00:00:00 2001 From: Alain Brenzikofer Date: Fri, 11 Sep 2026 14:19:31 +0000 Subject: [PATCH] core: the column holds entropy, so name it entropy The mnemonic comes back from the entropy exactly, which is what export returns, and the seed PBKDF2 derives from the mnemonic is computed when a key is needed and never stored. The column was the one thing called seed that is not the seed, which is why it needed a comment saying so. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX --- docs/rfcs/2026-09-10-wallet-keys.md | 9 +++++++-- .../Store/Postgres/Migrations/M20260908_wallet_seeds.hs | 2 +- .../Chat/Store/Postgres/Migrations/chat_schema.sql | 2 +- .../Store/SQLite/Migrations/M20260908_wallet_seeds.hs | 2 +- src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql | 2 +- src/Simplex/Chat/Store/Wallets.hs | 4 ++-- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/rfcs/2026-09-10-wallet-keys.md b/docs/rfcs/2026-09-10-wallet-keys.md index f2923dd2c7..ecf5215a2a 100644 --- a/docs/rfcs/2026-09-10-wallet-keys.md +++ b/docs/rfcs/2026-09-10-wallet-keys.md @@ -68,7 +68,7 @@ Internal API. The names commands will call these; users will not. ``` /_wallet the next name addresses /_wallet create new generate the seed -/_wallet create mnemonic= take the seed from a mnemonic +/_wallet create mnemonic= take the entropy from a mnemonic /_wallet export the seed mnemonic /_wallet export one derived secret, as 0x and 64 hex digits /_wallet delete delete the seed @@ -86,7 +86,7 @@ not leave the device, and the raw command would be logged there. ```sql CREATE TABLE wallet_seeds ( wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT, - seed BLOB NOT NULL, + entropy BLOB NOT NULL, next_name_index INTEGER NOT NULL DEFAULT 1, single_seed INTEGER NOT NULL DEFAULT 1 ); @@ -94,6 +94,11 @@ CREATE TABLE wallet_seeds ( No other table is touched. +What is stored is the BIP-39 entropy, 16 to 32 bytes. The mnemonic comes back +from it exactly, as the entropy and its checksum determine the words, and that +is what export returns. The seed itself, the 64 bytes PBKDF2 derives from the +mnemonic, is computed when a key is needed and never stored. + `next_name_index` is a high-water mark, not a count of names held. A name a device no longer tracks still owns its address, so an index is never reused. diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260908_wallet_seeds.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260908_wallet_seeds.hs index d8617d0841..7dd970d8f6 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/M20260908_wallet_seeds.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260908_wallet_seeds.hs @@ -11,7 +11,7 @@ m20260908_wallet_seeds = [r| CREATE TABLE wallet_seeds ( wallet_seed_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - seed BYTEA NOT NULL, + entropy BYTEA NOT NULL, -- see the SQLite migration next_name_index BIGINT NOT NULL DEFAULT 1, -- one seed per device for now diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index c5e5fa52e5..1cdf43e011 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -1535,7 +1535,7 @@ ALTER TABLE test_chat_schema.users ALTER COLUMN user_id ADD GENERATED ALWAYS AS CREATE TABLE test_chat_schema.wallet_seeds ( wallet_seed_id bigint NOT NULL, - seed bytea NOT NULL, + entropy bytea NOT NULL, next_name_index bigint DEFAULT 1 NOT NULL, single_seed smallint DEFAULT 1 NOT NULL ); diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260908_wallet_seeds.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260908_wallet_seeds.hs index a2fdc2b500..9f3b0468c9 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20260908_wallet_seeds.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260908_wallet_seeds.hs @@ -10,7 +10,7 @@ m20260908_wallet_seeds = [sql| CREATE TABLE wallet_seeds ( wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT, - seed BLOB NOT NULL, -- BIP-39 entropy, 16-32 bytes + entropy BLOB NOT NULL, -- known issue: after an import this starts at 1, so it can hand out a name -- key at a path that already owns a name next_name_index INTEGER NOT NULL DEFAULT 1, diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 80c72a42e7..a3c86389db 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -854,7 +854,7 @@ CREATE TABLE rcv_roster_transfers( ) STRICT; CREATE TABLE wallet_seeds( wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT, - seed BLOB NOT NULL, -- BIP-39 entropy, 16-32 bytes + entropy BLOB NOT NULL, -- known issue: after an import this starts at 1, so it can hand out a name -- key at a path that already owns a name next_name_index INTEGER NOT NULL DEFAULT 1, diff --git a/src/Simplex/Chat/Store/Wallets.hs b/src/Simplex/Chat/Store/Wallets.hs index bd09feb3a0..a9a098b96c 100644 --- a/src/Simplex/Chat/Store/Wallets.hs +++ b/src/Simplex/Chat/Store/Wallets.hs @@ -28,7 +28,7 @@ toSeed (sId, seed) = WalletSeed {wsId = sId, wsEntropy = seed} getDeviceSeed :: DB.Connection -> IO (Maybe WalletSeed) getDeviceSeed db = maybeFirstRow toSeed $ - DB.query_ db "SELECT wallet_seed_id, seed FROM wallet_seeds ORDER BY wallet_seed_id LIMIT 1" + DB.query_ db "SELECT wallet_seed_id, entropy FROM wallet_seeds ORDER BY wallet_seed_id LIMIT 1" -- | The index the next name bought on this device takes. getNextNameIndex :: DB.Connection -> SeedId -> IO NameIndex @@ -43,7 +43,7 @@ createSeed :: DB.Connection -> ByteString -> IO Bool createSeed db entropy = getDeviceSeed db >>= \case Just _ -> pure False - Nothing -> True <$ DB.execute db "INSERT INTO wallet_seeds (seed) VALUES (?)" (Only $ DB.Binary entropy) + Nothing -> True <$ DB.execute db "INSERT INTO wallet_seeds (entropy) VALUES (?)" (Only $ DB.Binary entropy) deleteSeed :: DB.Connection -> SeedId -> IO () deleteSeed db sId = DB.execute db "DELETE FROM wallet_seeds WHERE wallet_seed_id = ?" (Only sId)