Skip to content
12 changes: 12 additions & 0 deletions database/models/datum.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,15 @@ type Datum struct {
func (Datum) TableName() string {
return "datum"
}

// PlutusData represents a Plutus data value in the witness set
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

Transaction *Transaction
}

func (PlutusData) TableName() string {
return "plutus_data"
}
4 changes: 4 additions & 0 deletions database/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,20 @@ var MigrateModels = []any{
&DeregistrationDrep{},
&Drep{},
&Epoch{},
&KeyWitness{},
&Pool{},
&PoolRegistration{},
&PoolRegistrationOwner{},
&PoolRegistrationRelay{},
&PoolRetirement{},
&PParams{},
&PParamUpdate{},
&PlutusData{},
&Registration{},
&RegistrationDrep{},
&Redeemer{},
&ResignCommitteeCold{},
&Script{},
&StakeDelegation{},
&StakeDeregistration{},
&StakeRegistration{},
Expand Down
31 changes: 31 additions & 0 deletions database/models/redeemer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2025 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package models

// Redeemer represents a redeemer in the witness set
type Redeemer struct {
ID uint `gorm:"primaryKey"`
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.

ExUnitsMemory uint64
ExUnitsCPU uint64
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.

We probably don't want/need this reference back to Transaction when we've already got the TransactionId field

}

func (Redeemer) TableName() string {
return "redeemer"
}
33 changes: 33 additions & 0 deletions database/models/script.go
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 file to witness_script.go

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2025 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package models

// Script represents a script entry in the witness set
// Type corresponds to ScriptRefType constants from gouroboros/ledger/common:
// 0=NativeScript (ScriptRefTypeNativeScript)
// 1=PlutusV1 (ScriptRefTypePlutusV1)
// 2=PlutusV2 (ScriptRefTypePlutusV2)
// 3=PlutusV3 (ScriptRefTypePlutusV3)
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)

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.

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.

We probably don't want/need this reference back to Transaction when we've already got the TransactionId field

}

func (Script) TableName() string {
return "script"
}
20 changes: 12 additions & 8 deletions database/models/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ package models

// Transaction represents a transaction record
type Transaction struct {
Hash []byte `gorm:"uniqueIndex"`
BlockHash []byte `gorm:"index"`
Inputs []Utxo `gorm:"foreignKey:SpentAtTxId;references:Hash"`
Outputs []Utxo `gorm:"foreignKey:TransactionID;references:ID"`
ReferenceInputs []Utxo `gorm:"foreignKey:ReferencedByTxId;references:Hash"`
Collateral []Utxo `gorm:"foreignKey:CollateralByTxId;references:Hash"`
CollateralReturn *Utxo `gorm:"foreignKey:TransactionID;references:ID"`
ID uint `gorm:"primaryKey"`
Hash []byte `gorm:"uniqueIndex"`
BlockHash []byte `gorm:"index"`
Inputs []Utxo `gorm:"foreignKey:SpentAtTxId;references:Hash"`
Outputs []Utxo `gorm:"foreignKey:TransactionID;references:ID"`
ReferenceInputs []Utxo `gorm:"foreignKey:ReferencedByTxId;references:Hash"`
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"`
Redeemers []Redeemer `gorm:"foreignKey:TransactionID;references:ID"`
PlutusData []PlutusData `gorm:"foreignKey:TransactionID;references:ID"`
ID uint `gorm:"primaryKey"`
Type int
BlockIndex uint32
Metadata []byte
Expand Down
33 changes: 33 additions & 0 deletions database/models/witness.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2025 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

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

type KeyWitness struct {
ID uint `gorm:"primaryKey"`
TransactionID uint `gorm:"index"`
Type uint8 `gorm:"index"` // 0=Vkey, 1=Bootstrap
Vkey []byte `gorm:"type:bytea"`
Signature []byte `gorm:"type:bytea"`
PublicKey []byte `gorm:"type:bytea"` // For Bootstrap
ChainCode []byte `gorm:"type:bytea"` // For Bootstrap
Attributes []byte `gorm:"type:bytea"` // For Bootstrap
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.

We probably don't want/need this reference back to Transaction when we've already got the TransactionId field

}

func (KeyWitness) TableName() string {
return "key_witness"
}
109 changes: 109 additions & 0 deletions database/plugin/metadata/sqlite/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,118 @@
return result.Error
}
}
// Extract and save witness set data
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 {


// Add Vkey Witnesses
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.

Vkey: vkey.Vkey,
Signature: vkey.Signature,
}
if result := txn.Create(&keyWitness); result.Error != nil {
return fmt.Errorf("create vkey witness: %w", result.Error)
}
}

// Add Bootstrap Witnesses
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.

PublicKey: bootstrap.PublicKey,
Signature: bootstrap.Signature,
ChainCode: bootstrap.ChainCode,
Attributes: bootstrap.Attributes,
}
if result := txn.Create(&keyWitness); result.Error != nil {
return fmt.Errorf("create bootstrap witness: %w", result.Error)
}
}

// Add Native Scripts
for _, script := range ws.NativeScripts() {
scriptRecord := models.Script{
TransactionID: tmpTx.ID,
Type: uint8(lcommon.ScriptRefTypeNativeScript),
ScriptData: script.Cbor(),
}
if result := txn.Create(&scriptRecord); result.Error != nil {
return fmt.Errorf("create native script: %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.

There's no need for this type switch. All 4 types have the Hash() and RawScriptBytes() functions, so you can simply loop over scripts and call the same code.

for _, script := range s {
  if err := createScriptRecords(script.Hash(), script.RawScriptBytes()); err != nil {
    return err
  }
}

}

// Add PlutusV1 Scripts
for _, script := range ws.PlutusV1Scripts() {
scriptRecord := models.Script{
TransactionID: tmpTx.ID,
Type: uint8(lcommon.ScriptRefTypePlutusV1),
ScriptData: script,
}
if result := txn.Create(&scriptRecord); result.Error != nil {
return fmt.Errorf("create plutus v1 script: %w", result.Error)
}
}

// Add PlutusV2 Scripts
for _, script := range ws.PlutusV2Scripts() {
scriptRecord := models.Script{
TransactionID: tmpTx.ID,
Type: uint8(lcommon.ScriptRefTypePlutusV2),
ScriptData: script,
}
if result := txn.Create(&scriptRecord); result.Error != nil {
return fmt.Errorf("create plutus v2 script: %w", result.Error)
}
}

// Add PlutusV3 Scripts
for _, script := range ws.PlutusV3Scripts() {
scriptRecord := models.Script{
TransactionID: tmpTx.ID,
Type: uint8(lcommon.ScriptRefTypePlutusV3),
ScriptData: script,
}
if result := txn.Create(&scriptRecord); result.Error != nil {
return fmt.Errorf("create plutus v3 script: %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
  ...
  }


// Add PlutusData (Datums)
for _, datum := range ws.PlutusData() {
plutusData := models.PlutusData{
TransactionID: tmpTx.ID,
Data: datum.Cbor(),
}
if result := txn.Create(&plutusData); result.Error != nil {
return fmt.Errorf("create plutus data: %w", result.Error)
}
}

// Add Redeemers
if ws.Redeemers() != nil {
for key, value := range ws.Redeemers().Iter() {
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),
}
if result := txn.Create(&redeemer); result.Error != nil {
return fmt.Errorf("create redeemer: %w", result.Error)
}
}
}
}

// Avoid updating associations
result = txn.Omit(clause.Associations).Save(&tmpTx)

Check failure on line 390 in database/plugin/metadata/sqlite/transaction.go

View workflow job for this annotation

GitHub Actions / lint

G115: integer overflow conversion int64 -> uint64 (gosec)
if result.Error != nil {

Check failure on line 391 in database/plugin/metadata/sqlite/transaction.go

View workflow job for this annotation

GitHub Actions / lint

G115: integer overflow conversion int64 -> uint64 (gosec)
return result.Error
}
return nil
Expand Down
Loading