Skip to content

Conversation

@wolf31o2
Copy link
Member

@wolf31o2 wolf31o2 commented Nov 7, 2025

Implements a unified certificate persistence system to handle all Cardano certificate types through a single interface.

Changes

  • Add SetCertificate method handling all certificate types (stake, pool, DRep, vote, committee, genesis, MIR, Leios)
  • Switch interface from uint64 slot to ocommon.Point for consistency
  • Implement dual storage strategy with specialized tables and unified mapping table
  • Add comprehensive test suite with 19 test cases covering all certificate types
  • Remove deprecated certificate setter methods in favor of unified interface
  • Add certificate mapping model and update migration list

Testing

  • All 19 certificate tests pass
  • Complete end-to-end validation of certificate persistence
  • No regressions in existing functionality

Closes #878

Summary by CodeRabbit

  • New Features

    • Account active/inactive tracking; transactions now expose Fee and TTL; new record types for genesis-key-delegation and Leios endorser-block; ledger can calculate per-certificate deposits.
  • Refactor

    • Metadata storage reorganized for per-certificate persistence, stronger uniqueness/indexing, deduplication, and improved SQLite initialization/cleanup.
  • Chores / API

    • Many legacy per-certificate setter methods removed; SetTransaction API now accepts precomputed deposits.
  • Tests

    • Added idempotency and duplicate-prevention tests for certificate persistence.

@wolf31o2 wolf31o2 requested a review from a team as a code owner November 7, 2025 23:01
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 7, 2025

📝 Walkthrough

Walkthrough

Certificate persistence was centralized: per-certificate Set* methods across database and sqlite metadata implementations were removed and replaced by a single MetadataStore.SetTransaction that accepts a deposits map. Ledger now computes certificate deposits via LedgerState.CalculateCertificateDeposits and passes that map into SetTransaction; ledger delta logic was updated accordingly. A new top-level certificates table model was added and several certificate-specific models were introduced (GenesisKeyDelegation, LeiosEb, MoveInstantaneousRewards). Many GORM model tag/index adjustments and field renames were applied. SQLite metadata plugin init/close behavior and account retrieval/upsert semantics were adjusted. Transaction and metadata APIs were reworked to carry certificate deposit info.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • High-attention areas:
    • database/plugin/metadata/sqlite/transaction.go — large, dense refactor of SetTransaction, per-type certificate persistence, conflict resolution, bulk cleanup, and many helpers.
    • ledger/certs.go and ledger/delta.go — new CalculateCertificateDeposits and integration into transaction storage flow; era/pparams usage and error handling.
    • database/plugin/metadata/store.go, database/transaction.go, and database/transaction.go call sites — interface/signature changes (SetTransaction, types) and their propagation.
    • database/models/* and models.MigrateModels — new certificates table, new models, many GORM tag/index changes with migration and uniqueness implications.
    • database/plugin/metadata/sqlite/database.go and plugin.go — SQLite initialization, PRAGMA/ WAL and dataDir handling, and removed plugin option.

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main objective: implementing a unified certificate persistence system to consolidate certificate handling across all Cardano certificate types.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #878: creates a unified certificates tracking table, mirrors ledger.Transaction interface structure, and retains specialized per-type certificate tables.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the unified certificate persistence implementation. Minor collateral changes like adding testify dependency, updating UTXO model fields, and Drep credential renaming are all supporting changes for the certificate system.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/certificate-persistence-unified

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6040f74 and 3443840.

📒 Files selected for processing (32)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (3 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (2 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/utxo.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (5 hunks)
  • database/plugin/metadata/sqlite/drep.go (2 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (6 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • database/plugin/metadata/sqlite/certs.go
  • database/plugin/metadata/sqlite/plugin.go
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/pool.go
  • database/account.go
  • database/drep.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • database/models/move_instantaneous_rewards.go
  • database/models/account_test.go
  • database/models/leios_eb.go
  • internal/config/config_test.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/drep.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
  • database/plugin/metadata/sqlite/utxo.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/models/transaction.go
  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
  • database/models/genesis_key_delegation.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/utxo.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/models/utxo.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/sqlite/certs_test.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/sqlite/certs_test.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
🧬 Code graph analysis (9)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/transaction.go (1)
database/models/utxo.go (1)
  • Utxo (24-40)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/plugin/metadata/sqlite/transaction.go (11)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/account.go (20)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
  • StakeDeregistration (96-101)
  • StakeDeregistration (103-105)
  • Registration (72-78)
  • Registration (80-82)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
database/models/pool.go (10)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-23)
  • AuthCommitteeHot (25-27)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-24)
  • ResignCommitteeCold (26-28)
database/models/drep.go (7)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
  • Drep (21-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/certs_test.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • New (50-123)
database/models/pool.go (4)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/account.go (4)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
database/models/drep.go (6)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
ledger/certs.go (1)
ledger/state.go (1)
  • LedgerState (78-102)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • Utxo (24-40)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (54-54)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: go-test (1.25.x, ubuntu-latest)
  • GitHub Check: go-test (1.25.x, macos-latest)
  • GitHub Check: go-test (1.24.x, macos-latest)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: lint
  • GitHub Check: Analyze (go)
🔇 Additional comments (38)
database/models/utxo.go (1)

39-39: LGTM! IsCollateralReturn field addition is well-placed.

The new boolean field with an index is appropriately positioned after OutputIdx and supports efficient queries for collateral return UTXOs.

go.mod (1)

22-22: LGTM! Testify addition supports comprehensive test suite.

The addition of github.com/stretchr/testify as a direct dependency aligns with the 19-test suite added in this PR. The promotion of golang.org/x/text to a direct dependency indicates it's now used by the certificate persistence implementation.

Also applies to: 32-32

database/models/auth_committee_hot.go (1)

18-21: Composite unique index correctly enforces hot authorization uniqueness.

The composite index uniq_auth_committee_hot_cert across ColdCredential and CertificateID appropriately prevents duplicate committee hot authorizations while allowing the same cold credential to appear in different certificates.

database/models/genesis_key_delegation.go (1)

17-24: LGTM! Composite unique index enables idempotent genesis key delegation persistence.

The composite unique constraint uniq_genesis_delegation across GenesisHash, GenesisDelegateHash, and VrfKeyHash correctly enforces idempotency for OnConflict-based insertions as required by the persistence logic.

database/models/models.go (1)

29-31: LGTM! New certificate models correctly added to migration list.

The three new models (GenesisKeyDelegation, LeiosEb, MoveInstantaneousRewards) are appropriately included in MigrateModels to ensure their tables are created during database initialization.

database/plugin/metadata/sqlite/database.go (3)

187-202: LGTM! Close() now properly stops the vacuum scheduler before closing the database.

The implementation correctly:

  • Acquires the timer mutex and sets the closed flag
  • Stops the vacuum timer to prevent further scheduling
  • Releases the mutex before potentially blocking on DB().DB()
  • Wraps errors with context

This addresses the issue of vacuum operations continuing after Close().


71-80: LGTM! Directory validation and creation logic is robust.

The implementation properly:

  • Distinguishes between non-existent directories and permission/access errors
  • Creates the directory hierarchy with appropriate permissions
  • Provides clear error messages for both failure modes

86-87: LGTM! WAL mode configuration optimizes SQLite for concurrent access.

The PRAGMA settings are appropriate for a persistent metadata store:

  • journal_mode=WAL enables better concurrency
  • sync=OFF improves write performance (acceptable for metadata that can be reconstructed)
  • Increased cache size reduces I/O
database/models/resign_committee_cold.go (1)

17-24: LGTM! Composite unique index correctly enforces resignation uniqueness.

The composite index uniq_resign_committee_cold_cert across ColdCredential and CertificateID appropriately prevents duplicate committee cold resignations while maintaining consistency with the pattern used in auth_committee_hot.go.

database/models/pool.go (2)

55-55: LGTM! PoolRegistration unique index correctly enforces certificate idempotency while allowing pool re-registrations.

The unique constraint on CertificateID alone (line 55) ensures idempotent certificate persistence, while PoolKeyHash remains a non-unique index (line 48), correctly allowing the same pool to submit multiple registration certificates over time to update parameters.


92-96: LGTM! PoolRetirement composite unique index appropriately constrains retirement timing.

The composite unique constraint uniq_pool_retirement across PoolKeyHash and Epoch (lines 92, 96) correctly allows one retirement per pool per epoch, while the separate unique constraint on CertificateID (line 93) ensures certificate-level idempotency.

database/plugin/metadata/sqlite/utxo.go (1)

22-22: LGTM! Import alias and type signature updates are correct.

The changes consistently update from ledger to lcommon alias and migrate address/transaction input types to use lcommon.Address and lcommon.TransactionInput. The query logic correctly uses payment_key (matching the model field) and the Blake2b224 comparisons are appropriate.

Also applies to: 98-98, 229-229

database/models/transaction.go (2)

22-25: LGTM! Collateral output filtering correctly implemented.

The where:is_collateral_return clauses properly distinguish regular outputs from collateral returns, leveraging the IsCollateralReturn field in the Utxo model. The snake_case column names match GORM's default naming convention.


28-29: LGTM! Type simplification from custom wrapper to native uint64.

Changing Fee and TTL from types.Uint64 to native uint64 simplifies the model and reduces indirection. This is a sensible refactor for basic numeric fields.

database/certificate.go (1)

21-39: LGTM! Certificate persistence correctly centralized through SetTransaction.

The updated comments clearly document that certificate persistence is now handled via SetTransaction, with the ledger calculating deposits and passing them through in a single operation. This architectural shift reduces API surface by removing individual certificate setters while preserving query methods for pool and stake registrations.

database/transaction.go (2)

22-29: LGTM! SetTransaction signature correctly expanded to include deposits.

The addition of the deposits map[int]uint64 parameter enables certificate deposit tracking to flow from the ledger layer through to metadata persistence, aligning with the unified certificate persistence system described in the PR objectives.


48-54: LGTM! Metadata persistence call correctly updated with deposits.

The call to d.metadata.SetTransaction now includes the deposits parameter, properly threading certificate deposit information through to the metadata layer while preserving the existing UTXO blob persistence logic.

database/models/drep.go (2)

17-29: LGTM! Field rename improves clarity and consistency.

The rename from Credential to DrepCredential is more descriptive and eliminates ambiguity. The field is correctly tagged with gorm:"index" (not unique), allowing legitimate DRep lifecycle operations (register → deregister → re-register).


35-68: LGTM! Consistent field naming across all DRep certificate models.

The DrepCredential rename is consistently applied across DeregistrationDrep, RegistrationDrep, and UpdateDrep models, maintaining structural coherence.

ledger/delta.go (2)

52-72: LGTM! Invalid transactions correctly skip deposit calculation.

The code properly sets deposits to nil for invalid transactions (line 60), aligning with Cardano ledger rules where phase-2 validation failures do not apply certificate effects or deposit changes—only collateral is collected.


74-96: LGTM! Valid transaction deposit calculation correctly implemented.

The deposit calculation logic properly:

  • Only calculates deposits when certificates are present (line 76)
  • Uses ls.CalculateCertificateDeposits to compute the deposits map
  • Handles errors from deposit calculation (lines 81-83)
  • Passes the deposits map to SetTransaction (line 91)

This ensures certificate deposits are tracked for valid transactions while avoiding unnecessary computation when no certificates are present.

database/models/account.go (2)

28-28: LGTM! Account.StakingKey correctly uses non-unique index.

Changing from uniqueIndex to index allows multiple account records per staking key, which is appropriate for tracking account state changes over time.


60-66: LGTM! Composite unique constraints correctly prevent duplicate certificates.

The uniqueIndex combining StakingKey and CertificateID ensures each certificate is stored once while allowing multiple certificates per stake key. This design properly supports legitimate stake key lifecycle operations (register → deregister → re-register) without constraint violations.

Also applies to: 107-113

database/plugin/metadata/sqlite/drep.go (2)

24-41: LGTM! GetDrep query correctly updated for renamed field.

The query predicates consistently use "drep_credential = ?" in both transactional and non-transactional paths, aligning with the model field rename from Credential to DrepCredential.


43-73: LGTM! SetDrep correctly updated with renamed field and conflict resolution.

The method properly:

  • Uses DrepCredential field name matching the model (line 53)
  • Updates the OnConflict column reference to "drep_credential" (line 60)
  • Maintains upsert semantics with UpdateAll: true

This ensures DRep records can be updated without constraint violations while tracking the correct credential field.

database/models/certificate.go (1)

17-30: LGTM!

The Certificate model design is well-structured for unified certificate mapping. The composite unique index on (transaction_id, cert_index) correctly prevents duplicate certificates at the same position, and the cascade delete constraint ensures proper cleanup when transactions are removed.

database/plugin/metadata/sqlite/certs_test.go (4)

13-95: LGTM!

The pool certificate tests correctly verify:

  • Re-registration is allowed (multiple PoolRegistration records with different CertificateIDs)
  • Retirement uniqueness on (pool_key_hash, epoch) composite key

The fix to create fresh structs in each loop iteration ensures the tests validate the actual business constraints rather than primary key collisions.


97-197: LGTM!

The stake certificate tests correctly verify the uniqueness constraint on certificate_id:

  • Multiple registrations/deregistrations for the same staking key are allowed when using different CertificateIDs
  • Duplicate CertificateID values are correctly rejected

The tests validate the intended behavior after the fix to use fresh structs in each iteration.


199-294: LGTM!

The DRep certificate tests correctly verify that multiple operations (registration, deregistration, update) are allowed for the same DRep credential, matching the model schemas which permit historical tracking of DRep state changes.


296-380: LGTM!

The genesis key delegation test correctly validates idempotent insertion using ON CONFLICT DO NOTHING. The test properly handles GORM's behavior of leaving ID at 0 when a conflict occurs by re-querying to retrieve the existing record's ID.

ledger/certs.go (1)

23-60: LGTM!

The CalculateCertificateDeposits method correctly:

  • Computes deposits using the current era's CertDepositFunc with protocol parameters
  • Includes zero-value deposits in the returned map (line 56), ensuring callers can distinguish between "no deposit" and "missing entry"
  • Provides proper error context with certificate index (lines 49-53)

This aligns with the updated transaction persistence flow that requires deposit information for all certificates.

database/plugin/metadata/sqlite/account.go (2)

45-69: LGTM!

The SetAccount signature update to include an active parameter is consistent with the broader move toward explicit account state management. The removal of upsert logic (OnConflict) aligns with the approach of creating historical account records rather than updating in place.


25-43: No action required—code behavior is intentional and validated.

The concern assumes GetAccount filters returning active accounts is a problematic breaking change. However, the evidence shows this behavior is intentional and expected:

  1. Explicit documentation (line 639): The comment "Try to get existing active account" proves callers expect active-only filtering
  2. createOrUpdateAccount: Designed to create or update an active account; expects nil when no active account exists
  3. deactivateAccount: Retrieves an active account to mark it inactive; the logic validates this is correct

All callers correctly handle and depend on this active-only filtering behavior. The implementation is sound.

Likely an incorrect or invalid review comment.

database/plugin/metadata/sqlite/transaction.go (4)

55-379: LGTM!

The SetTransaction implementation correctly handles the updated signature and certificate persistence:

  • Reloads transaction ID after upsert when tmpTx.ID == 0 (lines 112-116)
  • Skips certificate processing for invalid transactions (lines 314-316)
  • Validates that deposits are provided when certificates exist (lines 323-328)
  • Cleans up obsolete certificates before persisting new ones (lines 331-333)
  • Validates deposit presence for certificate types that require deposits (lines 342-359)

The implementation addresses all critical issues raised in past reviews.


381-521: LGTM!

The certificate cleanup logic is well-structured:

  • cleanupObsoleteCertificates correctly identifies certificates that are no longer present by comparing current and existing indices
  • Groups deletions by type for efficient bulk operations
  • bulkRemoveCertificateRecords uses appropriate delete keys (certificate_id vs id) based on each model's schema
  • Handles all certificate types including GenesisKeyDelegation, MoveInstantaneousRewards, and LeiosEb

This ensures certificate data stays consistent when transactions are replayed or modified.


523-784: LGTM!

The certificate storage infrastructure is well-designed:

  • storeCertificate uses a closure pattern for insertCert to handle Certificate mapping creation with proper ID retrieval after conflicts (lines 560-569)
  • snakeToPascal correctly converts snake_case to PascalCase for reflection-based field lookup (lines 621-629), addressing past review concerns
  • Helper functions (createOrUpdateAccount, deactivateAccount, createOrUpdateDrep) encapsulate common account and DRep state management patterns
  • createCertificateModel provides generic conflict resolution with proper ID retrieval after upsert (lines 756-775)

The implementation addresses all critical issues from past reviews regarding ID retrieval and field name conversion.


786-1858: LGTM!

The per-certificate-type storage handlers follow a consistent and correct pattern:

  • Create or update related entities (accounts, pools, DReps) as needed
  • Create the specialized certificate record using createCertificateModel
  • Call insertCert to create the Certificate mapping record
  • Update the CertificateID field on the specialized record to link back to the mapping

Special cases like GenesisKeyDelegation correctly use ON CONFLICT DO NOTHING for idempotency and re-query when the ID is 0 (lines 1725-1736).

The implementation is comprehensive, covering all certificate types mentioned in the PR objectives.

database/plugin/metadata/store.go (1)

29-147: LGTM!

The MetadataStore interface updates are consistent with the implementation:

  • SetTransaction signature correctly includes the deposits map[int]uint64 parameter to support certificate deposit tracking (lines 126-132)
  • UTXO methods use canonical lcommon.Address and lcommon.TransactionInput types (lines 143, 145), addressing past review concerns
  • Removal of per-certificate setter methods aligns with the unified transaction-based certificate persistence flow

The interface accurately reflects the refactored certificate persistence system.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6540fb2 and 38f903c.

📒 Files selected for processing (14)
  • database/account.go (0 hunks)
  • database/certificates.go (1 hunks)
  • database/certs.go (0 hunks)
  • database/drep.go (0 hunks)
  • database/models/certificates.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (0 hunks)
  • database/plugin/metadata/sqlite/database.go (2 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/store.go (1 hunks)
  • go.mod (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/certs_test.go (1 hunks)
  • ledger/delta.go (1 hunks)
💤 Files with no reviewable changes (5)
  • database/certs.go
  • database/plugin/metadata/sqlite/drep.go
  • database/drep.go
  • database/plugin/metadata/sqlite/account.go
  • database/account.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/certs_test.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • ledger/certs_test.go
📚 Learning: 2025-10-22T20:19:25.277Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:434-434
Timestamp: 2025-10-22T20:19:25.277Z
Learning: In this repo’s tests, use models.Block.Decode() to obtain a ledger.Block before calling chain.Chain.AddBlock, since AddBlock expects a ledger.Block, while LedgerState.GetBlock returns a database/models.Block.

Applied to files:

  • ledger/certs_test.go
🧬 Code graph analysis (5)
database/models/models.go (1)
database/models/certificates.go (2)
  • Certificates (20-26)
  • Certificates (28-30)
database/certificates.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
ledger/certs_test.go (6)
database/plugin/metadata/sqlite/database.go (2)
  • New (256-328)
  • MetadataStoreSqlite (247-253)
chain/manager.go (2)
  • NewManager (46-61)
  • ChainManager (37-44)
ledger/state.go (2)
  • NewLedgerState (104-123)
  • LedgerStateConfig (58-68)
database/database.go (1)
  • Database (45-50)
database/models/certificates.go (2)
  • Certificates (20-26)
  • Certificates (28-30)
database/models/pool.go (1)
  • Pool (26-38)
database/plugin/metadata/sqlite/database.go (7)
database/models/certificates.go (2)
  • Certificates (20-26)
  • Certificates (28-30)
database/models/account.go (20)
  • StakeRegistration (81-87)
  • StakeRegistration (89-91)
  • StakeDeregistration (70-75)
  • StakeDeregistration (77-79)
  • StakeDelegation (58-64)
  • StakeDelegation (66-68)
  • Registration (46-52)
  • Registration (54-56)
  • Deregistration (35-40)
  • Deregistration (42-44)
  • StakeRegistrationDelegation (93-100)
  • StakeRegistrationDelegation (102-104)
  • VoteDelegation (133-139)
  • VoteDelegation (141-143)
  • StakeVoteDelegation (106-113)
  • StakeVoteDelegation (115-117)
  • VoteRegistrationDelegation (145-152)
  • VoteRegistrationDelegation (154-156)
  • StakeVoteRegistrationDelegation (119-127)
  • StakeVoteRegistrationDelegation (129-131)
database/models/pool.go (5)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
database/types/types.go (1)
  • Uint64 (54-54)
database/models/drep.go (7)
  • Drep (17-25)
  • RegistrationDrep (43-51)
  • RegistrationDrep (53-55)
  • DeregistrationDrep (31-37)
  • DeregistrationDrep (39-41)
  • UpdateDrep (57-64)
  • UpdateDrep (66-68)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-23)
  • ResignCommitteeCold (25-27)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/txn.go (1)
  • Txn (27-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: lint
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 38f903c to 1f75a2d Compare November 7, 2025 23:58
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
ledger/certs.go (1)

35-53: Populate deposit before calling SetCertificate.

deposit stays zero for every certificate because this loop never sets it. The metadata/store path needs the real deposit value to persist stake registration/deregistration funding and refunds; sending zero regresses deposit accounting in the DB. Please restore the prior deposit calculation (the logic that consulted protocol parameters before this refactor) and pass that value into SetCertificate, returning an error if the lookup fails.

database/plugin/metadata/sqlite/database.go (4)

35-43: Populate certificates with the owning transaction.

insertCert always writes TransactionID: 0, so every row in certificates ends up detached from the transaction it belongs to. We already have txHash in scope—please look up the models.Transaction row once (inside SetCertificate) and use its ID when creating the mapping. Without that link the new unified table cannot satisfy the "track transaction certificates" requirement.

Apply this diff to fix the issue:

+	var txRow models.Transaction
+	if err := txn.Where(&models.Transaction{Hash: txHash}).First(&txRow).Error; err != nil {
+		return fmt.Errorf("lookup transaction for certificate: %w", err)
+	}
 	// Helper closure to insert certificates mapping row
 	insertCert := func(certType uint, targetID uint) error {
 		certRow := models.Certificates{
-			TransactionID: 0,
+			TransactionID: txRow.ID,
 			CertIndex:     uint(blockIndex),
 			CertType:      certType,
 			CertificateID: targetID,

76-88: Persist full pool registration payload.

The pool registration branch no longer records margin, metadata (URL/hash), owners, or relays, all of which were persisted by the old SetPoolRegistration path. Dropping them is a data-loss regression. Please restore the previous field population (including helper logic for owners/relays) before inserting the model so the unified method keeps parity with the legacy setters.


130-168: Do not drop the DRep payload.

All vote-related certificates leave models.*.Drep empty (or missing entirely), so every delegation/registration written through the new path loses the DRep selection. That breaks #878's goal of tracking certificate content. Please carry over the DRep serialization we had in the removed SetVoteDelegation, SetStakeVoteDelegation, SetVoteRegistrationDelegation, and SetStakeVoteRegistrationDelegation implementations so the bytes written to the DB still identify the chosen DRep.


180-216: Restore anchor fields for DRep/committee certs.

ResignCommitteeColdCertificate, RegistrationDrepCertificate, DeregistrationDrepCertificate, and UpdateDrepCertificate all expose anchor data (URL/hash) that we currently discard. The legacy setters populated AnchorUrl/AnchorHash; without that the new unified flow loses essential governance metadata. Please reapply the anchor assignments from the old code before inserting the models.

🧹 Nitpick comments (2)
ledger/delta.go (1)

101-103: Remove duplicate transaction index validation.

This range check duplicates the validation already performed at lines 49-50. The transaction index is already verified before processing begins.

Apply this diff to remove the duplicate check:

 		// Certificates
 		txHash := tr.Tx.Hash()
 		txHashBytes := txHash.Bytes()
-		if tr.Index < 0 || tr.Index > 4294967295 {
-			return fmt.Errorf("transaction index out of range: %d", tr.Index)
-		}
 		err = ls.ProcessTransactionCertificates(
ledger/certs.go (1)

54-56: Remove redundant certificate index validation.

The idx variable comes from a range loop over the certs slice (line 34), so it's always within valid bounds (0 to len(certs)-1). This explicit range check against math.MaxUint32 is unnecessary.

Apply this diff to remove the redundant check:

-		if idx < 0 || idx > 4294967295 {
-			return fmt.Errorf("certificate index out of range: %d", idx)
-		}
 		if err := ls.db.SetCertificate(cert, txHash, uint32(idx), blockPoint, deposit, amount, txn); err != nil {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38f903c and 1f75a2d.

📒 Files selected for processing (12)
  • database/account.go (0 hunks)
  • database/certificates.go (1 hunks)
  • database/certs.go (0 hunks)
  • database/drep.go (0 hunks)
  • database/models/certificates.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (0 hunks)
  • database/plugin/metadata/sqlite/database.go (2 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/store.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (1 hunks)
💤 Files with no reviewable changes (5)
  • database/certs.go
  • database/plugin/metadata/sqlite/drep.go
  • database/drep.go
  • database/account.go
  • database/plugin/metadata/sqlite/account.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • database/models/models.go
  • database/models/certificates.go
  • database/certificates.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/database.go
🧬 Code graph analysis (3)
ledger/delta.go (1)
database/models/certificates.go (2)
  • Certificates (20-26)
  • Certificates (28-30)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/database.go (7)
database/models/certificates.go (2)
  • Certificates (20-26)
  • Certificates (28-30)
database/models/account.go (20)
  • StakeRegistration (81-87)
  • StakeRegistration (89-91)
  • StakeDeregistration (70-75)
  • StakeDeregistration (77-79)
  • StakeDelegation (58-64)
  • StakeDelegation (66-68)
  • Registration (46-52)
  • Registration (54-56)
  • Deregistration (35-40)
  • Deregistration (42-44)
  • StakeRegistrationDelegation (93-100)
  • StakeRegistrationDelegation (102-104)
  • VoteDelegation (133-139)
  • VoteDelegation (141-143)
  • StakeVoteDelegation (106-113)
  • StakeVoteDelegation (115-117)
  • VoteRegistrationDelegation (145-152)
  • VoteRegistrationDelegation (154-156)
  • StakeVoteRegistrationDelegation (119-127)
  • StakeVoteRegistrationDelegation (129-131)
database/models/pool.go (5)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
database/types/types.go (1)
  • Uint64 (54-54)
database/models/drep.go (7)
  • Drep (17-25)
  • RegistrationDrep (43-51)
  • RegistrationDrep (53-55)
  • DeregistrationDrep (31-37)
  • DeregistrationDrep (39-41)
  • UpdateDrep (57-64)
  • UpdateDrep (66-68)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-23)
  • ResignCommitteeCold (25-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (2)
database/plugin/metadata/sqlite/database.go (1)

262-268: LGTM: In-memory configuration aligns with shared-cache mode.

The change to use file::memory:?cache=shared with SkipDefaultTransaction: false properly enables concurrent transaction support for in-memory testing scenarios.

database/plugin/metadata/store.go (1)

100-109: LGTM: Clean consolidation of certificate storage interface.

The unified SetCertificate method successfully replaces 16 specialized setters with a single, well-documented API. This simplifies the interface while providing all necessary parameters (certificate, transaction hash, index, block point, deposit, and amount) for complete certificate persistence.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 1f75a2d to d567984 Compare November 8, 2025 16:14
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
database/models/certificates.go (2)

20-26: Consider adding temporal metadata for certificate queries.

The Certificates mapping table currently lacks slot and block hash fields, which could make temporal queries (e.g., "certificates active at slot X") more expensive since they would require joining through the Transaction table. If certificate lifecycle queries by slot or block are anticipated, consider adding:

 type Certificates struct {
 	ID            uint `gorm:"primaryKey"`
 	TransactionID uint `gorm:"index"`
+	Slot          uint64 `gorm:"index"`
+	BlockHash     []byte
 	CertIndex     uint `gorm:"column:cert_index"` // Index of the certificate in the transaction
 	CertType      uint // Type of certificate (matches lcommon.Certificate type)
 	CertificateID uint `gorm:"index"` // FK to the appropriate certificate table (e.g., certs, registration, etc.)
 }

25-25: Document the implicit foreign key relationship.

CertificateID is described as a FK to various certificate tables, but GORM cannot enforce a foreign key constraint when the target table varies by CertType. Consider adding a comment clarifying this is a logical (not database-enforced) FK and that referential integrity must be maintained at the application layer.

-	CertificateID uint `gorm:"index"` // FK to the appropriate certificate table (e.g., certs, registration, etc.)
+	CertificateID uint `gorm:"index"` // Logical FK to the appropriate certificate table based on CertType (e.g., certs, pool_registration, etc.). Not enforced by DB constraints.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f75a2d and d567984.

📒 Files selected for processing (15)
  • database/account.go (0 hunks)
  • database/certificates.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/certificates.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (0 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/pparams.go (2 hunks)
  • database/transaction.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
💤 Files with no reviewable changes (4)
  • database/plugin/metadata/sqlite/drep.go
  • database/plugin/metadata/sqlite/account.go
  • database/account.go
  • database/drep.go
✅ Files skipped from review due to trivial changes (1)
  • internal/config/config_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • database/models/models.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: utxorpc tests must pass github.com/blinklabs-io/gouroboros/ledger/common.Block to chain.Chain.AddBlock. They should create database/models.Block only as a storage row, then call models.Block.Decode() to obtain a ledger/common.Block for AddBlock and any ledger/protocol-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: chain.Chain.AddBlock expects a github.com/blinklabs-io/gouroboros/ledger.Block. In tests, construct a database/models.Block only as a container and call its Decode() to obtain the gouroboros ledger.Block (which also satisfies github.com/blinklabs-io/gouroboros/ledger/common.Block) before inserting into the chain.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (5)
database/certificates.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/transaction.go (7)
database/models/certificates.go (2)
  • Certificates (20-26)
  • Certificates (28-30)
database/models/account.go (20)
  • StakeRegistration (81-87)
  • StakeRegistration (89-91)
  • StakeDeregistration (70-75)
  • StakeDeregistration (77-79)
  • StakeDelegation (58-64)
  • StakeDelegation (66-68)
  • Registration (46-52)
  • Registration (54-56)
  • Deregistration (35-40)
  • Deregistration (42-44)
  • StakeRegistrationDelegation (93-100)
  • StakeRegistrationDelegation (102-104)
  • VoteDelegation (133-139)
  • VoteDelegation (141-143)
  • StakeVoteDelegation (106-113)
  • StakeVoteDelegation (115-117)
  • VoteRegistrationDelegation (145-152)
  • VoteRegistrationDelegation (154-156)
  • StakeVoteRegistrationDelegation (119-127)
  • StakeVoteRegistrationDelegation (129-131)
database/models/pool.go (9)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/drep.go (7)
  • Drep (17-25)
  • RegistrationDrep (43-51)
  • RegistrationDrep (53-55)
  • DeregistrationDrep (31-37)
  • DeregistrationDrep (39-41)
  • UpdateDrep (57-64)
  • UpdateDrep (66-68)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-23)
  • ResignCommitteeCold (25-27)
database/plugin/metadata/sqlite/database.go (2)
database/database.go (1)
  • Config (35-42)
database/plugin/log.go (1)
  • Logger (18-27)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/utxo.go (1)
  • Utxo (23-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: nilaway
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: Analyze (go)
🔇 Additional comments (11)
database/pparams.go (1)

32-45: Correct error propagation fix.

Switching to ppErr ensures we actually surface metadata failures instead of silently returning decoded results.

ledger/delta.go (1)

71-90: Deposits wiring looks solid.

Calculating deposits up front and threading them through the persistence path keeps the invalid-transaction shortcut intact while enriching the valid path.

database/transaction.go (1)

47-54: Metadata call update matches new API.

Forwarding certificateDeposits here cleanly bridges the ledger layer to the metadata store.

database/certificates.go (1)

21-38: Docs now reflect unified persistence.

Nice to see the comments pointing callers at SetTransaction; that prevents misuse of the old setters.

ledger/certs.go (2)

26-50: LGTM: Deposit calculation correctly implemented.

The deposit calculation logic properly:

  • Iterates through certificates and computes deposits using the current era's deposit function
  • Populates the deposits slice at each index (line 46)
  • Provides clear error context including the certificate index
  • Returns the complete deposits array for downstream storage

The past review concern about deposits staying zero has been addressed.


52-67: LGTM: Clear deprecation strategy.

The deprecation approach is well-executed:

  • Returns an explicit error rather than silently failing
  • Error message directs developers to the replacement pattern (CalculateCertificateDeposits + SetTransaction)
  • Function signature retained for backward compatibility during transition
database/plugin/metadata/sqlite/database.go (3)

84-85: Verify sync OFF is acceptable for your consistency requirements.

The persistent database uses _pragma=sync(OFF) which disables fsync, trading durability for performance. If the process crashes or the system loses power, recent commits may be lost. For a blockchain indexer that can resync from the network, this is often acceptable. However, if your use case requires strict durability guarantees or you lack easy resync capability, consider using _pragma=sync(NORMAL) instead.


175-189: LGTM: Proper cleanup in Close().

The Close() method now stops the vacuum timer (lines 179-181) before closing the database connection, preventing potential goroutine leaks. Error wrapping provides clear context if obtaining the DB handle fails.


191-228: LGTM: Wrapper methods expose GORM operations.

The wrapper methods (Create, DB, First, Order, Transaction, Where) provide a clean public API for database operations. The documentation comments clearly describe each method's purpose, and the Transaction() wrapper appropriately calls Begin() to start a new transaction.

database/plugin/metadata/store.go (2)

126-132: LGTM: SetTransaction signature aligns with new deposit calculation flow.

The updated signature correctly:

  • Adds uint32 transaction index for certificate position tracking within the block
  • Adds []uint64 certificate deposits that match the return type from LedgerState.CalculateCertificateDeposits
  • Maintains the *gorm.DB transaction parameter for atomic operations

This change enables the ledger layer to calculate deposits once and pass them to the database layer for storage, avoiding redundant protocol parameter lookups.


143-145: LGTM: Type consolidation improves consistency.

Changing from ledger.Address/ledger.TransactionInput to lcommon.Address/lcommon.TransactionInput standardizes the interface to use common ledger types throughout. This reduces import confusion and aligns with the broader codebase pattern of using lcommon.* types at API boundaries.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from d567984 to 051af00 Compare November 8, 2025 17:02
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
database/plugin/metadata/sqlite/transaction.go (1)

667-690: Specialized certificate tables still leak on retries
cleanupExistingCertificates only purges the mapping rows; the specialised tables keep the previous inserts. When SetTransaction replays a block (which is expected on restart or rollback), every certificate is duplicated in stake_registration, pool_registration, etc. Downstream queries that read those tables directly now see phantom entries, and storage bloat compounds on every retry. Please delete the underlying per-type rows before dropping the mappings, just like the removed setters did.

 func (d *MetadataStoreSqlite) cleanupExistingCertificates(
 	transactionID uint,
 	txn *gorm.DB,
 ) error {
-	// Delete from the certificates mapping table
-	// Note: This relies on application-level cleanup since we can't use FK cascades
-	// due to the polymorphic nature of CertificateID
-	result := txn.Where("transaction_id = ?", transactionID).
-		Delete(&models.Certificates{})
-	if result.Error != nil {
-		return fmt.Errorf("delete certificates mapping: %w", result.Error)
-	}
-
-	// Note: Individual certificate records in specialized tables (stake_registration,
-	// pool_registration, etc.) are not automatically cleaned up here since they don't
-	// have back-references to the certificates table. In practice, this is acceptable
-	// since the mapping table is the primary way to access certificates by transaction,
-	// and orphaned records in specialized tables don't affect correctness.
-	//
-	// If cleanup of specialized tables becomes necessary, it would require:
-	// 1. First querying certificates table for the certificate IDs and types
-	// 2. Then deleting from each specialized table based on the certificate type
-	// However, this adds significant complexity and may not be worth the storage overhead.
-	//
-
-	return nil
+	var existing []models.Certificates
+	if err := txn.
+		Where("transaction_id = ?", transactionID).
+		Find(&existing).Error; err != nil {
+		return fmt.Errorf("lookup certificates for transaction %d: %w", transactionID, err)
+	}
+
+	for _, certRow := range existing {
+		if err := d.deleteCertificateRecord(txn, certRow.CertType, certRow.CertificateID); err != nil {
+			return fmt.Errorf(
+				"delete certificate %d (type %d): %w",
+				certRow.CertificateID,
+				certRow.CertType,
+				err,
+			)
+		}
+	}
+
+	if err := txn.
+		Where("transaction_id = ?", transactionID).
+		Delete(&models.Certificates{}).Error; err != nil {
+		return fmt.Errorf("delete certificates mapping: %w", err)
+	}
+
+	return nil
 }
+
+func (d *MetadataStoreSqlite) deleteCertificateRecord(
+	txn *gorm.DB,
+	certType uint,
+	certID uint,
+) error {
+	switch certType {
+	case lcommon.CertificateTypeStakeRegistration:
+		return txn.Delete(&models.StakeRegistration{}, certID).Error
+	case lcommon.CertificateTypeStakeDeregistration:
+		return txn.Delete(&models.StakeDeregistration{}, certID).Error
+	case lcommon.CertificateTypeStakeDelegation:
+		return txn.Delete(&models.StakeDelegation{}, certID).Error
+	case lcommon.CertificateTypePoolRegistration:
+		return txn.Delete(&models.PoolRegistration{}, certID).Error
+	case lcommon.CertificateTypePoolRetirement:
+		return txn.Delete(&models.PoolRetirement{}, certID).Error
+	case lcommon.CertificateTypeRegistration:
+		return txn.Delete(&models.Registration{}, certID).Error
+	case lcommon.CertificateTypeDeregistration:
+		return txn.Delete(&models.Deregistration{}, certID).Error
+	case lcommon.CertificateTypeStakeRegistrationDelegation:
+		return txn.Delete(&models.StakeRegistrationDelegation{}, certID).Error
+	case lcommon.CertificateTypeVoteDelegation:
+		return txn.Delete(&models.VoteDelegation{}, certID).Error
+	case lcommon.CertificateTypeStakeVoteDelegation:
+		return txn.Delete(&models.StakeVoteDelegation{}, certID).Error
+	case lcommon.CertificateTypeVoteRegistrationDelegation:
+		return txn.Delete(&models.VoteRegistrationDelegation{}, certID).Error
+	case lcommon.CertificateTypeStakeVoteRegistrationDelegation:
+		return txn.Delete(&models.StakeVoteRegistrationDelegation{}, certID).Error
+	case lcommon.CertificateTypeAuthCommitteeHot:
+		return txn.Delete(&models.AuthCommitteeHot{}, certID).Error
+	case lcommon.CertificateTypeResignCommitteeCold:
+		return txn.Delete(&models.ResignCommitteeCold{}, certID).Error
+	case lcommon.CertificateTypeRegistrationDrep:
+		return txn.Delete(&models.RegistrationDrep{}, certID).Error
+	case lcommon.CertificateTypeDeregistrationDrep:
+		return txn.Delete(&models.DeregistrationDrep{}, certID).Error
+	case lcommon.CertificateTypeUpdateDrep:
+		return txn.Delete(&models.UpdateDrep{}, certID).Error
+	case lcommon.CertificateTypeGenesisKeyDelegation,
+		lcommon.CertificateTypeMoveInstantaneousRewards,
+		lcommon.CertificateTypeLeiosEb:
+		return txn.Delete(&models.Certificate{}, certID).Error
+	default:
+		return txn.Delete(&models.Certificate{}, certID).Error
+	}
+}
🧹 Nitpick comments (1)
database/models/certificates.go (1)

24-30: Add indexes to enforce lookup & uniqueness.

Right now BlockHash isn’t indexed and (TransactionID, CertIndex) isn’t enforced unique. Block-level fetches will scan the whole table, and duplicate rows for the same certificate can slip in, defeating the mapping guarantee. Please add an index on BlockHash and a shared unique index across TransactionID and CertIndex (e.g. gorm:"index;uniqueIndex:uniq_tx_cert" / gorm:"column:cert_index;uniqueIndex:uniq_tx_cert"). (gorm.io)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d567984 and 051af00.

📒 Files selected for processing (15)
  • database/account.go (0 hunks)
  • database/certificates.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/certificates.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (0 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/pparams.go (2 hunks)
  • database/transaction.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
💤 Files with no reviewable changes (4)
  • database/drep.go
  • database/plugin/metadata/sqlite/account.go
  • database/account.go
  • database/plugin/metadata/sqlite/drep.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/config/config_test.go
  • database/transaction.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: utxorpc tests must pass github.com/blinklabs-io/gouroboros/ledger/common.Block to chain.Chain.AddBlock. They should create database/models.Block only as a storage row, then call models.Block.Decode() to obtain a ledger/common.Block for AddBlock and any ledger/protocol-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: chain.Chain.AddBlock expects a github.com/blinklabs-io/gouroboros/ledger.Block. In tests, construct a database/models.Block only as a container and call its Decode() to obtain the gouroboros ledger.Block (which also satisfies github.com/blinklabs-io/gouroboros/ledger/common.Block) before inserting into the chain.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (7)
database/models/models.go (1)
database/models/certificates.go (2)
  • Certificates (23-31)
  • Certificates (34-36)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/txn.go (1)
  • Txn (27-34)
ledger/delta.go (1)
database/models/certificates.go (2)
  • Certificates (23-31)
  • Certificates (34-36)
database/plugin/metadata/sqlite/database.go (2)
config.go (1)
  • Config (33-55)
database/database.go (1)
  • Config (35-42)
database/plugin/metadata/sqlite/transaction.go (9)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (38-44)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/certificates.go (2)
  • Certificates (23-31)
  • Certificates (34-36)
database/models/account.go (20)
  • StakeRegistration (81-87)
  • StakeRegistration (89-91)
  • StakeDeregistration (70-75)
  • StakeDeregistration (77-79)
  • StakeDelegation (58-64)
  • StakeDelegation (66-68)
  • Registration (46-52)
  • Registration (54-56)
  • Deregistration (35-40)
  • Deregistration (42-44)
  • StakeRegistrationDelegation (93-100)
  • StakeRegistrationDelegation (102-104)
  • VoteDelegation (133-139)
  • VoteDelegation (141-143)
  • StakeVoteDelegation (106-113)
  • StakeVoteDelegation (115-117)
  • VoteRegistrationDelegation (145-152)
  • VoteRegistrationDelegation (154-156)
  • StakeVoteRegistrationDelegation (119-127)
  • StakeVoteRegistrationDelegation (129-131)
database/models/pool.go (9)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/drep.go (7)
  • Drep (17-25)
  • RegistrationDrep (43-51)
  • RegistrationDrep (53-55)
  • DeregistrationDrep (31-37)
  • DeregistrationDrep (39-41)
  • UpdateDrep (57-64)
  • UpdateDrep (66-68)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-23)
  • ResignCommitteeCold (25-27)
database/certificates.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/utxo.go (1)
  • Utxo (23-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: nilaway
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: Analyze (go)

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 051af00 to b942939 Compare November 8, 2025 17:30
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 051af00 and b942939.

📒 Files selected for processing (15)
  • database/account.go (0 hunks)
  • database/certificates.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/certificates.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (0 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/pparams.go (2 hunks)
  • database/transaction.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
💤 Files with no reviewable changes (4)
  • database/drep.go
  • database/plugin/metadata/sqlite/account.go
  • database/plugin/metadata/sqlite/drep.go
  • database/account.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • database/pparams.go
  • internal/config/config_test.go
  • database/models/models.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: utxorpc tests must pass github.com/blinklabs-io/gouroboros/ledger/common.Block to chain.Chain.AddBlock. They should create database/models.Block only as a storage row, then call models.Block.Decode() to obtain a ledger/common.Block for AddBlock and any ledger/protocol-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: chain.Chain.AddBlock expects a github.com/blinklabs-io/gouroboros/ledger.Block. In tests, construct a database/models.Block only as a container and call its Decode() to obtain the gouroboros ledger.Block (which also satisfies github.com/blinklabs-io/gouroboros/ledger/common.Block) before inserting into the chain.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (5)
database/certificates.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/transaction.go (8)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/certificates.go (2)
  • Certificates (19-27)
  • Certificates (30-32)
database/models/account.go (20)
  • StakeRegistration (81-87)
  • StakeRegistration (89-91)
  • StakeDeregistration (70-75)
  • StakeDeregistration (77-79)
  • StakeDelegation (58-64)
  • StakeDelegation (66-68)
  • Registration (46-52)
  • Registration (54-56)
  • Deregistration (35-40)
  • Deregistration (42-44)
  • StakeRegistrationDelegation (93-100)
  • StakeRegistrationDelegation (102-104)
  • VoteDelegation (133-139)
  • VoteDelegation (141-143)
  • StakeVoteDelegation (106-113)
  • StakeVoteDelegation (115-117)
  • VoteRegistrationDelegation (145-152)
  • VoteRegistrationDelegation (154-156)
  • StakeVoteRegistrationDelegation (119-127)
  • StakeVoteRegistrationDelegation (129-131)
database/models/pool.go (9)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/drep.go (7)
  • Drep (17-25)
  • RegistrationDrep (43-51)
  • RegistrationDrep (53-55)
  • DeregistrationDrep (31-37)
  • DeregistrationDrep (39-41)
  • UpdateDrep (57-64)
  • UpdateDrep (66-68)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-23)
  • ResignCommitteeCold (25-27)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/utxo.go (1)
  • Utxo (23-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: nilaway
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: Analyze (go)
🔇 Additional comments (2)
database/plugin/metadata/store.go (2)

126-131: Unified SetTransaction signature looks solid

Passing the []uint64 deposit slice through the interface keeps the metadata layer in sync with the new ledger-side certificate deposit calculation while preserving existing call semantics (nil for “no deposits”). All good here.


143-145: Nice consistency on ledger/common types

Switching to lcommon.Address and lcommon.TransactionInput matches the current ledger/common usage elsewhere, so the interface stays coherent across layers.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from b942939 to 4c8a343 Compare November 8, 2025 17:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
database/models/certificates.go (1)

19-27: Consider explicit foreign key constraint on TransactionID.

The model properly implements the unified mapping table with a composite unique index on (TransactionID, CertIndex). However, there's no explicit GORM foreign key tag on TransactionID to reference the transaction table's ID field.

While GORM's conventions may handle this implicitly, consider making the relationship explicit for clarity and to ensure referential integrity:

-	TransactionID uint   `gorm:"index;uniqueIndex:uniq_tx_cert"`
+	TransactionID uint   `gorm:"index;uniqueIndex:uniq_tx_cert;constraint:OnDelete:CASCADE"`

This would ensure that deleting a transaction cascades to remove its certificate mappings, maintaining referential integrity at the database level.

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b942939 and 4c8a343.

📒 Files selected for processing (15)
  • database/account.go (0 hunks)
  • database/certificates.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/certificates.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (0 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/pparams.go (2 hunks)
  • database/transaction.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
💤 Files with no reviewable changes (4)
  • database/plugin/metadata/sqlite/drep.go
  • database/drep.go
  • database/plugin/metadata/sqlite/account.go
  • database/account.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • database/plugin/metadata/sqlite/database.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: utxorpc tests must pass github.com/blinklabs-io/gouroboros/ledger/common.Block to chain.Chain.AddBlock. They should create database/models.Block only as a storage row, then call models.Block.Decode() to obtain a ledger/common.Block for AddBlock and any ledger/protocol-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: chain.Chain.AddBlock expects a github.com/blinklabs-io/gouroboros/ledger.Block. In tests, construct a database/models.Block only as a container and call its Decode() to obtain the gouroboros ledger.Block (which also satisfies github.com/blinklabs-io/gouroboros/ledger/common.Block) before inserting into the chain.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (7)
database/models/models.go (1)
database/models/certificates.go (2)
  • Certificates (19-27)
  • Certificates (30-32)
ledger/delta.go (1)
database/models/certificates.go (2)
  • Certificates (19-27)
  • Certificates (30-32)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/transaction.go (9)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (38-44)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/certificates.go (2)
  • Certificates (19-27)
  • Certificates (30-32)
database/models/account.go (12)
  • StakeRegistration (81-87)
  • StakeRegistration (89-91)
  • StakeDeregistration (70-75)
  • StakeDeregistration (77-79)
  • StakeDelegation (58-64)
  • StakeDelegation (66-68)
  • Registration (46-52)
  • Registration (54-56)
  • Deregistration (35-40)
  • Deregistration (42-44)
  • StakeRegistrationDelegation (93-100)
  • StakeRegistrationDelegation (102-104)
database/models/pool.go (9)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/drep.go (7)
  • Drep (17-25)
  • RegistrationDrep (43-51)
  • RegistrationDrep (53-55)
  • DeregistrationDrep (31-37)
  • DeregistrationDrep (39-41)
  • UpdateDrep (57-64)
  • UpdateDrep (66-68)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-23)
  • ResignCommitteeCold (25-27)
database/certificates.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/transaction.go (1)
database/database.go (1)
  • Database (45-50)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/utxo.go (1)
  • Utxo (23-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (16)
internal/config/config_test.go (1)

1-13: License header aligns with compliance.

No code behavior changes and the header matches our standard licensing block.

database/pparams.go (1)

33-54: Good catch on propagating metadata errors.

Checking ppErr in both branches ensures we no longer swallow failures from GetPParams.

database/models/models.go (1)

24-24: Migration list update looks correct.

Including &Certificates{} keeps the new unified table in sync with schema migrations.

ledger/delta.go (1)

71-93: Nice integration of certificate deposit handling.

Calculating deposits once per transaction and passing them into SetTransaction keeps the ledger and persistence layers aligned with the new unified model.

database/certificates.go (1)

21-38: Comment updates improve clarity.

Documenting that SetTransaction now owns certificate persistence helps future readers navigate the unified flow.

database/transaction.go (1)

22-54: LGTM!

The signature extension to include certificateDeposits is clean and the forwarding to the metadata layer is correct. The updated comment accurately reflects that certificates are now persisted alongside the transaction.

ledger/certs.go (2)

26-50: LGTM - Deposit calculation properly implemented.

The function correctly computes deposits using protocol parameters via currentEra.CertDepositFunc. The error context includes the certificate index, which aids debugging. This addresses the concern from the previous review about missing deposit calculation.


52-67: Let me conduct a more comprehensive search to ensure there are truly no callers:

Based on the search results, I can now verify the concern. The grep search found no existing callers of ProcessTransactionCertificates in the codebase, which suggests the function may already be unused.

However, the Go community convention is that functions must be kept for compatibility with existing programs. The current implementation returns a hard error, which contradicts Go's backward compatibility principles. An API feature being deprecated does not mean it is going to be removed in the future; on the contrary, Go 1 compatibility means the feature will be preserved in its deprecated form to keep existing programs running.

Since the search found no internal callers and the deprecation appears intentional, verify whether this PR is part of a major version bump or an intentional breaking change before merging.

database/plugin/metadata/sqlite/transaction.go (6)

97-102: LGTM - Transaction ID reload resolves prior concern.

The transaction ID is now properly reloaded after the upsert when tmpTx.ID == 0. This ensures that the certificate persistence logic (lines 300-335) operates with the correct primary key value, addressing the critical issue raised in the previous review.


300-335: LGTM - Certificate persistence flow is well-structured.

The certificate handling logic properly:

  1. Validates that certificate and deposit counts match (lines 302-308)
  2. Cleans up existing certificates to prevent duplicates (lines 310-316)
  3. Persists each certificate with its deposit (lines 318-334)

All error handling includes proper context with wrapped errors for debugging. This resolves the duplicate certificate concern raised in the previous review.


340-413: LGTM - Well-structured helper with proper validation.

The storeCertificate helper function is well-designed:

  • Input validation prevents nil pointer issues (lines 349-354)
  • The getCertType local function provides a clean type mapping (lines 356-401)
  • The insertCert local function centralizes certificate mapping creation (lines 403-413)

This design pattern makes the subsequent switch statement cases cleaner and more maintainable.


415-660: Comprehensive certificate type coverage.

The switch statement handles all standard Cardano certificate types:

  • Stake operations: registration, deregistration, delegation (lines 416-444)
  • Pool operations: registration with relays/owners/metadata, retirement (lines 445-497)
  • Conway-era certificates: registration, deregistration, stake delegation combos (lines 498-527)
  • Vote/DRep operations: delegation, registration, deregistration, updates (lines 528-634)
  • Committee operations: hot key auth, cold key resignation (lines 572-595)
  • Special cases: Genesis delegation, MIR, Leios (lines 635-656)

Each case properly:

  • Extracts relevant fields from the typed certificate
  • Populates the specialized model with deposit where applicable
  • Creates the specialized record
  • Calls insertCert to create the mapping entry

The default case (line 658) returns a descriptive error for unsupported types.


662-695: LGTM - Cleanup logic prevents duplicates effectively.

The cleanup function properly:

  1. Queries existing certificate mappings for the transaction (lines 668-676)
  2. Deletes specialized certificate records via type-specific deletion (lines 677-686)
  3. Removes the mapping rows (lines 688-692)

The deletion order is correct (specialized records before mapping rows), and all errors include appropriate context for debugging.


697-749: LGTM - Type-aware deletion covers all certificate types.

The deletion helper properly maps each certificate type to its specialized table. The switch exhaustively covers all certificate types defined in getCertType (lines 356-401), and the fallback case (lines 746-747) handles any unknown types by attempting deletion from the generic Certificate table.

This ensures no orphaned certificate records remain after cleanup.

database/plugin/metadata/store.go (2)

126-132: LGTM - Interface signature aligns with unified persistence.

The SetTransaction signature update to include certificateDeposits []uint64 properly reflects the new unified certificate persistence model. The comment on line 130 clearly documents the parameter's purpose and that it's calculated by the ledger layer.


143-145: LGTM - Type unification improves consistency.

The parameter type changes from ledger.* to lcommon.* (ledger/common) improve consistency across the interface. This aligns with the broader refactoring to use common types from the gouroboros library.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch 2 times, most recently from 763416f to ba98f6e Compare November 8, 2025 20:49
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
database/plugin/metadata/store.go (1)

29-147: Remove orphaned certificate method implementations from pool.go.

The interface breaking change is verified—SetPoolRegistration and SetPoolRetirement (along with ~13 others) have been removed from the MetadataStore interface. The migration to SetTransaction is complete with no callers of the removed methods remaining in the codebase. However, two orphaned implementations remain:

  • SetPoolRegistration (database/plugin/metadata/sqlite/pool.go:141)
  • SetPoolRetirement (database/plugin/metadata/sqlite/pool.go:214)

These are dead code and should be removed as cleanup. The internal migration is successful, though external packages depending on these methods would be affected by the breaking API change.

🧹 Nitpick comments (1)
database/models/certificates.go (1)

19-27: Consider additional composite indexes for common query patterns.

The current indexes cover individual columns well, but common certificate query patterns might benefit from composite indexes:

  • (BlockHash, CertType) - for filtering certificates by block and type during rollback/chain reorganization
  • (Slot, CertType) - for range queries filtering by certificate type
  • (TransactionID, CertType) - for fetching specific certificate types within a transaction

Example composite index for rollback scenarios:

 type Certificates struct {
 	ID            uint   `gorm:"primaryKey"`
 	TransactionID uint   `gorm:"index;uniqueIndex:uniq_tx_cert;constraint:OnDelete:CASCADE"`
 	CertificateID uint   `gorm:"index"` // Polymorphic FK to certificate table based on CertType. Not DB-enforced.
 	Slot          uint64 `gorm:"index"`
 	CertIndex     uint   `gorm:"column:cert_index;uniqueIndex:uniq_tx_cert"`
-	CertType      uint
-	BlockHash     []byte `gorm:"index"`
+	CertType      uint   `gorm:"index:idx_block_cert,priority:2"`
+	BlockHash     []byte `gorm:"index;index:idx_block_cert,priority:1"`
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 763416f and ba98f6e.

📒 Files selected for processing (16)
  • .gitignore (1 hunks)
  • database/account.go (0 hunks)
  • database/certificates.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/certificates.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/pparams.go (2 hunks)
  • database/transaction.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
💤 Files with no reviewable changes (3)
  • database/plugin/metadata/sqlite/drep.go
  • database/drep.go
  • database/account.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • database/certificates.go
  • .gitignore
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/models/certificates.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
  • database/plugin/metadata/sqlite/database.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: utxorpc tests must pass github.com/blinklabs-io/gouroboros/ledger/common.Block to chain.Chain.AddBlock. They should create database/models.Block only as a storage row, then call models.Block.Decode() to obtain a ledger/common.Block for AddBlock and any ledger/protocol-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: chain.Chain.AddBlock expects a github.com/blinklabs-io/gouroboros/ledger.Block. In tests, construct a database/models.Block only as a container and call its Decode() to obtain the gouroboros ledger.Block (which also satisfies github.com/blinklabs-io/gouroboros/ledger/common.Block) before inserting into the chain.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (6)
database/models/models.go (1)
database/models/certificates.go (2)
  • Certificates (19-27)
  • Certificates (30-32)
ledger/delta.go (1)
database/models/certificates.go (2)
  • Certificates (19-27)
  • Certificates (30-32)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/plugin/metadata/sqlite/transaction.go (9)
database/plugin/metadata/sqlite/database.go (2)
  • MetadataStoreSqlite (38-44)
  • New (47-118)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/certificates.go (2)
  • Certificates (19-27)
  • Certificates (30-32)
database/models/account.go (20)
  • StakeRegistration (81-87)
  • StakeRegistration (89-91)
  • StakeDeregistration (70-75)
  • StakeDeregistration (77-79)
  • StakeDelegation (58-64)
  • StakeDelegation (66-68)
  • Registration (46-52)
  • Registration (54-56)
  • Deregistration (35-40)
  • Deregistration (42-44)
  • StakeRegistrationDelegation (93-100)
  • StakeRegistrationDelegation (102-104)
  • VoteDelegation (133-139)
  • VoteDelegation (141-143)
  • StakeVoteDelegation (106-113)
  • StakeVoteDelegation (115-117)
  • VoteRegistrationDelegation (145-152)
  • VoteRegistrationDelegation (154-156)
  • StakeVoteRegistrationDelegation (119-127)
  • StakeVoteRegistrationDelegation (129-131)
database/models/pool.go (9)
  • Pool (26-38)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/drep.go (7)
  • Drep (17-25)
  • RegistrationDrep (43-51)
  • RegistrationDrep (53-55)
  • DeregistrationDrep (31-37)
  • DeregistrationDrep (39-41)
  • UpdateDrep (57-64)
  • UpdateDrep (66-68)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-23)
  • ResignCommitteeCold (25-27)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/utxo.go (1)
  • Utxo (23-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (6)
database/models/certificates.go (2)

29-32: LGTM!

Standard GORM TableName implementation correctly maps the struct to the "certificates" table.


22-22: No action required—cleanup strategy is fully implemented.

The concern about orphaned CertificateID references is already addressed. The cleanupExistingCertificates() function (called during transaction processing) explicitly:

  1. Deletes all specialized certificate records by type
  2. Deletes all Certificates mapping records for the transaction in the same atomic transaction

This handles both transaction-level deletes (via CASCADE on TransactionID) and individual certificate removal. The CertificateID is intentionally not DB-enforced because it's polymorphic—a deliberate design trade-off documented in the model's inline comment.

database/plugin/metadata/sqlite/account.go (2)

58-61: Conflict resolution correctly updates all fields including Active.

The UpdateAll: true clause ensures that when a conflict occurs on the staking_key unique constraint, all fields (including the new Active field) are updated rather than just a subset. This is the correct behavior for maintaining account state consistency.


45-57: Verified: Active field exists in Account model.

The models.Account struct at database/models/account.go:28 includes Active bool with a default value of true. The SetAccount method correctly assigns the active parameter to this field.

database/plugin/metadata/store.go (2)

143-145: Type updates align with ledger package refactoring.

The changes from ledger.Address to lcommon.Address (line 143) and ledger.TransactionInput to lcommon.TransactionInput (line 145) appear to be part of a broader ledger package restructuring to use common types from gouroboros.


126-132: Optional certificate deposits are handled correctly in implementations.

The SQLite implementation in database/plugin/metadata/sqlite/transaction.go (lines 300-307) safely handles optional or empty certificate deposits. Certificate processing only executes when both certificates exist and deposits are provided (if len(certs) > 0 && len(certificateDeposits) > 0), with a length validation check to prevent count mismatches. No panic or crash scenarios exist when the deposits slice is nil or empty.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from ba98f6e to bd81d79 Compare November 8, 2025 22:19
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
ledger/state_test.go (1)

699-723: Clarify intent for unused helper function.

This helper function is marked as unused but appears complete and functional. If it's intended for future test validation, consider adding a comment explaining its purpose. Otherwise, if it's not needed, it should be removed to avoid code clutter.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba98f6e and bd81d79.

📒 Files selected for processing (17)
  • .gitignore (1 hunks)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/account.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/pparams.go (2 hunks)
  • database/transaction.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (1 hunks)
💤 Files with no reviewable changes (4)
  • database/plugin/metadata/sqlite/drep.go
  • database/models/certs.go
  • database/account.go
  • database/drep.go
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (2)
  • database/pparams.go
  • database/transaction.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: utxorpc tests must pass github.com/blinklabs-io/gouroboros/ledger/common.Block to chain.Chain.AddBlock. They should create database/models.Block only as a storage row, then call models.Block.Decode() to obtain a ledger/common.Block for AddBlock and any ledger/protocol-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: chain.Chain.AddBlock expects a github.com/blinklabs-io/gouroboros/ledger.Block. In tests, construct a database/models.Block only as a container and call its Decode() to obtain the gouroboros ledger.Block (which also satisfies github.com/blinklabs-io/gouroboros/ledger/common.Block) before inserting into the chain.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (4)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/txn.go (1)
  • Txn (27-34)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/transaction.go (9)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (38-44)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/certificate.go (2)
  • Certificate (23-31)
  • Certificate (34-36)
database/models/account.go (20)
  • StakeRegistration (81-87)
  • StakeRegistration (89-91)
  • StakeDeregistration (70-75)
  • StakeDeregistration (77-79)
  • StakeDelegation (58-64)
  • StakeDelegation (66-68)
  • Registration (46-52)
  • Registration (54-56)
  • Deregistration (35-40)
  • Deregistration (42-44)
  • StakeRegistrationDelegation (93-100)
  • StakeRegistrationDelegation (102-104)
  • VoteDelegation (133-139)
  • VoteDelegation (141-143)
  • StakeVoteDelegation (106-113)
  • StakeVoteDelegation (115-117)
  • VoteRegistrationDelegation (145-152)
  • VoteRegistrationDelegation (154-156)
  • StakeVoteRegistrationDelegation (119-127)
  • StakeVoteRegistrationDelegation (129-131)
database/models/pool.go (9)
  • Pool (26-38)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/drep.go (7)
  • Drep (17-25)
  • RegistrationDrep (43-51)
  • RegistrationDrep (53-55)
  • DeregistrationDrep (31-37)
  • DeregistrationDrep (39-41)
  • UpdateDrep (57-64)
  • UpdateDrep (66-68)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-23)
  • ResignCommitteeCold (25-27)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/utxo.go (1)
  • Utxo (23-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: nilaway
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: Analyze (go)
🔇 Additional comments (19)
internal/config/config_test.go (1)

1-13: LGTM: License header addition.

The Apache 2.0 license header has been properly added to the test file with no functional changes.

database/certificate.go (1)

21-39: LGTM: Clean architectural shift to unified certificate persistence.

The comments clearly document that certificate persistence is now centralized through SetTransaction, while appropriately retaining the getter methods for data access. This simplifies the API surface and aligns with the PR's objective of unified certificate handling.

database/plugin/metadata/sqlite/database.go (3)

171-179: LGTM: Improved error wrapping.

The error wrapping in the Close method provides clearer context when database handle retrieval fails, improving debugging.


166-212: LGTM: Clear documentation improvements.

The updated method comments provide clear descriptions of each method's purpose, improving code maintainability.


54-61: Verify intentionality of in-memory transaction behavior change.

The change from SkipDefaultTransaction: true to false (line 59) is confirmed as deliberate in commit bd81d79, but the commit message does not explain the rationale. This creates an inconsistency: in-memory databases now wrap operations in automatic transactions while persistent databases skip them (line 89: true).

Confirm whether:

  • This asymmetry is intentional
  • Tests rely on automatic transaction wrapping for in-memory databases
  • The performance implications have been evaluated
ledger/delta.go (2)

51-69: LGTM: Proper invalid transaction handling.

Invalid transactions correctly pass nil for the deposits parameter to SetTransaction, which is appropriate since certificate processing should be skipped for invalid transactions.


71-94: LGTM: Clean certificate deposit handling.

The certificate deposit calculation is properly separated from transaction persistence, with deposits computed only when certificates exist and errors appropriately wrapped with context. The deposits are then passed to SetTransaction for unified persistence.

ledger/certs.go (2)

26-65: LGTM: Well-designed deposit calculation API.

The CalculateCertificateDeposits function provides a clean separation between deposit calculation and persistence. Storing only non-zero deposits in the map is an efficient optimization, and error messages include certificate index for better debugging.


67-82: LGTM: Clear deprecation with migration guidance.

The deprecation of ProcessTransactionCertificates includes a clear error message directing users to the new API (CalculateCertificateDeposits and SetTransaction), which aids in migration.

database/plugin/metadata/sqlite/transaction.go (8)

31-34: LGTM: Appropriate placeholder for legacy certificates.

Using the maximum uint value for LegacyCertificateID is a good choice to avoid conflicts with actual database-generated IDs, which typically start from 1.


102-107: LGTM: Critical fix for ON CONFLICT path.

The transaction ID reload after upsert correctly handles the case where ON CONFLICT updates an existing row, ensuring tmpTx.ID is populated for subsequent certificate operations. This addresses the critical issue raised in previous reviews.


305-342: LGTM: Clean certificate processing flow.

The certificate processing properly:

  1. Early returns when no certificates exist
  2. Cleans up existing certificates to prevent duplicates
  3. Processes each certificate with its deposit (defaulting to 0 if not in map)
  4. Provides clear error context for each step

The approach of defaulting to 0 for missing deposits is reasonable since not all certificate types require deposits.


347-421: LGTM: Well-structured certificate storage setup.

The storeCertificate function has:

  • Proper nil checks for parameters
  • Clean helper closures for type mapping and certificate insertion
  • Clear separation of concerns

The helper functions are well-designed for reuse across all certificate types.


487-529: LGTM: Complete pool registration data persistence.

The pool registration handler now properly persists all certificate data including margin, metadata (URL/hash), owners, and relays. This addresses the data-loss regression concern from previous reviews.


593-687: LGTM: Complete vote delegation data persistence.

All vote-related certificate handlers (VoteDelegation, StakeVoteDelegation, VoteRegistrationDelegation, StakeVoteRegistrationDelegation) now properly persist the DRep credential in both the specialized tables and account records, addressing the data-loss concern from previous reviews.


698-750: LGTM: Complete governance certificate data persistence.

DRep and committee resignation certificates properly persist anchor data (URL and hash) when present, addressing the governance metadata concern from previous reviews.


768-856: LGTM: Comprehensive certificate cleanup.

The cleanup functions properly:

  1. Query existing certificates for the transaction
  2. Delete specialized table records by type
  3. Delete the certificate mapping entries
  4. Handle legacy types (no specialized record to delete)
  5. Provide fallback for unknown types

This prevents duplicate certificates on transaction retry/reprocessing.

database/plugin/metadata/sqlite/account.go (1)

45-56: LGTM! Active flag properly integrated.

The active parameter is correctly threaded through SetAccount and assigned to the Account model. The upsert logic remains intact with proper conflict handling.

database/plugin/metadata/store.go (1)

126-132: All SetTransaction implementations and callers are correctly updated.

Verification confirms:

  • Interface signature matches sqlite implementation exactly (lines 126-131 vs sqlite/transaction.go:58)
  • Database wrapper properly reorders parameters when delegating (database/transaction.go:48)
  • Both call sites in ledger/delta.go pass all 5 required parameters: transaction, point, index, certificate deposits map (nil or computed), and database connection
  • sqlite implementation correctly consumes the sparse deposits map with null-safe lookup (lines 318-325)

No issues found.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch 2 times, most recently from 278b22d to c4054fd Compare November 9, 2025 00:34
@wolf31o2
Copy link
Member Author

wolf31o2 commented Nov 9, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 9, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
database/plugin/metadata/sqlite/utxo.go (1)

107-119: Change "payment_addr" to "payment_key" on line 108.

The Utxo model defines a PaymentKey field which GORM maps to the payment_key column. The code incorrectly references payment_addr, which does not exist in the database schema. This will cause the query to fail at runtime.

if addr.PaymentKeyHash() != lcommon.NewBlake2b224(nil) {
	addrQuery = addrQuery.Or("payment_key = ?", addr.PaymentKeyHash().Bytes())
}
🧹 Nitpick comments (2)
database/models/leios_eb.go (1)

18-27: Consider renaming ElectionId to ElectionID for Go convention.

Go convention capitalizes all letters in acronyms (ID, URL, HTTP, etc.). The field is named ElectionId but should be ElectionID to match the pattern used for the ID field and follow idiomatic Go style.

Apply this diff:

 type LeiosEb struct {
 	ID                      uint   `gorm:"primaryKey"`
-	ElectionId              []byte `gorm:"index"`
+	ElectionID              []byte `gorm:"index"`
 	EndorserBlockHash       []byte
 	PersistentVotersData    []byte // JSON-encoded persistent voters
 	NonpersistentVotersData []byte // JSON-encoded non-persistent voters
 	AggregateEligSigData    []byte // JSON-encoded aggregate eligibility signature
 	AggregateVoteSigData    []byte // JSON-encoded aggregate vote signature
 	AddedSlot               uint64 `gorm:"index"`
 }
database/models/move_instantaneous_rewards.go (1)

17-24: Consider a more semantic type for the Source field.

The Source field uses uint to represent a binary choice (0=reserves, 1=treasury). While this works, consider using a boolean or a custom enum type for better type safety and clarity.

For example:

-	Source     uint   `gorm:"index"` // 0=reserves, 1=treasury
+	Source     bool   `gorm:"index"` // false=reserves, true=treasury

Or define a custom type:

type MIRSource uint
const (
    MIRSourceReserves MIRSource = 0
    MIRSourceTreasury MIRSource = 1
)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd81d79 and c4054fd.

📒 Files selected for processing (21)
  • .gitignore (1 hunks)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/pparams.go (2 hunks)
  • database/transaction.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (1 hunks)
💤 Files with no reviewable changes (3)
  • database/plugin/metadata/sqlite/drep.go
  • database/account.go
  • database/drep.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/transaction.go
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: utxorpc tests must pass github.com/blinklabs-io/gouroboros/ledger/common.Block to chain.Chain.AddBlock. They should create database/models.Block only as a storage row, then call models.Block.Decode() to obtain a ledger/common.Block for AddBlock and any ledger/protocol-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: chain.Chain.AddBlock expects a github.com/blinklabs-io/gouroboros/ledger.Block. In tests, construct a database/models.Block only as a container and call its Decode() to obtain the gouroboros ledger.Block (which also satisfies github.com/blinklabs-io/gouroboros/ledger/common.Block) before inserting into the chain.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
🔇 Additional comments (16)
database/pparams.go (1)

32-54: LGTM! Error variable separation improves clarity.

The refactor to use ppErr for parameter retrieval errors and err for decode errors makes the error flow more explicit and easier to follow. Both error paths are properly handled.

database/plugin/metadata/sqlite/database.go (2)

54-61: Verify the SkipDefaultTransaction: false change for in-memory database.

The in-memory database now uses SkipDefaultTransaction: false (line 59) while the disk database uses SkipDefaultTransaction: true (line 89). This creates an inconsistency between the two paths. The AI summary notes this as a behavior change from true to false.

Enabling default transactions in-memory may impact performance. Please confirm this change is intentional and aligns with the desired behavior for in-memory databases.


166-212: LGTM! Documentation and error handling improvements.

The enhanced method documentation and error wrapping in Close() improve code maintainability and debugging. The promRegistry parameter addition properly supports metrics integration.

database/models/genesis_key_delegation.go (1)

17-29: LGTM! Clean model definition.

The GenesisKeyDelegation model is well-structured with appropriate GORM tags and follows Go naming conventions. The indexed GenesisHash and AddedSlot fields support efficient queries.

ledger/certs.go (2)

26-65: Excellent refactor! Clear separation of concerns.

The new CalculateCertificateDeposits function properly separates deposit calculation from persistence, addressing the previous review concern. Key strengths:

  • Comprehensive documentation listing all certificate types with deposits
  • Proper error wrapping with certificate index context for debugging
  • Optimization to store only non-zero deposits in the map
  • Clean, focused implementation using the era's CertDepositFunc

67-82: LGTM! Clear deprecation with helpful guidance.

The deprecation of ProcessTransactionCertificates provides clear direction to use CalculateCertificateDeposits and SetTransaction instead. The error message properly guides developers to the new API.

database/models/models.go (1)

18-54: LGTM! Migration list properly expanded.

The additions of GenesisKeyDelegation, LeiosEb, and MoveInstantaneousRewards properly register the new certificate-type models for database migration. The existing Certificate model (line 23) remains to provide the unified mapping table mentioned in the PR objectives.

database/certificate.go (1)

21-39: LGTM! Clean architectural refactoring.

The updated comment clearly documents the shift to centralized certificate persistence via SetTransaction, while appropriately retaining the getter methods for data retrieval. This aligns well with the broader PR changes to unify certificate handling.

database/plugin/metadata/sqlite/utxo.go (1)

225-246: LGTM! Type migration completed correctly.

The parameter type update from ledger.TransactionInput to lcommon.TransactionInput aligns with the interface definition and the broader type migration across the PR.

ledger/delta.go (2)

51-69: LGTM! Invalid transaction handling is correct.

Invalid transactions correctly skip certificate processing and pass nil for deposits, which makes sense since they don't process certificates. The early continue prevents further processing as expected.


71-93: LGTM! Certificate deposit handling is well-structured.

The deposit calculation flow is clean:

  • Deposits are calculated only when certificates are present
  • Proper error handling with formatted error messages
  • nil deposits passed for transactions without certificates
  • Unified SetTransaction call handles both cases
database/models/certificate.go (1)

17-43: LGTM! Well-documented polymorphic certificate model.

The model design is sound:

  • Clear documentation of the polymorphic relationship and sentinel value convention
  • Appropriate indexes for query performance (Slot, CertType, BlockHash, CertificateID)
  • Unique constraint on (TransactionID, CertIndex) prevents duplicate entries
  • CASCADE delete maintains referential integrity

The comment correctly describes the LegacyCertificateID convention as ^uint(0) (maximum uint value), consistent with the implementation.

database/plugin/metadata/sqlite/account.go (1)

45-72: LGTM! Account activation tracking added correctly.

The addition of the active parameter and its assignment to Account.Active (Line 56) enables proper tracking of account registration state. The upsert logic with OnConflict{UpdateAll: true} correctly handles updates to existing accounts.

database/plugin/metadata/store.go (2)

126-132: LGTM! Clear interface definition with helpful documentation.

The updated SetTransaction signature is well-documented:

  • Line 130's comment clearly explains the sparse certificate deposits map
  • Parameter types are consistent with the broader type migration to lcommon.*
  • The signature correctly aligns with the call site in database/transaction.go

143-145: LGTM! Type migration to lcommon. completed correctly.*

The parameter types for UTXO methods now use lcommon.Address and lcommon.TransactionInput, consistent with the broader type migration across the codebase and matching the implementations.

database/transaction.go (1)

22-54: Parameter order is correct—no issues found.

Verification confirms the call at lines 48-54 correctly matches the interface definition. The parameters are passed in the expected order:

  • point (ocommon.Point)
  • tx (lcommon.Transaction)
  • idx (uint32)
  • certificateDeposits (map[int]uint64)
  • txn.Metadata() (*gorm.DB)

This order matches both the interface definition in database/plugin/metadata/store.go (lines 126-132) and the implementation in database/plugin/metadata/sqlite/transaction.go (lines 58-64).

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from c4054fd to 4fa99d3 Compare November 9, 2025 03:48
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
utxorpc/submit.go (1)

80-86: Simplify the error logging.

The nested fmt.Sprint(fmt.Errorf(...)) is redundant—fmt.Sprint on an error simply calls its Error() method. Prefer the original direct approach or use fmt.Errorf directly.

Apply this diff to simplify:

-			fmt.Sprint(
-				fmt.Errorf(
-					"failed to decode transaction from CBOR: %w",
-					err,
-				),
-			),
+			fmt.Sprintf(
+				"failed to decode transaction from CBOR: %v",
+				err,
+			),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4054fd and 4fa99d3.

📒 Files selected for processing (23)
  • .gitignore (1 hunks)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account_test.go (9 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/pparams.go (2 hunks)
  • database/transaction.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
  • utxorpc/submit.go (1 hunks)
💤 Files with no reviewable changes (4)
  • database/drep.go
  • ledger/state_test.go
  • database/account.go
  • database/plugin/metadata/sqlite/drep.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/config/config_test.go
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/models/account_test.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/models/account_test.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: utxorpc tests must pass github.com/blinklabs-io/gouroboros/ledger/common.Block to chain.Chain.AddBlock. They should create database/models.Block only as a storage row, then call models.Block.Decode() to obtain a ledger/common.Block for AddBlock and any ledger/protocol-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: chain.Chain.AddBlock expects a github.com/blinklabs-io/gouroboros/ledger.Block. In tests, construct a database/models.Block only as a container and call its Decode() to obtain the gouroboros ledger.Block (which also satisfies github.com/blinklabs-io/gouroboros/ledger/common.Block) before inserting into the chain.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (6)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/txn.go (1)
  • Txn (27-34)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/transaction.go (12)
database/plugin/metadata/sqlite/database.go (2)
  • MetadataStoreSqlite (38-44)
  • New (47-118)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/certificate.go (2)
  • Certificate (25-33)
  • Certificate (36-38)
database/models/account.go (20)
  • StakeRegistration (106-112)
  • StakeRegistration (114-116)
  • StakeDeregistration (95-100)
  • StakeDeregistration (102-104)
  • StakeDelegation (83-89)
  • StakeDelegation (91-93)
  • Registration (71-77)
  • Registration (79-81)
  • Deregistration (59-65)
  • Deregistration (67-69)
  • StakeRegistrationDelegation (118-125)
  • StakeRegistrationDelegation (127-129)
  • VoteDelegation (158-164)
  • VoteDelegation (166-168)
  • StakeVoteDelegation (131-138)
  • StakeVoteDelegation (140-142)
  • VoteRegistrationDelegation (170-177)
  • VoteRegistrationDelegation (179-181)
  • StakeVoteRegistrationDelegation (144-152)
  • StakeVoteRegistrationDelegation (154-156)
database/models/pool.go (10)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/drep.go (7)
  • Drep (17-25)
  • RegistrationDrep (43-51)
  • RegistrationDrep (53-55)
  • DeregistrationDrep (31-37)
  • DeregistrationDrep (39-41)
  • UpdateDrep (57-64)
  • UpdateDrep (66-68)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-23)
  • ResignCommitteeCold (25-27)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-30)
  • Transaction (32-34)
database/models/utxo.go (1)
  • Utxo (23-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: Analyze (go)
  • GitHub Check: nilaway
🔇 Additional comments (26)
.gitignore (1)

33-35: LGTM!

Standard IDE ignore entry for VS Code settings.

database/pparams.go (1)

33-44: LGTM!

Renaming to ppErr improves clarity and avoids variable shadowing with the outer err.

database/models/account_test.go (2)

27-96: LGTM!

Excellent test coverage expansion with edge cases (empty, nil, single-byte, all-zeros, all-ones staking keys). The use of hexDecode helper and multiline formatting improves readability.


155-159: LGTM!

Good addition of explicit error handling for ConvertBits in the round-trip validation.

database/models/move_instantaneous_rewards.go (1)

1-29: LGTM!

The MoveInstantaneousRewards model follows the established pattern with appropriate indexes on Source and AddedSlot for efficient queries, and RewardData as a JSON-encoded byte field for flexible reward storage.

database/plugin/metadata/sqlite/utxo.go (1)

22-22: LGTM!

The import alias change from ledger to lcommon improves consistency with other files in the PR.

database/models/leios_eb.go (1)

1-32: LGTM!

The LeiosEb model is well-structured with appropriate indexes on ElectionId and AddedSlot. Using JSON-encoded byte fields for voters and signature data provides flexibility for complex structures.

database/plugin/metadata/sqlite/database.go (4)

35-51: LGTM!

The enhanced documentation and updated constructor signature with logger and promRegistry parameters improve observability and align with the broader PR's instrumentation goals.


120-133: LGTM!

The init() method properly configures GORM tracing and schedules database vacuum operations. The discard logger fallback ensures safe operation even without an explicit logger.


166-211: LGTM!

The enhanced public method documentation improves API clarity for consumers of the metadata store.


56-60: Confirm the intentionality and performance trade-off for in-memory transaction wrapping.

The change from SkipDefaultTransaction: true to false for in-memory databases is confirmed. Setting SkipDefaultTransaction to false wraps each write in a transaction—providing safer atomicity but incurring ~30%+ write throughput overhead compared to the production configuration.

This creates an asymmetry where tests enable automatic transaction wrapping (slower but more consistent) while production disables it (faster). Verify this trade-off is intentional and document the rationale in code comments, then run your test suite to confirm the performance impact is acceptable for your test scenarios.

ledger/delta.go (1)

72-89: Appreciate the inline deposit wiring.

Calculating deposits only when certificates exist and threading them through SetTransaction keeps the hot path lean while giving metadata everything it needs. Nicely tightened.

database/models/genesis_key_delegation.go (1)

17-29: LGTM!

The GenesisKeyDelegation model is well-structured with appropriate GORM tags for indexing and follows the repository's model conventions.

ledger/certs.go (2)

26-65: LGTM!

The CalculateCertificateDeposits function correctly computes deposits using protocol parameters and provides detailed error context with certificate indices. The optimization to store only non-zero deposits in the map is efficient.


67-82: Deprecation handled appropriately.

The clear deprecation notice and error message guide users to the new workflow using CalculateCertificateDeposits and SetTransaction.

database/transaction.go (1)

22-55: LGTM!

The certificateDeposits parameter is properly threaded from the database layer to the metadata layer, aligning with the new unified certificate persistence workflow.

database/plugin/metadata/sqlite/transaction.go (8)

31-34: LGTM!

Using the maximum uint value as a sentinel for legacy certificate types is a reasonable approach that avoids conflicts with actual database IDs.


57-64: LGTM!

The updated SetTransaction signature correctly accepts the certificateDeposits parameter and matches the interface definition in database/plugin/metadata/store.go.


102-107: LGTM!

The transaction ID reload after upsert correctly addresses the ON CONFLICT scenario, ensuring that tmpTx.ID is populated before certificate persistence operations. This fixes the critical issue identified in previous reviews.


305-323: LGTM!

The certificate validation correctly requires certificateDeposits to be non-nil when certificates are present. The sparse map design (where only certificates with non-zero deposits have entries) aligns with CalculateCertificateDeposits behavior, so a length check is appropriately omitted.


325-347: LGTM!

The certificate persistence loop correctly handles the sparse deposit map by defaulting to zero and looking up each certificate's deposit. Good error context with certificate indices aids debugging.


352-899: LGTM!

The storeCertificate function comprehensively handles all certificate types with consistent patterns:

  • Proper parameter validation
  • Account/pool/DRep state updates where appropriate
  • JSON serialization for complex data structures
  • Consistent error handling with context
  • Creation of mapping entries via insertCert

901-934: LGTM!

The cleanupExistingCertificates function correctly removes both specialized certificate records and mapping entries, preventing duplicates during transaction re-processing.


936-990: LGTM!

The deleteCertificateRecord function correctly routes deletions to specialized tables based on certificate type, with a graceful fallback for unknown types.

database/plugin/metadata/store.go (2)

126-132: LGTM!

The SetTransaction signature is correctly updated with the certificateDeposits parameter, matching the implementation. The inline comment clearly documents the sparse map design (only certificates requiring deposits).


143-145: LGTM!

The parameter types correctly use lcommon.Address and lcommon.TransactionInput, aligning with the canonical ledger/common types as per repository conventions. Based on learnings.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 4fa99d3 to ac27b63 Compare November 9, 2025 03:58
@wolf31o2
Copy link
Member Author

wolf31o2 commented Nov 9, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 9, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from ac27b63 to 6bfc74e Compare November 10, 2025 22:45
@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 4ee0250 to 249ef47 Compare November 12, 2025 17:31
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
database/models/leios_eb.go (1)

21-24: Consider structured storage for voter and signature data.

Storing PersistentVotersData, NonpersistentVotersData, and signature data as JSON-encoded []byte is functional but has limitations:

  • Cannot efficiently query or filter by individual voters
  • Cannot index voter-related fields for lookups
  • Requires deserialization for any voter-level operations

If you need to query voter participation or signature components, consider:

  • Using GORM's JSON data type with JSON query support
  • Creating separate voter and signature tables with proper relationships
  • Adding indexed fields for commonly-queried voter attributes

If this data is only retrieved as a complete unit without field-level queries, the current approach is acceptable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ee0250 and 249ef47.

📒 Files selected for processing (31)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (7 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (4 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (3 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (1 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (3 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/drep.go
  • database/plugin/metadata/sqlite/certs.go
  • database/account.go
  • database/plugin/metadata/sqlite/plugin.go
  • database/drep.go
✅ Files skipped from review due to trivial changes (1)
  • internal/config/config_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • database/models/genesis_key_delegation.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/account_test.go
  • database/models/pool.go
  • database/plugin/metadata/sqlite/pool.go
  • go.mod
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/transaction.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/models/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: utxorpc tests must pass github.com/blinklabs-io/gouroboros/ledger/common.Block to chain.Chain.AddBlock. They should create database/models.Block only as a storage row, then call models.Block.Decode() to obtain a ledger/common.Block for AddBlock and any ledger/protocol-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: chain.Chain.AddBlock expects a github.com/blinklabs-io/gouroboros/ledger.Block. In tests, construct a database/models.Block only as a container and call its Decode() to obtain the gouroboros ledger.Block (which also satisfies github.com/blinklabs-io/gouroboros/ledger/common.Block) before inserting into the chain.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (9)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/drep.go (1)
database/types/types.go (1)
  • Uint64 (54-54)
database/models/transaction.go (1)
database/models/utxo.go (1)
  • Utxo (23-38)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/plugin/metadata/sqlite/transaction.go (11)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/pool.go (10)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/drep.go (7)
  • Drep (21-29)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/account.go (20)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • StakeDeregistration (96-101)
  • StakeDeregistration (103-105)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • Registration (72-78)
  • Registration (80-82)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-23)
  • ResignCommitteeCold (25-27)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/plugin/metadata/store.go (1)
database/models/utxo.go (1)
  • Utxo (23-38)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/account.go (1)
database/types/types.go (1)
  • Uint64 (54-54)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: nilaway
  • GitHub Check: lint
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: Analyze (go)
🔇 Additional comments (9)
database/models/auth_committee_hot.go (1)

18-18: Same rollback concern as ResignCommitteeCold.

This unique constraint on ColdCredential has the same potential rollback/reorg issue noted in resign_committee_cold.go. Ensure proper deletion of existing records during rollbacks.

database/models/leios_eb.go (1)

19-20: Verify rollback handling for composite unique constraint.

The composite unique index on (ElectionId, EndorserBlockHash) prevents duplicate endorsements but could cause issues during chain reorganizations if rolled-back records aren't properly deleted. Ensure rollback logic handles LeiosEb records.

database/models/transaction.go (1)

19-31: LGTM: Foreign key relationships correctly implemented.

The dual foreign-key pattern is properly maintained:

  • CollateralReturn and Outputs use TransactionID→ID (where UTXOs were created)
  • Inputs use SpentAtTxId→Hash (where UTXOs were consumed)
  • ReferenceInputs use ReferencedByTxId→Hash
  • Collateral use CollateralByTxId→Hash

The explicit primaryKey tag on ID and field reordering improve clarity without changing semantics.

Based on learnings.

database/models/account.go (1)

77-77: Verify types.Uint64 compatibility with GORM.

Same concern as in drep.go: ensure types.Uint64 properly implements GORM interfaces and existing queries work correctly with the new type.

Also applies to: 112-112, 125-125, 152-152, 177-177

database/plugin/metadata/store.go (2)

126-132: LGTM: SetTransaction interface extension aligns with unified certificate persistence.

The addition of the map[int]uint64 deposits parameter to SetTransaction successfully consolidates certificate deposit handling into the transaction persistence flow, removing the need for individual SetX certificate methods. This design:

  • Centralizes certificate and deposit persistence
  • Provides a sparse map (only certificates requiring deposits)
  • Aligns with the PR objective of unified certificate tracking

The parameter order (Point, Transaction, index, deposits) is consistent with implementations per past review resolution.


143-143: LGTM: Type changes to lcommon package improve consistency.

The parameter type changes from ledger.* to lcommon.* (ledger/common) for GetUtxosByAddress and SetUtxoDeletedAtSlot use the canonical cross-era types, which is the correct approach per project patterns.

Based on learnings.

Also applies to: 145-145

database/models/models.go (1)

29-31: LGTM: New certificate models properly added to migrations.

The three new models (GenesisKeyDelegation, LeiosEb, MoveInstantaneousRewards) are correctly added to the MigrateModels slice with:

  • Proper TableName() methods
  • Appropriate unique constraints for their use cases
  • Standard ID and AddedSlot fields

This supports the unified certificate persistence system objectives.

database/models/drep.go (1)

40-40: No issues found. The types.Uint64 implementation is correct and safe.

The verification confirms that:

  • types.Uint64 properly implements GORM's Value() method returning a string via strconv.FormatUint
  • The Scan() method correctly accepts string values from the database
  • No raw SQL queries bypass these interfaces, and no WHERE clauses directly reference DepositAmount—all queries use GORM's query builder
  • The database uses AutoMigrate for schema management, which automatically handles custom types with Scanner/Valuer implementations
  • DepositAmount assignments across all models use explicit type conversion consistently

The change is safe, handles data correctly, and requires no additional action.

database/models/resign_committee_cold.go (1)

19-19: Review comment is incorrect: OnConflict UPSERT pattern prevents constraint violations during re-application.

The ResignCommitteeCold model uses GORM's OnConflict clause with conflict resolution on cold_credential. When createCertificateModel is called during certificate storage, it executes: txn.Clauses(clause.OnConflict{Columns: ["cold_credential"], DoUpdates: [...]}).Create(model). If a rolled-back certificate is re-applied, this will UPDATE the existing record rather than fail the unique constraint. No deletion logic is needed for correctness.

However, a legitimate design inconsistency exists: unlike UTxOs (which use DeletedSlot soft-delete + cleanup functions), ResignCommitteeCold lacks a soft-delete mechanism. Rolled-back certificates remain in the database indefinitely, creating potential bloat, though insertion failures will not occur.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 249ef47 to d45f635 Compare November 12, 2025 20:04
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
database/models/pool.go (1)

48-60: Remove the unique constraint on PoolRegistration.PoolKeyHash.
Stake pools legitimately emit multiple registration certificates with the same pool key hash (e.g., metadata or pledge updates). With gorm:"uniqueIndex:uniq_pool_registration" applied to PoolRegistration.PoolKeyHash, the second certificate insert will raise a uniqueness violation and break block persistence. Please revert this field to a non-unique index (or no constraint) so every registration event can be stored independently.

Apply this diff:

-	PoolKeyHash   []byte `gorm:"uniqueIndex:uniq_pool_registration"`
+	PoolKeyHash   []byte `gorm:"index"`
🧹 Nitpick comments (2)
database/plugin/metadata/sqlite/transaction.go (1)

1359-1372: Consider hex encoding for JSON map keys.

Converting credential bytes directly to strings with string(cred.Credential.Bytes()) works but could be fragile if bytes aren't valid UTF-8. While hashes are typically safe, hex encoding would be more robust and debuggable:

-serializableRewards := make(map[string]uint64)
+serializableRewards := make(map[string]uint64)
 for cred, amount := range certTyped.Reward.Rewards {
-    serializableRewards[string(cred.Credential.Bytes())] = amount
+    serializableRewards[hex.EncodeToString(cred.Credential.Bytes())] = amount
 }

Apply the same to LeiosEb non-persistent voters (line 1414).

database/plugin/metadata/sqlite/certs_test.go (1)

13-92: Test coverage gap: validate actual conflict resolution behavior.

This test validates that database constraints prevent duplicates, but it doesn't test the actual storeCertificate path which uses ON CONFLICT DO UPDATE. The production code allows re-registration by updating existing records, but this test only verifies that plain Create() fails.

Consider adding tests that:

  1. Call SetTransaction with certificates (testing the full production path)
  2. Verify that ON CONFLICT updates work correctly (not just fail)
  3. Validate the returned certificate IDs are correct after conflicts
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 249ef47 and d45f635.

📒 Files selected for processing (31)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (6 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (3 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (3 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/plugin.go
  • database/plugin/metadata/sqlite/drep.go
  • database/account.go
  • database/drep.go
  • database/plugin/metadata/sqlite/certs.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • database/models/models.go
  • database/plugin/metadata/sqlite/pool.go
  • database/plugin/metadata/sqlite/database.go
  • database/models/genesis_key_delegation.go
  • database/models/account.go
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
  • database/models/transaction.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: utxorpc tests must pass github.com/blinklabs-io/gouroboros/ledger/common.Block to chain.Chain.AddBlock. They should create database/models.Block only as a storage row, then call models.Block.Decode() to obtain a ledger/common.Block for AddBlock and any ledger/protocol-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: chain.Chain.AddBlock expects a github.com/blinklabs-io/gouroboros/ledger.Block. In tests, construct a database/models.Block only as a container and call its Decode() to obtain the gouroboros ledger.Block (which also satisfies github.com/blinklabs-io/gouroboros/ledger/common.Block) before inserting into the chain.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
🧬 Code graph analysis (9)
ledger/certs.go (1)
ledger/state.go (1)
  • LedgerState (78-102)
database/plugin/metadata/sqlite/transaction.go (10)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/pool.go (10)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/drep.go (7)
  • Drep (21-29)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/account.go (20)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • StakeDeregistration (96-101)
  • StakeDeregistration (103-105)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • Registration (72-78)
  • Registration (80-82)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-23)
  • AuthCommitteeHot (25-27)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-24)
  • ResignCommitteeCold (26-28)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/models/pool.go (1)
database/types/types.go (1)
  • Uint64 (54-54)
database/plugin/metadata/sqlite/certs_test.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • New (47-119)
database/models/pool.go (4)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/account.go (4)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
database/models/drep.go (6)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/drep.go (1)
database/types/types.go (1)
  • Uint64 (54-54)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/plugin/metadata/store.go (1)
database/models/utxo.go (1)
  • Utxo (23-38)
database/models/transaction.go (1)
database/models/utxo.go (1)
  • Utxo (23-38)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (14)
internal/config/config_test.go (1)

1-14: LGTM! License header added correctly.

The Apache 2.0 license header is properly formatted and brings this test file into compliance with the project's licensing standards.

database/models/account_test.go (1)

29-30: LGTM! Clean test scaffolding refactoring.

The addition of wantErrMsg and expectedHRP fields to the test table structure improves maintainability by making validation expectations explicit. This follows Go table-driven test best practices and all test cases properly populate these fields.

Note: This change appears unrelated to the PR's main objective (certificate persistence), but it's a good cleanup.

database/plugin/metadata/sqlite/transaction.go (4)

102-107: LGTM: Transaction ID reload after upsert.

The ID reload logic correctly handles the case where ON CONFLICT updates an existing row, ensuring tmpTx.ID is populated before certificate persistence. This properly addresses the past critical issue.


305-339: LGTM: Proper certificate validation and persistence.

The validation correctly requires deposits when certificates are present and fails fast on mismatch, addressing the past critical issue about silently dropping certificates. The sparse map lookup with default 0 is appropriate for certificates without deposit requirements.


1305-1347: LGTM: Proper idempotent handling for genesis key delegation.

The implementation correctly uses ON CONFLICT DO NOTHING for idempotency and re-queries to retrieve the existing record's ID when a conflict occurs, addressing the past critical issue. The WHERE clause using struct fields works properly with GORM.


697-794: LGTM: Pool registration persistence is correct.

The implementation properly saves the pool record first, then creates the registration record with the correct PoolID, avoiding the duplicate record issue that was flagged in past reviews. The conflict resolution on pool_key_hash is appropriate for re-registrations.

database/plugin/metadata/sqlite/certs_test.go (3)

173-268: LGTM: Correct validation of DRep re-registration.

The test properly validates that DRep registration, deregistration, and update operations allow multiple records for the same credential, matching the production behavior where these operations have no conflict resolution.


270-354: LGTM: Proper idempotency validation.

The test correctly validates the idempotent behavior of genesis key delegation certificates, using the same conflict resolution logic as production and properly handling the zero-ID case that requires re-querying.


94-171: Test contradicts production behavior—duplicates should succeed, not fail.

The test expects subsequent StakeRegistration and Deregistration inserts to fail, but the production code at lines 609 and 643 explicitly passes empty conflict columns with comments stating "allow re-registration" and "allow re-registration for stake deregistration". When createCertificateModel() receives empty conflict columns (line 525–527), it bypasses conflict handling and simply calls txn.Create(), allowing duplicates.

Additionally, the StakeRegistration model has no unique constraint on staking_key—only a regular index (database/models/account.go:108).

Update the test to expect success on all inserts (remove the distinction between first and subsequent), or clarify whether re-registration should truly be prevented by adding an actual unique constraint to the model.

⛔ Skipped due to learnings
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.
database/transaction.go (1)

22-54: LGTM: Proper deposits parameter propagation.

The updated signature correctly adds the deposits parameter and passes it through to the metadata layer. The parameter order matches the interface definition.

database/plugin/metadata/sqlite/account.go (2)

33-35: LGTM: Active account filtering.

The added filter for active accounts and ordering by slot ensures only valid, current accounts are retrieved, aligning with the removal of separate deregistration tracking.


49-57: LGTM: Explicit active state parameter.

Adding the active parameter makes the account state explicit and supports the unified certificate persistence approach where account activation/deactivation is managed through the transaction flow.

database/plugin/metadata/store.go (2)

126-132: LGTM: Unified certificate persistence interface.

The updated signature properly adds the deposits parameter, enabling the unified certificate persistence approach. The sparse map design (only non-zero deposits) is efficient and matches the implementation.


143-145: LGTM: Consistent use of ledger/common types.

The parameter type changes to lcommon.Address and lcommon.TransactionInput ensure consistency across the ledger package usage, and the past review confirmed implementations were updated to match.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from d45f635 to 64cf902 Compare November 12, 2025 21:28
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
database/models/transaction.go (1)

19-19: The CollateralReturn association issue from the previous review remains unresolved.

As noted in the previous review, CollateralReturn reuses the same foreignKey:TransactionID;references:ID mapping as Outputs (line 20), but models.Utxo has no discriminator column to distinguish which row is the actual collateral return. GORM will preload an arbitrary output as the collateral return for any transaction with outputs, breaking the API contract.

Please either:

  1. Remove the CollateralReturn association field, OR
  2. Add a discriminator column to models.Utxo (e.g., IsCollateralReturn bool) and update the GORM relationship to use it
database/plugin/metadata/sqlite/transaction.go (1)

564-568: Fix field name conversion in conflict re-query logic.

The current implementation uses cases.Title() on the full snake_case column name, which preserves underscores (e.g., "genesis_hash" → "Genesis_Hash"). This will not match Go struct field names like GenesisHash, causing FieldByName() to fail and resulting in an incomplete WHERE clause.

Replace the single cases.Title() call with proper snake_case to PascalCase conversion:

-			caser := cases.Title(language.English)
-			for _, col := range conflictColumns {
-				field := v.FieldByName(
-					caser.String(col),
-				) // Convert snake_case to TitleCase
+			caser := cases.Title(language.English)
+			for _, col := range conflictColumns {
+				// Convert snake_case to PascalCase for Go field names
+				parts := strings.Split(col, "_")
+				for i := range parts {
+					parts[i] = caser.String(parts[i])
+				}
+				fieldName := strings.Join(parts, "")
+				field := v.FieldByName(fieldName)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d45f635 and 64cf902.

📒 Files selected for processing (31)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (1 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (1 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/certs.go
  • database/account.go
  • database/plugin/metadata/sqlite/drep.go
  • database/plugin/metadata/sqlite/plugin.go
  • database/drep.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • database/models/account.go
  • database/models/genesis_key_delegation.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/drep.go
  • database/models/leios_eb.go
  • internal/config/config_test.go
  • database/plugin/metadata/sqlite/pool.go
  • database/models/pool.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/models/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/database.go
  • database/models/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (7)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/plugin/metadata/sqlite/transaction.go (10)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/pool.go (10)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/drep.go (7)
  • Drep (21-29)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/account.go (20)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • StakeDeregistration (96-101)
  • StakeDeregistration (103-105)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • Registration (72-78)
  • Registration (80-82)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-23)
  • AuthCommitteeHot (25-27)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-24)
  • ResignCommitteeCold (26-28)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/transaction.go (1)
database/models/utxo.go (1)
  • Utxo (24-39)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • Utxo (24-39)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: lint
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: nilaway
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: Analyze (go)
🔇 Additional comments (19)
go.mod (1)

22-22: LGTM! Test dependencies align with the new comprehensive test suite.

The addition of testify and its indirect dependencies (go-spew, go-difflib) supports the 19 new certificate test cases mentioned in the PR objectives.

Also applies to: 32-32, 98-98, 159-159

database/models/transaction.go (1)

20-24: UTXO associations are correctly structured.

The foreign key patterns for Outputs, Inputs, ReferenceInputs, and Collateral properly preserve UTXO provenance using distinct foreign key columns that reference appropriate transaction identifiers.

Based on learnings

database/models/models.go (1)

29-31: LGTM! New certificate models correctly added to migration list.

The addition of GenesisKeyDelegation, LeiosEb, and MoveInstantaneousRewards aligns with the unified certificate persistence system objectives.

database/models/move_instantaneous_rewards.go (1)

17-29: LGTM! The MIR uniqueness constraint issue has been resolved.

The fields now use gorm:"index" instead of composite unique indexes, allowing multiple MIR certificates per slot as required by the ledger specification.

database/models/account_test.go (1)

26-96: LGTM! Test structure improved for clarity.

Adding wantErrMsg and expectedHRP fields to the test table makes error validation and address format checks more explicit and maintainable.

database/plugin/metadata/sqlite/utxo.go (3)

22-22: LGTM! Import alias updated for consistency.

The change from ledger to lcommon aligns with the broader refactoring to use github.com/blinklabs-io/gouroboros/ledger/common consistently across the codebase.


96-134: LGTM! Address-based UTXO queries are correct.

The method signature now uses lcommon.Address and the query correctly references payment_key (lines 109-110), which matches the PaymentKey field in models.Utxo. The previous field name mismatch has been resolved.


227-249: LGTM! UTXO deletion signature updated consistently.

The parameter type change to lcommon.TransactionInput aligns with the broader API refactoring.

ledger/delta.go (1)

47-113: LGTM! Certificate deposit calculation correctly integrated.

The logic appropriately:

  • Skips deposit calculation for invalid transactions (line 63: nil deposits)
  • Calculates deposits for valid transactions with certificates (lines 72-82)
  • Propagates deposits to SetTransaction (lines 85-94)
  • Handles errors at each step with clear context

The parameter order (point, tx, index, deposits, txn) is consistent across both valid and invalid transaction paths.

database/certificate.go (1)

21-39: LGTM! Documentation clearly explains the unified certificate persistence approach.

The comments appropriately direct developers to use SetTransaction for certificate persistence, which now handles all certificate types through the unified system. The retained getter methods provide necessary read access.

database/models/certificate.go (1)

17-40: LGTM: Well-structured certificate mapping model.

The Certificate model provides a clean polymorphic mapping between transactions and specialized certificate tables. The composite unique index on (transaction_id, cert_index) prevents duplicate certificate entries, and the cascade delete constraint maintains referential integrity.

ledger/certs.go (1)

23-62: LGTM: Proper era-aware deposit calculation.

The implementation correctly delegates to the current era's deposit calculation function and returns a sparse map containing only non-zero deposits. Error messages include the certificate index, which aids debugging.

database/transaction.go (1)

22-55: LGTM: Deposits parameter properly integrated.

The updated signature correctly accepts the deposits map and propagates it to the metadata layer. The parameter order is consistent with the interface definition.

database/plugin/metadata/sqlite/database.go (1)

46-119: LGTM: Constructor properly wired with observability.

The updated constructor correctly accepts logger and Prometheus registry parameters, handles both in-memory and on-disk database modes, and schedules vacuum operations appropriately.

database/plugin/metadata/sqlite/transaction.go (1)

55-350: LGTM: Transaction persistence with proper certificate handling.

The implementation correctly:

  • Reloads transaction ID after upsert to handle ON CONFLICT updates (lines 110-115)
  • Validates that deposits are provided when certificates are present (lines 313-323)
  • Delegates certificate storage to specialized handlers
database/plugin/metadata/sqlite/account.go (1)

24-69: LGTM: Account active state properly managed.

The updated GetAccount correctly filters for active accounts and returns the most recent by slot. SetAccount properly handles the new active parameter.

database/plugin/metadata/store.go (2)

126-132: LGTM: Interface signature properly updated for deposits.

The SetTransaction signature correctly adds the deposits map parameter and uses the proper ocommon.Point type. This enables the unified certificate persistence flow.


143-145: LGTM: Type parameters aligned with canonical ledger/common types.

The updated signatures correctly use lcommon.Address and lcommon.TransactionInput, which are the canonical cross-era types from gouroboros/ledger/common.

database/models/resign_committee_cold.go (1)

19-22: Verify no duplicate ColdCredential values exist before deploying the uniqueIndex change.

The uniqueIndex on ColdCredential will prevent duplicate committee cold resignations. However, if duplicate values already exist in the database, the migration will fail.

The CertificateID field is intentionally designed as a polymorphic foreign key (see database/models/certificate.go line 26: "Not DB-enforced"), with relationship validation handled at the application level based on the CertType field. This pattern is consistent across the codebase, so no additional FK constraint is needed.

Before deploying, confirm that your current resign_committee_cold table contains no duplicate ColdCredential values, or add migration logic to handle existing duplicates.

Comment on lines 18 to 21
ColdCredential []byte `gorm:"uniqueIndex:uniq_auth_committee_hot"`
HostCredential []byte `gorm:"index"`
ID uint `gorm:"primarykey"`
CertificateID uint `gorm:"index"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify migration safety for unique index and consider foreign key constraint.

The change from index to uniqueIndex on ColdCredential is a significant schema change that will prevent duplicate committee hot authorizations. However, if any duplicate ColdCredential values exist in the current database, the migration will fail.

Additionally, the new CertificateID field appears to reference the unified certificates table but lacks an explicit foreign key constraint in the struct tags. While this may be intentional or handled in migration code, consider whether referential integrity should be enforced at the database level.

Run the following script to check for existing duplicates and verify foreign key handling:


🏁 Script executed:

#!/bin/bash
# Description: Check for duplicate ColdCredential values and foreign key constraints

echo "Searching for migration files that handle auth_committee_hot schema changes..."
fd -e go -e sql "migration" --exec grep -l "auth_committee_hot" {}

echo -e "\nSearching for foreign key definitions related to CertificateID..."
rg -n "CertificateID.*FOREIGN KEY|REFERENCES.*certificates" -A 2 -B 2

echo -e "\nSearching for uniqueIndex migration handling..."
rg -n "uniq_auth_committee_hot|UniqueIndex.*ColdCredential" -C 3

Length of output: 715


🏁 Script executed:

#!/bin/bash
# Check for GORM auto-migration patterns and full struct definition

echo "1. Looking for GORM AutoMigrate calls..."
rg -n "AutoMigrate|migrations" --type go -B 2 -A 2 | head -50

echo -e "\n2. Full AuthCommitteeHot struct definition..."
fd -e go -x grep -A 20 "type AuthCommitteeHot struct" {}

echo -e "\n3. Looking for where AuthCommitteeHot is instantiated/used..."
rg -n "AuthCommitteeHot\{|AuthCommitteeHot{" --type go -B 2 -A 5 | head -40

echo -e "\n4. Checking for database initialization patterns..."
rg -n "InitDB|db.Migrat|database.*init" --type go -B 1 -A 3 | head -40

Length of output: 2899


🏁 Script executed:

#!/bin/bash
# Verify AuthCommitteeHot in MigrateModels and check for migration safety logic

echo "1. Check if AuthCommitteeHot is in MigrateModels list..."
fd -e go -x grep -A 30 "var MigrateModels" {} | grep -E "AuthCommitteeHot|&.*{}"

echo -e "\n2. Look for migration hooks, pre-migration cleanup, or constraint handling..."
rg -n "MigrateModels|migrate|constraint|unique.*index" --type go -B 2 -A 2 | grep -A 5 -B 5 "migrate"

echo -e "\n3. Search for AuthCommitteeHot usage/creation to understand duplication risk..."
rg -n "AuthCommitteeHot" --type go | grep -v "type AuthCommitteeHot\|TableName\|uniqueIndex"

echo -e "\n4. Check database initialization sequence..."
rg -n "AutoMigrate|MigrateModels" database/plugin/metadata/sqlite/database.go -A 3 -B 1

Length of output: 1151


🏁 Script executed:

#!/bin/bash
# Examine storeAuthCommitteeHotCertificate and migration patterns

echo "1. Full storeAuthCommitteeHotCertificate function..."
rg -n "storeAuthCommitteeHotCertificate" -A 50 --type go | head -80

echo -e "\n2. Check how ColdCredential is set..."
rg -n "ColdCredential" --type go -B 2 -A 2

echo -e "\n3. Look for migration hooks, pre-steps, or constraint handling..."
rg -n "DropTable|DeleteWhere|migrate" --type go | head -20

echo -e "\n4. Check Certificate model to understand CertificateID relationship..."
rg -n "type Certificate" -A 10 --type go | head -30

Length of output: 10209


Verify CertificateID field usage—currently unpopulated in AuthCommitteeHot creation.

The unique index migration itself is low-risk because ColdCredential derives from blockchain cryptographic credentials, making duplicates highly unlikely. However, there is a critical issue: the CertificateID field (line 21) is defined in the struct but never populated during record creation (transaction.go:1132–1136). This appears to be either incomplete implementation or unintended dead code.

The intentional absence of an explicit foreign key constraint is by design—the Certificate model explicitly documents this as a polymorphic FK pattern (certificate.go:26).

Action: Verify whether CertificateID should be set by insertCert(id) callback or if the field should be removed.

🤖 Prompt for AI Agents
In database/models/auth_committee_hot.go around lines 18 to 21 the CertificateID
uint field is declared but never populated when creating AuthCommitteeHot
records (see transaction.go:1132–1136); either populate CertificateID from the
certificate creation path or remove the field and corresponding index/migration.
Fix by locating insertCert(id) (or the certificate creation return) in
transaction.go and assign the returned certificate ID to the
AuthCommitteeHot.CertificateID before persisting, update any unit tests and
migrations to reflect the populated FK, or if the field is unused by design
delete the CertificateID field and its index/migration and remove any dead-code
references.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 64cf902 to 01f6fc6 Compare November 12, 2025 21:37
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
database/models/auth_committee_hot.go (1)

17-22: Add CertificateID field to AuthCommitteeHot struct.

The AuthCommitteeHot struct is missing the CertificateID field that all other certificate-related models include. All comparable models (ResignCommitteeCold, PoolRetirement, Drep variants, Account variants) define CertificateID uint with a gorm index, which establishes the bidirectional relationship required by the unified persistence system. Without this field, AuthCommitteeHot cannot properly link to the unified certificates table and breaks the polymorphic foreign key pattern.

type AuthCommitteeHot struct {
	ColdCredential []byte `gorm:"uniqueIndex:uniq_auth_committee_hot"`
	HostCredential []byte `gorm:"index"`
	ID             uint   `gorm:"primarykey"`
	CertificateID  uint   `gorm:"index"`  // Add this line
	AddedSlot      uint64
}
♻️ Duplicate comments (1)
database/models/transaction.go (1)

19-20: CollateralReturn association issue remains unresolved.

This duplicates an existing unresolved concern: CollateralReturn and Outputs both use foreignKey:TransactionID;references:ID, but there's no discriminator to distinguish which Utxo row is the collateral return. GORM will arbitrarily select the first matching row during preload, potentially returning a regular output as the collateral return. This was flagged in a previous review and remains unaddressed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64cf902 and 01f6fc6.

📒 Files selected for processing (31)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (1 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (1 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • database/plugin/metadata/sqlite/certs.go
  • database/drep.go
  • ledger/state_test.go
  • database/account.go
  • database/plugin/metadata/sqlite/drep.go
  • database/plugin/metadata/sqlite/plugin.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • database/plugin/metadata/sqlite/pool.go
  • database/models/genesis_key_delegation.go
  • internal/config/config_test.go
  • database/models/leios_eb.go
  • database/models/models.go
  • database/models/pool.go
  • go.mod
  • database/models/drep.go
  • database/models/account.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/models/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (7)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/plugin/metadata/sqlite/transaction.go (11)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/pool.go (10)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/drep.go (7)
  • Drep (21-29)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/account.go (20)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • StakeDeregistration (96-101)
  • StakeDeregistration (103-105)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • Registration (72-78)
  • Registration (80-82)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-22)
  • AuthCommitteeHot (24-26)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-24)
  • ResignCommitteeCold (26-28)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/transaction.go (1)
database/models/utxo.go (1)
  • Utxo (24-39)
database/plugin/metadata/sqlite/certs_test.go (6)
database/plugin/metadata/sqlite/database.go (1)
  • New (47-119)
database/plugin/metadata/store.go (1)
  • New (150-156)
database/models/pool.go (4)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/account.go (4)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
database/models/drep.go (6)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • Utxo (24-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: lint
  • GitHub Check: Analyze (go)
  • GitHub Check: nilaway
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
🔇 Additional comments (14)
database/models/account_test.go (1)

29-30: LGTM! Clean test table refactoring.

Moving wantErrMsg and expectedHRP into the test table struct definition improves readability and maintainability. All test cases consistently populate these fields where needed, and the validation logic correctly references them.

database/models/auth_committee_hot.go (1)

18-18: ****

The migration concern is unfounded. The code implements proper conflict resolution: storeAuthCommitteeHotCertificate calls createCertificateModel with conflictColumns=["cold_credential"], which uses GORM's OnConflict clause with DoUpdates on ["host_credential", "added_slot"]. This means duplicate ColdCredential values are handled gracefully by updating the existing record rather than causing insertion failures. The same defensive pattern is used consistently across similar models (e.g., ResignCommitteeCold). The unique index change is safe.

Likely an incorrect or invalid review comment.

database/models/certificate.go (1)

17-40: LGTM! Clean Certificate mapping model.

The Certificate model provides a clean unified interface for tracking all certificate types. The polymorphic CertificateID pattern is well-documented, the unique index on (transaction_id, cert_index) prevents duplicates, and the structure aligns well with the broader refactoring goals.

ledger/delta.go (1)

72-94: LGTM! Pre-storage deposit calculation properly implemented.

The refactored flow correctly calculates certificate deposits before storage and passes them to SetTransaction. The error handling ensures deposit calculation failures are surfaced early, and invalid transactions appropriately skip certificate processing by passing nil for deposits.

ledger/certs.go (1)

23-62: LGTM! Era-aware deposit calculation with clean design.

The CalculateCertificateDeposits method provides a clean, era-aware interface for deposit calculation. Returning only non-zero deposits in a sparse map is efficient, and the comprehensive documentation clearly identifies which certificate types require deposits in the Conway era.

database/transaction.go (1)

22-55: LGTM! Signature update correctly propagates deposits.

The updated SetTransaction signature cleanly propagates certificate deposits to the metadata layer while maintaining the existing UTXO persistence logic. The parameter order is consistent with the broader refactoring.

database/plugin/metadata/sqlite/certs_test.go (1)

1-354: LGTM! Comprehensive certificate persistence test coverage.

The test suite provides excellent coverage of certificate duplicate prevention, re-registration scenarios, and idempotent operations. Each test properly verifies unique constraints, handles conflict resolution, and validates expected record counts. The patterns are consistent and the tests effectively validate the certificate persistence behavior.

database/plugin/metadata/sqlite/transaction.go (3)

55-115: LGTM! Transaction persistence with proper ID handling.

The updated SetTransaction signature correctly includes the deposits parameter, and the transaction ID reload logic (lines 110-115) properly handles the case where an upsert updates an existing row, ensuring the ID is available for subsequent certificate processing.


313-432: LGTM! Certificate processing with proper validation.

The certificate processing logic correctly validates that deposits are provided when certificates exist, handles the sparse deposits map (where only non-zero deposits are present), and routes each certificate type to its specialized handler. The early return for transactions without certificates is efficient.


591-1471: LGTM! Comprehensive certificate storage handlers with consistent patterns.

The type-specific certificate storage functions follow a consistent pattern, include proper error wrapping, and appropriately specify conflict resolution for each certificate type. The helper functions (createOrUpdateAccount, deactivateAccount, createOrUpdateDrep) effectively consolidate common logic and reduce duplication.

database/plugin/metadata/sqlite/account.go (1)

25-68: LGTM! Account management simplified with active state.

The updates to GetAccount and SetAccount appropriately add support for the Active field. Filtering by active = true and ordering by added_slot DESC ensures retrieval of the most recent active account. The removal of certificate-specific methods aligns with the broader refactoring that centralizes certificate persistence in the transaction flow.

database/plugin/metadata/store.go (3)

17-27: LGTM! Clean import refactoring.

The removal of the top-level ledger import in favor of the more specific ledger/common package (aliased as lcommon) improves type clarity and aligns with the canonical address and transaction types used throughout the gouroboros library ecosystem.


126-132: LGTM! Well-designed sparse map for certificate deposits.

The updated SetTransaction signature effectively centralizes certificate persistence. The sparse map[int]uint64 parameter is an efficient design choice—only storing deposit amounts for certificates that require them avoids unnecessary zero entries. The inline comment clearly communicates this constraint to implementers.


143-145: LGTM! Type changes improve consistency.

The parameter type updates to lcommon.Address and lcommon.TransactionInput ensure the interface uses canonical cross-era types from ledger/common, maintaining consistency with the import refactoring and the broader gouroboros library conventions.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 01f6fc6 to d79cb27 Compare November 12, 2025 22:08
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
database/plugin/metadata/sqlite/database.go (1)

173-180: Stop the vacuum scheduler when closing the store.

Close does not stop timerVacuum before tearing down the DB connection. The scheduler continues rescheduling after Close() returns, causing VACUUM operations on a closed database and leaking a goroutine per store instance.

Apply this diff to stop the timer before closing:

 func (d *MetadataStoreSqlite) Close() error {
+	if d.timerVacuum != nil {
+		d.timerVacuum.Stop()
+	}
 	// get DB handle from gorm.DB
 	db, err := d.DB().DB()
 	if err != nil {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01f6fc6 and d79cb27.

📒 Files selected for processing (32)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (1 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (1 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/utxo.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (6 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • database/plugin/metadata/sqlite/plugin.go
  • database/plugin/metadata/sqlite/certs.go
  • database/plugin/metadata/sqlite/drep.go
  • ledger/state_test.go
  • database/drep.go
  • database/account.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • database/plugin/metadata/sqlite/pool.go
  • internal/config/config_test.go
  • database/models/pool.go
  • database/models/auth_committee_hot.go
  • go.mod
  • database/models/account_test.go
  • database/models/resign_committee_cold.go
  • database/models/leios_eb.go
  • database/models/models.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/models/certificate.go
  • database/models/genesis_key_delegation.go
  • database/plugin/metadata/sqlite/database.go
  • database/transaction.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
  • database/models/utxo.go
  • database/models/transaction.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/plugin/metadata/sqlite/transaction.go
  • database/models/utxo.go
  • database/models/transaction.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (9)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/certs_test.go (6)
database/plugin/metadata/sqlite/database.go (1)
  • New (47-119)
database/plugin/metadata/store.go (1)
  • New (150-156)
database/models/pool.go (4)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/account.go (4)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
database/models/drep.go (6)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/plugin/metadata/sqlite/database.go (1)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/plugin/metadata/sqlite/transaction.go (12)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/pool.go (9)
  • Pool (26-38)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/drep.go (7)
  • Drep (21-29)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/account.go (20)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • StakeDeregistration (96-101)
  • StakeDeregistration (103-105)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • Registration (72-78)
  • Registration (80-82)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-23)
  • AuthCommitteeHot (25-27)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-24)
  • ResignCommitteeCold (26-28)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • Utxo (24-40)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (54-54)
database/models/transaction.go (1)
database/models/utxo.go (1)
  • Utxo (24-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: nilaway
  • GitHub Check: lint
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: Analyze (go)
🔇 Additional comments (19)
database/models/drep.go (2)

17-19: LGTM: Import formatting.

The grouped import format is consistent with Go conventions.


23-23: LGTM: Index change enables DRep re-registration.

Changing from uniqueIndex to index on the Credential field correctly allows multiple Drep records with the same credential across different lifecycle events (register → deregister → re-register).

database/plugin/metadata/sqlite/database.go (3)

35-37: LGTM: Clear documentation.

The godoc comment accurately describes the MetadataStoreSqlite implementation.


46-66: LGTM: In-memory database path well documented.

The comment clarifies the testing use case for the in-memory database.


67-96: LGTM: Robust directory creation and WAL configuration.

The directory existence check with proper error handling and the WAL journal mode configuration are appropriate for production use.

database/models/account.go (1)

28-28: LGTM: Index change enables stake key lifecycle operations.

Changing from uniqueIndex to index on Account.StakingKey correctly allows tracking multiple account states and certificate events for the same stake key over time.

database/models/utxo.go (1)

39-39: LGTM: IsCollateralReturn field supports dual UTXO associations.

The new indexed boolean field enables the Transaction model's dual foreign-key pattern to distinguish standard outputs from collateral returns using WHERE clauses.

database/models/move_instantaneous_rewards.go (1)

17-24: LGTM: Non-unique indexes allow multiple MIR certificates per slot.

The change from composite uniqueIndex:uniq_mir to individual non-unique index tags correctly permits multiple MIR certificates in the same slot from the same pot, which is a valid ledger scenario.

database/transaction.go (2)

22-29: LGTM: Added deposits parameter for certificate tracking.

The signature change adds a deposits map to pass per-certificate deposit amounts to the metadata layer, supporting the unified certificate persistence system.


48-54: LGTM: Parameter order updated correctly.

The call to metadata.SetTransaction correctly passes the new deposits parameter and reordered arguments.

database/models/genesis_key_delegation.go (1)

17-24: LGTM: Composite unique constraint enforces idempotency.

The composite uniqueIndex:uniq_genesis_delegation on GenesisHash, GenesisDelegateHash, and VrfKeyHash correctly prevents duplicate genesis key delegations and enables idempotent insertions using clause.OnConflict.

database/plugin/metadata/sqlite/utxo.go (3)

22-22: LGTM: Import alias aligns with ledger/common usage.

The change to lcommon alias is consistent with broader codebase patterns for the shared ledger/common package.


97-134: LGTM: Address type updated to ledger/common.

The signature change from ledger.Address to lcommon.Address and corresponding updates to NewBlake2b224 calls are consistent with the ledger package refactoring.


228-249: LGTM: TransactionInput type updated to ledger/common.

The signature change from ledger.TransactionInput to lcommon.TransactionInput aligns with the broader type migration.

database/plugin/metadata/store.go (3)

143-143: Good: using canonical lcommon.Address type.

The type change from ledger.Address to lcommon.Address correctly uses the canonical cross-era Address type from the ledger/common package. Per the project learnings and past review comments, this alignment has been verified across implementations.


145-145: Good: using canonical lcommon.TransactionInput type.

The type change from ledger.TransactionInput to lcommon.TransactionInput correctly uses the canonical type from the ledger/common package, maintaining consistency with the project's type usage patterns. Per past review comments, implementations have been verified to match.


126-132: I need to see the complete call sites in ledger/delta.go to verify the deposits parameter is being passed. Let me run a follow-up verification:

I need to examine the ledger/delta.go file more closely to see the complete SetTransaction calls with the deposits parameter:Based on my analysis of the verification scripts and research, I can now provide a comprehensive assessment. The key findings are:

  1. SetTransaction implementations exist and correctly handle the map[int]uint64 deposits parameter
  2. SQLite implementation (lines 318-350 in sqlite/transaction.go) validates that deposits is not nil when certificates are present, and correctly defaults missing keys to 0 using the sparse map pattern
  3. ast-grep found NO results for CalculateCertificateDeposits function definition, which indicates it may not yet be implemented or is referenced from an external ledger module
  4. Call site outputs were truncated, preventing full visibility of the deposits parameter being passed

Since I cannot verify the complete call chain from ledger/delta.go showing the deposits parameter being populated and passed, and CalculateCertificateDeposits implementation is not found in the codebase, I must flag this for manual verification:

Verify the deposits map is calculated and passed correctly at all SetTransaction call sites.

The SetTransaction signature change is properly implemented in database layer implementations with correct sparse map handling for certificate deposits. However:

  1. The CalculateCertificateDeposits function referenced in the PR objectives was not found in the repository.
  2. The ledger/delta.go call sites could not be fully inspected to confirm deposits are being populated before being passed to SetTransaction.
  3. While the SQLite implementation correctly validates deposits != nil when certificates exist and properly handles sparse mapping (defaulting to 0 for missing certificate indices), the source of the deposits data requires manual verification.

Ensure:

  • CalculateCertificateDeposits exists in the ledger module or is properly imported
  • All SetTransaction call sites in ledger/delta.go calculate and pass the deposits map before the call
  • Deposits values are correctly in lovelace units
  • nil or empty maps are handled appropriately for transactions without deposits
database/plugin/metadata/sqlite/account.go (2)

33-35: No verification issues found. The callers explicitly expect active-only account retrieval.

The call sites confirm the filtering behavior is intentional:

  • Line 458-459: Comment states "Try to get existing active account" before calling GetAccount
  • Line 491-496: The deactivateAccount function correctly retrieves active accounts to mark them inactive, implementing the soft deletion pattern as designed

All call sites are aligned with the new filtering behavior. The breaking change concern is not applicable here—this is the intended behavior.


46-69: Concern about duplicate active records is mitigated by application logic but lacks DB-level enforcement.

The investigation confirms the append-only pattern is intentional for historical tracking. However, the concern is valid from a defensive programming perspective:

  • DB constraint: No unique constraint exists on (StakingKey, Active) where Active = true
  • Application logic: Prevents duplicates through createOrUpdateAccount which checks via GetAccount before calling SetAccount
  • Single call site: SetAccount is only called from one location (transaction.go:479) within the else branch after confirming no active record exists
  • GetAccount behavior: Always returns only the most recent active = true record via ORDER BY added_slot DESC, FIRST

The pattern works correctly in the current codebase. However, to prevent future issues if the calling logic changes or if SetAccount is called directly, consider adding a database constraint or documenting this as an intentional application-level invariant.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from d79cb27 to 1868c39 Compare November 12, 2025 22:25
@wolf31o2
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
database/models/auth_committee_hot.go (1)

21-21: Verify CertificateID population—previously flagged as unpopulated.

The CertificateID field remains defined but a previous review identified that it's never populated during AuthCommitteeHot record creation. Verify whether this field is now being set in the certificate persistence flow or should be removed if unused.

Run the following script to check if CertificateID is now being populated:

#!/bin/bash
# Description: Verify if AuthCommitteeHot.CertificateID is populated during creation

# Search for AuthCommitteeHot creation patterns
rg -n "AuthCommitteeHot\{" --type go -A 10 -B 2

# Check for CertificateID assignment
rg -n "CertificateID.*=" --type go -B 3 -A 3 | grep -i "committee"
database/plugin/metadata/sqlite/database.go (1)

172-180: Stop the vacuum timer before closing the database.

The Close() method doesn't stop timerVacuum, leaving the scheduler active after the DB connection closes. This causes avoidable errors when VACUUM attempts to run on a closed database and leaks a goroutine per store instance.

Apply this diff to stop the timer:

 func (d *MetadataStoreSqlite) Close() error {
+	if d.timerVacuum != nil {
+		d.timerVacuum.Stop()
+	}
 	// get DB handle from gorm.DB
 	db, err := d.DB().DB()
 	if err != nil {
 		return fmt.Errorf("get database handle: %w", err)
 	}
 	return db.Close()
 }
🧹 Nitpick comments (1)
database/plugin/metadata/sqlite/pool.go (1)

174-174: Redundant type conversion—deposit is already types.Uint64.

The deposit parameter is declared as types.Uint64 on Line 144, so wrapping it again with types.Uint64(deposit) is unnecessary. While harmless, removing the redundant conversion improves code clarity.

Apply this diff to remove the redundant conversion:

-		DepositAmount: types.Uint64(deposit),
+		DepositAmount: deposit,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d79cb27 and 1868c39.

📒 Files selected for processing (32)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (1 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (1 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/utxo.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (6 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/plugin.go
  • database/plugin/metadata/sqlite/drep.go
  • database/drep.go
  • database/plugin/metadata/sqlite/certs.go
  • database/account.go
✅ Files skipped from review due to trivial changes (1)
  • internal/config/config_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • database/models/resign_committee_cold.go
  • database/models/account.go
  • database/transaction.go
  • database/models/account_test.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/models/genesis_key_delegation.go
  • database/models/certificate.go
  • database/models/transaction.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/models/utxo.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🔇 Additional comments (14)
database/models/drep.go (1)

23-23: LGTM: Index changes correctly support DRep lifecycle flows.

The change from uniqueIndex to index on line 23, combined with the existing non-unique indexes on lines 36, 49, and 63, now allows legitimate DRep re-registration scenarios:

  1. Register (RegistrationDrep created)
  2. Deregister (DeregistrationDrep created)
  3. Re-register (new RegistrationDrep succeeds without constraint violation)

This correctly addresses the concern raised in the previous review about unique constraints preventing multiple lifecycle events for the same credential.

Also applies to: 36-36, 49-49, 63-63

database/models/genesis_key_delegation.go (1)

17-24: LGTM! Composite unique index properly configured.

The previously flagged issue regarding the missing composite unique index has been resolved. The uniqueIndex:uniq_genesis_delegation constraint on GenesisHash, GenesisDelegateHash, and VrfKeyHash ensures idempotent insertions when used with clause.OnConflict.

database/models/models.go (1)

29-31: LGTM! New models correctly added to migration list.

The addition of GenesisKeyDelegation, LeiosEb, and MoveInstantaneousRewards to the migration list is consistent with the PR's unified certificate persistence system.

database/models/utxo.go (1)

39-39: LGTM! IsCollateralReturn field properly added.

The new IsCollateralReturn field with an index enables efficient filtering of collateral return UTXOs, aligning with the broader transaction model refinements in this PR.

database/models/pool.go (1)

48-48: LGTM! Composite unique indexes correctly configured.

The unique indexes prevent duplicate pool operations:

  • PoolRegistration uses uniqueIndex:uniq_pool_registration on PoolKeyHash
  • PoolRetirement uses uniqueIndex:uniq_pool_retirement on the composite key (PoolKeyHash, Epoch)

This ensures a pool can only have one registration at a time and can only retire once per epoch, which aligns with Cardano ledger semantics.

Also applies to: 92-96

database/plugin/metadata/sqlite/utxo.go (1)

22-22: LGTM! Import alias updated and field name mismatch resolved.

The previously flagged critical issue regarding the payment_addr vs payment_key field name mismatch has been resolved. The query now correctly references payment_key (Line 109), which matches the model's PaymentKey field. The import alias change to lcommon is consistently applied throughout the file.

Also applies to: 98-113, 229-229

database/certificate.go (1)

21-39: LGTM! Documentation and API surface correctly updated.

The added comments clearly explain that certificate persistence is now handled by SetTransaction, aligning with the PR's unified certificate persistence system. The removal of SetPoolRegistration and SetPoolRetirement methods (per the AI summary) reduces API surface area as intended, while the remaining getter methods are properly documented.

database/models/transaction.go (2)

22-25: LGTM! CollateralReturn association properly fixed.

The where:IsCollateralReturn clauses correctly distinguish regular outputs from collateral returns, resolving the previous ambiguity where GORM could preload an arbitrary output. The filtering now uniquely targets the actual collateral return row.


28-29: LGTM! Type simplification is appropriate.

Replacing types.Uint64 with primitive uint64 removes unnecessary abstraction and is more idiomatic.

database/plugin/metadata/sqlite/account.go (2)

46-68: LGTM! SetAccount now implements append-only history pattern.

The method creates a new record on each call (no upsert), and the Active field is set from the parameter. Combined with GetAccount's active filtering, this implements an account history pattern where deactivations are recorded rather than deleted.


33-35: Behavioral change verified: GetAccount filtering is safe.

The method now returns only active accounts and the most recent by added_slot. All three call sites properly handle nil returns:

  • database/account.go:34-35 explicitly checks and returns ErrAccountNotFound
  • database/plugin/metadata/sqlite/transaction.go:464,495 conditionally use the account after nil checks

The change supports the account history pattern correctly.

database/plugin/metadata/store.go (2)

126-132: LGTM! SetTransaction signature properly extended for certificate deposits.

The signature changes support the unified certificate persistence system:

  • ocommon.Point first ensures consistency across the interface
  • uint32 transaction index enables proper ordering within blocks
  • Sparse map[int]uint64 efficiently tracks only certificates requiring deposits

143-145: LGTM! UTXO method signatures correctly use canonical lcommon types.

Both methods now use lcommon.* types (Address and TransactionInput), which are the canonical ledger types per the gouroboros library conventions. Based on learnings.

go.mod (1)

22-22: No security issues detected with the added dependencies.

github.com/stretchr/testify v1.11.1 is the latest stable version and has no public security advisories. golang.org/x/text v0.30.0 includes all fixes for the historical vulnerabilities (GO-2021-0113 fixed in v0.3.7, GO-2022-1059 fixed in v0.3.8) and no new advisories are present. The indirect dependencies (go-spew and go-difflib) are standard testing utilities with no security advisories.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 1868c39 to d36ea81 Compare November 13, 2025 01:22
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
database/plugin/metadata/store.go (1)

29-147: Removed certificate setter methods have no remaining call sites—implementation cleanup recommended.

Verification confirms no call sites remain for the removed methods. However, two orphaned implementations were found that are no longer exposed by the interface and should be removed:

  • SetPoolRegistration at database/plugin/metadata/sqlite/pool.go:141
  • SetPoolRetirement at database/plugin/metadata/sqlite/pool.go:215

These are dead code and should be deleted to complete the refactoring.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1868c39 and d36ea81.

📒 Files selected for processing (32)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (1 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (1 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/utxo.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (5 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (6 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/plugin.go
  • database/plugin/metadata/sqlite/certs.go
  • database/account.go
  • database/drep.go
  • database/plugin/metadata/sqlite/drep.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • database/models/account.go
  • go.mod
  • database/plugin/metadata/sqlite/pool.go
  • database/models/models.go
  • database/models/auth_committee_hot.go
  • database/models/drep.go
  • internal/config/config_test.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/models/certificate.go
  • database/models/transaction.go
  • database/plugin/metadata/sqlite/database.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/models/genesis_key_delegation.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (10)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/models/pool.go (1)
database/models/epoch.go (2)
  • Epoch (17-27)
  • Epoch (29-31)
database/models/transaction.go (1)
database/models/utxo.go (1)
  • Utxo (24-40)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/plugin/metadata/sqlite/database.go (1)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (54-54)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/plugin/metadata/sqlite/transaction.go (12)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/pool.go (10)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/drep.go (7)
  • Drep (21-29)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/account.go (20)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • StakeDeregistration (96-101)
  • StakeDeregistration (103-105)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • Registration (72-78)
  • Registration (80-82)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-23)
  • AuthCommitteeHot (25-27)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-24)
  • ResignCommitteeCold (26-28)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/plugin/metadata/sqlite/certs_test.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • New (49-122)
database/models/pool.go (4)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/account.go (4)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
database/models/drep.go (6)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • Utxo (24-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: go-test (1.25.x, macos-latest)
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: lint
  • GitHub Check: Analyze (go)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: nilaway
🔇 Additional comments (31)
database/models/account_test.go (1)

27-34: LGTM! Struct field reordering is cosmetic and functionally correct.

The reordering of wantErrMsg and expectedHRP to the top of the test struct has no functional impact since all test cases use named field initialization. The test logic correctly references these fields throughout.

database/models/resign_committee_cold.go (1)

19-19: No rollback handling issues detected. The code properly implements upsert semantics.

The unique constraint on ColdCredential is correct for resignation semantics. Chain rollbacks are already handled via the createCertificateModel function's OnConflict upsert pattern (line 560-562): when the same credential appears at a different slot during reorg, the AddedSlot field is updated rather than causing a conflict error.

database/models/certificate.go (1)

17-30: LGTM! Unified certificate mapping model is well-designed.

The Certificate model provides a clean polymorphic association pattern with appropriate indexing. The composite unique constraint on TransactionID and CertIndex ensures idempotent certificate insertion, and the CASCADE delete constraint maintains referential integrity.

database/plugin/metadata/sqlite/account.go (2)

33-35: LGTM! Active account filtering with proper ordering.

The addition of active = true filtering combined with added_slot DESC ordering ensures that GetAccount returns the most recent active registration, correctly handling account state transitions (registrations/deregistrations).


46-58: LGTM! Active parameter correctly integrated.

The active parameter is properly threaded through SetAccount and assigned to the model, enabling account state tracking for certificate-based registrations and deregistrations.

database/models/genesis_key_delegation.go (1)

17-24: LGTM! Composite unique constraint correctly enforces idempotency.

The uniq_genesis_delegation composite index on GenesisHash, GenesisDelegateHash, and VrfKeyHash properly supports the clause.OnConflict insertion pattern in the persistence layer, ensuring duplicate genesis key delegations are prevented.

database/models/utxo.go (1)

39-39: LGTM! Discriminator field enables proper collateral return association.

The IsCollateralReturn field with index provides the necessary discriminator for GORM's where clause in Transaction associations, correctly distinguishing collateral return outputs from regular transaction outputs.

database/plugin/metadata/sqlite/utxo.go (3)

22-22: LGTM! Import alias standardization.

The change from ledger to lcommon aligns with the codebase-wide convention for referencing gouroboros/ledger/common.


97-122: LGTM! Corrects nil pointer dereference for stake-only addresses.

The enhanced logic at lines 113-121 fixes a critical bug where querying UTXOs for stake-only addresses (no payment key hash) would cause addrQuery.Or() to be called on nil. The fix properly initializes addrQuery with the staking key condition when payment key hash is absent.


228-229: LGTM! Signature update aligns with type standardization.

The parameter type change from ledger.TransactionInput to lcommon.TransactionInput is consistent with the broader refactoring to use common ledger types.

database/models/pool.go (2)

91-98: LGTM! Composite uniqueness correctly models pool retirement lifecycle.

The composite unique index on PoolKeyHash + Epoch allows a pool to schedule multiple retirements at different epochs, which correctly models the Cardano protocol behavior where retirement certificates can be updated.


44-60: I need a deeper look at the persistence logic. The initial grep was too restrictive. Let me search for how PoolRegistration is actually saved/created:

Now let me search for the actual persistence operations on PoolRegistration in the transaction layer:

Let me search the codebase more directly for the persistence logic:

Now let me search the actual codebase for persistence patterns:

Let me search the codebase directly for how PoolRegistration and PoolRetirement are persisted:

Based on my analysis of the GORM documentation and the initial script output, I need to examine the transaction.go file more carefully to understand how PoolRegistration is actually persisted. The key issue is whether the code uses Clauses(clause.OnConflict{...}) to handle potential re-registrations. Let me generate the final response based on what we know:

Verify the uniqueness constraint handling for PoolRegistration persistence.

The review comment raises a valid concern about asymmetry between PoolRegistration and PoolRetirement uniqueness constraints. However, the initial script output was inconclusive because it only found test code and partial transaction code.

Without being able to locate the complete persistence implementation in the codebase via the grep results, I cannot definitively confirm:

  1. Whether PoolRegistration persistence uses db.Save(), db.Create(), or neither—and if so, whether it includes Clauses(clause.OnConflict{...}) for handling re-registrations
  2. How AddedSlot is actually used in the persistence layer
  3. Whether the persistence layer already handles conflicts through upsert logic

The asymmetry concern itself is technically valid—if pools can re-register with updated parameters, a composite unique constraint (PoolKeyHash + AddedSlot) would support versioning, while a single-field constraint on PoolKeyHash alone would prevent re-registration unless OnConflict DO UPDATE is explicitly configured.

To properly verify this comment, the developer should:

  • Review the handlePoolRegistrationCertificate method or equivalent persistence function in database/plugin/metadata/sqlite/transaction.go
  • Confirm whether GORM's Clauses(clause.OnConflict{UpdateAll: true}) or similar is used
  • Compare the actual behavior against the intended pool lifecycle (whether re-registration is expected)
database/transaction.go (2)

22-29: LGTM! Deposits parameter enables era-aware certificate tracking.

The addition of the deposits parameter and updated comment accurately reflect that SetTransaction now handles both UTXO and certificate persistence, with deposits computed upstream by the ledger layer.


48-54: LGTM! Parameters correctly threaded to metadata layer.

The updated call properly passes the deposits map to the metadata store's SetTransaction implementation, enabling deposit tracking during certificate persistence.

database/models/transaction.go (2)

21-25: LGTM! Discriminator-based associations correctly distinguish outputs.

The where clauses on Outputs (line 22) and CollateralReturn (line 25) properly leverage the IsCollateralReturn discriminator field in the Utxo model, ensuring GORM associates the correct UTXOs without ambiguity.


28-29: Verify database migration and intentionality of the type change from types.Uint64 to uint64.

The type change from types.Uint64 to uint64 is confirmed as intentional (Transaction fields are populated from tx.Fee() and tx.TTL() which return uint64). However, this introduces a critical storage format mismatch:

  • types.Uint64 uses a custom Value() method that serializes to string in the database (strconv.FormatUint())
  • Plain uint64 will be stored as numeric in the database
  • Other models (Utxo.Amount, Asset.Amount, Pool.Pledge/Cost, etc.) continue using types.Uint64 (string storage)

This is a breaking change requiring a data migration. Verify:

  1. That a database migration exists handling the Fee/TTL storage format conversion (none found in search)
  2. The intentionality behind storage format divergence from similar fields in other models
  3. Backward compatibility during the transition
database/certificate.go (1)

21-39: LGTM: Clear documentation of unified certificate persistence.

The documentation updates clearly explain that certificate persistence is now handled by SetTransaction, with the ledger layer calculating deposits before persistence. The retained getter methods appropriately delegate to the metadata layer.

database/models/move_instantaneous_rewards.go (1)

17-29: LGTM: MIR model properly uses non-unique indexes.

The model correctly uses non-unique indexes on Source, OtherPot, and AddedSlot, avoiding the conflict issues that would occur with a composite unique constraint. This allows multiple MIR certificates in the same slot from the same pot, which is a valid ledger scenario.

ledger/delta.go (1)

72-94: LGTM: Pre-storage deposit calculation correctly implemented.

The code properly calculates certificate deposits before calling SetTransaction, with appropriate error handling. Invalid transactions correctly pass nil deposits (line 63), while valid transactions with certificates compute deposits using CalculateCertificateDeposits before persistence.

database/models/leios_eb.go (1)

17-32: LGTM: LeiosEb model with appropriate uniqueness constraint.

The composite unique index on (ElectionId, EndorserBlockHash) correctly prevents duplicate Leios endorser block certificates for the same election and block combination. JSON encoding for the voter and signature data is a reasonable approach for storing these complex structures.

database/plugin/metadata/sqlite/certs_test.go (1)

13-354: LGTM: Comprehensive certificate idempotency and duplicate prevention tests.

The test suite thoroughly validates:

  • Pool registration/retirement unique constraints prevent duplicates
  • Stake registration/deregistration unique constraints prevent duplicates
  • DRep certificates allow re-registration (multiple records per credential)
  • Genesis key delegation is properly idempotent with ON CONFLICT DO NOTHING

The tests correctly verify both the constraint enforcement and the ID stability for idempotent operations.

ledger/certs.go (1)

23-62: LGTM: Era-aware deposit calculation correctly implemented.

The CalculateCertificateDeposits method properly:

  • Uses the current era's CertDepositFunc for era-specific deposit rules
  • Returns only non-zero deposits (storage optimization)
  • Wraps errors with certificate index for debugging
  • Documents which certificate types require deposits

This aligns with the PR's goal of pre-storage deposit calculation.

database/plugin/metadata/sqlite/transaction.go (5)

55-116: LGTM: Transaction ID properly reloaded after upsert.

The SetTransaction signature correctly accepts the deposits parameter, and the transaction ID reload logic (lines 112-116) ensures tmpTx.ID is valid even when ON CONFLICT updates an existing row. This addresses the critical issue where certificate storage would fail with a zero transaction ID.


314-350: LGTM: Certificate deposits validation prevents silent failures.

The code correctly validates that deposits are provided when certificates exist (lines 319-324), returning an error instead of silently dropping certificates. The loop properly extracts the deposit amount for each certificate index, defaulting to 0 if not present in the map.


440-603: LGTM: Field name conversion and conflict ID retrieval correctly implemented.

The snakeToPascal helper (lines 440-448) properly converts snake_case column names to PascalCase Go field names by splitting on underscores, title-casing each part, and joining without separators.

The createCertificateModel function (lines 540-603) correctly handles the zero-ID case after ON CONFLICT by re-querying using the conflict columns, with snakeToPascal ensuring field names match the struct.


450-538: LGTM: Account and DRep helpers correctly manage state.

The helper functions properly:

  • Create or update accounts/DReps with correct active flag management
  • Use txn.Save(tmpAccount) and txn.Save(tmpDrep) without extra indirection (lines 474, 497, 533), fixing the previous pointer-to-pointer issue
  • Handle error cases appropriately with wrapped errors

605-1518: LGTM: Certificate type handlers are comprehensive and correctly implemented.

The individual certificate storage handlers:

  • Follow a consistent pattern with proper error wrapping
  • Populate all required fields (pool registration includes margin, metadata, owners, relays at lines 752-789)
  • Use appropriate conflict resolution strategies for each certificate type
  • Genesis key delegation (lines 1357-1400) properly re-queries for the existing ID when ON CONFLICT DO NOTHING is triggered
  • MIR and Leios handlers correctly serialize complex data structures to JSON

All past review concerns regarding missing fields and ID retrieval have been addressed.

database/plugin/metadata/sqlite/database.go (2)

179-194: LGTM: Vacuum timer properly stopped in Close.

The Close method correctly:

  • Acquires the timerMutex before stopping the timer (thread-safe)
  • Stops the timerVacuum to prevent further VACUUM operations
  • Sets timerVacuum to nil to prevent double-stop
  • Only then closes the database connection

This addresses the previous concern about the vacuum scheduler continuing to run after Close, which would cause errors and leak goroutines.


36-122: LGTM: Constructor properly initializes logger and schedules vacuum.

The New constructor:

  • Accepts logger as a parameter for better observability
  • Properly handles in-memory vs on-disk database creation with appropriate comments
  • Creates data directory if it doesn't exist
  • Initializes logger in init() if nil (lines 125-129)
  • Schedules daily vacuum during initialization (line 135)

The initialization flow is clear and correct.

database/plugin/metadata/store.go (2)

143-143: Type change is correct.

The parameter type change to lcommon.Address aligns with the canonical ledger types. Per past review comments, implementation mismatches were addressed in commit c4054fd.


145-145: Type change is correct.

The parameter type change to lcommon.TransactionInput aligns with the canonical ledger types. Per past review comments, implementation mismatches were addressed in commit c4054fd.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from d36ea81 to 3ec8ff8 Compare November 13, 2025 01:49
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
database/models/auth_committee_hot.go (1)

21-21: Verify CertificateID population—still flagged from previous review.

A prior review identified that the CertificateID field is defined but never populated during AuthCommitteeHot record creation. If this has been addressed in the current changeset, please confirm. Otherwise, ensure the field is either populated via the certificate creation path or removed if unused.

🧹 Nitpick comments (2)
database/models/drep.go (1)

23-23: Consider standardizing the credential field naming.

There's an inconsistency in field naming:

  • Drep.Credential (line 23) and UpdateDrep.Credential (line 63)
  • DeregistrationDrep.DrepCredential (line 36) and RegistrationDrep.DrepCredential (line 49)

For better maintainability, consider standardizing to either Credential or DrepCredential across all models. While this may be an existing pattern, consistency would improve code clarity.

Also applies to: 36-36, 49-49, 63-63

Also applies to: 36-36, 49-49, 63-63

database/models/leios_eb.go (1)

21-24: Consider JSON schema validation for encoded fields.

The fields PersistentVotersData, NonpersistentVotersData, AggregateEligSigData, and AggregateVoteSigData store JSON-encoded data as raw bytes. Ensure that encoding/decoding logic elsewhere validates the structure to prevent malformed data from being persisted.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d36ea81 and 3ec8ff8.

📒 Files selected for processing (32)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (1 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (1 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/utxo.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (5 hunks)
  • database/plugin/metadata/sqlite/drep.go (0 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (6 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (7)
  • ledger/state_test.go
  • database/drep.go
  • database/plugin/metadata/sqlite/pool.go
  • database/plugin/metadata/sqlite/drep.go
  • database/plugin/metadata/sqlite/plugin.go
  • database/plugin/metadata/sqlite/certs.go
  • database/account.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • database/models/resign_committee_cold.go
  • database/models/account.go
  • internal/config/config_test.go
  • database/models/transaction.go
  • database/models/account_test.go
  • database/models/genesis_key_delegation.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/database.go
  • database/models/certificate.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (8)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (54-54)
database/plugin/metadata/sqlite/transaction.go (12)
database/plugin/metadata/sqlite/database.go (2)
  • MetadataStoreSqlite (39-46)
  • New (49-122)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/pool.go (10)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/drep.go (7)
  • Drep (21-29)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/account.go (20)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • StakeDeregistration (96-101)
  • StakeDeregistration (103-105)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • Registration (72-78)
  • Registration (80-82)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-23)
  • AuthCommitteeHot (25-27)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-24)
  • ResignCommitteeCold (26-28)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/plugin/metadata/sqlite/certs_test.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • New (49-122)
database/models/pool.go (4)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/account.go (4)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
database/models/drep.go (6)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
ledger/certs.go (1)
ledger/state.go (1)
  • LedgerState (78-102)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • Utxo (24-40)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: lint
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: nilaway
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: Analyze (go)
🔇 Additional comments (11)
database/models/pool.go (3)

55-55: Unique constraint on CertificateID ensures idempotency.

The unique index on CertificateID correctly prevents duplicate storage of the same certificate, aligning with the unified certificate persistence system objectives.


93-93: Unique constraint on CertificateID ensures idempotency.

The unique index on CertificateID correctly prevents duplicate storage of the same retirement certificate.


92-96: No issues identified — composite index design is correct.

The composite unique index on (PoolKeyHash, Epoch) is intentional and well-designed:

  • The Retirement []PoolRetirement field in the Pool model explicitly stores multiple retirements per pool
  • ledger/view.go iterates through multiple retirements per pool and finds the latest epoch
  • The index correctly prevents duplicate retirements at the same epoch while allowing a pool to retire at different epochs
  • This aligns with Cardano protocol semantics: a pool can be re-registered after retirement and retire again at a future epoch
database/models/drep.go (2)

17-19: LGTM: Import statement is correct.

The import is minimal and contains only what's needed (database/types for types.Uint64 used in deposit fields).


23-23: Constraint removal is properly implemented and test-validated.

The change from unique to index on credential fields is intentional and correct. The test TestDrepCertificateReRegistration explicitly validates this by creating multiple RegistrationDrep, DeregistrationDrep, and UpdateDrep records with the same credential, expecting all inserts to succeed and verifying that multiple records exist for the same credential.

Schema evolution is handled automatically by GORM's AutoMigrate mechanism. The drep models (&Drep{}, &DeregistrationDrep{}, &RegistrationDrep{}, and &UpdateDrep{}) are registered in the MigrateModels list, which GORM uses to automatically apply schema changes.

The business logic properly accommodates duplicate credentials through the AddedSlot field, enabling the legitimate re-registration flow: Register → Deregister → Re-register.

database/models/utxo.go (1)

25-39: LGTM - IsCollateralReturn field properly supports Babbage-era collateral handling.

The new IsCollateralReturn field correctly extends the UTXO model to distinguish collateral return outputs. The index will improve query performance when filtering outputs by this discriminator.

database/certificate.go (1)

21-24: LGTM - Certificate persistence correctly centralized.

The updated documentation accurately reflects the new unified approach where certificate persistence is handled through SetTransaction, eliminating the need for per-certificate-type setter methods.

database/models/move_instantaneous_rewards.go (1)

21-23: LGTM - Uniqueness constraint issue resolved.

The previous over-strict composite unique index has been correctly replaced with individual non-unique indexes, allowing multiple MIR certificates in the same slot from the same pot.

database/models/models.go (1)

29-31: LGTM - New models correctly added to migrations.

The three new certificate-related models (GenesisKeyDelegation, LeiosEb, MoveInstantaneousRewards) are properly registered for database migration.

go.mod (1)

22-22: LGTM - Standard test dependencies added.

The addition of testify and its dependencies (go-spew, go-difflib) supports the comprehensive test suite mentioned in the PR objectives. The promotion of golang.org/x/text to a direct dependency is also appropriate.

Also applies to: 32-32, 98-98, 159-159

database/plugin/metadata/sqlite/utxo.go (1)

22-22: LGTM - Consistent refactoring to use ledger/common types.

The import change to lcommon and corresponding type updates (lcommon.Address, lcommon.TransactionInput, lcommon.NewBlake2b224) are consistently applied throughout the file. The multiline query formatting at lines 107-111 also improves readability.

Also applies to: 98-98, 107-111, 113-113, 229-229

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 3ec8ff8 to c442844 Compare November 13, 2025 03:11
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
database/models/account.go (1)

61-66: Drop the new UNIQUE constraint on staking keys.

Cardano stake keys can register, deregister, and re-register multiple times over the chain. The new gorm:"uniqueIndex:…" tags on Deregistration.StakingKey and StakeRegistration.StakingKey now allow only a single row ever per key, so the second deregistration or re-registration will hit a UNIQUE constraint violation instead of being recorded. There is no lifecycle code here that deletes/overwrites the prior row, so normal ledger flows break. Please revert these fields to a plain index (or make the unique key composite with a distinguishing column such as certificate_id or added_slot) so multiple certificates for the same stake key can persist without errors.

Apply this diff:

-	StakingKey    []byte `gorm:"uniqueIndex:uniq_deregistration"`
+	StakingKey    []byte `gorm:"index"`-	StakingKey    []byte `gorm:"uniqueIndex:uniq_stake_registration"`
+	StakingKey    []byte `gorm:"index"`

Also applies to: 108-113

database/plugin/metadata/sqlite/database.go (1)

149-189: Vacuum timer still fires after Close.

Close stops the current timer but the callback’s defer d.scheduleDailyVacuum() runs even after you close the store, recreating a new timer. That leaves a goroutine running VACUUM against a closed connection and keeps logging errors forever. Please gate rescheduling with a “closing” flag (or similar) so the callback can exit without scheduling once shutdown has started, and only then close the DB handle.

🧹 Nitpick comments (1)
database/models/leios_eb.go (1)

25-25: Consider moving the primary key field to the top.

The ID field is conventionally placed first in database models for consistency and readability. While the current placement doesn't affect functionality, moving it to the top would align with common Go/GORM patterns.

Apply this diff:

 // LeiosEb represents a Leios endorser block certificate
 type LeiosEb struct {
+	ID                      uint   `gorm:"primaryKey"`
 	ElectionId              []byte `gorm:"uniqueIndex:uniq_leios_eb"`
 	EndorserBlockHash       []byte `gorm:"uniqueIndex:uniq_leios_eb"`
 	PersistentVotersData    []byte // JSON-encoded persistent voters
 	NonpersistentVotersData []byte // JSON-encoded non-persistent voters
 	AggregateEligSigData    []byte // JSON-encoded aggregate eligibility signature
 	AggregateVoteSigData    []byte // JSON-encoded aggregate vote signature
-	ID                      uint   `gorm:"primaryKey"`
 	AddedSlot               uint64 `gorm:"index"`
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec8ff8 and c442844.

📒 Files selected for processing (32)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (3 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (2 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/utxo.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (5 hunks)
  • database/plugin/metadata/sqlite/drep.go (2 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (6 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • database/plugin/metadata/sqlite/certs.go
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/plugin.go
  • database/plugin/metadata/sqlite/pool.go
  • database/drep.go
  • database/account.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • database/plugin/metadata/sqlite/drep.go
  • database/models/models.go
  • database/models/account_test.go
  • database/models/genesis_key_delegation.go
  • go.mod
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/utxo.go
  • database/models/transaction.go
  • database/models/certificate.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
  • database/plugin/metadata/sqlite/certs_test.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
  • database/plugin/metadata/sqlite/certs_test.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (8)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/transaction.go (1)
database/models/utxo.go (1)
  • Utxo (24-40)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (54-54)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/transaction.go (11)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/account.go (18)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
  • Registration (72-78)
  • Registration (80-82)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
database/models/pool.go (10)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-23)
  • AuthCommitteeHot (25-27)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-24)
  • ResignCommitteeCold (26-28)
database/models/drep.go (7)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
  • Drep (21-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • Utxo (24-40)
database/plugin/metadata/sqlite/certs_test.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • New (49-122)
database/models/pool.go (4)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/account.go (4)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
database/models/drep.go (6)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: Analyze (go)
  • GitHub Check: nilaway
  • GitHub Check: lint
  • GitHub Check: docker (ubuntu-latest, amd64)
🔇 Additional comments (5)
internal/config/config_test.go (1)

1-14: LGTM! Standard Apache 2.0 license header correctly added.

The license header is properly formatted and includes all required Apache 2.0 license text elements.

database/models/leios_eb.go (2)

29-32: LGTM!

The TableName method correctly follows GORM conventions for custom table naming.


21-24: JSON encoding/decoding is properly implemented with validation.

Verification confirms the concerns are unfounded:

  • Marshaling: All four JSON fields are explicitly marshaled with error handling (lines 1770–1798 in storeLeiosEbCertificate)
  • Validation: Input validation occurs via strong typing (certTyped.* parameters) and json.Marshal() validation on serialization; nil checks for optional fields
  • Schema/Documentation: Fields are strongly typed at the source (LeiosEbCertificate struct); comments document the expected structure; storage as bytes is appropriate for this write-only audit log pattern

No action required.

database/plugin/metadata/store.go (2)

127-132: SetTransaction signature looks solid

Passing ocommon.Point plus the per-certificate deposits map matches the new ledger workflow and keeps the interface in sync with CalculateCertificateDeposits. No issues spotted here.


143-145: Good call on switching to lcommon types

Using lcommon.Address and lcommon.TransactionInput keeps the interface consistent with the canonical ledger/common representations and avoids the mismatch we’ve tripped over before. Nicely done.
Based on learnings

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from c442844 to 2a57618 Compare November 13, 2025 03:42
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
database/plugin/metadata/sqlite/database.go (1)

151-187: Prevent vacuum from restarting after Close().

Stopping the timer is not enough: if Line 160’s callback is already running, its deferred scheduleDailyVacuum() still fires after Close marks the handle as shut. Because scheduleDailyVacuum has no “closed” guard, it spawns a fresh time.AfterFunc, so the goroutine keeps vacuuming a database that Line 191 just closed—continually logging errors and leaking a goroutine. Please gate rescheduling (and the vacuum itself) behind a close flag, e.g.:

 type MetadataStoreSqlite struct {
     promRegistry prometheus.Registerer
     db           *gorm.DB
     logger       *slog.Logger
     timerVacuum  *time.Timer
     timerMutex   sync.Mutex
     dataDir      string
+    closed       bool
 }

 func (d *MetadataStoreSqlite) runVacuum() error {
-    if d.dataDir == "" {
+    if d.dataDir == "" || d.closed {
         return nil
     }
     ...
 }

 func (d *MetadataStoreSqlite) scheduleDailyVacuum() {
     d.timerMutex.Lock()
     defer d.timerMutex.Unlock()
+    if d.closed {
+        return
+    }
     ...
 }

 func (d *MetadataStoreSqlite) Close() error {
     d.timerMutex.Lock()
+    d.closed = true
     if d.timerVacuum != nil {
         d.timerVacuum.Stop()
         d.timerVacuum = nil
     }
     d.timerMutex.Unlock()
     ...
 }

With that guard in place the deferred reschedule becomes a no-op, and any in-flight vacuum exits early instead of touching a closed connection.

🧹 Nitpick comments (1)
database/models/genesis_key_delegation.go (1)

17-24: Consider reordering struct fields for consistency.

While the current field ordering is valid, the typical convention in this codebase is to place the primary key (ID) before other fields. This would align with other models like Pool, Epoch, etc.

Apply this diff for consistency:

 type GenesisKeyDelegation struct {
+	ID                  uint   `gorm:"primaryKey"`
 	GenesisHash         []byte `gorm:"index;uniqueIndex:uniq_genesis_delegation"`
 	GenesisDelegateHash []byte `gorm:"uniqueIndex:uniq_genesis_delegation"`
 	VrfKeyHash          []byte `gorm:"uniqueIndex:uniq_genesis_delegation"`
-	ID                  uint   `gorm:"primaryKey"`
 	AddedSlot           uint64 `gorm:"index"`
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c442844 and 2a57618.

📒 Files selected for processing (32)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (3 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (2 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/utxo.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (5 hunks)
  • database/plugin/metadata/sqlite/drep.go (2 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (6 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • database/plugin/metadata/sqlite/certs.go
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/plugin.go
  • database/drep.go
  • database/plugin/metadata/sqlite/pool.go
  • database/account.go
✅ Files skipped from review due to trivial changes (1)
  • internal/config/config_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • database/models/auth_committee_hot.go
  • database/models/utxo.go
  • database/models/resign_committee_cold.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/account_test.go
  • database/models/drep.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/models/transaction.go
  • database/models/certificate.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/models/genesis_key_delegation.go
  • database/plugin/metadata/store.go
  • database/plugin/metadata/sqlite/database.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
  • database/plugin/metadata/sqlite/transaction.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/drep.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (8)
database/models/transaction.go (1)
database/models/utxo.go (1)
  • Utxo (24-40)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (78-102)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/transaction.go (11)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/account.go (20)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
  • StakeDeregistration (96-101)
  • StakeDeregistration (103-105)
  • Registration (72-78)
  • Registration (80-82)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
database/models/pool.go (10)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-23)
  • AuthCommitteeHot (25-27)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-24)
  • ResignCommitteeCold (26-28)
database/models/drep.go (7)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
  • Drep (21-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/models/pool.go (1)
database/models/epoch.go (2)
  • Epoch (17-27)
  • Epoch (29-31)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • Utxo (24-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: nilaway
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: Analyze (go)
🔇 Additional comments (14)
go.mod (1)

22-32: Test dependencies align with new coverage.

Adding testify and promoting golang.org/x/text to a direct dependency fits the new certificate persistence tests, so the module graph stays tidy.

database/models/certificate.go (1)

23-29: Composite key looks right for TX+cert mapping.

The shared uniqueIndex:uniq_tx_cert across TransactionID and CertIndex gives a deterministic one-to-one mapping per transaction slot without over-constraining anything else. Nicely structured.

database/models/account.go (1)

28-113: Index change unblocks repeated stake operations.

Relaxing Account.StakingKey to a plain index (while the per-certificate tables now enforce uniqueness per certificate) lets us persist re-registration and re-delegation history without hitting uniqueness violations. This lines up with the ledger lifecycle.

ledger/certs.go (1)

38-61: Deposit calculator matches era API.

Hooking into ls.currentEra.CertDepositFunc per certificate and only emitting non-zero entries gives downstream persistence the protocol-aware deposits it expects. Looks solid.

database/models/models.go (1)

29-31: LGTM!

The new certificate models are correctly added to the migration list. The models are defined in their respective files and this change ensures they'll be created during database migrations.

database/models/move_instantaneous_rewards.go (1)

17-29: LGTM!

The MIR certificate model correctly uses plain indexes rather than unique constraints, allowing multiple MIR certificates in the same slot. The JSON-encoded RewardData field is appropriate for storing variable reward maps.

database/models/pool.go (2)

44-60: LGTM! Pool re-registrations are now properly supported.

The PoolRegistration model correctly uses a plain index on PoolKeyHash (line 48) and a uniqueIndex on CertificateID (line 55). This allows multiple registration certificates for the same pool while preventing duplicate certificate entries, which aligns with Cardano's ledger behavior where pools can update their parameters over time.


91-98: LGTM! Pool retirement constraints are correctly defined.

The composite unique index on PoolKeyHash and Epoch (lines 92, 96) correctly prevents duplicate retirements of the same pool in the same epoch, while the separate unique index on CertificateID (line 93) prevents duplicate certificate entries. This properly models Cardano's retirement semantics.

database/models/transaction.go (2)

22-25: LGTM! Collateral return association is correctly implemented.

The where:is_collateral_return clauses correctly use snake_case column names, and the discriminator approach properly separates regular outputs from collateral returns. This resolves the previous issues with the association.


28-29: Fee and TTL type change is inconsistent with codebase patterns and likely breaks database persistence.

Throughout the codebase, amount-like fields are consistently typed as types.Uint64 (e.g., in StakeRegistration, Pool, Account, Asset models) and explicitly cast when assigned: DepositAmount: types.Uint64(deposit) (line 791 and similar patterns at lines 949-950, 1086, 1186, etc.).

The Transaction model's change to plain uint64 breaks this pattern. Since types.Uint64 implements custom Value() and Scan() methods for database driver integration, using plain uint64 removes these methods, changing how GORM persists and reads Fee/TTL from the database. This is a breaking change that's inconsistent with how other amount fields are handled across the codebase.

The assignments at lines 73-74 should either:

  • Keep types.Uint64 type in the model and cast the values: Fee: types.Uint64(tx.Fee())
  • Or verify GORM tests pass and document why this inconsistency is intentional
database/certificate.go (1)

21-39: LGTM! Certificate persistence consolidation is complete.

The removal of SetPoolRegistration and SetPoolRetirement methods (along with other certificate setters mentioned in the AI summary) in favor of the unified SetTransaction approach achieves the PR objective of centralizing certificate persistence. The clear documentation and retained getter methods maintain the query interface while simplifying the write path.

database/transaction.go (1)

23-29: All SetTransaction callers have been updated to the new signature.

Verification confirmed:

  • Both callers in ledger/delta.go (lines 73, 99) use the new parameter order: point, tx, idx, deposits, txn
  • Wrapper call in database/transaction.go (line 48) correctly passes all parameters including the new deposits map
  • SQLite implementation in database/plugin/metadata/sqlite/transaction.go has the matching signature
  • No references to the old signature were found
database/plugin/metadata/sqlite/account.go (2)

46-57: All SetAccount callers correctly pass the active parameter.

Verification found exactly one caller of SetAccount at database/plugin/metadata/sqlite/transaction.go:645. The call correctly passes all required parameters including active=true for new account creation. The logic is consistent and requires no changes.


33-35: Verify that the Active filter doesn't break existing callers—cannot conclusively confirm migration path.

The concern is valid: GetAccount now filters by active = true, and GORM's gorm:"default:true" tag only applies to NEW records created via GORM, not existing rows.

The code correctly sets Active = true for all new accounts through createOrUpdateAccount and SetAccount. However, no migration files were found in the codebase, so I cannot verify that existing accounts in the database were properly migrated when the Active column was added. If pre-existing accounts have NULL or unpopulated Active values, the filter will silently return nil for them.

Action required: Manually confirm that your database migration either:

  1. Added the Active column with a DEFAULT true constraint at the database level, or
  2. Included an UPDATE statement that set active = true for all existing accounts before adding the filter logic.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 2a57618 to 38b8b56 Compare November 13, 2025 04:12
@wolf31o2
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 13, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
database/plugin/metadata/sqlite/transaction.go (1)

331-354: Don’t reject legitimate zero deposits.

With Conway parameters it’s legal for the deposit for these certificate kinds to be zero. Because the ledger helper omits zero values today, every such certificate now trips this guard and the transaction is rejected. Once zero-value entries are preserved, we still need to check for the presence of a deposit entry, not its magnitude, so that true zero deposits continue to flow while genuinely missing entries still raise. Based on this PR stack

Apply this diff to validate by existence instead of value:

-		// Get deposit amount for this certificate (default to 0 if not present)
-		deposit := uint64(0)
-		if amt, exists := deposits[idx]; exists {
-			deposit = amt
-		}
-
-		// Validate that deposit-requiring certificate types have deposits
-		certType := cert.Type()
-		if deposit == 0 {
-			switch certType {
+		certType := cert.Type()
+		deposit, exists := deposits[idx]
+		if !exists {
+			switch certType {
 			case uint(lcommon.CertificateTypeStakeRegistration),
 				uint(lcommon.CertificateTypeRegistration),
 				uint(lcommon.CertificateTypeRegistrationDrep),
 				uint(lcommon.CertificateTypeStakeRegistrationDelegation),
 				uint(lcommon.CertificateTypeVoteRegistrationDelegation),
 				uint(lcommon.CertificateTypeStakeVoteRegistrationDelegation):
 				return fmt.Errorf(
-					"certificate type %d at index %d in transaction %d requires a non-zero deposit",
+					"certificate type %d at index %d in transaction %d is missing a deposit entry",
 					certType,
 					idx,
 					tmpTx.ID,
 				)
 			}
 		}
database/plugin/metadata/store.go (1)

143-145: Parameter types correctly use canonical ledger/common types.

The type changes from ledger.* to lcommon.* (ledger/common) align with project conventions and were verified in previous review iterations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a57618 and 38b8b56.

📒 Files selected for processing (32)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (3 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (2 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/utxo.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (5 hunks)
  • database/plugin/metadata/sqlite/drep.go (2 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (6 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • database/plugin/metadata/sqlite/pool.go
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/certs.go
  • database/plugin/metadata/sqlite/plugin.go
  • database/account.go
  • database/drep.go
✅ Files skipped from review due to trivial changes (1)
  • database/models/genesis_key_delegation.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • database/models/account.go
  • database/models/certificate.go
  • database/models/models.go
  • database/models/resign_committee_cold.go
  • internal/config/config_test.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/auth_committee_hot.go
  • database/models/leios_eb.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/models/transaction.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/drep.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🔇 Additional comments (21)
go.mod (3)

22-22: testify dependency addition aligns with comprehensive test suite.

The addition of github.com/stretchr/testify v1.11.1 as a direct dependency is appropriate given the PR introduces 19 new certificate test cases. Testify is a well-maintained assertion library for Go testing.


32-32: Verify rationale for promoting golang.org/x/text to direct dependency.

The PR objectives do not explicitly document why golang.org/x/text v0.30.0 was promoted to a direct dependency (vs. remaining indirect). The AI summary notes it was moved from the indirect block. Please clarify whether this is intentional for certificate/transaction handling or if it should remain transitive via its consumer.


98-98: Indirect dependencies are appropriately pinned.

The additions of go-spew and go-difflib as indirect dependencies with commit-pinned versions are consistent with transitive dependencies resolved by go mod tidy from testify. No action needed here.

Also applies to: 159-159

database/models/account_test.go (3)

29-30: LGTM! Test structure improved with enhanced validation fields.

The addition of wantErrMsg and expectedHRP fields enables more precise error message validation and Bech32 HRP verification, strengthening the test coverage.


117-124: LGTM! Error message validation implemented correctly.

The conditional check for non-empty wantErrMsg allows flexible test cases while ensuring error messages are validated when specified.


147-153: LGTM! HRP validation enhances address correctness checks.

The validation ensures the Bech32 Human-Readable Part matches expectations, adding an important layer of verification for address encoding correctness.

database/models/transaction.go (2)

28-29: Type change from types.Uint64 to uint64 is verified as safe and intentional.

This change is part of commit 2b5501e ("fix(database): preserve uint64 values in sqlite"). The original types.Uint64 type stored values as TEXT strings via custom database serialization, while uint64 maps directly to SQLite INTEGER via GORM's type mapping, which is the correct approach. GORM's AutoMigrate (called at database initialization in database/plugin/metadata/sqlite/database.go line 117) automatically handles the schema migration. No additional work is required.


22-25: All verification checks passed.

The implementation is complete and correct:

  • The Utxo model includes IsCollateralReturn bool field with proper GORM tags at database/models/utxo.go:39
  • Database migrations are handled via GORM AutoMigrate; both Transaction and Utxo models are registered in MigrateModels
  • Code properly sets IsCollateralReturn = true for collateral return UTXOs at database/plugin/metadata/sqlite/transaction.go:84
  • The where clauses in the Transaction model correctly use snake_case column names
database/plugin/metadata/sqlite/utxo.go (2)

97-134: LGTM! Type updates align with ledger/common refactor.

The signature changes from ledger.Address to lcommon.Address and updated hash construction using lcommon.NewBlake2b224 are correct and consistent with the broader refactoring to use gouroboros ledger/common types.


228-249: LGTM! SetUtxoDeletedAtSlot type update is correct.

The parameter type change from ledger.TransactionInput to lcommon.TransactionInput aligns with the ledger/common refactor and maintains the same logic.

database/models/pool.go (2)

44-60: LGTM! Unique constraint on CertificateID provides idempotency.

The change from gorm:"index" to gorm:"uniqueIndex:uniq_pool_registration_cert" on CertificateID (Line 55) correctly ensures each certificate is stored once while allowing multiple registrations per pool (since PoolKeyHash remains a plain index).


91-98: LGTM! Composite unique constraint correctly models pool retirement semantics.

The composite unique index on (PoolKeyHash, Epoch) (Lines 92, 96) correctly allows a pool to have multiple retirement certificates across different epochs, while the separate unique constraint on CertificateID (Line 93) ensures idempotency.

ledger/delta.go (1)

86-108: LGTM! Deposit calculation correctly precedes transaction persistence.

The logic correctly calculates certificate deposits before calling SetTransaction (Lines 86-105), ensuring deposits are available during certificate persistence.

database/models/move_instantaneous_rewards.go (1)

17-29: LGTM! MIR model correctly avoids over-strict uniqueness constraints.

The model uses plain indexes on Source, OtherPot, and AddedSlot (Lines 21-23), which correctly allows multiple MIR certificates per slot. This addresses the previous concern about composite unique constraints preventing valid ledger scenarios.

database/transaction.go (1)

22-55: LGTM! SetTransaction correctly updated for deposit-aware persistence.

The signature change adds the deposits parameter (Line 27) and reorders parameters for consistency. The comment (Line 22) helpfully clarifies that persistence spans both blob storage (UTXOs) and metadata storage (certificates). The internal delegation to d.metadata.SetTransaction (Lines 48-54) correctly passes all parameters.

database/plugin/metadata/sqlite/database.go (2)

48-122: LGTM! Constructor correctly handles in-memory and on-disk modes.

The constructor properly distinguishes between in-memory (empty dataDir) and on-disk modes (Lines 56-99), creating the data directory if needed and configuring WAL mode with appropriate pragmas for on-disk storage. Table migrations are correctly applied (Lines 115-120).


179-194: LGTM! Close properly stops the vacuum scheduler.

The Close method now correctly stops the vacuum timer with proper locking (Lines 181-186) before closing the database connection, preventing goroutine leaks and operations on a closed database.

database/certificate.go (1)

21-39: LGTM! API consolidation correctly shifts persistence to SetTransaction.

The removal of SetPoolRegistration and SetPoolRetirement methods aligns with the PR objective to centralize certificate persistence through SetTransaction. The updated comments (Lines 21-23) clearly document this architectural change, and the retained Get* methods provide necessary query functionality.

database/models/utxo.go (1)

50-79: Remove this review comment—IsCollateralReturn is properly populated at the call site.

The field is already being set to true when converting collateral return outputs. The current design is correct: UtxoLedgerToModel cannot determine this on its own since it receives only a single UTXO without transaction-level context. The caller in transaction.go correctly checks tx.CollateralReturn() and sets the flag only for matching outputs—this is the proper place for this logic.

Likely an incorrect or invalid review comment.

database/plugin/metadata/store.go (2)

126-132: Well-designed unified transaction persistence interface.

The SetTransaction signature changes effectively support the unified certificate persistence system:

  • ocommon.Point provides complete block context (slot + hash) instead of just slot
  • Transaction index enables proper ordering within blocks
  • Deposits map is clearly documented as sparse with explicit inline comments

The signature consolidates what were previously 15+ certificate-specific methods into a single transaction-scoped operation, aligning with the PR's goal of unified persistence.


29-147: Breaking change: Certificate-specific setters removed in favor of unified persistence.

The interface now removes 15+ certificate-specific setter methods (e.g., SetPoolRegistration, SetStakeDelegation, SetRegistrationDrep, etc.) in favor of the unified SetTransaction method that handles all certificate types. This is a significant breaking change but aligns with the PR objectives to implement a unified certificate persistence system per issue #878.

All certificate persistence is now consolidated into the SetTransaction flow, which processes certificates alongside the transaction and passes certificate-specific deposits via the sparse deposits map.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 38b8b56 to 6040f74 Compare November 13, 2025 11:52
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
database/plugin/metadata/sqlite/account.go (1)

52-68: Active flag never cleared before inserting new account rows

Removing the UNIQUE constraint means SetAccount now only does a Create. When we persist a deregistration (active=false) we insert a new inactive row but the previous active=true row is never updated. GetAccount now filters on active = true, so it keeps returning the stale record and never surfaces the deregistration (same issue for redelegations—old active rows stay marked true). We need to clear the prior rows before inserting the new one so the active flag reflects the current state.

Please update the existing rows for this staking key to active = false within the same transaction before creating the new record.

 func (d *MetadataStoreSqlite) SetAccount(
 	stakeKey, pkh, drep []byte,
 	slot uint64,
 	active bool,
 	txn *gorm.DB,
 ) error {
+	db := txn
+	if db == nil {
+		db = d.DB()
+	}
+	if err := db.
+		Model(&models.Account{}).
+		Where("staking_key = ?", stakeKey).
+		Update("active", false).Error; err != nil {
+		return err
+	}
 	tmpItem := models.Account{
 		StakingKey: stakeKey,
 		AddedSlot:  slot,
 		Pool:       pkh,
 		Drep:       drep,
 		Active:     active,
 	}
-	if txn != nil {
-		if result := txn.Create(&tmpItem); result.Error != nil {
-			return result.Error
-		}
-	} else {
-		if result := d.DB().Create(&tmpItem); result.Error != nil {
-			return result.Error
-		}
-	}
-	return nil
+	if result := db.Create(&tmpItem); result.Error != nil {
+		return result.Error
+	}
+	return nil
 }
database/plugin/metadata/sqlite/database.go (1)

179-194: Prevent the vacuum goroutine from rescheduling after Close()

Stopping the timer alone isn’t enough: if the callback is already running when Close() is called, it still executes defer d.scheduleDailyVacuum() and queues another run against a closed DB. That leaks a goroutine and yields repeated errors. Please add a shutdown flag and wait group so scheduleDailyVacuum bails out once we start closing, and so Close() waits for any in-flight vacuum run to finish before closing the handle.

 type MetadataStoreSqlite struct {
   promRegistry prometheus.Registerer
   db           *gorm.DB
   logger       *slog.Logger
   timerVacuum  *time.Timer
   timerMutex   sync.Mutex
+  vacuumWG     sync.WaitGroup
+  shutdown     bool
   dataDir      string
 }
@@
 func (d *MetadataStoreSqlite) scheduleDailyVacuum() {
   d.timerMutex.Lock()
   defer d.timerMutex.Unlock()
 
-  if d.timerVacuum != nil {
-    d.timerVacuum.Stop()
-  }
+  if d.shutdown {
+    return
+  }
+  if d.timerVacuum != nil {
+    if d.timerVacuum.Stop() {
+      d.vacuumWG.Done()
+    }
+    d.timerVacuum = nil
+  }
   daily := time.Duration(24) * time.Hour
-  f := func() {
+  d.vacuumWG.Add(1)
+  f := func() {
+    defer d.vacuumWG.Done()
     d.logger.Debug(
       "running vacuum on sqlite metadata database",
     )
@@
 func (d *MetadataStoreSqlite) Close() error {
   d.timerMutex.Lock()
+  d.shutdown = true
   if d.timerVacuum != nil {
-    d.timerVacuum.Stop()
+    if d.timerVacuum.Stop() {
+      d.vacuumWG.Done()
+    }
     d.timerVacuum = nil
   }
   d.timerMutex.Unlock()
 
+  d.vacuumWG.Wait()
+
   // get DB handle from gorm.DB
   db, err := d.DB().DB()
   if err != nil {
     return fmt.Errorf("get database handle: %w", err)
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38b8b56 and 6040f74.

📒 Files selected for processing (32)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (3 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/drep.go (2 hunks)
  • database/models/genesis_key_delegation.go (1 hunks)
  • database/models/leios_eb.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/move_instantaneous_rewards.go (1 hunks)
  • database/models/pool.go (2 hunks)
  • database/models/resign_committee_cold.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/utxo.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (0 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (5 hunks)
  • database/plugin/metadata/sqlite/drep.go (2 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (0 hunks)
  • database/plugin/metadata/sqlite/transaction.go (6 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (2 hunks)
  • database/transaction.go (2 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (2 hunks)
  • ledger/state_test.go (0 hunks)
💤 Files with no reviewable changes (6)
  • ledger/state_test.go
  • database/plugin/metadata/sqlite/certs.go
  • database/plugin/metadata/sqlite/plugin.go
  • database/plugin/metadata/sqlite/pool.go
  • database/account.go
  • database/drep.go
✅ Files skipped from review due to trivial changes (1)
  • internal/config/config_test.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • database/models/resign_committee_cold.go
  • database/models/auth_committee_hot.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/leios_eb.go
  • go.mod
  • database/models/utxo.go
  • database/models/genesis_key_delegation.go
  • database/models/drep.go
  • database/models/transaction.go
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-10-22T21:29:26.924Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T21:29:26.924Z
Learning: For tests under ledger/, prefer constructing a minimal ledger Block (MockBlock mirroring chain/chain_test.go) and pass it to Chain.AddBlock, rather than using database/models.Block or relying on models.Block.Decode().

Applied to files:

  • ledger/delta.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

  • database/plugin/metadata/sqlite/drep.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:19:26.360Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:91-91
Timestamp: 2025-10-22T20:19:26.360Z
Learning: In utxorpc/sync_test.go, tests should not construct database/models.Block directly for sync server use. Instead, build a models.Block only as a container for CBOR and call models.Block.Decode() to obtain a github.com/blinklabs-io/gouroboros/ledger/common.Block (or the repository’s ledger.Block interface), and use that decoded value in the test.

Applied to files:

  • database/plugin/metadata/sqlite/certs_test.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T21:28:48.396Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T21:28:48.396Z
Learning: In utxorpc/sync_test.go, tests must not pass database/models.Block into chain.Chain.AddBlock. They should call models.Block.Decode() and pass the resulting github.com/blinklabs-io/gouroboros/ledger/common.Block to AddBlock.

Applied to files:

  • database/plugin/metadata/sqlite/certs_test.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/models/account.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/plugin/metadata/sqlite/transaction.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: Unit tests under utxorpc should assert that models.Block.Decode() returns a github.com/blinklabs-io/gouroboros/ledger/common.Block, not rely on models.Block directly in protocol/ledger-facing logic.

Applied to files:

  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:13:35.164Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:56-56
Timestamp: 2025-10-22T20:13:35.164Z
Learning: In tests under ledger/state_test.go, PrimaryChain.AddBlock expects a "github.com/blinklabs-io/gouroboros/ledger/common".Block; use models.Block.Decode() to obtain that type before calling AddBlock.

Applied to files:

  • database/plugin/metadata/store.go
🧬 Code graph analysis (7)
ledger/certs.go (1)
ledger/state.go (1)
  • LedgerState (78-102)
database/plugin/metadata/sqlite/certs_test.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • New (49-122)
database/models/pool.go (4)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
database/models/account.go (4)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
database/models/drep.go (6)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/plugin/metadata/sqlite/transaction.go (13)
database/plugin/metadata/sqlite/database.go (2)
  • MetadataStoreSqlite (39-46)
  • New (49-122)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (2)
  • Certificate (22-30)
  • Certificate (33-35)
database/models/account.go (20)
  • StakeRegistration (107-113)
  • StakeRegistration (115-117)
  • Deregistration (60-66)
  • Deregistration (68-70)
  • StakeDelegation (84-90)
  • StakeDelegation (92-94)
  • VoteDelegation (159-165)
  • VoteDelegation (167-169)
  • VoteRegistrationDelegation (171-178)
  • VoteRegistrationDelegation (180-182)
  • StakeVoteDelegation (132-139)
  • StakeVoteDelegation (141-143)
  • StakeVoteRegistrationDelegation (145-153)
  • StakeVoteRegistrationDelegation (155-157)
  • StakeDeregistration (96-101)
  • StakeDeregistration (103-105)
  • Registration (72-78)
  • Registration (80-82)
  • StakeRegistrationDelegation (119-126)
  • StakeRegistrationDelegation (128-130)
database/models/pool.go (10)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
  • ErrPoolNotFound (24-24)
  • PoolRegistrationOwner (66-71)
  • PoolRegistrationOwner (73-75)
  • PoolRegistrationRelay (77-85)
  • PoolRegistrationRelay (87-89)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (17-23)
  • AuthCommitteeHot (25-27)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (17-24)
  • ResignCommitteeCold (26-28)
database/models/drep.go (7)
  • RegistrationDrep (47-55)
  • RegistrationDrep (57-59)
  • DeregistrationDrep (35-41)
  • DeregistrationDrep (43-45)
  • UpdateDrep (61-68)
  • UpdateDrep (70-72)
  • Drep (21-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/types/types.go (2)
  • Uint64 (54-54)
  • Rat (25-27)
database/transaction.go (2)
database/database.go (1)
  • Database (45-50)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (18-24)
  • GenesisKeyDelegation (27-29)
database/models/leios_eb.go (2)
  • LeiosEb (18-27)
  • LeiosEb (30-32)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (18-24)
  • MoveInstantaneousRewards (27-29)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (18-32)
  • Transaction (34-36)
database/models/utxo.go (1)
  • Utxo (24-40)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: nilaway
  • GitHub Check: lint
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: Analyze (go)
🔇 Additional comments (1)
database/models/account_test.go (1)

29-30: LGTM! Clean test structure refactoring.

The addition of wantErrMsg and expectedHRP as explicit fields in the test struct improves maintainability and makes the test table structure more readable. The fields are used consistently throughout the test cases.

- Add SetCertificate method handling all Cardano certificate types
- Switch interface from slot to ocommon.Point for consistency
- Add comprehensive test suite with 19 test cases
- Remove deprecated certificate setter methods
- Include certificate mapping model and migration updates

Signed-off-by: GitHub Copilot <[email protected]>
Signed-off-by: Chris Gianelloni <[email protected]>
@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 6040f74 to 3443840 Compare November 13, 2025 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Track transaction certificates

3 participants