witness: pre-sign approval guard with USD-based caps - #316
Open
CoderZhi wants to merge 6 commits into
Open
Conversation
…SD caps Introduces an ApprovalGuard that gates witness signing on configurable USD limits, with Lark-based admin approval for transfers above the single-tx cap. Disabled by default; opt-in per cashier via config. - util/coingecko: cached price source for USD conversion - util/lark_card: interactive approval card sender + HMAC signature verify - witness/approval: guard core, decision lock with DB-fallback after restart - witness/approval_server: HTTP callback handler with replay protection - witness/recorder: SignedAmountSince, MarkTransferAwaitingApproval, ApproveTransferToReady, RejectTransfer - types: new TransferAwaitingApproval and TransferRejected statuses - tokencashierbase: integrate guard before signing; fail closed on evaluation errors Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Make the per-token coingeckoID config field optional. At witness startup,
unset ids are resolved via GET /coins/{platform}/contract/{token1}. Bridged
or otherwise unindexed tokens can still be handled by setting coingeckoID
explicitly as an override. Existing configs are unchanged.
- util/coingecko: ResolveIDByContract + ErrCoinGeckoIDNotFound sentinel
- cmd/witness/main.go:
- chain → CoinGecko platform map + native-gas id map (for zero-addr token)
- coingeckoResolver (in-process cache, one HTTP call per unique address)
- tokenMetasFor* consult resolver when CoinGeckoID is empty
- newPriceFeed + startPriceFeedRunner split so cashier wiring can run
between cache construction and runner start (price feed now uses the
union of explicit + auto-resolved ids)
- buildApprovalGuard error message clarifies that coingeckoID is now
an override, not a requirement
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously the admin-approval flow flipped a row from `approval` straight back to `ready`, so the next signing tick re-ran guard.Check and the single-tx limit pushed the row back into `approval` again — the admin decision was silently undone. Introduce TransferApproved (8 chars, fits varchar(10)). ApproveTransfer now sets `approved`; the cashier loop skips guard.Check for that status and treats it like `ready` for signing/confirm. Rolling-window total keeps counting only post-sign statuses (pending/confirmed/settled), so the admin override does not leak into the window cap. Also drop the lingering MFA wording from comments in types.go and solrecorder.go — the actual flow is Lark interactive cards, not MFA. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
End-to-end coverage for the approval flow: tick 1 holds an over-limit transfer; flipping the status to `approved` (as Recorder.ApproveTransfer does in SQL) lets tick 2 sign it without re-running guard.Check. Guards against future regressions in the approval ↔ approved transition path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the relayer reports a stale block height that the witness can no longer fetch (the source chain/API pruned that history), ProcessStales used to return on the first failing height, blocking every later stale height behind it and re-logging the failure every tick. Now ProcessStales skips-and-continues past un-fetchable heights and warns the admin via a Lark card carrying the cashier + height with a single Mute button. Clicking Mute records the (cashier, height) in the per-cashier ApprovalGuard (in-memory) so the height is skipped on subsequent ticks and stops alerting. When no guard/Lark is configured, it degrades to a deduplicated text alert. The admin resolves the transfer out-of-band with sign-witness/submit-witness (no API fetch needed); a runbook documents find -> sign -> submit -> mute. - approval.go: mutedHeights/staleAlert state; IsHeightMuted, NotifyStaleFetchFailure (per-height dedup, muted suppression, card->text fallback), Mute (idempotent) - util/lark_card.go: LarkStaleWarning + SendLarkStaleWarningCard; Height on LarkCallback and height parsing in DecodeLarkCardCallback - approval_server.go: "mute" callback action - tokencashierbase.go: ProcessStales skip-muted + skip-and-continue - docs/missing-transaction-runbook.md + README link - tests: guard mute/dedup, card decode/send, approval-server mute path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ProcessStales: only alert on fetch failures at/below the witness tip; heights above tip mean the witness is catching up (not missing data), so skip them silently to avoid false "source data deleted" pages on restart. Documented the stuck-tip edge case. - ProcessStales: process current vs previous cashier heights separately and pass the owning cashier to the alert so the card attributes the right contract; the Mute button still routes back to the posting guard via a new LarkStaleWarning.GuardKey field. - ApprovalGuard.NotifyStaleFetchFailure: prune expired staleAlert entries on insert (mirrors seenNonces) so the dedup map can't grow unbounded. - util/lark_card.go: extract postLarkInteractiveCard (webhook check + JSON envelope + POST + status handling) shared by both card senders; extract uint64FromValue shared by the tidx/height parse paths. - runbook: note Mute requires the approval card; clarify above-tip skip. - tests: GuardKey routing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch adds two related witness-side safety features that share the same Lark-card / approval-server infrastructure:
1. Pre-sign approval guard (USD-based caps)
ApprovalGuardthat gates witness signing on a per-cashier rolling-window USD cap and a per-transfer USD cap, with Lark-based admin approve/reject for transfers above the single-tx cap.approval/priceFeedconfig blocks and per-cashierwindowValueLimit/singleTxValueLimit. Existing chain configs are unchanged.UPDATE ... WHERE status='approval'provides atomicity.cb.Cashieris used only for routing; SQLWHEREalways uses the guard's bound cashier key.2. Stale-height alert + mute (source data deleted)
When the relayer reports a stale block height the witness can no longer fetch (source chain/API pruned that history), the witness used to abort
ProcessStaleson the first failure — blocking every later stale height behind it and re-logging every tick.ProcessStalesnow skips-and-continues past un-fetchable heights, and posts a Lark warning card with a Mute button (cashier + height). Muting records the height in-memory in the per-cashierApprovalGuard, so it's skipped on later ticks and stops alerting. Falls back to a deduplicated text alert when no guard/card webhook is configured.LarkStaleWarning.GuardKey.sign-witness/submit-witness(no API fetch needed). New runbook:witness-service/docs/missing-transaction-runbook.md(find → sign → submit → mute).Changes
witness/approval.go— guard core,Check,RequestApproval,Approve/Reject, nonce/replay protection; stale-height mute state +IsHeightMuted/NotifyStaleFetchFailure/Mute(dedup map pruned on insert)witness/approval_server.go— HTTP callback handler with HMAC-SHA256 signature verify and replay protection;muteactionutil/lark_card.go— interactive approval + stale-warning card senders (sharedpostLarkInteractiveCardposter), signature verifier,uint64FromValueparse helperutil/coingecko.go— cached CoinGecko price source with refresh loop and max-agewitness/recorder.go—SignedAmountSince,MarkTransferAwaitingApproval,ApproveTransfer,RejectTransferwitness/types.go—TransferAwaitingApproval,TransferApproved,TransferRejectedstatuseswitness/tokencashierbase.go— integrate guard before signing; reworkProcessStales(skip muted, skip-and-continue, tip-aware alerting)cmd/witness/main.go— wire config, price feed, and approval serverdocs/missing-transaction-runbook.md— admin runbook for missing/deleted source transactionsTest plan
go build ./witness-service/...go test ./witness-service/...(all pass; includes guard mute/dedup, Lark card decode/send, approval-server mute path)approvalstate, verify admin click still resolves it via DB-fallback path🤖 Generated with Claude Code