feat(tips): grin public tips (voucher model via grin node get_outputs + get_kernel)

Grin as a new asset arm in the tips machinery (no grin-lws change, no custodial
grin_transactions/grin_outputs port):
- GrinClient::get_outputs([commit]) (funding: present+height=confirmed) +
  get_kernel(excess) (sweep spend-height); reached via state.chains.grin.
- create-gating (supported_tip_asset grin, validate 66-hex commitment,
  confirmations_for_asset grin=10), grin_commitment column (migration).
- confirmation worker grin arm: get_outputs present -> confs; amount AUTO-VERIFY
  with warn (voucher hides amount; not a theft vector; matches legacy).
- sweep reconciler grin arm: get_outputs-absent + get_kernel height at depth 10 ->
  settle; absence-without-kernel -> no settle (recoverable, never missettle);
  reorg revert only on commitment reappearing; probe error never settles/reverts.
- 5 grin tests + pure-helper unit tests; 251 lib + 24 tips green.

Amount-verification note: the voucher's bulletproof rewind nonce is random +
untransmitted, so it can't be rewound today; a clean non-custodial fast-follow is
to have the client send the per-voucher rewind nonce and verify via rangeproof
rewind (nonce reveals amount only, not spend authority).
This commit is contained in:
jwinterm
2026-07-10 18:40:56 -04:00
parent 5d8d5e193f
commit 1d6d67faa5
12 changed files with 695 additions and 30 deletions
@@ -0,0 +1,12 @@
-- Grin public tips use a VOUCHER model, not an address + view-key. The sender
-- builds a single-party voucher output locally and shares its blinding factor
-- (the "spend key" for a Pedersen commitment) in the encrypted claim blob. The
-- on-chain handle for that voucher is the output's Pedersen COMMITMENT (hex, 66
-- chars) — the grin confirmation + sweep workers query the node by commitment
-- (get_outputs present/absent) rather than by a txid.
--
-- The commitment also arrives in `tip_address` (the client sends the same value
-- for both), but store it in a dedicated column so the grin worker arms read a
-- clear, purpose-named field. Nullable: only grin tips populate it. Preferred
-- over `tip_address` where both exist.
ALTER TABLE social_tips ADD COLUMN grin_commitment VARCHAR(66);