mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
feat(funds): recover staged mutations
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import {describe, expect, it} from 'vitest'
|
||||
import {noteK1, noteSignature} from 'lnurlcash-kit'
|
||||
|
||||
import {ensureExactAmount} from './ops'
|
||||
import {requiredValue} from './test-utils'
|
||||
import {makeBearer, mint, noteUrl, secret, signatureKeysFor} from './ops.testHarness'
|
||||
import {fetchNoteInfo} from 'lnurlcash-kit'
|
||||
|
||||
// Landed split/merge outputs carry the mint's signature in their URL (sig
|
||||
// param), and earn verified:true only when that signature checks against a
|
||||
// trusted CURRENT or PREVIOUS key for the mint. Invalid, missing, or
|
||||
// wrong-key signatures - and mints with no trusted keys at all - keep the
|
||||
// staged outputs unverified; the money is tracked either way (the notes
|
||||
// exist at the mint), only the offline-verifiable badge is withheld.
|
||||
describe('landed mutation signature verification', () => {
|
||||
it('verifies a landed split against the current key and preserves its signature', async () => {
|
||||
const instance = await mint()
|
||||
const bearer = await makeBearer(instance, secret('a0'), 21_000)
|
||||
|
||||
const result = await ensureExactAmount([bearer], 5_000, {
|
||||
onCarve: () => undefined,
|
||||
mintSignatureKeys: signatureKeysFor(instance),
|
||||
})
|
||||
|
||||
expect(result.note.verified).toBe(true)
|
||||
const signature = noteSignature(result.note.url)
|
||||
expect(signature).not.toBeNull()
|
||||
expect(noteSignature(requiredValue(result.change).url)).not.toBeNull()
|
||||
// change stays unverified (its true post-fee value is unknown until a
|
||||
// refresh) even though its signature is preserved
|
||||
expect(result.change?.verified).toBe(false)
|
||||
})
|
||||
|
||||
it('verifies a landed merge against the current key and preserves its signature', async () => {
|
||||
const instance = await mint()
|
||||
const first = await makeBearer(instance, secret('a1'), 3_000)
|
||||
const second = await makeBearer(instance, secret('a2'), 4_000)
|
||||
|
||||
const result = await ensureExactAmount([first, second], 7_000, {
|
||||
onCarve: () => undefined,
|
||||
mintSignatureKeys: signatureKeysFor(instance),
|
||||
})
|
||||
|
||||
expect(result.note.verified).toBe(true)
|
||||
expect(noteSignature(result.note.url)).not.toBeNull()
|
||||
})
|
||||
|
||||
it('verifies against the previous key after the mint rotated its signing key', async () => {
|
||||
// the mint signs every output under its RETIRED key while advertising
|
||||
// the current one - exactly the rekey window a holder must tolerate
|
||||
const instance = await mint({
|
||||
previousPrivateKey: '22'.repeat(32),
|
||||
signWithPreviousKey: true,
|
||||
})
|
||||
const bearer = await makeBearer(instance, secret('a3'), 21_000)
|
||||
|
||||
const result = await ensureExactAmount([bearer], 5_000, {
|
||||
onCarve: () => undefined,
|
||||
mintSignatureKeys: signatureKeysFor(instance),
|
||||
})
|
||||
|
||||
expect(instance.state.previousPubkeys).toHaveLength(1)
|
||||
expect(result.note.verified).toBe(true)
|
||||
expect(noteSignature(result.note.url)).not.toBeNull()
|
||||
})
|
||||
|
||||
it('rejects verification when the previous key is not trusted', async () => {
|
||||
const instance = await mint({
|
||||
previousPrivateKey: '22'.repeat(32),
|
||||
signWithPreviousKey: true,
|
||||
})
|
||||
const bearer = await makeBearer(instance, secret('a4'), 21_000)
|
||||
|
||||
const result = await ensureExactAmount([bearer], 5_000, {
|
||||
onCarve: () => undefined,
|
||||
// only the current key is trusted - the signature under the retired
|
||||
// key must not verify
|
||||
mintSignatureKeys: () => [instance.state.pubkey],
|
||||
})
|
||||
|
||||
expect(result.note.verified).toBe(false)
|
||||
// the note and its signature are still tracked, recoverable by refresh
|
||||
expect(noteSignature(result.note.url)).not.toBeNull()
|
||||
const partK1 = requiredValue(noteK1(result.note.url))
|
||||
expect(instance.state.noteState(partK1)).toBe('outstanding')
|
||||
expect(instance.state.noteState(secret('a4'))).toBe('burned')
|
||||
})
|
||||
|
||||
it('keeps a wrong-key signature staged unverified', async () => {
|
||||
const instance = await mint()
|
||||
const bearer = await makeBearer(instance, secret('a5'), 21_000)
|
||||
const stranger = await mint({privateKey: '33'.repeat(32)})
|
||||
|
||||
const result = await ensureExactAmount([bearer], 5_000, {
|
||||
onCarve: () => undefined,
|
||||
// the only trusted key on file belongs to a DIFFERENT mint
|
||||
mintSignatureKeys: () => [stranger.state.pubkey],
|
||||
})
|
||||
|
||||
expect(result.note.verified).toBe(false)
|
||||
expect(noteSignature(result.note.url)).not.toBeNull()
|
||||
expect(instance.state.noteState(secret('a5'))).toBe('burned')
|
||||
expect(
|
||||
instance.state.noteState(requiredValue(noteK1(result.note.url))),
|
||||
).toBe('outstanding')
|
||||
})
|
||||
|
||||
it('keeps a landed split unsigned by the mint staged unverified and consumed', async () => {
|
||||
const instance = await mint({signatures: false})
|
||||
const k1 = secret('a6')
|
||||
const bearer = await makeBearer(instance, k1, 21_000)
|
||||
|
||||
const result = await ensureExactAmount([bearer], 5_000, {
|
||||
onCarve: () => undefined,
|
||||
mintSignatureKeys: signatureKeysFor(instance),
|
||||
})
|
||||
|
||||
// the mutation landed (the kit refused the missing signature, the
|
||||
// rescue probe proved the burn): the inputs are retired, the staged
|
||||
// outputs are tracked at exactly their staged secrets, unverified
|
||||
expect(result.consumed.map((entry) => entry.id)).toEqual([bearer.id])
|
||||
expect(result.note.verified).toBe(false)
|
||||
expect(noteSignature(result.note.url)).toBeNull()
|
||||
expect(instance.state.noteState(k1)).toBe('burned')
|
||||
const partK1 = requiredValue(noteK1(result.note.url))
|
||||
expect(instance.state.noteState(partK1)).toBe('outstanding')
|
||||
expect((await fetchNoteInfo(noteUrl(instance, partK1))).maxWithdrawable).toBe(5_000)
|
||||
})
|
||||
|
||||
it('never verifies a landed mutation without any trusted keys on file', async () => {
|
||||
const instance = await mint()
|
||||
const bearer = await makeBearer(instance, secret('a7'), 21_000)
|
||||
|
||||
const result = await ensureExactAmount([bearer], 5_000, {
|
||||
onCarve: () => undefined,
|
||||
// no key source at all: nothing may be trusted blindly
|
||||
})
|
||||
|
||||
expect(result.note.verified).toBe(false)
|
||||
expect(noteSignature(result.note.url)).not.toBeNull()
|
||||
expect(
|
||||
instance.state.noteState(requiredValue(noteK1(result.note.url))),
|
||||
).toBe('outstanding')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user