mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
test(funds): fence harness on v2 owner install
This commit is contained in:
@@ -9,7 +9,7 @@ import {finalizeEvent, getPublicKey} from 'nostr-tools/pure'
|
|||||||
import {v2 as nip44v2} from 'nostr-tools/nip44'
|
import {v2 as nip44v2} from 'nostr-tools/nip44'
|
||||||
import {buildNoteUrl} from 'lnurlcash-kit'
|
import {buildNoteUrl} from 'lnurlcash-kit'
|
||||||
|
|
||||||
import {deriveBearerAesKey, linkingPubKeyHex, saveLinkingKey} from './keys'
|
import {deriveBearerAesKey, deriveWalletMaterial, linkingPubKeyHex, saveWalletMaterial} from './keys'
|
||||||
import {
|
import {
|
||||||
BACKUP_EVENT_KIND,
|
BACKUP_EVENT_KIND,
|
||||||
backupPubkey,
|
backupPubkey,
|
||||||
@@ -40,6 +40,13 @@ import {requiredValue, stubLocalStorage} from './test-utils'
|
|||||||
const LINKING_KEY = new Uint8Array(32).fill(7)
|
const LINKING_KEY = new Uint8Array(32).fill(7)
|
||||||
const OTHER_KEY = new Uint8Array(32).fill(9)
|
const OTHER_KEY = new Uint8Array(32).fill(9)
|
||||||
const OWNER_ID = linkingPubKeyHex(LINKING_KEY)
|
const OWNER_ID = linkingPubKeyHex(LINKING_KEY)
|
||||||
|
// full v2 material for the fixed harness linking key (real BIP-32 cash root)
|
||||||
|
const MATERIAL = {
|
||||||
|
...deriveWalletMaterial(
|
||||||
|
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
||||||
|
),
|
||||||
|
linkingKeyHex: bytesToHex(LINKING_KEY),
|
||||||
|
}
|
||||||
|
|
||||||
const K1_A = 'aa'.repeat(32)
|
const K1_A = 'aa'.repeat(32)
|
||||||
const K1_B = 'bb'.repeat(32)
|
const K1_B = 'bb'.repeat(32)
|
||||||
@@ -103,7 +110,7 @@ describe('restoreFromNostr', () => {
|
|||||||
const {transport} = createRecordingTransport()
|
const {transport} = createRecordingTransport()
|
||||||
|
|
||||||
// device A: one note, one trusted mint, one setting - all published
|
// device A: one note, one trusted mint, one setting - all published
|
||||||
await saveLinkingKey(LINKING_KEY)
|
await saveWalletMaterial(MATERIAL)
|
||||||
await persistBearer(aesKey, bearerFixture({id: 'note-a'}))
|
await persistBearer(aesKey, bearerFixture({id: 'note-a'}))
|
||||||
await addTrustedMint('mint.example', MINT_PUBKEY, {ownerId: OWNER_ID})
|
await addTrustedMint('mint.example', MINT_PUBKEY, {ownerId: OWNER_ID})
|
||||||
persistSettings({defaultMint: 'mint.example'})
|
persistSettings({defaultMint: 'mint.example'})
|
||||||
@@ -120,7 +127,7 @@ describe('restoreFromNostr', () => {
|
|||||||
|
|
||||||
// device B: the same seed on empty storage
|
// device B: the same seed on empty storage
|
||||||
stubLocalStorage()
|
stubLocalStorage()
|
||||||
await saveLinkingKey(LINKING_KEY)
|
await saveWalletMaterial(MATERIAL)
|
||||||
const result = await restoreFromNostr(LINKING_KEY, RELAYS, {transport})
|
const result = await restoreFromNostr(LINKING_KEY, RELAYS, {transport})
|
||||||
|
|
||||||
expect(result.found).toEqual(['notes', 'mints', 'settings'])
|
expect(result.found).toEqual(['notes', 'mints', 'settings'])
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
|
|
||||||
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
||||||
|
|
||||||
import {linkingPubKeyHex, saveLinkingKey} from './keys'
|
import {bytesToHex} from '@noble/hashes/utils.js'
|
||||||
|
|
||||||
|
import {deriveWalletMaterial, linkingPubKeyHex, saveWalletMaterial} from './keys'
|
||||||
import {applyBackup, buildBackup, parseBackupFile} from './storage'
|
import {applyBackup, buildBackup, parseBackupFile} from './storage'
|
||||||
import {readFundsDocument, writeFundsDocument} from './storage/bearers'
|
import {readFundsDocument, writeFundsDocument} from './storage/bearers'
|
||||||
import {addTrustedMint, readTrustedMints} from './trustedMints'
|
import {addTrustedMint, readTrustedMints} from './trustedMints'
|
||||||
@@ -16,6 +18,14 @@ const LINKING_KEY = new Uint8Array(32).fill(7)
|
|||||||
const OWNER_ID = linkingPubKeyHex(LINKING_KEY)
|
const OWNER_ID = linkingPubKeyHex(LINKING_KEY)
|
||||||
const KEY_A = '02' + 'aa'.repeat(32)
|
const KEY_A = '02' + 'aa'.repeat(32)
|
||||||
const KEY_B = '03' + 'bb'.repeat(32)
|
const KEY_B = '03' + 'bb'.repeat(32)
|
||||||
|
// full v2 material for this harness's fixed linking key (real BIP-32 cash
|
||||||
|
// root, same construction as the lifecycle test harness)
|
||||||
|
const MATERIAL = {
|
||||||
|
...deriveWalletMaterial(
|
||||||
|
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
||||||
|
),
|
||||||
|
linkingKeyHex: bytesToHex(LINKING_KEY),
|
||||||
|
}
|
||||||
|
|
||||||
type LockRequest = {
|
type LockRequest = {
|
||||||
readonly callback: () => unknown
|
readonly callback: () => unknown
|
||||||
@@ -59,7 +69,7 @@ const backup = (server: string, mintPubkey: string) => ({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const installProvenOwner = (): Promise<void> => saveLinkingKey(LINKING_KEY)
|
const installProvenOwner = (): Promise<void> => saveWalletMaterial(MATERIAL)
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.unstubAllGlobals()
|
vi.unstubAllGlobals()
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { createPinia, setActivePinia } from 'pinia';
|
|||||||
import { buildNoteUrl } from 'lnurlcash-kit';
|
import { buildNoteUrl } from 'lnurlcash-kit';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { deriveBearerAesKey, linkingPubKeyHex } from '@/lnurlcash/keys';
|
import {
|
||||||
|
deriveBearerAesKey,
|
||||||
|
deriveWalletMaterial,
|
||||||
|
saveWalletMaterial,
|
||||||
|
} from '@/lnurlcash/keys';
|
||||||
import { loadBearers, readEncryptedBearers, readFundsDocument } from '@/lnurlcash/storage';
|
import { loadBearers, readEncryptedBearers, readFundsDocument } from '@/lnurlcash/storage';
|
||||||
import { FUNDS_STORAGE_KEY } from '@/lnurlcash/storage/bearers';
|
import { FUNDS_STORAGE_KEY } from '@/lnurlcash/storage/bearers';
|
||||||
import { WalletOwnerMismatchError } from '@/lnurlcash/storage/currentOwner';
|
import { WalletOwnerMismatchError } from '@/lnurlcash/storage/currentOwner';
|
||||||
@@ -11,7 +15,14 @@ import { stubLocalStorage } from '@/lnurlcash/test-utils';
|
|||||||
import type { NewBearer } from '@/lnurlcash/types';
|
import type { NewBearer } from '@/lnurlcash/types';
|
||||||
import { useWalletStore } from './wallet';
|
import { useWalletStore } from './wallet';
|
||||||
|
|
||||||
const OTHER_OWNER_ID = linkingPubKeyHex(new Uint8Array(32).fill(9));
|
// a full v2 material for the successor owner: real BIP-32 cash root, fixed
|
||||||
|
// other linking key (same construction as the lifecycle test harness)
|
||||||
|
const OTHER_MATERIAL = {
|
||||||
|
...deriveWalletMaterial(
|
||||||
|
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
||||||
|
),
|
||||||
|
linkingKeyHex: '09'.repeat(32),
|
||||||
|
};
|
||||||
|
|
||||||
// fund commits REQUIRE Web Locks now; tests that don't care about lock
|
// fund commits REQUIRE Web Locks now; tests that don't care about lock
|
||||||
// timing install a present-but-non-serializing fake
|
// timing install a present-but-non-serializing fake
|
||||||
@@ -28,19 +39,13 @@ const note = (secret: string): NewBearer => ({
|
|||||||
verified: true,
|
verified: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// a second tab installing its own wallet: the saved-key record flips to the
|
// a second tab installing its own wallet: the successor lifecycle writes its
|
||||||
// successor owner. Deliberately NO storage event is dispatched - the fence
|
// v2 wallet-material record and deletes the legacy linking-key record, so the
|
||||||
// must not depend on the wakeup arriving first.
|
// saved-key owner flips to the successor. Deliberately NO storage event is
|
||||||
const replacePersistedOwner = (): void => {
|
// dispatched - the fence must not depend on the wakeup arriving first.
|
||||||
localStorage.setItem(
|
const replacePersistedOwner = async (): Promise<void> => {
|
||||||
'sattle_linking_key',
|
localStorage.removeItem('sattle_linking_key');
|
||||||
JSON.stringify({
|
await saveWalletMaterial(OTHER_MATERIAL);
|
||||||
enc: false,
|
|
||||||
value: '09'.repeat(32),
|
|
||||||
ownerId: OTHER_OWNER_ID,
|
|
||||||
version: 1,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type LockRequest = {
|
type LockRequest = {
|
||||||
@@ -90,7 +95,7 @@ describe('stale-owner fencing of fund commits', () => {
|
|||||||
|
|
||||||
// When another tab replaced the wallet and this (still unlocked) tab
|
// When another tab replaced the wallet and this (still unlocked) tab
|
||||||
// tries to commit a fund change
|
// tries to commit a fund change
|
||||||
replacePersistedOwner();
|
await replacePersistedOwner();
|
||||||
await expect(
|
await expect(
|
||||||
wallet.applyChangeset({ add: [note('bb')], markSpent: [existing.id] }, ownerFence),
|
wallet.applyChangeset({ add: [note('bb')], markSpent: [existing.id] }, ownerFence),
|
||||||
).rejects.toBeInstanceOf(WalletOwnerMismatchError);
|
).rejects.toBeInstanceOf(WalletOwnerMismatchError);
|
||||||
@@ -109,7 +114,7 @@ describe('stale-owner fencing of fund commits', () => {
|
|||||||
const ownerFence = wallet.captureOwnerFence();
|
const ownerFence = wallet.captureOwnerFence();
|
||||||
const [existing] = await wallet.addBearers([note('aa')], ownerFence);
|
const [existing] = await wallet.addBearers([note('aa')], ownerFence);
|
||||||
if (!existing) throw new Error('Expected the initial bearer.');
|
if (!existing) throw new Error('Expected the initial bearer.');
|
||||||
replacePersistedOwner();
|
await replacePersistedOwner();
|
||||||
|
|
||||||
// When the stale tab marks the note spent
|
// When the stale tab marks the note spent
|
||||||
await expect(wallet.markSpent(existing.id, ownerFence)).rejects.toBeInstanceOf(
|
await expect(wallet.markSpent(existing.id, ownerFence)).rejects.toBeInstanceOf(
|
||||||
@@ -140,7 +145,7 @@ describe('stale-owner fencing of fund commits', () => {
|
|||||||
ownerFence,
|
ownerFence,
|
||||||
);
|
);
|
||||||
await vi.waitFor(() => expect(locks.requests.length).toBeGreaterThan(0));
|
await vi.waitFor(() => expect(locks.requests.length).toBeGreaterThan(0));
|
||||||
replacePersistedOwner();
|
await replacePersistedOwner();
|
||||||
await locks.releaseNext();
|
await locks.releaseNext();
|
||||||
|
|
||||||
// Then the commit fails closed instead of writing stale-owner ciphertext
|
// Then the commit fails closed instead of writing stale-owner ciphertext
|
||||||
@@ -185,7 +190,7 @@ describe('stale-owner fencing of fund commits', () => {
|
|||||||
|
|
||||||
const updating = wallet.markSpent(existing.id, ownerFence);
|
const updating = wallet.markSpent(existing.id, ownerFence);
|
||||||
await vi.waitFor(() => expect(locks.requests.length).toBeGreaterThan(0));
|
await vi.waitFor(() => expect(locks.requests.length).toBeGreaterThan(0));
|
||||||
replacePersistedOwner();
|
await replacePersistedOwner();
|
||||||
await locks.releaseNext();
|
await locks.releaseNext();
|
||||||
|
|
||||||
await expect(updating).rejects.toBeInstanceOf(WalletOwnerMismatchError);
|
await expect(updating).rejects.toBeInstanceOf(WalletOwnerMismatchError);
|
||||||
@@ -206,7 +211,7 @@ describe('stale-owner fencing of fund commits', () => {
|
|||||||
|
|
||||||
const removing = wallet.removeNote(existing.id, ownerFence);
|
const removing = wallet.removeNote(existing.id, ownerFence);
|
||||||
await vi.waitFor(() => expect(locks.requests.length).toBeGreaterThan(0));
|
await vi.waitFor(() => expect(locks.requests.length).toBeGreaterThan(0));
|
||||||
replacePersistedOwner();
|
await replacePersistedOwner();
|
||||||
await locks.releaseNext();
|
await locks.releaseNext();
|
||||||
|
|
||||||
await expect(removing).rejects.toBeInstanceOf(WalletOwnerMismatchError);
|
await expect(removing).rejects.toBeInstanceOf(WalletOwnerMismatchError);
|
||||||
|
|||||||
Reference in New Issue
Block a user