@@ -17,6 +17,7 @@ import { beforeEach, afterEach, describe, test } from 'node:test';
1717import assert from 'node:assert/strict' ;
1818import { assertMatchesObject } from '../test-helpers.ts' ;
1919import { STACKS_TESTNET } from '@stacks/network' ;
20+ import { SyntheticPoxEventName } from '../../../src/pox-helpers.ts' ;
2021
2122describe ( 'cache-control tests' , ( ) => {
2223 let db : PgWriteStore ;
@@ -1104,4 +1105,116 @@ describe('cache-control tests', () => {
11041105 assert . equal ( request7 . status , 304 ) ;
11051106 assert . equal ( request7 . text , '' ) ;
11061107 } ) ;
1108+
1109+ test ( 'principal cache control invalidates on PoX STX unlock' , async ( ) => {
1110+ const stacker = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6' ;
1111+ const url = `/extended/v2/addresses/${ stacker } /transactions` ;
1112+
1113+ // Block 1: initial block, no stacking activity.
1114+ await db . update (
1115+ new TestBlockBuilder ( {
1116+ block_height : 1 ,
1117+ index_block_hash : '0x01' ,
1118+ parent_index_block_hash : '0x00' ,
1119+ burn_block_height : 100 ,
1120+ } ) . build ( )
1121+ ) ;
1122+
1123+ const request1 = await supertest ( api . server ) . get ( url ) ;
1124+ assert . equal ( request1 . status , 200 ) ;
1125+ const etag0 = request1 . headers [ 'etag' ] ;
1126+
1127+ // Block 2: stacker locks STX via stack-stx, unlocking at burn height 200.
1128+ const block2 = new TestBlockBuilder ( {
1129+ block_height : 2 ,
1130+ index_block_hash : '0x02' ,
1131+ parent_index_block_hash : '0x01' ,
1132+ burn_block_height : 100 ,
1133+ } ) . addTx ( { tx_id : '0x0001' , sender_address : stacker } ) ;
1134+ block2 . txData . pox4Events . push ( {
1135+ event_index : 0 ,
1136+ tx_id : '0x0001' ,
1137+ tx_index : 0 ,
1138+ block_height : 2 ,
1139+ canonical : true ,
1140+ stacker : stacker ,
1141+ locked : 1000n ,
1142+ balance : 5000n ,
1143+ burnchain_unlock_height : 200n ,
1144+ pox_addr : null ,
1145+ pox_addr_raw : null ,
1146+ name : SyntheticPoxEventName . StackStx ,
1147+ data : {
1148+ lock_amount : 1000n ,
1149+ lock_period : 1n ,
1150+ start_burn_height : 100n ,
1151+ unlock_burn_height : 200n ,
1152+ signer_key : '0x0011223344' ,
1153+ end_cycle_id : null ,
1154+ start_cycle_id : null ,
1155+ } ,
1156+ } ) ;
1157+ await db . update ( block2 . build ( ) ) ;
1158+
1159+ // ETag changed due to the new transaction.
1160+ const request2 = await supertest ( api . server ) . get ( url ) ;
1161+ assert . equal ( request2 . status , 200 ) ;
1162+ const etag1 = request2 . headers [ 'etag' ] ;
1163+ assert . notEqual ( etag1 , etag0 ) ;
1164+
1165+ // Cache works with current ETag.
1166+ const request3 = await supertest ( api . server ) . get ( url ) . set ( 'If-None-Match' , etag1 ) ;
1167+ assert . equal ( request3 . status , 304 ) ;
1168+ assert . equal ( request3 . text , '' ) ;
1169+
1170+ // Block 3: chain advances, burn height still below unlock — no new tx for stacker.
1171+ await db . update (
1172+ new TestBlockBuilder ( {
1173+ block_height : 3 ,
1174+ index_block_hash : '0x03' ,
1175+ parent_index_block_hash : '0x02' ,
1176+ burn_block_height : 150 ,
1177+ } ) . build ( )
1178+ ) ;
1179+
1180+ // Cache still works: pox_state is still 'locked', no new activity.
1181+ const request4 = await supertest ( api . server ) . get ( url ) . set ( 'If-None-Match' , etag1 ) ;
1182+ assert . equal ( request4 . status , 304 ) ;
1183+ assert . equal ( request4 . text , '' ) ;
1184+
1185+ // Block 4: burn height crosses unlock threshold — STX are now unlocked.
1186+ await db . update (
1187+ new TestBlockBuilder ( {
1188+ block_height : 4 ,
1189+ index_block_hash : '0x04' ,
1190+ parent_index_block_hash : '0x03' ,
1191+ burn_block_height : 201 ,
1192+ } ) . build ( )
1193+ ) ;
1194+
1195+ // Cache is now a miss because pox_state changed from 'locked' to 'unlocked'.
1196+ const request5 = await supertest ( api . server ) . get ( url ) . set ( 'If-None-Match' , etag1 ) ;
1197+ assert . equal ( request5 . status , 200 ) ;
1198+ const etag2 = request5 . headers [ 'etag' ] ;
1199+ assert . notEqual ( etag2 , etag1 ) ;
1200+
1201+ // New ETag works.
1202+ const request6 = await supertest ( api . server ) . get ( url ) . set ( 'If-None-Match' , etag2 ) ;
1203+ assert . equal ( request6 . status , 304 ) ;
1204+ assert . equal ( request6 . text , '' ) ;
1205+
1206+ // Block 5: chain advances further, no new activity — ETag stays stable.
1207+ await db . update (
1208+ new TestBlockBuilder ( {
1209+ block_height : 5 ,
1210+ index_block_hash : '0x05' ,
1211+ parent_index_block_hash : '0x04' ,
1212+ burn_block_height : 250 ,
1213+ } ) . build ( )
1214+ ) ;
1215+
1216+ const request7 = await supertest ( api . server ) . get ( url ) . set ( 'If-None-Match' , etag2 ) ;
1217+ assert . equal ( request7 . status , 304 ) ;
1218+ assert . equal ( request7 . text , '' ) ;
1219+ } ) ;
11071220} ) ;
0 commit comments