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
1 change: 1 addition & 0 deletions server/api/test/v1/ask-logical-sync.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ test('isolated PG16: filtered initial copy, atomic CDC, pause/resume, restart an
assert.equal((await target.query("SELECT bool_and(subenabled) AS enabled FROM pg_subscription")).rows[0].enabled,true);
assert.deepEqual((await target.query('SELECT id,dest_amount::text FROM public.cross_chain_swaps ORDER BY id')).rows,[{id:1,dest_amount:null},{id:2,dest_amount:null},{id:3,dest_amount:null},{id:4,dest_amount:'3.5'}]);
assert.deepEqual((await target.query("SELECT attname FROM pg_attribute WHERE attrelid='public.crosschain_venue_actions'::regclass AND attnum>0 AND NOT attisdropped ORDER BY attnum")).rows.map(r=>r.attname),['venue','id','action_type','status','swap_created_at','source_chain','source_token','dest_chain','dest_token','zec_amount_zat','source_amount_usd','dest_amount_usd']);
await target.query(read('swaps-grant-v1.sql'));
assert.equal((await reader.query("SELECT ready FROM analytics_public.dataset_coverage WHERE dataset='swap_daily'")).rows[0].ready,true);
assert.deepEqual((await reader.query("SELECT venue,direction,swaps::int,zec::text,missing_zec::int FROM analytics_public.swap_daily_query ORDER BY venue,direction")).rows,[
{venue:'maya',direction:'inflow',swaps:5,zec:'5',missing_zec:0},{venue:'near-intents',direction:'inflow',swaps:2,zec:null,missing_zec:1},{venue:'near-intents',direction:'outflow',swaps:1,zec:null,missing_zec:1}]);
Expand Down
5 changes: 4 additions & 1 deletion server/api/test/v1/ask-research-swaps.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ test('migrations are guarded, keep linkable fields private and order destination
// Additive only: the one ALTER TABLE adds the two amount columns.
assert.doesNotMatch(migration,/\bDROP\b|\bDELETE FROM\b|\bTRUNCATE\b|\bUPDATE\b|\bALTER (?:SUBSCRIPTION|PUBLICATION)\b/);
assert.deepEqual(migration.match(/ALTER TABLE [^;]+;/g),['ALTER TABLE public.cross_chain_swaps ADD COLUMN source_amount numeric,ADD COLUMN dest_amount numeric;']);
assert.deepEqual(migration.match(/GRANT [^;]+;/g),['GRANT SELECT ON public.crosschain_venue_actions TO ask_public_view_owner;','GRANT SELECT(source_amount,dest_amount) ON public.cross_chain_swaps TO ask_public_view_owner;','GRANT SELECT ON analytics_public.swap_daily_query TO ask_public_reader;']);
assert.deepEqual(migration.match(/GRANT [^;]+;/g),['GRANT SELECT ON public.crosschain_venue_actions TO ask_public_view_owner;','GRANT SELECT(source_amount,dest_amount) ON public.cross_chain_swaps TO ask_public_view_owner;']);
// The reader grant waits for the worker that lists the view (its guard refuses every query otherwise).
assert.doesNotMatch(migration,/ask_public_reader/);
assert.deepEqual(deploy('swaps-grant-v1.sql').match(/GRANT [^;]+;/g),['GRANT SELECT ON analytics_public.swap_daily_query TO ask_public_reader;']);
const added=/ADD TABLE([^;]*);/.exec(source)[1];
const venue=/CREATE TABLE public\.crosschain_venue_actions\((.*?),PRIMARY KEY/.exec(migration)[1].split(',').map(c=>c.trim().split(' ')[0]);
const venuePublished=/public\.crosschain_venue_actions\(([^)]*)\)/.exec(added)[1].split(',');
Expand Down
12 changes: 12 additions & 0 deletions server/deploy/ask-analytics/swaps-grant-v1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
\set ON_ERROR_STOP on
-- Reader grant for swap_daily_query, applied only AFTER the worker build that
-- lists swap_daily_query in coreViews is running (swaps.md step 7). The worker's
-- guard refuses every query while the reader can select a view it does not
-- list, so granting earlier takes all research offline.
-- Rollback: REVOKE ALL ON analytics_public.swap_daily_query FROM ask_public_reader;
BEGIN;
SET LOCAL lock_timeout='500ms';SET LOCAL statement_timeout='30s';
DO $$BEGIN IF current_database()<>'ask_analytics' OR pg_is_in_recovery() OR to_regclass('analytics_public.swap_daily_query') IS NULL
THEN RAISE EXCEPTION 'swaps-v1.sql required on the analytics store';END IF;END$$;
GRANT SELECT ON analytics_public.swap_daily_query TO ask_public_reader;
COMMIT;
4 changes: 2 additions & 2 deletions server/deploy/ask-analytics/swaps-v1.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
-- replicated Maya/THORChain venue action table (reviewed columns only). Apply
-- BEFORE swaps-source-v1.sql: a published column or table without its
-- destination stops apply, and disable_on_error halts all research.
-- Reader grants change only by adding one view. The view never exposes a single
-- The reader grant for the new view is a separate step (swaps-grant-v1.sql),
-- after the worker that knows the view is running. The view never exposes a single
-- swap's ZEC amount or time: flows already exposes exact shield/deshield amounts
-- with timestamps, so per-swap amounts would let SQL rebuild linkage edges.
-- Rollback: see swaps.md (source first, then swaps-v1-rollback.sql).
Expand Down Expand Up @@ -69,5 +70,4 @@ CREATE VIEW analytics_public.swap_daily_query WITH(security_barrier=true) AS
WHERE c.dataset='swap_daily' AND c.ready;
RESET ROLE;
REVOKE ALL ON public.crosschain_venue_actions FROM PUBLIC;
GRANT SELECT ON analytics_public.swap_daily_query TO ask_public_reader;
COMMIT;
4 changes: 3 additions & 1 deletion server/deploy/ask-analytics/swaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Proposal, not applied. The owner approves each rollout step. `swap_daily` ships
## Rollback (written first; reverse order, each step safe on its own)

- **Before step 10:**
0. Revoke the reader grant first if applied: `REVOKE ALL ON analytics_public.swap_daily_query FROM ask_public_reader;`
1. Re-deploy the previous worker drop-in (`zzzzzzzzzzzzzzzz-ask-reorgs-20261101.conf` is the current last) and restore the admission backup taken at step 7.
2. Restore the recovery config/state backups from step 5 (see `feed-recovery.md`, "Adding a replicated table").
3. On mainnet: `swaps-source-v1-rollback.sql`. It restores the original nine `cross_chain_swaps` columns and drops the venue table in one transaction.
Expand Down Expand Up @@ -50,7 +51,8 @@ The existing `swaps` dataset is unaffected throughout: its view reads only the o
- Stream the export without a file: `ssh zcash-mainnet "sudo -u postgres psql -X -d zcash_explorer_mainnet -c '\copy (SELECT id,source_amount,dest_amount FROM public.cross_chain_swaps ORDER BY id) TO STDOUT'"` piped into `sudo -u postgres psql -X -d ask_analytics -c '\copy analytics_meta.swap_amount_backfill FROM STDIN'` on the store.
- Measure the first batch: `-v lo=1 -v hi=20000 -f swaps-backfill-v1.sql` under `/usr/bin/time -v`. Then run 500,000-id ranges up to the max id.
- Verify zero mismatches: `SELECT count(*) FROM public.cross_chain_swaps s JOIN analytics_meta.swap_amount_backfill b USING(id) WHERE s.source_amount IS DISTINCT FROM b.source_amount OR s.dest_amount IS DISTINCT FROM b.dest_amount;` then drop the staging table.
7. **Worker build at the merge commit:** deploy it, back up the admission script and install the new one, then restart and check health 200.
7. **Worker build at the merge commit:** deploy it, back up the admission script and install the new one, then restart and check health 200
Only then apply `swaps-grant-v1.sql`, and confirm a live `sql_query` on `blocks` still succeeds. The worker's guard refuses every query while the reader can select a view the running build does not list, so the grant must never precede the worker. (On 2026-11-02 the grant shipped inside step 2 and research was offline until it was revoked.)
8. **Verify as `ask_public_reader`:**
- `sum(missing_zec)` is 0;
- NEAR Intents swap counts per date equal the `swaps` dataset;
Expand Down
Loading