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

    • Added support for querying inactive accounts.
    • Implemented certificate deposit calculations.
    • Added network-specific account formatting (mainnet/testnet).
    • Added collateral return flag tracking for UTXOs.
  • Refactor

    • Streamlined certificate persistence and handling.
    • Enhanced transaction deposit tracking.
  • Chores

    • Added automatic database maintenance scheduling.
    • Updated dependencies.

✏️ Tip: You can customize this high-level summary in your review settings.

@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

This PR implements a unified certificate tracking system by introducing a central Certificate table with polymorphic references to type-specific certificate tables via CertificateID. It consolidates scattered certificate persistence methods from the database, drep, and account layers into SetTransaction, which now computes deposits upfront and coordinates storage. The Uint64 type is refactored from a simple alias to a struct wrapper. Account queries gain includeInactive filtering. The LedgerState adds goroutine lifecycle management with context cancellation. Configuration parameters for ledger catchup timing are introduced, and comprehensive test coverage is added for certificate persistence, account handling, and transaction flows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Polymorphic certificate storage flow: Verify that storeCertificate correctly persists unified records and type-specific models with proper CertificateID linkage across all certificate types
  • Deposit calculation coordination: Ensure CalculateCertificateDeposits computes deposits correctly per certificate type and that deposits are properly threaded through SetTransaction and metadata layer
  • Uint64 struct migration: Confirm all usages of Uint64{Val: ...} are consistent and that scanner/valuer implementations handle edge cases (nil, overflow, type coercion)
  • Goroutine lifecycle in LedgerState: Validate context cancellation, wait group synchronization, and graceful shutdown on fatal errors
  • Unique index constraints: Review all composite unique indexes on certificate models to ensure they prevent duplicate certificates and maintain data integrity

Possibly related PRs

  • PR #972: Adds unified Certificate model to the data layer (directly aligns with this PR's certificate table consolidation approach)
  • PR #1017: Modifies Uint64 type handling and metadata store signatures for deposits (related type system and signature changes)
  • PR #1003: Extends Account.String() to StringWithNetwork(isTestnet) (directly overlaps with this PR's Account model string helper refactoring)

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 title 'feat: implement unified certificate persistence system' accurately describes the main objective: introducing a unified system to persist all Cardano certificate types through a single interface.
Linked Issues check ✅ Passed The PR successfully addresses the core requirements of issue #878: tracking transaction certificates, creating a unified 'certificates' table with schema mirroring ledger.Transaction.Certificates(), and retaining specialized per-type tables.
Out of Scope Changes check ✅ Passed The PR contains scope-appropriate changes: certificate persistence refactoring, model updates, database schema additions, and related test coverage. All changes support the unified certificate persistence objective.
✨ 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

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
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/certs.go (2)

253-263: Add missing .Bytes() call for PoolKeyHash assignment.

Line 256 assigns c.PoolKeyHash directly without calling .Bytes(), while other delegation certificates (lines 136, 303) correctly use c.PoolKeyHash.Bytes(). The model expects []byte, so this will fail to compile.

Apply this diff:

 		model := &models.StakeRegistrationDelegation{
 			StakingKey:    c.StakeCredential.Hash().Bytes(),
-			PoolKeyHash:   c.PoolKeyHash,
+			PoolKeyHash:   c.PoolKeyHash.Bytes(),
 			DepositAmount: types.Uint64{Val: deposit},
 			AddedSlot:     point.Slot,
 		}

276-286: Add missing .Bytes() call for PoolKeyHash assignment.

Line 279 assigns c.PoolKeyHash directly without calling .Bytes(), inconsistent with lines 136 and 303. The model expects []byte, so this will fail to compile.

Apply this diff:

 		model := &models.StakeVoteDelegation{
 			StakingKey:  c.StakeCredential.Hash().Bytes(),
-			PoolKeyHash: c.PoolKeyHash,
+			PoolKeyHash: c.PoolKeyHash.Bytes(),
 			Drep:        c.Drep.Credential,
 			AddedSlot:   point.Slot,
 		}
🧹 Nitpick comments (5)
database/plugin/metadata/sqlite/utxo.go (1)

96-137: Migration to lcommon types and address filtering looks consistent

Using lcommon.Address/TransactionInput keeps this API aligned with the rest of the lcommon migration, and the payment/staking key filters now target payment_key/staking_key columns that match models.Utxo. The zero-hash guard avoids useless queries for addresses without key hashes, and the OR combination correctly handles addresses with both payment and stake parts. If you want to simplify, you could start from addrQuery (e.g., addrQuery = addrQuery.Where("deleted_slot = 0")) instead of passing addrQuery back into txn.Where(...), but that’s purely cosmetic.

Also applies to: 230-252

database/models/leios_eb.go (1)

21-37: Confirm whether LeiosEb uniqueness should be composite or per-column

Both ElectionID and EndorserBlockHash use the same uniqueIndex:uniq_leios_eb tag, which in GORM creates a composite unique index (ElectionID, EndorserBlockHash). If the domain expects either ElectionID or EndorserBlockHash to be globally unique on its own, you’ll want separate unique indexes (e.g., omit the shared name or use two different names). If composite uniqueness is what you intended, this is fine as-is—just worth double‑checking.

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

348-405: Certificate deposit validation is correct but consider a minor readability tweak

The logic that:

  • defaults deposit to 0,
  • enforces presence of a deposits[idx] entry only for the specific requiresDeposit certificate types,

matches the intended behavior after CalculateCertificateDeposits started omitting zero‑deposit entries. The behavior is sound.

You might consider renaming the loop index (e.g., certIdx) to avoid shadowing the outer idx parameter and make logs easier to reason about, but this is purely cosmetic.

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

17-77: DRep lookup and upsert behavior look correct

GetDrep’s pointer return with explicit not‑found handling and SetDrep’s ON CONFLICT (drep_credential) upsert of slot/anchor/active provide a clear, idempotent DRep persistence path. If you want more diagnosability later, you could wrap the result.Error from Create with context (e.g., "upsert drep"), but the current behavior is functionally sound.

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

126-132: Clarify the contract for the deposits map in SetTransaction.

The new deposits parameter and its inline comment are helpful, but the interface is now the canonical contract for all plugins. Consider tightening the documentation (either here or in a method-level comment) to spell out expectations such as:

  • Whether deposits must be non‑nil when Transaction.Certificates() is non‑empty
  • How missing keys vs. explicit 0 values are interpreted
  • That indices must align exactly with the transaction’s certificate slice order

This will keep alternative MetadataStore implementations from subtly diverging from the current SQLite behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54a071c and 952e932.

📒 Files selected for processing (38)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.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 (2 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (6 hunks)
  • database/plugin/metadata/sqlite/drep.go (3 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 (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • go.mod (3 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (3 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (3)
  • database/account.go
  • database/drep.go
  • database/plugin/metadata/sqlite/plugin.go
✅ Files skipped from review due to trivial changes (1)
  • database/models/certificate_test.go
🚧 Files skipped from review as they are similar to previous changes (13)
  • database/models/asset.go
  • internal/config/config_test.go
  • database/models/move_instantaneous_rewards.go
  • ledger/view.go
  • database/models/models.go
  • database/types/types_test.go
  • database/models/auth_committee_hot.go
  • go.mod
  • ledger/state.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/account_test.go
  • database/models/resign_committee_cold.go
  • ledger/delta.go
🧰 Additional context used
🧠 Learnings (7)
📚 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/models/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/models/transaction.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/transaction.go
  • database/models/genesis_key_delegation.go
  • database/models/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
📚 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/transaction.go
  • database/models/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/models/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: 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/utxo.go
  • 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 (16)
internal/node/load.go (1)

173-199: LGTM! Well-structured cancellable wait pattern.

The timeout and ticker-based polling approach ensures the ledger catches up before proceeding, with proper context cancellation and resource cleanup.

ledger/certs.go (1)

37-60: LGTM! Deposit calculation properly centralized.

The function correctly computes deposits for all certificate types using the current era's parameters, includes zero-value deposits (as required), and provides helpful error context with certificate indices.

database/models/transaction.go (1)

26-29: LGTM! Associations correctly distinguish outputs from collateral return.

The where: clauses properly use snake_case column names and cleanly separate regular outputs from the collateral return output.

database/models/genesis_key_delegation.go (1)

21-34: LGTM! Model properly enforces uniqueness for idempotent operations.

The composite unique index on GenesisHash, GenesisDelegateHash, and VrfKeyHash ensures idempotency for genesis key delegations, which aligns with the OnConflict-based persistence logic.

database/types/types.go (1)

59-86: LGTM! Uint64 struct correctly implements database serialization.

The change from type alias to struct with Val field is consistent throughout the PR. The Value() and Scan() methods properly handle the struct field, and the type switch correctly handles both string and []byte inputs.

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

89-90: LGTM! Correctly uses .Val accessor for Uint64 struct.

Consistent with the type change from types.Uint64 alias to struct with Val field.

database/models/drep.go (1)

21-72: LGTM! Consistent field renaming across DRep models.

The Credential to DrepCredential rename is applied uniformly, and index types are appropriate: uniqueIndex on the main Drep table, and index on the historical certificate tables.

database/certificate.go (1)

21-39: Certificate persistence docs and getters align with the unified flow

The new comments make it clear that certificates are persisted via SetTransaction, and the getters correctly delegate to the metadata layer while keeping existing signatures. This matches the unified certificate persistence design without introducing behavioral changes.

database/models/pool.go (1)

44-60: Uniqueness constraints for pool registrations/retirements look well chosen

Making PoolRegistration.CertificateID uniquely indexed and using a composite unique index on (PoolKeyHash, Epoch) for PoolRetirement gives you idempotency per certificate and prevents duplicate retirements per pool/epoch while still allowing re-registrations. This matches the intended tracking semantics without reintroducing the earlier “one registration per pool” limitation.

Also applies to: 91-98

database/models/utxo.go (1)

23-40: Utxo mapping to types.Uint64 is consistent with the UTXO/transaction model

Wrapping AddedSlot and Amount as types.Uint64{Val: ...} matches the custom Uint64 type’s DB integration, and adding IsCollateralReturn as an indexed flag gives you a clean way to distinguish collateral-return outputs without affecting existing queries. The TransactionID/SpentAtTxId fields remain intact for the dual foreign‑key pattern in models.Transaction, so UTXO provenance should stay consistent. Based on learnings.

Also applies to: 50-79

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

36-48: Vacuum lifecycle and Close() coordination look robust now

The addition of vacuumWG, the dataDir == "" || closed guard in runVacuum, and the updated Close() (marking closed, stopping the timer, then waiting for in‑flight vacuums before closing the DB) collectively fix the prior “VACUUM on closed DB”/goroutine‑leak pattern. The in‑memory path and the lightweight wrapper methods with doc comments also make the store behavior clearer and safer for tests.

Also applies to: 50-124, 126-184, 186-189, 191-210, 212-243

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

52-145: SetTransaction idempotency and deposits handling look consistent

The reworked SetTransaction flow (hash-based alreadyExists guard, blockChanged handling, and gating of UTXO vs certificate processing) plus the deposits plumbing and required‑deposit validation form a coherent, idempotent path for new vs replayed transactions. I don’t see correctness issues in this control flow based on the current design.

database/models/certificate.go (1)

25-164: CertificateType and unified Certificate model are well‑structured

The CertificateType enum, its Value/Scan methods, and the Certificate schema (including the (transaction_id, cert_index) unique index and CertType field) align cleanly with the unified certificate mapping design and should work correctly with GORM/SQLite. The conservative range check in Scan is reasonable given the small, fixed certificate type set.

database/models/account.go (1)

27-188: Account and certificate model indexing strategy matches the unified certificate design

Switching Account.StakingKey from uniqueIndex to index, adding AddedSlot/Active indexes, and using composite uniques on (StakingKey, CertificateID) for Deregistration, StakeDeregistration, and StakeRegistration correctly support:

  • multiple historical certificates per stake key, and
  • de‑duplication at the (staking_key, certificate) level.

The accompanying comment block accurately documents this constraint strategy, and it lines up with the new GetAccount/SetAccount behavior in the sqlite store.

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

24-69: Account retrieval and persistence align with the new Account schema

GetAccount’s WHERE staking_key = ? AND active = true plus ORDER BY added_slot DESC correctly targets the latest active account record per stake key, consistent with the indexed fields in models.Account. SetAccount’s creation of a new Account row with Active and AddedSlot set by the caller matches that model and the broader move toward certificate‑driven account state.

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

143-145: UTXO methods now using lcommon types look correct and consistent.

Switching GetUtxosByAddress to lcommon.Address and SetUtxoDeletedAtSlot to lcommon.TransactionInput aligns the interface with the canonical ledger/common types, matching the established pattern elsewhere in the codebase. No further changes needed here.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 952e932 to b327165 Compare November 19, 2025 03: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: 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/store.go (1)

29-147: Type mismatch at call sites: database/utxo.go uses incompatible ledger. types*

The MetadataStore interface and sqlite implementations correctly use lcommon.Address and lcommon.TransactionInput, but database/utxo.go passes incompatible types:

  • Line 111: d.metadata.SetUtxoDeletedAtSlot(utxoId, ...) where utxoId: ledger.TransactionInput → expects lcommon.TransactionInput
  • Line 124: d.metadata.GetUtxosByAddress(addr, ...) where addr: ledger.Address → expects lcommon.Address

database/utxo.go only imports github.com/blinklabs-io/gouroboros/ledger but needs lcommon "github.com/blinklabs-io/gouroboros/ledger/common". Either convert the parameters or update function signatures to use lcommon.* types consistently (as done in database/transaction.go, database/certificate.go, etc.).

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

63-498: Per-type CertificateID fields are still unset for many certificate models, breaking uniqueness and cleanup semantics

storeCertificate wires unifiedCert.CertificateID = specificModelID, but only some per-type models set their own CertificateID to unifiedCert.ID (e.g., StakeRegistration, StakeDeregistration, Deregistration, PoolRegistration). Many others with a CertificateID column never initialize it:

  • Account-related: StakeDelegation, Registration, StakeRegistrationDelegation, VoteDelegation, StakeVoteDelegation, VoteRegistrationDelegation, StakeVoteRegistrationDelegation.
  • Pool: PoolRetirement.
  • DRep: RegistrationDrep, DeregistrationDrep, UpdateDrep.
  • Committee/genesis: AuthCommitteeHot, ResignCommitteeCold, GenesisKeyDelegation.
  • MIR/Leios: MoveInstantaneousRewards, LeiosEb.

Given the model definitions:

  • Several tables have CertificateID with a uniqueIndex:*_cert tag (e.g., PoolRegistration, PoolRetirement, GenesisKeyDelegation, AuthCommitteeHot, MoveInstantaneousRewards, LeiosEb, ResignCommitteeCold, various DRep models).
  • CertificateCleanupValidation in certs_test.go explicitly simulates cleanup by deleting specialized rows via WHERE certificate_id IN (?), implying cleanup relies on these fields.

Leaving CertificateID at the zero value for these models leads to two concrete problems:

  1. Uniqueness collisions: any table where CertificateID participates in a single-column unique index will accept the first insert (certificate_id = 0) and then reject subsequent inserts, even when they represent distinct certificates.
  2. Orphaned specialized rows on cleanup: bulk removal by certificate_id will not match rows where CertificateID was never set, so specialized certificate data can be left behind when the unified certificates row is deleted.

The final comment in the function:

// The specific model already has the correct CertificateID set, so no need to update it

is currently untrue for these types.

I recommend:

  • For every per-type model that defines a CertificateID field, set it to unifiedCert.ID when creating the model:
model := &models.MoveInstantaneousRewards{
    RewardData:    rewardData,
    Source:        c.Reward.Source,
    OtherPot:      c.Reward.OtherPot,
    AddedSlot:     types.Uint64{Val: point.Slot},
    CertificateID: unifiedCert.ID,
}

(similarly for LeiosEb, DRep types, AuthCommitteeHot, ResignCommitteeCold, GenesisKeyDelegation, PoolRetirement, delegation variants, etc.)

  • Where you use FirstOrCreate, keep the conflict key on the natural uniqueness (e.g., (GenesisHash, VrfKeyHash) or (ColdCredential, HostCredential)), but also ensure CertificateID is set on the resulting row; if needed, follow the pattern used in the stake registration case and apply an explicit Updates with the new CertificateID after creation.

This will make uniqueness constraints behave as intended and ensure bulk certificate cleanup can reliably remove specialized rows by certificate ID.

#!/bin/bash
# Verify all models with a CertificateID field and check whether storeCertificate sets it

echo "=== Models with CertificateID field ==="
rg -n 'CertificateID\s+uint' database/models -n -C2

echo -e "\n=== storeCertificate assignments to CertificateID ==="
rg -n 'CertificateID:' database/plugin/metadata/sqlite/certs.go -n -C3

echo -e "\n=== Check bulk removal implementation uses certificate_id ==="
rg -n 'bulkRemoveCertificateRecords' -n
rg -n 'certificate_id IN' database/plugin/metadata/sqlite -n -C3
database/plugin/metadata/sqlite/certs_test.go (1)

37-89: Invalid loop syntax: for i := range 3 does not compile

Several tests still use for i := range 3:

  • TestPoolCertificateDuplicatePrevention (pool registration and retirement loops).
  • TestStakeCertificateDuplicatePrevention (stake registration and deregistration loops).
  • TestDrepCertificateReRegistration (registration, deregistration, and update loops).
  • TestGenesisKeyDelegationCertificateIdempotent (outer loop).

In Go, range cannot be used on an integer literal; these loops will not compile.

Replace each with a counted loop, e.g.:

-    // Insert pool registration record multiple times
-    for i := range 3 {
+    // Insert pool registration record multiple times
+    for i := 0; i < 3; i++ {
         regCert := models.PoolRegistration{
             // ...
         }
         err := metadata.DB().Create(&regCert).Error
         // ...
     }

Apply the same pattern to all other for i := range 3 usages in this file.

Also applies to: 119-135, 164-209, 229-242, 259-267, 284-291, 329-368

🧹 Nitpick comments (1)
database/plugin/metadata/sqlite/transaction.go (1)

52-122: SetTransaction: idempotent UTXO writes, invalid-tx short-circuit, and deposit validation are well-structured; double‑check re-inclusion block metadata

This implementation hits the important behaviors:

  • Uses a hash-based alreadyExists flag so that:
    • First processing writes transaction, UTXO associations, and certificates.
    • Replays skip UTXO work and (via the alreadyExists checks) also skip certificate re-processing, making the path idempotent.
  • For invalid transactions, processCertificates returns immediately, so no certificate or deposit state is persisted, matching ledger semantics.
  • For new valid transactions with certificates, deposits is required and per-cert deposits are enforced only for the expected registration types, while still preserving legitimate zero-deposit entries.

One thing worth verifying: on re-inclusion with blockChanged == true you update transaction.block_hash/block_index and all certificate slots/added_slot, but the certificates.BlockHash field is left unchanged here. If any consumers rely on certificates.block_hash to reflect the current canonical block (rather than the first inclusion), you may want to update that column alongside slot in the re-inclusion path to avoid inconsistent block metadata between transaction and certificates.

Also applies to: 133-145, 343-422

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 952e932 and b327165.

📒 Files selected for processing (38)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (6 hunks)
  • database/plugin/metadata/sqlite/drep.go (3 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 (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • go.mod (3 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (3 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (3)
  • database/account.go
  • database/models/certs.go
  • database/drep.go
✅ Files skipped from review due to trivial changes (1)
  • database/models/account_test.go
🚧 Files skipped from review as they are similar to previous changes (10)
  • database/models/asset.go
  • database/models/certificate.go
  • database/models/genesis_key_delegation.go
  • database/models/models.go
  • database/models/leios_eb.go
  • database/types/types_test.go
  • go.mod
  • internal/config/config_test.go
  • database/models/transaction.go
  • database/types/types.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/transaction.go
  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/transaction.go
  • database/models/utxo.go
  • ledger/delta.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/transaction.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/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/state.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:

  • ledger/state.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:

  • ledger/state.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/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: 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/state.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/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/models/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/utxo.go
🧬 Code graph analysis (16)
database/models/move_instantaneous_rewards.go (1)
database/types/types.go (1)
  • Uint64 (59-61)
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 (79-103)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/models/resign_committee_cold.go (1)
database/types/types.go (1)
  • Uint64 (59-61)
database/models/certificate_test.go (1)
database/models/certificate.go (3)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeStakeDeregistration (35-37)
database/plugin/metadata/sqlite/transaction.go (4)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (59-61)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (10)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeRegistration (47-49)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • Certificate (146-154)
  • Certificate (157-159)
ledger/state.go (1)
database/plugin/log.go (1)
  • Logger (18-27)
database/plugin/metadata/sqlite/drep.go (1)
database/models/drep.go (1)
  • Drep (21-29)
database/transaction.go (3)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/utxo.go (1)
  • UtxoBlobKey (268-276)
database/plugin/metadata/sqlite/certs_test.go (11)
database/plugin/metadata/sqlite/database.go (1)
  • New (51-124)
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 (59-61)
database/models/account.go (7)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • Account (27-35)
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/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (59-61)
database/models/account.go (2)
database/models/pool.go (1)
  • Pool (26-38)
database/models/drep.go (1)
  • Drep (21-29)
ledger/delta.go (1)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/plugin/metadata/sqlite/certs.go (10)
database/models/certificate.go (3)
  • Certificate (146-154)
  • Certificate (157-159)
  • CertificateType (28-28)
database/models/account.go (20)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • StakeDelegation (92-98)
  • StakeDelegation (100-102)
  • Registration (80-86)
  • Registration (88-90)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeRegistrationDelegation (127-134)
  • StakeRegistrationDelegation (136-138)
  • VoteDelegation (167-173)
  • VoteDelegation (175-177)
  • StakeVoteDelegation (140-147)
  • StakeVoteDelegation (149-151)
  • VoteRegistrationDelegation (179-186)
  • VoteRegistrationDelegation (188-190)
  • StakeVoteRegistrationDelegation (153-161)
  • StakeVoteRegistrationDelegation (163-165)
database/types/types.go (2)
  • Uint64 (59-61)
  • Rat (25-27)
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/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/auth_committee_hot.go (1)
database/types/types.go (1)
  • Uint64 (59-61)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
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). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: nilaway
  • GitHub Check: docker (ubuntu-latest, amd64)
🔇 Additional comments (22)
internal/node/load.go (1)

173-199: LGTM: Robust wait mechanism with proper timeout and cleanup.

The cancellable context with 5-minute timeout and periodic polling ensures the ledger catches up before proceeding. Proper cleanup with deferred cancel and ticker.Stop() prevents resource leaks.

database/models/certificate_test.go (1)

25-166: Excellent test coverage for CertificateType database marshaling.

The tests comprehensively verify Value() and Scan() behavior with multiple input types, edge cases (zero, large values), error cases (invalid types, negative, overflow), and interface compliance. This ensures robust GORM integration.

database/models/pool.go (1)

44-98: Constraint design correctly supports pool lifecycle operations.

The index strategy properly allows:

  • Multiple registrations per pool (PoolKeyHash is indexed, not unique at line 48)
  • One retirement per pool per epoch (composite unique at lines 92, 96)
  • Certificate-level idempotency via CertificateID unique constraints (lines 55, 93)

This resolves the past review concern about preventing legitimate re-registrations.

database/models/utxo.go (1)

50-79: Proper migration to types.Uint64 struct with correct value wrapping.

The UtxoLedgerToModel function correctly wraps slot and amount values in types.Uint64{Val: ...} structs (lines 58-59). The IsCollateralReturn field addition supports collateral tracking per the Babbage-era UTXO model.

database/models/account.go (1)

60-121: Well-designed constraint strategy balancing idempotency and historical tracking.

The composite unique constraints on (StakingKey, CertificateID) for Deregistration, StakeDeregistration, and StakeRegistration (lines 69-71, 105-106, 116-117) properly prevent duplicate certificate processing while allowing legitimate re-registration cycles. Other certificate types use simple indexes for historical tracking.

The inline documentation (lines 60-67) clearly explains the design rationale. This addresses previous review concerns about preventing legitimate stake key lifecycle operations.

ledger/view.go (1)

111-112: Correct adaptation to types.Uint64 struct.

Accessing pool.Pledge.Val and pool.Cost.Val properly reflects the migration from type alias to struct with Val field.

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

89-90: Correct .Val field access for types.Uint64 migration.

The changes properly adapt to the types.Uint64 struct by accessing cert.Pledge.Val and cert.Cost.Val.

database/models/move_instantaneous_rewards.go (1)

21-34: Correct constraint design allows multiple MIR certificates per slot.

The uniqueness constraint on CertificateID alone (line 25) properly prevents duplicate certificate records while allowing multiple legitimate MIR certificates in the same slot from the same pot. Source and AddedSlot use simple indexes (lines 26, 28) for query performance without blocking valid scenarios.

This addresses the past review concern about over-strict composite uniqueness.

database/models/drep.go (1)

17-29: DRep credential/indexing layout now matches expected registration lifecycle

Using uniqueIndex on Drep.DrepCredential and plain index on the registration/deregistration/update tables gives you a single canonical DRep row while still allowing multiple cert records over time, which avoids the earlier re-registration constraint issue. The use of types.Uint64 for deposit amounts is consistent with the new typed-Uint64 handling.

Also applies to: 35-55, 61-68

database/models/auth_committee_hot.go (1)

17-27: AuthCommitteeHot uniqueness and certificate linkage look consistent

ColdCredential as unique plus a dedicated unique index on CertificateID provides clear 1:1 constraints for both the credential and backing certificate, and the types.Uint64 AddedSlot aligns with the new typed-Uint64 pattern used elsewhere.

ledger/state.go (1)

24-24: Certificate deposits and validation behavior are wired correctly into block processing

Precomputing deposits only for cert-bearing transactions and then calling delta.addTransactionWithDeposits(tx, i, deposits) integrates the new deposits path cleanly. The updated validation logic now only swallows "value not conserved" errors for transactions with certificates and still aborts on all other validation failures, restoring strict validation while keeping the temporary certificate-deposit workaround.

Also applies to: 845-895

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

21-24: UTxO address and input handling now correctly use ledger/common types and key hashes

GetUtxosByAddress now derives payment/stake key hashes from lcommon.Address and only builds payment_key/staking_key predicates when the corresponding hash is non-zero, which aligns with the UTXO schema and avoids spurious queries. Updating SetUtxoDeletedAtSlot to accept lcommon.TransactionInput keeps the metadata store consistent with the rest of the ledger/common plumbing.

Also applies to: 96-137, 230-252

database/certificate.go (1)

21-24: Certificate persistence docs and accessors align with the unified SetTransaction flow

The new comments make the “certs are persisted via SetTransaction” contract explicit, and the GetPoolRegistrations/GetStakeRegistrations helpers correctly delegate to the metadata store using the caller’s transaction context.

Also applies to: 25-31, 33-39

database/models/resign_committee_cold.go (1)

17-30: ResignCommitteeCold uniqueness and slot typing look appropriate

The unique constraint on ColdCredential plus a dedicated unique index on CertificateID matches the expected “one resignation per cold credential and certificate” semantics, and switching AddedSlot to types.Uint64 keeps it consistent with the new typed-Uint64 representation used in other committee models.

ledger/certs.go (1)

23-59: Certificate deposit calculation and zero-value preservation look correct

The helper cleanly centralizes deposit computation via CertDepositFunc, records an entry for every certificate index (including zero deposits), and returns precise error context per index. No issues from the ledger side.

database/transaction.go (1)

22-63: Transactional wrapper and deposits plumb-through look sound

The named-error + deferred commit/rollback pattern correctly finalizes only internally-created transactions, and any error from blob writes or metadata persistence prevents commit. Deposits are cleanly threaded into the metadata path without changing UTXO semantics.

ledger/delta.go (1)

25-47: Deposit propagation and invalid-transaction handling in LedgerDelta.apply are consistent

The updated delta flow correctly:

  • Skips deposit calculation for invalid transactions while still recording UTXO consumption.
  • Reuses precomputed deposits when available and otherwise derives them via CalculateCertificateDeposits only for certificate-bearing, valid transactions.
  • Passes nil deposits only when either the transaction is invalid or has no certificates, which aligns with the metadata layer’s checks.

This keeps deposit semantics and idempotency intact end-to-end.

Also applies to: 61-63, 76-101

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

427-475: Certificate slot re-inclusion updates now cover the stake-related tables

updateCertificateSlots correctly:

  • Updates certificates.slot for all rows tied to the transaction.
  • Propagates the new slot into added_slot for a comprehensive set of specialized certificate tables, including the previously-missing stake registration/deregistration/registration-delegation tables.

This keeps timing information consistent across the unified mapping and specialized tables on re-inclusion.

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

36-48: Vacuum lifecycle and in-memory initialization look robust

The added vacuumWG + closed checks ensure that:

  • VACUUM isn’t run for in-memory stores or after Close is initiated.
  • Any in-flight VACUUM completes before the DB handle is closed, preventing goroutine leaks and “VACUUM on closed DB” errors.

The in-memory path in New and the clarified Close/AutoMigrate wrappers are consistent with the rest of the metadata store.

Also applies to: 50-124, 141-156, 191-210

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

17-79: GetDrep/SetDrep DRep upsert flow looks correct

The updated GetDrep and SetDrep implementations correctly:

  • Query by drep_credential (matching models.Drep.DrepCredential).
  • Return (nil, nil) on ErrRecordNotFound.
  • Use an ON CONFLICT (drep_credential) upsert to maintain the latest added_slot, anchor_url, anchor_hash, and active fields.

No issues from a correctness standpoint.

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

24-69: SetAccount is dead code and never called—the raised concern does not apply to any active code path

Verification shows:

  • SetAccount is defined but never invoked anywhere in the codebase (production or otherwise)
  • Account records are never created in any real execution flow
  • Deactivation in tests uses direct Save() on retrieved records, bypassing SetAccount entirely
  • GetAccount is used, but its paired write function does not exist in production code

The concern about inserting deactivation records with active=false and causing stale reads is theoretically sound, but the code path does not exist—SetAccount is unreachable.

Either remove the unused GetAccount/SetAccount methods or clarify if they are intended infrastructure for a feature not yet implemented.

Likely an incorrect or invalid review comment.

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

392-462: Test coverage for certificate persistence and idempotency is strong

The test suite successfully validates:

  • Uniqueness and "re-registration allowed" behavior for pools, stake keys, DReps, and genesis key delegation.
  • Correct population and uniqueness of AuthCommitteeHot (including CertificateID).
  • Cleanup behavior matching bulkRemoveCertificateRecords by deleting per-type rows via certificate_id.
  • Validation of types.Uint64 round-trips in multiple models.
  • End-to-end behavior of SetTransaction under idempotent and re-inclusion scenarios, including deposit requirements and slot updates.
  • Data mapping for MIR, LeiosEb, and anchor-bearing certificates.

All 11 certificate-related tests pass without errors. This suite provides valuable regression testing for the new unified certificate persistence path.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from b327165 to 8d1aaa4 Compare November 19, 2025 14:56
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/utxo.go (1)

128-139: Propagate IsCollateralReturn when materializing UTXOs by address

models.Utxo now has an IsCollateralReturn flag, but UtxosByAddress builds a fresh struct without copying it. Collateral-return UTXOs returned by address look like normal outputs (flag remains false), which can break consumers relying on that distinction.

Include the flag in the struct literal:

 		tmpUtxo = models.Utxo{
 			ID:          utxo.ID,
 			TxId:        utxo.TxId,
 			OutputIdx:   utxo.OutputIdx,
 			AddedSlot:   utxo.AddedSlot,
 			DeletedSlot: utxo.DeletedSlot,
 			PaymentKey:  utxo.PaymentKey,
 			StakingKey:  utxo.StakingKey,
 			Amount:      utxo.Amount,
 			Assets:      utxo.Assets,
+			IsCollateralReturn: utxo.IsCollateralReturn,
 		}
♻️ Duplicate comments (2)
database/plugin/metadata/sqlite/certs.go (1)

240-252: Normalize PoolKeyHash mapping across stake delegation certificate variants

In the stake delegation-related cases, PoolKeyHash is mapped inconsistently:

  • StakeDelegationCertificate and StakeVoteRegistrationDelegationCertificate use c.PoolKeyHash.Bytes().
  • StakeRegistrationDelegationCertificate and StakeVoteDelegationCertificate assign PoolKeyHash: c.PoolKeyHash directly.

Given the models declare PoolKeyHash []byte, it would be good to confirm whether c.PoolKeyHash is a hash object (needing .Bytes()) or already a []byte for those two variants. If all four certificates expose the same typed hash, standardizing on .Bytes() everywhere would avoid surprises; if the types differ intentionally, a short comment here explaining why some cases pass []byte and others call .Bytes() would make this clearer.

Also applies to: 265-277

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

37-38: Fix invalid loop syntax for i := range 3 (tests won’t compile)

All of these loops:

for i := range 3 {
    // ...
}

are invalid Go; range does not operate on an integer. This will prevent the test file from compiling.

Replace them with standard counted loops, e.g.:

-	for i := range 3 {
+	for i := 0; i < 3; i++ {

Apply this change to every occurrence (TestPoolCertificateDuplicatePrevention, TestStakeCertificateDuplicatePrevention, TestDrepCertificateReRegistration, and TestGenesisKeyDelegationCertificateIdempotent).

Also applies to: 74-75, 119-120, 165-166, 229-230, 259-260, 285-286, 329-330

🧹 Nitpick comments (5)
database/types/types.go (1)

40-56: Consider handling NULL DB values explicitly in Scan for robustness

The Scan implementations for both Rat and Uint64 are sound for non-null string/[]byte columns. If any columns using these types can be NULL, scanning will currently fail with a type error. You may want to treat NULL as “leave zero value” instead of erroring.

Example adjustment:

 func (r *Rat) Scan(val any) error {
 	if r.Rat == nil {
 		r.Rat = new(big.Rat)
 	}
-	var v string
+	if val == nil {
+		// Keep zero-value rat when column is NULL.
+		return nil
+	}
+	var v string
 	switch t := val.(type) {
 	case string:
 		v = t
 	case []byte:
 		v = string(t)
 	default:
 		return fmt.Errorf(
 			"value was not expected type, wanted string or []byte, got %T",
 			val,
 		)
 	}
@@
 func (u *Uint64) Scan(val any) error {
-	var v string
+	if val == nil {
+		// Treat NULL as zero; adjust if a different semantic is desired.
+		u.Val = 0
+		return nil
+	}
+	var v string
 	switch t := val.(type) {
 	case string:
 		v = t
 	case []byte:
 		v = string(t)
 	default:
 		return fmt.Errorf(
 			"value was not expected type, wanted string or []byte, got %T",
 			val,
 		)
 	}
 	tmpUint, err := strconv.ParseUint(v, 10, 64)
 	if err != nil {
 		return err
 	}
 	u.Val = tmpUint
 	return nil
 }

Please double‑check against how these columns are defined in your migrations and what the SQL driver actually returns for the configured column types.

Also applies to: 58-85

database/certificate.go (1)

21-39: Guard certificate getters against nil transactions

The new comment correctly describes that certificates are now persisted via SetTransaction, and these getters just query metadata. However, both helpers call txn.Metadata() without checking for txn == nil, unlike other database methods that create a transaction when nil.

To make these safer and consistent with the rest of the DB API, consider:

 func (d *Database) GetPoolRegistrations(
 	poolKeyHash lcommon.PoolKeyHash,
 	txn *Txn,
 ) ([]lcommon.PoolRegistrationCertificate, error) {
-	return d.metadata.GetPoolRegistrations(poolKeyHash, txn.Metadata())
+	if txn == nil {
+		txn = d.Transaction(false)
+		defer txn.Commit() //nolint:errcheck
+	}
+	return d.metadata.GetPoolRegistrations(poolKeyHash, txn.Metadata())
 }
@@
 func (d *Database) GetStakeRegistrations(
 	stakingKey []byte,
 	txn *Txn,
 ) ([]lcommon.StakeRegistrationCertificate, error) {
-	return d.metadata.GetStakeRegistrations(stakingKey, txn.Metadata())
+	if txn == nil {
+		txn = d.Transaction(false)
+		defer txn.Commit() //nolint:errcheck
+	}
+	return d.metadata.GetStakeRegistrations(stakingKey, txn.Metadata())
 }

If callers always supply a non-nil txn here, documenting that contract would also help.

database/transaction.go (1)

22-64: SetTransaction lifecycle and deposits wiring are solid; avoid shadowing err

The new SetTransaction correctly:

  • Handles internally-created transactions with a single deferred commit/rollback decision, and
  • Threads the deposits map through to metadata after writing produced UTXO blobs.

To reduce confusion around the named return, consider avoiding := when setting err inside the loop:

-	for _, utxo := range tx.Produced() {
+	for _, utxo := range tx.Produced() {
 		// Add UTxO to blob DB
 		key := UtxoBlobKey(
 			utxo.Id.Id().Bytes(),
 			utxo.Id.Index(),
 		)
-		err := txn.Blob().Set(key, utxo.Output.Cbor())
-		if err != nil {
+		if err = txn.Blob().Set(key, utxo.Output.Cbor()); err != nil {
 			return err
 		}
 	}

Functionally this is equivalent, but it keeps all error handling clearly tied to the named err.

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

33-35: Clarify/enforce single-active-account invariant for SetAccount

GetAccount now returns the most recent active account for a staking key, and SetAccount always inserts a new row with the provided active flag. This works assuming other codepaths reliably mark previous accounts for the same key as Active = false; otherwise you can accumulate multiple active rows per staking key (even though GetAccount will pick only the latest).

If the intent is to guarantee a single active account per staking key, consider either:

  • explicitly deactivating prior rows for that key in the same transaction that calls SetAccount, or
  • enforcing it at the DB level with an additional partial unique index (e.g., on (staking_key, active) with active = true), if your SQLite setup allows it.

Also applies to: 52-58

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

126-132: Minor documentation clarification: specify deposit units and simplify default value explanation.

The deposits map comment is detailed but could be improved:

  1. Specify units: The comment doesn't indicate that deposit amounts are in lovelace (Cardano's base unit). Consider adding: map[int]uint64, // deposits: certificate index -> deposit amount in lovelace

  2. Simplify default behavior: The comment states "Missing keys default to 0. Non-deposit certificates should have 0 values" — this is somewhat redundant. Since missing keys already default to 0, you could simplify to: "Missing keys default to 0, so only include certificates with non-zero deposits."

This makes the caller's responsibility clearer: they only need to populate entries for certificates that actually have deposits.

Apply this diff to improve the comment:

 	SetTransaction(
 		ocommon.Point,
 		lcommon.Transaction,
 		uint32, // transaction index within block
-		map[int]uint64, // deposits: certificate index -> deposit amount. Keys must align with transaction certificate slice order. Missing keys default to 0. Non-deposit certificates should have 0 values.
+		map[int]uint64, // deposits: certificate index -> deposit amount in lovelace. Keys must align with transaction certificate slice order. Missing keys default to 0.
 		*gorm.DB,
 	) error
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b327165 and 8d1aaa4.

📒 Files selected for processing (40)
  • database/account.go (0 hunks)
  • database/certificate.go (1 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/blob/aws/database.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (6 hunks)
  • database/plugin/metadata/sqlite/drep.go (3 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 (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • database/utxo.go (3 hunks)
  • go.mod (3 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (3 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (3)
  • database/account.go
  • database/drep.go
  • database/models/certs.go
✅ Files skipped from review due to trivial changes (1)
  • database/plugin/blob/aws/database.go
🚧 Files skipped from review as they are similar to previous changes (16)
  • database/models/move_instantaneous_rewards.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/models.go
  • database/types/types_test.go
  • database/models/genesis_key_delegation.go
  • database/models/pool.go
  • ledger/view.go
  • database/models/leios_eb.go
  • database/models/certificate_test.go
  • go.mod
  • database/models/certificate.go
  • internal/node/load.go
  • internal/config/config_test.go
  • database/models/auth_committee_hot.go
  • database/models/account_test.go
  • database/models/drep.go
🧰 Additional context used
🧠 Learnings (11)
📚 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/transaction.go
  • database/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • ledger/delta.go
  • database/models/transaction.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/database.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/utxo.go
  • database/models/transaction.go
  • database/models/utxo.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/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/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:

  • database/utxo.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/utxo.go
  • ledger/state.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: 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/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: 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/utxo.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/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/utxo.go
  • ledger/state.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
📚 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/sqlite/certs_test.go
  • database/models/utxo.go
  • ledger/state.go
  • database/plugin/metadata/store.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/state.go
🧬 Code graph analysis (15)
database/models/resign_committee_cold.go (1)
database/types/types.go (1)
  • Uint64 (59-61)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/transaction.go (3)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/utxo.go (1)
  • UtxoBlobKey (268-276)
database/plugin/metadata/sqlite/transaction.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-48)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (59-61)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (10)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeRegistration (47-49)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • Certificate (146-154)
  • Certificate (157-159)
ledger/delta.go (1)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/models/transaction.go (2)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (59-61)
database/plugin/metadata/sqlite/certs_test.go (11)
database/plugin/metadata/sqlite/database.go (1)
  • New (51-124)
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 (59-61)
database/models/account.go (7)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • Account (27-35)
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/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (59-61)
ledger/state.go (1)
database/plugin/log.go (1)
  • Logger (18-27)
database/models/account.go (2)
database/models/pool.go (1)
  • Pool (26-38)
database/models/drep.go (1)
  • Drep (21-29)
database/plugin/metadata/sqlite/drep.go (1)
database/models/drep.go (1)
  • Drep (21-29)
ledger/certs.go (1)
ledger/state.go (1)
  • LedgerState (79-103)
database/models/asset.go (1)
database/types/types.go (1)
  • Uint64 (59-61)
database/plugin/metadata/sqlite/certs.go (11)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-48)
database/models/certificate.go (3)
  • Certificate (146-154)
  • Certificate (157-159)
  • CertificateType (28-28)
database/models/account.go (20)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • StakeDelegation (92-98)
  • StakeDelegation (100-102)
  • Registration (80-86)
  • Registration (88-90)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeRegistrationDelegation (127-134)
  • StakeRegistrationDelegation (136-138)
  • VoteDelegation (167-173)
  • VoteDelegation (175-177)
  • StakeVoteDelegation (140-147)
  • StakeVoteDelegation (149-151)
  • VoteRegistrationDelegation (179-186)
  • VoteRegistrationDelegation (188-190)
  • StakeVoteRegistrationDelegation (153-161)
  • StakeVoteRegistrationDelegation (163-165)
database/types/types.go (2)
  • Uint64 (59-61)
  • Rat (25-27)
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/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
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). (3)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (16)
ledger/certs.go (1)

23-59: Certificate deposit calculation looks correct and preserves zero-value semantics

The method cleanly delegates to CertDepositFunc, records an entry for every certificate index (including zero deposits), and surfaces errors with useful context. This aligns well with the new unified certificate persistence flow.

database/models/asset.go (1)

68-74: Struct-based Uint64 usage is correct here

Switching to types.Uint64{Val: amount} matches the new struct definition and keeps the previous amount semantics intact.

database/models/resign_committee_cold.go (1)

17-30: ResignCommitteeCold model constraints look consistent with intended semantics

The uniqueness on ColdCredential, the unique index on CertificateID, and the switch to types.Uint64 for AddedSlot bring this model in line with the unified certificate mapping and other certificate models.

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

89-90: Uint64 → uint64 extraction for pool registrations is correct

Using cert.Pledge.Val and cert.Cost.Val properly unwraps the new types.Uint64 fields when reconstructing PoolRegistrationCertificate.

database/utxo.go (1)

24-25: Type migration to lcommon inputs/addresses looks consistent

Switching UtxoConsume to lcommon.TransactionInput and UtxosByAddress to lcommon.Address matches the rest of the metadata/store interfaces in this PR and keeps DB APIs aligned with the ledger/common layer. I don’t see functional regressions here.

Also applies to: 102-112

database/models/transaction.go (1)

17-36: Transaction associations and monetary types now look correctly wired

Using where:is_collateral_return = false/true on Outputs and CollateralReturn aligns the GORM mappings with the new IsCollateralReturn column on Utxo, and reverting Fee/TTL to types.Uint64 keeps them consistent with other amount fields and existing DB encoding.

ledger/state.go (1)

849-895: Certificate deposit precomputation and narrowed validation suppression look correct

Precomputing deposits via CalculateCertificateDeposits and threading them through delta.addTransactionWithDeposits integrates cleanly with the new deposits flow. The validation logic now only swallows "value not conserved" errors for certificate-bearing transactions and treats all other ValidateTxFunc errors as fatal, which restores strict validation while still working around the known deposit issue.

ledger/delta.go (1)

25-48: Delta now models deposits correctly for valid vs. invalid transactions

Storing Deposits in TransactionRecord and using addTransactionWithDeposits lets you:

  • Ignore deposits entirely for invalid transactions (always passing nil into SetTransaction), and
  • Prefer precomputed deposits but fall back to CalculateCertificateDeposits for valid certificate-bearing txs when needed.

This matches the intended ledger semantics and keeps the deposit flow centralized.

Also applies to: 49-99

database/models/utxo.go (1)

25-39: UTxO type wrapping and collateral-return flag look consistent

Switching AddedSlot, DeletedSlot, and Amount to types.Uint64 and wrapping them in UtxoLedgerToModel is consistent with the rest of the models, and the IsCollateralReturn flag lines up with the transaction associations for collateral returns. No issues spotted here.

Also applies to: 58-59

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

63-503: Unified certificate + per-type models implementation is coherent

The storeCertificate flow (create unified Certificate, then per-type model, then back-link CertificateID) now consistently populates all key fields (including deposits, anchors, MIR/Leios JSON payloads, and committee/DRep data) and wires CertificateID both ways. This matches the schema in the various models.* types and should satisfy the unified tracking requirement without reintroducing the uniqueness/linkage issues from earlier revisions.

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

65-85: SetTransaction idempotency, deposits, and slot bumping look correct

The new SetTransaction flow cleanly separates concerns:

  • Upsert by hash with a precomputed alreadyExists flag and blockChanged detection.
  • UTXO creation only on first insertion, with collateral return flagged via IsCollateralReturn.
  • Certificate handling gated so invalid transactions and pure replays don’t create duplicates, while re-inclusion with a different block only bumps slot/added_slot via updateCertificateSlots.
  • Deposits are enforced only for the certificate types that actually require them, while zero-deposit certs still pass with a default of 0.

The updateCertificateSlots helper also correctly targets certificates plus all specialized tables that carry added_slot, keeping certificate timing consistent across a re-included transaction.

Also applies to: 343-375, 427-475

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

29-47: DRep get/upsert behavior is consistent with the model

GetDrep now has the expected “(nil, nil) on not-found” behavior, and SetDrep’s ON CONFLICT (drep_credential) upsert correctly drives added_slot, anchor fields, and active. This matches the models.Drep schema and should behave well under repeated updates.

Also applies to: 58-77

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

36-48: SQLite store lifecycle and vacuum coordination are well-handled

The revised New/init/runVacuum/Close flow cleanly separates in-memory vs on-disk behavior, guards VACUUM on dataDir == "" and closed, and uses vacuumWG plus the timer stop in Close to avoid running VACUUM against a closed DB or leaking goroutines. The small wrapper/docs additions on AutoMigrate/DB helpers are also straightforward.

Also applies to: 50-68, 141-156, 191-205

database/models/account.go (1)

28-35: Certificate constraint strategy and Account indexing are consistent

The move to:

  • plain index on Account.StakingKey with an indexed Active flag and AddedSlot, and
  • composite uniques on (StakingKey, CertificateID) only for Deregistration, StakeDeregistration, and StakeRegistration,

matches the documented strategy and the way storeCertificate now populates CertificateID. This allows multiple historical certs per stake key while still enforcing one row per certificate ID where needed, and keeps account lookups fast on (staking_key, active, added_slot).

Also applies to: 60-67, 68-75, 104-121

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

143-143: LGTM! Type consistency improvement.

The parameter type change from ledger.Address to lcommon.Address improves consistency across the interface, where other ledger types (PoolKeyHash, Blake2b256, TransactionInput) also use the lcommon package. Past review verified that implementations have been updated to match.


145-145: LGTM! Consistent with the GetUtxosByAddress type change.

The parameter type change from ledger.TransactionInput to lcommon.TransactionInput maintains interface consistency. Past review verified the implementation updates.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 8d1aaa4 to d70daec Compare November 19, 2025 19:20
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: 3

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/account.go (1)

52-82: SetAccount's FirstOrCreate pattern conflicts with multi-record account design—confirmed critical bug.

GetAccount retrieves accounts with Order("added_slot DESC").First(ret), indicating multiple records per staking key are expected, but SetAccount uses FirstOrCreate which retrieves the first (oldest) matching record.

When multiple records exist for a staking key:

  • GetAccount retrieves the most recent (by added_slot)
  • SetAccount retrieves the oldest (by default ID order in FirstOrCreate)

This causes SetAccount to update the wrong historical record. For example, if a stake key has records from registration (slot 1000) and delegation (slot 2000), SetAccount would modify the slot 1000 record rather than the current slot 2000 record.

Additionally, the Account model has gorm:"index" on StakingKey (non-unique), confirming the schema allows multiple records per staking key.

To fix:

  • If multi-record design is intended: Create new records rather than using FirstOrCreate, or query for the most recent record matching staking_key before updating
  • If single-record design is intended: Add gorm:"uniqueIndex" constraint on StakingKey and fix the misleading comment
♻️ Duplicate comments (1)
database/plugin/metadata/sqlite/certs_test.go (1)

37-38: Fix invalid for i := range 3 loops (compile-time error)

All occurrences of:

for i := range 3 {
    ...
}

are invalid in Go (range cannot iterate over an integer). This file will not compile.

Replace them with standard counted loops:

-	for i := range 3 {
+	for i := 0; i < 3; i++ {

Apply this change to all the 3-iteration loops in this file (pool registration/retirement, stake registration/deregistration, DRep registration/deregistration/update, and genesis key delegation tests).

Also applies to: 74-75, 119-120, 229-230, 259-260, 285-286, 329-330

🧹 Nitpick comments (6)
internal/node/load.go (1)

93-198: Async ledger start and cancellable catch-up loop look good; consider configurability and safety checks.

The switch to ls.Start() plus a timeout-based wait with a ticker is a nice improvement over a blind block. Two small follow-ups to consider:

  • The 5*time.Minute timeout and 5*time.Second poll interval are hard-coded. Depending on network size and hardware, five minutes might be too short or overly conservative. Making these values configurable (or at least constants) would give you some operational flexibility.
  • The loop assumes ls.Tip() always returns a non-nil value with a valid Point. If Tip() can ever transiently return a zero/empty value (e.g., before any block is processed), a defensive check (or a documented guarantee on Tip()) would avoid surprises.

These are polish items; the overall control flow and error handling look solid.

database/database_test.go (1)

22-24: TestUtxosByAddressCollateralReturnFlag doesn’t exercise the real UtxosByAddress path.

Right now the test builds an originalUtxo, manually copies every field (including IsCollateralReturn) into copiedUtxo, and then asserts they match. This only verifies the test’s own copy code, not the production logic that previously dropped the flag.

To make this guard against regressions, consider:

  • Creating an in-memory DB/metadata store, inserting a UTXO row with IsCollateralReturn = true, and then
  • Calling the exported API that’s supposed to return UTXOs by address (e.g., the database-level UtxosByAddress wrapper), asserting that the returned slice contains the flag set on the corresponding element.

That way, if the mapping logic changes or a new field gets missed again, the test will fail for the right reason instead of still passing due to in-test duplication.

Also applies to: 74-121

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

96-137: Address-based UTXO query logic is sound in intent; verify GORM subquery usage and consider simplifying.

The new flow (derive payment/stake hashes from lcommon.Address, ignore zero hashes, and early-return when both are empty) is a clear improvement and matches how addresses decompose into payment/stake parts.

The one area that’s a bit subtle is:

if addrQuery == nil {
    return ret, nil
}
result := txn.
    Where("deleted_slot = 0").
    Where(addrQuery).
    Find(&ret)

Here addrQuery is a *gorm.DB built from txn. In GORM, passing a *gorm.DB into Where is treated as a subquery; the generated SQL typically ends up as ... WHERE deleted_slot = 0 AND id IN (SELECT id FROM utxos WHERE ...) (or similar, based on primary key). That’s likely what you want, but it’s non-obvious and easy to get wrong if the schema or primary key changes.

Two suggestions:

  • Double-check the actual SQL generated (e.g., with Debug()) to ensure it’s effectively deleted_slot = 0 AND (payment_key = ? OR staking_key = ?) and not something looser/tighter than intended.
  • For readability, consider building a single query := txn.Model(&models.Utxo{}).Where("deleted_slot = 0") and adding explicit Where/Or conditions with parentheses in the string, so future readers don’t have to reason about GORM’s subquery semantics.

This doesn’t look immediately broken, but it’s subtle enough that a quick verification would be worthwhile.

database/types/types.go (1)

36-60: Clarify/extend Scan handling for driver numeric types and NULL semantics

The new Scan implementations look coherent, but there is one subtle interoperability concern:

  • Rat.Scan now cleanly handles nil, string, and []byte — that’s fine for text-based columns.
  • Uint64.Scan now:
    • Treats nil as zero, which is a behavioral choice that callers should be aware of.
    • Only accepts string / []byte, and errors for anything else.

If any columns using types.Uint64 are backed by integer types (e.g., SQLite/Postgres INTEGER where the driver surfaces int64), those rows will now hit the default case and fail with "wanted string or []byte".

Two follow-ups I’d recommend:

  • Verify that all Uint64-backed columns are mapped as text/char so that the driver actually returns string or []byte.
  • If not guaranteed, consider extending Scan to handle numeric types as well, e.g.:
 func (u *Uint64) Scan(val any) error {
@@
-	switch t := val.(type) {
-	case string:
-		v = t
-	case []byte:
-		v = string(t)
-	default:
-		return fmt.Errorf(
-			"value was not expected type, wanted string or []byte, got %T",
-			val,
-		)
-	}
+	switch t := val.(type) {
+	case string:
+		v = t
+	case []byte:
+		v = string(t)
+	case int64:
+		u.Val = uint64(t) // #nosec G115 – application-level guarantee about range
+		return nil
+	default:
+		return fmt.Errorf(
+			"value was not expected type, wanted string, []byte, or int64, got %T",
+			val,
+		)
+	}

This would make the type more robust to how different drivers materialize integer columns, while preserving your current string/[]byte behavior and nil→0 convention.

Also applies to: 63-95

ledger/state.go (1)

850-895: Certificate deposit precomputation & validation gating look good; reconsider deposits usage on swallowed errors

The new flow in ledgerProcessBlock is much safer:

  • Deposits are only precomputed for certificate TXs.
  • CalculateCertificateDeposits failures abort block processing with context.
  • Validation errors are now only swallowed when the TX has certificates and the error message contains "value not conserved"; all other errors abort, restoring strong validation guarantees.

One subtle point to think through:

  • In the "value not conserved" case you log and continue, but you still pass the previously computed deposits map into delta.addTransactionWithDeposits.
  • If that specific error is symptomatic of a disagreement between ledger validation and your deposit computation, you may want to explicitly discard deposits in this path to avoid persisting deposit data that the ledger just flagged as inconsistent, e.g.:
				if err != nil {
-					// For transactions with certificates, deposits may not be properly accounted for in validation
-					// Only swallow "value not conserved" errors for certificate transactions
-					if len(tx.Certificates()) > 0 &&
-						strings.Contains(err.Error(), "value not conserved") {
-						ls.config.Logger.Warn(
-							"TX with certificates failed deposit validation (known issue): " + err.Error(),
-						)
-					} else {
-						return nil, fmt.Errorf("TX validation failure: %w", err)
-					}
+					// For transactions with certificates, deposits may not be properly accounted for in validation.
+					// Only swallow "value not conserved" errors for certificate transactions, and in that case
+					// skip recording deposits to avoid persisting potentially inconsistent values.
+					if len(tx.Certificates()) > 0 &&
+						strings.Contains(err.Error(), "value not conserved") {
+						ls.config.Logger.Warn(
+							"TX with certificates failed deposit validation (known issue): "+err.Error(),
+						)
+						deposits = nil
+					} else {
+						return nil, fmt.Errorf("TX validation failure: %w", err)
+					}
				}

If your intent is explicitly to trust your own CalculateCertificateDeposits result even when the ledger complains, then the current behavior is fine — but it would be good to document that choice in a comment so future readers know this is intentional rather than an oversight. The string-match on "value not conserved" is also inherently brittle, so having that rationale spelled out will help if the underlying library ever changes its error text.

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

128-185: Minor redundancy around transaction rollback in TestSetAccountWithTransaction

The test both defers txn.Rollback() and calls txn.Rollback() explicitly before verifying the absence of the account. It’s harmless but redundant; consider dropping the deferred rollback and keeping the explicit call to simplify the control flow.

-	// Start a transaction
-	txn := metadataStore.DB().Begin()
-	require.NoError(t, txn.Error)
-	defer txn.Rollback()
+	// Start a transaction
+	txn := metadataStore.DB().Begin()
+	require.NoError(t, txn.Error)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d1aaa4 and d70daec.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (42)
  • database/account.go (1 hunks)
  • database/certificate.go (1 hunks)
  • database/database_test.go (2 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/blob/aws/database.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/account_test.go (1 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (3 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 (3 hunks)
  • database/transaction.go (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • database/utxo.go (4 hunks)
  • go.mod (4 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (3 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (2)
  • database/drep.go
  • database/models/certs.go
✅ Files skipped from review due to trivial changes (3)
  • database/plugin/blob/aws/database.go
  • internal/config/config_test.go
  • database/models/certificate_test.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • database/models/models.go
  • database/models/transaction.go
  • database/utxo.go
  • database/types/types_test.go
  • database/models/asset.go
  • database/models/move_instantaneous_rewards.go
  • go.mod
  • database/models/drep.go
🧰 Additional context used
🧠 Learnings (10)
📚 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/database_test.go
  • database/models/utxo.go
  • ledger/state.go
  • 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/database_test.go
  • database/models/utxo.go
  • ledger/state.go
  • database/plugin/metadata/sqlite/certs_test.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/database_test.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • 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: 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/database_test.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:

  • database/database_test.go
  • ledger/state.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: 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/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/models/utxo.go
  • database/models/genesis_key_delegation.go
  • ledger/delta.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/database.go
  • database/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/utxo.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:

  • ledger/state.go
  • database/plugin/metadata/store.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/state.go
🧬 Code graph analysis (23)
database/models/leios_eb.go (1)
database/types/types.go (1)
  • Uint64 (63-65)
database/database_test.go (2)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (63-65)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (63-65)
database/models/genesis_key_delegation.go (1)
database/types/types.go (1)
  • Uint64 (63-65)
ledger/state.go (1)
database/plugin/log.go (1)
  • Logger (18-27)
database/plugin/metadata/sqlite/certs.go (11)
database/models/certificate.go (4)
  • CertificateTypeStakeRegistration (32-34)
  • Certificate (146-154)
  • Certificate (157-159)
  • CertificateType (28-28)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-49)
database/models/account.go (20)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • StakeDelegation (92-98)
  • StakeDelegation (100-102)
  • Registration (80-86)
  • Registration (88-90)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeRegistrationDelegation (127-134)
  • StakeRegistrationDelegation (136-138)
  • VoteDelegation (167-173)
  • VoteDelegation (175-177)
  • StakeVoteDelegation (140-147)
  • StakeVoteDelegation (149-151)
  • VoteRegistrationDelegation (179-186)
  • VoteRegistrationDelegation (188-190)
  • StakeVoteRegistrationDelegation (153-161)
  • StakeVoteRegistrationDelegation (163-165)
database/types/types.go (2)
  • Uint64 (63-65)
  • Rat (25-27)
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/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
ledger/delta.go (1)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/plugin/metadata/sqlite/transaction.go (4)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (63-65)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (10)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeRegistration (47-49)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • Certificate (146-154)
  • Certificate (157-159)
database/plugin/metadata/sqlite/certs_test.go (11)
database/plugin/metadata/sqlite/database.go (1)
  • New (52-125)
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 (63-65)
database/models/account.go (7)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • Account (27-35)
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/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/certificate.go (3)
  • Certificate (146-154)
  • Certificate (157-159)
  • CertificateTypeStakeRegistration (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/plugin/metadata/sqlite/database.go (1)
database/plugin/log.go (1)
  • Logger (18-27)
database/models/resign_committee_cold.go (1)
database/types/types.go (1)
  • Uint64 (63-65)
database/plugin/metadata/sqlite/drep.go (1)
database/models/drep.go (1)
  • Drep (21-29)
ledger/view.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
database/models/auth_committee_hot.go (1)
database/types/types.go (1)
  • Uint64 (63-65)
database/transaction.go (3)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/utxo.go (1)
  • UtxoBlobKey (269-277)
database/plugin/metadata/sqlite/pool.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
database/models/account.go (2)
database/models/pool.go (1)
  • Pool (26-38)
database/models/drep.go (1)
  • Drep (21-29)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (79-103)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/models/utxo.go (1)
  • Utxo (24-40)
database/account.go (3)
database/models/account.go (1)
  • Account (27-35)
database/database.go (1)
  • Database (45-50)
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/account_test.go (8)
database/plugin/metadata/sqlite/database.go (2)
  • New (52-125)
  • MetadataStoreSqlite (39-49)
database/plugin/metadata/store.go (1)
  • New (151-157)
database/models/pool.go (1)
  • Pool (26-38)
database/models/drep.go (1)
  • Drep (21-29)
database/models/account.go (3)
  • Account (27-35)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (63-65)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/plugin/metadata/sqlite/account.go (1)
database/models/account.go (1)
  • Account (27-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). (8)
  • GitHub Check: lint
  • GitHub Check: go-test (1.25.x, macos-latest)
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: go-test (1.24.x, macos-latest)
  • GitHub Check: go-test (1.25.x, ubuntu-latest)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (28)
database/models/account_test.go (1)

27-34: Structured test table with explicit wantErrMsg/expectedHRP improves clarity.

Reordering the table to surface wantErrMsg and expectedHRP and then using them selectively in the assertions makes the tests easier to reason about while keeping behavior unchanged. The gating on wantErr and validateAddr is correct and avoids accidental HRP checks on error cases.

Also applies to: 98-167

database/models/leios_eb.go (1)

21-37: LeiosEb model and indexes look consistent with certificate mapping design.

The composite unique index on (ElectionID, EndorserBlockHash) plus a separate unique index on CertificateID gives you stable identity from both the Leios view and the unified certificate view, and AddedSlot types.Uint64 matches the rest of the schema. No issues spotted.

database/models/resign_committee_cold.go (1)

17-30: ResignCommitteeCold uniqueness constraints are now explicit and consistent.

Making ColdCredential unique and tagging CertificateID with its own unique index cleanly enforces “one resign per cold credential, one record per certificate” at the DB level. The shift to types.Uint64 for AddedSlot also keeps this model in line with the rest of the certificate models.

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

80-91: Pool registration field mappings correctly track the new types.

Using uint(...) for CertType and reading Pledge/Cost from .Val is the right adaptation to the updated types in the models. The rest of the mapping logic is unchanged and still coherent.

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

232-252: Switch to lcommon.TransactionInput for delete marker aligns with ledger/common usage.

Updating SetUtxoDeletedAtSlot to accept lcommon.TransactionInput and then using utxoId.Id().Bytes() / utxoId.Index() keeps the metadata store consistent with the rest of the codebase’s move toward ledger/common types. The deleted_slot update query itself is unchanged and remains straightforward.

Just ensure at call sites that utxoId.Id() is always non-nil/valid before invoking this method so you don’t risk panics when dereferencing or calling Bytes().

database/models/pool.go (1)

55-60: Pool registration/retirement uniqueness aligns with certificate-centric persistence

The updated GORM tags look consistent with the unified certificate model:

  • PoolRegistration.CertificateID as uniqueIndex:uniq_pool_registration_cert gives you a clean 1:1 mapping from registration certificate → row, which is ideal for idempotent upserts.
  • PoolRetirement now has:
    • PoolKeyHash and Epoch participating in uniqueIndex:uniq_pool_retirement, allowing multiple retirements per pool across epochs but preventing duplicate retirements for the same (pool, epoch) pair.
    • CertificateID with its own unique index, again giving a 1:1 certificate→row mapping.

This preserves support for re-registrations and multiple retirements while enforcing the right idempotency guarantees at the certificate level.

Also applies to: 91-98

database/transaction.go (1)

22-63: Consolidated SetTransaction lifecycle and deposits propagation look solid

This implementation is much cleaner:

  • The named return + single deferred closure ensures internally created transactions are either fully committed on success or rolled back on any error, without risking a commit-after-rollback sequence.
  • UTXO blob writes short-circuit on failure and correctly propagate the error into the deferred rollback path.
  • deposits is threaded straight through to metadata.SetTransaction, matching the new ledger/delta deposit handling.

Behavior for caller-supplied txn is preserved (no implicit commit/rollback), so this is a safe evolution of the API.

ledger/view.go (1)

103-113: PoolCurrentState correctly adapts to new Uint64 and CertType representations

The adjustments here look correct:

  • Using uint(lcommon.CertificateTypePoolRegistration) matches the updated CertType field type on lcommon.PoolRegistrationCertificate.
  • Reading Pledge and Cost from pool.Pledge.Val / pool.Cost.Val is the right adaptation to the new types.Uint64 struct, and preserves the previous numeric behavior (defaulting to 0 when the pool was not found and a zero-value models.Pool{} is used).

No further changes needed in this method.

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

28-126: SetAccount tests nicely verify update semantics and inactive filtering

This test exercises first‑write, in‑place update (including Active flip), uniqueness of the account row, and the includeInactive flag on GetAccount; behavior matches the Account model and unified account semantics.


187-285: End-to-end DeleteCertificate test is thorough and well-scoped

Manually creating the Transaction, unified Certificate, and StakeRegistration rows, then asserting both their removal and idempotent behavior on a second DeleteCertificate call, gives strong coverage of the new unified certificate deletion path.

database/certificate.go (1)

21-47: Getter helpers for pool/stake registrations are simple and consistent

The auto‑transaction pattern when txn == nil plus direct delegation to the metadata layer keeps the public API easy to use while preserving transactional control for advanced callers; no issues spotted.

database/models/auth_committee_hot.go (1)

17-27: AuthCommitteeHot model updates align with unified certificate mapping

Moving AddedSlot to types.Uint64, adding CertificateID, and enforcing uniqueness on ColdCredential bring this model in line with the rest of the certificate‑backed schema; looks coherent with the unified Certificate mapping.

database/models/certificate.go (1)

1-164: CertificateType Valuer/Scanner and unified Certificate model look correct

The CertificateType constants mirror the ledger types, the Valuer/Scanner implementations defensively handle common integer/[]byte shapes with overflow and basic range checks, and the unified Certificate struct (TransactionID + cert_index unique pair, polymorphic CertificateID, indexed CertType) matches the intended polymorphic certificate mapping.

Please ensure your existing tests cover round‑tripping CertType values through the actual DB driver(s) you use so that all driver return types encountered in practice are exercised.

database/models/genesis_key_delegation.go (1)

21-34: GenesisKeyDelegation model correctly encodes uniqueness and 1:1 certificate mapping

The composite uniq_genesis_delegation unique index plus the unique index on CertificateID capture both idempotent insertion and the one‑to‑one relationship to the unified certificates row; using types.Uint64 for AddedSlot keeps it consistent with other certificate models.

If you rely on OnConflict clauses for this model in the sqlite persistence layer, double‑check that the conflict target matches these unique index columns/names so idempotency behaves as intended.

ledger/certs.go (1)

37-59: Certificate deposit calculation is consistent and preserves zero-value entries

The loop correctly defers deposit semantics to CertDepositFunc, records an entry for every certificate index (including zeros), and surfaces per-index errors with context, which matches how SetTransaction expects to consume the map. No issues from this implementation.

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

52-60: Stake registration getter change looks correct

Using CertType: uint(lcommon.CertificateTypeStakeRegistration) when reconstructing StakeRegistrationCertificate instances brings the returned value in line with the ledger type metadata and avoids zero-type ambiguities.

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

127-141: Lifecycle, vacuum, and certificate-cleanup scheduling look sound

The revised init, runVacuum, scheduleDailyVacuum, runCertificateCleanup, scheduleCertificateCleanup, and Close implementations correctly coordinate timers with the closed flag and timerMutex, and the vacuumWG ensures no vacuum runs against a closed DB. The certificate cleanup hook is safely a guarded no-op for now and won’t race Close.

Also applies to: 144-159, 189-241, 248-264

database/models/utxo.go (1)

23-40: UTxO model and conversion align with new transaction associations

Switching AddedSlot/Amount to types.Uint64 and adding IsCollateralReturn cleanly support the Transaction.Outputs vs CollateralReturn associations, and UtxoLedgerToModel populates the typed fields as expected, leaving the collateral-return flag to the caller. No issues here.

Also applies to: 50-60

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

52-145: SetTransaction’s idempotency and deposit plumbing are coherent

The revised SetTransaction correctly:

  • Detects existing transactions by hash up front and reloads the ID post-upsert.
  • Avoids re-processing UTXOs for already-seen transactions.
  • Skips certificate handling for invalid transactions.
  • Uses the deposits map only for new, valid transactions, while allowing zero-value deposits and guarding only the certificate types that must carry a deposit.

This matches the intended ledger flow and the new tests around re-inclusion and deposit presence.

Also applies to: 343-425

ledger/delta.go (1)

25-29: LedgerDelta now threads deposits into persistence correctly

The new TransactionRecord.Deposits field and addTransactionWithDeposits/apply logic cleanly separate:

  • Invalid transactions: consume UTxOs, record the tx with nil deposits, and skip certificate effects.
  • Valid transactions: reuse precomputed deposits when supplied or compute them once via CalculateCertificateDeposits when certificates are present, then pass them through to SetTransaction.

This matches the intended two-phase validation semantics and the new database API.

Also applies to: 37-47, 49-89

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

25-47: DRep lookup and upsert behavior are now consistent with the model

GetDrep correctly queries by drep_credential and uses (*models.Drep, error) with (nil, nil) for not-found, while SetDrep upserts on drep_credential and updates slot/anchor/active fields atomically. This aligns with the models.Drep definition and simplifies caller logic.

Also applies to: 49-79

database/models/account.go (2)

28-35: Account model index changes support multi-record design correctly.

The transition from uniqueIndex to index on StakingKey enables multiple account records per stake key over time, while the new indexes on AddedSlot, CertificateID, and Active support the query patterns for retrieving current/historical state.


60-67: Excellent documentation of certificate constraint strategy.

The comment block clearly articulates the design rationale: composite unique constraints for deregistration types prevent duplicate certificate storage, while simple indexes for other types enable historical tracking across multiple certificates.

database/account.go (1)

22-51: Clean delegation pattern preserves backward compatibility.

The refactoring of GetAccount to delegate to GetAccountWithInactive with includeInactive=false maintains existing behavior while providing the flexibility to query inactive accounts when needed.

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

36-48: GetAccount query logic correctly implements most-recent-record retrieval.

The conditional filtering by active and ordering by added_slot DESC properly retrieves the most recent account record, with optional filtering to active accounts only.

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

56-60: GetAccount signature correctly reflects includeInactive support.

The added bool parameter for includeInactive properly exposes the capability to query both active and inactive accounts, consistent with the implementation in sqlite/account.go.


127-133: SetTransaction signature properly includes deposits for certificate processing.

The updated signature adds the transaction index (uint32) and deposits map (map[int]uint64), enabling proper certificate deposit tracking. The comment clearly documents the deposits map semantics: keys align with certificate slice order and missing keys default to 0.


144-146: Parameter types correctly use lcommon canonical types.

The updated parameter types (lcommon.Address, lcommon.TransactionInput) align with the canonical ledger/common types used throughout the codebase, ensuring consistent type usage across the interface.

Comment on lines 427 to 476
// updateCertificateSlots updates the slot for all certificates associated with a transaction
func (d *MetadataStoreSqlite) updateCertificateSlots(
txID uint,
newSlot uint64,
txn *gorm.DB,
) error {
if txn == nil {
txn = d.DB()
}

// Update slot in certificates table
if err := txn.Model(&models.Certificate{}).
Where("transaction_id = ?", txID).
Update("slot", newSlot).Error; err != nil {
return fmt.Errorf("update certificate slot: %w", err)
}

// Update added_slot in specialized certificate tables
// These tables have certificate_id that references the certificates.id
certTables := []string{
"pool_registration",
"pool_retirement",
"genesis_key_delegations",
"move_instantaneous_rewards",
"resign_committee_cold",
"auth_committee_hot",
"registration",
"deregistration",
"stake_registration",
"stake_deregistration",
"stake_delegation",
"stake_registration_delegation",
"vote_delegation",
"stake_vote_delegation",
"vote_registration_delegation",
"stake_vote_registration_delegation",
"registration_drep",
"deregistration_drep",
"update_drep",
}

for _, table := range certTables {
// Update added_slot where certificate_id is in the certificates for this transaction
if err := txn.Table(table).
Where("certificate_id IN (SELECT id FROM certificates WHERE transaction_id = ?)", txID).
Update("added_slot", newSlot).Error; err != nil {
return fmt.Errorf("update %s added_slot: %w", table, err)
}
}
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 | 🟠 Major

Include LeiosEb in re-inclusion slot updates

updateCertificateSlots updates added_slot for most specialized certificate tables, but the certTables slice omits the leios_ebs table even though models.LeiosEb has an AddedSlot indexed field and participates in the unified certificate mapping.

As a result, when a transaction is re-included in a different block (and blockChanged is true), certificates.slot is updated, but LeiosEb rows keep the old added_slot, leaving inconsistent timing for that certificate type.

Consider adding it:

 	certTables := []string{
 		"pool_registration",
 		"pool_retirement",
 		"genesis_key_delegations",
 		"move_instantaneous_rewards",
 		"resign_committee_cold",
 		"auth_committee_hot",
 		"registration",
 		"deregistration",
+		"leios_ebs",
 		"stake_registration",
 		"stake_deregistration",
 		"stake_delegation",
 		"stake_registration_delegation",
 		"vote_delegation",

to keep LeiosEb added_slot in sync with other certificates on re-inclusion.

🤖 Prompt for AI Agents
In database/plugin/metadata/sqlite/transaction.go around lines 427 to 475, the
certTables slice used to update added_slot for specialized certificate tables
omits the leios_ebs table, so LeiosEb rows keep old added_slot on transaction
re-inclusion; fix by adding "leios_ebs" to the certTables slice so the loop also
executes the UPDATE for that table and keeps LeiosEb.added_slot in sync with
certificates.slot.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from d70daec to a14738d Compare November 19, 2025 21: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: 3

Caution

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

⚠️ Outside diff range comments (1)
ledger/state.go (1)

647-688: Epoch rollover errors are silently ignored due to err shadowing

Inside the needsEpochRollover block:

var err error
...
txn := ls.db.Transaction(true)
err := txn.Do(func(txn *database.Txn) error { ... })
ls.Unlock()
if err != nil {
    ...
}

The err := creates a new inner err scoped only to that block; the if err != nil that follows uses the outer err, which is still its previous value (likely nil). Any failure from txn.Do will therefore be dropped, and the function continues as if the epoch rollover succeeded.

Use assignment instead of declaration so the outer err is updated:

-        txn := ls.db.Transaction(true)
-        err := txn.Do(func(txn *database.Txn) error {
+        txn := ls.db.Transaction(true)
+        err = txn.Do(func(txn *database.Txn) error {
             ...
         })
         ls.Unlock()
         if err != nil {
             ls.config.Logger.Error(
                 "failed to process epoch rollover: " + err.Error(),
             )
             return
         }

(or equivalently if err := txn.Do(...); err != nil { ... } with logging and early return kept inside that block).

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

21-26: Confirm uniqueness migration safety and CertificateID population for AuthCommitteeHot.

Changing ColdCredential to gorm:"unique" and adding a unique index on CertificateID (uniq_auth_committee_hot_cert) hardens constraints, but both can break AutoMigrate if there is existing data:

  • When CertificateID is added, all existing rows will have the same default value (typically 0), so creating a unique index can fail unless the table is empty or you backfill distinct values first.
  • The CertificateID field now must be populated consistently from the unified certificates flow (e.g., in the storeAuthCommitteeHot/certificate persistence path), otherwise new inserts/updates may hit unique‑index violations or store meaningless 0 values.

Please double‑check:

  1. That any existing auth_committee_hot data is either absent or migrated so the new unique index can be created safely.
  2. That the code creating AuthCommitteeHot records always sets CertificateID to the corresponding unified certificate record ID, and that there is a plan for backfilling older rows if they exist.
database/plugin/metadata/sqlite/certs_test.go (1)

36-58: Fix invalid for i := range 3 loops – file won’t compile

Several tests use for i := range 3 { ... }, but range cannot be used on an integer literal in Go; this is a compile-time error. You need standard counted loops.

Update all these loops (pool, stake, DRep, genesis tests, etc.) to:

-    for i := range 3 {
+    for i := 0; i < 3; i++ {
         ...
     }

Apply this pattern to every for i := range 3 occurrence in this file.

Also applies to: 73-89, 118-135, 228-243, 259-267, 285-292, 329-368

🧹 Nitpick comments (5)
database/plugin/metadata/sqlite/account_test.go (1)

28-126: Good coverage of account upsert and inactive behavior; consider factoring setup.

This test exercises the full lifecycle (create, update, deactivate, includeInactive flag) and even checks the underlying models.Account row, which is great for catching regressions in upsert logic and the includeInactive semantics.

If you add more metadata tests, consider extracting the DB/metadataStore setup (database.New + cast to *sqlite.MetadataStoreSqlite) into a small helper to reduce duplication across tests, but it’s not required here.

database/models/certificate.go (1)

25-139: CertificateType Value/Scan pair is robust; consider minor input/range tweaks.

The CertificateType implementation correctly:

  • Stores as an int64 via Value() (with the G115 suppression justified by the small constant range), and
  • Handles common database representations in Scan (int64, int, uint, uint64 with overflow checks, and []byte decimal strings), plus a sanity range check.

Two optional refinements you could consider (non‑blocking):

  • Accept string values in Scan as well, in case a driver hands back text instead of []byte.
  • Replace the hard‑coded val > 1000 bound with something derived from the maximum known lcommon.CertificateType* value (or simply drop the upper bound and rely on the set of valid constants elsewhere), to avoid surprises if new certificate types are added with larger numeric codes.
database/plugin/metadata/sqlite/database.go (1)

36-49: Background vacuum/cleanup lifecycle is safe; mirror WG pattern when cleanup gains work

The vacuum path now correctly guards on dataDir == "" || closed, tracks in‑flight runs with vacuumWG, and blocks Close() until they finish, which addresses earlier races with VACUUM on a closed DB. The certificate‑cleanup timer is also properly gated by closed and stopped in Close().

Once runCertificateCleanup is implemented with real work, consider mirroring the vacuumWG pattern there too so any long‑running cleanup cannot outlive the store or race with shutdown.

Also applies to: 127-141, 144-187, 189-241, 248-264

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

64-244: Unified storeCertificate correctly populates per‑type models and deposit fields

The new storeCertificate implementation looks solid:

  • Creates a unified models.Certificate row up front, then per‑type rows with CertificateID set to the unified ID, and finally backfills unifiedCert.CertificateID with the per‑type primary key. That symmetric linkage matches how updateCertificateSlots and DeleteCertificate expect to join.
  • Pool registration now restores the full payload (margin, metadata URL/hash, owners, relays, pledge, cost, deposit, slot) and attaches owners/relays to both the registration and pool via PoolRegistrationID/PoolID.
  • MIR and Leios certificates now persist all semantic data via JSON blobs and identifiers, instead of just a slot.
  • DRep and committee certs once again carry anchors where present, and all specialized models receive a non‑zero CertificateID, avoiding the previous uniqueness and orphaning issues.
  • Deregistration certificates now use c.Amount instead of the deposit map, which correctly reflects the on‑chain deregistration amount; the >= 0 guard is defensive but could be simplified once the underlying type guarantees non‑negative values.

The follow‑up updateAccountState centralizes stake/vote account updates and looks consistent with Conway semantics for staking key activation, (pool, drep) delegation, and legacy registration/deregistration.

Also applies to: 283-513


655-811: DeleteCertificate matches the new CertificateID wiring; consider coverage guard for future types

DeleteCertificate now:

  • Locates the unified models.Certificate by (transaction_id, cert_index).
  • Deletes the corresponding specialized row by filtering on certificate_id = cert.ID, which is consistent with the new “specialized.CertificateID → certificates.id” mapping established in storeCertificate.
  • Then removes the unified certificate row itself, cleanly tearing down both sides for all explicitly handled CertificateType* variants.

This aligns with the new schema and should avoid the earlier orphaning bugs. As a future‑proofing measure, you might add a small test or assert that every models.CertificateType* is covered by the switch so new certificate kinds can’t silently fall into the default branch and leak specialized rows.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d70daec and a14738d.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (43)
  • database/account.go (1 hunks)
  • database/certificate.go (1 hunks)
  • database/database_test.go (2 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/blob/aws/database.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/account_test.go (1 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (3 hunks)
  • database/plugin/metadata/sqlite/pool.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (3 hunks)
  • database/plugin/metadata/store.go (3 hunks)
  • database/transaction.go (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • database/utxo.go (4 hunks)
  • go.mod (4 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (11 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (2)
  • database/drep.go
  • database/models/certs.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • database/models/asset.go
  • internal/config/config_test.go
  • database/models/genesis_key_delegation.go
  • database/types/types_test.go
  • database/models/drep.go
  • database/models/account_test.go
  • database/models/account.go
🧰 Additional context used
🧠 Learnings (12)
📚 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/utxo.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/store.go
  • database/models/utxo.go
  • database/models/transaction.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/utxo.go
  • database/plugin/metadata/sqlite/utxo.go
  • ledger/delta.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/database_test.go
  • database/plugin/metadata/store.go
  • database/transaction.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/database.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/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/database_test.go
  • database/plugin/metadata/store.go
  • database/models/utxo.go
  • ledger/state.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/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/database_test.go
  • database/plugin/metadata/store.go
  • database/models/utxo.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:

  • database/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/database_test.go
  • ledger/state.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/utxo.go
  • database/plugin/metadata/store.go
  • ledger/state.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/utxo.go
  • database/database_test.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/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/utxo.go
  • database/models/utxo.go
  • database/models/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/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/database_test.go
  • database/plugin/metadata/store.go
  • database/models/utxo.go
  • ledger/state.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/state.go
📚 Learning: 2025-10-22T20:18:45.125Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T20:18:45.125Z
Learning: In tests, Chain.AddBlock must be given a gouroboros ledger.Block (not database/models.Block). Use a lightweight MockBlock (as in chain/chain_test.go) or call models.Block.Decode() when starting from stored rows.

Applied to files:

  • ledger/state.go
🧬 Code graph analysis (25)
database/models/resign_committee_cold.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/pool.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
ledger/view.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/plugin/metadata/sqlite/utxo.go (1)
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)
database/plugin/metadata/sqlite/certs_test.go (11)
database/plugin/metadata/sqlite/database.go (1)
  • New (52-125)
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 (66-68)
database/models/account.go (7)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • Account (27-35)
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/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (79-107)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/models/leios_eb.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/certificate_test.go (1)
database/models/certificate.go (3)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeStakeDeregistration (35-37)
database/models/auth_committee_hot.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
ledger/delta.go (1)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/plugin/metadata/sqlite/transaction.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-49)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (9)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeRegistration (47-49)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • Certificate (146-154)
  • Certificate (157-159)
database/database_test.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • New (52-125)
database/database.go (1)
  • New (118-154)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/plugin/metadata/sqlite/drep.go (1)
database/models/drep.go (1)
  • Drep (21-29)
database/models/move_instantaneous_rewards.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/models/utxo.go (1)
  • Utxo (24-40)
database/plugin/metadata/sqlite/certs.go (11)
database/models/certificate.go (4)
  • CertificateTypeStakeRegistration (32-34)
  • Certificate (146-154)
  • Certificate (157-159)
  • CertificateType (28-28)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-49)
database/models/account.go (18)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • StakeDelegation (92-98)
  • StakeDelegation (100-102)
  • Registration (80-86)
  • Registration (88-90)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeRegistrationDelegation (127-134)
  • StakeRegistrationDelegation (136-138)
  • VoteDelegation (167-173)
  • VoteDelegation (175-177)
  • StakeVoteDelegation (140-147)
  • StakeVoteDelegation (149-151)
  • VoteRegistrationDelegation (179-186)
  • VoteRegistrationDelegation (188-190)
database/types/types.go (2)
  • Uint64 (66-68)
  • Rat (25-27)
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/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/transaction.go (3)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/utxo.go (1)
  • UtxoBlobKey (269-277)
internal/config/config.go (1)
internal/node/load.go (3)
  • LedgerCatchupTimeout (36-36)
  • LedgerCatchupPollInterval (38-38)
  • LedgerCatchupStallTimeout (40-40)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/account.go (1)
database/models/account.go (1)
  • Account (27-35)
database/plugin/metadata/sqlite/account_test.go (5)
database/plugin/metadata/sqlite/database.go (2)
  • New (52-125)
  • MetadataStoreSqlite (39-49)
database/models/account.go (3)
  • Account (27-35)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/account.go (3)
database/models/account.go (1)
  • Account (27-35)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
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). (3)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
  • GitHub Check: docker (ubuntu-latest, amd64)
🔇 Additional comments (40)
database/plugin/blob/aws/database.go (1)

69-71: LGTM!

The multi-line formatting of the error message improves readability. No functional change.

internal/config/config.go (3)

22-22: LGTM!

The time import is necessary for the new duration-based configuration fields.


46-68: LGTM!

The new duration fields are properly integrated with appropriate YAML tags and consistent formatting.


70-92: LGTM!

The default values align with the constants defined in internal/node/load.go and follow a sensible progression (5s poll interval, 5m initial timeout, 10m stall timeout).

internal/node/load.go (2)

101-104: LGTM!

The updated error message accurately reflects the asynchronous ledger startup pattern.


197-244: Consider clarifying the stall detection semantics.

The current stall detection resets whenever any progress is made (line 223-226). This means extremely slow progress (e.g., 1 slot per 9 minutes) would never trigger the stall timeout. If this is the intended behavior (detect complete stalls only, not slow progress), the implementation is correct. Otherwise, consider whether a minimum progress rate should also be enforced.

The nolint comment on line 197 mentions "for-select pattern" but this is actually an infinite loop that exits via return statements within the select case. Consider updating the comment for clarity:

-	for { //nolint:staticcheck // Correct use of for-select pattern for ticker-based polling
+	for { //nolint:staticcheck // Infinite loop exits via return when catchup completes or stalls
ledger/view.go (1)

104-112: LGTM! Type conversions align with unified certificate persistence.

The type conversions for CertType, Pledge, and Cost properly adapt the Pool model fields to the ledger certificate interface. The cast to uint for CertType and accessing .Val for Pledge/Cost are consistent with the broader refactor to typed wrappers.

database/account.go (1)

22-51: LGTM! Clean delegation pattern for account retrieval.

The new GetAccountWithInactive method adds flexibility while maintaining backward compatibility. The delegation in GetAccount with includeInactive=false preserves existing behavior for current callers.

database/models/resign_committee_cold.go (1)

17-30: LGTM! Model properly integrated with unified certificate system.

The changes correctly implement the certificate persistence pattern:

  • Unique constraint on ColdCredential prevents duplicate resignations
  • CertificateID with unique index links to the unified certificate mapping
  • AddedSlot type migration to types.Uint64 is consistent across models
database/models/models.go (1)

29-31: LGTM! New certificate models registered for migration.

The three new certificate models (GenesisKeyDelegation, LeiosEb, MoveInstantaneousRewards) are correctly added to the migration list, enabling proper schema creation for the expanded certificate type coverage.

database/database_test.go (1)

76-176: LGTM! Comprehensive test coverage for IsCollateralReturn flag.

This test thoroughly validates that the IsCollateralReturn flag is preserved through:

  1. Database persistence and metadata retrieval
  2. UTXO copying operations

The test structure is well-organized with clear setup, execution, and assertions.

go.mod (2)

14-14: LGTM! Dependency update to support new certificate types.

The update to gouroboros v0.140.0 is necessary to support the new certificate types (Genesis, Leios, MIR) introduced in this PR.


24-24: LGTM! Standard testing library addition.

Adding testify v1.11.1 provides assertion utilities for the new test suite, particularly for the comprehensive certificate tests.

database/models/move_instantaneous_rewards.go (1)

21-34: LGTM! MIR certificate model properly structured.

The model correctly implements the certificate persistence pattern:

  • CertificateID with unique index ensures 1:1 mapping to the unified certificate table
  • Source and AddedSlot are indexed for efficient lookups
  • AddedSlot uses types.Uint64 consistent with other certificate models
  • RewardData as JSON blob accommodates variable reward structures
database/utxo.go (3)

24-24: LGTM! Consistent import alias for ledger common types.

The change from ledger to lcommon alias improves clarity and consistency with other files in the codebase.


102-112: LGTM! Signature updated to use lcommon types.

The UtxoConsume signature correctly uses lcommon.TransactionInput, aligning with the standardized type usage across the codebase.


114-147: LGTM! Complete UTXO mapping includes IsCollateralReturn.

The UtxosByAddress method properly:

  • Uses lcommon.Address parameter type
  • Maps all UTXO fields including the new IsCollateralReturn flag (line 139)
  • Maintains consistency with the test coverage in database_test.go
database/models/leios_eb.go (1)

21-37: LGTM! Well-structured Leios certificate model.

The LeiosEb model follows established patterns with appropriate unique indexes: the composite index on ElectionID+EndorserBlockHash prevents duplicate endorser blocks, and the separate CertificateID unique index ensures proper one-to-one mapping with the certificates table.

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

82-90: LGTM! Correctly adapted for types.Uint64 struct.

The changes properly access the Val field of the types.Uint64 struct for Pledge and Cost, and correctly cast CertType to uint for the certificate construction.

database/models/transaction.go (1)

26-29: LGTM! Correct filtering of outputs and collateral return.

The where clauses now properly use the snake_case column name is_collateral_return to discriminate between regular outputs and collateral return UTXOs, addressing the previous critical issue.

database/certificate.go (1)

21-46: LGTM! Clean autonomous transaction pattern.

The lifecycle comment clearly documents the new persistence model, and the autonomous transaction creation in the Get methods provides convenient read-only access without requiring explicit transaction management from callers.

database/models/certificate_test.go (2)

25-166: LGTM! Comprehensive test coverage for CertificateType.

The test suite thoroughly covers Value/Scan behavior with various input types, error cases, and interface conformance verification. Well-structured and follows testing best practices.


124-135: Range validation in CertificateType.Scan() is correctly implemented and consistent with test expectations.

The verification confirms that Scan() at line 134 of database/models/certificate.go validates the range val < 0 || val > 1000, which matches the test cases expecting errors for 1001 and uint overflow conditions. The implementation handles both range validation and overflow checks as intended.

database/types/types.go (1)

65-101: LGTM! Systematic Uint64 refactor from primitive to struct.

The change from type Uint64 uint64 to struct { Val uint64 } is systematic and complete across the PR. The updated Value/Scan methods properly handle the Val field and maintain string-based database serialization. All usage sites in the PR have been updated to access the Val field.

database/models/pool.go (1)

44-98: LGTM! Properly configured unique indexes for pool certificates.

The unique index configuration correctly allows pool re-registrations (no unique constraint on PoolKeyHash alone) while preventing duplicate retirements at the same epoch via the composite (PoolKeyHash, Epoch) unique index. The separate CertificateID unique indexes ensure proper one-to-one certificate mapping.

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

96-136: LGTM! Clearer and more maintainable UTXO address query.

The refactored GetUtxosByAddress uses explicit conditionals for different key combinations, making the logic easier to understand and maintain. The early return for addresses with no valid keys avoids unnecessary queries.

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

128-184: Transactional SetAccount behavior test looks solid.

The pattern of doing Begin(), calling SetAccount with the transaction, asserting visibility via GetAccount in the same txn, and then rolling back to verify nothing is persisted through the default handle cleanly validates that SetAccount respects the passed *gorm.DB without auto‑committing.


186-284: DeleteCertificate test nicely validates unified ↔ specialized mapping and cascade.

Manually wiring models.Transaction, a unified models.Certificate, and the corresponding models.StakeRegistration, then:

  • Updating unifiedCert.CertificateID to the specialized record ID,
  • Calling DeleteCertificate(txID, certIndex, txn), and
  • Asserting both the certificate and stake‑registration counts drop to zero,

gives strong confidence that the delete path correctly traverses the unified certificate mapping and cleans up specialized rows, while also confirming that deleting a non‑existent certificate is a no‑op.

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

56-60: Interface changes for accounts, transactions, and UTXOs look consistent with ledger/common usage.

  • Extending GetAccount with includeInactive matches how you’re using it in the new tests and keeps the default (includeInactive=false) behavior intact.
  • The SetTransaction signature (point, lcommon.Transaction, idx, map[int]uint64 deposits, *gorm.DB) lines up with the database wrapper and the ledger/delta deposits flow.
  • Switching GetUtxosByAddress and SetUtxoDeletedAtSlot to lcommon.Address / lcommon.TransactionInput matches the prior guidance to standardize on ledger/common types in this layer, avoiding alias/interface mismatch issues. Based on learnings.

Just ensure all concrete implementations (currently sqlite) and any mock/test implementations have been updated to these exact signatures; go build will catch anything that was missed.

Also applies to: 127-147

database/transaction.go (1)

22-63: Deferred commit/rollback logic looks correct; verify UTXO writes for invalid transactions.

The refactor to a named err return plus the deferred closure that calls Rollback on error and Commit on success fixes the previous “commit after rollback” risk for internally created transactions and keeps the API straightforward for callers that pass an existing *Txn.

One behavior worth double‑checking: the for _, utxo := range tx.Produced() loop runs unconditionally, so any transaction that reaches this method will have its produced outputs written to the blob store, regardless of validity. If callers can pass invalid transactions here (as suggested by the invalid‑tx path in the ledger/delta flow), please confirm that:

  • Either tx.Produced() already accounts for validity (i.e., returns only outputs that should be persisted), or
  • Consumers do not treat the blob store as the canonical ledger UTXO set.

If invalid transactions should not have their outputs written, consider guarding the loop with a validity check or documenting the intended semantics in the function comment.

database/models/certificate.go (1)

141-164: Unified Certificate model and indexing strategy look appropriate.

The Certificate struct cleanly models:

  • A unique (TransactionID, CertIndex) pair with uniq_tx_cert, and
  • A polymorphic CertificateID pointing to the specialized per‑type model, while TransactionID has OnDelete:CASCADE to clear certificates when a transaction is removed.

This matches the unified certificate‑mapping design and should make it straightforward to join from transactions to certificates and then to the specialized tables without relying on complex joins or ambiguous keys.

ledger/certs.go (1)

23-59: Certificate deposit calculator is correct and side‑effect free

The helper now consistently records deposits for every certificate index (including legitimate zero‑deposit entries) and wraps per‑index failures clearly. This matches the new SetTransaction deposit usage and avoids the prior “missing vs zero” ambiguity.

database/models/utxo.go (1)

25-39: UTxO model and mapper align with new collateral‑return semantics

Switching slot/amount to types.Uint64 and introducing IsCollateralReturn with an index matches the updated Transaction associations and the new collateral‑return handling in SetTransaction. The mapper correctly sets AddedSlot and Amount via types.Uint64.

Also applies to: 58-59

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

52-145: SetTransaction idempotency and deposit handling look correct

The reordered flow—checking existence by hash before upsert, reloading the transaction ID after ON CONFLICT, and skipping UTXO/certificate processing on replays—gives a sane idempotent behavior for reprocessing and re‑inclusion. The certificate section correctly:

  • Skips certs on invalid transactions and when none are present.
  • Requires a deposits map only for new transactions with certificates.
  • Treats missing entries as an error only for the known deposit‑carrying certificate types, while defaulting others to 0.

Assuming callers always invoke this inside a DB transaction and roll back on error, the write‑then‑validate pattern remains safe.

Also applies to: 343-375


427-477: Certificate slot update on re‑inclusion is consistent with unified mapping

updateCertificateSlots updates certificates.slot and then added_slot across all specialized tables, including leios_eb, by joining on certificate_idcertificates.id. This matches the new symmetric Certificate/per‑type CertificateID linkage and should keep timing fields coherent when a transaction is re‑included in a different block.

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

26-47: DRep lookup/upsert flow looks consistent with the model schema

Using drep_credential as the key with a pointer-returning GetDrep (nil on not‑found) and a single SetDrep upsert via ON CONFLICT on drep_credential matches models.Drep and gives clear "latest state wins" semantics for URL/hash/active/slot. I don’t see correctness issues here.

Also applies to: 50-79

ledger/delta.go (1)

25-29: Certificate deposit handling in delta.apply aligns with validity semantics

Storing precomputed deposits on TransactionRecord and, in apply, (a) forcing invalid transactions to use deposits == nil while still consuming UTxOs, and (b) only calculating deposits for valid cert‑bearing transactions (or using precomputed ones) matches the intended “no deposits for invalid txs” rule and avoids recomputation where the caller already provided a map. This logic looks correct and side‑effect free.

Also applies to: 37-47, 61-88, 90-99

ledger/state.go (1)

885-936: Precomputing certificate deposits before validation is consistent with the new delta flow

The new ledgerProcessBlock logic calculates deposits once per cert‑bearing transaction and threads them via delta.addTransactionWithDeposits; delta.apply then uses these only for valid transactions and passes nil for invalid ones. This avoids redundant deposit calculations and ensures the store layer always gets a coherent per‑certificate deposit map. The behavior here looks correct.

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

26-49: GetAccount’s includeInactive flag and ordering look reasonable

The refactored GetAccount cleanly supports both “only active” and “include inactive” queries via the includeInactive flag and centralizes the logic around staking_key, returning nil on not‑found. The extra ORDER BY added_slot DESC is consistent with “latest state” semantics and harmless even with the unique staking_key constraint.


52-82: SetAccount’s find-or-create + bulk update pattern is appropriate

Using FirstOrCreate keyed by StakingKey and then a single Updates call for pool, drep, added_slot, and active gives a clear account-centric upsert that respects an optional transaction DB handle. This should play nicely with the unified certificate/account flows; I don’t see correctness or race issues here.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from a14738d to 93d81a8 Compare November 19, 2025 22:27
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: 3

Caution

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

⚠️ Outside diff range comments (1)
database/types/types_test.go (1)

75-81: Fix incorrect test assertion.

Line 79 compares tmpScanner (the scanned struct) against testDef.expectedValue (a string like "123"), which will always fail. It should compare against testDef.origValue to verify round-trip fidelity.

Apply this diff:

 		if !reflect.DeepEqual(tmpScanner, testDef.origValue) {
 			t.Fatalf(
 				"did not get expected value after Scan(): got %#v, expected %#v",
 				tmpScanner,
-				testDef.expectedValue,
+				testDef.origValue,
 			)
 		}
♻️ Duplicate comments (3)
database/plugin/metadata/sqlite/certs_test.go (1)

37-89: Fix for i := range 3 loops – file will not compile

All the counted loops using for i := range 3 are invalid Go and will prevent this test file from compiling (e.g., in TestPoolCertificateDuplicatePrevention, TestStakeCertificateDuplicatePrevention, TestDrepCertificateReRegistration, TestGenesisKeyDelegationCertificateIdempotent, etc.).

Replace each with a standard counted loop:

-	for i := range 3 {
+	for i := 0; i < 3; i++ {

Apply consistently to every occurrence in this file.

Also applies to: 119-179, 229-243, 259-291, 329-368

internal/node/load.go (1)

34-41: Clean up legacy timeout constant and use the shared default

LedgerCatchupTimeout is marked deprecated and isn’t referenced; plus the stall timeout fallback still hardcodes 10 * time.Minute even though LedgerCatchupStallTimeout exists. It would be clearer to drop the unused legacy constant and use LedgerCatchupStallTimeout for the fallback to keep all defaults in one place.

Also applies to: 186-189

ledger/state.go (1)

635-643: Still a race: closing readChainResultCh from the consumer can panic if producer sends concurrently

ledgerProcessBlocks closes readChainResultCh on ls.ctx.Done(), but ledgerReadChain still owns sending to this channel and only checks ls.ctx.Done() at the top of the outer loop. There is a window where:

  • ledgerProcessBlocks sees context cancellation, closes readChainResultCh, and returns, while
  • ledgerReadChain has already passed its select on ls.ctx.Done() and later executes resultCh <- result.

This will panic with “send on closed channel”. Only one side should ever close the channel—typically the producer—or you can avoid closing it entirely and rely solely on ctx.Done() for shutdown.

Suggestion: remove close(readChainResultCh) from the consumer and either:

  • Don’t close the channel at all (both goroutines exit on ctx.Done()), or
  • Move defer close(resultCh) into ledgerReadChain and let the consumer stop when ok == false on receive.
#!/bin/bash
# Verify there is no remaining consumer-side close on readChainResultCh
rg -n "readChainResultCh" ledger/state.go -n -C3

Also applies to: 655-661, 696-717

🧹 Nitpick comments (6)
database/plugin/metadata/sqlite/pool.go (1)

53-75: Pool registration mapping looks correct; confirm pool key hashing

  • Mapping of Pledge/Cost via .Val from types.Uint64 and setting CertType from lcommon.CertificateTypePoolRegistration are consistent with the updated models.
  • One thing to double‑check: GetPoolRegistrations filters on pool_key_hash = lcommon.Blake2b224(pkh).Bytes(), whereas GetPool uses pkh.Bytes() directly. If lcommon.PoolKeyHash already represents a Blake2b224 hash, this could be double‑hashing and prevent lookups from matching persisted rows.

Please confirm whether pkh is pre‑hashed or raw here and align GetPool/GetPoolRegistrations accordingly if needed.

Also applies to: 80-95

database/database_test.go (1)

76-176: Solid verification of IsCollateralReturn propagation; consider using db.UtxosByAddress directly

This test does a good job of asserting that GetUtxosByAddress returns a row with IsCollateralReturn set and that the flag survives a struct‑to‑struct copy.

If you want even stronger coverage against future refactors, you could also exercise the exported db.UtxosByAddress helper (if available) instead of manually reconstructing copiedUtxo, so that the test breaks if the database‑level mapping ever forgets to copy IsCollateralReturn.

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

21-24: GetUtxosByAddress query is structurally sound; clarify OR vs AND semantics

  • The refactor to lcommon.Address, explicit deleted_slot = 0, and filtering on payment_key/staking_key columns matches the models.Utxo schema and resolves the previous payment_addr column mismatch.
  • The zeroHash/PaymentKeyHash()StakeKeyHash() checks look reasonable, assuming the address implementation returns a zero Blake2b224 when a credential is absent.

One thing to double‑check: when both credentials are present you currently query with:

query = query.Where("payment_key = ? OR staking_key = ?", pkh.Bytes(), skh.Bytes())

If the intent is “UTxOs belonging to this exact address,” an AND on both columns would be stricter and avoid picking up UTxOs that share only the payment key or only the staking key if those are ever reused with different counterparts. If, instead, you explicitly want “either credential matches,” the current behavior is correct but worth documenting.

SetUtxoDeletedAtSlot’s switch to lcommon.TransactionInput is consistent with the rest of the ledger‑facing API.

Also applies to: 96-137, 230-252

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

64-513: Unified certificate storage is much healthier; be clear about DeleteCertificate vs account state

The new storeCertificate / updateAccountState / DeleteCertificate trio looks substantially better:

  • Per-type models now set CertificateID to the unified row ID, and the unified Certificate.CertificateID is updated with the specialized PK, so the polymorphic mapping and uniqueness constraints line up.
  • Pool registrations, MIR, Leios, and DRep/committee certs now carry their full semantic payload (margin, metadata, owners/relays, reward maps, voter/signature data, anchors, etc.), avoiding the earlier data-loss regressions.
  • Deregistration correctly records the actual on-chain amount rather than the (always-zero) deposit, and DRep/vote certs now persist their DRep credentials.

DeleteCertificate consistently deletes specialized rows by certificate_id = cert.ID and then removes the unified record, which matches the new linkage model. Just be aware that it intentionally doesn’t touch account state; if you ever intend to drive rollbacks via DeleteCertificate, you’ll likely want a complementary mechanism to recompute or adjust SetAccount outputs as well.

Overall, this file is in good shape.

Also applies to: 515-653, 655-811

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

36-49: Metadata store background lifecycle looks solid; coordinate future cert cleanup with Close

The refactor of MetadataStoreSqlite’s background work is well thought out:

  • runVacuum now respects dataDir == "" and closed, and uses vacuumWG so Close can safely wait for in-flight vacuums before closing the DB.
  • Daily vacuum and the new 6‑hour certificate cleanup scheduler both honor the closed flag and avoid rescheduling once shutdown begins.
  • Close correctly stops both timers, marks the store closed, waits on vacuumWG, and only then closes the underlying DB handle.

Given runCertificateCleanup is currently a no-op with logging, this is safe. When you later introduce real cleanup logic that hits the DB, it would be worth mirroring the vacuumWG pattern (or a dedicated WaitGroup) around that work as well so Close can wait for cleanup tasks just as it does for vacuum.

Also applies to: 127-141, 144-159, 189-241, 248-264

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

36-41: Remove unnecessary Order("added_slot DESC") in GetAccount function.

The Account model has a uniqueIndex constraint on StakingKey, so only one record can exist per staking key. Ordering by added_slot DESC before calling First() adds unnecessary query overhead with no semantic effect.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a14738d and 93d81a8.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (44)
  • blockfetch.go (1 hunks)
  • database/account.go (1 hunks)
  • database/certificate.go (1 hunks)
  • database/database_test.go (2 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/blob/aws/database.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/account_test.go (1 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (3 hunks)
  • database/plugin/metadata/sqlite/pool.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (3 hunks)
  • database/plugin/metadata/store.go (3 hunks)
  • database/transaction.go (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • database/utxo.go (4 hunks)
  • go.mod (4 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (11 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (2)
  • database/drep.go
  • database/models/certs.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • internal/config/config_test.go
  • database/models/certificate_test.go
  • database/models/asset.go
  • database/models/drep.go
  • database/models/certificate.go
  • database/plugin/blob/aws/database.go
  • database/utxo.go
  • database/models/auth_committee_hot.go
  • go.mod
  • database/models/genesis_key_delegation.go
  • ledger/view.go
  • database/models/models.go
🧰 Additional context used
🧠 Learnings (11)
📚 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/transaction.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/database_test.go
  • database/plugin/metadata/sqlite/utxo.go
  • ledger/delta.go
  • database/models/transaction.go
  • database/plugin/metadata/sqlite/database.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/models/utxo.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/transaction.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/utxo.go
  • database/database_test.go
  • ledger/state.go
  • database/plugin/metadata/sqlite/certs_test.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/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/database_test.go
  • 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/models/utxo.go
  • database/database_test.go
  • ledger/state.go
  • database/plugin/metadata/sqlite/certs_test.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/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: 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/database_test.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:

  • database/database_test.go
  • ledger/state.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:

  • ledger/state.go
  • database/plugin/metadata/store.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/state.go
📚 Learning: 2025-10-22T20:18:45.125Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T20:18:45.125Z
Learning: In tests, Chain.AddBlock must be given a gouroboros ledger.Block (not database/models.Block). Use a lightweight MockBlock (as in chain/chain_test.go) or call models.Block.Decode() when starting from stored rows.

Applied to files:

  • ledger/state.go
🧬 Code graph analysis (24)
database/transaction.go (3)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/utxo.go (1)
  • UtxoBlobKey (269-277)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/pool.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
internal/config/config.go (2)
internal/node/load.go (3)
  • LedgerCatchupTimeout (36-36)
  • LedgerCatchupPollInterval (38-38)
  • LedgerCatchupStallTimeout (40-40)
config.go (1)
  • Config (33-55)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (79-107)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/models/move_instantaneous_rewards.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/account_test.go (6)
database/plugin/metadata/sqlite/database.go (2)
  • New (52-125)
  • MetadataStoreSqlite (39-49)
database/plugin/metadata/store.go (1)
  • New (151-157)
database/models/account.go (3)
  • Account (27-35)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/transaction.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-49)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (10)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeRegistration (47-49)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • Certificate (146-154)
  • Certificate (157-159)
database/database_test.go (3)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/plugin/metadata/sqlite/utxo.go (1)
database/models/utxo.go (1)
  • Utxo (24-40)
ledger/delta.go (1)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/plugin/metadata/sqlite/drep.go (1)
database/models/drep.go (1)
  • Drep (21-29)
ledger/state.go (1)
database/plugin/log.go (1)
  • Logger (18-27)
database/models/resign_committee_cold.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/transaction.go (2)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/types/types_test.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/certs.go (11)
database/models/certificate.go (7)
  • CertificateTypeStakeRegistration (32-34)
  • Certificate (146-154)
  • Certificate (157-159)
  • CertificateType (28-28)
  • CertificateTypeStakeDeregistration (35-37)
  • CertificateTypeMoveInstantaneousRewards (86-88)
  • CertificateTypeLeiosEb (89-91)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-49)
database/models/account.go (20)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • StakeDelegation (92-98)
  • StakeDelegation (100-102)
  • Registration (80-86)
  • Registration (88-90)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeRegistrationDelegation (127-134)
  • StakeRegistrationDelegation (136-138)
  • VoteDelegation (167-173)
  • VoteDelegation (175-177)
  • StakeVoteDelegation (140-147)
  • StakeVoteDelegation (149-151)
  • VoteRegistrationDelegation (179-186)
  • VoteRegistrationDelegation (188-190)
  • StakeVoteRegistrationDelegation (153-161)
  • StakeVoteRegistrationDelegation (163-165)
database/types/types.go (2)
  • Uint64 (66-68)
  • Rat (25-27)
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/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/leios_eb.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/account.go (3)
database/models/account.go (1)
  • Account (27-35)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/certs_test.go (11)
database/plugin/metadata/sqlite/database.go (1)
  • New (52-125)
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 (66-68)
database/models/account.go (7)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • Account (27-35)
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/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/certificate.go (2)
  • Certificate (146-154)
  • Certificate (157-159)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/pool.go (1)
database/models/epoch.go (2)
  • Epoch (17-27)
  • Epoch (29-31)
database/plugin/metadata/sqlite/account.go (1)
database/models/account.go (1)
  • Account (27-35)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
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: go-test (1.24.x, macos-latest)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: Analyze (go)
🔇 Additional comments (34)
database/models/account_test.go (1)

26-96: LGTM!

The test struct reorganization improves clarity by grouping expected validation fields (wantErrMsg, expectedHRP) before the input field (stakingKey). The test coverage is comprehensive, including edge cases for empty, nil, single-byte, and boundary values.

database/models/move_instantaneous_rewards.go (1)

21-29: LGTM!

The previous over-restrictive composite unique index has been correctly addressed. The model now uses a unique constraint only on CertificateID (line 25), while Source and AddedSlot have non-unique indexes for query performance. This allows multiple MIR certificates with the same source/slot combination to be stored independently.

database/certificate.go (2)

21-23: LGTM!

The comment clearly documents the architectural decision to handle certificate persistence through SetTransaction, aligning with the unified certificate persistence system described in the PR objectives.


30-33: LGTM!

The auto-transaction creation pattern is correct and consistent with the pattern used in GetStakeRegistrations below. The deferred Commit() with error suppression is appropriate for read-only transactions.

database/types/types.go (3)

40-63: LGTM!

The enhanced Scan() method properly handles nil values (preserving zero-value Rat) and supports multiple input types (string, []byte, int64) with appropriate error messages. The early return for int64 (line 51-52) is an efficient optimization.


65-68: Approved: Uint64 type refactor from alias to struct.

Changing Uint64 from a type alias to a struct wrapper is a breaking change, but it enables proper nil/NULL handling in database contexts and aligns with how GORM handles custom types. The AI summary confirms this change has been propagated throughout the codebase.


74-101: Assumption verified: Application-level guarantee is sound but relies on upstream ledger validation.

The int64→uint64 cast at line 87 assumes non-negative values because Uint64 fields (AddedSlot, Amount, DepositAmount, etc.) are always initialized with data from the blockchain ledger layer (e.g., types.Uint64{Val: utxo.Output.Amount()}, types.Uint64{Val: c.Pledge}). These values are pre-validated by the ledger before reaching the database layer. However, the Scan() method itself contains no defensive validation. The approach is acceptable but could be more explicit: either add a runtime check for negative values or document that the safety guarantee depends entirely on trustworthy upstream validation.

database/types/types_test.go (1)

82-92: LGTM!

The additional test path for scanning []byte input ensures the Scan() method correctly handles both string and byte slice representations, which is important for database driver compatibility.

database/models/pool.go (1)

44-60: LGTM!

The PoolRegistration model correctly addresses the past review concern. The unique constraint is now only on CertificateID (line 55), while PoolKeyHash uses a non-unique index (line 48), allowing pools to submit multiple registration certificates over time to update their parameters.

database/models/leios_eb.go (1)

21-32: Code constraint is correct; no issues found.

The web search confirms that the Leios protocol guarantees exactly one certificate per (election-ID + endorser-block hash) pair. Leios produces a single certificate for an endorser block once the voting quorum is reached, which directly aligns with the composite unique index in the code. The constraint is necessary and correctly specified.

ledger/delta.go (1)

25-35: Deposit wiring and invalid‑tx behavior look correct now

  • TransactionRecord.Deposits plus addTransactionWithDeposits give a clear way to thread precomputed deposits into apply, and the precedence over on‑the‑fly CalculateCertificateDeposits is explicit.
  • The invalid‑transaction branch now skips deposit calculation and passes nil into SetTransaction, addressing the prior issue of persisting deposits for script‑invalid txs.
  • For valid txs, you guarantee a non‑nil deposits map whenever Certificates() is non‑empty (either via tr.Deposits or CalculateCertificateDeposits), which matches the semantics exercised in the new SetTransaction tests.

No changes requested here.

Also applies to: 37-47, 54-88, 90-101

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

28-126: Good coverage of SetAccount lifecycle (active/inactive + includeInactive)

This test exercises first‑time creation, update/inactivation, exclusion from the default GetAccount path, and retrieval with includeInactive=true, plus verifying the same DB row is reused. The direct models.Account assertions via metadataStore.DB() give strong guarantees that SetAccount’s upsert semantics match expectations.


128-184: Transactional SetAccount behavior is well‑validated

Using an explicit GORM transaction to verify visibility within the txn and absence after rollback is a nice sanity check on SetAccount’s use of the provided txn. No issues spotted.


186-284: DeleteCertificate cascade behavior is exercised thoroughly

Manually wiring up models.Transaction, unified models.Certificate, and models.StakeRegistration, then calling DeleteCertificate and asserting both mapping and specialized row removal (and that repeated deletes are no‑ops) gives good end‑to‑end coverage of the cascade behavior. Looks solid.

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

718-836: Idempotent SetTransaction behavior is exercised correctly

TestTransactionCertificateIdempotency now goes through metadata.SetTransaction with a real CBOR transaction, checking:

  • first inclusion creates certificates,
  • re‑calling with the same point is idempotent, and
  • re‑inclusion at a different point updates the Slot without duplicating rows.

This is exactly the sort of regression guard you want around the new unified certificate persistence.


838-1036: Table‑driven processing tests cover key SetTransaction edge cases

The scenarios in TestCertificateProcessingDataIntegrity (missing deposits erroring, idempotent re‑processing with same/different block, and slot updates) give good coverage of the new “deposits required for certs” contract. The final expectedSlot check keeps behavior tight across future refactors. No changes requested.


1038-1094: Certificate Slot persistence test aligns with SetTransaction semantics

TestCertificateSlotField validates that all models.Certificate rows end up with Slot equal to the block’s slot after SetTransaction, which is the key invariant for later cleanup and querying. Looks correct and complements the idempotency tests.


1141-1398: Specialized certificate model mapping tests are straightforward and useful

The MIR, LeiosEb, and anchor‑bearing certificate tests directly assert field‑for‑field round‑tripping through the GORM models. Given these are relatively new tables, these explicit checks are helpful to catch tag or field regressions. No issues spotted.

database/models/transaction.go (1)

17-19: Collateral return association now correctly distinguished from regular outputs

Using where:is_collateral_return = false for Outputs and where:is_collateral_return = true for CollateralReturn matches the IsCollateralReturn column on models.Utxo and fixes the earlier ambiguity where both associations shared the same FK without a discriminator. Fee/TTL staying as types.Uint64 also keeps DB behavior consistent with other monetary fields.

Also applies to: 25-36

internal/config/config.go (1)

17-23: Ledger catch‑up configuration fields are correctly added and defaulted

The three new duration fields on Config (timeout, poll interval, stall timeout) with their 5m/5s/10m defaults align with the node’s hard‑coded constants and follow the existing YAML/envconfig tagging style. Looks good.

Also applies to: 46-68, 70-92

database/models/resign_committee_cold.go (1)

21-29: ResignCommitteeCold uniqueness and slot typing look consistent

ColdCredential uniqueness plus CertificateID unique index give the expected 1:1 mapping, and switching AddedSlot to types.Uint64 aligns with other models. No issues spotted here.

database/transaction.go (1)

22-62: Transactional handling and error propagation in SetTransaction look solid

The switch to a named error with a single deferred commit/rollback plus early returns on blob/metadata errors makes this path much safer and clearer. The UTXO write loop and metadata call are straightforward and consistent with the dual-store design.

database/models/utxo.go (1)

23-40: UTxO typing and collateral-return flag changes are consistent

Using types.Uint64 for slot/amount plus the IsCollateralReturn flag matches the updated Transaction associations and other models. UtxoLedgerToModel correctly populates the wrapped values, so persistence and queries should remain coherent.

Also applies to: 50-60

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

65-83: Idempotent, deposit-aware SetTransaction is well-structured; double-check assumptions for existing rows

The new flow looks coherent:

  • alreadyExists gates UTxO and certificate work, with re-inclusion detected via blockChanged.
  • Transaction upsert on hash plus the ID reload handles ON CONFLICT correctly.
  • Certificate handling is clean: invalid tx short-circuit, deposit map required only for new tx with certs, and deposit presence enforced only for known deposit-bearing types, using the index-aligned deposits map.
  • updateCertificateSlots correctly propagates the new slot into certificates and all of the specialized tables, including leios_eb.

Two behavioral assumptions are worth keeping in mind:

  1. If a transaction row somehow exists without its certificates (e.g., from legacy or partially failed code before this refactor), the alreadyExists guard will skip certificate processing entirely, and those certs will never be backfilled.
  2. On re-inclusion with different block info, only certificate slots are updated; UTxO added_slot / deleted_slot remain at their original values. If you ever rely on those slots to reflect the final inclusion point after a reorg, you may want a similar update pass for UTxOs keyed by tx_id / spent_at_tx_id.

If those edge cases are acceptable for your migration story and reorg handling, the current implementation is in good shape.

Also applies to: 98-145, 343-376, 377-422, 427-476

ledger/certs.go (1)

23-60: Certificate deposit calculation helper is correct and side-effect free

CalculateCertificateDeposits cleanly delegates to the era’s CertDepositFunc, annotates errors with the certificate index, and always records an entry (even when the deposit is zero), which is exactly what the metadata layer needs to distinguish “zero” from “missing”. No issues here.

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

25-47: GetDrep pointer return + not-found handling look good

Returning (*models.Drep, nil) with (nil, nil) for not-found is idiomatic, and using drep_credential plus errors.Is(..., gorm.ErrRecordNotFound) matches the model and avoids leaking ORM-specific sentinel errors to callers.


49-79: Upsert-by-credential semantics in SetDrep are consistent

Constructing models.Drep with DrepCredential and using OnConflict on drep_credential to update added_slot, anchor_url, anchor_hash, and active gives a clean, idempotent “current DRep state by credential” write-path and matches the GORM tags in models.Drep.

ledger/state.go (2)

103-107: Goroutine lifecycle management via context + WaitGroup is solid

Initializing ctx/cancel in NewLedgerState, wrapping background processing (ledgerProcessBlocks and its reader) with wg.Add/Done, and using Close to cancel() then wg.Wait() provides a clear, leak-free lifecycle for ledger goroutines.

Also applies to: 127-129, 176-180, 209-217


885-903: Certificate deposits precomputation and strict validation behavior look correct

Precomputing deposits only when len(tx.Certificates()) > 0, propagating any error from CalculateCertificateDeposits, then passing the map into delta.addTransactionWithDeposits matches the new unified certificate/deposit model. Tightening ValidateTxFunc handling to return nil, fmt.Errorf("TX validation failure: %w", err) ensures invalid transactions abort block processing instead of being silently indexed.

Also applies to: 905-926, 919-920

database/models/account.go (2)

27-35: Account metadata fields and indexing strategy make sense

Adding AddedSlot, CertificateID, and an indexed Active flag on Account provides enough metadata to track when/why the account last changed while still enforcing a single current row per StakingKey via the unique index.


60-67: Composite unique constraints on certificate models align with the documented strategy

The explanation comment and the uniqueIndex:uniq_*_cert tags on (StakingKey, CertificateID) for Deregistration, StakeDeregistration, and StakeRegistration correctly enforce “one row per certificate per staking key” while allowing multiple distinct certificates per key. Using only plain indexes for other certificate types preserves full historical tracking for those tables.

Also applies to: 68-74, 104-109, 115-121

database/account.go (1)

21-27: GetAccountWithInactive helper cleanly extends behavior without breaking callers

Having GetAccount delegate to GetAccountWithInactive(..., false, ...) preserves existing semantics, while the new helper cleanly wires through includeInactive to metadata.GetAccount and safely manages a read-only transaction when the caller doesn’t provide one.

Also applies to: 29-51

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

56-61: MetadataStore interface updates are correctly implemented in the sqlite backend

All interface changes have matching implementations:

  • GetAccount with includeInactive parameter (account.go:26-29)
  • SetTransaction with deposits map (transaction.go:53-56)
  • GetUtxosByAddress and SetUtxoDeletedAtSlot using ledger/common types (utxo.go:97-100, 231-234)

Tests confirm proper usage of the new signatures across all functions.

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

71-79: Clarify zero-value handling in Updates.

GORM's Updates with a map skips zero values by default. If pkh or drep are nil or empty byte slices, they won't be updated, preserving the existing values in the database.

If the intent is to explicitly clear these fields (set them to nil), consider using Select to force update specific fields or use struct-based updates:

if err := db.Model(account).Select("Pool", "Drep", "AddedSlot", "Active").Updates(models.Account{
    Pool:      pkh,
    Drep:      drep,
    AddedSlot: slot,
    Active:    active,
}).Error; err != nil {
    return fmt.Errorf("failed to update account: %w", err)
}

If preserving existing values when nil is passed is the desired behavior, consider documenting this in a comment.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 93d81a8 to 5196a3f Compare November 20, 2025 01:34
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/account.go (1)

52-82: SetAccount must accept and set CertificateID to track certificate origins.

The refactored SetAccount method omits the CertificateID field despite the Account model having this field with a database index (models.Account line ~30). Other certificate-related models—StakeRegistration, StakeDelegation, VoteDelegation, etc.—correctly set CertificateID during creation (certs.go lines 94, 106, 119, etc.), but Account records updated via SetAccount do not.

Required changes:

  1. Add certificateID uint parameter to SetAccount signature (line 52)
  2. Add "certificate_id": certificateID to the updates map (line 71-76)
  3. Update all SetAccount calls in StoreCertificate (certs.go lines 524-634) to pass the unified Certificate ID

This ensures Account records properly track their certificate origins, consistent with other certificate-related models.

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

40-40: Fix invalid for i := range 3 loops; tests won’t compile as written.

range in Go cannot iterate over an integer literal; all these for i := range 3 loops are compile-time errors. Use a counted loop instead.

Suggested fix:

-	// Insert pool registration record multiple times
-	for i := range 3 {
+	// Insert pool registration record multiple times
+	for i := 0; i < 3; i++ {
@@
-	// Insert pool retirement record multiple times
-	for i := range 3 {
+	// Insert pool retirement record multiple times
+	for i := 0; i < 3; i++ {
@@
-	// Insert stake registration record multiple times with different certificate IDs
-	for i := range 3 {
+	// Insert stake registration record multiple times with different certificate IDs
+	for i := 0; i < 3; i++ {
@@
-	// Insert stake deregistration record multiple times with different certificate IDs
-	for i := range 3 {
+	// Insert stake deregistration record multiple times with different certificate IDs
+	for i := 0; i < 3; i++ {
@@
-	// Insert DRep registration record multiple times (should all succeed now)
-	for i := range 3 {
+	// Insert DRep registration record multiple times (should all succeed now)
+	for i := 0; i < 3; i++ {
@@
-	// Insert DRep deregistration record multiple times (should all succeed)
-	for i := range 3 {
+	// Insert DRep deregistration record multiple times (should all succeed)
+	for i := 0; i < 3; i++ {
@@
-	// Insert DRep update record multiple times (should all succeed)
-	for i := range 3 {
+	// Insert DRep update record multiple times (should all succeed)
+	for i := 0; i < 3; i++ {
@@
-	for i := range 3 {
+	for i := 0; i < 3; i++ {

Make the same substitution anywhere else for i := range 3 appears in this file.

Also applies to: 77-77, 128-128, 175-175, 239-239, 269-269, 294-294, 338-338

🧹 Nitpick comments (13)
database/plugin/metadata/sqlite/certs_test.go (2)

1410-1487: TestDeleteCertificateTypeCoverage currently only asserts “no panic”, not switch exhaustiveness.

Despite the comment, this test will happily pass even if DeleteCertificate returns an “unhandled type” error for a known CertificateType, because it just logs any error. If you actually want coverage that forces the switch to handle all known types, you’ll need to treat unexpected errors as failures (and, ideally, have DeleteCertificate return a specific sentinel error for unknown types that the test can assert on).

For now this is a nice smoke test, but it doesn’t guard against newly added cert types being forgotten in the delete switch.


727-1103: End-to-end SetTransaction tests are valuable; consider de-duplicating shared CBOR/fixture setup.

All three transaction/certificate tests use the same large CBOR blob and very similar decode/setup logic. Functionally this is fine and the assertions around idempotency, re-inclusion, and Slot updates look good; if this file grows further, you may want a small helper to centralize the “decode transaction from CBOR” + deposits map construction to keep future changes to the fixture in one place.

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

22-24: GetUtxosByAddress refactor and type switch look correct and fix prior column mismatch

  • Using lcommon.Address/lcommon.TransactionInput and comparing PaymentKeyHash/StakeKeyHash against a zero Blake2b224 mirrors existing patterns and is consistent with models.Utxo which exposes PaymentKey/StakingKey columns (so payment_key / staking_key in SQL). This resolves the earlier payment_addr vs PaymentKey mismatch from prior reviews. Based on learnings.
  • The early return when both key hashes are zero is a reasonable safeguard given the schema only indexes key credentials; if you ever need to support script‑only addresses here, you’ll need to extend the UTXO model/query rather than loosening this guard.

Also applies to: 96-133

database/models/utxo.go (1)

23-40: UTxO model Uint64 migration and helper look consistent with the rest of the DB layer

  • Moving AddedSlot, DeletedSlot, and Amount to types.Uint64 and constructing them in UtxoLedgerToModel via types.Uint64{Val: ...} matches how other models (e.g., Asset.Amount) use the wrapper type and keeps schema handling uniform. Based on learnings.
  • The new IsCollateralReturn flag is wired into the model and indexed; as long as the transaction‑persistence path sets this when mapping collateral‑return outputs into models.Utxo, the associations on models.Transaction (Outputs vs CollateralReturn) will behave as intended.

Also applies to: 50-79

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

28-126: Account tests accurately cover upsert, active flag, and transactional semantics

  • TestSetAccount validates initial creation, update of pool/drep/slot, deactivation, and that GetAccount respects the includeInactive flag while the underlying DB row and ID remain stable.
  • TestSetAccountWithTransaction correctly asserts that changes are visible within the provided GORM transaction and disappear after rollback, confirming that SetAccount honors the caller‑supplied txn.
    If models.Account.AddedSlot is ever migrated to types.Uint64 like other slot fields, you’ll just need to adjust the assert.Equal checks to compare .Val.

Also applies to: 128-184


186-284: DeleteCertificate test gives good coverage of unified certificate → stake_registration cascade

  • Manually seeding models.Transaction, unified models.Certificate, and models.StakeRegistration, then calling DeleteCertificate and asserting both the certificate and stake registration rows are gone exercises the new unified certificate mapping and cascade behavior well.
  • The additional “delete non‑existent certificate” case ensures the API is idempotent and safe to call in cleanup paths.
    If deposit/slot fields on StakeRegistration are later migrated to types.Uint64, the struct literals here should be updated accordingly, but the overall test structure looks solid.
database/plugin/metadata/sqlite/pool.go (1)

53-90: Pool registration query and amount extraction are aligned with the models

  • Filtering on pool_key_hash = ? with pkh.Bytes() in both transactional and non‑transactional paths lines up with models.PoolRegistration.PoolKeyHash []byte, removing the prior mismatch.
  • Using cert.Pledge.Val / cert.Cost.Val matches the migration of those columns to types.Uint64 and returns plain uint amounts as expected by the ledger/common types.
    The CertType: uint(lcommon.CertificateTypePoolRegistration) assignment assumes PoolRegistrationCertificate.CertType is a uint; if upstream changes that field back to a strong enum, you may need to swap this for the enum value directly.
internal/node/load.go (1)

99-103: Async ledger start plus progress-based catchup loop address prior stall issues

  • Starting the ledger via ls.Start() and then using a ticker to poll ls.Tip() until the ledger slot reaches the immutable tip decouples loading from processing and avoids blocking inside the ledger implementation.
  • The new loop:
    • treats prolonged tip.Point.Slot == 0 as a stall (with zeroSlotSeenAt), and
    • treats lack of slot advancement beyond the last seen slot for longer than stallTimeout as a stall,
      both of which prevent Load from hanging indefinitely if the ledger never makes progress.
  • Logging of progress and completion provides good observability for long syncs.

Also applies to: 189-258

database/utxo.go (1)

24-24: lcommon type alignment and extended UTXO mapping look good

Switching UtxoConsume/UtxosByAddress to use lcommon types and explicitly copying IsCollateralReturn (plus the other core fields) keeps the DB API consistent with the ledger layer and preserves the new collateral-return flag end‑to‑end. The logic in both methods is sound.

If you want a tiny optimization, you could preallocate the result slice in UtxosByAddress:

ret := make([]models.Utxo, 0, len(utxos))

to avoid potential reallocations.

Also applies to: 103-112, 115-140

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

64-513: Unified certificate storage and payload mapping look consistent and complete

The new storeCertificate flow (unified row first, then per‑type row linked via CertificateID, then back‑linking Certificate.CertificateID) is coherent and matches the model definitions:

  • Pool registrations now include margin, metadata URL/hash, owners, and relays, so GetPoolRegistrations and related readers won’t see partial data.
  • MIR and Leios certificates persist their full semantic payloads as JSON blobs (rewards map, voters, aggregated signatures), which restores the previously missing information.
  • Stake/vote/DRep/committee certs all set CertificateID on their specialized models to the unified cert ID, aligning with the delete and slot‑update paths.
  • The deregistration amount is sourced from c.Amount rather than the deposits map, which fixes the earlier “always zero” regression.

Given how large this switch is, extracting small helpers per family (stake, pool, vote, DRep, MIR/Leios) could reduce cognitive load, but that’s purely structural and can be deferred.

ledger/certs.go (1)

23-59: Deposit calculation helper is simple and correct

CalculateCertificateDeposits now computes deposits for each certificate index via currentEra.CertDepositFunc and always records an entry (including zeros), which removes the earlier ambiguity around missing vs zero deposits. Error wrapping with the index is also helpful for debugging.

If you ever expect eras without a CertDepositFunc but with certificates, a defensive nil check before calling it would avoid a panic, but that’s optional given current era semantics.

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

52-425: Idempotent SetTransaction with deposit-aware certificate persistence looks solid

The reworked SetTransaction flow addresses the earlier issues:

  • Idempotency is based on a pre‑upsert hash check (alreadyExists) and an ON CONFLICT(hash) upsert, so the first insert still processes certificates while replays skip or just adjust slots.
  • UTXO ingestion/consumption runs only when alreadyExists == false, preventing double‑spend bookkeeping on replays.
  • The processCertificates section cleanly handles:
    • Invalid txs (no cert persistence),
    • Re‑inclusion with changed block info (updateCertificateSlots then return),
    • New txs with certs (enforce non‑nil deposits, per‑index deposit lookup, and strict checks for the known deposit‑requiring cert types before delegating to storeCertificate).

Control flow via the processCertificates label is a bit dense but logically correct. If you touch this again, you might consider splitting the certificate handling into a small dedicated helper to reduce the size/complexity of SetTransaction.

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

36-50: Background VACUUM and certificate-cleanup lifecycle are correctly synchronized with Close

The MetadataStoreSqlite changes introduce a solid pattern:

  • scheduleDailyVacuum / runVacuum and scheduleCertificateCleanup / runCertificateCleanup all respect d.closed and use timerMutex plus vacuumWG / certificateCleanupWG so no background job runs against a closed store.
  • Close marks closed, stops both timers, waits for in‑flight jobs to finish via the WaitGroups, and then closes the underlying DB handle, which prevents the previous “VACUUM after Close” issue and avoids goroutine leaks.
  • In-memory mode (dataDir == "") is correctly excluded from VACUUM via the guard in runVacuum.

The certificate cleanup is currently a logged no‑op with clear TODO comments, which is a reasonable staging point for adding real cleanup logic later.

Also applies to: 52-126, 128-188, 190-245, 252-278, 280-311

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93d81a8 and 5196a3f.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (44)
  • blockfetch.go (1 hunks)
  • database/account.go (1 hunks)
  • database/certificate.go (1 hunks)
  • database/database_test.go (2 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/blob/aws/database.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/account_test.go (1 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (3 hunks)
  • database/plugin/metadata/sqlite/pool.go (2 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (3 hunks)
  • database/plugin/metadata/store.go (3 hunks)
  • database/transaction.go (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • database/utxo.go (4 hunks)
  • go.mod (4 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (12 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (2)
  • database/models/certs.go
  • database/drep.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • database/types/types_test.go
  • database/models/asset.go
  • database/certificate.go
  • blockfetch.go
  • database/models/certificate_test.go
  • database/models/models.go
  • database/models/auth_committee_hot.go
  • database/models/certificate.go
  • database/models/pool.go
  • internal/config/config_test.go
  • database/models/leios_eb.go
  • database/models/drep.go
  • database/models/genesis_key_delegation.go
  • database/database_test.go
🧰 Additional context used
🧠 Learnings (12)
📚 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/utxo.go
  • database/utxo.go
  • database/plugin/metadata/store.go
  • database/models/transaction.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
  • ledger/delta.go
  • database/transaction.go
  • database/utxo.go
  • database/plugin/metadata/store.go
  • database/plugin/metadata/sqlite/database.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/models/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/utxo.go
  • database/plugin/metadata/store.go
  • ledger/state.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/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/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: 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/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/utxo.go
  • database/plugin/metadata/store.go
  • ledger/state.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/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:

  • database/plugin/metadata/sqlite/certs_test.go
  • database/utxo.go
  • ledger/state.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/utxo.go
  • database/plugin/metadata/store.go
  • ledger/state.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/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: 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/utxo.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/state.go
📚 Learning: 2025-10-22T20:18:45.125Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T20:18:45.125Z
Learning: In tests, Chain.AddBlock must be given a gouroboros ledger.Block (not database/models.Block). Use a lightweight MockBlock (as in chain/chain_test.go) or call models.Block.Decode() when starting from stored rows.

Applied to files:

  • ledger/state.go
🧬 Code graph analysis (21)
database/plugin/metadata/sqlite/utxo.go (1)
database/models/utxo.go (1)
  • Utxo (24-40)
internal/config/config.go (1)
internal/node/load.go (2)
  • LedgerCatchupPollInterval (36-36)
  • LedgerCatchupStallTimeout (38-38)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (79-107)
database/models/certificate.go (2)
  • Certificate (157-165)
  • Certificate (168-170)
ledger/view.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
database/plugin/metadata/sqlite/certs.go (11)
database/models/certificate.go (23)
  • CertificateTypeStakeRegistration (32-34)
  • Certificate (157-165)
  • Certificate (168-170)
  • CertificateType (28-28)
  • CertificateTypeStakeDeregistration (35-37)
  • CertificateTypeStakeDelegation (38-40)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypePoolRetirement (44-46)
  • CertificateTypeRegistration (47-49)
  • CertificateTypeDeregistration (50-52)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteDelegation (56-58)
  • CertificateTypeStakeVoteDelegation (59-61)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • CertificateTypeAuthCommitteeHot (68-70)
  • CertificateTypeResignCommitteeCold (71-73)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeDeregistrationDrep (77-79)
  • CertificateTypeUpdateDrep (80-82)
  • CertificateTypeGenesisKeyDelegation (83-85)
  • CertificateTypeMoveInstantaneousRewards (86-88)
  • CertificateTypeLeiosEb (89-91)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-50)
database/models/account.go (20)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • StakeDelegation (92-98)
  • StakeDelegation (100-102)
  • Registration (80-86)
  • Registration (88-90)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeRegistrationDelegation (127-134)
  • StakeRegistrationDelegation (136-138)
  • VoteDelegation (167-173)
  • VoteDelegation (175-177)
  • StakeVoteDelegation (140-147)
  • StakeVoteDelegation (149-151)
  • VoteRegistrationDelegation (179-186)
  • VoteRegistrationDelegation (188-190)
  • StakeVoteRegistrationDelegation (153-161)
  • StakeVoteRegistrationDelegation (163-165)
database/types/types.go (2)
  • Uint64 (66-68)
  • Rat (25-27)
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/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/move_instantaneous_rewards.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/transaction.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-50)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (10)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeRegistration (47-49)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • Certificate (157-165)
  • Certificate (168-170)
ledger/delta.go (1)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/models/resign_committee_cold.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/transaction.go (3)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/utxo.go (1)
  • UtxoBlobKey (269-277)
database/plugin/metadata/sqlite/drep.go (1)
database/models/drep.go (1)
  • Drep (21-29)
database/plugin/metadata/sqlite/pool.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
database/plugin/metadata/sqlite/certs_test.go (11)
database/plugin/metadata/sqlite/database.go (2)
  • New (53-126)
  • MetadataStoreSqlite (39-50)
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 (66-68)
database/models/account.go (7)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • Account (27-35)
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/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/certificate.go (3)
  • Certificate (157-165)
  • Certificate (168-170)
  • CertificateType (28-28)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/models/utxo.go (1)
  • Utxo (24-40)
database/plugin/metadata/sqlite/account.go (1)
database/models/account.go (1)
  • Account (27-35)
ledger/state.go (2)
database/txn.go (1)
  • Txn (27-34)
database/plugin/log.go (1)
  • Logger (18-27)
database/plugin/metadata/sqlite/account_test.go (5)
database/plugin/metadata/sqlite/database.go (2)
  • New (53-126)
  • MetadataStoreSqlite (39-50)
database/models/account.go (3)
  • Account (27-35)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/certificate.go (2)
  • Certificate (157-165)
  • Certificate (168-170)
database/plugin/metadata/sqlite/database.go (1)
database/plugin/log.go (1)
  • Logger (18-27)
database/account.go (3)
database/models/account.go (1)
  • Account (27-35)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/models/transaction.go (2)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (66-68)
⏰ 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: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (29)
database/plugin/blob/aws/database.go (1)

69-71: LGTM! Minor formatting improvement.

The multi-line formatting of the error construction improves readability without any functional change.

go.mod (1)

14-14: Dependency bumps and new test tooling look consistent; ensure module graph is tidy.

The gouroboros bump and addition of testify + its indirects match the broader ledger/cert/test changes. Please just confirm go.sum is in sync (e.g., via a local go mod tidy) and that there are no unintended version drifts elsewhere in the repo.

Also applies to: 24-24, 97-97, 158-158

database/models/move_instantaneous_rewards.go (1)

21-34: MIR model matches unified certs design and avoids prior uniqueness pitfall.

Single-unique CertificateID plus indexed Source and AddedSlot allow multiple MIR certs per slot/pot while keeping lookup efficient. Struct fields and tags are consistent with other new cert models; nothing blocking here.

database/models/transaction.go (1)

17-19: Outputs/CollateralReturn filters and Uint64 usage look correct.

The where:is_collateral_return = … filters now align with the Utxo.IsCollateralReturn column name, and reverting Fee/TTL to types.Uint64 keeps DB encoding consistent with other monetary fields. This struct layout looks sound.

Also applies to: 25-36

database/types/types.go (2)

36-63: Rat.Scan improvements safely broaden input handling.

Handling nil, string, []byte, and int64 while keeping the zero-value on NULL is reasonable and matches typical SQL driver behavior. No issues spotted.


65-101: Uint64 struct Scan/Value semantics are coherent with existing storage scheme.

Switching Uint64 to a struct with Val plus string-based Value() and tolerant Scan() (nil → 0, string/[]byte/int64 inputs) preserves prior on-disk representation while making usage in models explicit. This looks correct and well-scoped.

database/models/account_test.go (1)

26-96: Test table fields now properly support both HRP and error-message assertions.

Bringing wantErrMsg and expectedHRP into the table and using them selectively for error vs. success cases makes the tests clearer and more extensible; the current cases cover empty/nil keys and multiple valid edge patterns well.

ledger/view.go (1)

92-167: PoolCurrentState mapping aligns with new pledge/cost and cert typing.

Casting CertType from the ledger enum to uint and pulling Pledge/Cost via pool.Pledge.Val/pool.Cost.Val matches the new model types and keeps the ledger-facing certificate consistent. The latest-registration and latest-retirement selection logic remains unchanged and correct.

internal/config/config.go (1)

17-23: New ledger catchup tuning knobs are wired correctly with sane defaults.

The added duration fields (timeout, poll interval, stall timeout) have appropriate YAML/env tags and default values that match the node load constants, so they should be easy to override without surprising behavior.

Also applies to: 46-68, 70-92

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

230-247: SetUtxoDeletedAtSlot signature change is consistent with lcommon migration

Switching the utxoId parameter to lcommon.TransactionInput while still using Id().Bytes() and Index() in the WHERE clause preserves behavior and keeps this API aligned with other lcommon‑based call sites.

database/transaction.go (1)

22-63: SetTransaction lifecycle and deposits wiring look solid

  • The named return + deferred commit/rollback for internally created transactions removes the prior risk of calling Commit() after a Rollback() and centralizes error handling cleanly. Based on learnings.
  • Always iterating tx.Produced() and writing UTXO blobs is appropriate given Babbage Produced() semantics (only collateral return for invalid tx, full outputs for valid ones), and threading deposits straight through to metadata.SetTransaction keeps the persistence API cohesive.
ledger/delta.go (2)

25-46: TransactionRecord deposits field and helper are straightforward

Adding Deposits map[int]uint64 to TransactionRecord and using addTransactionWithDeposits to append {Tx, Index, Deposits} cleanly threads per‑certificate deposit data through the delta without complicating the main apply loop.


49-99: Deposit handling for invalid vs valid transactions now matches ledger rules

  • In the invalid‑transaction branch you now skip any deposit calculation and pass a nil deposits map into SetTransaction, while still consuming UTXOs; this aligns with Cardano semantics where phase‑2 invalid txs don’t apply certificate actions or deposits but do affect collateral. Based on learnings.
  • For valid txs, preferring tr.Deposits when supplied and otherwise calling CalculateCertificateDeposits on tr.Tx.Certificates() gives you flexibility to precompute deposits upstream without changing persistence logic.
database/models/resign_committee_cold.go (1)

17-30: ResignCommitteeCold uniqueness and Uint64 migration are coherent

The model now clearly enforces:

  • a single resignation per ColdCredential via gorm:"unique", and
  • a 1:1 link to the unified certificate row via CertificateID’s unique index,

while migrating AddedSlot to types.Uint64 in line with other certificate‑related models. The in‑code comments match the actual constraints.

internal/node/load.go (1)

34-39: Configurable poll/stall constants and fallbacks improve maintainability

Defining LedgerCatchupPollInterval and LedgerCatchupStallTimeout and then using them as fallbacks when the corresponding config values are zero avoids hard‑coded literals and makes the catch‑up behavior easy to tune without touching code.

Also applies to: 179-187

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

515-653: Account-state updates for certificate effects are well covered

updateAccountState cleanly maps certificate types to SetAccount calls:

  • Stake registration/deregistration and all stake/vote delegation variants correctly toggle Active and wire Pool/Drep as expected.
  • Legacy registration/deregistration types are handled alongside the newer Conway variants.
  • Pool and DRep‑only certificates intentionally no-op, which matches their lack of direct staking key.

This looks aligned with the models in database/models/account.go and the unified certificate storage.


655-808: I need to search more broadly for call sites and understand the relationship with account state management.

Now I need to search the codebase for the broader context around updateAccountState and DeleteCertificate usage.

Now let me search directly in the codebase for updateAccountState and broader DeleteCertificate usage:

Based on my verification through search and web resources, the original search showed that DeleteCertificate is currently only used in test cases (account_test.go and certs_test.go), with no visible production call sites in the repository. The reviewer's architectural concern about account state rollback is conceptually valid—when certificates are deleted, any state changes they caused should theoretically be reversed. However, the practical impact depends entirely on how and when this function is actually used in the codebase.

The cascading delete itself appears to be correctly implemented using cert.ID to match the unified certificate record to its specialized type tables. The concern raised by the reviewer is a valid design consideration rather than a bug in the current code shown.

Review the usage context of DeleteCertificate to clarify whether account state must be managed during certificate deletion

The cascading delete correctly uses the unified certificate ID across all related tables. The architectural consideration about updateAccountState effects is valid but requires clarification on actual usage:

  • Verify whether DeleteCertificate is invoked only during straight replays (where effects should be reapplied) or also for true removal scenarios (where effects should be reversed)
  • If used for removal scenarios, add a comment documenting that callers must handle any Account/DRep state reconciliation
  • If used only during replays, document this constraint and consider whether the comment is premature documentation of a hypothetical concern

Currently visible usage is test-only; ensure this function's integration path is clear before deployment.

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

427-478: Slot propagation across certificate tables is comprehensive

updateCertificateSlots updates the slot column in certificates and then propagates added_slot across all specialized tables via a subquery on transaction_id. The certTables list correctly includes all new certificate tables (including leios_eb) and matches the schema in database/models.

Looks good for keeping certificate timing consistent on transaction re‑inclusion.

ledger/state.go (2)

103-107: Context-based lifecycle and goroutine tracking are well wired

Introducing ctx, cancel, and wg into LedgerState and:

  • Starting ledgerProcessBlocks under Start() with wg.Add/Done,
  • Spawning ledgerReadChain from within ledgerProcessBlocks with its own wg accounting,
  • Respecting ls.ctx.Done() in both loops, and
  • Having Close() call cancel() and wg.Wait(),

gives you a clean, race‑free shutdown story. Moving the close(resultCh) responsibility into ledgerReadChain also removes the earlier “send on closed channel” hazard.

Also applies to: 127-129, 132-182, 542-645


890-925: Certificate deposit calculation and validation integration look correct

Within ledgerProcessBlock:

  • Deposits are computed only when len(tx.Certificates()) > 0 via CalculateCertificateDeposits, and any error there aborts block processing with good context.
  • Validation is then run (when shouldValidate and a ValidateTxFunc exist), and all validation errors are now fatal, addressing the prior over‑broad suppression.
  • The call to delta.addTransactionWithDeposits(tx, i, deposits) passes nil for non‑certificate transactions and the populated map for certificate transactions, which matches the delta’s new API.

This aligns the ledger path with the store’s expectations about the deposits map.

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

18-47: DRep lookup and upsert behavior align with the model schema

  • GetDrep now queries by drep_credential and returns (*models.Drep, nil) on success, nil, nil on not‑found, and a real error otherwise, which is a clean API for callers.
  • SetDrep uses OnConflict(Columns: drep_credential) to upsert added_slot, anchor_url, anchor_hash, and active, and correctly initializes DrepCredential in the model.

This matches the models.Drep definition and should behave well for both initial registration and subsequent updates.

Also applies to: 49-78

database/models/account.go (1)

32-35: Composite unique constraints now match the unified certificate storage design

  • Indexing Account.AddedSlot, Account.CertificateID, and Account.Active aligns with how account state is queried and filtered.
  • Switching Deregistration, StakeDeregistration, and StakeRegistration to composite uniques on (StakingKey, CertificateID) fixes the earlier “one row per staking key” limitation while still preventing duplicate entries for the same certificate mapping.
  • The explanatory comment block documents the strategy and makes the intent clear.

Given storeCertificate now sets these models’ CertificateID fields to the unified certificate ID, the schema and persistence logic are in sync.

Also applies to: 60-67, 68-74, 104-109, 115-121

database/account.go (2)

22-27: LGTM: Clean delegation pattern maintains backward compatibility.

The refactoring to delegate GetAccount to GetAccountWithInactive with includeInactive=false preserves existing behavior while enabling the new functionality through the extended method.


29-51: LGTM: Well-implemented extension for inactive account retrieval.

The new method correctly:

  • Handles transaction lifecycle (nil check and deferred commit)
  • Propagates the includeInactive parameter to the metadata layer
  • Preserves existing error handling patterns including the ErrAccountNotFound mapping
database/plugin/metadata/sqlite/account.go (1)

26-49: LGTM: Correct conditional query logic for inactive accounts.

The implementation correctly:

  • Builds the base query filtering by staking_key
  • Conditionally adds active = true filter only when includeInactive is false
  • Preserves the existing error handling pattern (returns nil for ErrRecordNotFound)
database/plugin/metadata/store.go (4)

56-60: LGTM: Interface extension for inactive account retrieval is well-documented.

The addition of the includeInactive boolean parameter is clearly commented and consistent with the implementations reviewed in the SQLite plugin.


127-133: LGTM: SetTransaction signature change is well-documented.

The updated signature adds critical parameters for unified certificate persistence:

  • uint32 transaction index enables proper block-level ordering
  • map[int]uint64 deposits map with comprehensive inline documentation explaining the key-value semantics and default behavior

The deposits map comment clearly states the alignment requirement with transaction certificate slice order and the default-to-zero behavior for missing keys.


144-144: LGTM: Type change to canonical lcommon.Address improves consistency.

The parameter type change from ledger.Address to lcommon.Address aligns with the canonical types used throughout the ledger packages, as confirmed in the past review verification.


146-146: LGTM: Type change to canonical lcommon.TransactionInput improves consistency.

The parameter type change from ledger.TransactionInput to lcommon.TransactionInput maintains consistency with the type standardization applied across the interface.

@wolf31o2
Copy link
Member Author

Hey @coderabbitai range over int is definitely valid Go. Stop claiming that it is not. It's been in Go since 1.22 and we use it everywhere and use a minimum Go 1.23 (usually 1.24 or higher) everywhere.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 20, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the Go version in go.mod
cat go.mod | grep -E '^go [0-9]'

Length of output: 9


@wolf31o2: You're absolutely right, and I apologize for the incorrect feedback. Range over int is indeed valid Go syntax since Go 1.22, and your repository is using Go 1.24.0 as confirmed in your go.mod. I should not have flagged this as invalid syntax.

Thank you for the clarification. I've noted this to ensure I don't make this mistake again in future reviews of your code.

  (\(\
  (•.•)
  />📝 Noted!

✏️ Learnings added
Learnt from: wolf31o2
URL: https://github.com/blinklabs-io/dingo/pull/998

Timestamp: 2025-11-20T02:07:07.043Z
Learning: In the blinklabs-io/dingo repository, range over int is valid Go syntax. The repository uses Go 1.24.0 (minimum Go 1.23), and range over int has been supported since Go 1.22. This syntax should not be flagged as invalid.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch 2 times, most recently from 1648d9f to 2c4ba00 Compare November 20, 2025 02:13
@wolf31o2
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 20, 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: 1

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

144-146: UTXO methods now use ledger/common types, matching canonical ledger usage.

Changing GetUtxosByAddress to take lcommon.Address and SetUtxoDeletedAtSlot to take lcommon.TransactionInput aligns the interface with the ledger/common package, which is the canonical type family for ledger-facing code in this repo. This resolves the earlier mismatch noted between interface and SQLite implementation; just ensure all implementations and callers consistently import and pass ledger/common types.

Based on learnings.

🧹 Nitpick comments (9)
database/plugin/metadata/sqlite/drep.go (1)

32-44: Document the (nil, nil) return pattern for not-found DReps.

GetDrep returns (nil, nil) when a DRep is not found (lines 34, 41), rather than returning an error. This pattern can be confusing for callers who may interpret (nil, nil) as "success with no result" rather than "not found."

Consider either:

  1. Returning a sentinel error like models.ErrDrepNotFound (similar to models.ErrAccountNotFound elsewhere)
  2. Adding a doc comment explicitly stating that (nil, nil) means "not found"
database/plugin/metadata/sqlite/database.go (1)

191-218: Track or document the no-op certificate cleanup placeholder.

The runCertificateCleanup() method is a TODO placeholder that performs no actual work (lines 202-207 list intended functionality). This is scheduled to run every 6 hours (line 232), but currently only logs a debug message.

Consider:

  1. Opening a tracking issue for implementing the cleanup logic, or
  2. Adding a doc comment stating this is intentionally a no-op until specific cleanup requirements are identified

Do you want me to help draft an issue description or update the documentation?

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

44-132: Consider using per‑test TempDir data dirs for stronger isolation

Several of the earlier tests (TestPoolCertificateDuplicatePrevention, TestStakeCertificateDuplicatePrevention, TestDrepCertificateReRegistration, etc.) construct databases with DataDir: "", while the later transaction/idempotency tests use t.TempDir() for an isolated DB file per test.

For consistency and to avoid any subtle cross‑test interference via a shared default path, consider switching these to use DataDir: t.TempDir() as well. Behavior is currently correct, this would just harden isolation.

Also applies to: 134-242, 244-339

database/database_test.go (1)

21-27: Strengthen the test by exercising the actual UtxosByAddress copy path

The metadata part of the test is solid: inserting a UTXO with IsCollateralReturn = true and asserting it survives GetUtxosByAddress gives good coverage of the query/mapping layer.

For the “database-level copying logic” section, you’re manually reconstructing the models.Utxo instead of going through the real helper that production code uses to copy from metadata results. That means this test could keep passing even if the actual copy path forgets to propagate IsCollateralReturn.

Two small refinements:

  • Prefer calling the real helper (e.g., the UtxosByAddress API that wraps GetUtxosByAddress) rather than re-implementing the copy in the test, so you’re validating the production mapping end-to-end.
  • Since you already copy AddedSlot and DeletedSlot, consider asserting them too to catch regressions on those fields.

Also applies to: 76-176

database/models/certificate.go (1)

25-101: CertificateType enum and Scan/Value implementation are solid

The CertificateType constants mirror the ledger/common types, and the Value/Scan methods correctly handle the usual DB driver variants (ints, uints, byte slices, strings) with overflow and range checks. This should integrate cleanly with GORM and prevent invalid enum values from being loaded.

If you ever add new certificate kinds, consider deriving maxCertificateTypeValue from the highest defined constant (or validating against a small allowed set) to avoid the “1000” magic number drifting out of sync.

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

666-823: Cascading certificate deletion matches the new mapping model

DeleteCertificate correctly:

  • looks up the unified models.Certificate by (transaction_id, cert_index),
  • deletes the corresponding specialized row(s) by certificate_id = cert.ID for each supported CertType, and
  • then deletes the unified certificate row itself,

while treating “not found” as a no-op and returning a specific error for unknown types (useful in tests). For higher robustness, you may eventually want to wrap the specialized delete + unified delete in an explicit DB transaction when txn == nil, but functionally this implementation aligns with the unified certificate schema.

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

18-29: Idempotent transaction + certificate persistence flow looks correct

The reworked SetTransaction:

  • determines alreadyExists by hash before the upsert,
  • uses an ON CONFLICT (hash) upsert to keep block hash/index up to date and then reloads tmpTx.ID when necessary,
  • only performs UTXO and association writes on the first insertion,
  • cleanly short-circuits certificate processing for invalid transactions and txs without certificates,
  • treats reprocessing of the same tx+block as a no-op, and
  • for new cert-bearing txs, requires a deposits map, defaults missing entries to 0, and enforces presence only for deposit-requiring cert types before delegating to storeCertificate.

This matches the new CalculateCertificateDeposits contract and gives you idempotent behavior without reintroducing the earlier “missing deposit vs zero deposit” ambiguity.

If you touch this again, consider replacing the goto processCertificates with a structured branch to make the control flow slightly easier to follow.

Also applies to: 52-145, 214-341, 343-425


427-479: Certificate slot re-inclusion updates are consistent; confirm table naming

updateCertificateSlots correctly updates:

  • the slot column in certificates for the given transaction_id, and
  • each specialized table’s added_slot where certificate_id joins back to those certificates,

including LeiosEb via the "leios_eb" entry. The overall re-inclusion behavior (only when alreadyExists && blockChanged) lines up with the SetTransaction logic.

The only thing worth a quick local check is that each string in certTables matches the actual GORM table name for its model (particularly "leios_eb" vs any pluralization), so that all specialized rows get their added_slot updated as intended.

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

52-84: Clarify whether added_slot should be first-seen or last-updated.

SetAccount uses FirstOrCreate and then always updates added_slot to the current slot, even when the account already exists. If added_slot is intended to mean “creation/first registration slot”, you likely want to set it only when the row is newly created (e.g., check result.RowsAffected or account.ID). If instead it’s meant as “last updated by certificate at this slot”, current behavior is fine, but consider adding a brief comment to document that intent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5196a3f and 2c4ba00.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (44)
  • blockfetch.go (1 hunks)
  • database/account.go (1 hunks)
  • database/certificate.go (1 hunks)
  • database/database_test.go (2 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/blob/aws/database.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/account_test.go (1 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (3 hunks)
  • database/plugin/metadata/sqlite/pool.go (2 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (3 hunks)
  • database/plugin/metadata/store.go (3 hunks)
  • database/transaction.go (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • database/utxo.go (4 hunks)
  • go.mod (4 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (12 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (2)
  • database/drep.go
  • database/models/certs.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 (11)
  • blockfetch.go
  • database/models/certificate_test.go
  • internal/config/config.go
  • database/plugin/blob/aws/database.go
  • database/models/models.go
  • database/models/genesis_key_delegation.go
  • database/models/leios_eb.go
  • database/models/move_instantaneous_rewards.go
  • database/models/account_test.go
  • database/plugin/metadata/sqlite/account_test.go
  • database/models/drep.go
🧰 Additional context used
🧠 Learnings (13)
📚 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/utxo.go
  • database/plugin/metadata/sqlite/utxo.go
  • ledger/delta.go
  • database/models/transaction.go
  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
  • database/transaction.go
  • database/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/models/utxo.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/transaction.go
  • database/plugin/metadata/store.go
  • database/utxo.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/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/database_test.go
  • database/models/transaction.go
  • ledger/state.go
  • database/plugin/metadata/store.go
  • database/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/models/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/database_test.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/plugin/metadata/store.go
  • database/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: 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/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/database_test.go
  • ledger/state.go
  • database/plugin/metadata/store.go
  • database/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/utxo.go
  • database/models/transaction.go
  • database/utxo.go
📚 Learning: 2025-11-20T02:07:07.043Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 0
File: :0-0
Timestamp: 2025-11-20T02:07:07.043Z
Learning: In the blinklabs-io/dingo repository, range over int is valid Go syntax. The repository uses Go 1.24.0 (minimum Go 1.23), and range over int has been supported since Go 1.22. This syntax should not be flagged as invalid.

Applied to files:

  • database/plugin/metadata/sqlite/certs_test.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:

  • database/plugin/metadata/sqlite/certs_test.go
  • ledger/state.go
  • database/utxo.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/database_test.go
  • database/utxo.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:

  • ledger/state.go
  • database/plugin/metadata/store.go
  • database/utxo.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/state.go
📚 Learning: 2025-10-22T20:18:45.125Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T20:18:45.125Z
Learning: In tests, Chain.AddBlock must be given a gouroboros ledger.Block (not database/models.Block). Use a lightweight MockBlock (as in chain/chain_test.go) or call models.Block.Decode() when starting from stored rows.

Applied to files:

  • ledger/state.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/utxo.go
🔇 Additional comments (35)
go.mod (3)

97-97: Transitive dependencies from testify look appropriate.

The go-spew and go-difflib indirect dependencies with specific commit hashes are expected transitive dependencies of testify and are commonly used for enhanced test output and diffing. These versions appear consistent with testify v1.11.1.

Also applies to: 97-97, 158-158, 158-158


24-24: v1.11.1 is stable, current, and free of known security vulnerabilities.

v1.11.1 is the latest stable version of github.com/stretchr/testify (released Aug 27, 2025), and no known security issues or CVEs have been publicly reported for this version. The dependency is safe to include.


14-14: Verified: gouroboros v0.140.0 is an appropriate stable release for this PR.

v0.140.0 (released 18 Nov 2025) includes fixed multiple ledger CBOR/serialization and certificate-related bugs including certificate shapes, certificate deposit validation, and correct utxorpc cert type. These certificate-specific improvements directly support the PR's unified certificate persistence system.

database/types/types.go (2)

36-63: LGTM: Rat.Scan properly handles multiple input types.

The updated Scan method correctly handles nil (preserving zero-value), string, []byte, and int64 inputs with appropriate conversions.


86-88: Verify that negative int64 values cannot reach the Uint64.Scan method; clarify the intent of the application-level guarantee.

The cast from int64 to uint64 at line 87 will silently wrap negative values without runtime validation. The #nosec G115 comment asserts an "application-level guarantee about range," but verification could not locate explicit upstream validation logic or constraints that prevent negative values.

Based on domain context (Cardano slot numbers are inherently non-negative), the assumption is reasonable. However, confirm:

  1. All callers of Uint64.Scan() pass only non-negative int64 values or have upstream validation
  2. Database schema or ORM layer does not allow negative values to reach this code path
  3. The intent is to document and accept this implicit assumption rather than add runtime checks

If the guarantee is implicit to the blockchain domain, document it in a code comment (e.g., "Slot numbers from Cardano are always non-negative"). If runtime safety is preferred, add a check before the cast.

database/models/asset.go (1)

73-73: LGTM: Asset.Amount correctly uses new Uint64 struct literal.

The change from types.Uint64(amount) to types.Uint64{Val: amount} properly aligns with the updated Uint64 type definition.

database/types/types_test.go (1)

34-34: LGTM: Test properly updated for struct-based Uint64 and extended coverage.

The test now uses the struct literal form and adds verification for []byte input, ensuring Scan handles multiple input types correctly.

Also applies to: 82-92

ledger/view.go (1)

104-104: LGTM: Pool certificate construction aligns with updated type signatures.

The changes to use uint(lcommon.CertificateTypePoolRegistration), pool.Pledge.Val, and pool.Cost.Val correctly reflect the updated Uint64 struct shape and certificate type handling.

Also applies to: 111-112

ledger/delta.go (2)

54-74: LGTM: Invalid transactions correctly skip deposit calculation.

Per Cardano ledger rules, invalid (phase-2 validation failure) transactions do not apply certificate effects or deposits. The code now correctly:

  1. Processes consumed UTxOs for collateral
  2. Sets deposits to nil
  3. Passes nil deposits to SetTransaction()
  4. Stops further processing

This addresses the critical issue flagged in previous reviews.


76-100: LGTM: Valid transactions properly calculate and persist deposits.

For valid transactions, deposits are:

  • Reused from tr.Deposits if pre-calculated, or
  • Calculated via CalculateCertificateDeposits() when certificates are present
  • Passed to SetTransaction() before protocol parameter updates

This ensures deposit state is correctly maintained for all certificate types.

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

58-79: LGTM: SetDrep uses unified upsert with OnConflict.

The consolidated upsert approach using OnConflict with DoUpdates on specific fields (added_slot, anchor_url, anchor_hash, active) properly handles both insert and update cases in a single operation.

database/models/account.go (2)

60-67: LGTM: Clear documentation of composite unique constraint strategy.

The comment effectively explains why certain certificate models use composite unique constraints (StakingKey, CertificateID) while others use simple indexes. This design correctly allows:

  • Single deregistration/stake registration per certificate ID (preventing duplicates)
  • Multiple delegations/registrations per staking key across different certificates (preserving history)

69-71: LGTM: Composite unique constraints enable proper certificate lifecycle.

The uniqueIndex on (StakingKey, CertificateID) for Deregistration, StakeDeregistration, and StakeRegistration correctly addresses the previous critical issue where simple unique constraints on StakingKey prevented legitimate re-registrations and re-delegations.

Per past review comments, CertificateID is now properly populated during certificate persistence (addressed in commit 1a0053f).

Also applies to: 105-106, 116-117

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

145-160: LGTM: Proper lifecycle management for background operations.

The wait group pattern correctly ensures:

  1. runVacuum() increments vacuumWG while the store is known to be open (lines 152-154)
  2. Close() stops both timers and waits for in-flight operations to complete (lines 256-270)
  3. Operations check d.closed before starting work (lines 147, 193)

This addresses the previous critical issue where vacuum could run after close.

Also applies to: 260-270

database/models/pool.go (1)

55-60: CertificateID uniqueness tags align with pool re-registration semantics

Using per-table unique indexes on CertificateID while keeping PoolKeyHash/Epoch as non-unique indexes correctly allows multiple registrations/retirements for the same pool while enforcing 1:1 mapping per certificate row. This is consistent with the new duplicate‑prevention/re‑registration tests.

Also applies to: 91-98

database/transaction.go (1)

22-63: SetTransaction transactional control and error propagation look correct

The new signature and deferred rollback/commit logic cleanly separate caller‑managed vs internally created transactions and ensure blob writes and metadata persistence are rolled back together on error. No commit‑after‑rollback path remains, and UTXO persistence + metadata.SetTransaction are handled atomically for the internal case.

database/certificate.go (1)

21-47: Certificate read helpers and documentation are consistent with the new write path

The top‑level comment correctly documents that certificates are persisted via SetTransaction, and the updated GetPoolRegistrations/GetStakeRegistrations behavior (auto‑creating and committing a read transaction only when txn is nil) is a sensible, non‑intrusive convenience for callers.

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

63-84: Loop style using for i := range 3 is fine for this repo

The for i := range 3 loops used across these tests are valid and idiomatic given this project’s minimum Go version (≥1.23, using 1.24 in go.mod). No change needed here; the loops clearly express fixed small iteration counts.

Based on learnings

Also applies to: 100-117, 151-167, 197-212, 261-275, 291-300, 316-324, 360-401

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

96-137: UTXO address/query refactor and input type change look sound

The new GetUtxosByAddress implementation is clearer: it always filters deleted_slot = 0, then conditionally applies payment/staking key predicates based on what the address carries, and short‑circuits when neither key is present. The switch to lcommon.Address/lcommon.TransactionInput matches the rest of the ledger/common usage in this PR without changing observable behavior.

Also applies to: 230-252

database/models/transaction.go (1)

17-19: Outputs/CollateralReturn associations now correctly discriminate collateral-return UTXOs

Using where:is_collateral_return = false/true on the TransactionID→ID FK keeps the existing dual-FK UTXO pattern while ensuring the CollateralReturn relation doesn’t randomly bind to a regular output. The import of database/types for Fee/TTL is also consistent with other monetary/slot-like fields. This addresses the earlier mis-association and casing issues without introducing new ones.

Based on learnings

Also applies to: 26-36

database/models/auth_committee_hot.go (1)

17-27: AuthCommitteeHot schema aligns with unified certificate model

The unique constraint on ColdCredential, the unique index on CertificateID, and the shift of AddedSlot to types.Uint64 all line up with the unified certificate + slot handling used elsewhere in the models. Assuming CertificateID is now populated in the certificate persistence path, this struct definition looks consistent and ready for use; the only thing to keep in mind operationally is that the new uniqueness constraints can cause migrations to fail if any historical duplicates exist.

database/models/resign_committee_cold.go (1)

17-30: ResignCommitteeCold uniqueness and slot typing look consistent

The clarified comment plus gorm:"unique" on ColdCredential and a unique index on CertificateID make the intended 1:1 relationships explicit and enforceable at the DB layer, and moving AddedSlot to types.Uint64 matches the rest of the certificate-related models. Schema-wise this is coherent with the unified certificate design and should be safe as long as existing data doesn’t violate the new uniqueness constraints.

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

61-69: Pool registration fetch and mapping now match stored representations

Switching the query to pool_key_hash = ? with pkh.Bytes() avoids re-hashing the pool key when looking up registrations and should line up with how the key hash is stored on models.PoolRegistration. Likewise, setting CertType via uint(lcommon.CertificateTypePoolRegistration) and sourcing Pledge/Cost from cert.Pledge.Val / cert.Cost.Val correctly reflects the move to types.Uint64 in the models and keeps the ledger-facing certificate struct using plain numeric values.

Also applies to: 82-91

database/models/utxo.go (1)

25-39: Uint64 wrapper migration and collateral-return flag look consistent

The transition of AddedSlot, DeletedSlot, and Amount to types.Uint64 plus the addition of IsCollateralReturn integrates cleanly with UtxoLedgerToModel, and indexing/GORM tags remain coherent with prior UTXO usage. No issues spotted here.

Also applies to: 58-60

internal/node/load.go (1)

34-39: Catch-up timeout logic correctly handles initialization and stalls

The new poll/stall defaults and progress-based wait loop correctly (a) special-case a tip stuck at slot 0 with its own timeout, (b) reset timers on forward progress, and (c) exit cleanly once immutableTip.Slot is reached or a stall is detected. The behavior looks sound and avoids the previous “hang forever at slot 0” issue.

Also applies to: 179-257

database/models/certificate.go (1)

152-175: Unified Certificate model matches the new persistence strategy

The Certificate struct (including CertType and CertificateID) aligns with the unified-plus-specialized mapping used in the sqlite metadata layer, and TableName() / Type() give a clean public surface for querying. The commentary now correctly documents that CertificateID points at the per-type record.

ledger/certs.go (1)

23-59: Centralized certificate deposit computation behaves as intended

CalculateCertificateDeposits cleanly delegates to the era’s CertDepositFunc, returns a per-index map, and preserves zero-valued deposits instead of omitting them, which is important for downstream validation. Error reporting includes the certificate index, which will help diagnose protocol-parameter issues.

ledger/state.go (2)

18-19: Context/WaitGroup orchestration and channel handling look correct

The introduction of ctx, cancel, and wg on LedgerState, plus:

  • starting ledgerProcessBlocks and ledgerReadChain under the shared wg,
  • checking ls.ctx.Done() in both loops, and
  • having ledgerReadChain be the sole closer of resultCh,

eliminates the earlier close‑race on the channel and gives a clean, leak‑free shutdown path via Close(). The concurrency and lifecycle wiring here looks solid.

Also applies to: 80-107, 127-130, 176-180, 209-216, 542-644, 634-717


885-935: Precomputing certificate deposits and restoring strict TX validation are appropriate

For each transaction, deposits are now computed once via CalculateCertificateDeposits when certificates are present, and then passed into delta.addTransactionWithDeposits, keeping deposit logic centralized. Validation has been returned to a fail‑fast behavior (ValidateTxFunc errors now abort block processing instead of being logged and ignored), which matches expected ledger semantics. The interaction between shouldValidate, deposits (nil vs non‑nil), and delta application looks consistent.

database/utxo.go (1)

24-26: Common-types alignment and collateral-return propagation are correct

Updating UtxoConsume/UtxosByAddress to use lcommon.TransactionInput and lcommon.Address, and threading IsCollateralReturn through the UTXO mapping while preserving the existing fields, keeps the DB API consistent with the newer ledger/common types and exposes the collateral-return flag to callers without changing prior behavior.

Also applies to: 102-112, 114-147

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

29-62: Unified certificate storage and account-state updates are comprehensively wired

storeCertificate now:

  • creates a unified models.Certificate row per ledger certificate,
  • writes fully-populated per-type rows (including pool margin/metadata, MIR reward payloads, Leios voter/signature JSON, anchors, etc.), and
  • links both directions (specialized.CertificateID = unified.ID, Certificate.CertificateID = specialized.ID),

and updateAccountState consistently maps all staking-key–driven certificate types onto SetAccount transitions (pool delegation, DRep delegation, activation/deactivation) while correctly no-oping for pool/DRep-only certs.

This resolves the earlier gaps around under-populated models, missing CertificateID fields, and incomplete account updates.

Also applies to: 64-513, 515-664

database/account.go (2)

21-27: GetAccount delegation to GetAccountWithInactive is clean and keeps behavior centralized.

Forwarding through GetAccountWithInactive(stakeKey, false, txn) is a straightforward way to preserve the existing API while adding the new flag; no issues here.


29-51: Confirm callers handle ErrAccountNotFound instead of a nil account.

GetAccountWithInactive now converts a nil result from metadata.GetAccount into models.ErrAccountNotFound. If any existing callers relied on (nil, nil) or checked for gorm.ErrRecordNotFound, they need to be updated to look for ErrAccountNotFound. Please confirm tests cover both the “found” and “not found” cases under the new behavior.

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

25-49: includeInactive query behavior in GetAccount looks correct.

The query builder adds active = true only when includeInactive is false and otherwise returns any matching account by staking key. Returning (nil, nil) on ErrRecordNotFound cleanly delegates “not found” semantics to the outer layer. This is consistent and looks good.

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

56-61: Interface changes for GetAccount and SetTransaction are consistent; verify all implementations/callers are updated.

  • GetAccount now takes an includeInactive flag, matching the new database.Account.GetAccountWithInactive and the SQLite implementation.
  • SetTransaction now has signature (ocommon.Point, lcommon.Transaction, uint32, map[int]uint64, *gorm.DB), centralizing certificate deposit handling via the deposits map.

Please double-check any non-SQLite MetadataStore implementations (if any) and all call sites to ensure they pass includeInactive and the deposits map correctly, and that deposit map indices align with the transaction’s Certificates() slice order (with missing keys effectively treated as zero).

Also applies to: 127-133

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 2c4ba00 to 8f01e40 Compare November 20, 2025 11:27
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/models/utxo.go (1)

50-79: Set IsCollateralReturn on collateral return UTXOs ingested via utxo.go

The helper UtxoLedgerToModel does not set IsCollateralReturn, relying on callers to set it explicitly. While database/plugin/metadata/sqlite/transaction.go:103–104 correctly handles this for collateral returns, database/plugin/metadata/sqlite/utxo.go:216 creates UTXO models without checking or setting the flag. This causes collateral return UTXOs ingested through this path to be marked as regular UTXOs (IsCollateralReturn=false), resulting in incomplete query results.

Either derive IsCollateralReturn from the ledger UTXO type (e.g., via a dedicated interface), or update the caller at utxo.go:216 to check if the UTXO is a collateral return and set the flag accordingly.

🧹 Nitpick comments (8)
database/certificate.go (1)

21-35: Auto-creating read-only transactions for getters looks sane

The new doc comment and the txn == nil branches in both getters make the API easier to use and avoid nil dereferences while keeping explicit transaction control for callers that need it. Ignoring Commit errors is consistent with treating these as read-only metadata lookups; if you ever see flaky reads in this path, consider at least logging commit failures for the auto-created transaction.

Also applies to: 37-47

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

497-749: CertificatePersistenceValidation subtests cover key invariants

The grouped subtests validate:

  • StakeDeregistration composite uniqueness on (staking_key, certificate_id).
  • Manual cleanup behavior for specialized and mapping tables (using certificate_id and mapping IDs).
  • Account Active flag transitions.
  • types.Uint64 persistence/round‑trip on stake registration and deregistration, including within an explicit gorm transaction.

The only minor nit is that the “end‑to‑end conversion through store functions” comment in the last subtest slightly overstates things—you’re still constructing StakeRegistration directly rather than calling the higher‑level store—though the DB‑level conversion check is still useful.

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

28-287: Account and DeleteCertificate tests provide good coverage of new behavior

These tests nicely exercise:

  • SetAccount create/update semantics, including the includeInactive flag.
  • Transaction scoping for SetAccount and GetAccount when using an explicit GORM transaction.
  • DeleteCertificate cascading removal of both the unified certificate row and the linked StakeRegistration row, plus the non-existent delete case.

One possible extension (optional): add a focused test that drives SetAccount via the real certificate flow (non‑zero certificateID) and asserts that account rows are correctly linked to the originating certificate, to lock in the new unified-cert mapping.

database/database_test.go (1)

76-176: Consider exercising Database.UtxosByAddress directly for the copy-path check

The test does a good job validating that:

  • GetUtxosByAddress returns the UTXO with IsCollateralReturn == true, and
  • a field‑by‑field copy preserves that flag.

For the second part, you might tighten the contract by calling the real database.UtxosByAddress helper instead of manually constructing copiedUtxo. That way the test will fail if the production mapping ever forgets to propagate IsCollateralReturn or other fields.

database/models/certificate.go (1)

94-150: CertificateType Value/Scan implementation looks solid; max range is the only minor coupling

Value/Scan correctly satisfy the driver.Valuer/sql.Scanner contracts, handle the common DB-returned types, and guard against overflow and obviously invalid values. The only small coupling is maxCertificateTypeValue = 1000; if new certificate types are added with higher numeric IDs, you’ll need to bump this constant, but that’s an acceptable trade-off for early validation.

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

64-513: Unified certificate storage and account-state updates look correct and complete

The storeCertificate flow now cleanly implements the unified mapping:

  • Unified models.Certificate row created first, with CertType and CertIndex set.
  • Each specialized model is inserted with CertificateID: unifiedCert.ID, and all previously-missed fields (pool margin/metadata/owners/relays, anchors for committee/DRep, MIR payload, Leios payload, etc.) are now populated.
  • After insert, unifiedCert.CertificateID is updated to the specialized row’s ID, giving a symmetric link that matches the models’ uniqueness constraints.
  • updateAccountState only touches staking/DRep-related accounts and correctly wires pool and DRep targets from the certificate, leaving non-account certificates as no-ops.

The Deregistration certificate path now uses c.Amount instead of the deposit map, which fixes the earlier loss of deregistration amounts; clamping negative amounts to zero is defensively safe, though you could choose to treat a negative value as an error if you ever want to surface upstream inconsistencies.

Also applies to: 515-664


666-823: DeleteCertificate now deletes specialized rows by unified certificate ID; consider pool owner/relay cleanup

Deleting specialized records via certificate_id = cert.ID matches how storeCertificate populates per-type CertificateID fields, so cascading deletes now align with the unified mapping table. The exhaustive switch over CertType plus an explicit error for unknown types makes future additions easier to catch in tests.

If you ever decide that PoolRegistrationOwner / PoolRegistrationRelay rows should be physically removed when a pool registration certificate is deleted (rather than kept as historical orphans), you’ll need an extra delete step or DB-level cascade on those associations.

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

343-424: Re‑inclusion only updates certificate slots, not BlockHash

On re‑inclusion (alreadyExists && blockChanged), only certificates.slot and specialized added_slot fields are updated; certificates.block_hash remains pointing at the old block. If any consumers rely on Certificate.BlockHash (rather than joining via TransactionID) to attribute certificates to blocks, this can leave stale block references after a reorg.

If block‑level queries will ever use certificates.block_hash, consider extending updateCertificateSlots to also update the block hash (e.g., accept newBlockHash []byte and update both slot and block_hash in the same helper), or document that block_hash on the mapping is not re‑inclusion aware.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c4ba00 and 8f01e40.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (44)
  • blockfetch.go (1 hunks)
  • database/account.go (1 hunks)
  • database/certificate.go (1 hunks)
  • database/database_test.go (2 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/blob/aws/database.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/account_test.go (1 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (2 hunks)
  • database/plugin/metadata/sqlite/pool.go (2 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (3 hunks)
  • database/plugin/metadata/store.go (3 hunks)
  • database/transaction.go (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • database/utxo.go (4 hunks)
  • go.mod (4 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (12 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (2)
  • database/drep.go
  • database/models/certs.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • internal/config/config_test.go
  • blockfetch.go
  • database/models/certificate_test.go
  • internal/config/config.go
  • ledger/view.go
  • go.mod
  • database/models/leios_eb.go
🧰 Additional context used
🧠 Learnings (13)
📚 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/database_test.go
  • ledger/state.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/utxo.go
  • database/models/transaction.go
  • database/utxo.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/database_test.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/models/transaction.go
  • database/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: 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/database_test.go
  • ledger/state.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/models/utxo.go
  • database/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:

  • database/database_test.go
  • ledger/state.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/utxo.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/database_test.go
  • database/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/transaction.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/utxo.go
  • database/models/genesis_key_delegation.go
  • database/plugin/metadata/sqlite/database.go
  • database/plugin/metadata/sqlite/transaction.go
  • ledger/delta.go
  • database/models/transaction.go
  • database/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/models/utxo.go
  • database/models/transaction.go
  • database/utxo.go
  • 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:

  • ledger/state.go
  • database/utxo.go
  • database/plugin/metadata/store.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/state.go
📚 Learning: 2025-10-22T20:18:45.125Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T20:18:45.125Z
Learning: In tests, Chain.AddBlock must be given a gouroboros ledger.Block (not database/models.Block). Use a lightweight MockBlock (as in chain/chain_test.go) or call models.Block.Decode() when starting from stored rows.

Applied to files:

  • ledger/state.go
📚 Learning: 2025-11-20T02:07:07.043Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 0
File: :0-0
Timestamp: 2025-11-20T02:07:07.043Z
Learning: In the blinklabs-io/dingo repository, range over int is valid Go syntax. The repository uses Go 1.24.0 (minimum Go 1.23), and range over int has been supported since Go 1.22. This syntax should not be flagged as invalid.

Applied to files:

  • database/plugin/metadata/sqlite/certs_test.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/models/transaction.go
  • database/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: 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/utxo.go
🧬 Code graph analysis (25)
database/database_test.go (4)
database/plugin/metadata/sqlite/database.go (1)
  • New (53-126)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/transaction.go (3)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/utxo.go (1)
  • UtxoBlobKey (269-277)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (79-107)
database/models/certificate.go (2)
  • Certificate (157-165)
  • Certificate (168-170)
database/models/move_instantaneous_rewards.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/utxo.go (1)
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)
database/plugin/metadata/sqlite/certs_test.go (11)
database/plugin/metadata/sqlite/database.go (2)
  • New (53-126)
  • MetadataStoreSqlite (39-50)
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 (66-68)
database/models/account.go (7)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • Account (27-35)
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/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/certificate.go (5)
  • Certificate (157-165)
  • Certificate (168-170)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateType (28-28)
  • CertificateTypeLeiosEb (89-91)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/plugin/metadata/sqlite/account_test.go (5)
database/plugin/metadata/sqlite/database.go (2)
  • New (53-126)
  • MetadataStoreSqlite (39-50)
database/models/account.go (3)
  • Account (27-35)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/certificate.go (2)
  • Certificate (157-165)
  • Certificate (168-170)
database/plugin/metadata/sqlite/drep.go (1)
database/models/drep.go (1)
  • Drep (21-29)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/genesis_key_delegation.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/certs.go (11)
database/models/certificate.go (5)
  • CertificateTypeStakeRegistration (32-34)
  • Certificate (157-165)
  • Certificate (168-170)
  • CertificateType (28-28)
  • CertificateTypeLeiosEb (89-91)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-50)
database/models/account.go (20)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • StakeDelegation (92-98)
  • StakeDelegation (100-102)
  • Registration (80-86)
  • Registration (88-90)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeRegistrationDelegation (127-134)
  • StakeRegistrationDelegation (136-138)
  • VoteDelegation (167-173)
  • VoteDelegation (175-177)
  • StakeVoteDelegation (140-147)
  • StakeVoteDelegation (149-151)
  • VoteRegistrationDelegation (179-186)
  • VoteRegistrationDelegation (188-190)
  • StakeVoteRegistrationDelegation (153-161)
  • StakeVoteRegistrationDelegation (163-165)
database/types/types.go (2)
  • Uint64 (66-68)
  • Rat (25-27)
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/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/asset.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/types/types_test.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/pool.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
database/models/auth_committee_hot.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/database.go (1)
database/plugin/log.go (1)
  • Logger (18-27)
database/plugin/metadata/sqlite/transaction.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-50)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-79)
database/models/certificate.go (10)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeRegistration (47-49)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • Certificate (157-165)
  • Certificate (168-170)
database/account.go (1)
database/models/account.go (1)
  • Account (27-35)
ledger/delta.go (1)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/plugin/metadata/sqlite/account.go (1)
database/models/account.go (1)
  • Account (27-35)
database/models/transaction.go (2)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
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). (3)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (49)
database/plugin/blob/aws/database.go (1)

69-71: LGTM!

The multi-line formatting improves readability. No functional change.

database/models/account_test.go (1)

29-30: LGTM!

Field reordering within the test struct improves organization. No functional change.

database/models/transaction.go (1)

17-19: LGTM!

The changes correctly filter outputs and collateral returns using the IsCollateralReturn column with proper snake_case naming in the where: clauses. Field reordering and import formatting are cosmetic improvements.

Also applies to: 26-26, 29-29, 34-35

ledger/delta.go (1)

25-29: LGTM!

The deposits handling is correctly implemented:

  • Invalid transactions pass nil deposits (aligned with Cardano ledger rules)
  • Valid transactions calculate deposits only when certificates are present
  • Clean integration with the unified certificate persistence system

Also applies to: 37-47, 61-100

ledger/state.go (3)

104-106: LGTM - Clean lifecycle management!

The goroutine lifecycle management is properly implemented with context cancellation and waitgroup synchronization. This enables graceful shutdown of background processing.

Also applies to: 127-128, 176-180, 210-217


542-543: LGTM - Correct channel closing pattern!

The channel is now closed only by the producer (ledgerReadChain via defer close(resultCh)), while the consumer checks ctx.Done() for cancellation. This eliminates the "send on closed channel" race condition.

Also applies to: 636-644, 656-661, 697-717


890-925: LGTM - Correct deposit pre-calculation!

Deposits are calculated before validation when certificates are present, and validation errors are properly returned (not swallowed). This aligns with the unified certificate persistence approach.

internal/node/load.go (1)

34-39: LGTM - Robust catchup implementation!

The ticker-based catchup loop with progress tracking and stall detection is well-implemented:

  • Uses defined constants for timeouts
  • Handles slot 0 initialization with timeout (prevents infinite hangs)
  • Tracks progress and detects stalls correctly
  • Clean cancellation handling

All past review concerns have been addressed.

Also applies to: 179-259

database/models/asset.go (1)

73-73: LGTM!

Correctly uses the new types.Uint64 struct syntax, consistent with the type system changes across the codebase.

database/types/types_test.go (1)

34-34: LGTM - Correct type usage and enhanced test coverage!

The struct literal syntax aligns with the new types.Uint64 definition, and the additional []byte scanning test improves coverage.

Also applies to: 82-92

database/models/pool.go (1)

55-55: CertificateID‑scoped unique indexes align with unified certificate mapping

Making CertificateID unique for both PoolRegistration and PoolRetirement gives you a clean 1:1 relation between each unified certificate row and its per-type row, while leaving PoolKeyHash/Epoch as non-unique indexes. That matches Cardano semantics and avoids the earlier constraint issues around re-registrations and multiple retirements.

Also applies to: 91-98

database/models/models.go (1)

29-31: New certificate-related models correctly wired into migrations

Registering GenesisKeyDelegation, LeiosEb, and MoveInstantaneousRewards in MigrateModels ensures their tables and indexes are created alongside existing certificate/pool tables. Ordering here looks compatible with the rest of the model set.

database/types/types.go (2)

36-63: Rat.Scan improvements handle NULL and int64 cleanly

Extending Rat.Scan to tolerate nil and int64 while keeping string/[]byte support makes it more robust against different driver behaviors, and the “keep zero-value rat when column is NULL” behavior is clearly documented. This looks correct and side‑effect free.


65-101: Uint64 struct + Scan/Value implementation looks consistent with intended DB semantics

Wrapping uint64 in a Uint64{Val} struct with explicit Value/Scan:

  • Gives you a single place to encode/decode DB values.
  • Handles NULL deterministically as zero (and documents that choice).
  • Accepts the common DB representations: string, []byte, int64.

Assuming the “NULL means 0” decision matches your domain expectations for slots/amounts, this implementation looks solid.

database/models/move_instantaneous_rewards.go (1)

1-34: MoveInstantaneousRewards model matches unified MIR storage design

The MIR model cleanly captures the certificate payload (JSON reward map, source pot, other-pot amount, added slot) and uses a unique index on CertificateID for 1:1 mapping with the unified certificates table. Using types.Uint64 for AddedSlot keeps it consistent with the new Uint64 wrapper, and dropping any composite uniqueness on Source/OtherPot/AddedSlot avoids the prior collision issue.

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

20-42: Helper setup for CBOR and deposits looks solid

testTransactionCBORHex, decodeTestTransaction, and getTestDeposits give a clean, reusable basis for all the SetTransaction-based tests and correctly guard decode errors via require.NoError. No issues here.


44-132: Pool registration/retirement tests align with updated uniqueness semantics

The pool tests now deliberately allow multiple registrations and retirements for the same pool (different CertificateID / slots) and assert on counts instead of expecting uniqueness failures. This matches the current PoolRegistration / PoolRetirement schema where uniqueness is per-certificate, not per key/epoch, and avoids the previous false positives from reusing structs.


134-242: Stake registration/deregistration uniqueness tests correctly target composite index

These tests now:

  • Insert three rows per staking key with distinct CertificateID values and assert all succeed.
  • Then assert that reusing the same (staking_key, certificate_id) pair fails for both StakeRegistration and Deregistration.

That cleanly exercises the uniq_*_cert composite unique indexes and avoids coupling to primary-key behavior.


244-339: DRep re-registration flows are properly validated

The DRep tests confirm that:

  • Multiple RegistrationDrep, DeregistrationDrep, and UpdateDrep rows can exist for the same DrepCredential.
  • There are no uniqueness constraints blocking legitimate re-registration/update flows.

This directly guards the regression that was previously called out around over‑strict uniqueness on DRep credential fields.


341-423: Genesis key delegation idempotency test mirrors store behavior

The TestGenesisKeyDelegationCertificateIdempotent test accurately reproduces the OnConflict(...).DoNothing insert pattern and verifies:

  • Only the first insert allocates an ID.
  • Subsequent inserts succeed but all lookups resolve to the same row/ID.
  • Exactly one row exists for the (genesis_hash, genesis_delegate_hash, vrf_key_hash) triple.

Good coverage of the intended idempotent semantics.


425-495: AuthCommitteeHot test now validates CertificateID usage and uniqueness

TestAuthCommitteeHotCertificateIdPopulation proves that:

  • CertificateID is populated on creation and round‑trips correctly from the DB.
  • The ColdCredential uniqueness constraint prevents a second auth‑hot row for the same cold credential.

This directly addresses the earlier concern that CertificateID might be dead/unused in this model.


751-859: SetTransaction idempotency and re‑inclusion behavior well covered

TestTransactionCertificateIdempotency now drives everything through metadata.SetTransaction with:

  • An initial inclusion.
  • A second call with the same point (strict idempotency on count and slot).
  • A third call with a different point (re‑inclusion), asserting stable counts and updated Slot for all certificates.

This is exactly the sort of black‑box coverage needed for the unified certificate persistence path.


861-1051: Table‑driven SetTransaction tests exercise error, idempotent, and re‑inclusion paths

TestCertificateProcessingDataIntegrity’s table‑driven cases now:

  • Check the failure path when deposits are omitted (including message text).
  • Verify one‑shot processing when called once.
  • Verify idempotency when called twice with the same point.
  • Verify that a second call with a different point either leaves counts unchanged on failure or also updates slots on success.

The final expectedSlot logic, gated on reprocessingSucceeded, correctly matches the comments and avoids the earlier “acceptable failure” vs assertion mismatch.


1053-1096: Certificate Slot field test is straightforward and correct

TestCertificateSlotField is a clean sanity check that certificates persisted via SetTransaction all carry the block slot from the Point. It complements the more complex idempotency tests without duplicating logic.


1098-1400: Deposit and data‑mapping tests for DRep/MIR/Leios/anchor-bearing certs are appropriate

The remaining tests verify:

  • DeregistrationDrep.DepositAmount round‑trips as types.Uint64.
  • MoveInstantaneousRewards, LeiosEb, ResignCommitteeCold, RegistrationDrep, and UpdateDrep persist and retrieve their JSON/blob and anchor fields exactly.

These are good low‑level guards around the new certificate‑adjacent models’ field mappings.


1402-1525: DeleteCertificate type coverage test effectively guards switch completeness

TestDeleteCertificateTypeCoverage ensures:

  • All known CertificateType constants can be inserted and deleted via DeleteCertificate without error.
  • An unknown type (999) yields a clear, type‑specific error message.

This gives a practical safety net that the DeleteCertificate switch is kept in sync as new certificate types are added. Just remember to extend knownTypes whenever a new CertificateType* constant appears.

database/models/drep.go (1)

17-72: DRep model/tag adjustments now match intended lifecycle semantics

  • Drep.DrepCredential remains uniqueIndex, giving a single authoritative DRep row per credential.
  • RegistrationDrep, DeregistrationDrep, and UpdateDrep now use plain index tags, allowing multiple historical records per credential (as exercised by TestDrepCertificateReRegistration).
  • Deposit fields use types.Uint64 consistently.

This layout matches the Cardano DRep lifecycle and the new certificate persistence tests.

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

21-24: GetUtxosByAddress refactor matches Utxo schema and filters correctly

The refactored GetUtxosByAddress:

  • Always filters on deleted_slot = 0.
  • Uses payment_key / staking_key column names that match models.Utxo’s PaymentKey/StakingKey fields.
  • Distinguishes four cases (both PKH+SKH, PKH only, SKH only, neither) and returns an empty slice when there are no usable keys.

This addresses the prior field‑name mismatch and makes the selection logic much clearer.

Also applies to: 96-137


230-252: SetUtxoDeletedAtSlot signature and query are consistent with model indices

Switching to lcommon.TransactionInput and querying by tx_id/output_idx is consistent with how UTXOs are keyed and how UtxoBlobKey is constructed. The update is scoped to the deleted_slot field only, so it’s safe for concurrent readers relying on other indices.

database/models/auth_committee_hot.go (1)

17-27: AuthCommitteeHot model now fits unified certificate + slot conventions

  • ColdCredential is unique, preventing multiple hot entries per cold credential.
  • CertificateID provides a unique linkage into the unified certificates table.
  • AddedSlot now uses types.Uint64 and is indexed, aligning with other certificate‑adjacent models.
  • TestAuthCommitteeHotCertificateIdPopulation exercises these fields, so the struct is no longer at risk of being “write‑only”.

Looks consistent with the rest of the certificate persistence design.

database/transaction.go (1)

22-63: SetTransaction transaction management and deposit threading look correct

  • When txn is nil, a new Txn is created and a single deferred closure decides between Rollback and Commit based on the named err, eliminating the earlier “commit after rollback” risk.
  • Produced UTXOs are written to the blob store first, with immediate failure propagation.
  • d.metadata.SetTransaction is then invoked with the new deposits map and txn.Metadata(), keeping blob and metadata changes within the same transaction when one is created here.
  • Callers that pass their own *Txn remain responsible for commit/rollback.

This aligns well with the new certificate/deposit semantics and previous review feedback.

database/models/resign_committee_cold.go (1)

21-29: ResignCommitteeCold constraints and types look consistent with unified cert mapping

ColdCredential uniqueness plus a unique index on CertificateID give you both “one resignation per cold credential” and a 1:1 link from the unified certificates table. Using types.Uint64 for AddedSlot is consistent with the rest of the schema and the new Scanner/Valuer-based uint64 wrapper.

database/models/genesis_key_delegation.go (1)

21-34: GenesisKeyDelegation indexing matches intended idempotent inserts

The composite uniqueIndex:uniq_genesis_delegation over genesis hash, delegate hash, and VRF key plus a separate unique index on CertificateID gives you clear idempotency and a 1:1 link from the unified certificates table. AddedSlot using types.Uint64 keeps the schema aligned with other certificate models.

database/models/utxo.go (1)

23-40: Utxo schema updates align with Uint64 wrapper and collateral-return tracking

Switching AddedSlot/DeletedSlot/Amount to types.Uint64 and adding an indexed IsCollateralReturn flag are consistent with the rest of the schema (e.g., assets) and give you explicit support for collateral-return outputs while keeping queryability via indices.

database/models/certificate.go (1)

152-175: Unified Certificate model matches the new persistence design

The Certificate struct (with (TransactionID, CertIndex) unique index and CertType discriminator) lines up with storeCertificate/DeleteCertificate usage and the polymorphic CertificateID pattern. The mapping table should give you efficient lookups without forcing heavy joins, and the comments now accurately describe the linkage.

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

61-91: Pool registration lookups and reconstruction are now consistent with stored data

Using pkh.Bytes() in both transactional and non-transactional branches matches how PoolKeyHash is written via c.Operator.Bytes(), and mapping Pledge/Cost from cert.Pledge.Val/cert.Cost.Val correctly unwraps the model’s types.Uint64 fields back to the ledger types. The explicit uint(lcommon.CertificateTypePoolRegistration) cast also aligns CertType with the field’s concrete type.

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

29-62: Stake registration CertType cast matches the new enum usage

Setting CertType: uint(lcommon.CertificateTypeStakeRegistration) is consistent with the field’s concrete type on lcommon.StakeRegistrationCertificate, while the rest of the mapping (staking key only) preserves the previous behavior.

database/utxo.go (1)

24-26: UTxO APIs now align with ledger/common types and preserve collateral-return flags

Switching UtxoConsume/UtxosByAddress to use lcommon.TransactionInput and lcommon.Address keeps the DB API consistent with the rest of the ledger stack, and copying IsCollateralReturn through into the returned models.Utxo ensures the new flag survives all the way out of the metadata layer. The CBOR loading path is unchanged.

Also applies to: 102-112, 115-147

ledger/certs.go (1)

23-60: Certificate deposit calculation matches the documented behavior

CalculateCertificateDeposits now delegates per-certificate logic to ls.currentEra.CertDepositFunc and records an entry for every certificate index (including zero-value deposits), which aligns with the comment and lets downstream persistence distinguish “no deposit” from “missing entry”. The indexed error wrapping also makes it clear which certificate caused a failure; just ensure CertDepositFunc is defined for all eras you support.

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

52-145: SetTransaction idempotency and deposit handling look sound

The new SetTransaction flow correctly distinguishes first-seen vs replayed transactions using the pre-insert hash count, avoids re‑creating UTXOs when alreadyExists, and gates certificate processing/deposit validation to the “new transaction” case. The early tx.IsValid() / len(certs) == 0 checks and the per‑type requiresDeposit guard give a clear, predictable contract for deposits without reintroducing the earlier zero‑deposit regressions. The ON CONFLICT upsert plus ID reload covers the “update existing row” path cleanly.

No changes requested here.


427-476: Verify LeiosEb table name used in updateCertificateSlots

The certTables slice includes "leios_eb". That needs to match the actual table name for models.LeiosEb (either via TableName() or GORM’s default pluralization). If the table is named "leios_ebs" instead, Leios EB certificates won’t get their added_slot updated on re‑inclusion.

Please double‑check the LeiosEb model’s TableName and align this string if needed.

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

25-77: DRep lookup and upsert behavior are consistent and robust

Returning (*models.Drep, error) with (nil, nil) for not‑found and using errors.Is around gorm.ErrRecordNotFound is a clean API. SetDrep’s ON CONFLICT (drep_credential) upsert correctly keeps AddedSlot, anchor fields, and Active in sync without losing data.

Looks good as implemented.

database/models/account.go (1)

27-121: Composite certificate constraints align with intended lifecycle

Indexing Account.AddedSlot, CertificateID, and Active, and moving Deregistration/Stake(Der|Reg)istration to composite uniques on (StakingKey, CertificateID) match the described strategy: one row per certificate per staking key while allowing multiple historical certs per key. Other certificate tables remaining non‑unique on StakingKey also fits the “event history” role.

Assuming migrations mirror these tags, this looks correct and resolves the earlier “one cert per staking key” constraint problem without losing history.

database/account.go (1)

21-50: GetAccountWithInactive nicely generalizes the lookup API

Delegating GetAccount to GetAccountWithInactive(..., false, ...) keeps the existing behavior while centralizing the include‑inactive logic. Passing includeInactive through to metadata.GetAccount and normalizing “not found” to models.ErrAccountNotFound makes the higher‑level API predictable.

No changes needed here.

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

36-311: Background vacuum/cleanup lifecycle is well‑coordinated

The combination of closed + timerMutex, per‑task WaitGroups, and the guards in runVacuum/runCertificateCleanup ensures:

  • No new timers are scheduled after Close()
  • In‑flight vacuum/cleanup runs complete before the DB handle is closed
  • In‑memory stores skip VACUUM via the dataDir == "" check

The certificate cleanup helper is intentionally a no‑op scaffold right now, but its scheduling and shutdown semantics are solid and ready for real logic when you add it.

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

25-49: GetAccount: includeInactive flag and query construction look correct

The conditional active = true filter matches the new includeInactive semantics, and the nil-txn handling cleanly falls back to d.DB() while still returning nil, nil on not-found. No issues from a correctness standpoint.


51-86: SetAccount: find-or-create plus consolidated updates align with account model

Using FirstOrCreate keyed on StakingKey and then a single Updates call for pool, drep, added_slot, active, and certificate_id is consistent with models.Account and the “last updated by certificate” semantics. Transaction handling via the optional txn parameter is also correct.

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

56-60: Account and transaction API extensions are coherent with the new certificate flow

Adding includeInactive to GetAccount and extending SetTransaction with an explicit transaction index and deposits map gives callers the controls they need for unified certificate persistence while keeping the interface compact. Assuming implementations and call sites are updated (as indicated elsewhere in the PR), this looks solid.

Also applies to: 127-133


143-147: UTXO helper signature changes align with the updated metadata model

The updated GetUtxosByAddress and SetUtxoDeletedAtSlot signatures are consistent with the rest of the PR’s UTXO/model changes and keep the UTXO helpers focused and clear. No additional concerns here.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 8f01e40 to 86dd722 Compare November 20, 2025 21:09
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/models/account.go (1)

27-58: Account.String() HRP is hard-coded to mainnet value; incorrect for testnets.

Cardano testnets use "stake_test" HRP while mainnet uses "stake". The Account.String() method hard-codes HRP to "stake", which will produce incorrect bech32 addresses when the application runs against testnet networks (preview, preprod). The Account struct has no access to network configuration to determine the correct HRP at runtime.

Fix: Either (1) add a network-aware parameter to String() or (2) move bech32 encoding to a service layer that has access to network config. Alternatively, document that String() is intended for mainnet-only and create a separate method for testnet encoding. Tests currently verify only "stake" HRP; add testnet variants if multi-network support is intended.

♻️ Duplicate comments (1)
blockfetch.go (1)

41-42: Clarify recv queue size comment vs literal.

The comment says “2x our block batch size” but the configured value is 512. If 512 is now the intentional size (and #1020 will handle any batch size changes), consider updating the comment to reflect the actual relationship, or vice versa, to avoid future confusion.

🧹 Nitpick comments (7)
database/models/resign_committee_cold.go (1)

28-28: Remove the composite index name from CertificateID.

The CertificateID field uses uniqueIndex:uniq_resign_committee_cold_cert, which suggests a composite unique constraint, but no other field shares this index name. Since both CertificateID and ColdCredential require separate unique constraints (1:1 relationship with certificates table and preventing duplicate resignations, respectively), the named index is misleading.

Apply this diff to use a simple unique constraint:

-	CertificateID  uint         `gorm:"uniqueIndex:uniq_resign_committee_cold_cert"`
+	CertificateID  uint         `gorm:"unique"`

Alternatively, if a composite unique constraint on (ColdCredential, CertificateID) is intended for a different business reason, add the same index name to ColdCredential:

-	ColdCredential []byte `gorm:"unique"` // Unique constraint to prevent duplicate resignations
+	ColdCredential []byte `gorm:"uniqueIndex:uniq_resign_committee_cold_cert"` // Unique constraint to prevent duplicate resignations
database/plugin/metadata/sqlite/certs_test.go (2)

341-423: Genesis key delegation idempotency test mirrors store behavior but could call the real API.

Reusing the exact OnConflict clause from the store to assert “one logical record despite repeated inserts” is accurate, but it does duplicate implementation details. If you later change the conflict target or strategy in the store, this test could silently drift. Consider, in a follow-up, adding an integration-style test that drives the same scenario via the production path (e.g., through whatever function ultimately persists genesis key delegation certificates) so behavior is verified even if the internal clause changes.

If you refactor the store, please double-check both the store and this test still agree on which columns form the conflict target.


751-1051: Idempotency and re-inclusion tests are strong; you might also assert deposit effects.

TestTransactionCertificateIdempotency and TestCertificateProcessingDataIntegrity do a good job checking that SetTransaction is idempotent, doesn’t create duplicate certificate rows, and updates Slot on re-inclusion when the second call succeeds. If you want even more protection around the new deposits plumbing, you could add a small assertion that deposit-bearing specialized rows (e.g., stake registrations) have their DepositAmount populated as expected after SetTransaction, using the same CBOR transaction fixture.

Verifying one or two representative deposit-bearing cert types against getTestDeposits() would catch regressions in how deposits map[int]uint64 is threaded into SetTransaction and the SQLite store.

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

311-370: Optional: avoid rolling back transactions that you explicitly commit in tests

In TestSetAccountWithCertificateLinkage, txn is committed and also deferred for rollback. GORM tolerates this, but it’s slightly misleading when reading the test. Consider either dropping the defer txn.Rollback() in tests that always commit, or wrapping it in a defer that only rolls back on error to better reflect the intended control flow.

ledger/state.go (1)

80-107: Cancel context on fatal errors in ledgerProcessBlocks to avoid leaking ledgerReadChain

The new ctx/cancel/wg wiring plus having ledgerReadChain close resultCh is a solid improvement for normal Start/Close lifecycles. However, when ledgerProcessBlocks returns early due to errors (e.g., epoch rollover failure or ledgerProcessBlock error), ls.ctx is not canceled, so ledgerReadChain keeps running and will eventually block on resultCh <- result with no receiver. Consider calling ls.cancel() (or otherwise canceling the context) before these early returns so the reader goroutine and any other ctx-aware code shut down cleanly on fatal errors.

Also applies to: 127-129, 132-182, 542-563, 636-644, 656-660, 695-717

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

25-47: DRep lookup/upsert behavior is clear; confirm CertificateID usage

The updated GetDrep/SetDrep pair looks good:

  • GetDrep now returns (*models.Drep, error) and uses (nil, nil) for not-found, which is easier for callers to distinguish.
  • SetDrep cleanly upserts on drep_credential, updating added_slot, anchor fields, and active.

Given that models.Drep still has a CertificateID field, please confirm whether it’s intentionally decoupled from this helper now (e.g., managed only via certificate persistence), or whether SetDrep should also track the latest certificate ID; if the former, a brief comment or eventual field removal would help avoid confusion.

Also applies to: 49-77

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

140-143: Consider refactoring to eliminate goto.

The goto at line 142 skips UTXO processing for existing transactions. While this works correctly, consider refactoring to a helper function or early return pattern for improved readability.

For example, extract UTXO processing into a helper:

// If transaction already existed, skip UTXO processing
if !alreadyExists {
    if err := d.processUtxos(&tmpTx, tx, point, txHash, txn); err != nil {
        return err
    }
}

// Continue to certificate processing...
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f01e40 and 86dd722.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (44)
  • blockfetch.go (1 hunks)
  • database/account.go (1 hunks)
  • database/certificate.go (1 hunks)
  • database/database_test.go (2 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (1 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/blob/aws/database.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/account_test.go (1 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (2 hunks)
  • database/plugin/metadata/sqlite/pool.go (2 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (3 hunks)
  • database/transaction.go (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • database/utxo.go (4 hunks)
  • go.mod (4 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (12 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (2)
  • database/models/certs.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 (12)
  • database/models/pool.go
  • database/types/types_test.go
  • database/models/move_instantaneous_rewards.go
  • database/models/leios_eb.go
  • database/models/asset.go
  • database/models/certificate.go
  • database/models/genesis_key_delegation.go
  • database/models/auth_committee_hot.go
  • database/models/certificate_test.go
  • database/models/drep.go
  • database/types/types.go
  • database/models/account_test.go
🧰 Additional context used
🧠 Learnings (14)
📚 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:

  • blockfetch.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/utxo.go
  • database/database_test.go
  • database/models/transaction.go
  • database/models/utxo.go
  • ledger/state.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:

  • blockfetch.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/plugin/metadata/sqlite/transaction.go
  • database/utxo.go
  • database/database_test.go
  • database/models/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/utxo.go
  • database/models/transaction.go
  • database/models/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/transaction.go
  • database/utxo.go
  • database/models/transaction.go
  • ledger/delta.go
  • database/models/utxo.go
  • database/plugin/metadata/sqlite/database.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/sqlite/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/utxo.go
  • database/database_test.go
  • database/models/utxo.go
  • ledger/state.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-11-20T02:07:07.043Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 0
File: :0-0
Timestamp: 2025-11-20T02:07:07.043Z
Learning: In the blinklabs-io/dingo repository, range over int is valid Go syntax. The repository uses Go 1.24.0 (minimum Go 1.23), and range over int has been supported since Go 1.22. This syntax should not be flagged as invalid.

Applied to files:

  • database/plugin/metadata/sqlite/certs_test.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:

  • database/plugin/metadata/sqlite/certs_test.go
  • database/utxo.go
  • database/database_test.go
  • ledger/state.go
📚 Learning: 2025-11-20T19:58:38.844Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 0
File: :0-0
Timestamp: 2025-11-20T19:58:38.844Z
Learning: In Go, test dependencies (such as github.com/stretchr/testify) are correctly marked as direct dependencies in go.mod, not as indirect dependencies, even when they are only used in test files. This is a standard practice in Go's module system.

Applied to files:

  • go.mod
📚 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/utxo.go
  • ledger/state.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: 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/utxo.go
  • database/database_test.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/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/utxo.go
  • database/models/transaction.go
  • database/models/utxo.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/state.go
📚 Learning: 2025-10-22T20:18:45.125Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T20:18:45.125Z
Learning: In tests, Chain.AddBlock must be given a gouroboros ledger.Block (not database/models.Block). Use a lightweight MockBlock (as in chain/chain_test.go) or call models.Block.Decode() when starting from stored rows.

Applied to files:

  • ledger/state.go
🧬 Code graph analysis (23)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (79-107)
database/models/certificate.go (2)
  • Certificate (157-165)
  • Certificate (168-170)
database/plugin/metadata/sqlite/utxo.go (1)
database/models/utxo.go (2)
  • Utxo (24-40)
  • UtxoLedgerToModel (50-81)
database/plugin/metadata/sqlite/certs_test.go (11)
database/plugin/metadata/sqlite/database.go (2)
  • New (53-126)
  • MetadataStoreSqlite (39-50)
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 (66-68)
database/models/account.go (7)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • Account (27-35)
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/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/certificate.go (5)
  • Certificate (157-165)
  • Certificate (168-170)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateType (28-28)
  • CertificateTypeLeiosEb (89-91)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/certs.go (10)
database/models/certificate.go (6)
  • CertificateTypeStakeRegistration (32-34)
  • Certificate (157-165)
  • Certificate (168-170)
  • CertificateType (28-28)
  • CertificateTypeStakeDeregistration (35-37)
  • CertificateTypeLeiosEb (89-91)
database/models/account.go (20)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
  • StakeDeregistration (104-109)
  • StakeDeregistration (111-113)
  • StakeDelegation (92-98)
  • StakeDelegation (100-102)
  • Registration (80-86)
  • Registration (88-90)
  • Deregistration (68-74)
  • Deregistration (76-78)
  • StakeRegistrationDelegation (127-134)
  • StakeRegistrationDelegation (136-138)
  • VoteDelegation (167-173)
  • VoteDelegation (175-177)
  • StakeVoteDelegation (140-147)
  • StakeVoteDelegation (149-151)
  • VoteRegistrationDelegation (179-186)
  • VoteRegistrationDelegation (188-190)
  • StakeVoteRegistrationDelegation (153-161)
  • StakeVoteRegistrationDelegation (163-165)
database/types/types.go (2)
  • Uint64 (66-68)
  • Rat (25-27)
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/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/plugin/metadata/sqlite/pool.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
database/plugin/metadata/sqlite/account_test.go (5)
database/plugin/metadata/sqlite/database.go (2)
  • New (53-126)
  • MetadataStoreSqlite (39-50)
database/models/account.go (3)
  • Account (27-35)
  • StakeRegistration (115-121)
  • StakeRegistration (123-125)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/certificate.go (2)
  • Certificate (157-165)
  • Certificate (168-170)
database/models/resign_committee_cold.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/transaction.go (4)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-81)
database/models/certificate.go (10)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeRegistration (47-49)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • Certificate (157-165)
  • Certificate (168-170)
database/transaction.go (3)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/utxo.go (1)
  • UtxoBlobKey (269-277)
database/database_test.go (3)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
internal/config/config.go (2)
internal/node/load.go (2)
  • LedgerCatchupPollInterval (36-36)
  • LedgerCatchupStallTimeout (38-38)
config.go (1)
  • Config (33-55)
database/plugin/metadata/sqlite/drep.go (1)
database/models/drep.go (1)
  • Drep (21-29)
database/models/transaction.go (2)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (66-68)
ledger/delta.go (1)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (66-68)
ledger/view.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
database/plugin/metadata/sqlite/database.go (1)
database/plugin/log.go (1)
  • Logger (18-27)
ledger/state.go (1)
database/txn.go (1)
  • Txn (27-34)
database/account.go (3)
database/models/account.go (1)
  • Account (27-35)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/account.go (1)
database/models/account.go (1)
  • Account (27-35)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
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). (3)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (43)
database/models/models.go (1)

29-31: LGTM! Correct addition of new certificate models to migration list.

The three new certificate models (GenesisKeyDelegation, LeiosEb, MoveInstantaneousRewards) are properly added to enable database migrations. The placement correctly maintains dependency ordering—all three models reference the Certificate model (line 23) via their CertificateID fields, and they appear after it in the migration list.

database/models/resign_committee_cold.go (4)

17-19: LGTM: Import addition is necessary.

The import of database/types is required for the types.Uint64 type used in the AddedSlot field.


21-22: LGTM: Clear documentation.

The struct comment clearly explains the purpose and the rationale for the unique constraint on ColdCredential.


25-25: LGTM: Unique constraint enforces business logic.

The unique constraint on ColdCredential correctly prevents duplicate resignation certificates for the same cold credential.


29-29: LGTM: Type change aligns with refactoring.

The migration from uint64 to types.Uint64 is consistent with the broader refactoring to use wrapped types. The index on AddedSlot is appropriate for query performance.

go.mod (3)

188-188: Security vulnerability fix confirmed for golang.org/x/text.

Line 188 shows golang.org/x/text v0.31.0, which addresses the known security vulnerabilities (CVE-2021-38561, CVE-2022-32149) that were present in v0.30.0. The upgrade is appropriate and necessary.


24-24: Testify addition correctly marked as direct dependency.

The addition of github.com/stretchr/testify v1.11.1 as a direct requirement (line 24) aligns with Go best practices for test-related dependencies and supports the 19 new certificate test cases mentioned in the PR summary. The transitive dependencies (go-spew and go-difflib at lines 97 and 158) are appropriately marked as indirect.


14-14: I need to search for the specific GitHub repository to find the release information. Let me query for the blinklabs-io/gouroboros project releases.

Let me search more specifically for the v0.140.0 release.

Based on my web search, I cannot locate any public releases for gouroboros v0.139.0 or v0.140.0. The latest available version appears to be v0.129.0 (released July 21, 2025). Without access to release notes or changelog entries for the claimed versions, I cannot verify the assertions about certificate types, ledger protocol updates, or API changes mentioned in your review comment.

Verify the existence and contents of gouroboros v0.140.0 release or confirm the version numbers in go.mod.

The versions v0.139.0 and v0.140.0 do not appear to have public releases on GitHub or pkg.go.dev. Confirm whether these are unreleased development versions, pre-release tags, or if different version numbers should be used. If the upgrade is to an actual released version, manually verify the changelog to confirm alignment with the PR objectives around certificate handling (stake, pool, DRep, vote, committee, genesis, MIR, Leios), parameter type changes (uint64 → ocommon.Point), and ledger protocol updates.

database/plugin/blob/aws/database.go (1)

66-72: No behavior change in New; formatting looks fine.

The tightened validation and multi-line errors.New(...) preserve behavior while improving readability. Nothing to change here.

ledger/view.go (1)

92-113: Pool registration view mapping matches new model types.

Using pool.Pledge.Val/pool.Cost.Val and casting CertificateTypePoolRegistration to uint keeps PoolRegistrationCertificate aligned with the updated types.Uint64/CertificateType modeling. This looks consistent with models.Pool and models.CertificateTypePoolRegistration.

database/models/transaction.go (1)

17-36: Collateral-return associations now correctly filtered.

Using where:is_collateral_return = false/true on Outputs and CollateralReturn matches the IsCollateralReturn boolean column on Utxo and keeps the dual-FK pattern intact. This should fix earlier “no such column” issues and cleanly separates regular outputs from the collateral return row.

If you want extra assurance, you can run a quick migration/SQL dump to confirm the generated column name is indeed is_collateral_return and that preloading Outputs/CollateralReturn emits valid SQL under your GORM/SQLite versions.

database/models/account.go (1)

60-121: Composite unique constraints for stake certs match the documented strategy.

The (StakingKey, CertificateID) composite uniques on Deregistration, StakeDeregistration, and StakeRegistration align with the comment and allow multiple lifecycle events per staking key while preventing duplicate rows for the same certificate ID. This is a good balance between correctness and historical tracking.

If you haven’t already, it’s worth running a quick migration against a test DB and verifying the generated index names/definitions match expectations (uniq_* over both columns).

database/certificate.go (1)

21-47: Helper transactions for certificate reads look good.

Letting GetPoolRegistrations/GetStakeRegistrations auto-create a metadata transaction when txn is nil makes these safe convenience APIs while still allowing callers to pass their own Txn for batching. The new comment clarifying that SetTransaction is the write-path for certificates keeps the surface area coherent.

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

44-242: Certificate uniqueness tests now exercise the right constraints.

The pool, stake registration, and deregistration tests now create a fresh model per iteration and vary CertificateID, so they truly validate the (StakingKey/PoolKeyHash, CertificateID) uniqueness instead of tripping on primary-key reuse. This gives good coverage of the revised composite-constraint design.


1402-1525: DeleteCertificate coverage test gives good safety net for switch completeness.

Enumerating all current CertificateType* constants and verifying DeleteCertificate doesn’t error for them (plus expecting a specific error for an unknown type) is a nice guard against missing cases in the delete switch. Just keep in mind you’ll need to update knownTypes whenever new certificate types are added so this continues to reflect the full set.

database/transaction.go (1)

22-63: SetTransaction’s unified blob/metadata transaction handling looks correct—confirm Transaction mode flag.

The refactored SetTransaction now:

  • Uses a named err plus a single deferred closure to rollback on error or commit on success when it creates the Txn itself.
  • Writes all tx.Produced() UTxOs into the blob store before delegating to metadata.SetTransaction.
  • Shares the same Txn for blob and metadata operations, which is the right shape for atomic persistence.

The only small thing to double‑check is the d.Transaction(false) call: in other code, false is used for read‑style operations. If that parameter controls read/write mode, ensure false is indeed appropriate for write paths like this; otherwise, consider switching to the write-enabled mode.

If you want to be sure, a quick grep of Transaction( call sites and the implementation in txn.go will confirm the intended meaning of the boolean flag.

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

22-23: UTXO address lookup and type alignment look solid

GetUtxosByAddress now cleanly derives payment_key/staking_key predicates from lcommon.Address with a zero-hash guard and always enforces deleted_slot = 0, which matches the models.Utxo schema. AddUtxos and SetUtxoDeletedAtSlot correctly adapt to UtxoLedgerToModel(..., isCollateralReturn bool) and lcommon.TransactionInput without changing behavior.

Also applies to: 96-133, 216-247

database/database_test.go (1)

76-179: Good coverage of IsCollateralReturn through metadata UTXO lookup

The test wires address components and stored key hashes consistently, then verifies that GetUtxosByAddress returns exactly one matching UTXO with IsCollateralReturn and all core fields preserved, plus a second copy step to guard against flag loss during mapping. This should catch regressions both in the SQL predicate and in future copy logic around the new flag and types.Uint64 fields.

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

53-91: Pool registration lookup and value decoding are consistent with model types

Using pkh.Bytes() in the pool_key_hash WHERE clause matches the stored []byte key, and unwrapping cert.Pledge.Val / cert.Cost.Val correctly bridges from types.Uint64 in the model to the ledger certificate struct. The explicit CertType value also keeps the mapping unambiguous.

internal/config/config.go (1)

17-23: Ledger catchup timing config is wired cleanly

Exposing LedgerCatchupTimeout, LedgerCatchupPollInterval, and LedgerCatchupStallTimeout as time.Duration fields with sensible defaults in globalConfig aligns with how Load in internal/node/load.go reads them and lets YAML/env overrides tune behavior without changing call sites.

Also applies to: 46-68, 70-92

internal/node/load.go (1)

34-39: Catchup loop and stall detection are robust and configurable

Starting ledger asynchronously via ls.Start() and then using a ticker-driven loop with configurable LedgerCatchupPollInterval/LedgerCatchupStallTimeout gives a clear, bounded wait for ls.Tip() to reach immutableTip.Slot. The explicit handling of tip.Point.Slot == 0 with zeroSlotSeenAt avoids the previous “stuck at slot 0” hang, and the lastSlot/lastProgressTime tracking cleanly detects lack of progress before failing.

Also applies to: 86-102, 179-257

ledger/delta.go (1)

25-29: Deposit handling now matches ledger semantics and supports precomputed maps

Extending TransactionRecord with Deposits and introducing addTransactionWithDeposits lets callers supply precomputed certificate deposits while still falling back to CalculateCertificateDeposits when needed. In apply, invalid transactions no longer compute or persist deposits and record with nil deposits, while valid ones correctly attach deposits only when certificates are present. This aligns with Cardano’s “no deposits for invalid txs” rule and keeps SetTransaction’s API consistent.

Also applies to: 37-46, 49-89, 90-99

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

28-128: Comprehensive coverage for account lifecycle and certificate linkage

These tests exercise the key behaviors of the updated metadata layer: account creation/update and inactive handling, transactional visibility and rollback, certificate deletion cascading into stake_registration, and linking accounts to originating certificates. The direct assertions against the underlying sqlite store and models provide good confidence that the new APIs and schema wiring behave as intended.

Also applies to: 130-187, 189-287, 289-402

database/models/utxo.go (1)

23-40: UTxO model updates for types.Uint64 and collateral-return flag are consistent

Switching AddedSlot, DeletedSlot, and Amount to types.Uint64 and wrapping assignments in UtxoLedgerToModel with types.Uint64{Val: ...} matches the established pattern for typed amounts and slots. The new IsCollateralReturn field is correctly threaded via the isCollateralReturn parameter and is exercised by the new tests that query by address and assert the flag survives mapping. Based on learnings

Also applies to: 50-63

database/utxo.go (1)

24-24: UTXO API now consistently uses common-layer types and preserves collateral-return flag

Switching UtxoConsume/UtxosByAddress to lcommon.TransactionInput/lcommon.Address and wiring through IsCollateralReturn into the returned models.Utxo slice is consistent with the rest of the PR and keeps the existing CBOR loading behavior intact. No issues spotted here.

Also applies to: 102-112, 114-140

ledger/certs.go (1)

23-25: Certificate-deposit calculation looks correct and preserves zero entries

CalculateCertificateDeposits uses the era’s CertDepositFunc with ls.currentPParams, records an entry for every certificate index (including legitimate zero deposits), and returns a clear, indexed error on failure. This aligns with the documented Conway-era deposit behavior and fixes the prior “missing zero deposit” issue.

Also applies to: 37-60

ledger/state.go (1)

885-904: Precomputing certificate deposits and threading them into delta looks correct

Computing deposits once per transaction via CalculateCertificateDeposits and passing them into delta.addTransactionWithDeposits (with nil for non-certificate TXs) cleanly separates deposit calculation from validation and persistence. Erroring out on deposit-calculation failures is appropriate, and the behavior for empty/zero-deposit maps is well defined.

Also applies to: 905-926

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

64-513: Unified certificate persistence and account-state updates look consistent

storeCertificate now follows a clean pattern: create the unified certificates row, insert the type-specific model with CertificateID: unifiedCert.ID, then backfill Certificate.CertificateID with the specific model’s primary key and invoke updateAccountState using the unified ID. Per-type mappings (including pool margin/metadata, MIR data, Leios EB payloads, and delegation/DRep variants) are populated with the expected fields, and the SetAccount calls in updateAccountState mirror the staking/voting semantics. This resolves the earlier uniqueness and under-population issues and looks solid overall.


666-823: I need to check how migrations are defined and whether CASCADE constraints are configured. Let me search for the migration/schema setup:

Now let me search for the PoolRegistration struct definition to see if CASCADE constraints are defined:

Let me now search the codebase directly for the PoolRegistration struct definition and GORM constraint tags:

Based on my analysis of the web search results and the code output, I can now assess the review comment. Let me search for the actual PoolRegistration model definition in the codebase with full tag information:

Based on my thorough verification using web search, I can now provide a comprehensive assessment. The review comment raises a legitimate concern about cascading deletes for related records. GORM AutoMigrate creates database foreign key constraints automatically and creates missing foreign keys, constraints, columns and indexes. However, every new connection to a sqlite database needs to explicitly turn on foreign key enforcement for that connection. Additionally, you can setup OnUpdate, OnDelete constraints with tag constraint, and it will be created when migrating with GORM.

The review comment is accurate in identifying that:

  1. The DeleteCertificate function deletes PoolRegistration records via certificate_id
  2. Related tables like PoolRegistrationOwner, PoolRegistrationRelay, and potentially MoveInstantaneousRewards payloads depend on these records
  3. Without explicit ON DELETE CASCADE constraints at the database level (or manual deletes), orphaned records may remain

Verify whether CASCADE constraints are defined in PoolRegistration and related model relationships

Check the full PoolRegistration model definition for constraint tags on the Owners and Relays relationship fields. If neither constraint:OnDelete:CASCADE tags are present in the model nor explicit deletes are performed in DeleteCertificate, this represents a data integrity risk.

Additionally, confirm that PRAGMA foreign_keys is enabled at database connection initialization to ensure that any CASCADE constraints actually function in SQLite (which disables foreign key enforcement by default).

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

18-18: LGTM: Import and signature changes support new functionality.

The bytes import enables the re-inclusion check, and the updated SetTransaction signature properly adds the deposits map parameter with clear documentation.

Also applies to: 53-59


65-83: LGTM: Idempotency check correctly positioned.

The existence check now runs before the upsert, allowing certificate processing to proceed on first insert. The re-inclusion detection properly handles transactions moving between blocks.


114-137: LGTM: Transaction ID properly hydrated after upsert.

The ID reload at lines 132-137 ensures tmpTx.ID is populated even when the ON CONFLICT path is taken, addressing the critical issue identified in past reviews.


426-483: LGTM: Certificate slot updates properly handle re-inclusion.

The updateCertificateSlots helper correctly updates both the unified certificates table and all specialized certificate tables, including leios_eb which was previously missing.


377-421: Now let me search for more specific information about Conway era certificate types and deposits:

Now let me verify the exact struct names in the account model to map them to certificate types:

Based on my verification using web searches and protocol documentation, the deposit-requiring certificate types listed in the code are complete and accurate. Registration certificates require deposits matching ppKeyDepositL in protocol parameters. The combined registration+delegation certificates (StakeRegistrationDelegation, VoteRegistrationDelegation, StakeVoteRegistrationDelegation) all require deposits. DRep registration requires a 500 ada refundable deposit. Pool registration also requires deposits per the protocol. Notably, delegation-only certificates and deregistration certificates do not require deposits (deregistrations return deposits instead).

The list of deposit-requiring certificate types is complete and correctly validated.

database/account.go (1)

22-51: LGTM: Clean delegation pattern for inactive account support.

The GetAccount method now delegates to GetAccountWithInactive with includeInactive=false, maintaining backward compatibility while adding the flexibility to include inactive accounts when needed. The error handling correctly returns ErrAccountNotFound when the account is nil.

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

36-50: LGTM: Struct fields properly support lifecycle management.

The new fields (timerCertificateCleanup, vacuumWG, certificateCleanupWG, dataDir, etc.) enable proper tracking and coordination of background operations and clean shutdown.


190-246: LGTM: Certificate cleanup infrastructure in place with clear placeholder.

The runCertificateCleanup and scheduleCertificateCleanup functions establish the scheduling infrastructure with clear TODO comments for future implementation. The 6-hour cleanup interval is reasonable.


253-279: LGTM: Close properly orchestrates shutdown.

The Close method now correctly stops both timers and waits for in-flight vacuum and certificate cleanup operations to complete before closing the database handle. This addresses the critical issue from past reviews about background goroutine leaks.

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

26-49: LGTM: GetAccount properly supports inactive account filtering.

The conditional query construction at lines 36-41 correctly filters by active = true only when includeInactive is false, providing the flexibility needed for different query scenarios.


52-86: LGTM: SetAccount refactored to cleaner pattern.

The rewrite uses FirstOrCreate followed by Updates, which is more straightforward than the previous OnConflict approach. The certificate_id field is properly included in the updates map, and the comment at lines 72-73 helpfully clarifies the added_slot semantics.

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

56-60: LGTM: GetAccount interface extended for inactive account support.

The includeInactive bool parameter properly extends the interface to support retrieving inactive accounts when needed.


127-133: LGTM: SetTransaction interface updated with deposits parameter.

The updated signature adds the deposits map parameter with clear documentation explaining that it maps certificate index to deposit amount. The parameter order change (point first) improves consistency with other methods.


144-146: LGTM: UTXO method signatures use canonical ledger types.

The type changes from ledger.* to lcommon.* align with the repository's convention of using ledger/common as the canonical type package, consistent with learnings from previous reviews.

@wolf31o2 wolf31o2 force-pushed the feat/certificate-persistence-unified branch from 86dd722 to d748f01 Compare November 21, 2025 03:51
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/models/drep.go (1)

17-68: Add column:credential tag to all DRep DrepCredential fields or create schema migration

The field rename from CredentialDrepCredential without a column: tag will cause GORM's AutoMigrate to create a new drep_credential column while leaving the old credential column untouched. Existing data will become inaccessible.

All four affected models in database/models/drep.go are registered in MigrateModels and will be auto-migrated:

  • Drep (line 22)
  • DeregistrationDrep (line 36)
  • RegistrationDrep (line 48)
  • UpdateDrep (line 62)

Add gorm:"column:credential;..." to pin the column name, or create a proper database migration to rename the column and update indexes.

🧹 Nitpick comments (12)
internal/config/config.go (1)

22-92: Ledger catchup duration config looks good; watch for duplicated defaults

The new time.Duration fields with YAML/envconfig tags are appropriate, and the defaults (5m/5s/10m) look sane and consistent with internal/node/load.go. One minor maintenance nit: these literal defaults now exist in both this file and internal/node/load.go; if they ever change, you’ll need to remember to update both. If possible, consider centralizing the defaults (e.g., constants in one place) to avoid drift.

database/types/types_test.go (1)

33-92: Nice added coverage for []byte Scan; consider strengthening assertions

The extra Scan([]byte(...)) checks are useful and align with the updated Uint64 struct. Note that tmpScanner and testDef.origValue are the same pointer, so reflect.DeepEqual(tmpScanner, testDef.origValue) only guarantees pointer identity, not that the underlying numeric value is as expected. If you want stricter tests (optional), you could type‑switch on testDef.origValue (e.g., *types.Uint64, *types.Rat) and assert on the underlying fields after the []byte Scan.

database/models/move_instantaneous_rewards.go (1)

21-29: MoveInstantaneousRewards model fits unified certificate schema

The model wiring (unique CertificateID, indexed Source/AddedSlot, free-form RewardData) aligns with the rest of the certificate tables and the unified mapping design. If you later want uniform handling of all value-like amounts, you could consider switching OtherPot to types.Uint64 for consistency with deposits, but it’s not required for correctness right now.

database/certificate.go (1)

21-47: Auto-transaction behavior for registration queries is reasonable

Documenting that certificates are persisted via SetTransaction and having GetPoolRegistrations / GetStakeRegistrations lazily open a read-only Txn when txn == nil keeps call sites simple and still allows callers to supply an existing transaction when needed. Ignoring the commit error on a read-only transaction is acceptable here given the usage.

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

425-749: AuthCommitteeHot and certificate validation tests give good coverage of edge cases

Highlights:

  • TestAuthCommitteeHotCertificateIdPopulation validates both initial CertificateID population and the uniqueness of ColdCredential.
  • TestCertificatePersistenceValidation covers:
    • StakeDeregistration uniqueness on (staking_key, certificate_id).
    • Manual cleanup behavior mirroring bulkRemoveCertificateRecords and cert deletion.
    • Account activation/deactivation state changes.
    • Round‑trip behavior of types.Uint64 on both registration deposits and deregistration amounts, including transactional usage.

These tests collectively exercise the key invariants around uniqueness, cleanup, and type wrappers without re‑implementing too much store logic.


1053-1669: Slot, mapping, DeleteCertificate coverage, and DRep SetDrep tests are comprehensive

This tail section of the file:

  • Confirms Certificate.Slot is correctly persisted via SetTransaction.
  • Verifies DeregistrationDrep.DepositAmount and types.Uint64 behave as expected.
  • Checks specialized data mapping for MIR, LeiosEb, and all anchor‑bearing certs (ResignCommitteeCold, RegistrationDrep, UpdateDrep).
  • Enforces that every known CertificateType is accepted by DeleteCertificate, and that unknown types return a clear “unknown certificate type …” error.
  • Validates SetDrep both on initial creation and on update (ensuring the single Drep row is updated rather than duplicated, with slot, anchor, active flag, and CertificateID updated correctly).

All of this is well targeted at the new unified certificate model and should catch most regressions in the certificate‑related metadata layer.

database/database_test.go (1)

76-179: Consider driving this through Database.UtxosByAddress instead of reimplementing the copy

The test correctly:

  • Inserts a UTXO with IsCollateralReturn = true.
  • Asserts that GetUtxosByAddress returns it with the flag and key/amount fields intact.
  • Manually copies fields into a new models.Utxo and verifies the flag survives the copy.

Functionally this all works, but the manual copy at the bottom is duplicating what Database.UtxosByAddress does today. To guard against future drift, you might prefer to call db.UtxosByAddress(testAddr) directly and assert on that result instead of recreating the mapping inline.

database/models/certificate.go (1)

25-150: CertificateType scanning/range guard is solid; consider making the max value more obviously future‑proof

The CertificateType wrapper and Scan implementation look robust: you handle all common driver return types (int64, uint64, []byte, string), check for overflow before casting, and enforce a small integer range. Functionally this is in good shape.

Two minor maintainability thoughts:

  • maxCertificateTypeValue = 1000 is a reasonable safety net, but it’s not clearly tied to the actual maximum in lcommon. You might want to either:

    • Add a short comment explicitly stating “current highest value in lcommon is X; bump this if new types are added”, or
    • Move the limit next to the constant definitions and base it on the largest known lcommon.CertificateType* value so it’s harder to forget during future upgrades.
  • Scan(nil) sets *ct = 0. That’s fine if 0 is either a valid “unknown/zero” type or an unused sentinel. If 0 is not representable from the ledger, it might be safer to treat nil as an error or a clearly documented sentinel so callers don’t accidentally treat it as a real certificate type.

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

190-246: Certificate cleanup scheduler is a good hook; consider early guards when you add real logic

The runCertificateCleanup / scheduleCertificateCleanup pair mirrors the vacuum pattern nicely (timer + certificateCleanupWG + closed guard), and is safe as a no‑op placeholder.

When you eventually implement the TODOs here, it would be worth:

  • Mirroring runVacuum’s dataDir == "" check if the cleanup will touch on‑disk state only (to avoid unnecessary work for purely in‑memory/testing setups).
  • Ensuring any heavier cleanup work runs in short‑lived transactions or with appropriate context timeouts so these background goroutines can’t get stuck indefinitely on DB I/O.

Nothing to change now; just considerations for when you flesh out the cleanup logic.

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

25-47: GetDrep behavior and not‑found semantics look good; minor duplication you could trim

GetDrep’s contract of returning (nil, nil) when a DRep isn’t found is clear and matches the comment. The use of "drep_credential = ?" aligns with the unique index on DrepCredential.

You have a small amount of duplication between the txn != nil and txn == nil branches. If you want to tighten this up later, you could follow the SetDrep pattern:

db := txn
if db == nil {
	db = d.DB()
}
result := db.First(ret, "drep_credential = ?", cred)

and keep the error handling once.


49-85: SetDrep upsert correctly captures anchor + certificate linkage; check slot ordering assumptions

The SetDrep upsert via OnConflict{Columns: drep_credential} looks appropriate:

  • New records get DrepCredential, AddedSlot, AnchorUrl, AnchorHash, Active, and CertificateID populated in line with models.Drep.
  • Existing records are updated with the latest values on conflict, keeping the DRep row in sync with the most recent certificate affecting it.

One thing to double‑check at the call sites: this assumes calls arrive in ledger order. If SetDrep can be invoked out of order in terms of slot (e.g., during backfills or recovery), you may want to add a guard in the DoUpdates clause (or at the caller) so an older certificate cannot overwrite a newer AddedSlot and associated fields.

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

51-86: SetAccount upsert behavior looks correct; relies on callers to pass the desired final state

The new SetAccount implementation:

  • Uses FirstOrCreate keyed on StakingKey, ensuring a single account row per staking key.
  • Then updates pool, drep, added_slot, active, and certificate_id in one Updates call, which matches the fields on models.Account.

This is a clean consolidation of account updates. Just keep in mind that it always overwrites all those fields with the values provided; if some certificate types should leave pool or DRep unchanged, the corresponding callers need to pass through the current values rather than zero them out.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86dd722 and d748f01.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (46)
  • blockfetch.go (1 hunks)
  • database/account.go (1 hunks)
  • database/certificate.go (1 hunks)
  • database/database_test.go (2 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (4 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/blob/aws/database.go (1 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/account_test.go (1 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (2 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (2 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (3 hunks)
  • database/transaction.go (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • database/utxo.go (4 hunks)
  • go.mod (4 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/certs_test.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (14 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (3)
  • database/plugin/metadata/sqlite/plugin.go
  • database/models/certs.go
  • database/drep.go
✅ Files skipped from review due to trivial changes (1)
  • database/plugin/blob/aws/database.go
🚧 Files skipped from review as they are similar to previous changes (10)
  • database/models/genesis_key_delegation.go
  • database/models/pool.go
  • database/models/asset.go
  • internal/config/config_test.go
  • go.mod
  • ledger/view.go
  • blockfetch.go
  • database/models/auth_committee_hot.go
  • database/models/leios_eb.go
  • database/models/certificate_test.go
🧰 Additional context used
🧠 Learnings (13)
📚 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/database_test.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • ledger/certs_test.go
  • ledger/state.go
  • database/models/utxo.go
  • database/utxo.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/database_test.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • ledger/certs_test.go
  • database/models/utxo.go
  • database/utxo.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: 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/database_test.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • ledger/certs_test.go
  • ledger/state.go
  • database/models/utxo.go
  • database/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: 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/database_test.go
  • database/utxo.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:

  • database/database_test.go
  • database/plugin/metadata/sqlite/certs_test.go
  • ledger/certs_test.go
  • ledger/state.go
  • database/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/database_test.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/transaction.go
  • database/models/utxo.go
  • database/utxo.go
  • database/transaction.go
  • database/plugin/metadata/sqlite/transaction.go
  • ledger/delta.go
  • database/plugin/metadata/sqlite/database.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/models/utxo.go
  • database/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-11-20T02:07:07.043Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 0
File: :0-0
Timestamp: 2025-11-20T02:07:07.043Z
Learning: In the blinklabs-io/dingo repository, range over int is valid Go syntax. The repository uses Go 1.24.0 (minimum Go 1.23), and range over int has been supported since Go 1.22. This syntax should not be flagged as invalid.

Applied to files:

  • database/plugin/metadata/sqlite/certs_test.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/utxo.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
  • ledger/state.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:

  • ledger/state.go
  • database/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-10-22T20:18:45.125Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T20:18:45.125Z
Learning: In tests, Chain.AddBlock must be given a gouroboros ledger.Block (not database/models.Block). Use a lightweight MockBlock (as in chain/chain_test.go) or call models.Block.Decode() when starting from stored rows.

Applied to files:

  • ledger/state.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/utxo.go
🧬 Code graph analysis (25)
database/database_test.go (3)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types_test.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/utxo.go (1)
database/models/utxo.go (2)
  • Utxo (24-40)
  • UtxoLedgerToModel (50-81)
internal/config/config.go (1)
internal/node/load.go (2)
  • LedgerCatchupPollInterval (36-36)
  • LedgerCatchupStallTimeout (38-38)
database/plugin/metadata/sqlite/certs_test.go (11)
database/plugin/metadata/sqlite/database.go (2)
  • New (53-126)
  • MetadataStoreSqlite (39-50)
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 (66-68)
database/models/account.go (7)
  • StakeRegistration (127-133)
  • StakeRegistration (135-137)
  • Deregistration (80-86)
  • Deregistration (88-90)
  • StakeDeregistration (116-121)
  • StakeDeregistration (123-125)
  • Account (27-35)
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/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/certificate.go (23)
  • Certificate (157-165)
  • Certificate (168-170)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateType (28-28)
  • CertificateTypeStakeDeregistration (35-37)
  • CertificateTypeStakeDelegation (38-40)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypePoolRetirement (44-46)
  • CertificateTypeRegistration (47-49)
  • CertificateTypeDeregistration (50-52)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteDelegation (56-58)
  • CertificateTypeStakeVoteDelegation (59-61)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • CertificateTypeAuthCommitteeHot (68-70)
  • CertificateTypeResignCommitteeCold (71-73)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeDeregistrationDrep (77-79)
  • CertificateTypeUpdateDrep (80-82)
  • CertificateTypeGenesisKeyDelegation (83-85)
  • CertificateTypeMoveInstantaneousRewards (86-88)
  • CertificateTypeLeiosEb (89-91)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/transaction.go (2)
database/models/utxo.go (1)
  • Utxo (24-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/move_instantaneous_rewards.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
ledger/certs_test.go (2)
ledger/state.go (1)
  • LedgerState (79-107)
ledger/eras/conway.go (1)
  • ConwayEraDesc (33-45)
database/models/models.go (3)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/resign_committee_cold.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (79-107)
database/models/certificate.go (2)
  • Certificate (157-165)
  • Certificate (168-170)
database/plugin/metadata/sqlite/certs.go (10)
database/models/certificate.go (4)
  • CertificateTypeStakeRegistration (32-34)
  • Certificate (157-165)
  • Certificate (168-170)
  • CertificateType (28-28)
database/models/account.go (20)
  • StakeRegistration (127-133)
  • StakeRegistration (135-137)
  • StakeDeregistration (116-121)
  • StakeDeregistration (123-125)
  • StakeDelegation (104-110)
  • StakeDelegation (112-114)
  • Registration (92-98)
  • Registration (100-102)
  • Deregistration (80-86)
  • Deregistration (88-90)
  • StakeRegistrationDelegation (139-146)
  • StakeRegistrationDelegation (148-150)
  • VoteDelegation (179-185)
  • VoteDelegation (187-189)
  • StakeVoteDelegation (152-159)
  • StakeVoteDelegation (161-163)
  • VoteRegistrationDelegation (191-198)
  • VoteRegistrationDelegation (200-202)
  • StakeVoteRegistrationDelegation (165-173)
  • StakeVoteRegistrationDelegation (175-177)
database/types/types.go (2)
  • Uint64 (66-68)
  • Rat (25-27)
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/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
ledger/state.go (3)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/txn.go (1)
  • Txn (27-34)
database/plugin/log.go (1)
  • Logger (18-27)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/account_test.go (1)
database/models/account.go (1)
  • Account (27-35)
database/plugin/metadata/sqlite/account_test.go (5)
database/plugin/metadata/sqlite/database.go (2)
  • New (53-126)
  • MetadataStoreSqlite (39-50)
database/models/account.go (3)
  • Account (27-35)
  • StakeRegistration (127-133)
  • StakeRegistration (135-137)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/certificate.go (2)
  • Certificate (157-165)
  • Certificate (168-170)
database/transaction.go (3)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/utxo.go (1)
  • UtxoBlobKey (269-277)
database/account.go (3)
database/models/account.go (1)
  • Account (27-35)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/drep.go (1)
database/models/drep.go (1)
  • Drep (21-29)
database/plugin/metadata/sqlite/pool.go (1)
database/models/certificate.go (1)
  • CertificateTypePoolRegistration (41-43)
database/plugin/metadata/sqlite/transaction.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-50)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-81)
database/models/certificate.go (10)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeRegistration (47-49)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • Certificate (157-165)
  • Certificate (168-170)
database/plugin/metadata/sqlite/account.go (1)
database/models/account.go (1)
  • Account (27-35)
ledger/delta.go (1)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
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). (3)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (35)
database/transaction.go (1)

22-62: SetTransaction transactional flow and error propagation look solid

The refactor to use a named err with a single deferred commit/rollback for internally created Txn instances makes the transaction lifecycle much safer and clearer. UTXO blob writes short‑circuit on error, and metadata.SetTransaction is wired into the same flow with proper error propagation. Behavior when a caller passes a non‑nil Txn is preserved.

database/models/models.go (1)

29-31: MigrateModels registration for new certificate-related tables looks correct

Registering GenesisKeyDelegation, LeiosEb, and MoveInstantaneousRewards in MigrateModels is consistent with their definitions and placement relative to Certificate should be fine for migrations.

database/types/types.go (2)

36-63: Rat.Scan nil and type handling look correct

The updated Scan correctly:

  • Initializes r.Rat when needed.
  • Treats NULL as a no-op zero value.
  • Accepts string, []byte, and int64 and rejects everything else with a clear error.

This is a reasonable and explicit set of accepted database representations for NUMERIC/text-based rationals.


65-101: Uint64 wrapper Scan/Value implementation is sound

The new Uint64 struct with:

  • Value() returning a base‑10 string, and
  • Scan() handling NULL, string, []byte, and int64 (with a guarded cast),

gives predictable behavior for metadata columns while keeping on-disk representation compatible with existing string/int encodings. The NULL→zero behavior is clearly documented and matches the new tests.

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

20-42: Test helpers for CBOR decoding and deposits are well factored

decodeTestTransaction and getTestDeposits centralize shared fixture setup and keep the downstream tests focused on behavior instead of mechanics. This matches the intent of driving everything through real gledger.Transaction instances.


44-242: Pool and stake duplicate-prevention tests correctly exercise updated constraints

The pool and stake tests now:

  • Build a fresh cert struct inside each loop iteration with unique CertificateIDs.
  • Use for i := range 3 to get three iterations, which is valid for the Go version this repo targets (so the file compiles as intended).
  • Verify that re-registrations and multiple retirements/deregistrations for the same key are allowed, while duplicate (staking_key, certificate_id) pairs are rejected.

This accurately reflects the revised schema semantics around composite uniqueness and re-registration behavior.


244-423: DRep re‑registration and genesis delegation idempotency tests match schema design

The DRep tests confirm:

  • Multiple RegistrationDrep/DeregistrationDrep/UpdateDrep rows can exist per credential (no over‑strict uniqueness on the credential itself).
  • Counts for each table match expectations after re‑registration/update loops.

The genesis key delegation test mirrors the store’s ON CONFLICT DO NOTHING behavior and asserts that repeated inserts hit the same row (ID), plus that only a single row exists for a given (genesis_hash, delegate, vrf) triple. This neatly guards the intended idempotent semantics.


751-1051: SetTransaction idempotency and data‑integrity tests align with unified certificate flow

TestTransactionCertificateIdempotency and TestCertificateProcessingDataIntegrity now:

  • Drive everything through metadata.SetTransaction with a real decoded transaction and deposit map.
  • Assert non‑zero certificate creation on first insert, full idempotency when called again with the same block point, and stable certificate count when re‑included at a different point.
  • Verify slot updates on successful re‑inclusion, while explicitly allowing the “already processed” error path as long as no extra certs are created (with expectedSlot conditioned on whether the second call actually succeeded).

This gives strong coverage of the subtle re‑org/idempotency behavior without duplicating the production logic.

database/models/account_test.go (1)

26-235: Excellent test coverage for network-aware address formatting.

The test suite comprehensively covers mainnet/testnet HRP handling, various staking key lengths (28-byte, 32-byte), and edge cases (empty, nil, single-byte, all-zeros, all-ones). The round-trip validation via bech32 decode ensures encoding correctness.

database/models/transaction.go (1)

17-36: LGTM! Collateral return filtering correctly implemented.

The where:is_collateral_return clauses correctly filter regular outputs from the collateral return using the snake_case column name. This addresses the previous review concerns about distinguishing collateral returns from regular outputs.

database/models/resign_committee_cold.go (1)

17-30: Well-structured constraints for resignation tracking.

The unique constraint on ColdCredential correctly prevents duplicate resignations for the same cold credential. The CertificateID with uniqueIndex properly links to the unified certificate table, and the types.Uint64 usage for AddedSlot is consistent with other models.

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

96-137: Improved query logic with explicit conditions.

The refactored GetUtxosByAddress with explicit zero-hash checks and conditional where clauses is clearer and more maintainable than the previous implementation. The early return for no valid keys prevents unnecessary database queries.

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

75-95: Correct usage of types.Uint64 wrapper fields.

The .Val access for Pledge and Cost correctly unwraps the types.Uint64 wrapper, and the uint(lcommon.CertificateTypePoolRegistration) cast aligns with the updated CertType field type.

ledger/certs_test.go (1)

27-299: Comprehensive test coverage for certificate deposit calculation.

The test suite thoroughly validates deposit calculation across all Cardano certificate types, including:

  • Registration certificates with deposits (stake, pool, DRep)
  • Delegation certificates with zero deposits
  • Combined registration+delegation certificates
  • Edge case with zero protocol parameters

The integration-style test at line 174 provides end-to-end validation of the complete certificate type set.

database/models/account.go (2)

41-70: Well-implemented network-aware address formatting.

The StringWithNetwork method correctly handles mainnet ("stake") vs testnet ("stake_test") HRP, with proper error handling for empty staking keys and encoding failures. The delegation from String() to StringWithNetwork(false) preserves backward compatibility.


72-202: Clear certificate constraint strategy documentation.

The comment block at lines 72-78 clearly articulates the design: composite unique constraints on (StakingKey, CertificateID) for registration/deregistration certificates prevent duplicates while allowing re-registration, and simple indexes on other certificate types enable historical tracking. This addresses the legitimate certificate lifecycle concerns from previous reviews.

ledger/delta.go (2)

25-47: Correct deposit threading through transaction records.

The Deposits field in TransactionRecord and the renamed addTransactionWithDeposits method correctly thread deposit information through the transaction processing pipeline, enabling proper tracking of certificate-related deposits.


49-119: Properly handles deposits for valid vs invalid transactions.

The logic correctly implements Cardano's ledger rules:

  • Invalid transactions (lines 54-74): No deposit calculation; only collateral is processed
  • Valid transactions (lines 76-100): Deposits are calculated from certificates or used from pre-calculated values

This aligns with the phase-2 validation failure behavior where certificates are not applied for invalid transactions.

internal/node/load.go (1)

34-39: Catch‑up constants and progress‑based stall detection look solid

Using exported defaults plus a ticker with lastProgressTime and explicit slot‑0 handling gives you bounded wait, clear logs, and no more “stuck at slot 0 forever” risk. The exit and error paths are consistent and don’t leak goroutines.

Also applies to: 179-257

database/utxo.go (1)

24-24: UTxO API alignment with lcommon and collateral-return flag propagation look correct

Switching to lcommon.TransactionInput/lcommon.Address here and explicitly copying IsCollateralReturn into the returned models.Utxo slice keeps the database API consistent with the ledger/common types and preserves the collateral‑return flag end‑to‑end.

Also applies to: 102-112, 115-140

ledger/certs.go (1)

23-60: Centralized certificate‑deposit calculation is correct and preserves zero values

This helper cleanly delegates to the era’s CertDepositFunc, records every certificate index (including legitimate zero deposits), and wraps errors with the offending index, which is exactly what the downstream transaction/certificate persistence needs.

ledger/state.go (1)

17-19: Context-based lifecycle and channel ownership fixes look correct

The addition of ctx/cancel/wg, producer‑only close(resultCh), and ctx checks in both ledgerReadChain and ledgerProcessBlocks fixes the earlier channel‑closure race and gives you clean shutdown on errors or Close(). The deposit pre‑computation plus addTransactionWithDeposits integration in ledgerProcessBlock is also straightforward and consistent with the new certificate/deposit persistence flow.

Also applies to: 80-107, 127-129, 176-181, 542-644, 656-719, 838-845, 865-939

database/models/utxo.go (1)

23-40: UTxO model updates and collateral-return flag wiring are consistent

Extending Utxo with IsCollateralReturn and moving AddedSlot/DeletedSlot/Amount to types.Uint64, then setting those via UtxoLedgerToModel(…, isCollateralReturn) matches the GORM associations on Transaction and cleanly separates normal outputs from collateral return.

Also applies to: 50-63

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

1-401: Account and certificate linkage tests thoroughly exercise the new behavior

The four tests nicely cover SetAccount’s active/inactive semantics (with and without explicit transactions), DeleteCertificate’s cascade into specialized tables, and the account↔certificate linkage via CertificateID. Using the in‑memory metadata store keeps them fast and focused.

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

17-62: Unified certificate storage, account-state updates, and cascade delete are coherent

storeCertificate correctly establishes the mapping row first, then populates each specialized model with the right fields (including margin, anchors, MIR and Leios JSON payloads) and links both directions via CertificateID. updateAccountState restricts SetAccount calls to staking-key certs, and DeleteCertificate now deletes specialized rows by the unified certificate ID before removing the mapping, which matches the schema and the new tests.

Also applies to: 64-874

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

52-151: Idempotent transaction upsert and deposit-aware certificate persistence are well structured

The new SetTransaction flow uses a hash-based upsert, cleanly distinguishes “already exists” vs new inserts, and only processes UTXOs once. processTransactionCertificates correctly:

  • skips non-cert and invalid txs,
  • treats re-inclusion by updating certificate slots/block hashes without duplicating rows, and
  • enforces that deposit-requiring certificate types have entries in the deposits map while still allowing legitimate zero-deposit certs.

This matches the new ledger-side CalculateCertificateDeposits behavior and achieves the unified certificate persistence goal without reintroducing duplicate records.

Also applies to: 361-451


453-507: Certificate slot/block-hash re-inclusion updates, including LeiosEb, look correct

updateCertificateSlots updates both the unified certificates rows and all specialized tables’ added_slot (now including leios_eb) via a subquery on certificates.id, which keeps all certificate rows consistent when a transaction is re-included in a different block.

database/models/certificate.go (1)

152-175: Unified Certificate mapping model and indexing look correct

The Certificate struct layout and GORM tags make sense for the unified certificate table:

  • TransactionID + cert_index as a unique composite index gives you a stable per‑transaction certificate ordering.
  • constraint:OnDelete:CASCADE on TransactionID ensures mappings are cleaned up with their owning transaction.
  • CertType + CertificateID give you a simple polymorphic link into the specialized certificate tables without forcing DB‑level polymorphism.

No functional or modeling issues from what’s shown here.

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

36-50: Vacuum scheduling and shutdown lifecycle look correct

The added state (timerVacuum, timerMutex, vacuumWG, closed, dataDir) plus the new runVacuum/scheduleDailyVacuum flow look sound:

  • runVacuum skips work when dataDir == "" or closed, and uses vacuumWG so Close() can reliably wait for any in‑flight VACUUM before closing the DB.
  • scheduleDailyVacuum cancels any previous timer, checks closed, and reschedules via time.AfterFunc with a simple self‑rescheduling closure.

This addresses the earlier concern about VACUUM running after Close() and avoids running it against in‑memory stores entirely.

Also applies to: 128-188


253-312: Close now correctly stops background work before closing the DB

Close now:

  • Sets closed = true under timerMutex and stops both timerVacuum and timerCertificateCleanup.
  • Waits for vacuumWG and certificateCleanupWG to drain before actually closing the underlying DB handle.

That sequencing ensures no background VACUUM or cleanup run can outlive the store or race against db.Close(). The thin wrappers (Create, DB, First, Order, Transaction, Where) are straightforward pass‑throughs and look fine.

database/account.go (1)

21-51: GetAccountWithInactive nicely extends behavior without breaking callers

The split between:

  • GetAccountGetAccountWithInactive(..., false, ...), and
  • GetAccountWithInactive handling txn creation, delegating to metadata.GetAccount, and normalizing nil to models.ErrAccountNotFound

keeps existing behavior intact while adding an explicit way to query inactive accounts. The transaction management (if txn == nil { txn = d.Transaction(false); defer txn.Commit() }) is consistent with other database methods in this codebase.

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

25-49: Account lookup correctly honors includeInactive flag and not‑found behavior

GetAccount’s use of:

  • staking_key = ? as the primary key (matching the unique index on StakingKey), and
  • a conditional active = true filter only when includeInactive is false,

gives you the expected semantics for active‑only vs all‑accounts queries. Returning (nil, nil) on ErrRecordNotFound lines up with how database.GetAccountWithInactive translates that into ErrAccountNotFound.

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

56-60: LGTM! Clear and straightforward parameter addition.

The includeInactive flag provides a clean way to filter inactive accounts. The parameter name is self-documenting, and the addition maintains interface clarity.


127-133: Excellent refactor with clear documentation.

The signature changes strengthen the interface by:

  • Using ocommon.Point for richer chain location context (vs. raw slot)
  • Adding the Transaction parameter for direct access to certificate data
  • Providing a well-documented deposits map with a clear contract

The comment explicitly states the requirements: "Keys must align with transaction certificate slice order. Missing keys default to 0." This prevents ambiguity and helps implementers understand the expected behavior.


144-146: Consistent use of canonical ledger/common types.

The updated signatures correctly use lcommon.Address and lcommon.TransactionInput, establishing consistency with the gouroboros ledger/common package as the canonical type source. This prevents type mismatches and aligns with the repository's pattern.

if isTestnet {
hrp = "stake_test"
}
encoded, err := bech32.Encode(hrp, convData)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should use gOuroboros's common.Address for this

CertificateTypeLeiosEb CertificateType = CertificateType(
lcommon.CertificateTypeLeiosEb,
)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

If we want to define a special type for certificate types, we should really do this in gOuroboros and make it consistent throughout.

}
*ct = CertificateType(val)
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need all of this special DB handling for a uint type?

blockfetch.go Outdated
blockfetch.WithBlockTimeout(2 * time.Second),
// Set the recv queue size to 2x our block batch size
blockfetch.WithRecvQueueSize(1000),
blockfetch.WithRecvQueueSize(512),
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this being changed? This was set to its current value very specifically

- 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 d748f01 to a82866f Compare November 21, 2025 18: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

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

425-495: Consider adding an integration test that exercises AuthCommitteeHot via the actual store path

TestAuthCommitteeHotCertificateIdPopulation currently creates models.AuthCommitteeHot records directly through GORM and validates CertificateID persistence and the uniqueness of ColdCredential. That’s valuable, but it doesn’t hit whatever higher‑level persistence path (e.g., SetCertificate / SetTransaction) is responsible for wiring up CertificateID in production.

To fully close the loop on the earlier concern that CertificateID might be left unset, consider adding a test that:

  • Builds a minimal transaction containing an auth‑committee‑hot certificate.
  • Calls the real SetTransaction / metadata store entry point.
  • Asserts that the resulting AuthCommitteeHot row has the expected non‑zero CertificateID and that the uniqueness constraint behaves as intended under that path.

This would ensure the model, tags, and store logic all agree.

🧹 Nitpick comments (5)
database/models/certificate_test.go (1)

69-142: Strengthen Scan error‑path assertions to guard against partial writes

The TestCertificateType_Scan table covers a good range of input shapes and error cases, but for the error cases (invalid type, negative value, too large value, uint overflow) the test only checks that Scan returns an error and doesn’t assert anything about the final value of ct. To catch accidental partial assignments in Scan, consider also asserting that ct remains at its zero value (or some explicitly expected value) when wantErr is true.

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

1402-1525: DeleteCertificate coverage relies on a hand‑maintained constant list

TestDeleteCertificateTypeCoverage is valuable in that it asserts:

  • Each listed CertificateType can be round‑tripped through DeleteCertificate without error.
  • An explicit unknown type produces the expected “unknown certificate type …” error.

One thing to be aware of is that the knownTypes slice is manual: if new CertificateType constants are added in models, this test will not fail until you also update knownTypes. The comment currently suggests automatic coverage (“If new certificate types are added, this test will fail…”), which isn’t strictly true.

Consider either:

  • Updating the comment to note that knownTypes must be kept in sync with new constants, or
  • Adding a small helper (e.g., a central AllCertificateTypes() list in models) that both the switch and this test consume to avoid drift.
database/plugin/metadata/sqlite/account_test.go (1)

28-128: Nice coverage of SetAccount state and transaction semantics

These two tests thoroughly exercise the new SetAccount behavior: active→inactive transitions with includeInactive filtering, and transaction-scoped visibility/rollback. They should catch most regressions in account upserts. If you want to trim repetition later, consider a small helper for the in‑memory DB/metadata store setup, but not required for this PR.

Also applies to: 130-187

ledger/state.go (1)

80-107: Goroutine lifecycle management via context and WaitGroup is solid

Adding ctx, cancel, and wg to LedgerState, wiring them in NewLedgerState, Start, and Close, gives you explicit ownership of the long‑running block processing goroutine and its child readers. One small follow‑up you might consider is stopping timerCleanupConsumedUtxos in Close as well, so the periodic cleanup callback doesn’t try to touch a closed DB after shutdown.

Also applies to: 109-129, 132-182, 209-218

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

190-219: Certificate cleanup is a no-op placeholder.

The runCertificateCleanup function is currently a placeholder with TODO comments (lines 203-208) but is scheduled to run every 6 hours. While the infrastructure is in place, the actual cleanup logic is not implemented yet.

This is acceptable for now since the PR objectives focus on certificate persistence, not cleanup. Consider creating a follow-up issue to track the cleanup implementation if it's needed for production use.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d748f01 and a82866f.

📒 Files selected for processing (44)
  • database/account.go (1 hunks)
  • database/certificate.go (1 hunks)
  • database/database_test.go (2 hunks)
  • database/drep.go (0 hunks)
  • database/models/account.go (4 hunks)
  • database/models/account_test.go (4 hunks)
  • database/models/asset.go (1 hunks)
  • database/models/auth_committee_hot.go (1 hunks)
  • database/models/certificate.go (1 hunks)
  • database/models/certificate_test.go (1 hunks)
  • database/models/certs.go (0 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 (2 hunks)
  • database/plugin/metadata/sqlite/account.go (2 hunks)
  • database/plugin/metadata/sqlite/account_test.go (1 hunks)
  • database/plugin/metadata/sqlite/certs.go (2 hunks)
  • database/plugin/metadata/sqlite/certs_test.go (1 hunks)
  • database/plugin/metadata/sqlite/database.go (4 hunks)
  • database/plugin/metadata/sqlite/drep.go (2 hunks)
  • database/plugin/metadata/sqlite/plugin.go (0 hunks)
  • database/plugin/metadata/sqlite/pool.go (2 hunks)
  • database/plugin/metadata/sqlite/transaction.go (5 hunks)
  • database/plugin/metadata/sqlite/utxo.go (4 hunks)
  • database/plugin/metadata/store.go (3 hunks)
  • database/transaction.go (1 hunks)
  • database/types/types.go (2 hunks)
  • database/types/types_test.go (2 hunks)
  • database/utxo.go (4 hunks)
  • go.mod (3 hunks)
  • internal/config/config.go (3 hunks)
  • internal/config/config_test.go (1 hunks)
  • internal/node/load.go (3 hunks)
  • ledger/certs.go (1 hunks)
  • ledger/certs_test.go (1 hunks)
  • ledger/delta.go (4 hunks)
  • ledger/state.go (14 hunks)
  • ledger/view.go (1 hunks)
💤 Files with no reviewable changes (3)
  • database/plugin/metadata/sqlite/plugin.go
  • database/drep.go
  • database/models/certs.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • internal/config/config_test.go
  • database/plugin/metadata/sqlite/utxo.go
  • database/models/genesis_key_delegation.go
  • database/models/models.go
  • database/models/drep.go
  • database/models/asset.go
  • go.mod
  • database/models/transaction.go
  • database/models/move_instantaneous_rewards.go
  • database/database_test.go
  • ledger/certs_test.go
🧰 Additional context used
🧠 Learnings (13)
📚 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/utxo.go
  • database/utxo.go
  • database/transaction.go
  • ledger/delta.go
  • database/plugin/metadata/store.go
  • database/plugin/metadata/sqlite/transaction.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/utxo.go
  • database/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/models/utxo.go
  • database/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/models/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • ledger/state.go
  • database/utxo.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/models/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • database/utxo.go
  • database/plugin/metadata/store.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: 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/utxo.go
  • database/plugin/metadata/sqlite/certs_test.go
  • ledger/state.go
  • database/utxo.go
  • database/plugin/metadata/store.go
📚 Learning: 2025-11-20T02:07:07.073Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 0
File: :0-0
Timestamp: 2025-11-20T02:07:07.073Z
Learning: In the blinklabs-io/dingo repository, range over int is valid Go syntax. The repository uses Go 1.24.0 (minimum Go 1.23), and range over int has been supported since Go 1.22. This syntax should not be flagged as invalid.

Applied to files:

  • database/plugin/metadata/sqlite/certs_test.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:

  • database/plugin/metadata/sqlite/certs_test.go
  • ledger/state.go
  • database/utxo.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:

  • ledger/state.go
  • database/utxo.go
  • database/plugin/metadata/store.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/state.go
📚 Learning: 2025-10-22T20:18:45.125Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: ledger/state_test.go:248-248
Timestamp: 2025-10-22T20:18:45.125Z
Learning: In tests, Chain.AddBlock must be given a gouroboros ledger.Block (not database/models.Block). Use a lightweight MockBlock (as in chain/chain_test.go) or call models.Block.Decode() when starting from stored rows.

Applied to files:

  • ledger/state.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/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: 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/utxo.go
🧬 Code graph analysis (22)
database/models/leios_eb.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/certificate.go (2)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/models/utxo.go (2)
database/models/asset.go (2)
  • Asset (24-32)
  • Asset (34-36)
database/types/types.go (1)
  • Uint64 (66-68)
ledger/certs.go (2)
ledger/state.go (1)
  • LedgerState (79-107)
database/models/certificate.go (2)
  • Certificate (157-165)
  • Certificate (168-170)
database/models/auth_committee_hot.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/types/types_test.go (1)
database/types/types.go (2)
  • Uint64 (66-68)
  • Rat (25-27)
database/plugin/metadata/sqlite/certs.go (10)
database/models/certificate.go (23)
  • Certificate (157-165)
  • Certificate (168-170)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeStakeDeregistration (35-37)
  • CertificateTypeStakeDelegation (38-40)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypePoolRetirement (44-46)
  • CertificateTypeRegistration (47-49)
  • CertificateTypeDeregistration (50-52)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteDelegation (56-58)
  • CertificateTypeStakeVoteDelegation (59-61)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • CertificateTypeAuthCommitteeHot (68-70)
  • CertificateTypeResignCommitteeCold (71-73)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeDeregistrationDrep (77-79)
  • CertificateTypeUpdateDrep (80-82)
  • CertificateTypeGenesisKeyDelegation (83-85)
  • CertificateTypeMoveInstantaneousRewards (86-88)
  • CertificateTypeLeiosEb (89-91)
database/models/account.go (20)
  • StakeRegistration (127-133)
  • StakeRegistration (135-137)
  • StakeDeregistration (116-121)
  • StakeDeregistration (123-125)
  • StakeDelegation (104-110)
  • StakeDelegation (112-114)
  • Registration (92-98)
  • Registration (100-102)
  • Deregistration (80-86)
  • Deregistration (88-90)
  • StakeRegistrationDelegation (139-146)
  • StakeRegistrationDelegation (148-150)
  • VoteDelegation (179-185)
  • VoteDelegation (187-189)
  • StakeVoteDelegation (152-159)
  • StakeVoteDelegation (161-163)
  • VoteRegistrationDelegation (191-198)
  • VoteRegistrationDelegation (200-202)
  • StakeVoteRegistrationDelegation (165-173)
  • StakeVoteRegistrationDelegation (175-177)
database/types/types.go (2)
  • Uint64 (66-68)
  • Rat (25-27)
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/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
database/models/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/plugin/metadata/sqlite/certs_test.go (10)
database/plugin/metadata/sqlite/database.go (2)
  • New (53-126)
  • MetadataStoreSqlite (39-50)
database/models/pool.go (5)
  • PoolRegistration (44-60)
  • PoolRegistration (62-64)
  • PoolRetirement (91-98)
  • PoolRetirement (100-102)
  • Pool (26-38)
database/models/account.go (7)
  • StakeRegistration (127-133)
  • StakeRegistration (135-137)
  • Deregistration (80-86)
  • Deregistration (88-90)
  • StakeDeregistration (116-121)
  • StakeDeregistration (123-125)
  • Account (27-35)
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/genesis_key_delegation.go (2)
  • GenesisKeyDelegation (22-29)
  • GenesisKeyDelegation (32-34)
database/models/auth_committee_hot.go (2)
  • AuthCommitteeHot (21-27)
  • AuthCommitteeHot (29-31)
database/models/certificate.go (17)
  • Certificate (157-165)
  • Certificate (168-170)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateType (28-28)
  • CertificateTypeStakeDeregistration (35-37)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypePoolRetirement (44-46)
  • CertificateTypeRegistration (47-49)
  • CertificateTypeDeregistration (50-52)
  • CertificateTypeAuthCommitteeHot (68-70)
  • CertificateTypeResignCommitteeCold (71-73)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeDeregistrationDrep (77-79)
  • CertificateTypeUpdateDrep (80-82)
  • CertificateTypeGenesisKeyDelegation (83-85)
  • CertificateTypeMoveInstantaneousRewards (86-88)
  • CertificateTypeLeiosEb (89-91)
database/models/move_instantaneous_rewards.go (2)
  • MoveInstantaneousRewards (22-29)
  • MoveInstantaneousRewards (32-34)
database/models/leios_eb.go (2)
  • LeiosEb (22-32)
  • LeiosEb (35-37)
database/models/resign_committee_cold.go (2)
  • ResignCommitteeCold (23-30)
  • ResignCommitteeCold (32-34)
internal/config/config.go (1)
internal/node/load.go (2)
  • LedgerCatchupPollInterval (36-36)
  • LedgerCatchupStallTimeout (38-38)
database/models/account_test.go (1)
database/models/account.go (1)
  • Account (27-35)
database/transaction.go (3)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/utxo.go (1)
  • UtxoBlobKey (269-277)
ledger/delta.go (1)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
internal/node/load.go (1)
internal/config/config.go (2)
  • DefaultLedgerCatchupPollInterval (37-37)
  • DefaultLedgerCatchupStallTimeout (38-38)
database/models/resign_committee_cold.go (1)
database/types/types.go (1)
  • Uint64 (66-68)
database/plugin/metadata/sqlite/account_test.go (7)
database/plugin/metadata/sqlite/database.go (2)
  • New (53-126)
  • MetadataStoreSqlite (39-50)
database/models/pool.go (1)
  • Pool (26-38)
database/models/drep.go (1)
  • Drep (21-29)
database/models/account.go (3)
  • Account (27-35)
  • StakeRegistration (127-133)
  • StakeRegistration (135-137)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/certificate.go (2)
  • Certificate (157-165)
  • Certificate (168-170)
database/plugin/metadata/store.go (2)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/models/utxo.go (1)
  • Utxo (24-40)
database/plugin/metadata/sqlite/drep.go (2)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-50)
database/models/drep.go (1)
  • Drep (21-29)
database/plugin/metadata/sqlite/account.go (1)
database/models/account.go (1)
  • Account (27-35)
database/models/certificate_test.go (1)
database/models/certificate.go (3)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeStakeDeregistration (35-37)
database/account.go (3)
database/models/account.go (1)
  • Account (27-35)
database/database.go (1)
  • Database (45-50)
database/txn.go (1)
  • Txn (27-34)
database/plugin/metadata/sqlite/transaction.go (5)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (39-50)
database/models/transaction.go (2)
  • Transaction (22-36)
  • Transaction (38-40)
database/types/types.go (1)
  • Uint64 (66-68)
database/models/utxo.go (1)
  • UtxoLedgerToModel (50-81)
database/models/certificate.go (10)
  • CertificateType (28-28)
  • CertificateTypeStakeRegistration (32-34)
  • CertificateTypeRegistration (47-49)
  • CertificateTypePoolRegistration (41-43)
  • CertificateTypeRegistrationDrep (74-76)
  • CertificateTypeStakeRegistrationDelegation (53-55)
  • CertificateTypeVoteRegistrationDelegation (62-64)
  • CertificateTypeStakeVoteRegistrationDelegation (65-67)
  • Certificate (157-165)
  • Certificate (168-170)
database/plugin/metadata/sqlite/database.go (1)
database/plugin/log.go (1)
  • Logger (18-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). (3)
  • GitHub Check: docker (ubuntu-latest, amd64)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (48)
internal/node/load.go (3)

34-39: LGTM!

The constants properly expose the config defaults for use as fallbacks in the catchup loop.


99-102: LGTM!

The error message update correctly reflects the asynchronous start semantics.


179-258: LGTM!

The progress-based catchup loop correctly implements:

  • Configurable poll interval and stall timeout with proper fallbacks
  • Stuck-at-slot-0 detection and timeout (addresses past review concern)
  • Progress tracking with stall detection
  • Clean exit paths for success and timeout scenarios

All past review comments have been addressed in this implementation.

internal/config/config.go (3)

72-74: LGTM!

The Config fields follow the established pattern with appropriate YAML tags and directives.


96-98: LGTM!

The globalConfig initialization correctly sets the new fields to their default constants, following the standard initialization pattern.


34-39: The constant DefaultLedgerCatchupTimeout is actively used and there is no issue.

The verification shows that DefaultLedgerCatchupTimeout is used at line 96 in internal/config/config.go to initialize the LedgerCatchupTimeout field in the Config struct initialization. This is legitimate usage within the same file, which is the appropriate pattern for package-level default constants.

Likely an incorrect or invalid review comment.

ledger/view.go (1)

111-112: LGTM!

The field assignments correctly access .Val from the types.Uint64 struct fields, consistent with the Uint64 type migration from alias to struct throughout this PR.

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

18-23: LGTM!

Import changes align with the refactored code: errors is now needed for ErrRecordNotFound checks, and unused imports have been removed.


49-85: LGTM!

The SetDrep refactoring correctly:

  • Adds the certificateID parameter to support unified certificate tracking
  • Updates the DrepCredential field usage
  • Configures OnConflict to upsert all fields including certificate_id

The implementation properly handles both transactional and non-transactional paths.


25-47: GetDrep's (nil, nil) pattern is consistent with similar Get methods.

Verification confirms GetDrep follows the standard pattern used by six other similar pointer-returning Get methods in the sqlite store (GetUTXO, GetTransaction, GetPool, GetAccount, GetDatum). All return (nil, nil) when a record is not found. While some unrelated methods use different patterns (e.g., GetTip returns a zero-initialized value, GetCommitTimestamp returns a scalar), this is due to differing return types, not inconsistency.

database/types/types_test.go (2)

33-35: LGTM!

The Uint64 construction correctly uses the struct literal syntax types.Uint64{Val: 123}, consistent with the type migration from alias to struct.


82-108: Excellent test coverage!

The extended test validates that both Uint64 and Rat correctly restore their underlying values when scanning from []byte, using type-appropriate comparison methods (.Val equality for Uint64, .Cmp() for Rat).

database/models/utxo.go (3)

35-39: LGTM!

The field type changes and addition of IsCollateralReturn are consistent with:

  • The Uint64 struct migration across the PR
  • Support for tracking collateral return UTXOs (per PR objectives and test coverage in database/database_test.go)

50-54: LGTM!

The addition of the isCollateralReturn parameter enables callers to specify whether a UTXO is a collateral return, which is then persisted in the model.


56-63: LGTM!

Field assignments correctly wrap values in the types.Uint64 struct and properly set the IsCollateralReturn field from the parameter.

database/models/pool.go (2)

55-55: LGTM!

The unique index on CertificateID correctly enforces that each certificate appears at most once in the pool_registration table, supporting idempotent certificate persistence in the unified system.


93-93: LGTM!

The unique index on CertificateID correctly enforces that each retirement certificate appears at most once in the pool_retirement table, mirroring the pattern used for pool registrations.

database/types/types.go (3)

40-43: LGTM!

The nil handling for Rat.Scan is appropriate and well-documented, preserving a zero-value rat for NULL database columns.


44-58: LGTM!

The type switch comprehensively handles the common database driver value types (string, []byte, int64), with appropriate fallback handling for int64.


66-72: LGTM!

The Uint64 type change from alias to struct is implemented correctly, with Value() properly accessing the Val field for serialization.

database/models/leios_eb.go (2)

21-32: LGTM!

The LeiosEb model follows established patterns from other certificate models in this PR:

  • Composite unique index on the business key (ElectionID + EndorserBlockHash)
  • Separate unique index on CertificateID for certificate system integration
  • []byte fields for JSON-encoded complex data structures (voters, signatures)

The indexing strategy correctly balances uniqueness enforcement with queryability.


34-37: LGTM!

The TableName() method correctly specifies the database table name.

database/certificate.go (3)

21-23: LGTM!

The comment clearly documents the new unified certificate persistence strategy, clarifying that certificates are now persisted via SetTransaction with deposits calculated by the ledger layer.


25-35: LGTM!

The auto-transaction pattern improves caller convenience by handling the common case where no transaction is provided. The //nolint:errcheck on the deferred Commit() is acceptable for read-only transactions where commit failures don't affect data integrity.


37-47: LGTM!

The auto-transaction pattern is consistently applied, matching the implementation in GetPoolRegistrations.

database/models/auth_committee_hot.go (1)

21-27: AuthCommitteeHot model changes look consistent with unified certificate design

ColdCredential as a single‑column unique plus a separate unique index on CertificateID, and AddedSlot using types.Uint64, align with the other certificate models and the new unified certificate mapping. I don’t see issues with the tags or field types here.

database/models/account_test.go (1)

26-235: Expanded Account string tests correctly cover mainnet/testnet HRPs

The refactored table and new cases for 28‑ and 32‑byte staking keys, edge cases (single byte, all‑zeros, all‑ones), and the isTestnet/expectedHRP wiring give good coverage of StringWithNetwork. The downstream checks using bech32 decoding and round‑tripping the payload still look coherent, and keeping the separate String() tests for idempotence and format on mainnet makes sense.

database/models/resign_committee_cold.go (1)

21-30: ResignCommitteeCold uniqueness and type changes look appropriate

The explicit comment plus gorm:"unique" on ColdCredential and a dedicated unique index on CertificateID, together with AddedSlot switched to types.Uint64, line up with the unified certificate mapping and with how other certificate‑backed models are structured. No issues from a modeling or migration perspective.

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

53-91: GetPoolRegistrations query and type mappings match the updated models

Using pkh.Bytes() directly in the pool_key_hash filter (for both transactional and non‑transactional paths) is consistent with how the pool key hash is stored and avoids any double‑hashing. Mapping Pledge and Cost via cert.Pledge.Val / cert.Cost.Val and casting CertType to uint also align with the new types.Uint64 model fields and the updated certificate type representation. No functional issues stand out here.

ledger/delta.go (1)

25-35: Deposit handling in LedgerDelta.apply now correctly separates valid/invalid transactions

Storing deposits on TransactionRecord and, in apply, skipping deposit calculation entirely for !tr.Tx.IsValid() while still consuming UTxOs, then using either precomputed tr.Deposits or CalculateCertificateDeposits only for valid transactions, matches the intended Cardano semantics and removes the earlier bug where invalid transactions could get certificate deposits recorded. The updated SetTransaction calls with the deposits map look consistent with that design.

Also applies to: 37-89

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

24-42: CBOR decode and deposit helpers are clear and reusable

decodeTestTransaction and getTestDeposits give a nice, centralized way to drive multiple tests off the same real transaction payload and deposit map. The helper use of gledger.DetermineTransactionType and NewTransactionFromCbor keeps individual tests focused on persistence behavior rather than CBOR plumbing.


44-339: Pool/stake/DRep duplicate‑handling tests align with the new uniqueness semantics

The pool, stake, and DRep tests correctly mirror the GORM tag semantics:

  • Pool registration/retirement tests confirm that multiple rows per pool (and per epoch for retirements) are allowed, since uniqueness is on CertificateID only.
  • Stake registration/deregistration tests verify the composite uniqueness on (staking_key, certificate_id) by allowing distinct certificate IDs and rejecting duplicates.
  • DRep registration/deregistration/update tests ensure re‑registration and multiple updates are permitted for the same credential.

Using for i := range 3 with varying CertificateID/AddedSlot values gives clear, repeatable coverage without false positives from primary‑key reuse.


341-423: GenesisKeyDelegation idempotency test matches ON CONFLICT behavior

TestGenesisKeyDelegationCertificateIdempotent accurately reproduces the store’s ON CONFLICT (genesis_hash, genesis_delegate_hash, vrf_key_hash) DO NOTHING behavior and verifies that repeated inserts for the same triple always resolve to the same row ID. This is a good guardrail against regressions in idempotent handling for genesis key delegation.


497-749: Certificate persistence and Uint64 conversion tests give good end‑to‑end coverage

The grouped subtests in TestCertificatePersistenceValidation and the later Uint64ConversionValidation exercise:

  • Uniqueness on StakeDeregistration (staking_key, certificate_id).
  • Manual bulk cleanup semantics for specialized tables plus the unified Certificate mapping (mirroring bulkRemoveCertificateRecords).
  • Account state transitions (active → inactive).
  • Storage and retrieval of types.Uint64 fields (DepositAmount, Amount) for both stake registration and deregistration.

These tests significantly reduce the risk of subtle migration or type‑conversion bugs across the unified certificate schema.


751-859: Transaction re‑inclusion/idempotency behavior is well covered

TestTransactionCertificateIdempotency drives the real MetadataStore.SetTransaction with a decoded CBOR transaction and deposit map, and checks:

  • Certificates are created on first inclusion with Slot matching the block point.
  • A second call with the same block point doesn’t change the certificate count (idempotent).
  • A third call with a different block point preserves the count but updates all certificate Slot values.

This is a solid end‑to‑end check that the unified certificate persistence is idempotent and correctly tracks re‑inclusion after a reorg.


861-1051: Flexible re‑processing test captures both idempotent and “already processed” behaviors

TestCertificateProcessingDataIntegrity does a good job of covering:

  • Required deposits for certificate‑bearing transactions (negative case when provideDeposits is false).
  • Single‑shot processing with deposits.
  • Re‑processing with same or different block points, checking that certificate counts never increase and that Slot is updated only when a second call actually succeeds on a different block.

The reprocessingSucceeded flag and final expectedSlot logic correctly reconcile the comment that a failing second call is acceptable as long as it doesn’t create duplicates, while still asserting slot updates when re‑inclusion succeeds.


1053-1096: Slot persistence test nicely isolates the Certificate.Slot field

TestCertificateSlotField is a concise sanity check that, given a real transaction and deposits, all stored models.Certificate rows have their Slot field set to the block point’s slot. That’s a helpful focused guard in addition to the broader idempotency tests.


1098-1141: DeregistrationDrep deposit persistence is verified end‑to‑end

TestDeregistrationDrepDepositAmountPersistence directly exercises DeregistrationDrep with a types.Uint64 DepositAmount and confirms that the raw Val survives round‑trip through SQLite. This aligns with the broader use of types.Uint64 across the certificate models.


1143-1400: MIR, LeiosEb, and anchor-bearing certificate mapping tests are thorough

The TestCertificateDataMappingValidation subtests validate:

  • JSON payload and numeric fields for MoveInstantaneousRewards.
  • All primary byte fields for LeiosEb (IDs, voter data, signatures).
  • Anchor URL/hash propagation for ResignCommitteeCold, RegistrationDrep, and UpdateDrep.

These direct model‑level checks cover the new certificate‑specific tables well and should catch schema/tag regressions for these more specialized certificate types.


1527-1669: DRep SetDrep tests effectively validate creation and update semantics

The two DRep tests:

  • Confirm that SetDrep creates a general Drep record with the expected credential, anchor info, slot, active flag, and CertificateID.
  • Verify that calling SetDrep again for the same credential updates the existing record in place (slot, anchor URL/hash, active flag, and CertificateID), while keeping only a single row.

This is a good end‑to‑end verification of the DRep aggregation behavior on top of the per‑certificate tables.

database/transaction.go (1)

22-63: Reworked SetTransaction transaction management fixes the rollback/commit hazard

Changing SetTransaction to use a named err return with a single deferred closure that chooses between Rollback and Commit only when it creates the Txn itself cleanly resolves the previous risk of committing after a rollback. The UTXO blob writes now propagate errors immediately, and the final metadata.SetTransaction call includes the deposits map as intended. The behavior for caller‑supplied txn (no auto‑commit/rollback) is preserved.

database/utxo.go (1)

24-25: UTxO API alignment with ledger/common and collateral-return flag looks good

Using lcommon.TransactionInput/lcommon.Address here matches the updated metadata store interface, and the UtxosByAddress mapper now correctly includes IsCollateralReturn while preserving all existing fields. No functional issues spotted.

Also applies to: 102-112, 114-140

ledger/certs.go (1)

23-60: Well-factored, side-effect-free certificate deposit calculation

CalculateCertificateDeposits cleanly centralizes deposit computation, always records entries (including zeros), and surfaces errors with the certificate index for debugging. This aligns with the new deposits-aware flow in ledger/state.go/ledger/delta.go and addresses earlier concerns about distinguishing missing vs. zero deposits.

database/models/account.go (2)

32-35: Bech32 helpers are clear and network-aware

Delegating String() to StringWithNetwork(false) and gating HRP selection on isTestnet makes the API explicit while keeping the common mainnet case simple. Empty-key and encoding failure paths are handled cleanly.

Also applies to: 41-69


72-79: Composite certificate constraints now match the intended lifecycle semantics

The documented strategy and tags on Deregistration, StakeDeregistration, and StakeRegistration (composite uniqueIndex on (StakingKey, CertificateID)) prevent duplicate processing per unified certificate while still allowing multiple historical records across other models (Registration, StakeDelegation, etc.). This resolves the earlier “one cert per staking key” issue without sacrificing history for non-terminal certs.

Also applies to: 80-86, 116-121, 127-133

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

189-287: Certificate delete/linkage tests validate the unified mapping design

TestDeleteCertificate and TestSetAccountWithCertificateLinkage correctly model the unified certificate row plus specific StakeRegistration, then assert:

  • Cascading removal of both unified and specific rows via DeleteCertificate.
  • Proper wiring of Account.CertificateID when a non-zero certificate ID is passed into SetAccount, including reverse lookup by certificate_id.

This gives strong signal that the composite constraints and new SetAccount/SetCertificate flows are behaving as intended.

Also applies to: 289-401

ledger/state.go (2)

542-632: Channel + context handling between ledgerReadChain and ledgerProcessBlocks looks race-free now

Moving close(resultCh) into ledgerReadChain (with defer), adding the ctx.Done() checks in both producer and consumer, and removing receiver-side closure eliminates the prior “send on closed channel” risk. The small buffer on readChainResultCh is also a reasonable choice to decouple the iterator loop from transient processing delays.

Also applies to: 634-645, 656-661, 697-719, 838-845


887-907: Deposit precomputation and strict validation error handling in ledgerProcessBlock

Precomputing per-certificate deposits via CalculateCertificateDeposits and passing them into addTransactionWithDeposits keeps deposit logic centralized and ensures the delta layer has all information upfront. At the same time, ValidateTxFunc errors are now propagated directly (TX validation failure: %w), restoring the expected “invalid TX aborts block processing” behavior. This combination matches the unified certificate/deposit design and avoids silent validation failures.

Also applies to: 908-923, 927-936

Comment on lines 74 to 100
func (u *Uint64) Scan(val any) error {
v, ok := val.(string)
if !ok {
if val == nil {
// Treat NULL as zero; adjust if a different semantic is desired.
u.Val = 0
return nil
}
var v string
switch t := val.(type) {
case string:
v = t
case []byte:
v = string(t)
case int64:
u.Val = uint64(t) // #nosec G115 – application-level guarantee about range
return nil
default:
return fmt.Errorf(
"value was not expected type, wanted string, got %T",
"value was not expected type, wanted string, []byte, or int64, got %T",
val,
)
}
tmpUint, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return err
}
*u = Uint64(tmpUint)
u.Val = tmpUint
return nil
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 | 🟠 Major

🧩 Analysis chain

Verify the int64 → uint64 cast safety assumption.

Line 87 casts int64 to uint64 with #nosec G115 and claims "application-level guarantee about range," but no bounds checking is present. If a database driver returns a negative int64 value, this cast will produce an incorrect large uint64.

Options to address this:

  1. Add bounds checking (recommended):
 case int64:
+	if t < 0 {
+		return fmt.Errorf("cannot convert negative int64 %d to uint64", t)
+	}
 	u.Val = uint64(t) // #nosec G115
  1. Document the guarantee: If the application truly guarantees non-negative values at the database driver level, document this in the function comment.

Run the following to check if similar patterns exist elsewhere:


🏁 Script executed:

#!/bin/bash
# Description: Check for similar int64→uint64 casts and their handling

# Find other int64 to uint64 conversions
echo "=== int64 to uint64 conversions ==="
rg -n "uint64\(.*int64" database/ --type=go -C 2

# Check if there are validation patterns
echo "=== Negative value checks before uint conversion ==="
rg -n "< 0.*uint64|negative.*uint" database/ --type=go -C 2

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Verify file location and content

echo "=== Locating types.go files ==="
fd "types.go" --type f

echo -e "\n=== Content of database/types/types.go (lines 74-100) ==="
if [ -f "database/types/types.go" ]; then
  sed -n '74,100p' "database/types/types.go"
else
  echo "File not found at database/types/types.go"
fi

echo -e "\n=== Search for all int64 and uint64 in types.go ==="
if [ -f "database/types/types.go" ]; then
  rg -n "int64|uint64" "database/types/types.go" -C 1
fi

echo -e "\n=== Check all Scan method implementations ==="
rg -n "func.*Scan" --type=go -A 15 | head -100

Length of output: 5664



🏁 Script executed:

#!/bin/bash
# Check if tests cover negative int64 input to Uint64.Scan

echo "=== Scan tests in types_test.go ==="
rg -n "Scan" "database/types/types_test.go" -A 10 -B 2

echo -e "\n=== Look for negative test cases ==="
rg -n "negative|-[0-9]|< *0" "database/types/types_test.go"

Length of output: 2231


Add bounds checking to prevent silent wraparound on negative int64 values.

Line 87 in database/types/types.go casts int64 directly to uint64 without validation. The #nosec G115 comment claims "application-level guarantee about range," but:

  • Tests (lines 27–109 in types_test.go) do not cover negative int64 input
  • Similar code in database/models/certificate.go:113–117 explicitly validates uint overflow before conversion
  • A negative int64 from a database driver would silently wrap to a large positive uint64

Add bounds checking:

 case int64:
+	if t < 0 {
+		return fmt.Errorf("cannot convert negative int64 %d to uint64", t)
+	}
 	u.Val = uint64(t) // #nosec G115

Also add a test case covering negative int64 input in database/types/types_test.go.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (u *Uint64) Scan(val any) error {
v, ok := val.(string)
if !ok {
if val == nil {
// Treat NULL as zero; adjust if a different semantic is desired.
u.Val = 0
return nil
}
var v string
switch t := val.(type) {
case string:
v = t
case []byte:
v = string(t)
case int64:
u.Val = uint64(t) // #nosec G115 – application-level guarantee about range
return nil
default:
return fmt.Errorf(
"value was not expected type, wanted string, got %T",
"value was not expected type, wanted string, []byte, or int64, got %T",
val,
)
}
tmpUint, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return err
}
*u = Uint64(tmpUint)
u.Val = tmpUint
return nil
func (u *Uint64) Scan(val any) error {
if val == nil {
// Treat NULL as zero; adjust if a different semantic is desired.
u.Val = 0
return nil
}
var v string
switch t := val.(type) {
case string:
v = t
case []byte:
v = string(t)
case int64:
if t < 0 {
return fmt.Errorf("cannot convert negative int64 %d to uint64", t)
}
u.Val = uint64(t) // #nosec G115 – application-level guarantee about range
return nil
default:
return fmt.Errorf(
"value was not expected type, wanted string, []byte, or int64, got %T",
val,
)
}
tmpUint, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return err
}
u.Val = tmpUint
return nil
🤖 Prompt for AI Agents
In database/types/types.go around lines 74–100, the Scan method casts int64 to
uint64 without checking for negative values which would silently wrap; modify
the int64 case to validate the value is non-negative before converting (return
an error if negative or out of expected range), mirror the explicit overflow
validation pattern used in database/models/certificate.go, and keep the existing
#nosec only if a proper range check is performed; additionally add a unit test
in database/types/types_test.go covering a negative int64 input to assert Scan
returns an error.

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