Skip to content

Commit 6f5b11b

Browse files
author
Jenita
committed
feat: track witness set for transaction
Signed-off-by: Jenita <jkawan@blinklabs.io>
1 parent dab5d82 commit 6f5b11b

File tree

8 files changed

+185
-19
lines changed

8 files changed

+185
-19
lines changed

database/models/models.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var MigrateModels = []any{
4040
&Redeemer{},
4141
&ResignCommitteeCold{},
4242
&Script{},
43+
&ScriptContent{},
4344
&StakeDelegation{},
4445
&StakeDeregistration{},
4546
&StakeRegistration{},

database/models/redeemer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type Redeemer struct {
2020
TransactionID uint `gorm:"index"`
2121
Tag uint8 `gorm:"index"` // Redeemer tag
2222
Index uint32 `gorm:"index"`
23-
Data []byte `gorm:"type:bytea"` // Plutus data
23+
Data []byte // Plutus data
2424
ExUnitsMemory uint64
2525
ExUnitsCPU uint64
2626
Transaction *Transaction

database/models/script.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,21 @@
1414

1515
package models
1616

17-
// Script represents a script entry in the witness set
17+
// Script represents a reference to a script in the witness set
1818
// Type corresponds to ScriptRefType constants from gouroboros/ledger/common:
1919
// 0=NativeScript (ScriptRefTypeNativeScript)
2020
// 1=PlutusV1 (ScriptRefTypePlutusV1)
2121
// 2=PlutusV2 (ScriptRefTypePlutusV2)
2222
// 3=PlutusV3 (ScriptRefTypePlutusV3)
23+
//
24+
// To avoid storing duplicate script data for the same script used in multiple
25+
// transactions, we store only the script hash here. The actual script content
26+
// is stored separately in ScriptContent table, indexed by hash.
2327
type Script struct {
2428
ID uint `gorm:"primaryKey"`
2529
TransactionID uint `gorm:"index"`
2630
Type uint8 `gorm:"index"` // Script type
27-
ScriptData []byte `gorm:"type:bytea"`
31+
ScriptHash []byte `gorm:"index"` // Hash of the script
2832
Transaction *Transaction
2933
}
3034

database/models/script_content.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2025 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package models
16+
17+
// ScriptContent represents the content of a script, indexed by its hash
18+
// This avoids storing duplicate script data when the same script appears
19+
// in multiple transactions
20+
type ScriptContent struct {
21+
ID uint `gorm:"primaryKey"`
22+
Hash []byte `gorm:"index;unique"` // Script hash
23+
Type uint8 `gorm:"index"` // Script type
24+
Content []byte // Script content
25+
CreatedSlot uint64 // Slot when this script was first seen
26+
}
27+
28+
func (ScriptContent) TableName() string {
29+
return "script_content"
30+
}

database/models/witness.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,24 @@
1414

1515
package models
1616

17+
const (
18+
// KeyWitnessTypeVkey represents a Vkey witness
19+
KeyWitnessTypeVkey uint8 = iota
20+
// KeyWitnessTypeBootstrap represents a Bootstrap witness
21+
KeyWitnessTypeBootstrap
22+
)
23+
1724
// KeyWitness represents a key witness entry (Vkey or Bootstrap)
18-
// Type: 0 = VkeyWitness, 1 = BootstrapWitness
25+
// Type: KeyWitnessTypeVkey = VkeyWitness, KeyWitnessTypeBootstrap = BootstrapWitness
1926
type KeyWitness struct {
2027
ID uint `gorm:"primaryKey"`
2128
TransactionID uint `gorm:"index"`
22-
Type uint8 `gorm:"index"` // 0=Vkey, 1=Bootstrap
23-
Vkey []byte `gorm:"type:bytea"`
24-
Signature []byte `gorm:"type:bytea"`
25-
PublicKey []byte `gorm:"type:bytea"` // For Bootstrap
26-
ChainCode []byte `gorm:"type:bytea"` // For Bootstrap
27-
Attributes []byte `gorm:"type:bytea"` // For Bootstrap
29+
Type uint8 `gorm:"index"` // See KeyWitnessType* constants
30+
Vkey []byte // Vkey witness key
31+
Signature []byte // Witness signature
32+
PublicKey []byte // For Bootstrap witness
33+
ChainCode []byte // For Bootstrap witness
34+
Attributes []byte // For Bootstrap witness
2835
Transaction *Transaction
2936
}
3037

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2025 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sqlite
16+
17+
import (
18+
"errors"
19+
20+
"github.com/blinklabs-io/dingo/database/models"
21+
lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
22+
"gorm.io/gorm"
23+
)
24+
25+
// GetScriptContent returns the script content by its hash
26+
func (d *MetadataStoreSqlite) GetScriptContent(
27+
hash lcommon.ScriptHash,
28+
txn *gorm.DB,
29+
) (*models.ScriptContent, error) {
30+
ret := &models.ScriptContent{}
31+
if txn == nil {
32+
txn = d.DB()
33+
}
34+
result := txn.First(ret, "hash = ?", hash[:])
35+
if result.Error != nil {
36+
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
37+
return nil, nil
38+
}
39+
return nil, result.Error
40+
}
41+
return ret, nil
42+
}
43+
44+
// GetScriptsByTransaction returns all scripts (references) used in a particular transaction
45+
func (d *MetadataStoreSqlite) GetScriptsByTransaction(
46+
txID uint,
47+
txn *gorm.DB,
48+
) ([]models.Script, error) {
49+
var scripts []models.Script
50+
if txn == nil {
51+
txn = d.DB()
52+
}
53+
result := txn.Where("transaction_id = ?", txID).Find(&scripts)
54+
if result.Error != nil {
55+
return nil, result.Error
56+
}
57+
return scripts, nil
58+
}

database/plugin/metadata/sqlite/transaction.go

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ func (d *MetadataStoreSqlite) SetTransaction(
8989
if result.Error != nil {
9090
return fmt.Errorf("create transaction: %w", result.Error)
9191
}
92-
// If ID is still zero (conflict path with SQLite), fetch it by hash
92+
// SQLite's ON CONFLICT clause doesn't return the ID of an existing row when
93+
// the conflict path is taken (no insert occurs). We need to fetch the ID
94+
// explicitly so we can associate witness records with the correct transaction.
9395
if tmpTx.ID == 0 {
9496
existingTx, err := d.GetTransactionByHash(txHash, txn)
9597
if err != nil {
@@ -304,14 +306,14 @@ func (d *MetadataStoreSqlite) SetTransaction(
304306
return fmt.Errorf("delete existing plutus data: %w", result.Error)
305307
}
306308
}
307-
if tx.Witnesses() != nil {
308-
ws := tx.Witnesses()
309+
ws := tx.Witnesses()
310+
if ws != nil {
309311

310312
// Add Vkey Witnesses
311313
for _, vkey := range ws.Vkey() {
312314
keyWitness := models.KeyWitness{
313315
TransactionID: tmpTx.ID,
314-
Type: 0, // VkeyWitness
316+
Type: models.KeyWitnessTypeVkey,
315317
Vkey: vkey.Vkey,
316318
Signature: vkey.Signature,
317319
}
@@ -324,7 +326,7 @@ func (d *MetadataStoreSqlite) SetTransaction(
324326
for _, bootstrap := range ws.Bootstrap() {
325327
keyWitness := models.KeyWitness{
326328
TransactionID: tmpTx.ID,
327-
Type: 1, // BootstrapWitness
329+
Type: models.KeyWitnessTypeBootstrap,
328330
PublicKey: bootstrap.PublicKey,
329331
Signature: bootstrap.Signature,
330332
ChainCode: bootstrap.ChainCode,
@@ -337,50 +339,106 @@ func (d *MetadataStoreSqlite) SetTransaction(
337339

338340
// Add Native Scripts
339341
for _, script := range ws.NativeScripts() {
342+
scriptHash := script.Hash()
340343
scriptRecord := models.Script{
341344
TransactionID: tmpTx.ID,
342345
Type: uint8(lcommon.ScriptRefTypeNativeScript),
343-
ScriptData: script.Cbor(),
346+
ScriptHash: scriptHash.Bytes(),
344347
}
345348
if result := txn.Create(&scriptRecord); result.Error != nil {
346349
return fmt.Errorf("create native script: %w", result.Error)
347350
}
351+
// Also store the script content separately to avoid duplicates
352+
scriptContent := models.ScriptContent{
353+
Hash: scriptHash.Bytes(),
354+
Type: uint8(lcommon.ScriptRefTypeNativeScript),
355+
Content: script.Cbor(),
356+
CreatedSlot: point.Slot,
357+
}
358+
if result := txn.Clauses(clause.OnConflict{
359+
Columns: []clause.Column{{Name: "hash"}},
360+
DoNothing: true,
361+
}).Create(&scriptContent); result.Error != nil {
362+
return fmt.Errorf("create native script content: %w", result.Error)
363+
}
348364
}
349365

350366
// Add PlutusV1 Scripts
351367
for _, script := range ws.PlutusV1Scripts() {
368+
scriptHash := script.Hash()
352369
scriptRecord := models.Script{
353370
TransactionID: tmpTx.ID,
354371
Type: uint8(lcommon.ScriptRefTypePlutusV1),
355-
ScriptData: script,
372+
ScriptHash: scriptHash.Bytes(),
356373
}
357374
if result := txn.Create(&scriptRecord); result.Error != nil {
358375
return fmt.Errorf("create plutus v1 script: %w", result.Error)
359376
}
377+
// Also store the script content separately to avoid duplicates
378+
scriptContent := models.ScriptContent{
379+
Hash: scriptHash.Bytes(),
380+
Type: uint8(lcommon.ScriptRefTypePlutusV1),
381+
Content: script.RawScriptBytes(),
382+
CreatedSlot: point.Slot,
383+
}
384+
if result := txn.Clauses(clause.OnConflict{
385+
Columns: []clause.Column{{Name: "hash"}},
386+
DoNothing: true,
387+
}).Create(&scriptContent); result.Error != nil {
388+
return fmt.Errorf("create plutus v1 script content: %w", result.Error)
389+
}
360390
}
361391

362392
// Add PlutusV2 Scripts
363393
for _, script := range ws.PlutusV2Scripts() {
394+
scriptHash := script.Hash()
364395
scriptRecord := models.Script{
365396
TransactionID: tmpTx.ID,
366397
Type: uint8(lcommon.ScriptRefTypePlutusV2),
367-
ScriptData: script,
398+
ScriptHash: scriptHash.Bytes(),
368399
}
369400
if result := txn.Create(&scriptRecord); result.Error != nil {
370401
return fmt.Errorf("create plutus v2 script: %w", result.Error)
371402
}
403+
// Also store the script content separately to avoid duplicates
404+
scriptContent := models.ScriptContent{
405+
Hash: scriptHash.Bytes(),
406+
Type: uint8(lcommon.ScriptRefTypePlutusV2),
407+
Content: script.RawScriptBytes(),
408+
CreatedSlot: point.Slot,
409+
}
410+
if result := txn.Clauses(clause.OnConflict{
411+
Columns: []clause.Column{{Name: "hash"}},
412+
DoNothing: true,
413+
}).Create(&scriptContent); result.Error != nil {
414+
return fmt.Errorf("create plutus v2 script content: %w", result.Error)
415+
}
372416
}
373417

374418
// Add PlutusV3 Scripts
375419
for _, script := range ws.PlutusV3Scripts() {
420+
scriptHash := script.Hash()
376421
scriptRecord := models.Script{
377422
TransactionID: tmpTx.ID,
378423
Type: uint8(lcommon.ScriptRefTypePlutusV3),
379-
ScriptData: script,
424+
ScriptHash: scriptHash.Bytes(),
380425
}
381426
if result := txn.Create(&scriptRecord); result.Error != nil {
382427
return fmt.Errorf("create plutus v3 script: %w", result.Error)
383428
}
429+
// Also store the script content separately to avoid duplicates
430+
scriptContent := models.ScriptContent{
431+
Hash: scriptHash.Bytes(),
432+
Type: uint8(lcommon.ScriptRefTypePlutusV3),
433+
Content: script.RawScriptBytes(),
434+
CreatedSlot: point.Slot,
435+
}
436+
if result := txn.Clauses(clause.OnConflict{
437+
Columns: []clause.Column{{Name: "hash"}},
438+
DoNothing: true,
439+
}).Create(&scriptContent); result.Error != nil {
440+
return fmt.Errorf("create plutus v3 script content: %w", result.Error)
441+
}
384442
}
385443

386444
// Add PlutusData (Datums)

database/plugin/metadata/store.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ type MetadataStore interface {
8383
[]byte, // hash
8484
*gorm.DB,
8585
) (*models.Transaction, error)
86+
GetScriptContent(
87+
lcommon.ScriptHash,
88+
*gorm.DB,
89+
) (*models.ScriptContent, error)
90+
GetScriptsByTransaction(
91+
uint, // txID
92+
*gorm.DB,
93+
) ([]models.Script, error)
8694

8795
SetBlockNonce(
8896
[]byte, // blockHash

0 commit comments

Comments
 (0)