Skip to content

Conversation

@jkawan
Copy link
Contributor

@jkawan jkawan commented Nov 17, 2025

  1. Changes to track witness set for transaction
    Closes Track transaction witness set #874

Summary by CodeRabbit

  • New Features

    • Transactions now capture and store richer witness and script metadata: key witnesses, native & Plutus scripts, datums (Plutus data), and redeemers.
    • Script content can be stored once and retrieved by hash to avoid duplication.
  • Chores

    • Database schema and migrations expanded to support new witness/script models and deduplicated script content.
    • Transaction persistence for witness/script data is idempotent: existing related records are replaced when reprocessing.

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

Signed-off-by: Jenita <jkawan@blinklabs.io>
@jkawan jkawan requested a review from a team as a code owner November 17, 2025 23:58
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 17, 2025

📝 Walkthrough

Walkthrough

Adds GORM models to persist transaction witness data: KeyWitness, WitnessScripts, Redeemer, PlutusData, and Script (script content) with TableName methods and appropriate gorm tags (primaryKey, indices, unique on script hash). Registers these models in MigrateModels and extends Transaction with associations for KeyWitnesses, WitnessScripts, Redeemers, and PlutusData. Updates SQLite persistence logic (database/plugin/metadata/sqlite/transaction.go) to ensure a transaction ID after upsert, delete existing associated witness/script/redeemer/plutus_data rows when present, and insert vkey/bootstrap witnesses, native and Plutus scripts (and Script content), datums (PlutusData), and redeemers (including ExUnits). Adds MetadataStore/GetScript and its sqlite implementation to fetch Script by hash. Adds workflow timeout-minutes: 10 to nilaway job.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect database/plugin/metadata/sqlite/transaction.go for:
    • Correct post-upsert ID retrieval and branches when tmpTx.ID == 0.
    • Idempotent delete-then-insert handling for KeyWitness, WitnessScripts, Redeemer, and PlutusData.
    • Correct creation and uniqueness handling of Script (script content) rows and linking from WitnessScripts.
    • Proper mapping of CBOR/raw bytes and ExUnits to model fields and size/type suitability.
    • Use of txn vs. d.DB() and clear contextual error messages.
  • Review all new model structs for correct GORM tags and TableName methods, and verify MigrateModels includes each new type.
  • Verify MetadataStore interface change (GetScript) matches implementation and call sites.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning All changes are directly related to tracking witness sets; however, the workflow file modification adding a timeout to the nilaway job appears unrelated to the primary objective. Remove the unrelated nilaway.yml timeout change or provide justification for its inclusion in this witness set tracking pull request.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: track witness set for transaction' accurately and concisely describes the main changes, which add database models and logic to track and store transaction witness sets.
Linked Issues check ✅ Passed The pull request fully implements the requirement from #874 to track and store transaction witness sets, adding comprehensive database models and persistence logic for witnesses, scripts, redeemers, and plutus data.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-track-witnessset

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3260f67 and d8edaf6.

📒 Files selected for processing (1)
  • .github/workflows/nilaway.yml (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
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.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: go-test (1.24.x, macos-latest)
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: nilaway
  • GitHub Check: lint
  • GitHub Check: Analyze (go)
🔇 Additional comments (1)
.github/workflows/nilaway.yml (1)

17-17: Reasonable timeout increase for expanded static analysis surface.

The addition of multiple new database models and associated logic (KeyWitness, WitnessScripts, Redeemer, PlutusData, Script) increases the code surface that nilaway must analyze. A timeout of 10 minutes is a sensible buffer for the static analysis pass on the expanded codebase.


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: 0

🧹 Nitpick comments (2)
database/models/witness.go (1)

29-29: Consider using typed enum for stronger type safety.

The Type field uses uint8 for GORM compatibility, but using WitnessType directly would provide compile-time type safety while GORM still stores it as uint8 under the hood.

Apply this diff if you want stronger typing:

-	Type          uint8  `gorm:"index"` // WitnessType (0=Vkey, 1=Bootstrap)
+	Type          WitnessType  `gorm:"index"`

The same suggestion applies to Script.Type and Redeemer.Tag in their respective files.

database/models/redeemer.go (1)

32-34: Consider adding compound index for redeemer lookups.

Queries that filter redeemers by tag and index within a transaction would benefit from a compound index. The combination (TransactionID, Tag, Index) is likely a common query pattern.

Apply this diff to add a compound index:

 type Redeemer struct {
 	ID            uint   `gorm:"primaryKey"`
-	TransactionID uint   `gorm:"index"`
-	Tag           uint8  `gorm:"index"` // RedeemerTag
-	Index         uint32 `gorm:"index"`
+	TransactionID uint   `gorm:"index;index:idx_redeemer_lookup,priority:1"`
+	Tag           uint8  `gorm:"index:idx_redeemer_lookup,priority:2"` // RedeemerTag
+	Index         uint32 `gorm:"index:idx_redeemer_lookup,priority:3"`
 	Data          []byte `gorm:"type:bytea"` // Plutus data

This maintains the individual indexes while adding a compound index for efficient lookup by (TransactionID, Tag, Index).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e39788 and eb6ab54.

📒 Files selected for processing (6)
  • database/models/datum.go (1 hunks)
  • database/models/models.go (1 hunks)
  • database/models/redeemer.go (1 hunks)
  • database/models/script.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/witness.go (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
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.
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

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

Applied to files:

  • database/models/transaction.go
🧬 Code graph analysis (6)
database/models/witness.go (1)
database/models/transaction.go (2)
  • Transaction (18-34)
  • Transaction (36-38)
database/models/transaction.go (5)
database/models/utxo.go (1)
  • Utxo (23-38)
database/models/witness.go (2)
  • Witness (26-36)
  • Witness (38-40)
database/models/script.go (2)
  • Script (28-34)
  • Script (36-38)
database/models/redeemer.go (2)
  • Redeemer (30-39)
  • Redeemer (41-43)
database/models/datum.go (2)
  • PlutusData (29-34)
  • PlutusData (36-38)
database/models/datum.go (1)
database/models/transaction.go (2)
  • Transaction (18-34)
  • Transaction (36-38)
database/models/script.go (1)
database/models/transaction.go (2)
  • Transaction (18-34)
  • Transaction (36-38)
database/models/redeemer.go (1)
database/models/transaction.go (2)
  • Transaction (18-34)
  • Transaction (36-38)
database/models/models.go (4)
database/models/witness.go (2)
  • Witness (26-36)
  • Witness (38-40)
database/models/script.go (2)
  • Script (28-34)
  • Script (36-38)
database/models/redeemer.go (2)
  • Redeemer (30-39)
  • Redeemer (41-43)
database/models/datum.go (2)
  • PlutusData (29-34)
  • PlutusData (36-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). (2)
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (6)
database/models/transaction.go (1)

26-29: LGTM! Witness set relationships properly defined.

The foreign key relationships for witness set components correctly use TransactionID→ID, which is appropriate since these entities are owned by the transaction (unlike inputs which reference by hash). The pattern is consistent across all four new relationships and aligns with the child model definitions.

database/models/models.go (1)

51-54: LGTM! All new models registered for migration.

All four witness set models are correctly added to the migration list, ensuring database schema updates will include the new tables.

database/models/datum.go (1)

28-38: LGTM! PlutusData model correctly structured.

The model follows GORM conventions with proper primary key, indexed foreign key, and bytea type for binary data. The back-reference to Transaction enables bidirectional navigation.

database/models/script.go (1)

20-25: Based on my verification, the ScriptType enum values in the reviewed code appear to align with Cardano's ledger implementation. The cardano-ledger Language type uses toCBOR = toCBOR . fromEnum and fromCBOR = toEnum . fromIntegral <$> decodeWord64, indicating CBOR encoding uses enum ordinal values. The PlutusScript encoding shows PlutusV1 at CBOR value 1, and the pattern continues sequentially. The values specified in the review (Native=0, PlutusV1=1, PlutusV2=2, PlutusV3=3) follow the expected sequential encoding consistent with Cardano's script type serialization.

database/models/witness.go (1)

20-23: Enum values are correctly aligned with Cardano specification.

Verification confirms that WitnessTypeVkey = 0 and WitnessTypeBootstrap = 1 match the Cardano witness type specification. Vkey (type 0) represents the classic Shelley-era Ed25519 witness, and Bootstrap (type 1) represents the legacy Byron-era witness structure. The enum definition in database/models/witness.go is correct, and the struct comment accurately documents this mapping.

database/models/redeemer.go (1)

20-27: RedeemerTag enum values verified as correct against Cardano specification.

The enum values (0—Spend, 1—Mint, 2—Cert, 3—Reward, 4—Voting, 5—Proposing) precisely match the Cardano CDDL specification and ledger types. The Conway-era governance tags (Voting=4, Proposing=5) are properly defined. The direct type cast to cardano.RedeemerPurpose in utxorpc/submit.go line 217 is semantically correct because the local enum values align with the utxorpc library's standards. The implementation is consistent across the codebase (database models, conway.go, submit.go) with no type conversion issues.

Copy link
Contributor

@agaffney agaffney left a comment

Choose a reason for hiding this comment

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

There's nothing inserting data into these tables

RedeemerTagReward RedeemerTag = 3
RedeemerTagVoting RedeemerTag = 4
RedeemerTagProposing RedeemerTag = 5
)
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 reuse the constants from gouroboros/ledger/common

Signed-off-by: Jenita <jkawan@blinklabs.io>
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)
database/models/witness.go (1)

17-33: KeyWitness model looks good; consider typed constants for Type

Struct layout and GORM tags look consistent with the rest of the models. To avoid magic numbers for Type in both the model and the SQLite persistence code, consider adding typed constants here and reusing them at call sites.

 package models
 
-// KeyWitness represents a key witness entry (Vkey or Bootstrap)
-// Type: 0 = VkeyWitness, 1 = BootstrapWitness
+// KeyWitnessType values identify the concrete witness encoding.
+const (
+	KeyWitnessTypeVkey      uint8 = 0
+	KeyWitnessTypeBootstrap uint8 = 1
+)
+
+// KeyWitness represents a key witness entry (Vkey or Bootstrap).
+// See KeyWitnessType* constants for Type values.
 type KeyWitness struct {
@@
 	TransactionID uint   `gorm:"index"`
-	Type          uint8  `gorm:"index"` // 0=Vkey, 1=Bootstrap
+	Type          uint8  `gorm:"index"` // KeyWitnessTypeVkey / KeyWitnessTypeBootstrap

Then in database/plugin/metadata/sqlite/transaction.go, you can reference models.KeyWitnessTypeVkey / models.KeyWitnessTypeBootstrap instead of raw 0 / 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 eb6ab54 and 6d6530d.

📒 Files selected for processing (6)
  • database/models/models.go (1 hunks)
  • database/models/redeemer.go (1 hunks)
  • database/models/script.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/witness.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • database/models/models.go
  • database/models/script.go
  • database/models/redeemer.go
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
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.
📚 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/models/witness.go
  • database/models/transaction.go
📚 Learning: 2025-10-26T14:12:53.587Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 977
File: database/plugin/metadata/sqlite/transaction.go:71-81
Timestamp: 2025-10-26T14:12:53.587Z
Learning: In Babbage-era transactions (gouroboros ledger), the Produced() method behavior depends on transaction validity: for valid transactions (IsValid() == true), Produced() returns all transaction outputs; for invalid transactions (IsValid() == false), Produced() returns only the collateral return UTXO (if non-nil) with index len(t.Outputs()). The collateral return Output is set to t.CollateralReturn() in the returned Utxo struct.

Applied to files:

  • database/models/transaction.go
🧬 Code graph analysis (3)
database/plugin/metadata/sqlite/transaction.go (4)
database/models/witness.go (2)
  • KeyWitness (19-29)
  • KeyWitness (31-33)
database/models/script.go (2)
  • Script (23-29)
  • Script (31-33)
database/models/datum.go (2)
  • PlutusData (29-34)
  • PlutusData (36-38)
database/models/redeemer.go (2)
  • Redeemer (18-27)
  • Redeemer (29-31)
database/models/witness.go (1)
database/models/transaction.go (2)
  • Transaction (18-34)
  • Transaction (36-38)
database/models/transaction.go (4)
database/models/witness.go (2)
  • KeyWitness (19-29)
  • KeyWitness (31-33)
database/models/script.go (2)
  • Script (23-29)
  • Script (31-33)
database/models/redeemer.go (2)
  • Redeemer (18-27)
  • Redeemer (29-31)
database/models/datum.go (2)
  • PlutusData (29-34)
  • PlutusData (36-38)
🔇 Additional comments (1)
database/models/transaction.go (1)

19-30: Witness/script relationships on Transaction are consistent with existing FK pattern

The added KeyWitnesses, Scripts, Redeemers, and PlutusData fields use foreignKey:TransactionID;references:ID, matching the established pattern used for Outputs and other associations, so they should preload and join cleanly with GORM. Based on learnings.

Signed-off-by: Jenita <jkawan@blinklabs.io>
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 6d6530d and c3fa7a0.

📒 Files selected for processing (1)
  • database/plugin/metadata/sqlite/transaction.go (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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
🧬 Code graph analysis (1)
database/plugin/metadata/sqlite/transaction.go (4)
database/models/witness.go (2)
  • KeyWitness (19-29)
  • KeyWitness (31-33)
database/models/script.go (2)
  • Script (23-29)
  • Script (31-33)
database/models/redeemer.go (2)
  • Redeemer (18-27)
  • Redeemer (29-31)
database/models/datum.go (2)
  • PlutusData (29-34)
  • PlutusData (36-38)
🪛 GitHub Actions: golangci-lint
database/plugin/metadata/sqlite/transaction.go

[error] 416-416: golangci-lint: G115 integer overflow conversion int64 -> uint64 (gosec)

⏰ 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: Analyze (go)
  • GitHub Check: nilaway
🔇 Additional comments (5)
database/plugin/metadata/sqlite/transaction.go (5)

92-102: Good fix for ID population on upsert conflict path.

This correctly addresses the concern from the previous review about tmpTx.ID remaining zero when the ON CONFLICT path is taken. By explicitly fetching the transaction ID when needed, you ensure all subsequent witness records will have a valid TransactionID.


293-306: Idempotency fix looks good.

This correctly addresses the concern from the previous review about duplicate witness records on retry. The delete-before-insert pattern ensures that re-running SetTransaction for the same transaction hash will replace rather than accumulate witness data.

The check if tmpTx.ID != 0 is defensive programming—after lines 92-102, the ID should always be non-zero or the function would have returned with an error. This safeguard is reasonable.


311-336: LGTM: Vkey and Bootstrap witness insertion.

The mapping is correct: Type 0 for Vkey witnesses (with Vkey and Signature fields) and Type 1 for Bootstrap witnesses (with PublicKey, Signature, ChainCode, and Attributes). Error handling is properly implemented.


339-384: LGTM: Script persistence across all types.

The implementation correctly handles Native, PlutusV1, PlutusV2, and PlutusV3 scripts with appropriate Type values from lcommon.ScriptRefType* constants. The distinction between CBOR encoding for native scripts and raw bytes for Plutus scripts is properly maintained.


387-395: LGTM: PlutusData persistence.

The datum persistence correctly stores CBOR-encoded PlutusData with proper error handling.

Signed-off-by: Jenita <jkawan@blinklabs.io>
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 (2)
database/plugin/metadata/sqlite/transaction.go (2)

397-412: Redeemer persistence addresses past concern, but consider explicit validation.

The max(0, ...) approach on lines 405-406 prevents the integer overflow issue flagged in the previous review by clamping negative ExUnits values to zero. The //nolint:gosec comments suppress the linter warnings.

However, silently clamping negative values could mask data corruption or upstream bugs. Consider whether explicit validation with an error return would be more appropriate:

if value.ExUnits.Memory < 0 || value.ExUnits.Steps < 0 {
    return fmt.Errorf("invalid ExUnits for redeemer tag=%d index=%d: Memory=%d, Steps=%d", 
        key.Tag, key.Index, value.ExUnits.Memory, value.ExUnits.Steps)
}
redeemer := models.Redeemer{
    TransactionID: tmpTx.ID,
    Tag:           uint8(key.Tag),
    Index:         key.Index,
    Data:          value.Data.Cbor(),
    ExUnitsMemory: uint64(value.ExUnits.Memory),
    ExUnitsCPU:    uint64(value.ExUnits.Steps),
}

The current implementation is acceptable if negative values are expected to be impossible or if silent clamping is the intended behavior for robustness.


307-413: Consider batch inserts for witness data (optional optimization).

The current implementation inserts witness records one at a time in loops. While this is functionally correct and maintains good error handling, consider using GORM's CreateInBatches for better performance when dealing with transactions that have many witnesses:

// Example for Vkey witnesses
vkeyWitnesses := make([]models.KeyWitness, 0, len(ws.Vkey()))
for _, vkey := range ws.Vkey() {
    vkeyWitnesses = append(vkeyWitnesses, models.KeyWitness{
        TransactionID: tmpTx.ID,
        Type:          0,
        Vkey:          vkey.Vkey,
        Signature:     vkey.Signature,
    })
}
if len(vkeyWitnesses) > 0 {
    if result := txn.CreateInBatches(vkeyWitnesses, 100); result.Error != nil {
        return fmt.Errorf("create vkey witnesses: %w", result.Error)
    }
}

This optimization is not essential but could improve performance for transactions with large witness sets.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3fa7a0 and dab5d82.

📒 Files selected for processing (1)
  • database/plugin/metadata/sqlite/transaction.go (2 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
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.
📚 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
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

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

Applied to files:

  • database/plugin/metadata/sqlite/transaction.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
🧬 Code graph analysis (1)
database/plugin/metadata/sqlite/transaction.go (4)
database/models/witness.go (2)
  • KeyWitness (19-29)
  • KeyWitness (31-33)
database/models/script.go (2)
  • Script (23-29)
  • Script (31-33)
database/models/redeemer.go (2)
  • Redeemer (18-27)
  • Redeemer (29-31)
database/models/datum.go (2)
  • PlutusData (29-34)
  • PlutusData (36-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). (2)
  • GitHub Check: Analyze (go)
  • GitHub Check: nilaway
🔇 Additional comments (5)
database/plugin/metadata/sqlite/transaction.go (5)

92-102: Excellent fix for ID population after upsert!

This correctly addresses the concern from the previous review. When SQLite's ON CONFLICT DO UPDATE path is taken, GORM may not populate tmpTx.ID. Fetching by hash ensures the ID is always available for subsequent witness record inserts.


291-306: Idempotency correctly implemented!

This addresses the concern from the previous review about duplicate witness rows on retry. Deleting existing child records before re-inserting ensures that SetTransaction is idempotent, even after partial failures or re-execution for the same transaction hash.


310-336: LGTM: Vkey and Bootstrap witness persistence.

The mapping from ledger witness types to KeyWitness model records is correct. The Type discriminator (0 for Vkey, 1 for Bootstrap) and field assignments align with the model definition. Error handling is appropriate.


338-384: LGTM: Script persistence for all script types.

The persistence logic correctly handles Native, PlutusV1, PlutusV2, and PlutusV3 scripts with appropriate Type discriminators from lcommon constants. The distinction between .Cbor() for native scripts and direct byte usage for Plutus scripts aligns with the witness set interface. Error handling is comprehensive.


386-395: LGTM: PlutusData/Datums persistence.

The mapping from witness set datums to PlutusData model records is correct. Field assignments and error handling are appropriate.

TransactionID uint `gorm:"index"`
Tag uint8 `gorm:"index"` // Redeemer tag
Index uint32 `gorm:"index"`
Data []byte `gorm:"type:bytea"` // Plutus data
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a Postgres data type, which is not supported by SQLite. We should let GORM determine the correct type to use under the hood where it can.

ID uint `gorm:"primaryKey"`
TransactionID uint `gorm:"index"`
Type uint8 `gorm:"index"` // Script type
ScriptData []byte `gorm:"type:bytea"`
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a Postgres data type, which is not supported by SQLite. We should let GORM determine the correct type to use under the hood where it can.

if result := txn.Where("transaction_id = ?", tmpTx.ID).Delete(&models.PlutusData{}); result.Error != nil {
return fmt.Errorf("delete existing plutus data: %w", result.Error)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of deleting existing records, we should probably use ON CONFLICT clauses to update the existing record(s)

}
}
if tx.Witnesses() != nil {
ws := tx.Witnesses()
Copy link
Contributor

Choose a reason for hiding this comment

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

You can save a repeated function call by doing:

if ws := tx.Witnesses(); ws != nil {

for _, vkey := range ws.Vkey() {
keyWitness := models.KeyWitness{
TransactionID: tmpTx.ID,
Type: 0, // VkeyWitness
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 define our own constants for the "key witness" type, since there aren't any provided by gOuroboros.

for _, bootstrap := range ws.Bootstrap() {
keyWitness := models.KeyWitness{
TransactionID: tmpTx.ID,
Type: 1, // BootstrapWitness
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 define our own constants for the "key witness" type, since there aren't any provided by gOuroboros.

Type uint8 `gorm:"index"` // Script type
ScriptData []byte `gorm:"type:bytea"`
Transaction *Transaction
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Storing the entire script for each transaction that it appears on is going to kill us for PlutusV1 scripts where the same script content was included in the TX witness set of literally thousands of TXs. We don't want to store all of those copies in the database.

We do want a way to know which script hash was provided in the witness set so we can look up the content, but we don't need a direct link in the DB between a particular TX and the script content

package models

// KeyWitness represents a key witness entry (Vkey or Bootstrap)
// Type: 0 = VkeyWitness, 1 = BootstrapWitness
Copy link
Contributor

Choose a reason for hiding this comment

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

Create constants for these

@wolf31o2
Copy link
Member

I've added the first 200 chunks of immutable DB data from the Haskell cardano-node to the code. You can access it in database/immutable/testdata and there's even a make test-load which will import it using "dingo load" mechanics "go run ./cmd/dingo load database/immutable/testdata" which is useful for testing. This is the first 40k blocks of the preview network from genesis and it takes under a minute to load.

Signed-off-by: Jenita <jkawan@blinklabs.io>
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/models/models.go (1)

29-43: New models correctly registered in migrations; consider restoring strict alpha order

KeyWitness, PlutusData, Redeemer, Script, and ScriptContent are all properly added to MigrateModels, so schema migrations will see the new tables. The only nit is that the slice is no longer strictly alphabetical (e.g., Redeemer comes after Registration*). If you still want to keep this list sorted as per earlier feedback, a small reorder would address it, but it’s not functionally required.

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

17-30: ScriptContent layout matches deduplication goal

Hash+unique plus a separate Script table that stores only hashes is a good shape for avoiding duplicate script payloads, and CreatedSlot provides a clear “first seen” marker. If you later need to query scripts by slot range, consider adding gorm:"index" on CreatedSlot, but it’s fine to defer until there’s a concrete use case.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dab5d82 and 6f5b11b.

📒 Files selected for processing (8)
  • database/models/models.go (1 hunks)
  • database/models/redeemer.go (1 hunks)
  • database/models/script.go (1 hunks)
  • database/models/script_content.go (1 hunks)
  • database/models/witness.go (1 hunks)
  • database/plugin/metadata/sqlite/script.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (2 hunks)
  • database/plugin/metadata/store.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • database/models/redeemer.go
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
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.
📚 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/models/script.go
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

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

Applied to files:

  • database/plugin/metadata/sqlite/transaction.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
🧬 Code graph analysis (5)
database/plugin/metadata/sqlite/script.go (2)
database/models/script_content.go (2)
  • ScriptContent (20-26)
  • ScriptContent (28-30)
database/models/script.go (2)
  • Script (27-33)
  • Script (35-37)
database/models/models.go (5)
database/models/witness.go (2)
  • KeyWitness (26-36)
  • KeyWitness (38-40)
database/models/datum.go (2)
  • PlutusData (29-34)
  • PlutusData (36-38)
database/models/redeemer.go (2)
  • Redeemer (18-27)
  • Redeemer (29-31)
database/models/script.go (2)
  • Script (27-33)
  • Script (35-37)
database/models/script_content.go (2)
  • ScriptContent (20-26)
  • ScriptContent (28-30)
database/models/script.go (1)
database/models/transaction.go (2)
  • Transaction (18-34)
  • Transaction (36-38)
database/plugin/metadata/store.go (2)
database/models/script_content.go (2)
  • ScriptContent (20-26)
  • ScriptContent (28-30)
database/models/script.go (2)
  • Script (27-33)
  • Script (35-37)
database/models/witness.go (1)
database/models/transaction.go (2)
  • Transaction (18-34)
  • Transaction (36-38)
🪛 GitHub Actions: golangci-lint
database/plugin/metadata/sqlite/transaction.go

[error] 379-379: golangci-lint: unnecessary leading newline (whitespace) in transaction.go:379

🪛 GitHub Check: lint
database/plugin/metadata/sqlite/transaction.go

[failure] 379-379:
unnecessary leading newline (whitespace)

⏰ 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: cubic · AI code reviewer
  • GitHub Check: nilaway
  • GitHub Check: go-test (1.24.x, macos-latest)
  • GitHub Check: Analyze (go)
🔇 Additional comments (5)
database/models/witness.go (1)

17-40: KeyWitness model and type constants look solid

The enum-style Type plus separate fields for vkey vs bootstrap data cleanly model the two witness kinds, and the indexing on TransactionID/Type matches how SetTransaction persists them. No changes needed here.

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

82-93: Script-related MetadataStore extensions are consistent and minimal

The GetScriptContent and GetScriptsByTransaction methods fit the existing API style (hash/ID + optional txn) and line up with the sqlite implementation signatures. Looks good.

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

25-58: Script lookup helpers for SQLite are straightforward and consistent

Both GetScriptContent and GetScriptsByTransaction follow the existing nil-txn fallback and error-handling patterns, and they query on the expected keys (hash and transaction_id). Returning (nil, nil) on not-found for content is also consistent with other getters.

database/models/script.go (1)

17-37: Script-as-hash reference aligns well with ScriptContent deduplication

Modeling Script as just (transaction_id, type, script_hash) with comments tying Type to the ScriptRefType* values keeps per-transaction rows lean and delegates payload storage to ScriptContent as intended. This looks good.

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

92-104: Upsert ID fix correctly guarantees a valid TransactionID for child records

The explicit GetTransactionByHash fallback when tmpTx.ID == 0 closes the gap where SQLite’s ON CONFLICT path wouldn’t populate the ID, so all subsequent witness rows now get a non-zero TransactionID. This addresses the earlier orphaning risk; the logic here looks good.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 10 files

Signed-off-by: Jenita <jkawan@blinklabs.io>
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 (2)
database/plugin/metadata/sqlite/transaction.go (2)

295-308: Consider using ON CONFLICT clauses instead of deleting witness records.

The delete-before-insert pattern achieves idempotency but requires extra DELETE operations. Using ON CONFLICT DO UPDATE (or DO NOTHING where appropriate) on the witness tables would be more database-idiomatic and potentially more efficient, as it would only touch rows that actually changed.

This would require adding appropriate unique constraints to the witness tables (e.g., (transaction_id, type, vkey) for KeyWitness, (transaction_id, tag, index) for Redeemer, etc.).

Based on learnings from previous reviews, consider whether this aligns with the codebase's approach to GORM associations and foreign keys.


309-310: Combine witness retrieval with nil check.

You can make this more concise by combining the assignment and condition check:

-	ws := tx.Witnesses()
-	if ws != nil {
+	if ws := tx.Witnesses(); ws != nil {

This is a common Go idiom and avoids declaring ws in the broader scope.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f5b11b and 04852d0.

📒 Files selected for processing (1)
  • database/plugin/metadata/sqlite/transaction.go (2 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
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.
📚 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
📚 Learning: 2025-10-22T20:13:35.972Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 971
File: utxorpc/sync_test.go:339-339
Timestamp: 2025-10-22T20:13:35.972Z
Learning: In utxorpc code paths, always operate on github.com/blinklabs-io/gouroboros/ledger/common.Block. Use database/models.Block only as the storage type and call models.Block.Decode() to obtain a ledger/common.Block before further processing.

Applied to files:

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

Applied to files:

  • database/plugin/metadata/sqlite/transaction.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
⏰ 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: cubic · AI code reviewer
  • GitHub Check: nilaway
  • GitHub Check: go-test (1.24.x, macos-latest)
  • GitHub Check: Analyze (go)
🔇 Additional comments (2)
database/plugin/metadata/sqlite/transaction.go (2)

92-104: ID lookup after upsert is correctly implemented.

This block properly addresses the previously flagged critical issue where tmpTx.ID would remain zero on the ON CONFLICT DO UPDATE path. The explicit fetch by hash ensures witness records can always be associated with the correct transaction.

The comment clearly explains the SQLite/GORM behavior. @agaffney, regarding your question about this code block: when the ON CONFLICT DO UPDATE path executes (transaction hash already exists), SQLite doesn't return the existing row's ID in the RETURNING clause. This forces us to explicitly query for the ID before inserting witness records, otherwise TransactionID would be zero and orphan the witness data.


312-470: KeyWitnessType constants confirmed and code is correct.

The constants models.KeyWitnessTypeVkey and models.KeyWitnessTypeBootstrap are properly defined in database/models/witness.go (lines 19 and 21). The witness insertion logic is sound: all witness types, script types, plutus data, and redeemers are handled correctly with proper error context, script deduplication via ON CONFLICT DO NOTHING is implemented, and ExUnits overflow protection with max(0, ...) is in place.

type PlutusData struct {
ID uint `gorm:"primaryKey"`
TransactionID uint `gorm:"index"`
Data []byte `gorm:"type:bytea"`
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove the GORM field type override. This is a Postgres column type that won't work in SQLite

// To avoid storing duplicate script data for the same script used in multiple
// transactions, we store only the script hash here. The actual script content
// is stored separately in ScriptContent table, indexed by hash.
type Script struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename this to WitnessScripts to be a bit more specific about its function (mapping TX ID to script hash when the script appears in the TX witness)

// ScriptContent represents the content of a script, indexed by its hash
// This avoids storing duplicate script data when the same script appears
// in multiple transactions
type ScriptContent struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename this to Script

Collateral []Utxo `gorm:"foreignKey:CollateralByTxId;references:Hash"`
CollateralReturn *Utxo `gorm:"foreignKey:TransactionID;references:ID"`
KeyWitnesses []KeyWitness `gorm:"foreignKey:TransactionID;references:ID"`
Scripts []Script `gorm:"foreignKey:TransactionID;references:ID"`
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename to WitnessScripts

// KeyWitnessTypeVkey represents a Vkey witness
KeyWitnessTypeVkey uint8 = iota
// KeyWitnessTypeBootstrap represents a Bootstrap witness
KeyWitnessTypeBootstrap
Copy link
Contributor

Choose a reason for hiding this comment

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

Be explicit with the values here. It's generally a bad idea to use iota in cases where the value can persist between older and newer versions of the code in case values are added/removed.

)

// GetScriptContent returns the script content by its hash
func (d *MetadataStoreSqlite) GetScriptContent(
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename this to GetScript

}

// GetScriptsByTransaction returns all scripts (references) used in a particular transaction
func (d *MetadataStoreSqlite) GetScriptsByTransaction(
Copy link
Contributor

Choose a reason for hiding this comment

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

GetScriptWitnessesByTransaction

But is this even needed? We probably don't need to query script witnesses for a particular TX without querying for the entire TX, which will include that data.

}
// Extract and save witness set data
// Delete existing witness records to ensure idempotency on retry
if tmpTx.ID != 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this condition. We assign the ID above when it's 0 and throw an error otherwise, so this check will always succeed.

if result := txn.Where("transaction_id = ?", tmpTx.ID).Delete(&models.KeyWitness{}); result.Error != nil {
return fmt.Errorf("delete existing key witnesses: %w", result.Error)
}
if result := txn.Where("transaction_id = ?", tmpTx.ID).Delete(&models.Script{}); result.Error != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

ScriptWitness

}).Create(&scriptContent); result.Error != nil {
return fmt.Errorf("create plutus v3 script content: %w", result.Error)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

You could de-duplicate the above 4 blocks by processing them all at once:

scripts := []lcommon.Script{
  ws.NativeScripts(),
  ws.PlutusV1Scripts(),
  ...
}
for _, script := range scripts {
  ...
  var scriptType int
  switch script.(type) {
  case lcommon.NativeScript:
    scriptType = lcommon.ScriptTypeNative
  ...
  }

Signed-off-by: Jenita <jkawan@blinklabs.io>
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/models/models.go (1)

29-55: Keep MigrateModels alphabetized for easier maintenance

The new Redeemer entry breaks the otherwise alphabetical ordering in this slice. To keep things consistent with earlier feedback, consider reordering the relevant lines like:

-	&PParams{},
-	&PParamUpdate{},
-	&PlutusData{},
-	&Registration{},
-	&RegistrationDrep{},
-	&Redeemer{},
+	&PParams{},
+	&PParamUpdate{},
+	&PlutusData{},
+	&Redeemer{},
+	&Registration{},
+	&RegistrationDrep{},

This keeps the list sorted and makes future merges and scans simpler.

🧹 Nitpick comments (2)
database/models/script.go (1)

17-37: WitnessScripts model matches intended mapping TX → script hash

The struct and TableName align with how the witness scripts are persisted and queried, and the comment clearly ties Type to the ScriptRefType constants.

If you expect frequent lookups or integrity checks by (transaction_id, script_hash), you might consider adding a composite index or unique index on those columns later, but the current shape is perfectly fine to ship.

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

92-104: Robust ID resolution after transaction upsert

The post-ON CONFLICT fallback to GetTransactionByHash when tmpTx.ID == 0 closes the gap where witness records could be written with a zero TransactionID. This makes the subsequent witness inserts safe even on the conflict path.

If you ever see this path hit frequently, you might consider a lighter-weight helper that just selects id by hash (without Preload(clause.Associations)), but functionally this is solid.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04852d0 and 3260f67.

📒 Files selected for processing (9)
  • database/models/datum.go (1 hunks)
  • database/models/models.go (2 hunks)
  • database/models/script.go (1 hunks)
  • database/models/script_content.go (1 hunks)
  • database/models/transaction.go (1 hunks)
  • database/models/witness.go (1 hunks)
  • database/plugin/metadata/sqlite/script.go (1 hunks)
  • database/plugin/metadata/sqlite/transaction.go (2 hunks)
  • database/plugin/metadata/store.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • database/plugin/metadata/store.go
  • database/models/witness.go
  • database/models/script_content.go
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
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.
📚 Learning: 2025-10-24T21:56:15.978Z
Learnt from: wolf31o2
Repo: blinklabs-io/dingo PR: 975
File: database/models/transaction.go:21-26
Timestamp: 2025-10-24T21:56:15.978Z
Learning: In database/models/transaction.go, the Transaction struct uses a dual foreign-key pattern for UTXO associations: Outputs []Utxo uses TransactionID→ID (where UTXOs were created), and Inputs []Utxo uses SpentAtTxId→Hash (where UTXOs were consumed). Explicit GORM tags `gorm:"foreignKey:SpentAtTxId;references:Hash"` for Inputs and `gorm:"foreignKey:TransactionID;references:ID"` for Outputs prevent association conflicts and preserve UTXO provenance.

Applied to files:

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

Applied to files:

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

Applied to files:

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

Applied to files:

  • database/plugin/metadata/sqlite/transaction.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
🧬 Code graph analysis (4)
database/models/script.go (1)
database/models/transaction.go (2)
  • Transaction (18-34)
  • Transaction (36-38)
database/models/datum.go (1)
database/models/transaction.go (2)
  • Transaction (18-34)
  • Transaction (36-38)
database/plugin/metadata/sqlite/script.go (2)
database/plugin/metadata/sqlite/database.go (1)
  • MetadataStoreSqlite (36-42)
database/models/script_content.go (2)
  • Script (20-26)
  • Script (28-30)
database/models/models.go (5)
database/models/witness.go (2)
  • KeyWitness (26-36)
  • KeyWitness (38-40)
database/models/datum.go (2)
  • PlutusData (29-34)
  • PlutusData (36-38)
database/models/redeemer.go (2)
  • Redeemer (18-27)
  • Redeemer (29-31)
database/models/script_content.go (2)
  • Script (20-26)
  • Script (28-30)
database/models/script.go (2)
  • WitnessScripts (27-33)
  • WitnessScripts (35-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: go-test (1.24.x, ubuntu-latest)
  • GitHub Check: go-test (1.24.x, macos-latest)
  • GitHub Check: lint
  • GitHub Check: nilaway
  • GitHub Check: Analyze (go)
🔇 Additional comments (4)
database/plugin/metadata/sqlite/script.go (1)

25-41: GetScript lookup and not-found semantics look good

The method cleanly handles optional transactions, correctly queries by the byte hash, and uses (nil, nil) for “not found,” which matches the pattern used by other getters in this package.

database/models/datum.go (1)

28-38: PlutusData schema is consistent with existing transaction child tables

The PlutusData model correctly mirrors the existing FK pattern (TransactionID → Transaction.ID) and avoids driver-specific type overrides, which keeps it compatible with SQLite.

database/models/transaction.go (1)

19-33: Witness-related associations on Transaction are wired correctly

The new KeyWitnesses, WitnessScripts, Redeemers, and PlutusData fields follow the established pattern of using TransactionID → ID for child tables and align with the way SetTransaction populates those records.

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

293-435: Witness, script, datum, and redeemer persistence is coherent and idempotent

The pattern of deleting existing KeyWitness, WitnessScripts, Redeemer, and PlutusData rows by transaction_id and then repopulating from tx.Witnesses() gives you a clean, idempotent mapping for retries and re-runs.

The processScripts helper nicely unifies the four script-type loops, correctly sets WitnessScripts.Type using ScriptRefType* constants, and de-duplicates script content with ON CONFLICT (hash) DO NOTHING. ExUnits clamping with max(0, …) before casting to uint64 also addresses the previous overflow concern.

Overall, this block looks ready to ship.

@jkawan jkawan force-pushed the feat-track-witnessset branch from f776f13 to 9dff05d Compare November 25, 2025 00:22
Jenita added 2 commits November 24, 2025 18:23
Signed-off-by: Jenita <jkawan@blinklabs.io>
Signed-off-by: Jenita <jkawan@blinklabs.io>
@jkawan jkawan force-pushed the feat-track-witnessset branch from 9dff05d to d351aa8 Compare November 25, 2025 00:23
Signed-off-by: Jenita <jkawan@blinklabs.io>
@wolf31o2 wolf31o2 requested a review from agaffney November 25, 2025 13:42
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 witness set

4 participants