Skip to content

Commit 4ffda66

Browse files
committed
feat: manage rollbacks properly.
Instead of dropping all AdaHandles and recalculating from scratch on rollback, drop only those that were affected by rollback or restore them to there last known state based on the history.
1 parent eecc820 commit 4ffda66

5 files changed

Lines changed: 131 additions & 10 deletions

File tree

src/main/java/org/cardanofoundation/tools/adahandle/resolver/repository/AdaHandleHistoryRepository.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,23 @@
1212
@Repository
1313
public interface AdaHandleHistoryRepository extends JpaRepository<AdaHandleHistoryItem, String> {
1414

15-
@Modifying
15+
// flushAutomatically + clearAutomatically so a subsequent find* in the same tx reads
16+
// the post-delete state from the DB and isn't served a stale persistence-context entity.
17+
@Modifying(flushAutomatically = true, clearAutomatically = true)
1618
@Query("DELETE FROM AdaHandleHistoryItem WHERE slot > :target")
17-
void deleteAllAfterSlot(@Param("target") long target);
19+
int deleteAllAfterSlot(@Param("target") long target);
1820

19-
@Query("SELECT item FROM AdaHandleHistoryItem item WHERE (name, slot) IN (SELECT name, MAX(slot) AS max_slot FROM AdaHandleHistoryItem GROUP BY name)")
20-
List<AdaHandleHistoryItem> getLatestHistoryItemByName();
21+
/**
22+
* Distinct handle names that have history on the abandoned fork (slot &gt; target).
23+
* Backed by {@code idx_ada_handle_history_item_slot}; a shallow range scan on a real reorg.
24+
*/
25+
@Query("SELECT DISTINCT item.name FROM AdaHandleHistoryItem item WHERE item.slot > :target")
26+
List<String> findNamesWithSlotGreaterThan(@Param("target") long target);
27+
28+
/**
29+
* Latest remaining history entry for a handle. With the composite PK {@code (name, slot)}
30+
* this resolves as a PK index seek to the last entry — O(log n) — instead of the previous
31+
* full-table {@code GROUP BY name, MAX(slot)} over all history rows.
32+
*/
33+
AdaHandleHistoryItem findFirstByNameOrderBySlotDesc(String name);
2134
}

src/main/java/org/cardanofoundation/tools/adahandle/resolver/service/AdaHandleHistoryService.java

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package org.cardanofoundation.tools.adahandle.resolver.service;
22

33
import com.bloxbean.cardano.yaci.store.common.domain.AddressUtxo;
4+
5+
import lombok.extern.slf4j.Slf4j;
6+
47
import org.cardanofoundation.tools.adahandle.resolver.entity.AdaHandleHistoryItem;
58
import org.cardanofoundation.tools.adahandle.resolver.mapper.AdaHandleHistoryMapper;
69
import org.cardanofoundation.tools.adahandle.resolver.repository.AdaHandleHistoryRepository;
@@ -10,6 +13,7 @@
1013
import java.util.List;
1114

1215
@Service
16+
@Slf4j
1317
public class AdaHandleHistoryService {
1418

1519
@Autowired
@@ -19,13 +23,40 @@ public class AdaHandleHistoryService {
1923
private AdaHandleService adaHandleService;
2024

2125
public void rollbackToSlot(long slot) {
22-
adaHandleHistoryRepository.deleteAllAfterSlot(slot);
23-
List<AdaHandleHistoryItem> adaHandleHistoryItems = getLatestAdaHandleHistoryItemsByName();
24-
adaHandleService.recalculateAdaHandlesFromHistory(adaHandleHistoryItems);
25-
}
26+
log.info("Rollback to slot {}", slot);
27+
28+
// Capture the distinct handle names touched on the abandoned fork BEFORE deleting,
29+
// so we know exactly which ada_handle rows need repair. Backed by
30+
// idx_ada_handle_history_item_slot — a shallow range scan on a real reorg.
31+
List<String> affectedNames = adaHandleHistoryRepository.findNamesWithSlotGreaterThan(slot);
32+
33+
int deleted = adaHandleHistoryRepository.deleteAllAfterSlot(slot);
34+
if (deleted == 0) {
35+
// No-op rollback (e.g. the node's rollbackward to the current cursor point at the
36+
// catch-up-to-tip transition). Nothing was removed, so the handle table is already
37+
// correct — skip the recalculation entirely.
38+
log.info("Rollback to slot {} deleted 0 history rows — nothing to do", slot);
39+
return;
40+
}
41+
42+
log.info("Rollback to slot {} deleted {} history rows across {} handle(s)",
43+
slot, deleted, affectedNames.size());
44+
45+
// Restore each affected handle to its latest REMAINING history entry (the pre-fork
46+
// owner), or drop it if it no longer has any history (it was first minted on the
47+
// abandoned fork). This is O(affected handles) with a PK seek per handle — not the
48+
// previous O(all history) re-aggregation + O(all handles) full-table rewrite — so it
49+
// stays cheap even on a slow DB and never blocks the chainsync event-loop thread.
50+
for (String name : affectedNames) {
51+
AdaHandleHistoryItem latest = adaHandleHistoryRepository.findFirstByNameOrderBySlotDesc(name);
52+
if (latest == null) {
53+
adaHandleService.deleteByName(name);
54+
} else {
55+
adaHandleService.upsert(AdaHandleHistoryMapper.toAdaHandle(latest));
56+
}
57+
}
2658

27-
public List<AdaHandleHistoryItem> getLatestAdaHandleHistoryItemsByName() {
28-
return adaHandleHistoryRepository.getLatestHistoryItemByName();
59+
log.info("Finished rollback to slot {}", slot);
2960
}
3061

3162
public void saveAdaHandleHistoryItems(List<AddressUtxo> addressUtxoList) {

src/main/java/org/cardanofoundation/tools/adahandle/resolver/service/AdaHandleService.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,14 @@ public void recalculateAdaHandlesFromHistory(List<AdaHandleHistoryItem> adaHandl
4545
List<AdaHandle> adaHandles = adaHandleHistoryItems.stream().map(AdaHandleHistoryMapper::toAdaHandle).toList();
4646
adaHandleRepository.saveAll(adaHandles);
4747
}
48+
49+
/** Upsert a single handle row (name is the @Id, so save() merges). */
50+
public void upsert(AdaHandle adaHandle) {
51+
adaHandleRepository.save(adaHandle);
52+
}
53+
54+
/** Delete a single handle by its name (primary key). No-op if the row doesn't exist. */
55+
public void deleteByName(String name) {
56+
adaHandleRepository.deleteById(name);
57+
}
4858
}

src/main/java/org/cardanofoundation/tools/adahandle/resolver/storage/AdaHandleProcessor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import com.bloxbean.cardano.yaci.store.events.internal.CommitEvent;
77
import com.bloxbean.cardano.yaci.store.utxo.domain.AddressUtxoEvent;
88
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
911
import org.cardanofoundation.tools.adahandle.resolver.service.AdaHandleHistoryService;
1012
import org.cardanofoundation.tools.adahandle.resolver.service.AdaHandleService;
1113
import org.springframework.context.event.EventListener;
@@ -18,6 +20,7 @@
1820

1921
@Component
2022
@RequiredArgsConstructor
23+
@Slf4j
2124
public class AdaHandleProcessor {
2225
private final AdaHandleService adaHandleService;
2326
private final AdaHandleHistoryService adaHandleHistoryService;
@@ -86,6 +89,7 @@ public void handleCommitEvent(CommitEvent commitEvent) {
8689
@EventListener
8790
@Transactional
8891
public void handleRollback(RollbackEvent rollbackEvent) {
92+
log.info("Handle rollback");
8993
adaHandleHistoryService.rollbackToSlot(rollbackEvent.getRollbackTo().getSlot());
9094
}
9195

src/test/java/org/cardanofoundation/tools/adahandle/resolver/service/AdaHandleServiceTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.cardanofoundation.tools.adahandle.resolver.service;
22

3+
import org.cardanofoundation.tools.adahandle.resolver.entity.AdaHandle;
34
import org.cardanofoundation.tools.adahandle.resolver.entity.AdaHandleHistoryItem;
45
import org.cardanofoundation.tools.adahandle.resolver.projection.Addresses;
56
import org.junit.jupiter.api.*;
@@ -64,6 +65,68 @@ public void testRollback() {
6465
assertThat(adaHandles, hasItems("Tom", "Otto"));
6566
}
6667

68+
@Test
69+
public void testRollbackRestoresHandleToOlderVersion() {
70+
// "Eve" was transferred several times. The most recent transfer (slot 3000) is on the
71+
// abandoned fork; the rollback must restore Eve to its immediately-preceding owner
72+
// (the slot-2000 version) — not the oldest (slot 1000) and not the deleted fork owner.
73+
adaHandleHistoryService.saveAll(List.of(
74+
new AdaHandleHistoryItem("Eve", "stake1eveOldA", "addr1eveOldA", 1000L),
75+
new AdaHandleHistoryItem("Eve", "stake1eveOldB", "addr1eveOldB", 2000L),
76+
new AdaHandleHistoryItem("Eve", "stake1eveNewFork", "addr1eveNewFork", 3000L)));
77+
adaHandleService.upsert(new AdaHandle("Eve", "stake1eveNewFork", "addr1eveNewFork"));
78+
79+
// Pre-rollback Eve resolves to the fork owner.
80+
Addresses eve = adaHandleService.getAddressesByAdaHandle("Eve");
81+
assertThat(eve.getPaymentAddress(), equalTo("addr1eveNewFork"));
82+
83+
// Rollback past slot 3000 only — the slot-2000 version is the one to restore to.
84+
adaHandleHistoryService.rollbackToSlot(2500L);
85+
86+
// Eve is restored to the immediately-older (slot 2000) owner, not deleted and not the
87+
// oldest slot-1000 owner. This is the path exercised by findFirstByNameOrderBySlotDesc.
88+
eve = adaHandleService.getAddressesByAdaHandle("Eve");
89+
assertThat(eve, is(not(nullValue())));
90+
assertThat(eve.getStakeAddress(), equalTo("stake1eveOldB"));
91+
assertThat(eve.getPaymentAddress(), equalTo("addr1eveOldB"));
92+
93+
// A deeper rollback past slot 2000 restores Eve to the oldest (slot 1000) owner,
94+
// proving the PK-seek re-picks the new latest after each delete.
95+
adaHandleHistoryService.rollbackToSlot(1500L);
96+
eve = adaHandleService.getAddressesByAdaHandle("Eve");
97+
assertThat(eve.getStakeAddress(), equalTo("stake1eveOldA"));
98+
assertThat(eve.getPaymentAddress(), equalTo("addr1eveOldA"));
99+
}
100+
101+
@Test
102+
public void testRollbackRemovesHandleMintedOnlyOnFork() {
103+
// "Bob" was first minted on the abandoned fork (slot 1400), so it has no history
104+
// before the rollback point and must be removed from ada_handle entirely (not just
105+
// reverted to an earlier owner).
106+
adaHandleHistoryService.saveAll(List.of(
107+
new AdaHandleHistoryItem("Bob", "stake1bob0000", "addr1bob0000", 1400L)));
108+
adaHandleService.upsert(new AdaHandle("Bob", "stake1bob0000", "addr1bob0000"));
109+
110+
assertThat(adaHandleService.getAddressesByAdaHandle("Bob").getPaymentAddress(), equalTo("addr1bob0000"));
111+
112+
adaHandleHistoryService.rollbackToSlot(1202L);
113+
114+
// Bob's fork-only history is gone and it has no earlier history → handle removed.
115+
assertThat(adaHandleService.getAddressesByAdaHandle("Bob"), equalTo(null));
116+
}
117+
118+
@Test
119+
public void testRollbackNoOpDoesNotRecompute() {
120+
// A rollback to a slot with no history above it (here the current tip of the fixture,
121+
// slot 1305) is the no-op the node sends at the catch-up-to-tip transition. It must
122+
// not touch the handle table at all.
123+
Addresses tomBefore = adaHandleService.getAddressesByAdaHandle("Tom");
124+
adaHandleHistoryService.rollbackToSlot(1305L);
125+
Addresses tomAfter = adaHandleService.getAddressesByAdaHandle("Tom");
126+
assertThat(tomAfter.getStakeAddress(), equalTo(tomBefore.getStakeAddress()));
127+
assertThat(tomAfter.getPaymentAddress(), equalTo(tomBefore.getPaymentAddress()));
128+
}
129+
67130
@Test
68131
public void testDollarSign() {
69132
Addresses addresses = adaHandleService.getAddressesByAdaHandle("$Tom");

0 commit comments

Comments
 (0)