Repository navigation
Conversation
On startup StartService rolls back to the cursor block and, when it uses BlockFetch (far from tip, or a sync-stop slot), fetches the range from that block inclusive, so the cursor block is processed again. Rollback handlers delete only slot > rollbackTo, so the block's rows stay and are written a second time. Tables keyed by natural keys are overwritten; tables keyed by a random UUID gain a duplicate row per restart. On a mainnet DB transaction_metadata had 76 rows for 38 (tx_hash, label) at one restart slot and 176 for 88 at another. Affected (random UUID id, JPA saveAll, deleteBySlotGreaterThan): transaction_metadata, assets, transaction_scripts, mir. Other per-block tables use natural keys or upserts; reward_rest/unclaimed_reward_rest are rewritten per epoch after a delete by (earned_epoch, type). The startup RollbackEvent now says whether the rollback point will be processed again (rollbackPointReprocessed: true only for the BlockFetch path; ChainSync starts after the intersection, so near tip the cursor block is not refetched and its rows must be kept). The four stores delete from that slot inclusive in that case. No schema change, and all other stores keep their behaviour. Rows already duplicated by earlier restarts are not cleaned up; that needs a one-off dedupe per table, keeping one row per natural key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKakmKURLn4uc33hgrz2hF
… rows transaction_metadata, assets, transaction_scripts and mir used a random UUID primary key and JPA saveAll, so every time a block was processed again (restart from the cursor block, re-fetch) its rows were inserted again with new ids and duplicated. The id is now a name based UUID (v5) of the row's natural key, and rows are written with a jOOQ batch upsert on the existing id primary key: - transaction_metadata: tx_hash, label - assets: tx_hash, unit (policy + hex asset name; asset_name is a decoded, sanitized name and can repeat) - transaction_scripts: tx_hash, purpose, redeemer_index (one row per redeemer) - mir: tx_hash, cert_index, credential (null for pot to pot transfers) No schema change or migration: existing rows keep their random ids and new rows get deterministic ids. metadata and mir are added to the jOOQ codegen modules. On MySQL the id is bound as binary(16), as Hibernate writes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKakmKURLn4uc33hgrz2hF
| public static UUID nameUUID(String... parts) { | ||
| MessageDigest sha1; | ||
| try { | ||
| sha1 = MessageDigest.getInstance("SHA-1"); |
|
Merge note for #1313 and #1266 (found while combining the #1328 PRs for the re-test)
Merged together as they are, those stores would lose the cursor block's rows on every restart in range mode. The combined branch resolves it in .rollbackPointReprocessed(fetchRange && processedBlockNumber == null)
|
|
Merge note for #1330 and #1266: when(tipFinderService.findTip(anyLong())).thenReturn(new Tip(new Point(slot, "tip_hash"), 10_000_100L));It is already applied on the combined test branch |
Fault-injection re-test resultWe ran the same 104 fault scenarios again on preprod (benchmark-vm): 43 during the initial sync and 61 at the tip. This time the build had all the fix PRs together: main + #1330, #1331, #1313, #1332, #1271, #1333, #1266 (branch
Details and the full list of findings: #1328 This PR in the test
|
… the MIR id The mir id was a name based UUID of (tx_hash, cert_index, credential). The credential column holds only the hash, and the credential kind (key or script) survives only in the reward address. A MIR certificate whose map has a key credential and a script credential with the same hash bytes would give both rows the same id, and the second upsert would overwrite the first. A script credential now adds a "script" part to the id. Key credentials and pot to pot transfers (null credential) keep the (tx_hash, cert_index, credential) id, so ids for the common case do not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJrQUCWhJpLCQR5Asv7nDb
TxScriptStorageImpl.saveAll used JPA saveAll, which runs in its own transaction when there is none. The jOOQ batch upsert that replaced it had no transaction boundary. In parallel mode ScriptRedeemerDatumProcessor calls saveAll from executor and parallel stream workers, which do not inherit the event listener's transaction, so on autocommit a failure in the middle of the batch left the earlier rows committed. saveAll is @transactional again, like the other store writers (TransactionStorageImpl, UtxoStorageImpl, ...). The test runs outside the test transaction, through the Spring bean, and checks that a batch whose second row fails writes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJrQUCWhJpLCQR5Asv7nDb
|
|
Updated after an external review of the 2.0.x backport #1345: 75c33ff, 652e1d8. Each finding, its verdict and its test are in the reply on #1339. |
|
Verdict: APPROVED reviewer1 — original-PR review at F1 and F2 from #1345 are resolved here: MIR IDs distinguish key/script credentials, and the script batch owns a transaction. I checked the main-specific metadata writer: it retains Keep The numbered findings refer to the counterpart review of #1345. No additional blocking finding remains. Validation on combined main: core 244 passed, 1 skipped at |
|
Fault-injection rerun on For this PR:
The baseline is unpatched |




Problem
transaction_metadata,assets,transaction_scriptsandmiruse a random UUID primary key and are saved with JPAsaveAll. When a block is processed a second time, its rows get new ids and are inserted again. This happens on restart: the app rolls back to the cursor block, and on the BlockFetch path it re-fetches from that block inclusive. Tables keyed by natural keys simply overwrite. These four gain one duplicate per row of the block. On a mainnet DB,transaction_metadatahad 76 rows for 38 (tx_hash, label) at one restart slot and 176 for 88 at another.Fix
RollbackEventnow says whether the cursor block will be re-fetched (rollbackPointReprocessed). When it will, the four stores delete from that slot inclusive. This also covers the first restart after upgrading, when the cursor block's rows still have random ids.idprimary key, so a re-processed row overwrites itself:transaction_metadata: tx_hash, labelassets: tx_hash, unit (asset_nameis a decoded, sanitized name and can repeat)transaction_scripts: tx_hash, purpose, redeemer_index (one row per redeemer)mir: tx_hash, cert_index, credential (null for pot-to-pot transfers)No schema change or migration. Existing rows keep their random ids and new rows get deterministic ones, so existing databases need no resync. On MySQL the id is bound as
binary(16), as Hibernate writes it. metadata and mir are added to the jOOQ codegen modules.Rows already duplicated by earlier restarts are not cleaned up. That needs a one-off dedupe per table.
Tests
UUIDUtilTest: deterministic, version 5 with the IETF variant, length-prefixed parts don't collide, null is distinct.StartServiceTestfor the reprocessed flag.ON CONFLICT (id) DO UPDATE), MySQL (ON DUPLICATE KEY UPDATE) and H2 (MERGE), but it has not yet been run against a real PostgreSQL or MySQL.🤖 Generated with Claude Code
https://claude.ai/code/session_01JKakmKURLn4uc33hgrz2hF