Documentation

Bitcoin meets Forth. Henceforth is a native iOS app that combines a Forth-2012 compliant interpreter with an integrated Bitcoin SV wallet — one stack-based language for arithmetic, scripting, and payments.

About

Henceforth is built and maintained by Henry Hudson at Henceforth Bitcoin Limited.

Source code

github.com/henryhudson/FORTHapp

Running the project requires Xcode on any iPhone or iPad. Optimisation for all Apple platforms is in progress. The repository is private at the time of writing — get in touch if you would like access.

Support

Follow @henceforth_app on Twitter

Goals

Achieved

  • Bitcoin (BSV) integration — full wallet with HD key derivation (BIP-44 and Type42), transaction building, ARC broadcasting with failover, SPV verification, Paymail support, and 140+ Bitcoin Script opcodes including the Chronicle upgrade.
  • Block headers — locally chain-linked header store anchored to a hardcoded checkpoint, auto-synced from chain tip on every launch via a WebSocket history stream. SPV Merkle proof verification is bounded to the chain-validated range — no silent network fallback for missing headers.
  • SwiftBSV — fully integrated for transactions, Script building, BIP-32/39 key management, and ECDSA signing.
  • OPCodes — all standard and Chronicle-restored opcodes available as FORTH words. Script Recording mode assembles Bitcoin Scripts from the terminal.
  • Files — full CRUD for .fs and .forth files with iCloud backup. Script files identified by SCRIPT-BEGIN / SCRIPT-END markers.
  • Colour customisation — foreground and background colour pickers, keyboard selection, and Dynamic Type support throughout.
  • Forth-2012 compliance — 133 / 133 CORE words implemented, verified by the Hayes test suite.

Get people coding

Provide a simple interface for the FORTH programming language, making it easy for people to learn to code.

Future

  • CRDT (Conflict-free Replicated Data Type) system using the timestamp server for user-defined words.
  • Trigonometry and calculus extensions.

Reference

Everything in FORTH is a word separated by spaces. That's all there is to the syntax. It's so simple.

Compile Words

One of the great things about FORTH is its ability to extend the compiler during run time.

:
Puts the user into compile mode where they can start to define their own word.
;
Exits compile mode so that word interpretation can again occur.
recurse
Calls the word being executed.
exit
Exits the current word.
immediate
Marks the most recently defined word as immediate, causing it to execute during compilation rather than being compiled.
postpone
Forces compilation of the next word, even if it is marked IMMEDIATE. Used inside definitions to defer execution of immediate words.
value(x --)
Creates a named value that can be read by name and changed with TO. Usage: 42 value myval.
to(x --)
Change the value of a word created with VALUE. Usage: 99 to myval.
does>( -- )
Defines the runtime behaviour of a CREATE'd word. Everything after DOES> runs when the created word is executed, with the data-field address on the stack. Usage: : array create cells allot does> swap cells + ;.
include( -- )
Loads and runs a FORTH file. Usage: include maths.fs.

Meta Programming

'( -- xt)
Gets the execution token of the named word. Used to get a reference to a word for later execution.
[']( -- xt)
Compile-time version of ' (tick). Compiles the execution token of a word into the current definition.
execute(i*x xt -- j*x)
Executes the word whose execution token is on the stack. The i*x/j*x notation acknowledges that the executed word may consume and produce arbitrary values; only the xt itself is consumed by EXECUTE.
literalCompile: (x -- ) Run-time: ( -- x)
At compile time, compiles a numeric literal into the current definition; at run time of the containing word, pushes that literal onto the stack. Used inside IMMEDIATE words to inject computed constants into the word being defined.
state( -- addr)
Returns the address of the STATE variable. STATE is 0 when interpreting, -1 when compiling.
[(--)
Switches from compilation mode to interpretation mode.
](--)
Switches from interpretation mode to compilation mode.
evaluate(c-addr u --)
Interprets the string at c-addr of length u as if it were typed at the terminal.
find(c-addr -- c-addr 0 | xt 1 | xt -1)
Searches the dictionary for the counted string at c-addr. Returns xt and 1 (immediate) or -1 (normal), or c-addr and 0 if not found.
>body(xt -- a-addr)
Returns the data-field address of a CREATE'd word given its execution token.
>in( -- a-addr)
Returns the address of the variable holding the current input parse position.
>number(ud1 c-addr1 u1 -- ud2 c-addr2 u2)
Converts characters at c-addr to a number using the current BASE. Stops at the first non-convertible character.

Arithmetic

Arithmetic is done using reverse Polish notation, so the numbers are required on the stack before the arithmetic operator can be used.

+(n1 n2 -- sum)
Takes the top two items from the stack and returns their sum.
-(n1 n2 -- difference)
Takes the top two items from the stack and returns their difference.
*(n1 n2 -- product)
Takes the top two items from the stack and returns their product.
/(n1 n2 -- quotient)
Takes the top two items from the stack and returns their quotient.
*/(n1 n2 n3 -- result)
result = (n1*n2/n3). Multiplies n1 by n2, then divides by n3.
mod(n1 n2 -- modulus)
Divides the first by the second; the remainder is added to the stack.
/mod(n1 n2 -- modulus quotient)
Returns the modulus and the quotient.
abs(n -- |n|)
Absolute value of the top item on the stack.
negate(n -- -n)
Negates the top item.
max(n1 n2 -- max)
Returns the greater of two values.
min(n1 n2 -- min)
Returns the smaller of two values.
1+(n -- n+1)
Increments the top item.
1-(n -- n-1)
Decrements the top item.
2+(n -- n+2)
Adds 2 to the top of the stack. Henceforth extension — equivalent to 2 +.
2-(n -- n-2)
Subtracts 2 from the top of the stack. Henceforth extension.
2*(n -- n*2)
Doubles the top item.
2/(n -- n/2)
Halves the top item.
sumStack(n1 ... n -- sum)
Sums every value on the stack into a single result. Henceforth extension — not in Forth-2012.
*/mod(n1 n2 n3 -- remainder quotient)
Multiplies n1 by n2, then divides by n3. Returns both the remainder and the quotient.

Double-Cell Arithmetic

Double-cell words operate on 64-bit values represented as two stack items (high cell on top). These are required by the Forth-2012 CORE standard for intermediate precision in calculations.

s>d(n -- d)
Sign-extends a single-cell number to a double-cell number. Required before FM/MOD or SM/REM.
m*(n1 n2 -- d)
Signed multiply of n1 by n2, producing a double-cell result. No overflow possible.
um*(u1 u2 -- ud)
Unsigned multiply of u1 by u2, producing an unsigned double-cell result.
fm/mod(d n1 -- n2 n3)
Floored division of double-cell d by n1. The quotient n3 rounds toward negative infinity. The remainder n2 has the same sign as the divisor.
sm/rem(d n1 -- n2 n3)
Symmetric division of double-cell d by n1. The quotient n3 rounds toward zero. The remainder n2 has the same sign as the dividend.
um/mod(ud u1 -- u2 u3)
Unsigned division of unsigned double-cell ud by u1. Returns remainder u2 and quotient u3.

Numeric Output

<#(--)
Initialises pictured numeric output conversion.
#(ud1 -- ud2)
Converts one digit from the double number and adds it to the pictured numeric output buffer.
#s(ud -- 0 0)
Converts all remaining digits from the double number until it becomes zero.
#>(xd -- c-addr u)
Completes pictured numeric conversion and returns the address and length of the formatted string.
hold(char --)
Adds a character to the beginning of the pictured numeric output buffer.
sign(n --)
If the number is negative, adds a minus sign to the pictured numeric output.
.r(n1 n2 --)
Displays a number right-aligned in a field of n2 characters width.
u.(u --)
Displays an unsigned number followed by a space.
u.r(u n --)
Displays an unsigned number right-aligned in a field of n characters width.

Comparison

=(n1 n2 -- f)
Returns the flag for n1 equal to n2.
<>(n1 n2 -- f)
Returns the flag for n1 not equal to n2.
<(n1 n2 -- f)
True if n1 is less than n2.
>(n1 n2 -- f)
True if n1 is greater than n2.
&&(n1 n2 -- f)
Logical AND — Henceforth extension. Returns -1 if both values are non-zero, 0 otherwise.
||(n1 n2 -- f)
Logical OR — Henceforth extension. Returns -1 if either value is non-zero, 0 otherwise.
0=(n -- f)
True if n is zero.
0<(n -- f)
True if n is less than zero.
0>(n -- f)
True if n is greater than zero.
u<(u1 u2 -- f)
Unsigned comparison. True if u1 is less than u2 when both are treated as unsigned.

Bitwise

and(x1 x2 -- x3)
Bitwise AND of the top two values (Forth-2012 6.1.0720). When both operands are well-formed flags (0 or -1), bitwise AND produces logical AND — so you don't need a separate logical operator if you keep flags well-formed.
or(x1 x2 -- x3)
Bitwise OR of the top two values (Forth-2012 6.1.1980).
xor(x1 x2 -- x3)
Bitwise XOR of the top two values (Forth-2012 6.1.2490).
invert(n -- ~n)
Bitwise inversion. Flips every bit of the top value on the stack.
rshift(n1 n2 -- n3)
Returns n1 bitwise right-shifted by n2.
lshift(n1 n2 -- n3)
Returns n1 bitwise left-shifted by n2.

Stack Operators

.s( -- )
Displays the current stack to the terminal.
.(n -- )
Pops the top value to the terminal.
dup(n -- n n)
The top value is duplicated and both the original and copy are returned.
?dup(n -- 0 | n n)
Duplicates the top item only if it is non-zero.
nip(n1 n2 -- n2)
Drops the first item below the top of the stack.
swap(n1 n2 -- n2 n1)
Swaps the top two values.
drop(n -- )
Discards the top item.
2dup(n1 n2 -- n1 n2 n1 n2)
Duplicates the top two values.
over(n1 n2 -- n1 n2 n1)
Copies the second value and pushes it on top.
pick(x_u ... x_1 x_0 u -- x_u ... x_1 x_0 x_u)
Pops u and pushes a copy of the u-th item from the stack (0-indexed from top: 0 PICK is DUP, 1 PICK is OVER).
rot(n1 n2 n3 -- n2 n3 n1)
Rotates the top three values left.
-rot(n1 n2 n3 -- n3 n1 n2)
Rotates the top three values right.
tuck(n1 n2 -- n2 n1 n2)
Copies the top value and places it before the second value (third deep).
2drop(n1 n2 -- )
Discards the top two values.
2swap(n1 n2 n3 n4 -- n3 n4 n1 n2)
Reverses the order of the top two pairs.
2over(n1 n2 n3 n4 -- n1 n2 n3 n4 n1 n2)
Copies items 3 and 4 deep onto the top.
reverseStack(n1 n2 ... nlast -- nlast ... n2 n1)
Reverses the order of every item on the stack. Henceforth extension — not in Forth-2012.
depth( -- n)
Returns the number of items currently on the data stack before DEPTH was executed.
roll(xu xu-1 ... x0 u -- xu-1 ... x0 xu)
Rotates u+1 items on the stack. The u-th item is moved to the top.
stackCount( -- n)
Pushes the number of items currently on the stack. Novice-friendly Henceforth alias for depth (Forth-2012 6.1.1200).

Return Stack

>r(x -- ) ( R: -- x)
Pops the parameter stack and pushes onto the return stack.
r>( -- x) ( R: x -- )
Pops the return stack and pushes onto the parameter stack.
r@( -- x) ( R: x -- x)
Copies the top of the return stack to the parameter stack.
i( -- x)
The innermost DO LOOP's index. Copies the top of the return stack.
i'( -- x)
The next-outer loop's index in nested DO LOOPs. Copies the second-from-top of the return stack. In Henceforth i' and j return the same value.
j( -- x)
The next-outer loop's index in nested DO LOOPs (Forth-2012 6.1.1730).
nth(u -- x)
Pops u and pushes the u-th item from the return stack (0-indexed from top). Henceforth extension.

Decision

if(x -- )
Pops one cell. If non-zero, executes the words up to ELSE or THEN. If zero, skips over them.
else( -- )
The optional false branch of an IF...THEN. No runtime stack effect — the flag was already consumed by IF.
then( -- )
Denotes the end of the IF...THEN statement. No runtime stack effect.
not(x -- f)
Synonym for 0= (returns -1 if x is 0, 0 otherwise). Retained for FIG-FORTH compatibility. Forth-2012 dropped NOT because pre-2012 implementations disagreed on whether it meant 0= (logical) or INVERT (bitwise). Henceforth's NOT is the logical version. Prefer 0= or INVERT in new code.
within(n1 n2 n3 -- f)
True if n2 is between n1 and n3.
true( -- -1)
Pushes the true flag (-1, all bits set). Per Forth-2012 standard.
false( -- 0)
Pushes the false flag (0).
case( -- )
Begins a CASE...OF...ENDOF...ENDCASE switch statement. The selector value must be on the stack.
of(x1 x2 -- | x1)
Compares the top of stack with the selector. If equal, drops both and executes the clause. If not, skips to ENDOF.
endof( -- )
Ends an OF clause. Branches to after ENDCASE.
endcase(x -- )
Ends a CASE statement. Drops the selector value if no OF clause matched.
catch(i*x xt -- j*x 0 | i*x n)
Executes the word referred to by xt while protecting the data stack. Returns 0 on success. If THROW is called during execution, restores the stack to its pre-CATCH depth and returns the thrown error code n.
throw(k*x n -- k*x | i*x n)
If n is 0, does nothing. Otherwise unwinds the stack to the nearest CATCH, leaving n as the result. The exception code is implementation-defined; -1 is conventional for ABORT, -2 for ABORT".

Loop

do(n1 n2 --)
n1 is endValue, n2 is startValue. Repeats words between DO and LOOP from startValue until endValue is met.
loop( -- )
Ends loop.
+loop(n1 -- )
Loops in n1 increments.
every(n1 -- )
Performs the following word every n1 seconds until STOP is called. Henceforth scheduling extension.
stop( -- )
Stops the EVERY loop. Henceforth extension — pairs with EVERY only.
begin( -- )
Marks the start of a BEGIN...UNTIL or BEGIN...WHILE...REPEAT loop.
while(x -- )
Tests a condition within a BEGIN...REPEAT loop. Pops one cell; if zero, exits the loop.
repeat( -- )
Marks the end of a BEGIN...WHILE...REPEAT loop and jumps back to BEGIN.
until(x -- )
Pops one cell; if zero, jumps back to BEGIN. If non-zero, falls through.
leave( -- )
Exits the current DO...LOOP or DO...+LOOP immediately. Must be used within a loop.
unloop( -- )
Discards the loop control parameters from the return stack. Used when exiting a loop early with LEAVE.
?do(n1 n2 --)
Like DO but skips the loop entirely if the start and end values are equal. Executes loop from n1 to n2-1.
again( -- )
Unconditional branch back to BEGIN. Creates an infinite loop (BEGIN...AGAIN). Use LEAVE or the Escape key to exit.

Base

Henceforth accepts integers as binary, octal, decimal and hexadecimal. Just change the base.

base( -- addr)
Returns the address of the BASE variable. Use BASE @ to read and n BASE ! to write. Supports bases 2 through 36.
decimal(--)
Sets BASE to 10. Forth-2012 6.1.1170.
binary(--)
Sets BASE to 2. Henceforth extension. Equivalent to 2 BASE !.
octal(--)
Sets BASE to 8. Henceforth extension. Equivalent to 8 BASE !.
hex(--)
Sets BASE to 16. Forth-2012 6.2.1660.

Pop-up

lineChart(--)
Displays a line chart representing the stack.
barChart(--)
Displays a bar chart representing the stack.
pieChart(--)
Displays a pie chart representing the stack with slices in proportion to each value's weight compared to the sum.

Constants & Variables

constant(n --)
Takes a value from the stack and a name; creates a constant of that value called by the name provided.
variable(--)
Creates a variable whose name will be the first string of characters following VARIABLE, initially zero.
!(n address --)
Stores a number to the address provided.
@(address -- n)
Gives the value from address.
?(address --)
Prints the value from address to the terminal.
c!(char addr --)
Stores a single character at the specified memory address.
c@(addr -- char)
Fetches a single character from the specified memory address and places it on the stack.
create(--)
Creates an array named by the first string of characters following CREATE.
,(n --)
Stores the top value as part of the array in the current data-space position.
allot(n --)
Allocates n cells of data space. Used after CREATE to allocate memory for arrays or data structures.
here( -- addr)
Returns the address of the next available data space location.
cells(n1 -- n2)
Converts a count to an address offset by multiplying by the cell size.
cell+(addr1 -- addr2)
Adds the size of one cell to an address, advancing it to the next cell boundary.
+!(n addr --)
Adds n to the value stored at the given address.
count(c-addr1 -- c-addr2 u)
Converts a counted string (length byte followed by characters) to an address and length suitable for TYPE.
bl( -- char)
Pushes the ASCII value for space character (32) onto the stack.
2@(addr -- x1 x2)
Fetches two consecutive cells from memory at the given address.
2!(x1 x2 addr --)
Stores two consecutive values to memory at the given address.
fill(addr u char --)
Fills u bytes of memory starting at addr with the specified character value.
move(addr1 addr2 u --)
Copies u address units from addr1 to addr2. Handles overlapping memory regions correctly.
erase(addr u --)
Fills u address units with zeros, effectively clearing the memory region.
allocate(u -- a-addr ior)
Allocates u cells of contiguous data space. Returns the start address and an I/O result code (0 = success, -1 = failure). Reuses previously freed blocks when possible.
free(a-addr -- ior)
Releases previously allocated memory starting at addr. Returns 0 on success. Adjacent freed blocks are merged to reduce fragmentation.
resize(a-addr u -- a-addr2 ior)
Changes the size of an allocation. If the new size fits within the existing block, returns the same address. Otherwise allocates a new block, copies existing data, and frees the old block.
c,(char --)
Reserves one character of data space and stores char in it.
char+(c-addr1 -- c-addr2)
Adds the size of one character to the address.
chars(n1 -- n2)
Returns the size in address units of n characters. In this system, 1 char = 1 unit.
align( -- )
Aligns the data-space pointer to a cell boundary.
aligned(addr -- a-addr)
Returns the first aligned address greater than or equal to addr.

Terminal

cr(--)
Newline added to terminal.
space(--)
Adds a single space to the terminal.
spaces(n --)
Adds n spaces to the terminal.
emit(n --)
Emits the ASCII code on top of the stack.
((--)
Starts inline comment. Everything between ( and ) will be ignored. Can appear anywhere on a line.
)(--)
Ends inline comment.
\\(--)
Line comment. Everything after \ to the end of the line is ignored. Supported in .fs files.
.rs(--)
Displays the return stack on the terminal. Henceforth extension — Forth-2012 only specifies .s for the data stack.
clear(i*x --)
Empties the data stack, return stack, and terminal screen. Henceforth extension — non-standard. For just the screen use page; for stacks alone use abort.
bye(--)
Clears everything, resets to default settings.
."(--)
Start print. Everything between quotes is printed.
"(--)
Ends print.
s"( -- addr u)
Creates a string literal. Returns the address and length of the string on the stack.
type(addr u --)
Displays the string at that address to the terminal.
page(--)
Clears the terminal screen (Forth-2012 6.2.2030). In Henceforth's append-only buffer this empties every prior entry so the next emission starts at the top. Does not affect the data, return, or data stack.
ms(u --)
Wait at least u milliseconds before the next loop iteration (Forth-2012 FACILITY 10.6.2.1905). Use it to pace animation and screen updates.
char( -- char)
Parses the next space-delimited word and returns the ASCII value of its first character.
[char]( -- n)
Compile-time version of CHAR. Compiles the character value into the current definition.
word(char -- c-addr)
Parses the input stream until the delimiter character is found and returns a counted string address.
parse(char -- c-addr u)
Parses the input stream until the delimiter character is found and returns address and length.
key( -- n)
Waits for a single keypress and pushes its ASCII value onto the stack. Blocks execution until a key is pressed.
?terminal( -- flag)
Returns -1 if the keyboard buffer has input or a terminal interrupt has been requested, 0 otherwise. The Forth-2012 standard name is ekey? (FACILITY 10.6.2.1306); both spellings dispatch to the same handler.
accept(c-addr n1 -- n2)
Receives up to n1 characters from the input source and stores them at c-addr. Returns n2, the actual number of characters received.
sessionInput(--)
Displays session input.
dictionary(--)
Shows the user's defined words, constants, and variables. Henceforth extension. For all words (built-in plus user-defined) use words or vlist.
vlist(--)
Displays a list of every built-in word Henceforth offers. The Forth-2012 standard name is words (TOOLS 15.6.1.2465); both spellings dispatch to the same handler.
see( "<spaces>name" --)
Shows what a word does: user-defined words print their source; built-in words print their stack diagram and description (TOOLS 15.6.1.2194). Usage: see pay-rent.
help( "<spaces>name" --)
Prints a word's stack diagram and description. Bare help opens the vocabulary browser. Henceforth extension.
forth(--)
Resets vocabulary to standard built-in words, removing all user definitions.
c"( -- c-addr)
Creates a counted string literal. Returns the address of a counted string (length byte followed by characters).
abort(--)
Clears all stacks and resets the interpreter state.
abort"(flag --)
If the flag is non-zero, displays the message and ABORTs. Otherwise discards the message and continues. Usage: 0= abort" value must not be zero".
quit(--)
Clears the return stack and enters interpretation state.
source( -- c-addr u)
Returns the address and length of the current input buffer.
environment?(c-addr u -- false | i*x true)
Queries the system for an environmental parameter. Returns false if unknown, or the value and true if known.

Other Words

date&time(-- s m h d M y)
Adds the device's current time to the stack: seconds, minutes, hours, day, month, year.
pay(--)
Presents the pay sheet, where you can send Bitcoin.
receive(--)
Shows where to get paid: prints the active wallet's current receiving address and presents it as a QR code. Reports only — the address rotates automatically after it receives a payment, never from this word.
.fs
Filename followed by .fs extension will load that file's contents into Henceforth.
startup.fs
A special file that runs automatically when Henceforth launches. Created as a blank template on first launch. Add your custom word definitions, aliases, and INCLUDE calls here so they are available every session.

Data Conversion

Henceforth has three stacks: the integer stack (main, inspected via .s), the data stack (raw bytes for Bitcoin Script operations, inspected via .ds), and the return stack (Forth-2012 >R / R> / R@, inspected via .rs). These words bridge the integer and data stacks.

hex>data( c-addr u -- )
Converts a hex string from the integer stack (c-addr u) to bytes on the data stack. Compose with S": S" deadbeef " hex>data. Legacy spelling hex2data still parses as a silent alias for backwards compatibility with pre-2026-05-19 scripts.
chars>data( c-addr u -- )
Converts a UTF-8 string from the integer stack (c-addr u) to bytes on the data stack. Compose with S": S" hello " chars>data. No auto-detect between hex and UTF-8 — silent hex interpretation of a UTF-8-intended OP_RETURN payload would broadcast wrong bytes to chain. Use hex>data for hex decoding, chars>data for raw UTF-8 bytes.
data>hex( -- )
Pops the top of the data stack and displays it as a lowercase hex string on the terminal. Legacy spelling data2hex still parses as a silent alias for backwards compatibility.
.ds( -- )
Displays the contents of the data stack.
>data( -- )
Converts the following token to raw data bytes and pushes them onto the data stack.
>addr(n -- )
Marks the top-of-stack integer value as a Bitcoin address string for transaction building.
>scriptnum( n -- )
Moves an integer from the integer stack to the script (data) stack as a minimally-encoded script number. Usage: 5 >scriptnum.
scriptnum>( -- n)
Decodes the top of the script stack as a script number (at most 4 bytes) and moves it to the integer stack.

String Operations

cmove(c-addr1 c-addr2 u -- )
Copies u characters from addr1 to addr2, proceeding low address to high (safe when addr2 > addr1).
cmove>(c-addr1 c-addr2 u -- )
Copies u characters from addr1 to addr2, proceeding high address to low (safe for overlapping regions when addr2 < addr1).
/string(c-addr1 u1 n -- c-addr2 u2)
Adjusts a string address and length by n characters. Advances the address by n and reduces the length by n.
search(c-addr1 u1 c-addr2 u2 -- c-addr3 u3 flag)
Searches for string2 within string1. If found, returns the match address, remaining length, and true. If not found, returns the original string and false.
blank(c-addr u -- )
Fills u characters of memory starting at addr with spaces (BL, ASCII 32).

Networking

wocbitcoinprice( -- n)
Fetches the bitcoin price in USD and adds it to the stack in pennies. The friendlier spelling price dispatches to the same handler.
woccirculatingsupply(--)
Fetches the circulating supply of bitcoin.
wocblockchaininfo(--)
Fetches latest general block info from WhatsOnChain and prints to terminal.
wocaddressinfo(--)
Takes an address and displays its information to the terminal.
wocgetbalance(c-addr u -- )
Takes an address string (from s") and displays that address's confirmed balance to the terminal. Usage: s" 1A1zP1..." wocgetbalance. For the active wallet's balance as a stack value, use balance.
wocgethistory(--)
Takes an address and displays its history information to the terminal.
wocgetblockbyheight(n--)
Takes a block height from the stack and searches WhatsOnChain for info on that block.

The three words below are awaitable: each suspends the line while its network query runs and resumes with the value pushed, so chain state lands on the stack instead of being prose in the transcript — : pay-rent balance 1000 > if 1000 s" 1A1zP1..." send then ; is both a Forth word and a payment policy. One query may be in flight at a time; while it runs the terminal shows awaiting balance… (esc cancels) and Escape cancels it. A query that cannot answer halts the line with an error — never a sentinel value that could silently flip an if.

balance( -- sats)
Queries the chain for the active wallet's total balance and pushes it in satoshis. The line suspends while the query runs and resumes when the value lands. A failed or partial query halts the line with an error — never a silent low number.
tx-confirmations(c-addr u -- n)
Takes a txid string (from s") and pushes the transaction's confirmation count. Pushes 0 when the transaction is known but still unconfirmed in the mempool. An unknown txid halts the line with an error.
spv-verified?(c-addr u -- flag)
Takes a txid string (from s") and pushes -1 when the transaction's Merkle proof verifies against the locally-validated header chain, 0 when it does not. A header-chain gap prints SPV PENDING alongside the 0; a proof-fetch failure halts the line with an error.

Transaction Builder

The transaction builder words allow programmatic construction of Bitcoin transactions directly from the FORTH terminal. Build a transaction step by step, then broadcast it.

tx-new( -- )
Initialises a new transaction builder. Clears any previous transaction state.
tx-set-fee(n -- )
Sets the fee rate in satoshis per kilobyte for the current transaction. Usage: 500 tx-set-fee.
tx-add-output(n c-addr u -- )
Adds a P2PKH output to the transaction. Takes the amount in satoshis plus an address string (from s") — the same convention as send. One address per call. Usage: 1000 s" 1A1zP1..." tx-add-output.
tx-add-data( -- )
Adds an OP_RETURN data output to the current transaction.
tx-preview-toggle( -- )
Toggles preview mode. When ON, TX-BROADCAST shows the transaction without sending it; when OFF, TX-BROADCAST broadcasts.
tx-show( -- )
Displays the current transaction being built (inputs, outputs, fee).
tx-clear( -- )
Clears the transaction builder, discarding all inputs and outputs.
tx-broadcast( -- )
Builds, signs, and broadcasts the current transaction to the BSV network via ARC.
send( amount c-addr u -- )
Standard payment in a single word. Pops the destination string from the data stack (placed there by S") and the amount from the integer stack. The string holds one address (single recipient) or N space-separated addresses (equal-split). Validates the inputs, then presents PayView with the amount + address pre-filled. The user long-presses to confirm; Face ID gates the actual broadcast via ARC. Terminal output on success: sent TXID: <id>. Usage: 1000 s" 1A1zP1..." send.
send-many( amount1 ... amountN c-addr u -- )
Multi-recipient payment with per-recipient amounts. Pops the address string first to derive N (the recipient count), then pops N amounts. Pairs amount[i] with address[i] in left-to-right order. Validates every payee, then presents MultiPaySheet with a per-recipient grid so the user reviews the output set before Face ID. On confirm, builds one transaction with N P2PKH outputs and broadcasts via ARC. Atomic — any validation failure rejects the whole transaction. Usage: 1000 5000 250 s" 1A 1B 1C" send-many.

Standard Transaction Script

Every installation includes a standard-transaction.fs file in the Files tab. This is a Bitcoin Script file that defines the OP_RETURN output attached to your payments. It is automatically loaded each time you open the Pay view.

standard-transaction.fs
( Standard Transaction Script )
 
SCRIPT-BEGIN
OP_FALSE OP_RETURN
." Hello, you are welcome." >data data>script
SCRIPT-END

Bitcoin Cryptography

sign-msg( -- )
Signs a message with the current wallet key using Bitcoin Signed Message (BSM). Usage: sign-msg <message>.
verify-msg( -- flag)
Verifies a BSM signature against an address. Pushes true (-1) or false (0). Usage: verify-msg <address> <signature> <message>.
encrypt-msg( -- )
Encrypts a message for a recipient using BRC-2 Electrum ECIES (ECDH + AES-CBC + HMAC-SHA256). Wire-compatible with @bsv/sdk EncryptedMessage. Usage: encrypt-msg <recipientPubKeyHex> <message>.
decrypt-msg( -- )
Decrypts a BRC-2 ECIES-encrypted message using the current wallet key. The plaintext is displayed. Usage: decrypt-msg <ciphertextHex>.

BAP Identity

BAP (Bitcoin Attestation Protocol) provides a decentralised identity system built on Bitcoin. Identity keys are derived using Type42 (BRC-42) key derivation from the wallet's master key.

bap-id( -- )
Shows the BAP identity key (ID) and signing address for the current wallet.
bap-sign( -- )
Signs a message with the BAP identity key (Type42 derived). Usage: bap-sign <message>.
bap-verify( -- flag)
Verifies a BAP-signed message. Pushes true (-1) or false (0). Usage: bap-verify <address> <signature> <message>.
bap-register( -- )
Builds a BAP ID registration transaction (OP_RETURN with BAP + AIP attestation).

Script Recording

Script recording is a macro assembler for Bitcoin Script. When recording is active, op_* words emit their opcodes into a script buffer rather than executing. Standard FORTH control flow still runs normally.

script-begin( -- )
Start recording Bitcoin Script. op_* words emit opcodes to the script buffer. FORTH control flow still runs normally.
script-end( -- )
Stop recording and finalise the script. Displays the compiled script hex.
script-show( -- )
Display the current script buffer contents as hex.
script-clear( -- )
Discard the current script buffer and stop recording.
script-use( -- )
Set the recorded script as the selected transaction script (for building transactions).
script-save( -- )
Save the current script with a name. Usage: script-save <name>.
script-load( -- )
Load a previously saved script by name. Usage: script-load <name>.
script-list( -- )
List all saved scripts.
script-step( -- )
Executes the next opcode of the loaded script buffer against the live script context and renders the stack — single-stepping through real Bitcoin Script. Reset by script-load, script-begin, script-clear, and clear.

Script Bridge

The bridge words connect FORTH's stacks to the script recording buffer, enabling dynamic script construction with computed values, strings, and file data.

>script(n -- )
Takes an integer from the stack and emits it as a Bitcoin Script number to the script buffer.
data>script( -- )
Takes the top item from the data stack and emits it as PUSHDATA to the script buffer. Use after crypto operations to inject computed hashes.
text>script( -- )
Converts the following token to UTF-8 bytes and emits as PUSHDATA to the script buffer. Usage: text>script image/jpeg.
file>script( -- )
Reads a file's raw contents and emits them as PUSHDATA to the script buffer. For uploading images, videos, or data on-chain. Usage: file>script photo.jpg.

Script Helpers

Pre-built script templates for common Bitcoin transaction patterns.

make-p2pkh-script( -- )
Creates a standard P2PKH (Pay-to-Public-Key-Hash) locking script from an address.
make-opreturn-script( -- )
Creates an OP_RETURN output script from data on the stack.
make-2of3-multisig( -- )
Creates a 2-of-3 multisig script from three public keys on the stack.
script-pushdata( -- )
Pushes raw data bytes to the script recording buffer using OP_PUSHDATA.

Script Execution Context

OP_CHECKSIG and OP_CHECKMULTISIG require a transaction context to verify against. These two words arm that context so signature-checking opcodes work in the terminal.

checksig-fixture( -- sig pubkey)
Builds and signs a synthetic transaction with a fixed keypair, arms the script context for OP_CHECKSIG, and pushes a genuinely valid signature and public key — the P2PKH walk verifies VALID with zero setup. Never fakes a result.
script-tx-context( satoshis n -- )
Loads a real transaction into the script context: rawtx and lockscript bytes from the data stack, satoshis and input index from the integer stack. Any on-chain transaction becomes replayable opcode-by-opcode. Usage: 0x<rawtx> 0x<lockscript> <satoshis> <input-index> script-tx-context.

OPCodes

All 140+ Bitcoin Script opcodes are exposed as FORTH words. Below are the categories — every opcode behaves like its Script consensus definition.

Push Data

op_0 … op_16( - x)
Push the corresponding integer onto the script stack. Equivalent to typing a numeric literal in FORTH.
op_false( - x) false
Pushes the empty byte vector (Script canonical zero / FALSE). Alias for OP_0.
op_pushdata1( - )
Frames a length-prefixed data push during script recording. At the prompt a 0x literal pushes its bytes directly.
op_pushdata2( - )
Frames a length-prefixed data push during script recording. At the prompt a 0x literal pushes its bytes directly.
op_pushdata4( - )
Frames a length-prefixed data push during script recording. At the prompt a 0x literal pushes its bytes directly.
op_1negate( - -1)
Pushes the script number -1 onto the script stack. (FORTH equivalent: -1.)

Control Flow

op_nop( - )
Does nothing. Consensus no-op.
op_ver( - version)
Pushes the transaction version (4-byte little-endian) onto the script stack. Chronicle upgrade of the historically-reserved opcode.
op_if( x - ) if
Consumes the top item as a consensus boolean and gates the script opcodes that follow.
op_notif( x - )
Runs the following branch when the top item is false. Mirror image of OP_IF. Composite FORTH: 0= if.
op_verif( threshold - bool)
Pushes 1 if the transaction version is at least the threshold. Chronicle upgrade.
op_vernotif( threshold - bool)
Pushes 1 if the transaction version is below the threshold. Chronicle upgrade.
op_else( - ) else
Flips the active branch — runs its body when the preceding OP_IF or OP_NOTIF branch did not run.
op_endif( - ) then
Closes the innermost OP_IF / OP_NOTIF block.
op_verify( x - )
Pops the top item and fails the script when it is false. Script-specific — no FORTH analogue.
op_return( - )
Marks the script as failed. In a locking script the data after it is provably unspendable — the standard way to record data on-chain.

Stack

op_toaltstack( x - ) >r
Pops the top item off the main stack and pushes it onto the alt stack.
op_fromaltstack( - x) r>
Pops the top item off the alt stack and pushes it onto the main stack.
op_2drop( x1 x2 - ) 2drop
Drops the top two items from the script stack.
op_2dup( x1 x2 - x1 x2 x1 x2) 2dup
Duplicates the top two items.
op_3dup( x1 x2 x3 - x1 x2 x3 x1 x2 x3)
Duplicates the top three items. Composite FORTH: 2dup over.
op_2over( x1 x2 x3 x4 - x1 x2 x3 x4 x1 x2) 2over
Copies the third and fourth items to the top.
op_2rot( x1 x2 x3 x4 x5 x6 - x3 x4 x5 x6 x1 x2)
Moves the fifth and sixth items to the top. Composite FORTH: chain of three rots.
op_2swap( x1 x2 x3 x4 - x3 x4 x1 x2) 2swap
Swaps the top two pairs.
op_ifdup( x - 0 | x x) ?dup
Duplicates the top item only if it is non-zero.
op_depth( - n) depth
Pushes the number of items currently on the script stack.
op_drop( x - ) drop
Drops the top item from the script stack.
op_dup( x - x x) dup
Duplicates the top item on the script stack.
op_nip( x1 x2 - x2) nip
Removes the second-from-top item, keeping the top.
op_over( x1 x2 - x1 x2 x1) over
Copies the second-from-top item to the top.
op_pick( ... n - ... xn) pick
Pops n and copies the n-th item (0-based from top) to the top.
op_roll( xn ... x0 n - xn-1 ... x0 xn) roll
Moves the n-th item (0-based from top) to the top.
op_rot( x1 x2 x3 - x2 x3 x1) rot
Rotates the top three items left so the third becomes the top.
op_swap( x1 x2 - x2 x1) swap
Swaps the top two items on the script stack.
op_tuck( x1 x2 - x2 x1 x2) tuck
Copies the top item below the second-from-top item.

Size & Separator

op_size( x -- x n)
Pushes the byte length of the top stack element without consuming it. Script-specific.
op_codeseparator( -- )
Marks the beginning of signature-checked data. All signature checks only use data after the most recent OP_CODESEPARATOR. Script-specific.
op_true( -- 1) true
Pushes the script number 1 onto the script stack. Alias for OP_1.

Splice (Chronicle restored)

op_cat( x1 x2 - x1x2)
Concatenates the top two byte vectors into one (x1 then x2). Script-specific — FORTH has no native byte-string concat.
op_split( x n - x1 x2)
Splits the byte vector at position n into a left and right half.
op_num2bin( x n - bytes)
Encodes a script number as a byte vector of length n (little-endian, sign bit).
op_bin2num( x - num)
Re-encodes a byte vector as a minimal script number, failing if it exceeds four bytes.
op_substr( x start len - substring)
Extracts a substring of the byte vector by start index and length. Chronicle upgrade (0xb3).
op_left( x n - prefix)
Extracts the leftmost n bytes of the byte vector. Chronicle upgrade (0xb4).
op_right( x n - suffix)
Extracts the rightmost n bytes of the byte vector. Chronicle upgrade (0xb5).

Bitwise

op_invert( x - ~x) invert
Inverts every bit of the top byte vector.
op_and( x1 x2 - and) and
Bitwise AND of the top two byte vectors (must be same length).
op_or( x1 x2 - or) or
Bitwise OR of the top two byte vectors (must be same length).
op_xor( x1 x2 - xor) xor
Bitwise XOR of the top two byte vectors (must be same length).
op_equal( x1 x2 - bool) =
Pops two byte vectors and pushes 1 if identical, otherwise the empty vector.
op_equalverify( x1 x2 - )
OP_EQUAL then OP_VERIFY — fails the script unless the top two items are equal. Script-specific composite.
op_reserved1, op_reserved2( - )
Reserved opcodes. Render the script invalid unless inside an unexecuted branch.

Arithmetic

op_1add( x - sum) 1+
Adds one to the top script number.
op_1sub( x - difference) 1-
Subtracts one from the top script number.
op_2mul( x - product) 2*
Doubles the top script number (Genesis-restored).
op_2div( x - quotient) 2/
Halves the top script number (Genesis-restored).
op_negate( x - -x) negate
Flips the sign of the top script number.
op_abs( x - |x|) abs
Replaces the top script number with its absolute value.
op_not( x - bool) 0=
Pushes 1 if the top item is zero, otherwise the empty vector.
op_0notequal( x - bool) 0<>
Pushes 1 if the top script number is non-zero, otherwise the empty vector.
op_add( x1 x2 - sum) +
Pops two script numbers and pushes their sum.
op_sub( x1 x2 - difference) -
Pops two script numbers and pushes the second minus the first.
op_mul( x1 x2 - product) *
Pops two script numbers and pushes their product (Genesis-restored).
op_div( x1 x2 - quotient) /
Pops two script numbers and pushes the second divided by the first (Genesis-restored).
op_mod( x1 x2 - modulus) mod
Pops two script numbers and pushes the remainder of the second divided by the first (Genesis-restored).
op_lshift( x n - shifted) lshift
Shifts the byte vector left by n bits (Genesis-restored).
op_rshift( x n - shifted) rshift
Shifts the byte vector right by n bits (Genesis-restored).
op_lshiftnum( x shift - result)
Numeric left shift preserving sign. Distinct from op_lshift which is bitwise on bytes. Chronicle upgrade (0xb6).
op_rshiftnum( x shift - result)
Numeric right shift preserving sign. Chronicle upgrade (0xb7).
op_booland( x1 x2 - bool) &&
Pushes 1 if both of the top two script numbers are non-zero, otherwise the empty vector.
op_boolor( x1 x2 - bool) ||
Pushes 1 if either of the top two script numbers is non-zero, otherwise the empty vector.
op_numequal( x1 x2 - bool) =
Pushes 1 if the top two script numbers are equal, otherwise the empty vector.
op_numequalverify( x1 x2 - )
OP_NUMEQUAL then OP_VERIFY — fails the script unless the numbers are equal. Script-specific composite.
op_numnotequal( x1 x2 - bool) <>
Pushes 1 if the top two script numbers differ, otherwise the empty vector.
op_lessthan( x1 x2 - bool) <
Pushes 1 if the second script number is less than the first.
op_greaterthan( x1 x2 - bool) >
Pushes 1 if the second script number is greater than the first.
op_lessthanorequal( x1 x2 - bool) <=
Pushes 1 if the second script number is less than or equal to the first.
op_greaterthanorequal( x1 x2 - bool) >=
Pushes 1 if the second script number is greater than or equal to the first.
op_min( x1 x2 - min) min
Pops two script numbers and pushes the smaller.
op_max( x1 x2 - max) max
Pops two script numbers and pushes the larger.
op_within( x min max - bool) within
Pushes 1 if x is at least min and less than max (min inclusive, max exclusive).

Crypto

op_ripemd160( x - hash)
Pops the top byte vector and pushes its RIPEMD-160 hash (20 bytes).
op_sha1( x - hash)
Pops the top byte vector and pushes its SHA-1 hash (20 bytes).
op_sha256( x - hash)
Pops the top byte vector and pushes its SHA-256 hash (32 bytes).
op_hash160( x - hash)
Pops the top byte vector and pushes RIPEMD-160(SHA-256(x)) — the 20-byte hash inside every P2PKH address.
op_hash256( x - hash)
Pops the top byte vector and pushes SHA-256(SHA-256(x)) — the 32-byte double hash used for transaction and block IDs.
op_checksig( sig pubkey - bool)
Pops a public key and a signature and pushes 1 if the signature is valid over the spending transaction. Needs a transaction context — load one with checksig-fixture (a signed demo pair) or script-tx-context (a real transaction).
op_checksigverify( sig pubkey - )
OP_CHECKSIG then OP_VERIFY — fails the script unless the signature is valid against the armed transaction context.
op_checkmultisig( x sig1...sigM M pub1...pubN N - bool)
Verifies M signatures against N public keys over the armed transaction context. Consumes an extra dummy item (the historic off-by-one).
op_checkmultisigverify( x sig1...sigM M pub1...pubN N - )
OP_CHECKMULTISIG then OP_VERIFY — fails the script unless the M-of-N check passes.

Lock Time / Reserved

op_nop2, op_nop3, op_pubkeyhash, op_pubkey, op_invalidopcode( - )
Reserved or pseudo-opcodes. OP_NOP2/NOP3 are no-ops in BSV Genesis; pubkeyhash, pubkey, and invalidopcode are pseudo-opcode placeholders.
op_nop1, op_nop4..6, op_nop9, op_nop10( - )
Reserved NOPs.

Bitcoin Wallet

Henceforth includes a full Bitcoin SV (BSV) wallet, enabling users to create wallets, receive and send transactions, and interact with the blockchain — all from the same app that runs FORTH programs.

Wallet Architecture

The wallet is built on a hierarchical deterministic (HD) key structure, meaning a single seed phrase generates an unlimited number of addresses.

  • Seed Phrase — a 12-word BIP-39 mnemonic generated when creating a new wallet. The master backup.
  • Key Derivation — two derivation methods are supported. Type42 (BRC-42) is the default for new wallets, using elliptic curve key tweaking for more flexible derivation paths. BIP-44 is the legacy standard, retained for restoring wallets created by other software; path m/44'/236'/0'/0/n.
  • Address Rotation — after each transaction, a new receiving address is derived automatically.

Trust-domain separation (Swift code organisation)

The Swift code that runs the wallet is partitioned into three independent trust domains, each owned by its own @MainActor @Observable type. A façade type, FORTH, holds one instance of each and is the only place that orchestrates across them.

  • ForthInterpreter — the Forth-2012 execution surface: two stacks, the user vocabulary, control flow, terminal text, KEY/ACCEPT, memory blocks, script recording.
  • WalletState — the wallet/payment surface: UTXO and transaction caches, payment-approval cache, PayView / MultiPaySheet / FeeRetry coordination, fee quotes, WoC viewmodels, the ARC service seam.
  • SPVHeaderStore — the SPV header chain: blockHeaders, validatedTipHeight, latestBlockHeight, the txVersion activation gate, the header-fetch viewmodel.

WalletState and SPVHeaderStore each hold a peer-reference to ForthInterpreter for legitimate terminal-IO reach (emitting an error, reading a stack value, writing to the terminal buffer). Deliberately there is no peer-reference between WalletState and SPVHeaderStore — payment code that tries to read blockHeaders directly is refused by the compiler, because the property simply isn't reachable from a WalletState extension. The handful of legitimate cross-domain reads (the wallet's transaction builder needs latestBlockHeight for the OP_VER gate) route through an explicit FORTH.shared?.headers.latestBlockHeight bridge, visible at every call site and reviewable by audit.

Three invariants previously honoured by policy are now enforced by the type system:

  • SPVHeaderStore.blockHeaders and validatedTipHeight are private(set). The four legitimate writers each have a named mutator: commitValidatedRun (chain-validated runs, atomic with tip advance), recordDisplayHeader (display-only WoC fetches, never advances the trust tip), bootstrapFromPersistence (atomic load on launch), resetToCheckpoint (wipe + reseed on checkpoint upgrade or forceResync).
  • WalletState.lastPaymentApproval is private(set). The only setter is setLastPaymentApproval, called by the biometric gate after a successful Face ID prompt or by recordPaymentApproval for upstream-stamped approvals.
  • BlockchainDataProvider is an actor rather than a @MainActor class. The cross-source verifier's async let primary / secondary for WhatsOnChain and JungleBus history fetches now runs in genuine parallel on the cooperative thread pool rather than serialised through the main thread.

The motivation is straightforward: the three domains have very different blast radii on compromise. A bug in interpreter code is a UX glitch. A bug in wallet code is potentially lost funds. A bug in SPV code is silent acceptance of fake "confirmed" status. Partitioning them so the compiler refuses cross-domain reach makes each domain's invariants reviewable in isolation and removes the entire class of "interpreter code accidentally mutating wallet state" failures that the prior single-class shape allowed.

Address Discovery

The wallet discovers addresses with funds using a shared Type42 scanner (scanType42Addresses). The scanner is derivationType-independent — it always tries Type42 first if the master key exists in Keychain, regardless of what derivationType says on the wallet card.

  • Dynamic scan range — scans at least 20 addresses (gap limit) past the last address with activity, with a minimum floor of 50 addresses.
  • Receive and change chains — scans both 1-wallet-{N} (receive) and 0-change-{N} (change) invoice numbers.
  • BIP-44 fallback — after Type42, scans standard BIP-44 paths. A wallet can have funds on both derivation paths.
  • Auto-repair — if Type42 addresses are discovered but derivationType is wrong, the scanner automatically corrects it.

Wallet Cards

Each wallet is represented by a Wallet Card in the app interface. A card stores wallet title, main address, derived addresses, balance, and derivation type. Multiple wallets can be managed simultaneously; biometric authentication is required to access sensitive details.

One wallet can be marked as the default wallet, indicated by a star on the wallet card. The default wallet is used by the terminal send word and other non-UI operations.

Transactions

Receiving

To receive BSV, share your wallet's receiving address or scan its QR code. Monitoring uses JungleBus REST polling every 30 seconds via GET /v1/address/get/{address}, plus a periodic sync as safety net.

Notifications use a UTXO-based deposit detection system tracking known outpoints per wallet — immune to confirmation state changes and mid-sync transient balances. First-launch outpoints are seeded silently.

Sending

Transactions are built locally on the device:

  1. UTXO Selection — avoiding ordinal-protected UTXOs.
  2. Fee Calculation — current rate from GorillaPool or TAAL via ARC policy endpoints.
  3. Change Output — returned to a fresh change address.
  4. Signing — each input signed with the correct private key.
  5. Anti-Fee-SnipingnLockTime set to current block height.
  6. Broadcasting — submitted via ARC with automatic failover.

The pay word opens the transaction builder with three tabs: OP_RETURN, Script, Upload (B:// protocol).

Fragmentation-Aware Selection

UTXO selection switches to a fragmentation-aware policy when the wallet has more than 100 UTXOs OR average UTXO value is below 5000 satoshis with more than 40 UTXOs. Dust-input cap rises from 10 to 50, sort flips to smallest-first, smart-change splitting disabled. A 100 KB ceiling refuses oversize transactions before biometric prompts.

Paymail

Paymail allows sending BSV to human-readable addresses (e.g., [email protected]) instead of raw Bitcoin addresses. Implements BRC-30 receive-transaction callbacks via .well-known/bsvalias discovery, and automatic resolution when a paymail address is entered in the pay view.

UTXO Sync

The wallet's UTXO set is reconstructed from locally-held transactions, not queried from a network endpoint. White paper §8 says SPV clients should derive ownership from chain-validated proofs they hold themselves; treating an external /unspent index as authoritative inverts that model. Henceforth follows the white paper.

  • History authoritative — every confirmed transaction in the local store contributes its outputs to the reconstructed UTXO set. WhatsOnChain /unspent corroborates and surfaces deposits we haven't fetched yet; it is never the source of truth.
  • Canonical address set — the wallet's master address, every derived address, and the currently-rotated receive address are unioned into a single set used by both history sync and UTXO reconstruction.
  • Strict removal policy — a UTXO disappears only on positive evidence of spend: the user broadcast a transaction that consumes it; a confirmed foreign transaction spending it lands in the local history; or the producing transaction fails SPV verification. Absence from an external endpoint is never a removal trigger.
  • Outpoint-consumption index — a derived set of "txid:vout" strings recording every consumed output, rebuilt from the local transaction store on every wallet open and updated incrementally as new transactions arrive.
  • Reconciliation — every full sync partitions outpoints into corroborated, provisional, and wallet-unknown. Wallet-unknown outpoints trigger a background fetch of the producing transaction.

Recovery Tools

  • Add address — paste any BSV address into the wallet's edit sheet; the app searches the wallet's seed across both Type42 and BIP-44 chains for a derivation path that produces it. Pure local computation.
  • Auto-rediscovery — when an incoming transaction pays an address the wallet doesn't yet know about, the app silently attempts the same rederivation in the background.
  • Advanced sync toggle — Settings → Bitcoin → Advanced Sync exposes the active UTXO sync model. Either side is safe; the toggle exists so users can self-recover if either path surfaces an anomaly.

SPV Architecture

The wallet implements Simplified Payment Verification per Section 8 of the Bitcoin white paper: clients trust a locally maintained chain of block headers and verify individual transactions against it using Merkle proofs.

Trust Anchor

The wallet ships with a hardcoded checkpoint in SPVCheckpoint.mainnet(height, hash, previousblockhash, merkleroot, version, time, nonce, bits) for a recent mainnet block roughly 500 confirmations below the then-tip. The checkpoint hash doubles as its own migration version marker: bumping it triggers a wipe of every user's local header store on next launch.

Header Store

FORTH.blockHeaders: [Int: WOCGetHeadersModel]. Every entry must satisfy:

  1. Non-empty merkleroot — pre-Phase-1 placeholder rows are dropped on load.
  2. Individual PoW validates — double-SHA256 of the serialized header must be below the target encoded in bits.
  3. Chain-linked to parentpreviousblockhash must equal the hash of the header at height - 1.
  4. Transitively anchored to the checkpoint.

Validated Tip

FORTH.validatedTipHeight is the highest block height whose header transitively chain-links back to the checkpoint via a contiguous run. SPV verification uses validatedTipHeight as the trust upper bound.

Commit Path

FORTH.commitValidatedHeaderRun(_:) is the single entry point. It requires: (1) the run's first header has a local anchor at firstHeight - 1; (2) the anchor's hash equals the run's first previousblockhash; (3) the full batch passes SPVService.validateBlockHeaderChain.

Auto-Sync

HeaderSyncService opens a WebSocket to wss://socket.whatsonchain.com/websocket/history?from=<validatedTipHeight+1>, buffers decoded headers, flushes runs of 500 via commitValidatedHeaderRun, with idle detection via cancellable Task.sleep(for: 3s) and exponential backoff 1s → 60s cap.

Verification

SPVService.verifyTransaction(...) fetches the TSC (BRC-74) Merkle proof from WhatsOnChain, looks up the containing header via findBoundedBlockHeader (returns the header only when its height falls in [checkpointHeight ... validatedTipHeight]), PoW-validates the local header, and verifies the proof against the local merkleroot. If the bounded lookup returns nil, verification returns blockHeaderNotFoundwithout fetching from WoC.

Checkpoint Maintenance

Bumping the checkpoint is deliberate: pick a target at least 500 confirmations below current tip; fetch from a primary source and cross-verify every field against a second independent source; commit the updated SPVCheckpoint.mainnet. The next launch detects the new hash and wipes/re-syncs.

Cold Mode & Air-Gap Bundles

Cold Mode (WalletCard.coldModeEnabled) defers broadcast: a transaction the user has approved with Face ID is signed and stored locally as a pending action rather than submitted to ARC. The user releases it later from the Pending Broadcasts view.

Three serialisable JSON formats carry data across the air-gap, exchanged via AirDrop and validated on import:

  • .hfheaders — a contiguous run of validated block headers anchored to a checkpoint hash. The importer hands the run to commitValidatedHeaderRun so chain-link, PoW, and checkpoint-anchor invariants self-enforce.
  • .hfreceipt — proof that a specific transaction was included in the blockchain. Carries (txid, raw tx hex, block height, block hash, TSC Merkle proof, containing header). verifyTransaction(fromBundle:) validates inclusion against the importer's own local header with no network call. After SPV passes, the importer re-derives output ownership and refuses receipts that don't pay this wallet.
  • .hfaddrpack — pre-derived receive addresses pushed signer → watcher. Each entry has the address, derivation scheme, and the scheme-specific derivation reference. Signed by the wallet's m/0/0 BIP32 identity key with DER ECDSA.

Settings → Bitcoin → Air-Gap Tools exposes three exporters and a single import button (routed by file extension). In v1.0, .hfaddrpack import validates the signature; persistence into a watch-only store is a follow-up in a later release.

Wallet Data Management

UTXO and transaction persistence is the most security-critical layer of the wallet. The rules below are invariants, not preferences.

  • Never overwrite UTXO files with partial data. Saved only when all addresses queried successfully.
  • Conservative merge. A UTXO is removed only when its owning address was queried and the UTXO was not in the response.
  • Never persist a smaller transaction set than what's on disk. persistTransactionsToDisk refuses the write if the in-memory snapshot has fewer entries.
  • Per-wallet file scoping. Transactions, UTXOs, and pending broadcast amounts live in wallet-scoped files. On wallet open, in-memory state is cleared and reloaded from that wallet's files only.
  • Trust the per-wallet file on load. Load-time byOwnership filtering has been removed — it dropped Type42 change and paymail receives, then silently wrote the smaller set back.

UTXOs & Balance

A UTXO represents a discrete amount of Bitcoin that can be spent.

Ordinal Protection

1Sat Ordinals are NFTs inscribed on single-satoshi UTXOs. Protected at three levels:

  1. Transaction builder — 1-satoshi UTXOs are excluded from UTXO selection.
  2. Inscription scanner — for display, 1-sat UTXOs are scanned by fetching the raw transaction and detecting the OP_FALSE OP_IF ... OP_ENDIF inscription pattern. Results are cached per outpoint.
  3. 1Sat indexer fallback — 1-sat UTXOs with no detectable current-output inscription are checked against ordinals.gorillapool.io. Tri-state: is-ordinal, not-ordinal, or fails closed on HTTP 5xx (protect the UTXO).

UTXO Split

From the UTXO detail view, a single UTXO can be split into up to 240 separate outputs across fresh derived addresses.

API Providers

CapabilityWhatsOnChainGorillaPoolTAALJungleBus
UTXOs (balance)
Transaction history
SPV Merkle proofs
BSV price
Fee quotes
Broadcast (ARC)✓*
Real-time monitoring

*TAAL requires a user-configured API key. GorillaPool is primary; TAAL is failover. API keys are stored in the iOS Keychain and configured in Settings → API & Miners.

Security

  • Biometric authentication — Face ID or Touch ID required to view private keys, seed phrases, and wallet details. Sessions expire after 120 seconds of inactivity. The same window is used by the payment approval cache.
  • Keychain storage — all sensitive material stored with .whenUnlockedThisDeviceOnly accessibility.
  • Seed verification — after creating a wallet, users verify their seed phrase via a 3-word quiz.
  • No server dependency — fully self-custodial. No account, no registration, no server holds your keys.
  • Message encryption (BRC-2) — Electrum ECIES (ECDH + AES-CBC + HMAC-SHA256). Wire-compatible with @bsv/sdk. Format: "BIE1" || pubkey || ciphertext || HMAC.
  • Wallet file encryption — AES-256-GCM with a Keychain-backed key before syncing to iCloud.

Payment Approval Cache

A successful biometric approval stamps the cache with (surface, amountSats, outputsDigest, timestamp). A subsequent broadcast within the 120-second window can skip the prompt when all three scoping axes match. The outputsDigest is a SHA-256 over a canonical encoding of the sorted output set: UInt32-LE(recipientKey.utf8.count) || recipientKey.utf8 || Int64-LE(satoshis) per output. Length-prefixing defeats engineered concatenation collisions.

Encrypted Off-Device Backup

.bep files produced by BackupService: a portable, password-encrypted blob restorable on any device.

  • Key derivation — PBKDF2-HMAC-SHA256 with 16-byte salt, 600,000 iterations.
  • Encryption — AES-256-GCM (authenticated; tampering yields .invalidPassword).
  • Wire format — magic prefix "HFBK1", salt (16), iteration count (4 BE), nonce (12), AES-GCM ciphertext + 16-byte tag.
  • Forward compatibility — version byte in the magic prefix allows future format bumps.

QR Code Scanner

Unified QR scanner in Settings, routed by content: hfauth: prefix → BRC-103 auth; Base58Check 0x80 → WIF import; 0x00 → Bitcoin address → pay view.

BRC-103 Authentication

Passwordless authentication: web service displays hfauth: QR with challenge + callback. Wallet displays the requesting domain, asks for approval, signs the challenge with the wallet's identity key, sends the signature to the callback. No passwords, no accounts.

BRC-100 Wallet Interface

henceforth://wallet/{method}?params=... URL scheme exposes key derivation, signing, encryption, and certificate management (BRC-52/53). All sensitive operations require user approval via on-screen confirmation + biometric.

Status: the URL scheme and signing infrastructure are implemented, but the full BRC-100 sign-in workflow is still in development and not yet available for general use.

Certificate Proofs (BRC-52/53)
  • Per-field HMAC keyring, ECDH-bound — each field's HMAC key is encrypted with an ECDH shared secret derived between the prover's and verifier's BRC-42 identity keys. A leaked proof is useless against any other party.
  • Issuer signature verificationverifyProof checks the issuer's BSM signature over the certificate's canonical field set before accepting any field as authentic.
  • Serial number carried in the proof — for correlation with revocation or re-issuance records.

Credits

Starting Forth — Leo Brodie

Leo Brodie, author of Starting Forth — a great textbook, suitable for both novices and professionals.

Starting FORTH (PDF)

BITSRFR

Fellow surfer of the binary ocean who has made FORTH tutorials and built a Swift command line FORTH application.

YouTube · cruxFORTH on GitHub

Bitcoin

The greatest monetary system ever created. Invented by Satoshi Nakamoto — please read the white paper.

Bitcoin White Paper

SwiftBSV

The Swift SDK that powers every cryptographic operation in the Henceforth wallet. Originally written by Will Townsend at wtsnz/SwiftBSV; the fork at henryhudson/SwiftBSV continues that foundation with Swift 6 strict concurrency, modern Apple platform targets, and a 41-page LaTeX book of its own.

SwiftBSV Book (PDF)