Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/appliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,10 +318,12 @@ key-only, and only if you need it.
**"Wrong token."** The token changes each time the setup service restarts — read the
current one from the console. After five wrong attempts it mints a new one on purpose.

**The address was rejected.** You most likely pasted a subaddress (starts with `8`) or an
integrated address. Use your primary address, which starts with `4` and is 95 characters.
If the message says the checksum failed, at least one character of the address is wrong —
re-copy it from your wallet rather than fixing it by eye.
**The address was rejected.** For Monero you most likely pasted a subaddress (starts with
`8`) or an integrated address. Use your primary address, which starts with `4` and is 95
characters. If the message says the checksum failed — for either the Monero or the Tari
address — at least one character is wrong: re-copy it from your wallet rather than fixing
it by eye. A Tari address rejected as the wrong network came from a testnet wallet; the
stack mines mainnet.

**It came back on the old version after an update.** That is the safety mechanism working:
the new version did not come up healthy, so the machine reverted. Nothing is lost. Check
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ control channel will commit, are unaffected either way.
| `monero.data_dir` | `auto` | Where the Monero blockchain lives on the host. `auto` = `./data/monero`. Point this at an existing `.bitmonero` directory to reuse a synced node. See [Reusing an existing node](#reusing-an-existing-node). |
| `monero.mem_limit` | `auto` | Upper limit on the monerod container's memory, so a leak/runaway OOM-restarts monerod alone instead of the host's OOM-killer picking a victim. `auto` is a generous ceiling (6 GB) that won't trip during normal operation or initial sync. monerod's OOM-triggering memory is small (~0.1 GiB at rest, ~1–3 GiB during sync) while its multi-GB blockchain DB is reclaimable, memory-mapped page cache that the kernel evicts under pressure rather than OOM-killing. Lower it only to free RAM. Raise it for a full (unpruned) node doing a heavy initial sync on a fast disk, or if a low-RAM host ever OOMs monerod during IBD (it restarts and resumes; the on-disk chain is transactional, no data loss). Accepts any Docker memory value, e.g. `"8g"`. (Tari has its own `tari.mem_limit`; the dashboard, P2Pool, Tor, and the proxies are small and carry fixed conservative ceilings in `docker-compose.yml`.) |
| `tari.mode` | `local` | `local` runs the bundled Tari base node; `remote` merge-mines against an external one (see `tari.remote` and [Remote Tari node](#remote-tari-node)). |
| `tari.wallet_address` | _required_ | Your Tari (Minotari) payout address. |
| `tari.wallet_address` | _required_ | Your Tari (Minotari) payout address, in either the base58 or the emoji form (single or dual, embedded payment id supported). Both forms carry a built-in checksum, and it is verified — a single mistyped character fails it, which otherwise means merge-mine rewards silently lost. A checksum-valid address for a Tari testnet is rejected too: the stack mines mainnet. `setup`/`apply` reject both cases. |
| `tari.view_key` | _empty_ | The private **view** key for the Tari payout address, to confirm merge-mine payouts on-chain (#462, the Tari sibling of `monero.view_key`). When set, the stack runs a view-only `minotari_console_wallet` against your local Tari node and the dashboard shows confirmed Tari payouts beside the time-to-block estimate (see [Dashboard › Payout confirmation](dashboard.md#payout-confirmation)). Requires `tari.spend_public_key` too. **Security:** a view key can scan but never spend — yet it reveals every incoming amount and its timing to anyone who can read `config.json`/`.env`, so it is handled like `node_password` (never logged or echoed, kept in the owner-only `.env`, and delivered to the container via a tmpfs secret, never `docker inspect`). Local Tari node only. Empty (the default) leaves the feature off. |
| `tari.spend_public_key` | _empty_ | The **public** spend key for the Tari payout address, exported alongside the view key (`minotari_console_wallet ... export-view-key-and-spend-key`; see [Dashboard › Exporting your keys](dashboard.md#exporting-your-keys)). Required whenever `tari.view_key` is set — a view-only Tari wallet is built from the private view key plus this public spend key. Public, not a secret. |
| `tari.payout_scan_birthday` | `auto` | Where the view-only Tari wallet starts scanning on first creation (#462). Unlike Monero's block-height restore point, a Tari birthday is **days since the Unix epoch** (a u16, 0–65535). `auto` = today when the wallet is first made, so it tracks payouts forward without rescanning from genesis. Set an earlier day to backfill older payouts (slower first scan). Only affects the first wallet creation; ignored once the wallet exists. |
Expand Down
146 changes: 137 additions & 9 deletions pithead
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,18 @@ doctor() {
*) dr_warn "Monero payout address doesn't look like a primary address (expected 95 chars starting with 4)." ;;
esac
fi
# Same class of check for the Tari payout address: both forms carry a DammSum checksum.
local _tw
_tw=$(jq -r '.tari.wallet_address // empty' "$CONFIG_FILE" 2>/dev/null)
if [ -n "$_tw" ] && [ "$_tw" != "your_tari_wallet_address" ]; then
case "$(tari_address_type "$_tw")" in
ok) dr_ok "Tari payout address passes its checksum." ;;
unchecked) dr_info "Could not verify the Tari payout address checksum (no usable python3)." ;;
checksum) dr_fail "Tari payout address FAILS its checksum — at least one character is mistyped, and Tari rewards are silently lost. Re-copy the address from your Tari wallet into tari.wallet_address and run './pithead apply'." ;;
network) dr_fail "Tari payout address is for a different Tari network — this stack mines MAINNET Tari. Set tari.wallet_address to your mainnet address and run './pithead apply'." ;;
*) dr_warn "Tari payout address doesn't look like a valid Tari address (base58 or emoji form)." ;;
esac
fi
fi

# --- Memory (Linux only) ---
Expand Down Expand Up @@ -4023,7 +4035,16 @@ wizard_ask_core() {
*) echo " ✗ Not a valid Monero primary address (95 chars, starts with 4). Try again." >&2 ;;
esac
done
read -r -p "Enter Tari Wallet Address: " IN_TARI_WALLET || true
while :; do
read -r -p "Enter Tari Wallet Address (base58 or emoji form): " IN_TARI_WALLET || break
[ -z "$IN_TARI_WALLET" ] && break # the shared required-fields check below owns this
case "$(tari_address_type "$IN_TARI_WALLET")" in
ok | unchecked) break ;;
checksum) echo " ✗ Checksum failed — at least one character is mistyped. Re-copy the address from your Tari wallet." >&2 ;;
network) echo " ✗ That address is for a Tari testnet — this stack mines MAINNET Tari. Use your mainnet address." >&2 ;;
*) echo " ✗ Not a valid Tari address (base58 or emoji form). Try again." >&2 ;;
esac
done

if [ -z "$IN_MONERO_WALLET" ] || [ -z "$IN_TARI_WALLET" ]; then
error "Wallet addresses are required. Aborting."
Expand Down Expand Up @@ -4325,6 +4346,105 @@ print(kind[0])
PYEOF
}

# Tari payout-address verdict: ok | checksum | network | invalid | unchecked. Tari addresses come
# in base58 AND emoji forms, single or dual, with an optional embedded payment id (RFC-0155) —
# but every form carries a 1-byte DammSum checksum, so a mistyped character is detectable here
# instead of at the Tari node at merge-mine time, or never visibly at all (#845). The decode and
# the check order (length, checksum, network byte, feature bits) mirror tari's own from_bytes.
#
# python3 does the math, same as the Monero gate above. Without it the address is "unchecked" —
# accepted, the pre-gate behaviour, degraded, never a false reject.
tari_address_type() {
command -v python3 >/dev/null 2>&1 || {
echo "unchecked"
return
}
python3 - "$1" <<'PYEOF' 2>/dev/null || echo "unchecked"
import os
import sys

# Bitcoin base58 (Tari uses the bs58 crate) — NOT Monero's block-wise scheme. In Tari's base58
# form the network byte and the features byte are each encoded ALONE (one character each), then
# the remaining bytes as one base58 string.
_B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

# The 256-emoji alphabet from tari's emoji.rs, index = byte value. Every entry is a single
# codepoint, and tari's own parser matches codepoints exactly — so exact .find() is faithful.
_EMOJI = (
"🐢📟🌈🌊🎯🐋🌙🤔🌕⭐🎋🌰🌴🌵🌲🌸🌹🌻🌽🍀🍁🍄🥑🍆🍇🍈🍉🍊🍋🍌🍍🍎"
"🍐🍑🍒🍓🍔🍕🍗🍚🍞🍟🥝🍣🍦🍩🍪🍫🍬🍭🍯🥐🍳🥄🍵🍶🍷🍸🍾🍺🍼🎀🎁🎂"
"🎃🤖🎈🎉🎒🎓🎠🎡🎢🎣🎤🎥🎧🎨🎩🎪🎬🎭🎮🎰🎱🎲🎳🎵🎷🎸🎹🎺🎻🎼🎽🎾"
"🎿🏀🏁🏆🏈⚽🏠🏥🏦🏭🏰🐀🐉🐊🐌🐍🦁🐐🐑🐔🙈🐗🐘🐙🐚🐛🐜🐝🐞🦋🐣🐨"
"🦀🐪🐬🐭🐮🐯🐰🦆🦂🐴🐵🐶🐷🐸🐺🐻🐼🐽🐾👀👅👑👒🧢💅👕👖👗👘👙💃👛"
"👞👟👠🥊👢👣🤡👻👽👾🤠👃💄💈💉💊💋👂💍💎💐💔🔒🧩💡💣💤💦💨💩➕💯"
"💰💳💵💺💻💼📈📜📌📎📖📿📡⏰📱📷🔋🔌🚰🔑🔔🔥🔦🔧🔨🔩🔪🔫🔬🔭🔮🔱"
"🗽😂😇😈🤑😍😎😱😷🤢👍👶🚀🚁🚂🚚🚑🚒🚓🛵🚗🚜🚢🚦🚧🚨🚪🚫🚲🚽🚿🧲"
)


def b58_decode(s):
n = 0
for ch in s:
d = _B58.find(ch)
if d < 0:
return None
n = n * 58 + d
body = n.to_bytes((n.bit_length() + 7) // 8, "big") if n else b""
return b"\x00" * (len(s) - len(s.lstrip("1"))) + body


def dammsum(data):
# DammSum over base 256 with coefficients [4,3,1] (mask 27); a valid array sums to 0.
r = 0
for d in data:
r ^= d
overflow = r & 0x80
r = (r << 1) & 0xFF
if overflow:
r ^= 27
return r


try:
# Re-decode argv as strict UTF-8: under a C locale the interpreter may have decoded the
# emoji bytes with surrogateescape, which would silently miss the alphabet.
s = os.fsencode(sys.argv[1]).decode("utf-8")
except UnicodeDecodeError:
print("invalid")
sys.exit(0)

if all(ord(c) < 128 for c in s):
if len(s) < 45: # tari's own minimum encoded length for the base58 form
print("invalid")
sys.exit(0)
parts = [b58_decode(s[0]), b58_decode(s[1]), b58_decode(s[2:])]
raw = None if any(p is None for p in parts) else b"".join(parts)
else:
raw = bytearray()
for c in s:
i = _EMOJI.find(c)
if i < 0:
raw = None
break
raw.append(i)

# A single address is exactly 35 bytes; a dual one 67, plus up to 256 payment-id bytes.
if raw is None or not (len(raw) == 35 or 67 <= len(raw) <= 67 + 256):
print("invalid")
elif dammsum(raw) != 0:
print("checksum")
elif raw[0] != 0x00:
# 0x00 is mainnet — the only network this stack mines. The other assigned network bytes
# mean a real address for the wrong network, which deserves its own message; anything
# else is no Tari address at all.
print("network" if raw[0] in (0x01, 0x02, 0x10, 0x24, 0x26) else "invalid")
elif raw[1] & ~0b111: # unknown feature bits (known: one-sided 1, interactive 2, payment-id 4)
print("invalid")
else:
print("ok")
PYEOF
}

# When the dashboard onion is enabled but no password is set, generate a strong one and save it to
# config.json (#343). Keeps the fail-closed onion usable without forcing the operator to invent a
# 16+ char secret; the plaintext lives in owner-only config.json, exactly like a hand-set password
Expand Down Expand Up @@ -4693,14 +4813,12 @@ parse_and_validate_config() {
if [ -z "$MONERO_WALLET" ] || [ -z "$TARI_WALLET" ]; then
error "Missing required wallet addresses in $CONFIG_FILE."
fi
# tari.wallet_address gets no exact-format gate like monero's below: Tari addresses come in
# base58 AND emoji forms, single/dual, with optional payment IDs — length and charset both vary
# (RFC-0155), and p2pool passes the string straight to the Tari node, which accepts either form,
# so a strict regex would false-reject valid addresses. The untouched placeholder is caught by the
# combined check just below; also reject embedded whitespace here — no Tari address of either form
# has an ASCII space, and a space isn't a control char so the central guard above misses it, yet it
# would silently mine to a wrong address (the #250 failure mode). A subtler typo is the node's to
# reject at merge-mine time — the DammSum checksum gate that would catch it here is #845.
# tari.wallet_address gets no shape regex: Tari addresses come in base58 AND emoji forms,
# single/dual, with optional payment IDs — length and charset both vary (RFC-0155), so a
# regex would false-reject valid addresses. Instead the real gate is tari_address_type below
# (full decode + DammSum checksum, #845). Whitespace still gets its own message here — it's
# the likeliest paste error, a space isn't a control char so the central guard above misses
# it, and "invalid" alone wouldn't say where to look.
case "$TARI_WALLET" in
*[[:space:]]*) error "tari.wallet_address contains whitespace — a Tari address (base58 or emoji) has none. Check for a stray space or line break in $CONFIG_FILE." ;;
esac
Expand All @@ -4718,6 +4836,16 @@ parse_and_validate_config() {
*) error "monero.wallet_address ('${MONERO_WALLET:0:6}…', ${#MONERO_WALLET} chars) is not a valid Monero primary address (expected 95 chars starting with 4)." ;;
esac

# The Tari sibling of the gate above (#845): full decode + DammSum verdict, both address
# forms. "unchecked" (no usable python3) passes — degraded to the pre-gate behaviour,
# never a false reject.
case "$(tari_address_type "$TARI_WALLET")" in
ok | unchecked) ;;
checksum) error "tari.wallet_address fails its checksum — at least one character is mistyped, and a mistyped address means Tari rewards are silently lost. Re-copy the address (base58 or emoji form) from your Tari wallet and try again." ;;
network) error "tari.wallet_address is for a different Tari network (a testnet). This stack mines MAINNET Tari — use your mainnet address." ;;
*) error "tari.wallet_address (${#TARI_WALLET} chars) is not a valid Tari address in either the base58 or the emoji form. Copy it from your Tari wallet." ;;
esac

MONERO_MODE=$(jq -r '.monero.mode // "local"' "$CONFIG_FILE")
case "$MONERO_MODE" in
local | remote) ;;
Expand Down
Loading