fix(server): don't start the check-in clock for an unconfirmed owner (#326) - #356
Merged
Merged
Conversation
…326) `vaults.owner_contact_verified_at` has existed since June and nothing has ever gated on it. An owner who never confirmed their email still got reminders, alarms, escalations and, at the end, handed their heir a claim link. Seven of sixteen mainnet vaults are in that state. One, `2e198479`, was rejected outright by the relay with a 550. Another, `4a7aaf77`, ran a reminder, an alarm, three escalations and a claim link with every row reading `sent` and, quite possibly, nobody at the other end of any of them. The cascade has exactly one entry point. Reminders, the ok->alarmed flip, escalations and heir contact all filter `status = 'ok'`, and only `activate_funded_vaults` puts a vault there. So the gate goes there: funding is necessary to start the clock, not sufficient. A funded vault whose owner email is unconfirmed stays `unfunded` and records one `activation_held` event. Only the email channel, and only with a sealed contact on file. There is no verification flow for sms or whatsapp and none for a legacy row with no ciphertext, so holding those would brick a vault permanently instead of prompting anyone. Not retroactive. Vaults already `ok` or `alarmed` keep running. Holding a live vault risks the unrecoverable failure (a dead owner's heir never inherits) to prevent a recoverable one (a live owner still holds their key, and heir contact is already gated on on-chain maturity). The seven existing ones want deciding one at a time against delivery evidence, not a blanket rule. `status: "unfunded"` now means two different things, so `VaultView` carries `activation_held` to tell them apart. Telling someone who just sent Bitcoin to go and send Bitcoin is how a person concludes their money is gone, so the dashboard swaps the fund card for one that says the money arrived and the clock is waiting on their email. `/health` gains `vaults_owner_unverified`, the number that would have shown this months ago. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VFkuFVtou7Rg1ipHLs6zCW
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #326.
The problem
vaults.owner_contact_verified_athas existed since June and nothing has ever gated on it. An owner who never confirmed their email still receives reminders, alarms and escalations, and their heir still gets a claim link at the end.Seven of sixteen mainnet vaults are in that state.
2e198479was rejected outright by the relay with550 Invalid 'to' field.4a7aaf77ran a reminder, an alarm, three escalations and a claim link, every row readingsent, and quite possibly nobody at the other end of any of them.The fix
The cascade has exactly one entry point. Reminders, the
ok -> alarmedflip, escalations and heir contact all filterstatus = 'ok', and onlyactivate_funded_vaultsputs a vault there. So the gate goes there.Funding is necessary to start the clock, not sufficient. A funded vault whose owner email is unconfirmed stays
unfundedand records oneactivation_heldevent. Confirming the email starts everything, so it is a hold, not a wall.Only the email channel, and only with a sealed contact on file. There is no verification flow for sms or whatsapp, and none for a legacy row with a channel but no ciphertext. Holding those would brick a vault permanently instead of prompting anyone. The condition mirrors the one already behind
VaultView::owner_contact_verified.Not retroactive. Vaults already
okoralarmedkeep running. The asymmetry: retroactively holding a live vault risks the unrecoverable failure, a dead owner's heir never inheriting, to prevent a recoverable one, since a live owner still holds their key andscheduler.rs:1434already gates heir contact on on-chain CSV maturity. And unverified is not the same as invalid: only one of the seven has hard evidence. Those seven want deciding one at a time against delivery data, not a blanket rule.What the owner sees
status: "unfunded"now means two different things: waiting for coins, and coins are here and waiting for you. Telling someone who has just sent Bitcoin to go and send Bitcoin is how a person concludes their money is gone.So
VaultViewcarriesactivation_held, and the dashboard swaps the fund card for one that says the money arrived, it is safe, only they can spend it, and one tap in an email starts the plan. Deliberately not an error tone: nothing is wrong and nothing is at risk.The existing confirm-email banner's stakes line changes too. It used to say a missed reminder "can trigger the inheritance by accident", which is now backwards; the clock is stopped, so nothing starts and nobody is contacted.
Operator visibility
/healthgainsvaults_owner_unverifiednext to the existingvaults_heir_unverified. That is the number that would have surfaced this months ago without a DB query.Testing
Four new scheduler tests:
unfunded, records the hold, and does not recordfundedcargo test -p ghostkey-server310 passed.RUSTFLAGS=-D warnings cargo clippy --workspace --all-targetsclean,cargo fmt --checkclean. Web:tsc,eslint --max-warnings=0, 134 vitest tests.Worth knowing before merging
This changes signet testing too. A tester who funds a vault and doesn't confirm their email now has an inert vault, which is the point, but it will look like a bug the first time it happens.
The scheduler does not re-send the verification email while holding. The dashboard has a resend with a cooldown, and re-mailing a possibly-invalid address on a timer is a good way to hurt sending reputation. Worth revisiting if held vaults pile up.
Follow-up
Holding the cascade on a negative delivery verdict rather than an unset flag. That is evidence rather than an unenforced boolean, so it carries none of the objection to retroactivity above, and it would have caught
2e198479automatically.🤖 Generated with Claude Code
https://claude.ai/code/session_01VFkuFVtou7Rg1ipHLs6zCW