Files
bal-electrum-plugin/docs/inheritance-options.md
svatantrya 1e80f7a0e0 docs: align all markdown docs with current code (v0.6.1)
- CHANGELOG: add entries #47 (is_selected/is_valid fee bounds, extremes allowed) and #48 (merge_will crash fix on missing date_to_check)
- HANDOFF: Electrum 4.7.2 and 4.8.0, current layout (qt.py, plugin.py, wallet_util/, docs/, bal_cli.py, 36-file ZIP), runtime/lint venv paths, standalone-script tests (427 collected), direct-to-main workflow, version history up to v0.6.1
- README: drop VERSION from layout, correct anticipate/expire/postpone behavior, update test commands
- docs/manual/README.md: fix Tools menu label to Will-Executors, add BASIC/ADVANCED notes, extend chromatic status table (Partially signed, Updated)
- docs/inheritance-options.md: add PARTIALLY_SIGNED status/transition/colour, version note to v0.6.1
- docs/README.md: make HTML-hosting instructions host-agnostic
- bal/README.md: Prepare button lives on the WILL tab
2026-08-01 23:08:07 -04:00

18 KiB
Raw Blame History

BAL — Inheritance Options Guide

How the Bitcoin After Life Electrum plugin reacts to every change you can make to your will: changing the date (earlier / later), adding or removing an heir, changing percentages, fees or willexecutors — and what happens to the transactions held by the willexecutor servers.

This guide describes the actual behaviour of the code (core/will.pyis_will_valid / check_willexecutors_and_heirs and gui/qt/window.pybuild_inheritance_transaction). It is meant for end users and for anyone who wants to understand the onchain consequences of each action.


1. The mental model in one paragraph

Your will is a tree of presigned Bitcoin transactions. Each leaf transaction sends your coins to your heirs and is timelocked (nLockTime) so it can only be broadcast after a future date/block. A copy of each signed transaction is handed to one or more willexecutor servers. While you are alive you periodically prove you are alive (the CheckAlive threshold). When you change anything in your will, BAL must decide between three outcomes:

  1. Do nothing — the will is still coherent.
  2. Rebuild (reprepare + resign, no onchain cost) — the will changed but nothing dangerous is already committed.
  3. Invalidate onchain first (costs a real Bitcoin fee) — a previously signed/sent transaction must be neutralised by spending its inputs, before a new will can safely replace it.

The whole point of rule 3 is safety: a willexecutor must never be able to broadcast an old transaction that would execute your inheritance too early.


2. Transaction states (status flags)

Every will item (WillItem) carries a set of boolean status flags. The most important ones:

Status Meaning Set when
VALID The item is the current, usable plan default True; cleared by INVALIDATED/REPLACED/CONFIRMED/MEMPOOL
COMPLETE (Signed) The transaction has been signed after you press Sign
PARTIALLY_SIGNED Only some of the required signatures are present a multisig will after a partial sign (cleared by COMPLETE)
PUSHED The signed tx was sent to the willexecutor(s) after Broadcast to executors
CHECKED The willexecutor confirmed it holds the tx after a successful server Check (implies PUSHED)
CHECK_FAIL The server check failed a queried executor did not return the tx
PUSH_FAIL Sending to the executor failed cleared when PUSHED becomes true
CONFIRMED The tx is mined onchain seen onchain with height > 0
MEMPOOL The tx is in the Electrum mempool (height 0) seen onchain, not yet mined (was named PENDING before v0.3.4)
INVALIDATED Its inputs were spent → it can never confirm invalidation tx created / inputs gone
REPLACED Superseded by a child tx with earlier locktime a replacing child was found
ANTICIPATED Its locktime was anticipated by 1 day vs a preexisting tx with the same heirs set_anticipate (the tx stays VALID)
UPDATED Replaced by a new tx that keeps the same locktime and same heirs a samelocktime replacement was applied (the tx stays VALID)
EXPIRED Its locktime is already in the past relative to the check date check_will_expired

Flag transitions enforced by set_status (the safety rules baked in the code):

  • Setting INVALIDATED / REPLACED / CONFIRMED / MEMPOOL → clears VALID.
  • Setting ANTICIPATEDkeeps VALID (anticipating only moves the locktime 1 day earlier; the tx stays valid).
  • Setting UPDATEDkeeps VALID (same locktime + same heirs; the tx stays valid).
  • Setting CONFIRMED / MEMPOOL → clears INVALIDATED.
  • Setting PUSHED → clears PUSH_FAIL and CHECK_FAIL.
  • Setting CHECKED → implies PUSHED (and clears PUSH_FAIL).
  • Setting COMPLETE → clears PARTIALLY_SIGNED.

How states map to row colour in the list

The colour is decided by status_color (in gui/qt/theme.py). The list below is in the exact priority order used by the code — the first matching status wins:

Priority State Colour Hex
1 INVALIDATED orange #f87838
2 REPLACED pink #ff97e9
3 UPDATED light violet #b266b2
4 CONFIRMED grey #bfbfbf
5 MEMPOOL yellow #ffce30
6 CHECK_FAIL (and not CHECKED) red #e83845
7 CHECKED green #8afa6c
8 PUSH_FAIL red #e83845
9 PUSHED teal #73f3c8
10 PARTIALLY_SIGNED amber #ffb347
11 COMPLETE (signed, not yet pushed) blue #2bc8ed
none of the above (e.g. plain VALID, prepared) default white #ffffff

Note (v0.3.3 fix): a will that is signed but not yet broadcast (COMPLETE and not PUSHED) is not queried on the server, so it stays blue instead of turning red. Only PUSHED wills are serverchecked.


3. The decision flow

When you press Prepare (or on the periodic Check, or when Electrum closes), BAL runs is_will_valid. Depending on what it finds it raises a specific exception, and each exception maps to one action.

📊 A styled version of this guide (with a live diagram) is in inheritance-options.html — open it via GitHub Pages or download and open it in any browser.

The static diagram below renders everywhere; the Mermaid block after it renders live on GitHub.

BAL inheritance change decision flow

flowchart TD
    A([You change something & press Prepare / Check]) --> B{Heirs defined?}
    B -- No --> Z1[/Show: "Heirs are not defined" — stop/]
    B -- Yes --> C{Check-Alive threshold<br/>in the future?}
    C -- No, it's in the past --> INV1[[Invalidate on-chain<br/>CheckAliveError]]
    C -- Yes --> D{New delivery date<br/>already in the PAST?}

    D -- "Yes (date is now expired)" --> INV2[[Invalidate on-chain FIRST<br/>WillExpired]]
    D -- "No (date still in the future)" --> AN{Did you move the date<br/>EARLIER (anticipate)?}

    AN -- "Yes (anticipate) — signed or not" --> R1[[Rebuild only<br/>no on-chain cost<br/>NEVER invalidates]]
    AN -- "No" --> F{Will-executor / fee /<br/>heirs unchanged?}
    F -- "Fee changed" --> R2[[Rebuild<br/>TxFeesChanged]]
    F -- "Will-executor changed/absent" --> R3[[Rebuild<br/>WillExecutorNotPresent / Change]]
    F -- "Heir added or removed,<br/>% or address changed" --> G{Is it a POSTPONE of an<br/>already signed/sent tx?}

    G -- "Yes (date later + signed/sent)" --> INV3[[Invalidate on-chain FIRST,<br/>then rebuild — WillPostponed]]
    G -- "No (never signed, or pure heir/% change)" --> R4[[Rebuild only<br/>HeirNotFound / HeirChange]]

    F -- "Nothing changed" --> OK([Will still coherent — do nothing])

    R1 --> SIGN
    R2 --> SIGN
    R3 --> SIGN
    R4 --> SIGN
    SIGN([Re-sign the new transactions]) --> PUSH([Broadcast to will-executors])

    INV1 --> SB
    INV2 --> SB
    INV3 --> SB
    SB([Sign & broadcast the INVALIDATION tx on-chain]) --> WAIT{Invalidation<br/>confirmed?}
    WAIT -- "Yes" --> REBUILD([Press Prepare again → build the new will])
    REBUILD --> SIGN

4. Every option, explained

4.1 Changing the CheckAlive date / heir locktime

The locktime is the future moment from which a transaction becomes spendable by the heir. BAL compares the requested locktime against the locktime frozen inside the alreadysigned transaction (w.tx.locktime), which is exactly what the willexecutors hold — not the inmemory copy.

You do… Tx already signed/sent? Result Onchain fee?
Move date LATER (postpone) No (never signed) Plain rebuild (HeirNotFound fallthrough) No
Move date LATER (postpone) Yes Invalidate first, then rebuild (WillPostponed) Yes
Move date EARLIER, still in the future (anticipate) any (signed or not) Plain rebuild with the new earlier locktime (HeirNotFound fallthrough) — never an onchain invalidation No
Move date EARLIER into the past (new date already passed) Will is genuinely expiredinvalidate (WillExpired) Yes
CheckAlive threshold already passed Invalidate (CheckAliveError) Yes

Why postpone needs an onchain invalidation: the willexecutor still holds the old transaction with the earlier locktime. If you simply resigned a later one, a malicious or buggy executor could still broadcast the old one as soon as its earlier locktime is reached — executing your inheritance too soon. Spending the old transaction's inputs onchain makes the old tx unminable. The plugin tells you this explicitly and offers to build the invalidation tx.

Why anticipate (move earlier, still future) is NOT onchain: moving the delivery date earlier only makes the inheritance available sooner. There is no earlyexecution risk to protect against — on the contrary, the new plan is more restrictive than the old one. So the plugin simply rebuilds the transactions with the new, earlier locktime; no onchain invalidation and no Bitcoin fee are needed. This holds even if the will was already signed/sent: anticipating never invalidates.

This is the opposite of postpone: postpone (later date) is dangerous because the willexecutor could still broadcast the earlier old tx; anticipate (earlier date) is safe because the old, later tx can only ever execute after the new one.

Note — only a date that lands in the past invalidates. "Move date earlier" only triggers an onchain invalidation in the separate case where the new date is already in the past relative to the CheckAlive date: then the will is truly expired (WillExpired) and must be invalidated, exactly like a CheckAlive threshold that has already passed.

4.2 Adding an heir

check_willexecutors_and_heirs walks every heir in the current set; an heir present in heirs but not yet found in the will raises HeirNotFoundException.

  • Result: rebuild (reprepare + resign).
  • Onchain fee: Nounless the will being changed was already signed/sent and the change also moves a locktime later (then the postpone rule in 4.1 applies).

4.3 Removing an heir

The will still carries an heir that is no longer in your current heirs set → HeirNotFoundException (the removedheir branch).

  • Result: rebuild, so the removed heir disappears from the new transactions.
  • Onchain fee: No for a will that was only prepared. If the old will was already signed/sent, you must invalidate it onchain first (same safety reasoning as a postpone), then rebuild.

v0.3.2 fix: removing an heir is now correctly detected on Check and on Electrum close, not only on Prepare.

4.4 Changing an heir's percentage or address

If the stored heir [address, amount/percentage] differs from the current one, the will is no longer coherent → it is treated like an heir change (HeirChangeException / HeirNotFound).

  • Result: rebuild with the new amounts.
  • Onchain fee: No (unless the old will was signed/sent → invalidate first).

Reminder shown by the plugin: “In the inheritance process the entire wallet is always fully emptied” — the amounts across all heirs must add up so that the whole spendable balance is distributed; otherwise an AmountException warns you to adjust.

Watch the dust limit. If a share is so small that it falls below Bitcoin's dust limit, that heir is skipped (see §4.8). Splitting a tiny balance among many heirs, or giving an heir a very small percentage, can produce dust shares.

4.5 Changing the transaction fee (sat/byte)

Each will item stores the fee rate it was built with. A different rate raises TxFeesChangedException.

  • Result: rebuild at the new fee rate.
  • Onchain fee: No to rebuild (you only pay when the inheritance — or an invalidation — is actually broadcast onchain).

4.6 Changing or removing a willexecutor

  • A selected willexecutor that the will does not reference raises WillExecutorNotPresent.

  • A willexecutor whose details changed raises WillexecutorChangeException.

  • Running with “no willexecutor” but no backup transaction raises NoWillExecutorNotPresent.

  • Result: rebuild and redistribute to the (new) executor set.

  • Onchain fee: No to rebuild. The new signed transactions are simply pushed to the new/updated executors; the old executor will eventually fail its own check and drop the obsolete tx.

4.7 Nothing changed

If heirs, percentages, fees, executors and locktimes all still match the signed transactions, is_will_valid returns True and nothing happens — your will stays exactly as broadcast to the executors.

4.8 An heir's share is below the dust limit (DUST)

Bitcoin refuses to create outputs that are too small to be worth spending — the socalled dust limit (the wallet's dust_threshold). BAL resolves every heir's final amount when it builds the will (Heirs.prepare_listsfixed_percent_lists_amount / normalize_perc) and compares each share against that limit.

  • Some heirs are dust, others are valid → build continues. Each dust heir is skipped (its share would be unspendable). The build proceeds with the remaining valid heirs, and the build report lists every excluded heir as “… is DUST excluded (amount below dust limit)” so you can see who was left out. Behaviour is unchanged: the will is still prepared, signed and checked with the payable heirs.

  • EVERY heir is dust → the build is blocked. If all heirs' shares are below the dust limit, the inheritance would pay nobody (only the change and the willexecutor fee). Previously such an empty will was still built, signed, checked and shown in the list. As of v0.4.7 BAL refuses it: Heirs.prepare_lists raises HeirAmountIsDustException, and the Building Will window stops with a clear red message: “All heirs' shares are below the dust limit: the inheritance cannot be created. Increase the amounts or reduce the number of heirs.” Nothing is built, signed, checked or added to the list.

When does this happen? Typically with a very small wallet balance split among percentage heirs (e.g. each heir ends up with a few hundred sats), or when every fixed amount is set below the dust limit. The fix is exactly what the message says: raise the perheir amounts, or reduce the number of heirs.

Note (v0.4.7): the dust check lives in prepare_lists, which sees all heirs across all locktimes with their final amounts. This is deliberate: a single transaction only ever covers the earliest locktime, so checking there would wrongly block a will whose later dates still have valid heirs.

  • Onchain fee: none — this is a prebuild safety check; nothing is broadcast.

5. What happens on the willexecutor servers

Your action Effect on the servers
Prepare (rebuild) Nothing yet — new txs exist only locally until you Sign + Broadcast.
Sign Still local; tx becomes COMPLETE (blue).
Broadcast to executors The signed txs are uploaded; items become PUSHED.
Check Each PUSHED will is queried; success → CHECKED (green), failure → CHECK_FAIL (red).
Invalidate (onchain) You spend the committed inputs on the Bitcoin network. Once confirmed, the executor's stored tx can no longer be mined; on the next check it is dropped / shown invalidated.
Rebroadcast a new will Executors replace the obsolete copy with the new signed tx.

A row turning red (CHECK_FAIL) after a Check means a willexecutor that should hold your transaction did not return it — reBroadcast, or rebuild, to fix it. A row that is merely blue is signedbutnotyetsent and is perfectly normal.


6. Quick reference — does it cost a Bitcoin fee?

Change Rebuild? Onchain invalidation (real fee)?
Add heir (will only prepared)
Remove heir (will only prepared)
Change % / address (only prepared)
Change fee rate
Change / remove willexecutor
Move date earlier, still in the future (anticipate) — signed or not
Move date earlier into the past (new date already passed) after yes
Move date later (postpone) — will signed/sent after yes
Move date later (postpone) — will only prepared
CheckAlive threshold already in the past after yes
Any change to an already signed/sent will that postpones it or expires it after yes (invalidate first)
Nothing changed
Every heir's share below the dust limit (alldust) — build blocked (§4.8)

7. Golden rules

  1. Before it's signed, changing anything is free — just Prepare again.
  2. After it's signed/sent, only postponing the date (moving it later), or letting it expire (a date now in the past), requires an onchain invalidation first (a small Bitcoin fee) so an old transaction can never be executed early. Anticipating (moving the date earlier, still in the future) never invalidates — it is just a free rebuild.
  3. Always finish with Sign → Broadcast → Check so the willexecutors hold the current plan (green), not an obsolete one.
  4. The wallet is always fully emptied by the inheritance, so heir amounts must add up.
  5. Mind the dust limit. A share below Bitcoin's dust limit is skipped; if every heir is dust the build is blocked with a clear message (§4.8) — raise the amounts or use fewer heirs.

This document reflects the current BAL plugin (v0.6.1). Behaviour is derived directly from core/will.py, core/heirs.py and gui/qt/window.py.